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.
- checksums.yaml +4 -4
- data/.claude/settings.local.json +17 -0
- data/CLAUDE.md +134 -0
- data/README.md +531 -17
- data/examples/01_echo_tool.rb +70 -0
- data/examples/02_openai_streaming.rb +73 -0
- data/examples/02b_anthropic_streaming.rb +61 -0
- data/examples/02c_gemini_streaming.rb +60 -0
- data/examples/03_openai_tools.rb +106 -0
- data/examples/03b_anthropic_tools.rb +96 -0
- data/examples/03c_gemini_tools.rb +95 -0
- data/lib/vsm/async_channel.rb +21 -0
- data/lib/vsm/capsule.rb +44 -0
- data/lib/vsm/drivers/anthropic/async_driver.rb +210 -0
- data/lib/vsm/drivers/family.rb +16 -0
- data/lib/vsm/drivers/gemini/async_driver.rb +149 -0
- data/lib/vsm/drivers/openai/async_driver.rb +202 -0
- data/lib/vsm/dsl.rb +50 -0
- data/lib/vsm/executors/fiber_executor.rb +10 -0
- data/lib/vsm/executors/thread_executor.rb +19 -0
- data/lib/vsm/homeostat.rb +19 -0
- data/lib/vsm/lens/event_hub.rb +73 -0
- data/lib/vsm/lens/server.rb +188 -0
- data/lib/vsm/lens/stats.rb +58 -0
- data/lib/vsm/lens/tui.rb +88 -0
- data/lib/vsm/lens.rb +79 -0
- data/lib/vsm/message.rb +6 -0
- data/lib/vsm/observability/ledger.rb +25 -0
- data/lib/vsm/port.rb +11 -0
- data/lib/vsm/roles/coordination.rb +49 -0
- data/lib/vsm/roles/governance.rb +9 -0
- data/lib/vsm/roles/identity.rb +11 -0
- data/lib/vsm/roles/intelligence.rb +168 -0
- data/lib/vsm/roles/operations.rb +33 -0
- data/lib/vsm/runtime.rb +18 -0
- data/lib/vsm/tool/acts_as_tool.rb +20 -0
- data/lib/vsm/tool/capsule.rb +12 -0
- data/lib/vsm/tool/descriptor.rb +16 -0
- data/lib/vsm/version.rb +1 -1
- data/lib/vsm.rb +33 -0
- data/llms.txt +322 -0
- 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
|
data/lib/vsm/runtime.rb
ADDED
@@ -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
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.
|