vsm 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/settings.local.json +17 -0
  3. data/CLAUDE.md +134 -0
  4. data/README.md +675 -17
  5. data/Rakefile +1 -5
  6. data/examples/01_echo_tool.rb +51 -0
  7. data/examples/02_openai_streaming.rb +73 -0
  8. data/examples/02b_anthropic_streaming.rb +58 -0
  9. data/examples/02c_gemini_streaming.rb +60 -0
  10. data/examples/03_openai_tools.rb +106 -0
  11. data/examples/03b_anthropic_tools.rb +93 -0
  12. data/examples/03c_gemini_tools.rb +95 -0
  13. data/examples/05_mcp_server_and_chattty.rb +63 -0
  14. data/examples/06_mcp_mount_reflection.rb +45 -0
  15. data/examples/07_connect_claude_mcp.rb +78 -0
  16. data/examples/08_custom_chattty.rb +63 -0
  17. data/examples/09_mcp_with_llm_calls.rb +49 -0
  18. data/examples/10_meta_read_only.rb +56 -0
  19. data/exe/vsm +17 -0
  20. data/lib/vsm/async_channel.rb +44 -0
  21. data/lib/vsm/capsule.rb +46 -0
  22. data/lib/vsm/cli.rb +78 -0
  23. data/lib/vsm/drivers/anthropic/async_driver.rb +210 -0
  24. data/lib/vsm/drivers/family.rb +16 -0
  25. data/lib/vsm/drivers/gemini/async_driver.rb +149 -0
  26. data/lib/vsm/drivers/openai/async_driver.rb +202 -0
  27. data/lib/vsm/dsl.rb +80 -0
  28. data/lib/vsm/dsl_mcp.rb +36 -0
  29. data/lib/vsm/executors/fiber_executor.rb +10 -0
  30. data/lib/vsm/executors/thread_executor.rb +19 -0
  31. data/lib/vsm/generator/new_project.rb +154 -0
  32. data/lib/vsm/generator/templates/Gemfile.erb +9 -0
  33. data/lib/vsm/generator/templates/README_md.erb +40 -0
  34. data/lib/vsm/generator/templates/Rakefile.erb +5 -0
  35. data/lib/vsm/generator/templates/bin_console.erb +11 -0
  36. data/lib/vsm/generator/templates/bin_setup.erb +7 -0
  37. data/lib/vsm/generator/templates/exe_name.erb +34 -0
  38. data/lib/vsm/generator/templates/gemspec.erb +24 -0
  39. data/lib/vsm/generator/templates/gitignore.erb +10 -0
  40. data/lib/vsm/generator/templates/lib_name_rb.erb +9 -0
  41. data/lib/vsm/generator/templates/lib_organism_rb.erb +44 -0
  42. data/lib/vsm/generator/templates/lib_ports_chat_tty_rb.erb +12 -0
  43. data/lib/vsm/generator/templates/lib_tools_read_file_rb.erb +32 -0
  44. data/lib/vsm/generator/templates/lib_version_rb.erb +6 -0
  45. data/lib/vsm/homeostat.rb +19 -0
  46. data/lib/vsm/lens/event_hub.rb +73 -0
  47. data/lib/vsm/lens/server.rb +188 -0
  48. data/lib/vsm/lens/stats.rb +58 -0
  49. data/lib/vsm/lens/tui.rb +88 -0
  50. data/lib/vsm/lens.rb +79 -0
  51. data/lib/vsm/mcp/client.rb +80 -0
  52. data/lib/vsm/mcp/jsonrpc.rb +92 -0
  53. data/lib/vsm/mcp/remote_tool_capsule.rb +35 -0
  54. data/lib/vsm/message.rb +6 -0
  55. data/lib/vsm/meta/snapshot_builder.rb +121 -0
  56. data/lib/vsm/meta/snapshot_cache.rb +25 -0
  57. data/lib/vsm/meta/support.rb +35 -0
  58. data/lib/vsm/meta/tools.rb +498 -0
  59. data/lib/vsm/meta.rb +59 -0
  60. data/lib/vsm/observability/ledger.rb +25 -0
  61. data/lib/vsm/port.rb +11 -0
  62. data/lib/vsm/ports/chat_tty.rb +112 -0
  63. data/lib/vsm/ports/mcp/server_stdio.rb +101 -0
  64. data/lib/vsm/roles/coordination.rb +49 -0
  65. data/lib/vsm/roles/governance.rb +9 -0
  66. data/lib/vsm/roles/identity.rb +11 -0
  67. data/lib/vsm/roles/intelligence.rb +172 -0
  68. data/lib/vsm/roles/operations.rb +33 -0
  69. data/lib/vsm/runtime.rb +18 -0
  70. data/lib/vsm/tool/acts_as_tool.rb +20 -0
  71. data/lib/vsm/tool/capsule.rb +12 -0
  72. data/lib/vsm/tool/descriptor.rb +16 -0
  73. data/lib/vsm/version.rb +1 -1
  74. data/lib/vsm.rb +43 -0
  75. data/llms.txt +322 -0
  76. data/mcp_update.md +162 -0
  77. metadata +93 -31
  78. data/.rubocop.yml +0 -8
data/llms.txt ADDED
@@ -0,0 +1,322 @@
1
+
2
+ LLMS CONTEXT FILE — VSM (Viable Systems for Agents) GEM
3
+ =======================================================
4
+
5
+ Audience: Large Language Models (LLMs) acting as coding assistants and human contributors.
6
+ Goal: Provide all key context to safely modify, extend, and use the VSM gem as a framework for agentic CLIs (e.g., `airb`).
7
+
8
+ — TL;DR —
9
+ - VSM is a small, async, message-driven runtime for building recursive “capsules” that each contain five named systems:
10
+ Operations, Coordination, Intelligence, Governance, Identity. Monitoring (observability) is also provided.
11
+ - Tools are implemented as *capsules* that *opt-in* to a tool interface (`ActsAsTool`) with JSON Schema descriptors.
12
+ - The runtime is fiber-based (`async` gem). Tool execution can run in parallel via executors (`:fiber`, `:thread`, optional `:ractor`/`:subprocess` later).
13
+ - Intelligence integrates provider drivers (OpenAI, Anthropic, Gemini) that support *structured tool calls* and (for OpenAI/Anthropic) *streaming*.
14
+ - A built-in “Lens” web visualizer (SSE) streams live events from the bus; enable with one call.
15
+ - Use the DSL to assemble a top-level capsule (the “organism”) and optional sub-capsules (recursive sub-agents or tools).
16
+
17
+ --------------------------------------------------------------------------------
18
+ 1) REPO LAYOUT (EXPECTED FILES)
19
+ --------------------------------------------------------------------------------
20
+ lib/
21
+ vsm.rb # top-level requires
22
+ vsm/message.rb # Message struct
23
+ vsm/async_channel.rb # async bus (fibers)
24
+ vsm/homeostat.rb # budgets/alerts (minimal)
25
+ vsm/observability/ledger.rb # Monitoring role: JSONL event ledger
26
+ vsm/roles/
27
+ operations.rb # runs tools (capsules) via executors
28
+ coordination.rb # scheduling, floor control, turn end
29
+ intelligence.rb # abstract; apps subclass this
30
+ governance.rb # policy/budgets/confirmation hooks
31
+ identity.rb # invariants/escalation
32
+ vsm/tool/
33
+ descriptor.rb # name/description/schema → provider shapes
34
+ acts_as_tool.rb # mixin to mark capsules as tools
35
+ capsule.rb # base tool capsule (implements #run)
36
+ vsm/executors/
37
+ fiber_executor.rb # default (IO-bound)
38
+ thread_executor.rb # CPU-ish or blocking libs
39
+ # (optional) ractor_executor.rb / subprocess_executor.rb
40
+ vsm/capsule.rb # Capsule: wires systems, async run loop
41
+ vsm/dsl.rb # DSL for composing organisms & children
42
+ vsm/port.rb # adapter base (CLI/TUI/HTTP/etc.)
43
+ vsm/runtime.rb # boot helper: start capsule + ports
44
+ vsm/drivers/
45
+ family.rb # returns :openai | :anthropic | :gemini
46
+ openai/async_driver.rb # SSE streaming + tools
47
+ anthropic/async_driver.rb # SSE streaming + tool_use blocks
48
+ gemini/async_driver.rb # non-streaming MVP + functionCall
49
+ vsm/lens.rb # Lens.attach!(capsule, ...)
50
+ vsm/lens/event_hub.rb # ring buffer + fan-out
51
+ vsm/lens/server.rb # Rack app + SSE + tiny HTML
52
+
53
+ spec/ # RSpec tests (smoke + providers + routing)
54
+ examples/ # small runnable demos
55
+
56
+
57
+ --------------------------------------------------------------------------------
58
+ 2) DESIGN GOALS
59
+ --------------------------------------------------------------------------------
60
+ - Small surface, idiomatic Ruby (SOLID/POODR), high cohesion/low coupling.
61
+ - Recursion-by-default: a Capsule can contain child Capsules.
62
+ - First-class asynchrony: non-blocking I/O; parallel tool calls where safe.
63
+ - Provider-agnostic Intelligence with a stable, minimal event API:
64
+ :assistant_delta, :assistant_final, :tool_calls.
65
+ - Observability out-of-the-box: JSONL ledger + SSE Lens.
66
+ - Safety hooks live in Governance (path sandbox, confirmations, budgets).
67
+
68
+
69
+ --------------------------------------------------------------------------------
70
+ 3) MESSAGE MODEL
71
+ --------------------------------------------------------------------------------
72
+ Struct: VSM::Message.new(
73
+ kind:, # Symbol — :user, :assistant_delta, :assistant, :tool_call, :tool_result, :plan, :policy, :audit, :confirm_request, :confirm_response, :progress
74
+ payload:, # Object/String — event-specific data (small where possible)
75
+ path:, # Array<Symbol> — addressing (e.g., [:airb, :operations, :read_file])
76
+ corr_id:, # String — correlates tool_call <-> tool_result
77
+ meta: # Hash — session_id, tool name, budgets, severity, etc.
78
+ )
79
+
80
+ Guidance for LLMs:
81
+ - Always set meta.session_id for multi-turn sessions.
82
+ - When emitting tool events, fill meta.tool and corr_id for pairing.
83
+ - Keep payload compact; include previews for large data (full bodies may be written to disk separately if needed).
84
+
85
+
86
+ --------------------------------------------------------------------------------
87
+ 4) CAPSULE + SYSTEMS (THE “ORG” SPINE)
88
+ --------------------------------------------------------------------------------
89
+ Capsule contains:
90
+ - bus: AsyncChannel (fiber-friendly queue + subscribers)
91
+ - homeostat: budgets/alerts (minimal for MVP)
92
+ - roles: five named systems
93
+ - Operations: runs tools, dispatches to child tool-capsules
94
+ - Coordination: schedules messages; grants floor per session; turn end
95
+ - Intelligence: orchestrates LLM driver; streams deltas; emits tool_calls
96
+ - Governance: policy gates (sandboxing, confirmation, budgets)
97
+ - Identity: invariants, escalation to owner/user
98
+ - children: Hash of name → child capsule (often tool capsules)
99
+
100
+ Dispatch order (typical): Operations → Intelligence → Identity
101
+ (Operations consumes :tool_call; Intelligence consumes :user/:tool_result; Identity handles policy updates/broadcasts.)
102
+
103
+
104
+ --------------------------------------------------------------------------------
105
+ 5) ASYNCHRONY & PARALLELISM
106
+ --------------------------------------------------------------------------------
107
+ - The bus is fiber-based (`async` gem). Capsule.run loops on bus.pop and lets Coordination drain/schedule messages.
108
+ - Operations executes each tool_call concurrently via an Executor:
109
+ - :fiber (default) for IO-aware code
110
+ - :thread for brief CPU spikes or blocking C libs
111
+ - (:ractor/:subprocess) later for isolation or heavy CPU
112
+ - Use Async::Semaphore to cap per-tool concurrency if needed.
113
+ - Coordination.wait_for_turn_end(session_id) enables CLI ports to block until the assistant final is emitted for the turn.
114
+
115
+
116
+ --------------------------------------------------------------------------------
117
+ 6) TOOLS AS CAPSULES
118
+ --------------------------------------------------------------------------------
119
+ Implement a tool by subclassing VSM::ToolCapsule and including ActsAsTool:
120
+
121
+ class MyTool < VSM::ToolCapsule
122
+ tool_name "search_repo"
123
+ tool_description "Search codebase by regex"
124
+ tool_schema({
125
+ type: "object",
126
+ properties: { pattern: { type: "string" }, path: { type: "string" } },
127
+ required: ["pattern"]
128
+ })
129
+
130
+ def execution_mode = :thread # optional; defaults to :fiber
131
+ def run(args)
132
+ # return a String or small JSON-compatible object
133
+ end
134
+ end
135
+
136
+ Notes:
137
+ - The JSON Schema should be compact and valid for OpenAI/Anthropic/Gemini.
138
+ - Governance is injected into tool capsules (if they expose #governance=), allowing helpers like safe_path().
139
+ - Keep results small. For big outputs, summarize or write artifacts to files and return a reference.
140
+
141
+
142
+ --------------------------------------------------------------------------------
143
+ 7) INTELLIGENCE & PROVIDER DRIVERS
144
+ --------------------------------------------------------------------------------
145
+ - Intelligence subclasses handle per-session conversation history and call a driver’s run!(conversation:, tools:, policy:) which yields three events:
146
+ - [:assistant_delta, String] — stream partial text
147
+ - [:assistant_final, String] — final text for the turn (may be empty if only tool calls)
148
+ - [:tool_calls, Array<Hash>] — each { id:, name:, arguments: Hash }
149
+ - Conversation messages passed into drivers are hashes like:
150
+ { role: "system"|"user"|"assistant"|"tool", content: String, tool_call_id?: String }
151
+
152
+ Providers:
153
+ - OpenAI::DriverAsync — SSE streaming; tools in choices[].delta.tool_calls; pass tools via `[{type:"function", function:{name, description, parameters}}]`.
154
+ - Anthropic::DriverAsync — SSE streaming; `tool_use` content blocks with `input_json_delta` fragments; pass tools via `[{name, description, input_schema}]`; tool_result fed back as a user content block `{type:"tool_result", tool_use_id, content}`.
155
+ - Gemini::DriverAsync — non-streaming MVP; declare tools via `function_declarations`; receive `functionCall`; reply next turn with `functionResponse`.
156
+
157
+ Driver selection:
158
+ - VSM::Intelligence::DriverFamily.of(@driver) → :openai | :anthropic | :gemini.
159
+ - Build provider-specific tool arrays from VSM::Tool::Descriptor:
160
+ - to_openai_tool, to_anthropic_tool, to_gemini_tool.
161
+
162
+ Important rules for LLMs modifying Intelligence:
163
+ - Do not parse tool calls from free-form text. Always use structured tool-calling outputs from drivers.
164
+ - Maintain conversation faithfully; append assistant/tool messages exactly as providers expect.
165
+ - Always emit :assistant_delta before :assistant when streaming text.
166
+
167
+
168
+ --------------------------------------------------------------------------------
169
+ 8) GOVERNANCE & IDENTITY
170
+ --------------------------------------------------------------------------------
171
+ - Governance.enforce(message) wraps routing. Add sandboxing, diff previews, confirmations, budgets, and timeouts here.
172
+ - Emit :confirm_request when needed; the Port must collect a :confirm_response.
173
+ - Identity holds invariants (e.g., “stay in workspace”), escalates algedonic alerts (homeostat.alarm?).
174
+
175
+ LLM policy changes should emit a :policy message that Identity can broadcast to children if necessary.
176
+
177
+
178
+ --------------------------------------------------------------------------------
179
+ 9) PORTS (INTERFACES)
180
+ --------------------------------------------------------------------------------
181
+ - Base: VSM::Port with #ingress(event) and #render_out(message).
182
+ - ChatTTY port: reads stdin lines, emits :user with meta.session_id; renders :assistant_delta (stream) and :assistant (final), handles :confirm_request → :confirm_response.
183
+ - Other ports: CommandTTY (one-shot task), HTTP, WebSocket, MCP client/server ports (planned), TUI.
184
+
185
+ Guidance for LLMs:
186
+ - Keep ports thin. No policy or LLM logic in ports.
187
+ - Always pass session_id; grant floor in Coordination for deterministic streaming.
188
+
189
+
190
+ --------------------------------------------------------------------------------
191
+ 10) OBSERVABILITY (MONITORING + LENS)
192
+ --------------------------------------------------------------------------------
193
+ - Monitoring subscribes to the bus and appends JSONL to .vsm.log.jsonl.
194
+ - Lens is a tiny Rack app serving an SSE `/events` feed with a simple HTML viewer.
195
+ - Enable in apps:
196
+ VSM::Lens.attach!(capsule, host: "127.0.0.1", port: 9292, token: ENV["VSM_LENS_TOKEN"])
197
+
198
+ Lens best practices:
199
+ - Include meta.session_id, path, corr_id, and meta.tool on events.
200
+ - Keep payload small to avoid UI lag; the server already truncates strings.
201
+ - For multi-process swarms, add a RemotePublisher that forwards events to one hub (future).
202
+
203
+
204
+ --------------------------------------------------------------------------------
205
+ 11) DSL FOR ASSEMBLY
206
+ --------------------------------------------------------------------------------
207
+ Example organism:
208
+
209
+ org = VSM::DSL.define(:airb) do
210
+ identity klass: MyIdentity, args: { identity: "airb", invariants: ["stay in workspace"] }
211
+ governance klass: MyGovernance, args: { workspace_root: Dir.pwd }
212
+ coordination klass: VSM::Coordination
213
+ intelligence klass: MyIntelligence, args: { driver: my_driver }
214
+ monitoring klass: VSM::Monitoring
215
+
216
+ operations do
217
+ capsule :list_files, klass: Tools::ListFiles
218
+ capsule :read_file, klass: Tools::ReadFile
219
+ capsule :edit_file, klass: Tools::EditFile
220
+ # capsule :editor, klass: Capsules::Editor (full sub-agent capsule)
221
+ end
222
+ end
223
+
224
+ Start:
225
+ VSM::Runtime.start(org, ports: [ChatTTY.new(capsule: org)])
226
+
227
+
228
+ --------------------------------------------------------------------------------
229
+ 12) PROVIDER CONFIG (ENV VARS)
230
+ --------------------------------------------------------------------------------
231
+ - OPENAI_API_KEY, AIRB_MODEL (e.g., "gpt-4o-mini")
232
+ - ANTHROPIC_API_KEY, AIRB_MODEL (e.g., "claude-3-5-sonnet-latest")
233
+ - GEMINI_API_KEY, AIRB_MODEL (e.g., "gemini-2.0-flash-001")
234
+ - AIRB_PROVIDER = openai | anthropic | gemini
235
+ - VSM_LENS=1, VSM_LENS_PORT=9292, VSM_LENS_TOKEN=...
236
+
237
+
238
+ --------------------------------------------------------------------------------
239
+ 13) CODING STANDARDS (FOR LLM CHANGES)
240
+ --------------------------------------------------------------------------------
241
+ - Idiomatic Ruby, small objects, SRP. Keep classes under ~150 LOC when possible.
242
+ - Favor explicit dependencies via initializer args.
243
+ - Avoid global mutable state. If you add caches, use per-capsule fields.
244
+ - Don’t block fibers: for I/O use async-http; for CPU spikes switch to thread executor.
245
+ - Tests for every new adapter/driver parser with fixtures; route tests for message sequencing.
246
+ - Prefer incremental diffs (unified patches) with file paths and clear commit titles:
247
+ - Title: <module>: <short imperative> (e.g., "intelligence/openai: handle empty delta lines")
248
+ - Body: “Why”, “What changed”, “Tests”.
249
+
250
+
251
+ --------------------------------------------------------------------------------
252
+ 14) TESTING (MINIMUM BASELINE)
253
+ --------------------------------------------------------------------------------
254
+ - Routing smoke test: :tool_call → :tool_result → :assistant
255
+ - Provider parsing tests:
256
+ - OpenAI SSE fixture → emits deltas + final + tool_calls
257
+ - Anthropic SSE fixture with tool_use/input_json_delta → emits tool_calls + final
258
+ - Gemini functionCall fixture → emits tool_calls or final text
259
+ - Governance tests: sandbox rejects path traversal; confirm flow produces :confirm_request
260
+ - Concurrency tests: parallel tool calls produce paired results (different corr_id), no interleaved confusion in Coordination
261
+
262
+
263
+ --------------------------------------------------------------------------------
264
+ 15) EXTENDING THE FRAMEWORK
265
+ --------------------------------------------------------------------------------
266
+ A) Add a new tool capsule
267
+ - Create class <YourTool> < VSM::ToolCapsule
268
+ - Declare name/description/schema; implement #run; optional #execution_mode
269
+ - Register in operations DSL
270
+
271
+ B) Add a sub-agent capsule
272
+ - Provide its own Operations/Coordination/Intelligence/Governance/Identity (recursive)
273
+ - Optionally include ActsAsTool and expose itself as a parent tool (its #run orchestrates internal steps and returns a string)
274
+
275
+ C) Add a provider
276
+ - Place a new driver_* under lib/vsm/intelligence/<provider>/
277
+ - Yield the same three events (:assistant_delta, :assistant_final, :tool_calls)
278
+ - Add a descriptor conversion if provider needs a special tool shape
279
+ - Update DriverFamily.of to map the class → symbol
280
+
281
+ D) Add MCP support (future plan)
282
+ - Implement Ports::MCP::Server to expose tools via MCP spec
283
+ - Implement Ports::MCP::Client to consume external MCP tools and wrap as tool capsules
284
+
285
+
286
+ --------------------------------------------------------------------------------
287
+ 16) SAFETY & SECURITY
288
+ --------------------------------------------------------------------------------
289
+ - Never write outside the workspace. Use Governance.safe_path() in tools.
290
+ - Confirm risky writes with :confirm_request → :confirm_response.
291
+ - Add timeouts on tool calls and LLM calls (budget via Homeostat or Governance).
292
+ - Use semaphores to cap concurrency per tool to avoid resource exhaustion.
293
+ - Do not log secrets. Mask API keys and sensitive args before emitting events.
294
+
295
+
296
+ --------------------------------------------------------------------------------
297
+ 17) KNOWN LIMITATIONS / TODOs
298
+ --------------------------------------------------------------------------------
299
+ - Ractor/Subprocess executors are stubs in some scaffolds; implement when needed.
300
+ - Gemini streaming is not wired yet (MVP uses non-streaming). Add Live/stream endpoints later.
301
+ - Homeostat budgets are placeholders; implement counters and algedonic signals as needed.
302
+ - Lens has minimal UI; extract richer vsm-lens gem when features grow.
303
+
304
+
305
+ --------------------------------------------------------------------------------
306
+ 18) HOW TO ASK THIS LLM FOR CHANGES
307
+ --------------------------------------------------------------------------------
308
+ - Provide concrete goals and constraints (e.g., “Add a `search_repo` tool that scans *.rb files for a pattern; thread executor; unit tests; and show it in Lens with meta.tool”).
309
+ - Ask for *unified diffs* with exact file paths under lib/ and spec/. Keep patches minimal and focused.
310
+ - Require updates to README snippets if public API changes.
311
+ - Have it add/extend tests and run them locally (`bundle exec rspec`).
312
+ - If the change affects the message kinds or meta fields, ensure Lens/TUI still render sensibly.
313
+
314
+
315
+ --------------------------------------------------------------------------------
316
+ 19) LICENSE & ATTRIBUTION
317
+ --------------------------------------------------------------------------------
318
+ - MIT by default (edit gemspec if different). Respect third-party licenses for gems you add.
319
+ - Keep provider SDKs optional; current drivers use `async-http` + stdlib only.
320
+
321
+
322
+ End of llms.txt.
data/mcp_update.md ADDED
@@ -0,0 +1,162 @@
1
+ # MCP Integration Plan and Built‑In Ports
2
+
3
+ This document proposes minimal, practical support to:
4
+ - Expose any VSM capsule as an MCP server over stdio (JSON‑RPC) implementing `tools/list` and `tools/call`.
5
+ - Add two reusable ports to VSM: a generic, customizable Chat TTY port and an MCP stdio server port.
6
+ - Dynamically reflect tools from external MCP servers and wrap them as VSM tool capsules.
7
+
8
+ The design uses Ruby’s dynamic capabilities and integrates cleanly with existing VSM roles (Operations, Intelligence, Governance, etc.).
9
+
10
+ ## Scope (Phase 1)
11
+ - MCP methods: `tools/list`, `tools/call` only.
12
+ - Transport: JSON‑RPC over stdio (NDJSON framing to start; can evolve to LSP framing without API changes).
13
+ - No additional MCP features (Prompts/Resources/Logs) in this phase.
14
+
15
+ ## Components
16
+ - `VSM::Ports::ChatTTY` — Generic, customizable chat terminal port.
17
+ - `VSM::Ports::MCP::ServerStdio` — MCP server over stdio exposing capsule tools.
18
+ - `VSM::MCP::Client` — Thin stdio JSON‑RPC client for MCP reflection and calls.
19
+ - `VSM::MCP::RemoteToolCapsule` — Wraps a remote MCP tool as a local VSM `ToolCapsule`.
20
+ - `VSM::DSL::ChildrenBuilder#mcp_server` — Reflect and register remote tools with include/exclude/prefix controls.
21
+ - (Tiny core tweak) Inject `bus` into children that accept `bus=` to allow rich observability from wrappers.
22
+
23
+ ## Design Overview
24
+ - Client reflection: spawn an MCP server process, call `tools/list`, build `RemoteToolCapsule`s per tool, and add them as children. Include/exclude/prefix options shape the local tool namespace.
25
+ - Server exposure: reflect local tools via `tools/list`; on `tools/call`, emit a normal VSM `:tool_call` and await matching `:tool_result` (corr_id = JSON‑RPC id) to reply.
26
+ - Operations routing: unchanged for Phase 1. Reflected tools register as regular children; prefixing avoids collisions. (Optional namespacing router can be added later.)
27
+ - Observability: all bridges emit events into the bus so Lens shows clear lanes (client: `[:mcp, :client, server, tool]`; server: `[:mcp, :server, tool]`).
28
+ - Coexistence: ChatTTY targets the user’s real TTY (e.g., `IO.console`) or stderr; MCP server uses stdio exclusively. They can run together without interfering.
29
+
30
+ ## File Layout (proposed)
31
+ - `lib/vsm/ports/chat_tty.rb`
32
+ - `lib/vsm/ports/mcp/server_stdio.rb`
33
+ - `lib/vsm/mcp/jsonrpc.rb` (shared stdio JSON‑RPC util)
34
+ - `lib/vsm/mcp/client.rb`
35
+ - `lib/vsm/mcp/remote_tool_capsule.rb`
36
+ - `lib/vsm/dsl_mcp.rb` (adds `mcp_server` to the DSL ChildrenBuilder)
37
+ - (Core) optional `bus` injection in `lib/vsm/capsule.rb`
38
+
39
+ ## APIs and Usage
40
+
41
+ ### Expose a Capsule via CLI and MCP (simultaneously)
42
+ ```ruby
43
+ require "vsm"
44
+ require "vsm/ports/chat_tty"
45
+ require "vsm/ports/mcp/server_stdio"
46
+
47
+ cap = VSM::DSL.define(:demo) do
48
+ identity klass: VSM::Identity, args: { identity: "demo", invariants: [] }
49
+ governance klass: VSM::Governance
50
+ coordination klass: VSM::Coordination
51
+ intelligence klass: VSM::Intelligence, args: { driver: VSM::Drivers::OpenAI::AsyncDriver.new(api_key: ENV["OPENAI_API_KEY"], model: "gpt-4o-mini") }
52
+ monitoring klass: VSM::Monitoring
53
+ operations do
54
+ # local tools …
55
+ end
56
+ end
57
+
58
+ ports = [
59
+ VSM::Ports::MCP::ServerStdio.new(capsule: cap), # machine IO (stdio)
60
+ VSM::Ports::ChatTTY.new(capsule: cap) # human IO (TTY/console)
61
+ ]
62
+ VSM::Runtime.start(cap, ports: ports)
63
+ ```
64
+
65
+ ### Mount a Remote MCP Server (dynamic reflection)
66
+ ```ruby
67
+ require "vsm"
68
+ require "vsm/dsl_mcp"
69
+
70
+ cap = VSM::DSL.define(:with_remote) do
71
+ identity klass: VSM::Identity, args: { identity: "with_remote", invariants: [] }
72
+ governance klass: VSM::Governance
73
+ coordination klass: VSM::Coordination
74
+ intelligence klass: VSM::Intelligence, args: { driver: VSM::Drivers::Anthropic::AsyncDriver.new(api_key: ENV["ANTHROPIC_API_KEY"], model: "claude-sonnet-4.0") }
75
+ monitoring klass: VSM::Monitoring
76
+ operations do
77
+ mcp_server :smith, cmd: "smith-server --stdio", include: %w[search read], prefix: "smith_", env: { "SMITH_TOKEN" => ENV["SMITH_TOKEN"] }
78
+ end
79
+ end
80
+
81
+ VSM::Runtime.start(cap, ports: [VSM::Ports::ChatTTY.new(capsule: cap)])
82
+ ```
83
+
84
+ ### Filter Tools Offered to the Model (optional)
85
+ ```ruby
86
+ class GuardedIntel < VSM::Intelligence
87
+ def offer_tools?(sid, descriptor)
88
+ descriptor.name.start_with?("smith_") # only offer smith_* tools
89
+ end
90
+ end
91
+ ```
92
+
93
+ ## Customization (ChatTTY)
94
+ - Constructor options: `input:`, `output:`, `banner:`, `prompt:`, `theme:`.
95
+ - Defaults: reads/writes to `IO.console` if available; otherwise reads are disabled and output goes to a safe stream (stderr/console). Never interferes with MCP stdio.
96
+ - Override points: subclass and override `banner(io)` and/or `render_out(message)` while reusing the main input loop.
97
+
98
+ Example (options only):
99
+ ```ruby
100
+ tty = VSM::Ports::ChatTTY.new(
101
+ capsule: cap,
102
+ banner: ->(io) { io.puts "\e[96mMy App\e[0m — welcome!" },
103
+ prompt: "Me> "
104
+ )
105
+ ```
106
+
107
+ Example (subclass):
108
+ ```ruby
109
+ class FancyTTY < VSM::Ports::ChatTTY
110
+ def banner(io)
111
+ io.puts "\e[95m\n ███ MY APP ███\n\e[0m"
112
+ end
113
+ def render_out(m)
114
+ super
115
+ @out.puts("\e[92m✓ #{m.payload.to_s.slice(0,200)}\e[0m") if m.kind == :tool_result
116
+ end
117
+ end
118
+ ```
119
+
120
+ ## Coexistence and IO Routing
121
+ - MCP stdio server: reads `$stdin`, writes `$stdout` with strict JSON (one message per line). No TTY assumptions.
122
+ - ChatTTY: prefers `IO.console` for both input and output; falls back to `$stderr` for output and disables input if no TTY is present.
123
+ - Result: Both ports can run in the same process without corrupting MCP stdio.
124
+
125
+ ## Observability
126
+ - Client wrapper emits `:progress`/`:audit` with `path: [:mcp, :client, server, tool]` around calls.
127
+ - Server port emits `:audit` and wraps `tools/call` into standard `:tool_call`/`:tool_result` with `corr_id` mirrored to JSON‑RPC id.
128
+ - Lens will show clear lanes and full payloads, subject to Governance redaction (if any).
129
+
130
+ ## Governance and Operations
131
+ - Operations: unchanged; executes capsules for `:tool_call` and emits `:tool_result`.
132
+ - Governance: gate by name/prefix/regex; apply timeouts/rate limits/confirmations; redact args/results in Lens if needed.
133
+ - Execution mode: remote wrappers default to `:thread` to avoid blocking the reactor on stdio I/O.
134
+
135
+ ## Configuration and Authentication
136
+ - Default via ENV (e.g., tokens/keys). Per‑mount overrides available through `mcp_server env: { … }`.
137
+ - CLI flags can be introduced later in a helper script if needed.
138
+
139
+ ## Backward Compatibility
140
+ - No changes to `airb`. `VSM::Ports::ChatTTY` is a reusable, minimal alternative for new apps.
141
+
142
+ ## Future Extensions (not in Phase 1)
143
+ - Namespaced mounts (`smith.search`) with a tiny router enhancement in Operations.
144
+ - Code generation flow (`vsm mcp import …`) to create durable wrappers.
145
+ - Additional MCP features (prompts/resources/logs) and WebSocket transport.
146
+ - Web interaction port: HTTP chat with customizable UI surfaces.
147
+
148
+ ## Milestones
149
+ 1) Implement ports and client/wrapper (files above), plus optional `bus` injection.
150
+ 2) Add small README/usage and example snippet in `examples/`.
151
+ 3) Manual tests:
152
+ - Start capsule with both ChatTTY and MCP ports; verify no IO collision.
153
+ - Reflect a known MCP server; verify tool listing and calls.
154
+ - Lens shows client/server lanes with corr_id continuity.
155
+ 4) Optional: DSL include/exclude/prefix validation and guardrails.
156
+
157
+ ## Acceptance Criteria
158
+ - Starting a capsule with `VSM::Ports::MCP::ServerStdio` exposes working `tools/list` and `tools/call` on stdio.
159
+ - Starting a capsule with `VSM::Ports::ChatTTY` provides a working chat loop; banner/prompt are overridable without re‑implementing the loop.
160
+ - Running both ports concurrently does not corrupt MCP stdio.
161
+ - Reflecting a remote MCP server via `mcp_server` registers local tool capsules that work with `Intelligence` tool‑calling.
162
+ - Lens displays meaningful events for both client and server paths.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vsm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Scott Werner
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '0.90'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rack
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.2'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.2'
40
54
  - !ruby/object:Gem::Dependency
41
55
  name: rspec
42
56
  requirement: !ruby/object:Gem::Requirement
@@ -52,60 +66,108 @@ dependencies:
52
66
  - !ruby/object:Gem::Version
53
67
  version: '3.13'
54
68
  - !ruby/object:Gem::Dependency
55
- name: rubocop
69
+ name: async-rspec
56
70
  requirement: !ruby/object:Gem::Requirement
57
71
  requirements:
58
72
  - - "~>"
59
73
  - !ruby/object:Gem::Version
60
- version: '1.79'
74
+ version: '1.17'
61
75
  type: :development
62
76
  prerelease: false
63
77
  version_requirements: !ruby/object:Gem::Requirement
64
78
  requirements:
65
79
  - - "~>"
66
80
  - !ruby/object:Gem::Version
67
- version: '1.79'
81
+ version: '1.17'
68
82
  description: |
69
83
  VSM is a small Ruby framework for building agentic systems using a
70
84
  Viable System Model–style architecture. It gives you Capsules: self‑contained components
71
85
  composed of five named systems (Operations, Coordination, Intelligence, Governance,
72
86
  Identity) plus an async runtime so many capsules can run concurrently.
73
-
74
- Highlights
75
- • Capsules & recursion: compose larger organisms from smaller capsules, each with its
76
- own Operations/Coordination/Intelligence/Governance/Identity.
77
- • Async runtime: fiber‑based (async gem) message bus and scheduler with “floor
78
- control” per session; great for streaming output and multi‑turn tool loops.
79
- • Tools as capsules: implement tools as first‑class capsules with JSON‑Schema
80
- descriptors, ready to expose to OpenAI‑compatible function calling; adapters for
81
- other providers (e.g., Anthropic/Gemini) fit the same interface.
82
- • Parallel execution: pluggable executors (fiber/thread today) so tool calls can run
83
- concurrently; swap in ractors/subprocess isolation later without API changes.
84
- • Observability: append‑only JSONL event ledger; simple hooks to add tracing/metrics
85
- so you can build a live “lens” UI or ship logs to your infra.
86
- • Ports (ingress/egress): clean adapters for TTY/HTTP/MCP/etc. so multiple
87
- interaction models can share the same organism.
88
-
89
- Use cases
90
- • CLI chat agents with streaming and native tool calling
91
- • Editor/CI sub‑agents (planner, tester, refactorer) composed as capsules
92
- • MCP tool hosts/clients and other integrator bots
93
-
94
- Status: early but practical. APIs may evolve as we add more provider drivers and
95
- ports, but the core Capsule/Systems abstractions should be stable.
96
87
  email:
97
88
  - scott@sublayer.com
98
- executables: []
89
+ executables:
90
+ - vsm
99
91
  extensions: []
100
92
  extra_rdoc_files: []
101
93
  files:
94
+ - ".claude/settings.local.json"
102
95
  - ".rspec"
103
- - ".rubocop.yml"
96
+ - CLAUDE.md
104
97
  - LICENSE.txt
105
98
  - README.md
106
99
  - Rakefile
100
+ - examples/01_echo_tool.rb
101
+ - examples/02_openai_streaming.rb
102
+ - examples/02b_anthropic_streaming.rb
103
+ - examples/02c_gemini_streaming.rb
104
+ - examples/03_openai_tools.rb
105
+ - examples/03b_anthropic_tools.rb
106
+ - examples/03c_gemini_tools.rb
107
+ - examples/05_mcp_server_and_chattty.rb
108
+ - examples/06_mcp_mount_reflection.rb
109
+ - examples/07_connect_claude_mcp.rb
110
+ - examples/08_custom_chattty.rb
111
+ - examples/09_mcp_with_llm_calls.rb
112
+ - examples/10_meta_read_only.rb
113
+ - exe/vsm
107
114
  - lib/vsm.rb
115
+ - lib/vsm/async_channel.rb
116
+ - lib/vsm/capsule.rb
117
+ - lib/vsm/cli.rb
118
+ - lib/vsm/drivers/anthropic/async_driver.rb
119
+ - lib/vsm/drivers/family.rb
120
+ - lib/vsm/drivers/gemini/async_driver.rb
121
+ - lib/vsm/drivers/openai/async_driver.rb
122
+ - lib/vsm/dsl.rb
123
+ - lib/vsm/dsl_mcp.rb
124
+ - lib/vsm/executors/fiber_executor.rb
125
+ - lib/vsm/executors/thread_executor.rb
126
+ - lib/vsm/generator/new_project.rb
127
+ - lib/vsm/generator/templates/Gemfile.erb
128
+ - lib/vsm/generator/templates/README_md.erb
129
+ - lib/vsm/generator/templates/Rakefile.erb
130
+ - lib/vsm/generator/templates/bin_console.erb
131
+ - lib/vsm/generator/templates/bin_setup.erb
132
+ - lib/vsm/generator/templates/exe_name.erb
133
+ - lib/vsm/generator/templates/gemspec.erb
134
+ - lib/vsm/generator/templates/gitignore.erb
135
+ - lib/vsm/generator/templates/lib_name_rb.erb
136
+ - lib/vsm/generator/templates/lib_organism_rb.erb
137
+ - lib/vsm/generator/templates/lib_ports_chat_tty_rb.erb
138
+ - lib/vsm/generator/templates/lib_tools_read_file_rb.erb
139
+ - lib/vsm/generator/templates/lib_version_rb.erb
140
+ - lib/vsm/homeostat.rb
141
+ - lib/vsm/lens.rb
142
+ - lib/vsm/lens/event_hub.rb
143
+ - lib/vsm/lens/server.rb
144
+ - lib/vsm/lens/stats.rb
145
+ - lib/vsm/lens/tui.rb
146
+ - lib/vsm/mcp/client.rb
147
+ - lib/vsm/mcp/jsonrpc.rb
148
+ - lib/vsm/mcp/remote_tool_capsule.rb
149
+ - lib/vsm/message.rb
150
+ - lib/vsm/meta.rb
151
+ - lib/vsm/meta/snapshot_builder.rb
152
+ - lib/vsm/meta/snapshot_cache.rb
153
+ - lib/vsm/meta/support.rb
154
+ - lib/vsm/meta/tools.rb
155
+ - lib/vsm/observability/ledger.rb
156
+ - lib/vsm/port.rb
157
+ - lib/vsm/ports/chat_tty.rb
158
+ - lib/vsm/ports/mcp/server_stdio.rb
159
+ - lib/vsm/roles/coordination.rb
160
+ - lib/vsm/roles/governance.rb
161
+ - lib/vsm/roles/identity.rb
162
+ - lib/vsm/roles/intelligence.rb
163
+ - lib/vsm/roles/operations.rb
164
+ - lib/vsm/runtime.rb
165
+ - lib/vsm/tool/acts_as_tool.rb
166
+ - lib/vsm/tool/capsule.rb
167
+ - lib/vsm/tool/descriptor.rb
108
168
  - lib/vsm/version.rb
169
+ - llms.txt
170
+ - mcp_update.md
109
171
  - sig/vsm.rbs
110
172
  homepage: https://github.com/sublayerapp/vsm
111
173
  licenses:
@@ -120,14 +182,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
120
182
  requirements:
121
183
  - - ">="
122
184
  - !ruby/object:Gem::Version
123
- version: '3.2'
185
+ version: '3.4'
124
186
  required_rubygems_version: !ruby/object:Gem::Requirement
125
187
  requirements:
126
188
  - - ">="
127
189
  - !ruby/object:Gem::Version
128
190
  version: '0'
129
191
  requirements: []
130
- rubygems_version: 3.6.9
192
+ rubygems_version: 3.7.2
131
193
  specification_version: 4
132
194
  summary: 'Async, recursive agent framework for Ruby (Viable System Model): capsules,
133
195
  tools-as-capsules, streaming tool calls, and observability.'
data/.rubocop.yml DELETED
@@ -1,8 +0,0 @@
1
- AllCops:
2
- TargetRubyVersion: 3.1
3
-
4
- Style/StringLiterals:
5
- EnforcedStyle: double_quotes
6
-
7
- Style/StringLiteralsInInterpolation:
8
- EnforcedStyle: double_quotes