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/README.md CHANGED
@@ -1,39 +1,697 @@
1
- # Vsm
1
+ # VSM — Viable Systems for Ruby Agents
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
3
+ [![Conforms to README.lint](https://img.shields.io/badge/README.lint-conforming-brightgreen)](https://github.com/discoveryworks/readme-dot-lint)
4
4
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/vsm`. To experiment with that code, run `bin/console` for an interactive prompt.
5
+ VSM is a tiny, idiomatic Ruby runtime for building agentic systems with a clear spine: Operations, Coordination, Intelligence, Governance, and Identity.
6
6
 
7
- ## Installation
7
+ 🌸 Why use VSM?
8
+ =============================
8
9
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
10
+ Building agentic systems often leads to tangled callback spaghetti and unclear responsibilities. As you add tools, LLM providers, and coordination logic, the complexity explodes. You end up with:
10
11
 
11
- Install the gem and add to the application's Gemfile by executing:
12
+ - Callbacks nested in callbacks with no clear flow
13
+ - Tool execution mixed with business logic
14
+ - No clear separation between "what the agent does" vs "how it decides" vs "what rules it follows"
15
+ - Difficulty testing individual components
16
+ - Lock-in to specific LLM providers or frameworks
17
+
18
+ VSM solves this by providing a composable, testable architecture with **named responsibilities** (POODR/SOLID style). You get clear separation of concerns from day one, and can start with a single capsule and grow to a swarm—without changing your interface or core loop.
19
+
20
+ The Viable System Model gives you a proven organizational pattern: every autonomous system needs Operations (doing), Coordination (scheduling), Intelligence (deciding), Governance (rules), and Identity (purpose). VSM makes this concrete in Ruby.
21
+
22
+ 🌸🌸 Who benefits from VSM?
23
+ =============================
24
+
25
+ **Ruby developers building AI agents** who want clean architecture over framework magic. If you've read Sandi Metz's POODR, appreciate small objects with single responsibilities, and want your agent code to be as clean as your Rails models, VSM is for you.
26
+
27
+ **Teams scaling from prototype to production** who need to start simple (one tool, one LLM call) but know they'll need multiple tools, streaming, confirmations, and policy enforcement later. VSM's recursive capsule design means your "hello world" agent uses the same architecture as your production swarm.
28
+
29
+ **Developers who want provider independence**. VSM doesn't lock you into OpenAI, Anthropic, or any specific provider. Your Intelligence component decides how to plan—whether that's calling an LLM, following a state machine, or using your own logic.
30
+
31
+ 🌸🌸🌸 What exactly is VSM?
32
+ =============================
33
+
34
+ VSM is a Ruby gem that provides:
35
+
36
+ 1. **Five named systems** that every agent needs:
37
+ - **Operations** — do the work (tools/skills)
38
+ - **Coordination** — schedule, order, and arbitrate conversations (the "floor")
39
+ - **Intelligence** — plan/decide (e.g., call an LLM driver, or your own logic)
40
+ - **Governance** — enforce policy, safety, and budgets
41
+ - **Identity** — define purpose and invariants
42
+
43
+ 2. **Capsules** — recursive building blocks. Every capsule has the five systems above plus a message bus. Capsules can contain child capsules, and "tools" are just capsules that opt-in to a tool interface.
44
+
45
+ 3. **Async-first architecture** — powered by the `async` gem, VSM runs streaming, I/O, and multiple tool calls concurrently without blocking.
46
+
47
+ 4. **Clean interfaces** — Ports translate external events (CLI, HTTP, MCP) into messages. Tools expose JSON Schema descriptors that work with any LLM provider.
48
+
49
+ 5. **Built-in observability** — append-only JSONL ledger of all events, ready to feed into a monitoring UI.
50
+
51
+ 🌸🌸🌸🌸 How do I use VSM?
52
+ =============================
53
+
54
+ ## Install
55
+
56
+ ```ruby
57
+ # Gemfile
58
+ gem "vsm", "~> 0.0.1"
59
+ ```
12
60
 
13
61
  ```bash
14
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
62
+ bundle install
15
63
  ```
16
64
 
17
- If bundler is not being used to manage dependencies, install the gem by executing:
65
+ Ruby 3.2+ recommended.
66
+
67
+ ## Quick Example
68
+
69
+ Here's a minimal agent with one tool:
70
+
71
+ ```ruby
72
+ require "securerandom"
73
+ require "vsm"
74
+
75
+ # 1) Define a tool as a capsule
76
+ class EchoTool < VSM::ToolCapsule
77
+ tool_name "echo"
78
+ tool_description "Echoes a message"
79
+ tool_schema({
80
+ type: "object",
81
+ properties: { text: { type: "string" } },
82
+ required: ["text"]
83
+ })
18
84
 
85
+ def run(args)
86
+ "you said: #{args["text"]}"
87
+ end
88
+ end
89
+
90
+ # 2) Define your Intelligence (decides what to do)
91
+ class DemoIntelligence < VSM::Intelligence
92
+ def handle(message, bus:, **)
93
+ return false unless message.kind == :user
94
+
95
+ if message.payload =~ /\Aecho:\s*(.+)\z/
96
+ # User said "echo: something" - call the tool
97
+ bus.emit VSM::Message.new(
98
+ kind: :tool_call,
99
+ payload: { tool: "echo", args: { "text" => $1 } },
100
+ corr_id: SecureRandom.uuid,
101
+ meta: message.meta
102
+ )
103
+ else
104
+ # Just respond
105
+ bus.emit VSM::Message.new(
106
+ kind: :assistant,
107
+ payload: "Try: echo: hello",
108
+ meta: message.meta
109
+ )
110
+ end
111
+ true
112
+ end
113
+ end
114
+
115
+ # 3) Build your agent using the DSL
116
+ capsule = VSM::DSL.define(:demo) do
117
+ identity klass: VSM::Identity, args: { identity: "demo", invariants: [] }
118
+ governance klass: VSM::Governance
119
+ coordination klass: VSM::Coordination
120
+ intelligence klass: DemoIntelligence
121
+ operations do
122
+ capsule :echo, klass: EchoTool
123
+ end
124
+ end
125
+
126
+ # 4) Add a simple CLI interface
127
+ class StdinPort < VSM::Port
128
+ def loop
129
+ session = SecureRandom.uuid
130
+ print "You: "
131
+ while (line = $stdin.gets&.chomp)
132
+ @capsule.bus.emit VSM::Message.new(
133
+ kind: :user,
134
+ payload: line,
135
+ meta: { session_id: session }
136
+ )
137
+ @capsule.roles[:coordination].wait_for_turn_end(session)
138
+ print "You: "
139
+ end
140
+ end
141
+
142
+ def render_out(msg)
143
+ case msg.kind
144
+ when :assistant
145
+ puts "\nBot: #{msg.payload}"
146
+ when :tool_result
147
+ puts "\nTool> #{msg.payload}"
148
+ @capsule.bus.emit VSM::Message.new(
149
+ kind: :assistant,
150
+ payload: "(done)",
151
+ meta: msg.meta
152
+ )
153
+ end
154
+ end
155
+ end
156
+
157
+ # 5) Start the runtime
158
+ VSM::Runtime.start(capsule, ports: [StdinPort.new(capsule:)])
159
+ ```
160
+
161
+ Run it:
19
162
  ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
163
+ ruby quickstart.rb
164
+ # You: echo: hello
165
+ # Tool> you said: hello
21
166
  ```
22
167
 
23
- ## Usage
168
+ ## Project Generator (CLI)
24
169
 
25
- TODO: Write usage instructions here
170
+ Scaffold a new VSM app with a ChatTTY interface:
26
171
 
27
- ## Development
172
+ ```bash
173
+ gem install vsm # or build/install locally
174
+ vsm new my_agent
175
+ cd my_agent
176
+ bundle install
177
+ bundle exec exe/my-agent
178
+ ```
28
179
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
180
+ Options:
181
+ - `--with-llm openai|anthropic|gemini` — choose LLM provider (default: openai)
182
+ - `--model <name>` — default model
183
+ - `--git` — initialize git and commit
184
+ - `--bundle` — run `bundle install`
185
+ - `--path <dir>` — target directory (default: `./<name>`)
186
+ - `--force` — overwrite an existing non-empty directory
30
187
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
188
+ Generated layout mirrors the `airb` example: an `Organism.build` to assemble the capsule, a default `ChatTTY` port, and a sample `echo` tool ready to extend.
32
189
 
33
- ## Contributing
190
+ ## Building a Real Agent
191
+
192
+ For a real agent with LLM integration:
193
+
194
+ ```ruby
195
+ capsule = VSM::DSL.define(:my_agent) do
196
+ identity klass: VSM::Identity,
197
+ args: { identity: "my_agent", invariants: ["stay in workspace"] }
198
+ governance klass: VSM::Governance
199
+ coordination klass: VSM::Coordination
200
+ intelligence klass: MyLLMIntelligence # Your class that calls OpenAI/Anthropic/etc
201
+ monitoring klass: VSM::Monitoring # Optional: writes JSONL event log
202
+
203
+ operations do
204
+ capsule :list_files, klass: ListFilesTool
205
+ capsule :read_file, klass: ReadFileTool
206
+ capsule :write_file, klass: WriteFileTool
207
+ end
208
+ end
209
+ ```
210
+
211
+ Your `MyLLMIntelligence` would:
212
+ 1. Maintain conversation history
213
+ 2. Call your LLM provider with available tools
214
+ 3. Emit `:tool_call` messages when the LLM wants to use tools
215
+ 4. Stream `:assistant_delta` tokens as they arrive
216
+ 5. Emit final `:assistant` message when done
217
+
218
+ 🌸🌸🌸🌸🌸 Extras
219
+ =============================
220
+
221
+ ## Table of Contents
222
+
223
+ - [Features](#features)
224
+ - [Core Concepts](#core-concepts)
225
+ - [Tools as Capsules](#tools-as-capsules)
226
+ - [Async & Parallelism](#async--parallelism)
227
+ - [Ports (Interfaces)](#ports-interfaces)
228
+ - [Observability](#observability)
229
+ - [Writing an Intelligence](#writing-an-intelligence)
230
+ - [Testing](#testing)
231
+ - [Design Goals](#design-goals)
232
+ - [Roadmap](#roadmap)
233
+ - [FAQ](#faq)
234
+ - [API Overview](#api-overview)
235
+ - [License](#license)
236
+ - [Contributing](#contributing)
237
+
238
+ ## Features
239
+
240
+ - **Named systems**: Operations, Coordination, Intelligence, Governance, Identity
241
+ - **Capsules**: recursive building blocks (a capsule can contain more capsules)
242
+ - **Async bus**: non‑blocking message channel with fan‑out subscribers
243
+ - **Structured concurrency**: streaming + multiple tool calls in parallel
244
+ - **Tools-as-capsules**: opt‑in tool interface + JSON Schema descriptors
245
+ - **Executors**: run tools in the current fiber or a thread pool (Ractor/Subprocess future)
246
+ - **Ports**: clean ingress/egress adapters for CLI/TUI/HTTP/MCP/etc.
247
+ - **Observability**: append‑only JSONL ledger you can feed into a UI later
248
+ - **POODR/SOLID**: small objects, high cohesion, low coupling
249
+
250
+ ## Meta Tools
251
+
252
+ VSM includes a set of read‑only meta tools you can attach to any capsule to inspect its structure and code:
253
+
254
+ - `meta_summarize_self` — Summarize the current capsule including roles and tools
255
+ - `meta_list_tools` — List all tools available in the organism (descriptors and paths)
256
+ - `meta_explain_tool` — Show code and context for a specific tool
257
+ - `meta_explain_role` — Explain a role implementation for a capsule, with source snippets
258
+
259
+ Attach them when building your capsule:
260
+
261
+ ```ruby
262
+ capsule = VSM::DSL.define(:my_agent) do
263
+ identity klass: VSM::Identity, args: { identity: "my_agent" }
264
+ governance klass: VSM::Governance
265
+ coordination klass: VSM::Coordination
266
+ intelligence klass: VSM::Intelligence
267
+ monitoring klass: VSM::Monitoring
268
+ operations do
269
+ meta_tools # registers the four meta tools above on this capsule
270
+ end
271
+ end
272
+ ```
273
+
274
+ Example calls:
275
+
276
+ - `meta_summarize_self {}` → high‑level snapshot and counts
277
+ - `meta_list_tools {}` → array of tools with descriptors
278
+ - `meta_explain_tool { "tool": "some_tool" }` → code snippet + descriptor
279
+ - `meta_explain_role { "role": "coordination" }` → role class, constructor args, source locations, and code blocks
280
+
281
+ ## Core Concepts
282
+
283
+ ### Capsule
284
+
285
+ A container with five named systems and a message bus:
286
+
287
+ ```
288
+ Capsule(:name)
289
+ ├─ Identity (purpose & invariants)
290
+ ├─ Governance (safety & budgets)
291
+ ├─ Coordination (scheduling & "floor")
292
+ ├─ Intelligence (planning/deciding)
293
+ ├─ Operations (tools/skills)
294
+ └─ Monitoring (event ledger; optional)
295
+ ```
296
+
297
+ Capsules can contain child capsules. Recursion means a "tool" can itself be a full agent if you want.
298
+
299
+ ### Message
300
+
301
+ ```ruby
302
+ VSM::Message.new(
303
+ kind: :user | :assistant | :assistant_delta | :tool_call | :tool_result | :plan | :policy | :audit | :confirm_request | :confirm_response,
304
+ payload: "any",
305
+ path: [:airb, :operations, :fs], # optional addressing
306
+ corr_id: "uuid", # correlate tool_call ↔ tool_result
307
+ meta: { session_id: "uuid", ... } # extra context
308
+ )
309
+ ```
310
+
311
+ ### AsyncChannel
312
+
313
+ A non‑blocking bus built on fibers (`async`). Emitting a message never blocks the emitter.
314
+
315
+ ## Tools as Capsules
316
+
317
+ Any capsule can opt‑in to act as a "tool" by including `VSM::ActsAsTool` (already included in `VSM::ToolCapsule`).
318
+
319
+ ```ruby
320
+ class ReadFile < VSM::ToolCapsule
321
+ tool_name "read_file"
322
+ tool_description "Read the contents of a UTF-8 text file at relative path."
323
+ tool_schema({
324
+ type: "object",
325
+ properties: { path: { type: "string" } },
326
+ required: ["path"]
327
+ })
328
+
329
+ def run(args)
330
+ path = governance_safe_path(args.fetch("path"))
331
+ File.read(path, mode: "r:UTF-8")
332
+ end
333
+
334
+ # Optional: choose how this tool executes
335
+ def execution_mode = :fiber # or :thread
336
+
337
+ private
338
+
339
+ def governance_safe_path(rel) = governance.instance_eval { # simple helper
340
+ full = File.expand_path(File.join(Dir.pwd, rel))
341
+ raise "outside workspace" unless full.start_with?(Dir.pwd)
342
+ full
343
+ }
344
+ end
345
+ ```
346
+
347
+ VSM provides provider‑agnostic descriptors:
348
+
349
+ ```ruby
350
+ tool = instance.tool_descriptor
351
+ tool.to_openai_tool # => {type:"function", function:{ name, description, parameters }}
352
+ tool.to_anthropic_tool # => {name, description, input_schema}
353
+ tool.to_gemini_tool # => {name, description, parameters}
354
+ ```
355
+
356
+ **Why opt‑in?** Not every capsule should be callable as a tool. Opt‑in keeps coupling low. Later you can auto‑expose selected capsules as tools or via MCP.
357
+
358
+ ## Async & Parallelism
359
+
360
+ VSM is async by default:
361
+
362
+ - The bus is fiber‑based and non‑blocking.
363
+ - The capsule loop drains messages without blocking emitters.
364
+ - Operations runs each tool call in its own task; tools can choose their execution mode:
365
+ - `:fiber` (default) — I/O‑bound, non‑blocking
366
+ - `:thread` — CPU‑ish work or blocking libraries
367
+
368
+ You can add Ractor/Subprocess executors later without changing the API.
369
+
370
+ ## Ports (Interfaces)
371
+
372
+ A Port translates external events into messages and renders outgoing messages. Examples: CLI chat, TUI, HTTP, MCP stdio, editor plugin.
373
+
374
+ ```ruby
375
+ class MyPort < VSM::Port
376
+ def loop
377
+ session = SecureRandom.uuid
378
+ while (line = $stdin.gets&.chomp)
379
+ @capsule.bus.emit VSM::Message.new(kind: :user, payload: line, meta: { session_id: session })
380
+ @capsule.roles[:coordination].wait_for_turn_end(session)
381
+ end
382
+ end
383
+
384
+ def render_out(msg)
385
+ case msg.kind
386
+ when :assistant_delta then $stdout.print(msg.payload)
387
+ when :assistant then puts "\nBot: #{msg.payload}"
388
+ when :confirm_request then confirm(msg)
389
+ end
390
+ end
391
+
392
+ def confirm(msg)
393
+ print "\nConfirm? #{msg.payload} [y/N] "
394
+ ok = ($stdin.gets || "").strip.downcase.start_with?("y")
395
+ @capsule.bus.emit VSM::Message.new(kind: :confirm_response, payload: { accepted: ok }, meta: msg.meta)
396
+ end
397
+ end
398
+ ```
399
+
400
+ Start everything:
401
+
402
+ ```ruby
403
+ VSM::Runtime.start(capsule, ports: [MyPort.new(capsule:)])
404
+ ```
405
+
406
+ ### Built-in Ports
407
+
408
+ - `VSM::Ports::ChatTTY` — A generic, customizable chat terminal UI. Safe to run alongside MCP stdio; prefers `IO.console` so it won’t pollute stdout.
409
+ - `VSM::Ports::MCP::ServerStdio` — Exposes your capsule as an MCP server on stdio implementing `tools/list` and `tools/call`.
410
+
411
+ Enable them:
412
+
413
+ ```ruby
414
+ require "vsm/ports/chat_tty"
415
+ require "vsm/ports/mcp/server_stdio"
416
+
417
+ ports = [
418
+ VSM::Ports::MCP::ServerStdio.new(capsule: capsule), # machine IO (stdio)
419
+ VSM::Ports::ChatTTY.new(capsule: capsule) # human IO (terminal)
420
+ ]
421
+ VSM::Runtime.start(capsule, ports: ports)
422
+ ```
423
+
424
+ ### MCP Client (reflect and wrap tools)
425
+
426
+ Reflect tools from an external MCP server and expose them as local tools using the DSL. This uses a tiny stdio JSON‑RPC client under the hood.
427
+
428
+ ```ruby
429
+ require "vsm/dsl_mcp"
430
+
431
+ cap = VSM::DSL.define(:mcp_client) do
432
+ identity klass: VSM::Identity, args: { identity: "mcp_client", invariants: [] }
433
+ governance klass: VSM::Governance
434
+ coordination klass: VSM::Coordination
435
+ intelligence klass: VSM::Intelligence # or your own
436
+ monitoring klass: VSM::Monitoring
437
+ operations do
438
+ # Prefix helps avoid name collisions
439
+ mcp_server :smith, cmd: "smith-server --stdio", prefix: "smith_", include: %w[search read]
440
+ end
441
+ end
442
+ ```
443
+
444
+ See `examples/06_mcp_mount_reflection.rb` and `examples/07_connect_claude_mcp.rb`.
445
+
446
+ Note: Many MCP servers speak LSP-style `Content-Length` framing on stdio. The
447
+ current minimal transport uses NDJSON for simplicity. If a server hangs or
448
+ doesn't respond, switch the transport to LSP framing in `lib/vsm/mcp/jsonrpc.rb`.
449
+
450
+ ### Customizing ChatTTY
451
+
452
+ You can customize ChatTTY via options or by subclassing to override only the banner and rendering methods, while keeping the input loop.
453
+
454
+ ```ruby
455
+ class FancyTTY < VSM::Ports::ChatTTY
456
+ def banner(io)
457
+ io.puts "\e[95m\n ███ CUSTOM CHAT ███\n\e[0m"
458
+ end
459
+
460
+ def render_out(m)
461
+ super # or implement your own formatting
462
+ end
463
+ end
464
+
465
+ VSM::Runtime.start(capsule, ports: [FancyTTY.new(capsule: capsule, prompt: "Me> ")])
466
+ ```
467
+
468
+ See `examples/08_custom_chattty.rb`.
469
+
470
+ ### LLM-driven MCP tools
471
+
472
+ Use an LLM driver (e.g., OpenAI) to automatically call tools reflected from an MCP server:
473
+
474
+ ```ruby
475
+ driver = VSM::Drivers::OpenAI::AsyncDriver.new(api_key: ENV.fetch("OPENAI_API_KEY"), model: ENV["AIRB_MODEL"] || "gpt-4o-mini")
476
+ cap = VSM::DSL.define(:mcp_with_llm) do
477
+ identity klass: VSM::Identity, args: { identity: "mcp_with_llm", invariants: [] }
478
+ governance klass: VSM::Governance
479
+ coordination klass: VSM::Coordination
480
+ intelligence klass: VSM::Intelligence, args: { driver: driver, system_prompt: "Use tools when helpful." }
481
+ monitoring klass: VSM::Monitoring
482
+ operations do
483
+ mcp_server :server, cmd: ["claude","mcp","serve"] # reflect tools
484
+ end
485
+ end
486
+ VSM::Runtime.start(cap, ports: [VSM::Ports::ChatTTY.new(capsule: cap)])
487
+ ```
488
+
489
+ See `examples/09_mcp_with_llm_calls.rb`.
490
+
491
+ ## Observability
492
+
493
+ VSM ships a tiny Monitoring role that writes an append‑only JSONL ledger:
494
+
495
+ ```
496
+ .vsm.log.jsonl
497
+ {"ts":"2025-08-14T12:00:00Z","kind":"user","path":null,"corr_id":null,"meta":{"session_id":"..."}}
498
+ {"ts":"...","kind":"tool_call", ...}
499
+ {"ts":"...","kind":"tool_result", ...}
500
+ {"ts":"...","kind":"assistant", ...}
501
+ ```
502
+
503
+ Use it to power a TUI/HTTP "Lens" later. Because everything flows over the bus, you get consistent events across nested capsules and sub‑agents.
504
+
505
+ ### MCP and ChatTTY Coexistence
506
+
507
+ - MCP stdio port only reads stdin and writes strict JSON to stdout.
508
+ - ChatTTY prefers `IO.console` or falls back to stderr and disables input if no TTY.
509
+ - You can run both in the same process: machine protocol on stdio, human UI on the terminal.
510
+
511
+ ## Writing an Intelligence
512
+
513
+ The Intelligence role is where you plan/decide. It might:
514
+
515
+ - forward a conversation to an LLM driver (OpenAI/Anthropic/Gemini),
516
+ - emit `:tool_call` messages when the model asks to use tools,
517
+ - stream `:assistant_delta` tokens and finish with `:assistant`.
518
+
519
+ Minimal example (no LLM, just logic):
520
+
521
+ ```ruby
522
+ class MyIntelligence < VSM::Intelligence
523
+ def initialize
524
+ @history = Hash.new { |h,k| h[k] = [] }
525
+ end
526
+
527
+ def handle(message, bus:, **)
528
+ return false unless [:user, :tool_result].include?(message.kind)
529
+ sid = message.meta&.dig(:session_id)
530
+ @history[sid] << message
531
+
532
+ if message.kind == :user && message.payload =~ /read (.+)/
533
+ bus.emit VSM::Message.new(
534
+ kind: :tool_call,
535
+ payload: { tool: "read_file", args: { "path" => $1 } },
536
+ corr_id: SecureRandom.uuid,
537
+ meta: { session_id: sid }
538
+ )
539
+ else
540
+ bus.emit VSM::Message.new(kind: :assistant, payload: "ok", meta: { session_id: sid })
541
+ end
542
+ true
543
+ end
544
+ end
545
+ ```
546
+
547
+ In your application, you can plug in provider drivers that stream and support native tool calling; Intelligence remains the same.
548
+
549
+ ## Testing
550
+
551
+ VSM is designed for unit tests:
552
+
553
+ - **Capsules**: inject fake systems and assert dispatch.
554
+ - **Intelligence**: feed `:user` / `:tool_result` messages and assert emitted messages.
555
+ - **Tools**: call `#run` directly.
556
+ - **Ports**: treat like adapters; they're thin.
557
+
558
+ Quick smoke test:
559
+
560
+ ```ruby
561
+ require "vsm"
562
+
563
+ RSpec.describe "tool dispatch" do
564
+ class T < VSM::ToolCapsule
565
+ tool_name "t"; tool_description "d"; tool_schema({ type: "object", properties: {}, required: [] })
566
+ def run(_args) = "ok"
567
+ end
568
+
569
+ it "routes tool_call to tool_result" do
570
+ cap = VSM::DSL.define(:test) do
571
+ identity klass: VSM::Identity, args: { identity: "t", invariants: [] }
572
+ governance klass: VSM::Governance
573
+ coordination klass: VSM::Coordination
574
+ intelligence klass: VSM::Intelligence
575
+ operations { capsule :t, klass: T }
576
+ end
577
+
578
+ q = Queue.new
579
+ cap.bus.subscribe { |m| q << m if m.kind == :tool_result }
580
+ cap.run
581
+ cap.bus.emit VSM::Message.new(kind: :tool_call, payload: { tool: "t", args: {} }, corr_id: "1")
582
+ expect(q.pop.payload).to eq("ok")
583
+ end
584
+ end
585
+ ```
586
+
587
+ ## Design Goals
588
+
589
+ - **Ergonomic Ruby** (small objects, clear names, blocks/DSL where it helps)
590
+ - **High cohesion, low coupling** (roles are tiny; tools are self‑contained)
591
+ - **Recursion by default** (any capsule can contain more capsules)
592
+ - **Async from day one** (non‑blocking bus; concurrent tools)
593
+ - **Portability** (no hard dependency on a specific LLM vendor)
594
+ - **Observability built‑in** (event ledger everywhere)
595
+
596
+ ## Roadmap
597
+
598
+ - [ ] **Executors**: Ractor & Subprocess for heavy/risky tools
599
+ - [ ] **Limiter**: per‑tool semaphores and budgets (tokens/time/IO) in Governance
600
+ - [ ] **Lens UI**: terminal/HTTP viewer for plans, tools, and audits
601
+ - [ ] **Drivers**: optional `vsm-openai`, `vsm-anthropic`, `vsm-gemini` add‑ons for native tool‑calling + streaming
602
+ - [ ] **MCP ports**: stdio server/client to expose/consume MCP tools
603
+
604
+ ## FAQ
605
+
606
+ **Does every capsule have to be a tool?**
607
+ No. Opt‑in via `VSM::ActsAsTool`. Many capsules (planner, auditor, coordinator) shouldn't be callable as tools.
608
+
609
+ **Can I run multiple interfaces at once (chat + HTTP + MCP)?**
610
+ Yes. Start multiple ports; Coordination arbitrates the "floor" per session.
611
+
612
+ **How do I isolate risky or CPU‑heavy tools?**
613
+ Set `execution_mode` to `:thread` today. Ractor/Subprocess executors are planned and will use the same API.
614
+
615
+ **What about streaming tokens?**
616
+ Handled by your Intelligence implementation (e.g., your LLM driver). Emit `:assistant_delta` messages as tokens arrive; finish with a single `:assistant`.
617
+
618
+ **Is VSM tied to any specific LLM?**
619
+ No. Write a driver that conforms to your Intelligence's expectations (usually "yield deltas" + "yield tool_calls"). Keep the provider in your app gem.
34
620
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/vsm.
621
+ ## API Overview
622
+
623
+ ```ruby
624
+ module VSM
625
+ # Messages
626
+ Message(kind:, payload:, path: nil, corr_id: nil, meta: {})
627
+
628
+ # Bus
629
+ class AsyncChannel
630
+ def emit(message); end
631
+ def pop; end
632
+ def subscribe(&block); end
633
+ attr_reader :context
634
+ end
635
+
636
+ # Roles (named systems)
637
+ class Operations; end # routes tool_call -> children
638
+ class Coordination; end # scheduling, floor, turn-end
639
+ class Intelligence; end # your planning/LLM driver
640
+ class Governance; end # policy/safety/budgets
641
+ class Identity; end # invariants & escalation
642
+ class Monitoring; end # JSONL ledger (observe)
643
+
644
+ # Capsules & DSL
645
+ class Capsule; end
646
+ module DSL
647
+ def self.define(:name, &block) -> Capsule
648
+ end
649
+
650
+ # Tools
651
+ module Tool
652
+ Descriptor#to_openai_tool / #to_anthropic_tool / #to_gemini_tool
653
+ end
654
+ module ActsAsTool; end
655
+ class ToolCapsule
656
+ # include ActsAsTool
657
+ # def run(args) ...
658
+ # def execution_mode = :fiber | :thread
659
+ end
660
+
661
+ # Ports & runtime
662
+ class Port
663
+ def initialize(capsule:); end
664
+ def loop; end # optional
665
+ def render_out(msg); end
666
+ def egress_subscribe; end
667
+ end
668
+
669
+ module Runtime
670
+ def self.start(capsule, ports: [])
671
+ end
672
+ end
673
+ ```
36
674
 
37
675
  ## License
38
676
 
39
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
677
+ MIT. See LICENSE.txt.
678
+
679
+ ## Contributing
680
+
681
+ Issues and PRs are welcome! Please include:
682
+
683
+ - A failing spec (RSpec) for bug reports
684
+ - Minimal API additions
685
+ - Clear commit messages
686
+
687
+ Run tests with:
688
+
689
+ ```bash
690
+ bundle exec rspec
691
+ ```
692
+
693
+ Lint with:
694
+
695
+ ```bash
696
+ bundle exec rubocop
697
+ ```