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.
- checksums.yaml +4 -4
- data/.claude/settings.local.json +17 -0
- data/CLAUDE.md +134 -0
- data/README.md +675 -17
- data/Rakefile +1 -5
- data/examples/01_echo_tool.rb +51 -0
- data/examples/02_openai_streaming.rb +73 -0
- data/examples/02b_anthropic_streaming.rb +58 -0
- data/examples/02c_gemini_streaming.rb +60 -0
- data/examples/03_openai_tools.rb +106 -0
- data/examples/03b_anthropic_tools.rb +93 -0
- data/examples/03c_gemini_tools.rb +95 -0
- data/examples/05_mcp_server_and_chattty.rb +63 -0
- data/examples/06_mcp_mount_reflection.rb +45 -0
- data/examples/07_connect_claude_mcp.rb +78 -0
- data/examples/08_custom_chattty.rb +63 -0
- data/examples/09_mcp_with_llm_calls.rb +49 -0
- data/examples/10_meta_read_only.rb +56 -0
- data/exe/vsm +17 -0
- data/lib/vsm/async_channel.rb +44 -0
- data/lib/vsm/capsule.rb +46 -0
- data/lib/vsm/cli.rb +78 -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 +80 -0
- data/lib/vsm/dsl_mcp.rb +36 -0
- data/lib/vsm/executors/fiber_executor.rb +10 -0
- data/lib/vsm/executors/thread_executor.rb +19 -0
- data/lib/vsm/generator/new_project.rb +154 -0
- data/lib/vsm/generator/templates/Gemfile.erb +9 -0
- data/lib/vsm/generator/templates/README_md.erb +40 -0
- data/lib/vsm/generator/templates/Rakefile.erb +5 -0
- data/lib/vsm/generator/templates/bin_console.erb +11 -0
- data/lib/vsm/generator/templates/bin_setup.erb +7 -0
- data/lib/vsm/generator/templates/exe_name.erb +34 -0
- data/lib/vsm/generator/templates/gemspec.erb +24 -0
- data/lib/vsm/generator/templates/gitignore.erb +10 -0
- data/lib/vsm/generator/templates/lib_name_rb.erb +9 -0
- data/lib/vsm/generator/templates/lib_organism_rb.erb +44 -0
- data/lib/vsm/generator/templates/lib_ports_chat_tty_rb.erb +12 -0
- data/lib/vsm/generator/templates/lib_tools_read_file_rb.erb +32 -0
- data/lib/vsm/generator/templates/lib_version_rb.erb +6 -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/mcp/client.rb +80 -0
- data/lib/vsm/mcp/jsonrpc.rb +92 -0
- data/lib/vsm/mcp/remote_tool_capsule.rb +35 -0
- data/lib/vsm/message.rb +6 -0
- data/lib/vsm/meta/snapshot_builder.rb +121 -0
- data/lib/vsm/meta/snapshot_cache.rb +25 -0
- data/lib/vsm/meta/support.rb +35 -0
- data/lib/vsm/meta/tools.rb +498 -0
- data/lib/vsm/meta.rb +59 -0
- data/lib/vsm/observability/ledger.rb +25 -0
- data/lib/vsm/port.rb +11 -0
- data/lib/vsm/ports/chat_tty.rb +112 -0
- data/lib/vsm/ports/mcp/server_stdio.rb +101 -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 +172 -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 +43 -0
- data/llms.txt +322 -0
- data/mcp_update.md +162 -0
- metadata +93 -31
- data/.rubocop.yml +0 -8
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "json"
|
|
3
|
+
require "async"
|
|
4
|
+
|
|
5
|
+
module VSM
|
|
6
|
+
module Ports
|
|
7
|
+
module MCP
|
|
8
|
+
# Exposes the capsule's tools as an MCP server over stdio (NDJSON JSON-RPC).
|
|
9
|
+
# Implemented methods: tools/list, tools/call.
|
|
10
|
+
class ServerStdio < VSM::Port
|
|
11
|
+
def initialize(capsule:)
|
|
12
|
+
super(capsule: capsule)
|
|
13
|
+
@waiters = {}
|
|
14
|
+
@waiters_mutex = Mutex.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def egress_subscribe
|
|
18
|
+
# Single subscriber that resolves tool_result waiters by corr_id
|
|
19
|
+
@capsule.bus.subscribe do |m|
|
|
20
|
+
next unless m.kind == :tool_result
|
|
21
|
+
q = nil
|
|
22
|
+
@waiters_mutex.synchronize { q = @waiters.delete(m.corr_id.to_s) }
|
|
23
|
+
q&.enqueue(m)
|
|
24
|
+
end
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def loop
|
|
29
|
+
$stdout.sync = true
|
|
30
|
+
while (line = $stdin.gets)
|
|
31
|
+
begin
|
|
32
|
+
req = JSON.parse(line)
|
|
33
|
+
rescue => e
|
|
34
|
+
write_err(nil, code: -32700, message: "Parse error: #{e.message}")
|
|
35
|
+
next
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
id = req["id"]
|
|
39
|
+
method = req["method"]
|
|
40
|
+
params = req["params"] || {}
|
|
41
|
+
case method
|
|
42
|
+
when "tools/list"
|
|
43
|
+
write_ok(id, { tools: list_tools })
|
|
44
|
+
when "tools/call"
|
|
45
|
+
name = params["name"].to_s
|
|
46
|
+
args = params["arguments"] || {}
|
|
47
|
+
res = call_local_tool(id, name, args)
|
|
48
|
+
write_ok(id, { content: [{ type: "text", text: res.to_s }] })
|
|
49
|
+
else
|
|
50
|
+
write_err(id, code: -32601, message: "Method not found: #{method}")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def list_tools
|
|
58
|
+
ops = @capsule.bus.context[:operations_children] || {}
|
|
59
|
+
ops.values
|
|
60
|
+
.select { _1.respond_to?(:tool_descriptor) }
|
|
61
|
+
.map { to_mcp_descriptor(_1.tool_descriptor) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def to_mcp_descriptor(desc)
|
|
65
|
+
{
|
|
66
|
+
"name" => desc.name,
|
|
67
|
+
"description" => desc.description,
|
|
68
|
+
"input_schema" => desc.schema
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def call_local_tool(req_id, name, args)
|
|
73
|
+
corr = req_id.to_s
|
|
74
|
+
q = Async::Queue.new
|
|
75
|
+
@waiters_mutex.synchronize { @waiters[corr] = q }
|
|
76
|
+
@capsule.bus.emit VSM::Message.new(
|
|
77
|
+
kind: :tool_call,
|
|
78
|
+
payload: { tool: name, args: args },
|
|
79
|
+
corr_id: corr,
|
|
80
|
+
meta: { session_id: "mcp:stdio" },
|
|
81
|
+
path: [:mcp, :server, name]
|
|
82
|
+
)
|
|
83
|
+
msg = q.dequeue
|
|
84
|
+
msg.payload
|
|
85
|
+
ensure
|
|
86
|
+
@waiters_mutex.synchronize { @waiters.delete(corr) }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def write_ok(id, result)
|
|
90
|
+
puts JSON.dump({ jsonrpc: "2.0", id: id, result: result })
|
|
91
|
+
$stdout.flush
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def write_err(id, code:, message:)
|
|
95
|
+
puts JSON.dump({ jsonrpc: "2.0", id: id, error: { code: code, message: message } })
|
|
96
|
+
$stdout.flush
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module VSM
|
|
3
|
+
class Coordination
|
|
4
|
+
def initialize
|
|
5
|
+
@queue = []
|
|
6
|
+
@floor_by_session = nil
|
|
7
|
+
@turn_waiters = {} # session_id => Async::Queue
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def observe(bus)
|
|
11
|
+
# Note: staging is handled by the capsule loop, not by subscription
|
|
12
|
+
# This method exists for consistency but doesn't auto-stage messages
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def stage(message) = (@queue << message)
|
|
16
|
+
|
|
17
|
+
def drain(bus)
|
|
18
|
+
return if @queue.empty?
|
|
19
|
+
@queue.sort_by! { order(_1) }
|
|
20
|
+
@queue.shift(@queue.size).each do |msg|
|
|
21
|
+
yield msg
|
|
22
|
+
if msg.kind == :assistant && (sid = msg.meta&.dig(:session_id)) && @turn_waiters[sid]
|
|
23
|
+
@turn_waiters[sid].enqueue(:done)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def grant_floor!(session_id) = (@floor_by_session = session_id)
|
|
29
|
+
|
|
30
|
+
def wait_for_turn_end(session_id)
|
|
31
|
+
q = (@turn_waiters[session_id] ||= Async::Queue.new)
|
|
32
|
+
q.dequeue
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def order(m)
|
|
36
|
+
base =
|
|
37
|
+
case m.kind
|
|
38
|
+
when :user then 0
|
|
39
|
+
when :tool_result then 1
|
|
40
|
+
when :plan then 2
|
|
41
|
+
when :assistant_delta then 3
|
|
42
|
+
when :assistant then 4
|
|
43
|
+
else 9
|
|
44
|
+
end
|
|
45
|
+
sid = m.meta&.dig(:session_id)
|
|
46
|
+
sid == @floor_by_session ? base - 1 : base
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module VSM
|
|
3
|
+
class Identity
|
|
4
|
+
def initialize(identity:, invariants: [])
|
|
5
|
+
@identity, @invariants = identity, invariants
|
|
6
|
+
end
|
|
7
|
+
def observe(bus); end
|
|
8
|
+
def handle(message, bus:, **) = false
|
|
9
|
+
def alert(message); end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
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
|
+
attr_reader :driver
|
|
16
|
+
|
|
17
|
+
def initialize(driver: nil, system_prompt: nil)
|
|
18
|
+
@driver = driver
|
|
19
|
+
@system_prompt = system_prompt
|
|
20
|
+
@sessions = Hash.new { |h,k| h[k] = new_session_state }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def observe(bus); end
|
|
24
|
+
|
|
25
|
+
def handle(message, bus:, **)
|
|
26
|
+
# If no driver is configured, the base implementation is inert.
|
|
27
|
+
# Subclasses can override #handle to implement non-LLM behavior.
|
|
28
|
+
return false if @driver.nil?
|
|
29
|
+
case message.kind
|
|
30
|
+
when :user
|
|
31
|
+
sid = message.meta&.dig(:session_id)
|
|
32
|
+
st = state(sid)
|
|
33
|
+
st[:history] << { role: "user", content: message.payload.to_s }
|
|
34
|
+
invoke_model(sid, bus)
|
|
35
|
+
true
|
|
36
|
+
|
|
37
|
+
when :tool_result
|
|
38
|
+
sid = message.meta&.dig(:session_id)
|
|
39
|
+
st = state(sid)
|
|
40
|
+
# map id -> tool name if we learned it earlier (useful for Gemini)
|
|
41
|
+
name = st[:tool_id_to_name][message.corr_id]
|
|
42
|
+
|
|
43
|
+
# Debug logging
|
|
44
|
+
if ENV["VSM_DEBUG_STREAM"] == "1"
|
|
45
|
+
$stderr.puts "Intelligence: Received tool_result for #{name}(#{message.corr_id}): #{message.payload.to_s.slice(0, 100)}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
st[:history] << { role: "tool_result", tool_call_id: message.corr_id, name: name, content: message.payload.to_s }
|
|
49
|
+
st[:pending_tool_ids].delete(message.corr_id)
|
|
50
|
+
# Only continue once all tool results for this turn arrived:
|
|
51
|
+
if st[:pending_tool_ids].empty?
|
|
52
|
+
# Re-enter model for the same turn with tool results in history:
|
|
53
|
+
invoke_model(sid, bus)
|
|
54
|
+
end
|
|
55
|
+
true
|
|
56
|
+
|
|
57
|
+
else
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# --- Extension points for apps ---
|
|
63
|
+
|
|
64
|
+
# Override to compute a dynamic prompt per session
|
|
65
|
+
def system_prompt(session_id)
|
|
66
|
+
@system_prompt
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Override to filter tools the model may use (by descriptor)
|
|
70
|
+
def offer_tools?(session_id, descriptor)
|
|
71
|
+
true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def new_session_state
|
|
77
|
+
{
|
|
78
|
+
history: [],
|
|
79
|
+
pending_tool_ids: Set.new,
|
|
80
|
+
tool_id_to_name: {},
|
|
81
|
+
inflight: false,
|
|
82
|
+
turn_seq: 0
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def state(sid) = @sessions[sid]
|
|
87
|
+
|
|
88
|
+
def invoke_model(session_id, bus)
|
|
89
|
+
st = state(session_id)
|
|
90
|
+
if st[:inflight] || !st[:pending_tool_ids].empty?
|
|
91
|
+
if ENV["VSM_DEBUG_STREAM"] == "1"
|
|
92
|
+
$stderr.puts "Intelligence: skip invoke sid=#{session_id} inflight=#{st[:inflight]} pending=#{st[:pending_tool_ids].size}"
|
|
93
|
+
end
|
|
94
|
+
return
|
|
95
|
+
end
|
|
96
|
+
st[:inflight] = true
|
|
97
|
+
st[:turn_seq] += 1
|
|
98
|
+
current_turn_id = st[:turn_seq]
|
|
99
|
+
|
|
100
|
+
# Discover tools available from Operations children:
|
|
101
|
+
descriptors, name_index = tool_inventory(bus, session_id)
|
|
102
|
+
|
|
103
|
+
# Debug logging
|
|
104
|
+
if ENV["VSM_DEBUG_STREAM"] == "1"
|
|
105
|
+
$stderr.puts "Intelligence: invoke_model sid=#{session_id} inflight=#{st[:inflight]} pending=#{st[:pending_tool_ids].size} turn_seq=#{st[:turn_seq]}"
|
|
106
|
+
$stderr.puts "Intelligence: Calling driver with #{st[:history].size} history entries"
|
|
107
|
+
st[:history].each_with_index do |h, i|
|
|
108
|
+
$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)}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
task = Async do
|
|
113
|
+
begin
|
|
114
|
+
@driver.run!(
|
|
115
|
+
conversation: st[:history],
|
|
116
|
+
tools: descriptors,
|
|
117
|
+
policy: { system_prompt: system_prompt(session_id) }
|
|
118
|
+
) do |event, payload|
|
|
119
|
+
case event
|
|
120
|
+
when :assistant_delta
|
|
121
|
+
# optionally buffer based on stream_policy
|
|
122
|
+
bus.emit VSM::Message.new(kind: :assistant_delta, payload: payload, meta: { session_id: session_id, turn_id: current_turn_id })
|
|
123
|
+
when :assistant_final
|
|
124
|
+
unless payload.to_s.empty?
|
|
125
|
+
st[:history] << { role: "assistant", content: payload.to_s }
|
|
126
|
+
end
|
|
127
|
+
bus.emit VSM::Message.new(kind: :assistant, payload: payload, meta: { session_id: session_id, turn_id: current_turn_id })
|
|
128
|
+
when :tool_calls
|
|
129
|
+
st[:history] << { role: "assistant_tool_calls", tool_calls: payload }
|
|
130
|
+
st[:pending_tool_ids] = Set.new(payload.map { _1[:id] })
|
|
131
|
+
payload.each { |c| st[:tool_id_to_name][c[:id]] = c[:name] }
|
|
132
|
+
if ENV["VSM_DEBUG_STREAM"] == "1"
|
|
133
|
+
$stderr.puts "Intelligence: tool_calls count=#{payload.size}; pending now=#{st[:pending_tool_ids].size}"
|
|
134
|
+
end
|
|
135
|
+
# Allow next invocation (after tools complete) without waiting for driver ensure
|
|
136
|
+
st[:inflight] = false
|
|
137
|
+
payload.each do |call|
|
|
138
|
+
bus.emit VSM::Message.new(
|
|
139
|
+
kind: :tool_call,
|
|
140
|
+
payload: { tool: call[:name], args: call[:arguments] },
|
|
141
|
+
corr_id: call[:id],
|
|
142
|
+
meta: { session_id: session_id, tool: call[:name], turn_id: current_turn_id }
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
ensure
|
|
148
|
+
if ENV["VSM_DEBUG_STREAM"] == "1"
|
|
149
|
+
$stderr.puts "Intelligence: driver completed sid=#{session_id}; pending=#{st[:pending_tool_ids].size}; inflight->false"
|
|
150
|
+
end
|
|
151
|
+
st[:inflight] = false
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
st[:task] = task
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Return [descriptors:Array<VSM::Tool::Descriptor>, index Hash{name=>capsule}]
|
|
158
|
+
def tool_inventory(bus, session_id)
|
|
159
|
+
ops = bus.context[:operations_children] || {}
|
|
160
|
+
descriptors = []
|
|
161
|
+
index = {}
|
|
162
|
+
ops.each do |name, capsule|
|
|
163
|
+
next unless capsule.respond_to?(:tool_descriptor)
|
|
164
|
+
desc = capsule.tool_descriptor
|
|
165
|
+
next unless offer_tools?(session_id, desc)
|
|
166
|
+
descriptors << desc
|
|
167
|
+
index[desc.name] = capsule
|
|
168
|
+
end
|
|
169
|
+
[descriptors, index]
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -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,50 @@
|
|
|
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/meta"
|
|
24
|
+
|
|
25
|
+
require_relative "vsm/executors/fiber_executor"
|
|
26
|
+
require_relative "vsm/executors/thread_executor"
|
|
27
|
+
|
|
28
|
+
require_relative "vsm/capsule"
|
|
29
|
+
require_relative "vsm/dsl"
|
|
30
|
+
require_relative "vsm/port"
|
|
31
|
+
require_relative "vsm/runtime"
|
|
32
|
+
|
|
33
|
+
require_relative "vsm/drivers/openai/async_driver"
|
|
34
|
+
require_relative "vsm/drivers/anthropic/async_driver"
|
|
35
|
+
require_relative "vsm/drivers/gemini/async_driver"
|
|
36
|
+
require_relative "vsm/drivers/family"
|
|
37
|
+
|
|
38
|
+
require_relative "vsm/lens"
|
|
39
|
+
|
|
40
|
+
# Optional/built-in ports and MCP integration
|
|
41
|
+
require_relative "vsm/ports/chat_tty"
|
|
42
|
+
require_relative "vsm/ports/mcp/server_stdio"
|
|
43
|
+
require_relative "vsm/mcp/jsonrpc"
|
|
44
|
+
require_relative "vsm/mcp/client"
|
|
45
|
+
require_relative "vsm/mcp/remote_tool_capsule"
|
|
46
|
+
require_relative "vsm/dsl_mcp"
|
|
47
|
+
|
|
5
48
|
module Vsm
|
|
6
49
|
class Error < StandardError; end
|
|
7
50
|
# Your code goes here...
|