samagotchi 0.65.0 → 0.67.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/CHANGELOG.md +107 -1
- data/README.md +2 -1
- data/bin/chi +51 -2
- data/docs/architecture.md +50 -28
- data/docs/cli.md +14 -2
- data/docs/configuration.md +101 -16
- data/docs/context.md +39 -2
- data/docs/desktop.md +90 -22
- data/docs/guardrails.md +8 -5
- data/docs/hooks.md +2 -2
- data/docs/internals/anthropic.md +156 -0
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bundles.md +2 -0
- data/docs/internals/engine.md +75 -30
- data/docs/internals/gemma4-contract.md +1 -1
- data/docs/internals/llm-context-bench.md +5 -4
- data/docs/internals/prompt-caching.md +36 -13
- data/docs/memory.md +1 -1
- data/docs/plugins.md +289 -25
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +16 -6
- data/docs/sub-agent.md +6 -1
- data/docs/testing.md +17 -3
- data/lib/samagotchi/bootstrap/probe.rb +4 -3
- data/lib/samagotchi/bootstrap_command.rb +13 -3
- data/lib/samagotchi/bridge/ring_buffer.rb +0 -5
- data/lib/samagotchi/bridge.rb +6 -3
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +7 -6
- data/lib/samagotchi/bundles/claude-peer/plugin.rb +40 -302
- data/lib/samagotchi/bundles/claude-peer/scripts/chi-peer-SKILL.md +8 -3
- data/lib/samagotchi/bundles/claude-peer/skill_claude_peer.md +14 -6
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +22 -0
- data/lib/samagotchi/bundles/codex-peer/plugin.rb +189 -0
- data/lib/samagotchi/bundles/codex-peer/scripts/chi-peer-SKILL.md +136 -0
- data/lib/samagotchi/bundles/codex-peer/scripts/pair +92 -0
- data/lib/samagotchi/bundles/codex-peer/skill_codex_peer.md +76 -0
- data/lib/samagotchi/bundles/coordinator/manifest.yml +5 -5
- data/lib/samagotchi/bundles/coordinator/plugin.rb +24 -2
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -1
- data/lib/samagotchi/bundles/mcp/manifest.yml +5 -5
- data/lib/samagotchi/bundles/mcp/plugin.rb +82 -294
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +7 -3
- data/lib/samagotchi/bundles/system/delegated.md +2 -1
- data/lib/samagotchi/bundles/system/manifest.yml +5 -5
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/cache_confirm.rb +9 -6
- data/lib/samagotchi/commands/registry.rb +25 -8
- data/lib/samagotchi/config.rb +23 -8
- data/lib/samagotchi/context_absorber.rb +66 -13
- data/lib/samagotchi/desktop/macos/AgentTerminal.swift +260 -0
- data/lib/samagotchi/desktop/macos/App.swift +3 -3
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +5 -2
- data/lib/samagotchi/desktop/macos/Images.swift +2 -2
- data/lib/samagotchi/desktop/macos/Kitty.swift +33 -197
- data/lib/samagotchi/desktop/macos/Panel.swift +118 -82
- data/lib/samagotchi/desktop/macos/ProcessRunner.swift +1 -1
- data/lib/samagotchi/desktop/macos/Tmux.swift +239 -0
- data/lib/samagotchi/desktop/macos.rb +84 -13
- data/lib/samagotchi/desktop_command.rb +14 -3
- data/lib/samagotchi/engine.rb +120 -107
- data/lib/samagotchi/engine_peers.rb +3 -0
- data/lib/samagotchi/guardrail_wiring.rb +1 -1
- data/lib/samagotchi/guardrails/approval.rb +1 -1
- data/lib/samagotchi/guardrails/approvals.rb +1 -1
- data/lib/samagotchi/guardrails/rules.rb +1 -1
- data/lib/samagotchi/guardrails/targets.rb +1 -1
- data/lib/samagotchi/hook_routes.rb +151 -0
- data/lib/samagotchi/hooks/registry.rb +2 -2
- data/lib/samagotchi/host_keys.rb +147 -0
- data/lib/samagotchi/kernel_loop.rb +9 -1
- data/lib/samagotchi/llm/adapter_common.rb +138 -0
- data/lib/samagotchi/llm/anthropic_assembly.rb +4 -2
- data/lib/samagotchi/llm/anthropic_messages.rb +28 -105
- data/lib/samagotchi/llm/api_key.rb +23 -7
- data/lib/samagotchi/llm/chat_loop.rb +10 -6
- data/lib/samagotchi/llm/openai_chat.rb +9 -92
- data/lib/samagotchi/llm/refusal_memo.rb +29 -0
- data/lib/samagotchi/llm_context_override.rb +1 -0
- data/lib/samagotchi/peer_sources.rb +58 -0
- data/lib/samagotchi/plugin/api.rb +138 -156
- data/lib/samagotchi/plugin/context.rb +22 -1
- data/lib/samagotchi/plugin/loader.rb +5 -2
- data/lib/samagotchi/plugin/peer.rb +394 -0
- data/lib/samagotchi/plugin/tool_spec.rb +166 -0
- data/lib/samagotchi/plugin_tasks.rb +2 -2
- data/lib/samagotchi/row_refs.rb +1 -1
- data/lib/samagotchi/self_report.rb +5 -3
- data/lib/samagotchi/send_command.rb +41 -15
- data/lib/samagotchi/session.rb +18 -1
- data/lib/samagotchi/session_commands.rb +16 -1
- data/lib/samagotchi/session_continue.rb +58 -15
- data/lib/samagotchi/session_inbox.rb +0 -13
- data/lib/samagotchi/session_manager.rb +4 -1
- data/lib/samagotchi/session_setup.rb +14 -7
- data/lib/samagotchi/system_prompt.rb +16 -1
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +19 -9
- data/lib/samagotchi/terminal_ui.rb +18 -2
- data/lib/samagotchi/tool_call_parser.rb +1 -1
- data/lib/samagotchi/tool_runner.rb +13 -1
- data/lib/samagotchi/toolkit.rb +246 -0
- data/lib/samagotchi/toolkit_binding.rb +138 -0
- data/lib/samagotchi/toolkit_command.rb +117 -0
- data/lib/samagotchi/tools/context_read.rb +14 -0
- data/lib/samagotchi/tools/execute.rb +12 -12
- data/lib/samagotchi/tools/registry.rb +43 -6
- data/lib/samagotchi/tools/task_create.rb +6 -6
- data/lib/samagotchi/tools/task_get.rb +13 -13
- data/lib/samagotchi/tools/task_list.rb +6 -6
- data/lib/samagotchi/tools/task_record.rb +52 -0
- data/lib/samagotchi/tools/task_runtime.rb +55 -91
- data/lib/samagotchi/tools/task_stop.rb +6 -6
- data/lib/samagotchi/tools/task_wait.rb +12 -13
- data/lib/samagotchi/tools/tool_index.rb +454 -0
- data/lib/samagotchi/update_command.rb +9 -6
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +28 -9
- data/lib/samagotchi/web/capabilities.rb +1 -1
- data/lib/samagotchi/web/public/activity.js +2 -2
- data/lib/samagotchi/web/public/annotate_actions.js +109 -0
- data/lib/samagotchi/web/public/annotate_bar.js +84 -2
- data/lib/samagotchi/web/public/app.js +52 -43
- data/lib/samagotchi/web/public/chains.js +1 -1
- data/lib/samagotchi/web/public/commands.js +6 -4
- data/lib/samagotchi/web/public/composer_size.js +2 -2
- data/lib/samagotchi/web/public/copy.js +1 -1
- data/lib/samagotchi/web/public/ctx.js +1 -1
- data/lib/samagotchi/web/public/delivery_line.js +4 -4
- data/lib/samagotchi/web/public/families_view.js +1 -1
- data/lib/samagotchi/web/public/format.js +11 -7
- data/lib/samagotchi/web/public/images.js +25 -2
- data/lib/samagotchi/web/public/index.html +9 -0
- data/lib/samagotchi/web/public/info_bar.js +5 -3
- data/lib/samagotchi/web/public/llm_context_chip.js +2 -2
- data/lib/samagotchi/web/public/notify.js +6 -4
- data/lib/samagotchi/web/public/question_card.js +1 -1
- data/lib/samagotchi/web/public/ref_scan.js +1 -1
- data/lib/samagotchi/web/public/session_state.js +17 -0
- data/lib/samagotchi/web/public/session_title.js +2 -3
- data/lib/samagotchi/web/public/sessions_list.js +0 -8
- data/lib/samagotchi/web/public/stage_model.js +2 -2
- data/lib/samagotchi/web/public/stream_handlers.js +13 -0
- data/lib/samagotchi/web/public/strip.js +3 -3
- data/lib/samagotchi/web/public/task_stop.js +3 -2
- data/lib/samagotchi/web/public/toolkit_chip.js +29 -0
- data/lib/samagotchi/web/public/turn_events.js +1 -1
- data/lib/samagotchi/web/public/turn_html.js +2 -2
- data/lib/samagotchi/web/public/turn_model.js +2 -2
- data/lib/samagotchi/web/public/turn_view.js +1 -1
- data/lib/samagotchi/web/server.rb +0 -4
- data/lib/samagotchi/worker.rb +13 -8
- data/lib/samagotchi/worker_inbound.rb +36 -4
- data/lib/samagotchi/worker_wakes.rb +67 -7
- metadata +23 -1
|
@@ -11,6 +11,7 @@ require_relative "../version"
|
|
|
11
11
|
require_relative "../self_report"
|
|
12
12
|
require_relative "../installed_gem"
|
|
13
13
|
require_relative "../config"
|
|
14
|
+
require_relative "../host_keys"
|
|
14
15
|
require_relative "../redacted_inspect"
|
|
15
16
|
|
|
16
17
|
module Samagotchi
|
|
@@ -48,6 +49,9 @@ module Samagotchi
|
|
|
48
49
|
# top of it (MacOS.broadcast_timeout).
|
|
49
50
|
BROADCAST_TRIAGE_DEADLINE = 20
|
|
50
51
|
BROADCAST_MARGIN = 10
|
|
52
|
+
# The agent terminals the helper pastes into (AgentTerminal.swift):
|
|
53
|
+
# one launch-file section each, in this order.
|
|
54
|
+
TERMINALS = %w[kitty tmux].freeze
|
|
51
55
|
|
|
52
56
|
# Runs a command without a shell; [output, success].
|
|
53
57
|
class Runner
|
|
@@ -69,10 +73,36 @@ module Samagotchi
|
|
|
69
73
|
{ "listen_on" => listen_on, "binary" => get.call("kitty.binary").to_s, "agents" => agents }
|
|
70
74
|
end
|
|
71
75
|
|
|
76
|
+
# The tmux: settings, or nil while tmux.enabled is off. The binary is
|
|
77
|
+
# tmux.binary, else the tmux on +env+'s PATH ("" when there is none:
|
|
78
|
+
# see #warnings); the socket dir is where tmux puts `-L` sockets
|
|
79
|
+
# ($TMUX_TMPDIR when it is a directory, else /tmp, then tmux-<uid>).
|
|
80
|
+
# Both are frozen at bake time, like the rest of the launch file.
|
|
81
|
+
def self.tmux_settings(get = Config.method(:get), env: ENV)
|
|
82
|
+
return nil unless get.call("tmux.enabled")
|
|
83
|
+
|
|
84
|
+
binary = get.call("tmux.binary").to_s.strip
|
|
85
|
+
binary = path_tmux(env) if binary.empty?
|
|
86
|
+
tmpdir = env["TMUX_TMPDIR"].to_s
|
|
87
|
+
tmpdir = "/tmp" if tmpdir.empty? || !File.directory?(tmpdir)
|
|
88
|
+
agents = get.call("tmux.agents").to_s.split("|").map(&:strip).reject(&:empty?)
|
|
89
|
+
{ "binary" => binary, "socket_dir" => File.join(tmpdir, "tmux-#{Process.uid}"), "agents" => agents }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def self.path_tmux(env)
|
|
93
|
+
env["PATH"].to_s.split(File::PATH_SEPARATOR).reject(&:empty?).map { File.join(_1, "tmux") }
|
|
94
|
+
.find { File.file?(_1) && File.executable?(_1) }.to_s
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Each switched-on terminal's settings by name ("kitty" => …).
|
|
98
|
+
def self.terminal_settings(get = Config.method(:get), env: ENV)
|
|
99
|
+
{ "kitty" => kitty_settings(get), "tmux" => tmux_settings(get, env: env) }.compact
|
|
100
|
+
end
|
|
101
|
+
|
|
72
102
|
# The helper as chi desktop and chi update make it: the settings it
|
|
73
103
|
# bakes into the launch file read from the config.
|
|
74
104
|
def self.from_config(register: true)
|
|
75
|
-
new(register: register,
|
|
105
|
+
new(register: register, terminals: terminal_settings, broadcast_timeout: broadcast_timeout)
|
|
76
106
|
end
|
|
77
107
|
|
|
78
108
|
# Seconds the helper gives `chi broadcast` before it stops it: the
|
|
@@ -86,7 +116,7 @@ module Samagotchi
|
|
|
86
116
|
|
|
87
117
|
def initialize(app_dir: nil, support_dir: nil, env: ENV, runner: Runner.new,
|
|
88
118
|
source_dir: SelfReport::SOURCE_DIR, ruby: RbConfig.ruby, version: VERSION,
|
|
89
|
-
arch: nil, sources_dir: SOURCES_DIR, register: true, chi_path: nil,
|
|
119
|
+
arch: nil, sources_dir: SOURCES_DIR, register: true, chi_path: nil, terminals: {},
|
|
90
120
|
broadcast_timeout: nil)
|
|
91
121
|
home = env["HOME"] || Dir.home
|
|
92
122
|
@app_dir = app_dir || File.join(home, "Applications")
|
|
@@ -100,7 +130,7 @@ module Samagotchi
|
|
|
100
130
|
@arch = arch
|
|
101
131
|
@sources_dir = sources_dir
|
|
102
132
|
@register = register
|
|
103
|
-
@
|
|
133
|
+
@terminals = terminals
|
|
104
134
|
@broadcast_timeout = broadcast_timeout
|
|
105
135
|
end
|
|
106
136
|
|
|
@@ -114,13 +144,13 @@ module Samagotchi
|
|
|
114
144
|
# What the helper runs: absolute ruby + chi (no shims, which need the
|
|
115
145
|
# shell's PATH), and the allowlisted env. chi is an installed gem's
|
|
116
146
|
# RubyGems wrapper (it activates the gem and outlives upgrades and
|
|
117
|
-
# `gem cleanup`), else this checkout's bin/chi.
|
|
118
|
-
#
|
|
147
|
+
# `gem cleanup`), else this checkout's bin/chi. A section per terminal
|
|
148
|
+
# switched on (TERMINALS order); the broadcast timeout when given.
|
|
119
149
|
def launch_config
|
|
120
150
|
env = { "LANG" => LANG }
|
|
121
151
|
ENV_ALLOWLIST.each { |name| env[name] = @env[name] unless @env[name].to_s.empty? }
|
|
122
152
|
config = { "version" => @version, "argv" => [@ruby, @chi_path], "env" => env, "sources_sha" => sources_sha }
|
|
123
|
-
config[
|
|
153
|
+
TERMINALS.each { |name| config[name] = @terminals[name] if @terminals[name] }
|
|
124
154
|
config["broadcast_timeout"] = @broadcast_timeout if @broadcast_timeout
|
|
125
155
|
config
|
|
126
156
|
end
|
|
@@ -145,11 +175,12 @@ module Samagotchi
|
|
|
145
175
|
end
|
|
146
176
|
|
|
147
177
|
# The launch file names another chi version or path than this one, or
|
|
148
|
-
# other
|
|
149
|
-
# that without a rebuild.
|
|
178
|
+
# other terminal settings or broadcast timeout: refresh_launch_file
|
|
179
|
+
# fixes that without a rebuild.
|
|
150
180
|
def launch_outdated?
|
|
151
181
|
launch = read_launch
|
|
152
|
-
launch["version"] != @version || Array(launch["argv"]) != launch_config["argv"] ||
|
|
182
|
+
launch["version"] != @version || Array(launch["argv"]) != launch_config["argv"] ||
|
|
183
|
+
TERMINALS.map { launch[_1] } != TERMINALS.map { @terminals[_1] } ||
|
|
153
184
|
launch["broadcast_timeout"] != @broadcast_timeout
|
|
154
185
|
end
|
|
155
186
|
|
|
@@ -163,10 +194,41 @@ module Samagotchi
|
|
|
163
194
|
end
|
|
164
195
|
|
|
165
196
|
def warnings
|
|
166
|
-
|
|
197
|
+
worktree = if File.file?(File.join(@source_dir, ".git"))
|
|
198
|
+
["chi runs from a linked git worktree (#{@source_dir}); the helper stops working once it is removed. " \
|
|
199
|
+
"Install from the main checkout or a gem install."]
|
|
200
|
+
end
|
|
201
|
+
Array(worktree) + terminal_warnings + host_key_warnings
|
|
202
|
+
end
|
|
167
203
|
|
|
168
|
-
|
|
169
|
-
|
|
204
|
+
# What a terminal section can't do as baked (chi update shows these
|
|
205
|
+
# too: they need no Keychain lookup).
|
|
206
|
+
def terminal_warnings
|
|
207
|
+
tmux = @terminals["tmux"]
|
|
208
|
+
return [] unless tmux && tmux["binary"].to_s.empty?
|
|
209
|
+
|
|
210
|
+
["tmux.enabled but no tmux found on PATH: set tmux.binary"]
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Each configured host key (api_key_env:) and whether the helper's chi
|
|
214
|
+
# gets it: the helper's bare env has no keys, so only the Keychain's
|
|
215
|
+
# count (HostKeys). :keychain, :shell_only (set in this shell, not in
|
|
216
|
+
# the Keychain) or :unset. Hosts come from the config the helper's chi
|
|
217
|
+
# reads: the launch file's XDG_CONFIG_HOME when baked; disabled hosts
|
|
218
|
+
# are left out. @return [Array<Hash>] name:, hosts:, state:
|
|
219
|
+
def host_keys
|
|
220
|
+
baked = read_launch["env"]
|
|
221
|
+
dir = baked["XDG_CONFIG_HOME"] if baked.is_a?(Hash)
|
|
222
|
+
# Spelled out: Paths.config_home's ~ is this process's HOME, not @env's.
|
|
223
|
+
config_env = { "XDG_CONFIG_HOME" => dir.to_s.empty? ? File.join(@env["HOME"] || Dir.home, ".config") : dir }
|
|
224
|
+
hosts = ConfigFile.hosts_config(env: config_env).values.select(&:api_key_env).group_by(&:api_key_env)
|
|
225
|
+
hosts.map do |name, entries|
|
|
226
|
+
state = if HostKeys.from_keychain(name, env: @env) then :keychain
|
|
227
|
+
elsif !@env[name].to_s.strip.empty? then :shell_only
|
|
228
|
+
else :unset
|
|
229
|
+
end
|
|
230
|
+
{ name: name, hosts: entries.map(&:name), state: state }
|
|
231
|
+
end
|
|
170
232
|
end
|
|
171
233
|
|
|
172
234
|
def info_plist
|
|
@@ -248,7 +310,8 @@ module Samagotchi
|
|
|
248
310
|
stale: stale?,
|
|
249
311
|
baked_dirs: env.select { |name, _| name.start_with?("XDG_") },
|
|
250
312
|
service: dump.include?(BUNDLE_ID),
|
|
251
|
-
running: running
|
|
313
|
+
running: running,
|
|
314
|
+
host_keys: host_keys
|
|
252
315
|
)
|
|
253
316
|
end
|
|
254
317
|
|
|
@@ -311,6 +374,14 @@ module Samagotchi
|
|
|
311
374
|
@arch ||= RbConfig::CONFIG["host_cpu"] == "arm64" ? "arm64" : `uname -m`.strip
|
|
312
375
|
end
|
|
313
376
|
|
|
377
|
+
def host_key_warnings
|
|
378
|
+
host_keys.reject { |key| key[:state] == :keychain }.map do |key|
|
|
379
|
+
where = key[:state] == :shell_only ? "is set in this shell only" : "is unset"
|
|
380
|
+
"#{key[:name]} #{where}: the helper's new sessions and model list won't reach host " \
|
|
381
|
+
"#{key[:hosts].join(", ")} until it is in the Keychain (#{HostKeys.store_command(key[:name])})"
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
314
385
|
def read_launch
|
|
315
386
|
launch = JSON.parse(File.read(launch_path))
|
|
316
387
|
launch.is_a?(Hash) ? launch : {}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "cli/command"
|
|
4
4
|
require_relative "cli/flags"
|
|
5
5
|
require_relative "desktop"
|
|
6
|
+
require_relative "host_keys"
|
|
6
7
|
|
|
7
8
|
module Samagotchi
|
|
8
9
|
# `chi desktop install|upgrade|uninstall|status`: the native helper that
|
|
@@ -121,12 +122,22 @@ module Samagotchi
|
|
|
121
122
|
dirs = s[:baked_dirs].empty? ? "(defaults)" : s[:baked_dirs].map { |name, value| "#{name}=#{value}" }.join(" ")
|
|
122
123
|
service = s[:service] ? "registered" : "not registered (try chi desktop upgrade, or log out and back in)"
|
|
123
124
|
running = s[:running] ? "running" : "not running (open -g \"#{s[:app_path]}\")"
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
rows = [["Chi Helper", version], ["app", s[:app_path]], ["runs chi", launch], ["state dirs", dirs],
|
|
126
|
+
["service", service], ["hotkey", hotkey(s[:hotkey])], ["process", running], ["at login", login(s[:login])]]
|
|
127
|
+
rows << ["host keys", host_keys(s[:host_keys])] unless Array(s[:host_keys]).empty?
|
|
128
|
+
@stdout.puts(format_rows(rows))
|
|
127
129
|
0
|
|
128
130
|
end
|
|
129
131
|
|
|
132
|
+
# `VAR keychain · VAR this shell only (security …)`: what the helper's chi gets.
|
|
133
|
+
def host_keys(keys)
|
|
134
|
+
keys.map do |key|
|
|
135
|
+
next "#{key[:name]} keychain" if key[:state] == :keychain
|
|
136
|
+
|
|
137
|
+
"#{key[:name]} #{key[:state] == :shell_only ? "this shell only" : "unset"} (#{HostKeys.store_command(key[:name])})"
|
|
138
|
+
end.join(" · ")
|
|
139
|
+
end
|
|
140
|
+
|
|
130
141
|
def hotkey(state)
|
|
131
142
|
return "unknown (the app writes it when it starts)" unless state
|
|
132
143
|
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -19,6 +19,8 @@ require_relative "context_window"
|
|
|
19
19
|
require_relative "kernel_loop"
|
|
20
20
|
require_relative "tools/builtins"
|
|
21
21
|
require_relative "tools/task_runtime"
|
|
22
|
+
require_relative "tools/tool_index"
|
|
23
|
+
require_relative "peer_sources"
|
|
22
24
|
require_relative "session_commands"
|
|
23
25
|
require_relative "plugin/loader"
|
|
24
26
|
require_relative "extension_load"
|
|
@@ -31,6 +33,7 @@ require_relative "llm/backend"
|
|
|
31
33
|
require_relative "llm/openai_chat"
|
|
32
34
|
require_relative "llm/turn_settings"
|
|
33
35
|
require_relative "session"
|
|
36
|
+
require_relative "toolkit_binding"
|
|
34
37
|
require_relative "answer_tail"
|
|
35
38
|
require_relative "archive_store"
|
|
36
39
|
require_relative "session_observer"
|
|
@@ -43,6 +46,7 @@ require_relative "recap_setup"
|
|
|
43
46
|
require_relative "idle_reminders"
|
|
44
47
|
require_relative "idle_scheduler"
|
|
45
48
|
require_relative "hooks"
|
|
49
|
+
require_relative "hook_routes"
|
|
46
50
|
require_relative "guardrails"
|
|
47
51
|
require_relative "reminder_store"
|
|
48
52
|
require_relative "reminder_queue"
|
|
@@ -145,9 +149,13 @@ module Samagotchi
|
|
|
145
149
|
begin
|
|
146
150
|
require_relative "memory_bundle/system_bundle"
|
|
147
151
|
MemoryBundle::SystemBundle.ensure!
|
|
148
|
-
rescue StandardError
|
|
149
|
-
|
|
152
|
+
rescue StandardError => e
|
|
153
|
+
Log.warn(:memory, "system_bundle_ensure_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
150
154
|
end
|
|
155
|
+
# What hooks and plugins do beyond reading their event (a notice, a
|
|
156
|
+
# question, a stop, a steer), routed to the UIs. Built before anything
|
|
157
|
+
# that may notify.
|
|
158
|
+
@hook_routes = build_hook_routes
|
|
151
159
|
# Load hooks from config (plugins) and create the registry; what fails
|
|
152
160
|
# to load is announced, and a required guardrail's failure denies
|
|
153
161
|
# every tool call. Rules load now too, so their errors are announced.
|
|
@@ -184,6 +192,15 @@ module Samagotchi
|
|
|
184
192
|
# plugins' tool sets from chi.replace_tools until the turn thread
|
|
185
193
|
# applies them (#apply_staged_tools!). Built before the plugins load.
|
|
186
194
|
@plugin_tasks = build_plugin_tasks
|
|
195
|
+
# The session's toolkit: the kits (the plugins' too, chi.toolkit),
|
|
196
|
+
# what its next turn declares, the turn-start apply.
|
|
197
|
+
@toolkit_binding = build_toolkit_binding
|
|
198
|
+
# The tools plugins publish without declaring (chi.tool_source), and
|
|
199
|
+
# the search and call tools that reach them (Tools::ToolIndex).
|
|
200
|
+
@tool_index = Tools::ToolIndex.new
|
|
201
|
+
# The context sources the peer bundles declare as a peer agent's
|
|
202
|
+
# answers (chi.peer_source); the worker's ContextAbsorber asks it.
|
|
203
|
+
@peer_sources = PeerSources.new
|
|
187
204
|
@lifecycle_mutex = Mutex.new
|
|
188
205
|
@shut_down = false
|
|
189
206
|
load_plugins if plugins
|
|
@@ -447,7 +464,8 @@ module Samagotchi
|
|
|
447
464
|
|
|
448
465
|
log_dropped_steers(steers, "answered")
|
|
449
466
|
[]
|
|
450
|
-
rescue StandardError
|
|
467
|
+
rescue StandardError => e
|
|
468
|
+
Log.warn(:turn, "take_steers_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
451
469
|
[]
|
|
452
470
|
end
|
|
453
471
|
private :take_steers
|
|
@@ -548,7 +566,8 @@ module Samagotchi
|
|
|
548
566
|
return saved.dup if !@prompt_builder.prompt_notes.nil? || !saved.empty? || @session.messages.to_a.any?
|
|
549
567
|
|
|
550
568
|
model_notes.map { |note| PromptNote.from_note(note) }
|
|
551
|
-
rescue StandardError
|
|
569
|
+
rescue StandardError => e
|
|
570
|
+
Log.warn(:model, "prompt_notes_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
552
571
|
[]
|
|
553
572
|
end
|
|
554
573
|
|
|
@@ -613,6 +632,9 @@ module Samagotchi
|
|
|
613
632
|
# built-ins, and the ones bundle plugins add
|
|
614
633
|
attr_reader :command_registry
|
|
615
634
|
|
|
635
|
+
# @return [PeerSources] the sources the peer bundles declared
|
|
636
|
+
attr_reader :peer_sources
|
|
637
|
+
|
|
616
638
|
# The model id sent for +full_ref+ (its alias applied, the host prefix off).
|
|
617
639
|
def bare_model_name(full_ref)
|
|
618
640
|
@host_registry.bare_name(full_ref)
|
|
@@ -1295,6 +1317,7 @@ module Samagotchi
|
|
|
1295
1317
|
llm_context: llm_context_summary,
|
|
1296
1318
|
thinking: thinking_explained&.summary,
|
|
1297
1319
|
reasoning_history: reasoning_history_explained&.summary,
|
|
1320
|
+
toolkit: toolkit_summary,
|
|
1298
1321
|
recap_enabled: !@recap.nil?,
|
|
1299
1322
|
recap_min_user_turns: @recap&.min_user_turns,
|
|
1300
1323
|
recap_inactivity_seconds: @recap&.inactivity&.to_i
|
|
@@ -1489,7 +1512,8 @@ module Samagotchi
|
|
|
1489
1512
|
|
|
1490
1513
|
def save_quietly(session)
|
|
1491
1514
|
session.save(state_dir: session_state_dir)
|
|
1492
|
-
rescue StandardError
|
|
1515
|
+
rescue StandardError => e
|
|
1516
|
+
Log.warn(:turn, "turn_end_save_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
1493
1517
|
nil
|
|
1494
1518
|
end
|
|
1495
1519
|
private :save_quietly
|
|
@@ -1527,121 +1551,59 @@ module Samagotchi
|
|
|
1527
1551
|
end
|
|
1528
1552
|
private :store_answer_display
|
|
1529
1553
|
|
|
1530
|
-
# ── The hook runtime (
|
|
1531
|
-
|
|
1532
|
-
#
|
|
1533
|
-
#
|
|
1534
|
-
def hook_runtime
|
|
1535
|
-
Hooks::Runtime.new(
|
|
1536
|
-
notify: ->(text:, level:, hook:, fallback_for: nil) { hook_notify(text, level, hook, fallback_for: fallback_for) },
|
|
1537
|
-
ask_user: ->(question:, options:, header:, allow_freeform:, hook:) { hook_ask_user(question, options, header, allow_freeform, hook) },
|
|
1538
|
-
stop_turn: ->(reason:, hook:) { hook_stop_turn(reason, hook) },
|
|
1539
|
-
steer: ->(text:, hook:) { steer(text, source: hook_source(hook)) },
|
|
1540
|
-
stop_generation: ->(reason:, hook:) { hook_stop_generation(reason, hook) }
|
|
1541
|
-
)
|
|
1542
|
-
end
|
|
1554
|
+
# ── The hook runtime (HookRoutes) ───────────────────────────────────────
|
|
1555
|
+
|
|
1556
|
+
# What a hook (Hooks::Runtime) and a plugin (Plugin::Host) can do beyond
|
|
1557
|
+
# reading its event, routed to the UIs by HookRoutes; these forward.
|
|
1558
|
+
def hook_runtime = @hook_routes.runtime
|
|
1543
1559
|
private :hook_runtime
|
|
1544
1560
|
|
|
1545
|
-
|
|
1546
|
-
# ("<file> (bundle <name>)"), the label of any other.
|
|
1547
|
-
def hook_source(hook)
|
|
1548
|
-
hook.to_s[/\(bundle (.+)\)\z/, 1] || hook.to_s
|
|
1549
|
-
end
|
|
1561
|
+
def hook_source(hook) = @hook_routes.source(hook)
|
|
1550
1562
|
private :hook_source
|
|
1551
1563
|
|
|
1552
1564
|
# What a notice may stand in for (hook_notify's fallback_for:).
|
|
1553
|
-
NOTICE_FALLBACKS =
|
|
1554
|
-
|
|
1555
|
-
# One line to the user (:hook_notice). During a turn it is a turn
|
|
1556
|
-
# event: the turn's sink (the REPL) and the observers (bridge, log).
|
|
1557
|
-
# Outside one (a plugin's command at the prompt) it is announced with
|
|
1558
|
-
# between_turns: true, which every UI shows as cards are shown.
|
|
1559
|
-
#
|
|
1560
|
-
# +fallback_for+ names what the line stands in for (NOTICE_FALLBACKS):
|
|
1561
|
-
# :display, the answer's display (AnswerDisplay), for a UI that doesn't
|
|
1562
|
-
# render its links; :editor, a ref the viewer could open in its editor
|
|
1563
|
-
# (Web::Capabilities#editor), for a UI or a viewer that can't. A UI that
|
|
1564
|
-
# does leaves the line out; the others, the log among them, show it. An
|
|
1565
|
-
# unknown value is dropped, so the line shows everywhere.
|
|
1566
|
-
def hook_notify(text, level, hook, fallback_for: nil)
|
|
1567
|
-
level = (level || :info).to_sym
|
|
1568
|
-
level = :info unless %i[info warn].include?(level)
|
|
1569
|
-
notice = { type: :hook_notice, hook: hook.to_s, text: text.to_s, level: level,
|
|
1570
|
-
fallback_for: notice_fallback(fallback_for, hook) }.compact
|
|
1571
|
-
return @extension_load.hold(notice) && nil if @extension_load.loading?
|
|
1565
|
+
NOTICE_FALLBACKS = HookRoutes::NOTICE_FALLBACKS
|
|
1572
1566
|
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
elsif current_init_task
|
|
1577
|
-
announce(notice.merge(between_turns: true))
|
|
1578
|
-
elsif in_turn
|
|
1579
|
-
emit_event(sink, notice)
|
|
1580
|
-
else
|
|
1581
|
-
announce_or_hold(notice.merge(between_turns: true))
|
|
1582
|
-
end
|
|
1583
|
-
nil
|
|
1567
|
+
# One line to the user (:hook_notice), HookRoutes#notify.
|
|
1568
|
+
def hook_notify(text, level, hook, fallback_for: nil, extra: {})
|
|
1569
|
+
@hook_routes.notify(text, level, hook, fallback_for: fallback_for, extra: extra)
|
|
1584
1570
|
end
|
|
1585
1571
|
private :hook_notify
|
|
1586
1572
|
|
|
1587
|
-
|
|
1588
|
-
# one is logged once at debug.
|
|
1589
|
-
def notice_fallback(value, hook)
|
|
1590
|
-
return if value.nil?
|
|
1591
|
-
|
|
1592
|
-
fallback = value.to_s.to_sym
|
|
1593
|
-
return fallback if NOTICE_FALLBACKS.include?(fallback)
|
|
1594
|
-
|
|
1595
|
-
@unknown_fallbacks ||= Set.new
|
|
1596
|
-
Log.debug(:hooks, "notice_unknown_fallback", hook: hook.to_s, value: value.to_s) if @unknown_fallbacks.add?(fallback)
|
|
1597
|
-
nil
|
|
1598
|
-
end
|
|
1599
|
-
private :notice_fallback
|
|
1600
|
-
|
|
1601
|
-
# A guardrails line to the user after load (:guardrail_warning, as the
|
|
1602
|
-
# load failures show): a running turn's event, else announced.
|
|
1603
|
-
def guardrail_notify(message)
|
|
1604
|
-
event = { type: :guardrail_warning, message: message.to_s }
|
|
1605
|
-
in_turn, sink = @turn_state.in_turn_sink
|
|
1606
|
-
in_turn ? emit_event(sink, event) : announce_or_hold(event)
|
|
1607
|
-
nil
|
|
1608
|
-
end
|
|
1573
|
+
def guardrail_notify(message) = @hook_routes.guardrail_notify(message)
|
|
1609
1574
|
private :guardrail_notify
|
|
1610
1575
|
|
|
1611
|
-
# A question through the question flow (REPL sync handler, attached TUI,
|
|
1612
|
-
# web), single-select, kind "hook". A --non-interactive run has no one
|
|
1613
|
-
# to ask: nil at once. Anything but an answer (a sync handler's text, no
|
|
1614
|
-
# answer, cancelled) is nil too.
|
|
1615
1576
|
# @return [Hash, nil] {selected:, freeform:, selected_indices:}
|
|
1616
1577
|
def hook_ask_user(question, options, header, allow_freeform, hook)
|
|
1617
|
-
@
|
|
1578
|
+
@hook_routes.ask_user(question, options, header, allow_freeform, hook)
|
|
1618
1579
|
end
|
|
1619
1580
|
private :hook_ask_user
|
|
1620
1581
|
|
|
1621
|
-
# Cancel the running turn (reason :hook), after a notice that says why.
|
|
1622
|
-
# The gate denies the rest of a tool batch once the controller is
|
|
1623
|
-
# cancelled; the next request ends the turn as :turn_canceled.
|
|
1624
1582
|
# @return [Boolean] true when a running turn was cancelled now
|
|
1625
|
-
def hook_stop_turn(reason, hook)
|
|
1626
|
-
ctrl = active_cancel_controller
|
|
1627
|
-
return false unless ctrl && !ctrl.cancelled?
|
|
1628
|
-
|
|
1629
|
-
hook_notify("stopped the turn: #{reason}", :warn, hook)
|
|
1630
|
-
ctrl.cancel!(:hook, { by: hook_source(hook), reason: reason.to_s })
|
|
1631
|
-
end
|
|
1583
|
+
def hook_stop_turn(reason, hook) = @hook_routes.stop_turn(reason, hook)
|
|
1632
1584
|
private :hook_stop_turn
|
|
1633
1585
|
|
|
1634
|
-
# Cut the streaming generation (reason :hook); the turn goes on and asks
|
|
1635
|
-
# the model again (the loops' cut path). No notice: the plugin posts its
|
|
1636
|
-
# own. The bundle and +reason+ go to the log and the retry's nudge.
|
|
1637
1586
|
# @return [Boolean] true when a streaming generation was cut now
|
|
1638
|
-
def hook_stop_generation(reason, hook)
|
|
1639
|
-
|
|
1640
|
-
return false unless ctrl
|
|
1587
|
+
def hook_stop_generation(reason, hook) = @hook_routes.stop_generation(reason, hook)
|
|
1588
|
+
private :hook_stop_generation
|
|
1641
1589
|
|
|
1642
|
-
|
|
1590
|
+
# The hook routes (see #initialize for when).
|
|
1591
|
+
def build_hook_routes
|
|
1592
|
+
HookRoutes.new(
|
|
1593
|
+
extension_load: -> { @extension_load },
|
|
1594
|
+
in_turn_sink: -> { @turn_state.in_turn_sink },
|
|
1595
|
+
anytime_thread: -> { anytime_thread? },
|
|
1596
|
+
announce_anytime: ->(event) { announce_anytime(event) },
|
|
1597
|
+
init_task: -> { current_init_task },
|
|
1598
|
+
announce: ->(event) { announce(event) },
|
|
1599
|
+
announce_or_hold: ->(event) { announce_or_hold(event) },
|
|
1600
|
+
emit: ->(sink, event) { emit_event(sink, event) },
|
|
1601
|
+
question_desk: -> { @question_desk },
|
|
1602
|
+
cancel_controller: -> { active_cancel_controller },
|
|
1603
|
+
steer: ->(text, source) { steer(text, source: source) }
|
|
1604
|
+
)
|
|
1643
1605
|
end
|
|
1644
|
-
private :
|
|
1606
|
+
private :build_hook_routes
|
|
1645
1607
|
|
|
1646
1608
|
# ── Ask-user-question (structured qualification) ──────────────────────────
|
|
1647
1609
|
|
|
@@ -2019,8 +1981,11 @@ module Samagotchi
|
|
|
2019
1981
|
# at its first request).
|
|
2020
1982
|
start_init_tasks!
|
|
2021
1983
|
await_init_tasks(turn.controller, turn.on_event)
|
|
2022
|
-
# Plugins' tool sets that changed since the last turn
|
|
1984
|
+
# Plugins' tool sets that changed since the last turn, then the
|
|
1985
|
+
# session's toolkit (a /toolkit change since the last turn): what the
|
|
1986
|
+
# prompt built next declares, unchanged until the next turn's start.
|
|
2023
1987
|
apply_staged_tools!
|
|
1988
|
+
apply_toolkit!(session, turn.on_event)
|
|
2024
1989
|
# A Stop during the wait above: the turn ends here,
|
|
2025
1990
|
# before the hooks run or a reminder is used up.
|
|
2026
1991
|
return false if turn.controller.cancelled?
|
|
@@ -2219,8 +2184,8 @@ module Samagotchi
|
|
|
2219
2184
|
# Persist deduped used memories onto the session for Web + reload.
|
|
2220
2185
|
begin
|
|
2221
2186
|
session.used_memory_names = used_memory_names
|
|
2222
|
-
rescue StandardError
|
|
2223
|
-
|
|
2187
|
+
rescue StandardError => e
|
|
2188
|
+
Log.warn(:memory, "used_memories_store_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
2224
2189
|
end
|
|
2225
2190
|
# Notify live observers of the updated memory list (so yellow bar refreshes
|
|
2226
2191
|
# even without a tool_call event if the preload was the only addition).
|
|
@@ -2229,8 +2194,8 @@ module Samagotchi
|
|
|
2229
2194
|
|
|
2230
2195
|
begin
|
|
2231
2196
|
emit_event(on_event, { type: :used_memories_updated, used_memory_names: used_memory_names })
|
|
2232
|
-
rescue StandardError
|
|
2233
|
-
|
|
2197
|
+
rescue StandardError => e
|
|
2198
|
+
Log.warn(:memory, "used_memories_emit_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
2234
2199
|
end
|
|
2235
2200
|
end
|
|
2236
2201
|
private :publish_used_memories
|
|
@@ -2666,12 +2631,17 @@ module Samagotchi
|
|
|
2666
2631
|
init: lambda { |bundle, label, plugin_label, provides_tools:, quiet:, timeout:, failed: nil, &block|
|
|
2667
2632
|
add_init_task(bundle: bundle, label: label, plugin_label: plugin_label, provides_tools: provides_tools,
|
|
2668
2633
|
quiet: quiet, timeout: timeout, failed: failed, &block)
|
|
2669
|
-
}
|
|
2634
|
+
},
|
|
2635
|
+
toolkits: @toolkit_binding.plugin_kits,
|
|
2636
|
+
tool_index: @tool_index,
|
|
2637
|
+
peer_sources: @peer_sources
|
|
2670
2638
|
)
|
|
2671
2639
|
# What plugins show while they load (an MCP server that didn't
|
|
2672
2640
|
# start) waits for the first turn, beside the load warnings: no UI
|
|
2673
2641
|
# is there yet, and the Engine isn't built.
|
|
2674
2642
|
@extension_load.load_plugins(registries, failures: @plugin_failures)
|
|
2643
|
+
# Declared once, from what the sources published while they loaded.
|
|
2644
|
+
@tool_index.declare(@tools, failures: @plugin_failures)
|
|
2675
2645
|
end
|
|
2676
2646
|
|
|
2677
2647
|
# Keep a plugin's new tool set (chi.replace_tools, from any thread)
|
|
@@ -2689,6 +2659,43 @@ module Samagotchi
|
|
|
2689
2659
|
end
|
|
2690
2660
|
public :apply_staged_tools!
|
|
2691
2661
|
|
|
2662
|
+
# The session's toolkit (ToolkitBinding): the kits, what its next turn
|
|
2663
|
+
# declares, ctx.use_toolkit and the turn-start apply.
|
|
2664
|
+
def toolkit_kits(errors = []) = @toolkit_binding.kits(errors)
|
|
2665
|
+
def resolved_toolkit(session, errors = []) = @toolkit_binding.resolved(session, errors)
|
|
2666
|
+
def toolkit_explained(toolkit = nil) = @toolkit_binding.explained(toolkit)
|
|
2667
|
+
def toolkit_summary = @toolkit_binding.summary
|
|
2668
|
+
|
|
2669
|
+
# The tools this session registered (Tools::Registry), declared or not.
|
|
2670
|
+
attr_reader :tools
|
|
2671
|
+
|
|
2672
|
+
public :tools, :toolkit_kits, :resolved_toolkit, :toolkit_explained, :toolkit_summary
|
|
2673
|
+
|
|
2674
|
+
# A plugin's ctx.use_toolkit (ToolkitBinding#use).
|
|
2675
|
+
# @return [Boolean] whether it was staged
|
|
2676
|
+
# @raise [ArgumentError] an unknown toolkit
|
|
2677
|
+
def use_toolkit(name) = @toolkit_binding.use(name)
|
|
2678
|
+
public :use_toolkit
|
|
2679
|
+
|
|
2680
|
+
# At a turn's start (ToolkitBinding#apply!).
|
|
2681
|
+
# @return [Boolean] whether the declared tools changed
|
|
2682
|
+
def apply_toolkit!(session, on_event = nil) = @toolkit_binding.apply!(session, on_event)
|
|
2683
|
+
private :apply_toolkit!
|
|
2684
|
+
|
|
2685
|
+
# The session's toolkit (see #initialize for when).
|
|
2686
|
+
def build_toolkit_binding
|
|
2687
|
+
ToolkitBinding.new(
|
|
2688
|
+
tools: -> { @tools },
|
|
2689
|
+
session: -> { @session },
|
|
2690
|
+
state_dir: -> { session_state_dir },
|
|
2691
|
+
can_stage: -> { !turn_running? && !history? },
|
|
2692
|
+
reset_prompt: -> { @prompt_builder&.reset! },
|
|
2693
|
+
emit: ->(sink, event) { emit_event(sink, event) },
|
|
2694
|
+
notify: ->(text, level, source, extra:) { hook_notify(text, level, source, extra: extra) }
|
|
2695
|
+
)
|
|
2696
|
+
end
|
|
2697
|
+
private :build_toolkit_binding
|
|
2698
|
+
|
|
2692
2699
|
# The tools changed (a plugin's chi.tools_changed!): the system prompts,
|
|
2693
2700
|
# which declare them, are built again on the next turn.
|
|
2694
2701
|
def tools_changed!
|
|
@@ -2715,6 +2722,9 @@ module Samagotchi
|
|
|
2715
2722
|
steer: ->(text, label) { steer(text, source: hook_source(label)) },
|
|
2716
2723
|
stop_turn: ->(reason, label) { hook_stop_turn(reason, label) },
|
|
2717
2724
|
stop_generation: ->(reason, label) { hook_stop_generation(reason, label) },
|
|
2725
|
+
toolkit: -> { @toolkit_binding.name },
|
|
2726
|
+
declared_tools: -> { toolkit_explained.declared.map(&:name) },
|
|
2727
|
+
use_toolkit: ->(name) { use_toolkit(name) },
|
|
2718
2728
|
ask_model: lambda { |request, timeout:, max_tokens:, cancel_controller:|
|
|
2719
2729
|
ask_side_model(request, timeout: timeout, max_tokens: max_tokens, cancel_controller: cancel_controller)
|
|
2720
2730
|
},
|
|
@@ -2865,7 +2875,8 @@ module Samagotchi
|
|
|
2865
2875
|
def with_generation_report(event)
|
|
2866
2876
|
report = @metrics.finish_generation
|
|
2867
2877
|
event.merge(speed: report.speed&.to_h, tokens: report.tokens, served_expected_by: served_expected_by(event[:served_model]))
|
|
2868
|
-
rescue StandardError
|
|
2878
|
+
rescue StandardError => e
|
|
2879
|
+
Log.warn(:turn, "generation_report_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
2869
2880
|
event
|
|
2870
2881
|
end
|
|
2871
2882
|
private :with_generation_report
|
|
@@ -2882,7 +2893,8 @@ module Samagotchi
|
|
|
2882
2893
|
# Capture used memories synchronously in the turn thread.
|
|
2883
2894
|
read_names = begin
|
|
2884
2895
|
@used_memories.capture(event, muted: @muted_memory_names)
|
|
2885
|
-
rescue StandardError
|
|
2896
|
+
rescue StandardError => e
|
|
2897
|
+
Log.warn(:memory, "used_memories_capture_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
2886
2898
|
nil
|
|
2887
2899
|
end
|
|
2888
2900
|
# Turn-scoped sink: receives the event hash as the stream handler
|
|
@@ -2891,8 +2903,9 @@ module Samagotchi
|
|
|
2891
2903
|
if on_event
|
|
2892
2904
|
begin
|
|
2893
2905
|
on_event.call(event)
|
|
2894
|
-
rescue StandardError
|
|
2906
|
+
rescue StandardError => e
|
|
2895
2907
|
# Sink errors must not break the kernel loop (same as KernelLoop's own handling)
|
|
2908
|
+
Log.warn(:turn, "sink_failed", event_type: event[:type].to_s, error: e.class.name, msg: e.message.to_s[0, 200])
|
|
2896
2909
|
end
|
|
2897
2910
|
end
|
|
2898
2911
|
|
|
@@ -20,6 +20,9 @@ module Samagotchi
|
|
|
20
20
|
def model_ref = engine.effective_model_ref
|
|
21
21
|
def thinking = engine.thinking_override
|
|
22
22
|
def reasoning_history = engine.reasoning_history_override
|
|
23
|
+
# The peer's name when +name+ is a source a peer bundle declared
|
|
24
|
+
# (PeerSources), for context_read's header.
|
|
25
|
+
def peer_label(name) = engine.peer_sources&.label_for(name)
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
# What the approval relay needs from the parent's Engine (Peers#relay):
|
|
@@ -228,7 +228,7 @@ module Samagotchi
|
|
|
228
228
|
|
|
229
229
|
# A plugin tool is asked about by its targets:' label:, else its
|
|
230
230
|
# label, as its row shows it; a call that acts as another tool (an MCP
|
|
231
|
-
# tool behind
|
|
231
|
+
# tool behind call_tool) by that tool's label, else (none registered)
|
|
232
232
|
# by its name (Approval.payload).
|
|
233
233
|
acts_as = verdict.targets&.acts_as
|
|
234
234
|
label = verdict.targets&.label ||
|
|
@@ -38,7 +38,7 @@ module Samagotchi
|
|
|
38
38
|
kind: KIND,
|
|
39
39
|
approval: {
|
|
40
40
|
tool: targets&.tool || verdict.call[:name].to_s,
|
|
41
|
-
# The tool the call acts as (an MCP tool behind
|
|
41
|
+
# The tool the call acts as (an MCP tool behind call_tool).
|
|
42
42
|
acts_as: targets&.acts_as,
|
|
43
43
|
label: label,
|
|
44
44
|
command: targets&.command,
|
|
@@ -111,7 +111,7 @@ module Samagotchi
|
|
|
111
111
|
# tool + the normalized command, or the sorted paths; for a plugin
|
|
112
112
|
# tool with neither (an MCP tool), its arguments, so "this call" is
|
|
113
113
|
# this call. A call that acts as another tool keys as
|
|
114
|
-
# `
|
|
114
|
+
# `call_tool>mcp_github_x`: the real name stays in, so it never reuses
|
|
115
115
|
# that tool's own approvals (nor they its).
|
|
116
116
|
def self.key_for(verdict)
|
|
117
117
|
t = verdict.targets
|
|
@@ -12,7 +12,7 @@ module Samagotchi
|
|
|
12
12
|
# tool: a tool name, a list, or "shell" (execute + task_create);
|
|
13
13
|
# a name may be a glob ("mcp_*", "mcp_{git,gh}_*"); it
|
|
14
14
|
# matches the call's name or the tool a plugin tool's call
|
|
15
|
-
# acts as (Targets#acts_as: an MCP tool behind
|
|
15
|
+
# acts as (Targets#acts_as: an MCP tool behind call_tool)
|
|
16
16
|
# command: a Ruby regex on a shell tool's command
|
|
17
17
|
# path: "outside_repo", or a glob on the resolved paths (absolute
|
|
18
18
|
# or "**/…" globs match the absolute path; others the path
|
|
@@ -50,7 +50,7 @@ module Samagotchi
|
|
|
50
50
|
attr_reader :args
|
|
51
51
|
|
|
52
52
|
# @return [String, nil] the tool a plugin tool's call acts as (its
|
|
53
|
-
# targets:' acts_as:, an MCP tool behind
|
|
53
|
+
# targets:' acts_as:, an MCP tool behind call_tool): a rule's tool:
|
|
54
54
|
# matches it as well as the call's name. Never a core tool's or
|
|
55
55
|
# another bundle's (.plugin_targets drops those). #tool stays the
|
|
56
56
|
# call's name.
|