samagotchi 0.66.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 +60 -1
- data/docs/architecture.md +50 -28
- data/docs/cli.md +3 -1
- data/docs/configuration.md +50 -9
- data/docs/context.md +35 -1
- 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 +71 -29
- data/docs/internals/gemma4-contract.md +1 -1
- data/docs/internals/llm-context-bench.md +5 -4
- data/docs/internals/prompt-caching.md +26 -11
- data/docs/plugins.md +126 -23
- data/docs/sessions.md +6 -6
- data/docs/sub-agent.md +4 -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 +34 -313
- 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 +7 -6
- data/lib/samagotchi/bundles/codex-peer/plugin.rb +37 -312
- data/lib/samagotchi/bundles/codex-peer/scripts/chi-peer-SKILL.md +8 -3
- data/lib/samagotchi/bundles/codex-peer/skill_codex_peer.md +14 -6
- 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/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/config.rb +15 -7
- 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 +96 -191
- 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/llm/adapter_common.rb +138 -0
- 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 +64 -155
- data/lib/samagotchi/plugin/loader.rb +4 -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 +21 -11
- data/lib/samagotchi/session_continue.rb +58 -15
- data/lib/samagotchi/session_inbox.rb +0 -13
- data/lib/samagotchi/tool_call_parser.rb +1 -1
- data/lib/samagotchi/toolkit_binding.rb +138 -0
- data/lib/samagotchi/tools/context_read.rb +14 -0
- data/lib/samagotchi/tools/execute.rb +12 -12
- data/lib/samagotchi/tools/registry.rb +1 -1
- 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 +16 -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 +1 -1
- data/lib/samagotchi/web/public/app.js +31 -36
- data/lib/samagotchi/web/public/chains.js +1 -1
- data/lib/samagotchi/web/public/commands.js +2 -2
- 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/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 +14 -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/strip.js +3 -3
- data/lib/samagotchi/web/public/task_stop.js +3 -2
- 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 +11 -8
- data/lib/samagotchi/worker_inbound.rb +36 -4
- data/lib/samagotchi/worker_wakes.rb +67 -7
- metadata +14 -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,7 +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"
|
|
34
|
-
require_relative "
|
|
36
|
+
require_relative "toolkit_binding"
|
|
35
37
|
require_relative "answer_tail"
|
|
36
38
|
require_relative "archive_store"
|
|
37
39
|
require_relative "session_observer"
|
|
@@ -44,6 +46,7 @@ require_relative "recap_setup"
|
|
|
44
46
|
require_relative "idle_reminders"
|
|
45
47
|
require_relative "idle_scheduler"
|
|
46
48
|
require_relative "hooks"
|
|
49
|
+
require_relative "hook_routes"
|
|
47
50
|
require_relative "guardrails"
|
|
48
51
|
require_relative "reminder_store"
|
|
49
52
|
require_relative "reminder_queue"
|
|
@@ -146,9 +149,13 @@ module Samagotchi
|
|
|
146
149
|
begin
|
|
147
150
|
require_relative "memory_bundle/system_bundle"
|
|
148
151
|
MemoryBundle::SystemBundle.ensure!
|
|
149
|
-
rescue StandardError
|
|
150
|
-
|
|
152
|
+
rescue StandardError => e
|
|
153
|
+
Log.warn(:memory, "system_bundle_ensure_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
151
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
|
|
152
159
|
# Load hooks from config (plugins) and create the registry; what fails
|
|
153
160
|
# to load is announced, and a required guardrail's failure denies
|
|
154
161
|
# every tool call. Rules load now too, so their errors are announced.
|
|
@@ -185,9 +192,15 @@ module Samagotchi
|
|
|
185
192
|
# plugins' tool sets from chi.replace_tools until the turn thread
|
|
186
193
|
# applies them (#apply_staged_tools!). Built before the plugins load.
|
|
187
194
|
@plugin_tasks = build_plugin_tasks
|
|
188
|
-
# The
|
|
189
|
-
#
|
|
190
|
-
@
|
|
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
|
|
191
204
|
@lifecycle_mutex = Mutex.new
|
|
192
205
|
@shut_down = false
|
|
193
206
|
load_plugins if plugins
|
|
@@ -451,7 +464,8 @@ module Samagotchi
|
|
|
451
464
|
|
|
452
465
|
log_dropped_steers(steers, "answered")
|
|
453
466
|
[]
|
|
454
|
-
rescue StandardError
|
|
467
|
+
rescue StandardError => e
|
|
468
|
+
Log.warn(:turn, "take_steers_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
455
469
|
[]
|
|
456
470
|
end
|
|
457
471
|
private :take_steers
|
|
@@ -552,7 +566,8 @@ module Samagotchi
|
|
|
552
566
|
return saved.dup if !@prompt_builder.prompt_notes.nil? || !saved.empty? || @session.messages.to_a.any?
|
|
553
567
|
|
|
554
568
|
model_notes.map { |note| PromptNote.from_note(note) }
|
|
555
|
-
rescue StandardError
|
|
569
|
+
rescue StandardError => e
|
|
570
|
+
Log.warn(:model, "prompt_notes_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
556
571
|
[]
|
|
557
572
|
end
|
|
558
573
|
|
|
@@ -617,6 +632,9 @@ module Samagotchi
|
|
|
617
632
|
# built-ins, and the ones bundle plugins add
|
|
618
633
|
attr_reader :command_registry
|
|
619
634
|
|
|
635
|
+
# @return [PeerSources] the sources the peer bundles declared
|
|
636
|
+
attr_reader :peer_sources
|
|
637
|
+
|
|
620
638
|
# The model id sent for +full_ref+ (its alias applied, the host prefix off).
|
|
621
639
|
def bare_model_name(full_ref)
|
|
622
640
|
@host_registry.bare_name(full_ref)
|
|
@@ -1494,7 +1512,8 @@ module Samagotchi
|
|
|
1494
1512
|
|
|
1495
1513
|
def save_quietly(session)
|
|
1496
1514
|
session.save(state_dir: session_state_dir)
|
|
1497
|
-
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])
|
|
1498
1517
|
nil
|
|
1499
1518
|
end
|
|
1500
1519
|
private :save_quietly
|
|
@@ -1532,122 +1551,59 @@ module Samagotchi
|
|
|
1532
1551
|
end
|
|
1533
1552
|
private :store_answer_display
|
|
1534
1553
|
|
|
1535
|
-
# ── The hook runtime (
|
|
1536
|
-
|
|
1537
|
-
#
|
|
1538
|
-
#
|
|
1539
|
-
def hook_runtime
|
|
1540
|
-
Hooks::Runtime.new(
|
|
1541
|
-
notify: ->(text:, level:, hook:, fallback_for: nil) { hook_notify(text, level, hook, fallback_for: fallback_for) },
|
|
1542
|
-
ask_user: ->(question:, options:, header:, allow_freeform:, hook:) { hook_ask_user(question, options, header, allow_freeform, hook) },
|
|
1543
|
-
stop_turn: ->(reason:, hook:) { hook_stop_turn(reason, hook) },
|
|
1544
|
-
steer: ->(text:, hook:) { steer(text, source: hook_source(hook)) },
|
|
1545
|
-
stop_generation: ->(reason:, hook:) { hook_stop_generation(reason, hook) }
|
|
1546
|
-
)
|
|
1547
|
-
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
|
|
1548
1559
|
private :hook_runtime
|
|
1549
1560
|
|
|
1550
|
-
|
|
1551
|
-
# ("<file> (bundle <name>)"), the label of any other.
|
|
1552
|
-
def hook_source(hook)
|
|
1553
|
-
hook.to_s[/\(bundle (.+)\)\z/, 1] || hook.to_s
|
|
1554
|
-
end
|
|
1561
|
+
def hook_source(hook) = @hook_routes.source(hook)
|
|
1555
1562
|
private :hook_source
|
|
1556
1563
|
|
|
1557
1564
|
# What a notice may stand in for (hook_notify's fallback_for:).
|
|
1558
|
-
NOTICE_FALLBACKS =
|
|
1565
|
+
NOTICE_FALLBACKS = HookRoutes::NOTICE_FALLBACKS
|
|
1559
1566
|
|
|
1560
|
-
# One line to the user (:hook_notice).
|
|
1561
|
-
# event: the turn's sink (the REPL) and the observers (bridge, log).
|
|
1562
|
-
# Outside one (a plugin's command at the prompt) it is announced with
|
|
1563
|
-
# between_turns: true, which every UI shows as cards are shown.
|
|
1564
|
-
#
|
|
1565
|
-
# +fallback_for+ names what the line stands in for (NOTICE_FALLBACKS):
|
|
1566
|
-
# :display, the answer's display (AnswerDisplay), for a UI that doesn't
|
|
1567
|
-
# render its links; :editor, a ref the viewer could open in its editor
|
|
1568
|
-
# (Web::Capabilities#editor), for a UI or a viewer that can't. A UI that
|
|
1569
|
-
# does leaves the line out; the others, the log among them, show it. An
|
|
1570
|
-
# unknown value is dropped, so the line shows everywhere.
|
|
1571
|
-
# +extra+: more fields for the UIs (a toolkit change's summary).
|
|
1567
|
+
# One line to the user (:hook_notice), HookRoutes#notify.
|
|
1572
1568
|
def hook_notify(text, level, hook, fallback_for: nil, extra: {})
|
|
1573
|
-
level
|
|
1574
|
-
level = :info unless %i[info warn].include?(level)
|
|
1575
|
-
notice = { type: :hook_notice, hook: hook.to_s, text: text.to_s, level: level,
|
|
1576
|
-
fallback_for: notice_fallback(fallback_for, hook) }.compact.merge(extra)
|
|
1577
|
-
return @extension_load.hold(notice) && nil if @extension_load.loading?
|
|
1578
|
-
|
|
1579
|
-
in_turn, sink = @turn_state.in_turn_sink
|
|
1580
|
-
if anytime_thread?
|
|
1581
|
-
announce_anytime(notice.merge(between_turns: true))
|
|
1582
|
-
elsif current_init_task
|
|
1583
|
-
announce(notice.merge(between_turns: true))
|
|
1584
|
-
elsif in_turn
|
|
1585
|
-
emit_event(sink, notice)
|
|
1586
|
-
else
|
|
1587
|
-
announce_or_hold(notice.merge(between_turns: true))
|
|
1588
|
-
end
|
|
1589
|
-
nil
|
|
1569
|
+
@hook_routes.notify(text, level, hook, fallback_for: fallback_for, extra: extra)
|
|
1590
1570
|
end
|
|
1591
1571
|
private :hook_notify
|
|
1592
1572
|
|
|
1593
|
-
|
|
1594
|
-
# one is logged once at debug.
|
|
1595
|
-
def notice_fallback(value, hook)
|
|
1596
|
-
return if value.nil?
|
|
1597
|
-
|
|
1598
|
-
fallback = value.to_s.to_sym
|
|
1599
|
-
return fallback if NOTICE_FALLBACKS.include?(fallback)
|
|
1600
|
-
|
|
1601
|
-
@unknown_fallbacks ||= Set.new
|
|
1602
|
-
Log.debug(:hooks, "notice_unknown_fallback", hook: hook.to_s, value: value.to_s) if @unknown_fallbacks.add?(fallback)
|
|
1603
|
-
nil
|
|
1604
|
-
end
|
|
1605
|
-
private :notice_fallback
|
|
1606
|
-
|
|
1607
|
-
# A guardrails line to the user after load (:guardrail_warning, as the
|
|
1608
|
-
# load failures show): a running turn's event, else announced.
|
|
1609
|
-
def guardrail_notify(message)
|
|
1610
|
-
event = { type: :guardrail_warning, message: message.to_s }
|
|
1611
|
-
in_turn, sink = @turn_state.in_turn_sink
|
|
1612
|
-
in_turn ? emit_event(sink, event) : announce_or_hold(event)
|
|
1613
|
-
nil
|
|
1614
|
-
end
|
|
1573
|
+
def guardrail_notify(message) = @hook_routes.guardrail_notify(message)
|
|
1615
1574
|
private :guardrail_notify
|
|
1616
1575
|
|
|
1617
|
-
# A question through the question flow (REPL sync handler, attached TUI,
|
|
1618
|
-
# web), single-select, kind "hook". A --non-interactive run has no one
|
|
1619
|
-
# to ask: nil at once. Anything but an answer (a sync handler's text, no
|
|
1620
|
-
# answer, cancelled) is nil too.
|
|
1621
1576
|
# @return [Hash, nil] {selected:, freeform:, selected_indices:}
|
|
1622
1577
|
def hook_ask_user(question, options, header, allow_freeform, hook)
|
|
1623
|
-
@
|
|
1578
|
+
@hook_routes.ask_user(question, options, header, allow_freeform, hook)
|
|
1624
1579
|
end
|
|
1625
1580
|
private :hook_ask_user
|
|
1626
1581
|
|
|
1627
|
-
# Cancel the running turn (reason :hook), after a notice that says why.
|
|
1628
|
-
# The gate denies the rest of a tool batch once the controller is
|
|
1629
|
-
# cancelled; the next request ends the turn as :turn_canceled.
|
|
1630
1582
|
# @return [Boolean] true when a running turn was cancelled now
|
|
1631
|
-
def hook_stop_turn(reason, hook)
|
|
1632
|
-
ctrl = active_cancel_controller
|
|
1633
|
-
return false unless ctrl && !ctrl.cancelled?
|
|
1634
|
-
|
|
1635
|
-
hook_notify("stopped the turn: #{reason}", :warn, hook)
|
|
1636
|
-
ctrl.cancel!(:hook, { by: hook_source(hook), reason: reason.to_s })
|
|
1637
|
-
end
|
|
1583
|
+
def hook_stop_turn(reason, hook) = @hook_routes.stop_turn(reason, hook)
|
|
1638
1584
|
private :hook_stop_turn
|
|
1639
1585
|
|
|
1640
|
-
# Cut the streaming generation (reason :hook); the turn goes on and asks
|
|
1641
|
-
# the model again (the loops' cut path). No notice: the plugin posts its
|
|
1642
|
-
# own. The bundle and +reason+ go to the log and the retry's nudge.
|
|
1643
1586
|
# @return [Boolean] true when a streaming generation was cut now
|
|
1644
|
-
def hook_stop_generation(reason, hook)
|
|
1645
|
-
|
|
1646
|
-
return false unless ctrl
|
|
1587
|
+
def hook_stop_generation(reason, hook) = @hook_routes.stop_generation(reason, hook)
|
|
1588
|
+
private :hook_stop_generation
|
|
1647
1589
|
|
|
1648
|
-
|
|
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
|
+
)
|
|
1649
1605
|
end
|
|
1650
|
-
private :
|
|
1606
|
+
private :build_hook_routes
|
|
1651
1607
|
|
|
1652
1608
|
# ── Ask-user-question (structured qualification) ──────────────────────────
|
|
1653
1609
|
|
|
@@ -2228,8 +2184,8 @@ module Samagotchi
|
|
|
2228
2184
|
# Persist deduped used memories onto the session for Web + reload.
|
|
2229
2185
|
begin
|
|
2230
2186
|
session.used_memory_names = used_memory_names
|
|
2231
|
-
rescue StandardError
|
|
2232
|
-
|
|
2187
|
+
rescue StandardError => e
|
|
2188
|
+
Log.warn(:memory, "used_memories_store_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
2233
2189
|
end
|
|
2234
2190
|
# Notify live observers of the updated memory list (so yellow bar refreshes
|
|
2235
2191
|
# even without a tool_call event if the preload was the only addition).
|
|
@@ -2238,8 +2194,8 @@ module Samagotchi
|
|
|
2238
2194
|
|
|
2239
2195
|
begin
|
|
2240
2196
|
emit_event(on_event, { type: :used_memories_updated, used_memory_names: used_memory_names })
|
|
2241
|
-
rescue StandardError
|
|
2242
|
-
|
|
2197
|
+
rescue StandardError => e
|
|
2198
|
+
Log.warn(:memory, "used_memories_emit_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
2243
2199
|
end
|
|
2244
2200
|
end
|
|
2245
2201
|
private :publish_used_memories
|
|
@@ -2676,12 +2632,16 @@ module Samagotchi
|
|
|
2676
2632
|
add_init_task(bundle: bundle, label: label, plugin_label: plugin_label, provides_tools: provides_tools,
|
|
2677
2633
|
quiet: quiet, timeout: timeout, failed: failed, &block)
|
|
2678
2634
|
},
|
|
2679
|
-
toolkits: @plugin_kits
|
|
2635
|
+
toolkits: @toolkit_binding.plugin_kits,
|
|
2636
|
+
tool_index: @tool_index,
|
|
2637
|
+
peer_sources: @peer_sources
|
|
2680
2638
|
)
|
|
2681
2639
|
# What plugins show while they load (an MCP server that didn't
|
|
2682
2640
|
# start) waits for the first turn, beside the load warnings: no UI
|
|
2683
2641
|
# is there yet, and the Engine isn't built.
|
|
2684
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)
|
|
2685
2645
|
end
|
|
2686
2646
|
|
|
2687
2647
|
# Keep a plugin's new tool set (chi.replace_tools, from any thread)
|
|
@@ -2699,100 +2659,42 @@ module Samagotchi
|
|
|
2699
2659
|
end
|
|
2700
2660
|
public :apply_staged_tools!
|
|
2701
2661
|
|
|
2702
|
-
# The
|
|
2703
|
-
#
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
def toolkit_kits(errors = [])
|
|
2709
|
-
@config_kits = Toolkit.config_kits unless defined?(@config_kits)
|
|
2710
|
-
Toolkit.kits(config: @config_kits, plugins: @plugin_kits, errors: errors)
|
|
2711
|
-
end
|
|
2712
|
-
|
|
2713
|
-
# The session's toolkit resolved (Toolkit::Resolved): what its next
|
|
2714
|
-
# turn declares. A plain full toolkit is nil, the Registry's "every tool".
|
|
2715
|
-
def resolved_toolkit(session, errors = [])
|
|
2716
|
-
resolved = Toolkit.of(session&.toolkit).resolve(toolkit_kits(errors))
|
|
2717
|
-
resolved.everything? ? nil : resolved
|
|
2718
|
-
end
|
|
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
|
|
2719
2668
|
|
|
2720
2669
|
# The tools this session registered (Tools::Registry), declared or not.
|
|
2721
2670
|
attr_reader :tools
|
|
2722
2671
|
|
|
2723
|
-
# +toolkit+ (the session's by default) as its next turn would declare
|
|
2724
|
-
# it (/toolkit, the UIs' chip).
|
|
2725
|
-
# @return [Toolkit::Explained]
|
|
2726
|
-
def toolkit_explained(toolkit = nil)
|
|
2727
|
-
toolkit ||= Toolkit.of(@session&.toolkit)
|
|
2728
|
-
resolved = toolkit.resolve(toolkit_kits)
|
|
2729
|
-
filter = resolved.everything? ? nil : resolved
|
|
2730
|
-
Toolkit::Explained.new(toolkit: toolkit, resolved: resolved, declared: @tools.entries(toolkit: filter),
|
|
2731
|
-
left_out: @tools.left_out_names(toolkit: filter), pending: filter != @tools.toolkit)
|
|
2732
|
-
end
|
|
2733
|
-
|
|
2734
|
-
# The session's toolkit for the UIs (the snapshot, command_ran): nil on
|
|
2735
|
-
# full, or with no session.
|
|
2736
|
-
def toolkit_summary
|
|
2737
|
-
@session && toolkit_explained.summary
|
|
2738
|
-
end
|
|
2739
2672
|
public :tools, :toolkit_kits, :resolved_toolkit, :toolkit_explained, :toolkit_summary
|
|
2740
2673
|
|
|
2741
|
-
# A plugin's ctx.use_toolkit
|
|
2742
|
-
# when it has no model reply yet and no turn runs (D3: no confirm from
|
|
2743
|
-
# plugins), and tell the user as /toolkit would; it applies at the next
|
|
2744
|
-
# turn's start.
|
|
2674
|
+
# A plugin's ctx.use_toolkit (ToolkitBinding#use).
|
|
2745
2675
|
# @return [Boolean] whether it was staged
|
|
2746
2676
|
# @raise [ArgumentError] an unknown toolkit
|
|
2747
|
-
def use_toolkit(name)
|
|
2748
|
-
raise ArgumentError, "no toolkit named #{name}" unless toolkit_kits.key?(name)
|
|
2749
|
-
return false if @session.nil? || turn_running? || history?
|
|
2750
|
-
|
|
2751
|
-
toolkit = Toolkit.new(name: name)
|
|
2752
|
-
return true if @session.toolkit == toolkit
|
|
2753
|
-
|
|
2754
|
-
@session.toolkit = toolkit
|
|
2755
|
-
@session.save(state_dir: session_state_dir)
|
|
2756
|
-
hook_notify("toolkit: #{name} from the next turn's start", :info, "toolkit", extra: { toolkit: toolkit_summary })
|
|
2757
|
-
true
|
|
2758
|
-
end
|
|
2677
|
+
def use_toolkit(name) = @toolkit_binding.use(name)
|
|
2759
2678
|
public :use_toolkit
|
|
2760
2679
|
|
|
2761
|
-
#
|
|
2762
|
-
# a turn's start (before its prompt is built), and build the prompts
|
|
2763
|
-
# again when what it declares changed. A name that doesn't resolve (a
|
|
2764
|
-
# typo, an uninstalled bundle) declares everything for now and says so
|
|
2765
|
-
# once; the saved toolkit stays as it is.
|
|
2680
|
+
# At a turn's start (ToolkitBinding#apply!).
|
|
2766
2681
|
# @return [Boolean] whether the declared tools changed
|
|
2767
|
-
def apply_toolkit!(session, on_event = nil)
|
|
2768
|
-
errors = []
|
|
2769
|
-
resolved = Toolkit.of(session&.toolkit).resolve(toolkit_kits(errors))
|
|
2770
|
-
warn_toolkit(session, resolved, errors, on_event)
|
|
2771
|
-
resolved = nil if resolved.everything?
|
|
2772
|
-
return false if resolved == @tools.toolkit
|
|
2773
|
-
|
|
2774
|
-
@tools.toolkit = resolved
|
|
2775
|
-
@prompt_builder&.reset!
|
|
2776
|
-
true
|
|
2777
|
-
end
|
|
2682
|
+
def apply_toolkit!(session, on_event = nil) = @toolkit_binding.apply!(session, on_event)
|
|
2778
2683
|
private :apply_toolkit!
|
|
2779
2684
|
|
|
2780
|
-
# The
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
Log.warn(:config, "toolkit_unresolved", message: line)
|
|
2792
|
-
emit_event(on_event, { type: :guardrail_warning, message: line, label: "toolkit" })
|
|
2793
|
-
end
|
|
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
|
+
)
|
|
2794
2696
|
end
|
|
2795
|
-
private :
|
|
2697
|
+
private :build_toolkit_binding
|
|
2796
2698
|
|
|
2797
2699
|
# The tools changed (a plugin's chi.tools_changed!): the system prompts,
|
|
2798
2700
|
# which declare them, are built again on the next turn.
|
|
@@ -2820,7 +2722,7 @@ module Samagotchi
|
|
|
2820
2722
|
steer: ->(text, label) { steer(text, source: hook_source(label)) },
|
|
2821
2723
|
stop_turn: ->(reason, label) { hook_stop_turn(reason, label) },
|
|
2822
2724
|
stop_generation: ->(reason, label) { hook_stop_generation(reason, label) },
|
|
2823
|
-
toolkit: -> { @
|
|
2725
|
+
toolkit: -> { @toolkit_binding.name },
|
|
2824
2726
|
declared_tools: -> { toolkit_explained.declared.map(&:name) },
|
|
2825
2727
|
use_toolkit: ->(name) { use_toolkit(name) },
|
|
2826
2728
|
ask_model: lambda { |request, timeout:, max_tokens:, cancel_controller:|
|
|
@@ -2973,7 +2875,8 @@ module Samagotchi
|
|
|
2973
2875
|
def with_generation_report(event)
|
|
2974
2876
|
report = @metrics.finish_generation
|
|
2975
2877
|
event.merge(speed: report.speed&.to_h, tokens: report.tokens, served_expected_by: served_expected_by(event[:served_model]))
|
|
2976
|
-
rescue StandardError
|
|
2878
|
+
rescue StandardError => e
|
|
2879
|
+
Log.warn(:turn, "generation_report_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
2977
2880
|
event
|
|
2978
2881
|
end
|
|
2979
2882
|
private :with_generation_report
|
|
@@ -2990,7 +2893,8 @@ module Samagotchi
|
|
|
2990
2893
|
# Capture used memories synchronously in the turn thread.
|
|
2991
2894
|
read_names = begin
|
|
2992
2895
|
@used_memories.capture(event, muted: @muted_memory_names)
|
|
2993
|
-
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])
|
|
2994
2898
|
nil
|
|
2995
2899
|
end
|
|
2996
2900
|
# Turn-scoped sink: receives the event hash as the stream handler
|
|
@@ -2999,8 +2903,9 @@ module Samagotchi
|
|
|
2999
2903
|
if on_event
|
|
3000
2904
|
begin
|
|
3001
2905
|
on_event.call(event)
|
|
3002
|
-
rescue StandardError
|
|
2906
|
+
rescue StandardError => e
|
|
3003
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])
|
|
3004
2909
|
end
|
|
3005
2910
|
end
|
|
3006
2911
|
|