vsm 0.0.1 → 0.1.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 (42) 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 +531 -17
  5. data/examples/01_echo_tool.rb +70 -0
  6. data/examples/02_openai_streaming.rb +73 -0
  7. data/examples/02b_anthropic_streaming.rb +61 -0
  8. data/examples/02c_gemini_streaming.rb +60 -0
  9. data/examples/03_openai_tools.rb +106 -0
  10. data/examples/03b_anthropic_tools.rb +96 -0
  11. data/examples/03c_gemini_tools.rb +95 -0
  12. data/lib/vsm/async_channel.rb +21 -0
  13. data/lib/vsm/capsule.rb +44 -0
  14. data/lib/vsm/drivers/anthropic/async_driver.rb +210 -0
  15. data/lib/vsm/drivers/family.rb +16 -0
  16. data/lib/vsm/drivers/gemini/async_driver.rb +149 -0
  17. data/lib/vsm/drivers/openai/async_driver.rb +202 -0
  18. data/lib/vsm/dsl.rb +50 -0
  19. data/lib/vsm/executors/fiber_executor.rb +10 -0
  20. data/lib/vsm/executors/thread_executor.rb +19 -0
  21. data/lib/vsm/homeostat.rb +19 -0
  22. data/lib/vsm/lens/event_hub.rb +73 -0
  23. data/lib/vsm/lens/server.rb +188 -0
  24. data/lib/vsm/lens/stats.rb +58 -0
  25. data/lib/vsm/lens/tui.rb +88 -0
  26. data/lib/vsm/lens.rb +79 -0
  27. data/lib/vsm/message.rb +6 -0
  28. data/lib/vsm/observability/ledger.rb +25 -0
  29. data/lib/vsm/port.rb +11 -0
  30. data/lib/vsm/roles/coordination.rb +49 -0
  31. data/lib/vsm/roles/governance.rb +9 -0
  32. data/lib/vsm/roles/identity.rb +11 -0
  33. data/lib/vsm/roles/intelligence.rb +168 -0
  34. data/lib/vsm/roles/operations.rb +33 -0
  35. data/lib/vsm/runtime.rb +18 -0
  36. data/lib/vsm/tool/acts_as_tool.rb +20 -0
  37. data/lib/vsm/tool/capsule.rb +12 -0
  38. data/lib/vsm/tool/descriptor.rb +16 -0
  39. data/lib/vsm/version.rb +1 -1
  40. data/lib/vsm.rb +33 -0
  41. data/llms.txt +322 -0
  42. metadata +67 -25
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+ require "set"
3
+ require_relative "../drivers/family"
4
+
5
+ module VSM
6
+ # Orchestrates multi-turn LLM chat with native tool-calls:
7
+ # - Maintains neutral conversation history per session_id
8
+ # - Talks to a provider driver that yields :assistant_delta, :assistant_final, :tool_calls
9
+ # - Emits :tool_call to Operations, waits for ALL results, then continues
10
+ #
11
+ # App authors can subclass and only customize:
12
+ # - system_prompt(session_id) -> String
13
+ # - offer_tools?(session_id, descriptor) -> true/false (filter tools)
14
+ class Intelligence
15
+ def initialize(driver:, system_prompt: nil)
16
+ @driver = driver
17
+ @system_prompt = system_prompt
18
+ @sessions = Hash.new { |h,k| h[k] = new_session_state }
19
+ end
20
+
21
+ def observe(bus); end
22
+
23
+ def handle(message, bus:, **)
24
+ case message.kind
25
+ when :user
26
+ sid = message.meta&.dig(:session_id)
27
+ st = state(sid)
28
+ st[:history] << { role: "user", content: message.payload.to_s }
29
+ invoke_model(sid, bus)
30
+ true
31
+
32
+ when :tool_result
33
+ sid = message.meta&.dig(:session_id)
34
+ st = state(sid)
35
+ # map id -> tool name if we learned it earlier (useful for Gemini)
36
+ name = st[:tool_id_to_name][message.corr_id]
37
+
38
+ # Debug logging
39
+ if ENV["VSM_DEBUG_STREAM"] == "1"
40
+ $stderr.puts "Intelligence: Received tool_result for #{name}(#{message.corr_id}): #{message.payload.to_s.slice(0, 100)}"
41
+ end
42
+
43
+ st[:history] << { role: "tool_result", tool_call_id: message.corr_id, name: name, content: message.payload.to_s }
44
+ st[:pending_tool_ids].delete(message.corr_id)
45
+ # Only continue once all tool results for this turn arrived:
46
+ if st[:pending_tool_ids].empty?
47
+ # Re-enter model for the same turn with tool results in history:
48
+ invoke_model(sid, bus)
49
+ end
50
+ true
51
+
52
+ else
53
+ false
54
+ end
55
+ end
56
+
57
+ # --- Extension points for apps ---
58
+
59
+ # Override to compute a dynamic prompt per session
60
+ def system_prompt(session_id)
61
+ @system_prompt
62
+ end
63
+
64
+ # Override to filter tools the model may use (by descriptor)
65
+ def offer_tools?(session_id, descriptor)
66
+ true
67
+ end
68
+
69
+ private
70
+
71
+ def new_session_state
72
+ {
73
+ history: [],
74
+ pending_tool_ids: Set.new,
75
+ tool_id_to_name: {},
76
+ inflight: false,
77
+ turn_seq: 0
78
+ }
79
+ end
80
+
81
+ def state(sid) = @sessions[sid]
82
+
83
+ def invoke_model(session_id, bus)
84
+ st = state(session_id)
85
+ if st[:inflight] || !st[:pending_tool_ids].empty?
86
+ if ENV["VSM_DEBUG_STREAM"] == "1"
87
+ $stderr.puts "Intelligence: skip invoke sid=#{session_id} inflight=#{st[:inflight]} pending=#{st[:pending_tool_ids].size}"
88
+ end
89
+ return
90
+ end
91
+ st[:inflight] = true
92
+ st[:turn_seq] += 1
93
+ current_turn_id = st[:turn_seq]
94
+
95
+ # Discover tools available from Operations children:
96
+ descriptors, name_index = tool_inventory(bus, session_id)
97
+
98
+ # Debug logging
99
+ if ENV["VSM_DEBUG_STREAM"] == "1"
100
+ $stderr.puts "Intelligence: invoke_model sid=#{session_id} inflight=#{st[:inflight]} pending=#{st[:pending_tool_ids].size} turn_seq=#{st[:turn_seq]}"
101
+ $stderr.puts "Intelligence: Calling driver with #{st[:history].size} history entries"
102
+ st[:history].each_with_index do |h, i|
103
+ $stderr.puts " [#{i}] #{h[:role]}: #{h[:role] == 'assistant_tool_calls' ? h[:tool_calls].map{|tc| "#{tc[:name]}(#{tc[:id]})"}.join(', ') : h[:content]&.slice(0, 100)}"
104
+ end
105
+ end
106
+
107
+ task = Async do
108
+ begin
109
+ @driver.run!(
110
+ conversation: st[:history],
111
+ tools: descriptors,
112
+ policy: { system_prompt: system_prompt(session_id) }
113
+ ) do |event, payload|
114
+ case event
115
+ when :assistant_delta
116
+ # optionally buffer based on stream_policy
117
+ bus.emit VSM::Message.new(kind: :assistant_delta, payload: payload, meta: { session_id: session_id, turn_id: current_turn_id })
118
+ when :assistant_final
119
+ unless payload.to_s.empty?
120
+ st[:history] << { role: "assistant", content: payload.to_s }
121
+ end
122
+ bus.emit VSM::Message.new(kind: :assistant, payload: payload, meta: { session_id: session_id, turn_id: current_turn_id })
123
+ when :tool_calls
124
+ st[:history] << { role: "assistant_tool_calls", tool_calls: payload }
125
+ st[:pending_tool_ids] = Set.new(payload.map { _1[:id] })
126
+ payload.each { |c| st[:tool_id_to_name][c[:id]] = c[:name] }
127
+ if ENV["VSM_DEBUG_STREAM"] == "1"
128
+ $stderr.puts "Intelligence: tool_calls count=#{payload.size}; pending now=#{st[:pending_tool_ids].size}"
129
+ end
130
+ # Allow next invocation (after tools complete) without waiting for driver ensure
131
+ st[:inflight] = false
132
+ payload.each do |call|
133
+ bus.emit VSM::Message.new(
134
+ kind: :tool_call,
135
+ payload: { tool: call[:name], args: call[:arguments] },
136
+ corr_id: call[:id],
137
+ meta: { session_id: session_id, tool: call[:name], turn_id: current_turn_id }
138
+ )
139
+ end
140
+ end
141
+ end
142
+ ensure
143
+ if ENV["VSM_DEBUG_STREAM"] == "1"
144
+ $stderr.puts "Intelligence: driver completed sid=#{session_id}; pending=#{st[:pending_tool_ids].size}; inflight->false"
145
+ end
146
+ st[:inflight] = false
147
+ end
148
+ end
149
+ st[:task] = task
150
+ end
151
+
152
+ # Return [descriptors:Array<VSM::Tool::Descriptor>, index Hash{name=>capsule}]
153
+ def tool_inventory(bus, session_id)
154
+ ops = bus.context[:operations_children] || {}
155
+ descriptors = []
156
+ index = {}
157
+ ops.each do |name, capsule|
158
+ next unless capsule.respond_to?(:tool_descriptor)
159
+ desc = capsule.tool_descriptor
160
+ next unless offer_tools?(session_id, desc)
161
+ descriptors << desc
162
+ index[desc.name] = capsule
163
+ end
164
+ [descriptors, index]
165
+ end
166
+ end
167
+ end
168
+
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../executors/fiber_executor"
4
+ require_relative "../executors/thread_executor"
5
+
6
+ module VSM
7
+ class Operations
8
+ EXECUTORS = {
9
+ fiber: Executors::FiberExecutor,
10
+ thread: Executors::ThreadExecutor
11
+ }.freeze
12
+
13
+ def observe(bus); end
14
+
15
+ def handle(message, bus:, children:, **)
16
+ return false unless message.kind == :tool_call
17
+
18
+ name = message.payload[:tool].to_s
19
+ tool_capsule = children.fetch(name) { raise "unknown tool capsule: #{name}" }
20
+ mode = tool_capsule.respond_to?(:execution_mode) ? tool_capsule.execution_mode : :fiber
21
+ executor = EXECUTORS.fetch(mode)
22
+
23
+ Async do
24
+ result = executor.call(tool_capsule, message.payload[:args])
25
+ bus.emit Message.new(kind: :tool_result, payload: result, corr_id: message.corr_id, meta: message.meta)
26
+ rescue => e
27
+ bus.emit Message.new(kind: :tool_result, payload: "ERROR: #{e.class}: #{e.message}", corr_id: message.corr_id, meta: message.meta)
28
+ end
29
+
30
+ true
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ require "async"
3
+
4
+ module VSM
5
+ module Runtime
6
+ def self.start(capsule, ports: [])
7
+ Async do |task|
8
+ capsule.run
9
+ ports.each do |p|
10
+ p.egress_subscribe if p.respond_to?(:egress_subscribe)
11
+ task.async { p.loop } if p.respond_to?(:loop)
12
+ end
13
+ task.sleep
14
+ end
15
+ end
16
+ end
17
+ end
18
+
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ module VSM
3
+ module ActsAsTool
4
+ def self.included(base) = base.extend(ClassMethods)
5
+
6
+ module ClassMethods
7
+ def tool_name(value = nil); @tool_name = value if value; @tool_name; end
8
+ def tool_description(value = nil); @tool_description = value if value; @tool_description; end
9
+ def tool_schema(value = nil); @tool_schema = value if value; @tool_schema; end
10
+ end
11
+
12
+ def tool_descriptor
13
+ VSM::Tool::Descriptor.new(
14
+ name: self.class.tool_name,
15
+ description: self.class.tool_description,
16
+ schema: self.class.tool_schema
17
+ )
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ module VSM
3
+ class ToolCapsule
4
+ include ActsAsTool
5
+ attr_writer :governance
6
+ def governance = @governance || (raise "governance not injected")
7
+ # Subclasses implement:
8
+ # def run(args) ... end
9
+ # Optional:
10
+ # def execution_mode = :fiber | :thread
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ module VSM
3
+ module Tool
4
+ Descriptor = Struct.new(:name, :description, :schema, keyword_init: true) do
5
+ def to_openai_tool
6
+ { type: "function", function: { name:, description:, parameters: schema } }
7
+ end
8
+ def to_anthropic_tool
9
+ { name:, description:, input_schema: schema }
10
+ end
11
+ def to_gemini_tool
12
+ { name:, description:, parameters: schema }
13
+ end
14
+ end
15
+ end
16
+ end
data/lib/vsm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vsm
4
- VERSION = "0.0.1"
4
+ VERSION = "0.1.0"
5
5
  end
data/lib/vsm.rb CHANGED
@@ -1,7 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "async"
4
+ require "async/queue"
5
+
3
6
  require_relative "vsm/version"
4
7
 
8
+ require_relative "vsm/message"
9
+ require_relative "vsm/async_channel"
10
+ require_relative "vsm/homeostat"
11
+ require_relative "vsm/observability/ledger"
12
+
13
+ require_relative "vsm/roles/operations"
14
+ require_relative "vsm/roles/coordination"
15
+ require_relative "vsm/roles/intelligence"
16
+ require_relative "vsm/roles/governance"
17
+ require_relative "vsm/roles/identity"
18
+
19
+ require_relative "vsm/tool/descriptor"
20
+ require_relative "vsm/tool/acts_as_tool"
21
+ require_relative "vsm/tool/capsule"
22
+
23
+ require_relative "vsm/executors/fiber_executor"
24
+ require_relative "vsm/executors/thread_executor"
25
+
26
+ require_relative "vsm/capsule"
27
+ require_relative "vsm/dsl"
28
+ require_relative "vsm/port"
29
+ require_relative "vsm/runtime"
30
+
31
+ require_relative "vsm/drivers/openai/async_driver"
32
+ require_relative "vsm/drivers/anthropic/async_driver"
33
+ require_relative "vsm/drivers/gemini/async_driver"
34
+ require_relative "vsm/drivers/family"
35
+
36
+ require_relative "vsm/lens"
37
+
5
38
  module Vsm
6
39
  class Error < StandardError; end
7
40
  # Your code goes here...
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.