samagotchi 0.154.0 → 0.155.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 +30 -1
- data/docs/architecture.md +3 -2
- data/docs/bundles.md +21 -0
- data/docs/cli.md +10 -8
- data/docs/guardrails.md +2 -2
- data/docs/hooks.md +1 -1
- data/docs/internals/anthropic.md +4 -4
- data/docs/internals/attached-context.md +8 -6
- data/docs/internals/block-renderers.md +8 -1
- data/docs/internals/bridge.md +18 -5
- data/docs/internals/engine.md +221 -100
- data/docs/internals/inline-widgets.md +136 -0
- data/docs/internals/objectives.md +8 -6
- data/docs/internals/participants.md +5 -5
- data/docs/internals/prompt-caching.md +12 -6
- data/docs/internals/web-frontend.md +2 -2
- data/docs/internals/working-set.md +2 -1
- data/docs/plugins.md +2 -0
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +3 -3
- data/docs/testing.md +16 -1
- data/lib/samagotchi/answer_text.rb +1 -2
- data/lib/samagotchi/block_renderers.rb +5 -0
- data/lib/samagotchi/bridge/card_places.rb +56 -7
- data/lib/samagotchi/bridge/card_store.rb +24 -9
- data/lib/samagotchi/bridge.rb +9 -0
- data/lib/samagotchi/bridge_client.rb +9 -5
- data/lib/samagotchi/bundle_command/check.rb +1 -1
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
- data/lib/samagotchi/bundles/btw/plugin.rb +1 -1
- data/lib/samagotchi/bundles/coordinator/manifest.yml +2 -2
- data/lib/samagotchi/bundles/coordinator/plugin.rb +3 -2
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/github-pr/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/plugin.rb +3 -3
- data/lib/samagotchi/bundles/inline-widgets/manifest.yml +11 -0
- data/lib/samagotchi/bundles/inline-widgets/plugin.rb +76 -0
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +3 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +1 -1
- data/lib/samagotchi/engine/context_judge.rb +19 -125
- data/lib/samagotchi/engine/guardrails.rb +4 -2
- data/lib/samagotchi/engine/messages.rb +9 -6
- data/lib/samagotchi/engine/objectives.rb +22 -90
- data/lib/samagotchi/engine/participants.rb +15 -10
- data/lib/samagotchi/engine/plugins.rb +3 -1
- data/lib/samagotchi/engine/questions.rb +1 -1
- data/lib/samagotchi/engine/side_requests.rb +5 -2
- data/lib/samagotchi/engine/toolkit.rb +33 -190
- data/lib/samagotchi/engine/turn_end.rb +1 -1
- data/lib/samagotchi/engine/turn_helpers.rb +16 -39
- data/lib/samagotchi/engine/verify.rb +7 -143
- data/lib/samagotchi/engine.rb +162 -99
- data/lib/samagotchi/engine_peers.rb +1 -1
- data/lib/samagotchi/engine_snapshots.rb +9 -8
- data/lib/samagotchi/event_fanout.rb +18 -12
- data/lib/samagotchi/guardrail_wiring.rb +83 -82
- data/lib/samagotchi/guardrails/gate.rb +41 -40
- data/lib/samagotchi/guardrails/pair_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/review_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/targets.rb +6 -4
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/idle_recap.rb +3 -1
- data/lib/samagotchi/inline_widgets.rb +325 -0
- data/lib/samagotchi/kernel_loop.rb +194 -216
- data/lib/samagotchi/llm/backend.rb +3 -1
- data/lib/samagotchi/llm/chat_loop.rb +66 -42
- data/lib/samagotchi/llm/native_backend.rb +3 -2
- data/lib/samagotchi/llm_context_forget.rb +5 -3
- data/lib/samagotchi/model_binding.rb +3 -3
- data/lib/samagotchi/plugin/prompt_sections.rb +7 -0
- data/lib/samagotchi/question_desk.rb +1 -1
- data/lib/samagotchi/revise_anchor.rb +1 -1
- data/lib/samagotchi/session_manager.rb +8 -13
- data/lib/samagotchi/session_retention.rb +24 -4
- data/lib/samagotchi/steer.rb +12 -1
- data/lib/samagotchi/stream_event.rb +22 -0
- data/lib/samagotchi/system_prompt.rb +84 -61
- data/lib/samagotchi/terminal_ui/attached_view.rb +0 -9
- data/lib/samagotchi/terminal_ui/input_support.rb +16 -15
- data/lib/samagotchi/terminal_ui/join_replay.rb +16 -12
- data/lib/samagotchi/terminal_ui/reline_seam.rb +0 -10
- data/lib/samagotchi/terminal_ui/status_row.rb +0 -7
- data/lib/samagotchi/testing/boundary.rb +7 -0
- data/lib/samagotchi/testing/bundle.rb +7 -5
- data/lib/samagotchi/testing/install.rb +3 -2
- data/lib/samagotchi/tool_activity.rb +8 -6
- data/lib/samagotchi/tool_declarations.rb +4 -4
- data/lib/samagotchi/tool_response.rb +4 -3
- data/lib/samagotchi/tool_runner.rb +93 -101
- data/lib/samagotchi/toolkit_binding.rb +40 -14
- data/lib/samagotchi/tools/deferred_tools.rb +17 -21
- data/lib/samagotchi/tools/registry.rb +43 -40
- data/lib/samagotchi/tools/tool_index.rb +135 -88
- data/lib/samagotchi/tools/tool_surface.rb +173 -0
- data/lib/samagotchi/turn_context.rb +67 -0
- data/lib/samagotchi/turn_flow.rb +7 -2
- data/lib/samagotchi/turn_stages/context_judge.rb +170 -0
- data/lib/samagotchi/turn_stages/objectives.rb +146 -0
- data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +261 -0
- data/lib/samagotchi/turn_stages/verify_anchors.rb +169 -0
- data/lib/samagotchi/turn_stages/warmup.rb +83 -0
- data/lib/samagotchi/turn_state.rb +46 -4
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +3 -1
- data/lib/samagotchi/web/markdown_renderer.rb +39 -14
- data/lib/samagotchi/web/message_display.rb +8 -1
- data/lib/samagotchi/web/public/app.js +5 -1
- data/lib/samagotchi/web/public/block_host.js +74 -199
- data/lib/samagotchi/web/public/block_renderers.css +2 -0
- data/lib/samagotchi/web/public/block_source.js +7 -4
- data/lib/samagotchi/web/public/block_watch.js +256 -0
- data/lib/samagotchi/web/public/card_stamps.js +27 -0
- data/lib/samagotchi/web/public/cards_place.js +5 -3
- data/lib/samagotchi/web/public/composer_fold.js +3 -0
- data/lib/samagotchi/web/public/composer_mode.js +1 -1
- data/lib/samagotchi/web/public/format.js +3 -2
- data/lib/samagotchi/web/public/index.html +28 -9
- data/lib/samagotchi/web/public/inline_widgets.js +146 -0
- data/lib/samagotchi/web/public/owner_verify.js +1 -1
- data/lib/samagotchi/web/public/participant.js +1 -1
- data/lib/samagotchi/web/public/revise_stack.js +1 -1
- data/lib/samagotchi/web/public/stage_watch.js +6 -2
- data/lib/samagotchi/web/public/text_roots.js +1 -1
- data/lib/samagotchi/web/public/turn_html.js +8 -4
- data/lib/samagotchi/web/public/verify_target.js +1 -1
- data/lib/samagotchi/web/public/widget_picks.js +130 -0
- data/lib/samagotchi/web/public/widget_walk.js +230 -0
- data/lib/samagotchi/web/public/widgets.css +70 -0
- data/lib/samagotchi/web/session_access_routes.rb +8 -0
- data/lib/samagotchi/web/session_show.rb +4 -0
- data/lib/samagotchi/worker.rb +8 -5
- data/lib/samagotchi.rb +0 -1
- metadata +19 -2
- data/lib/samagotchi/pending_input_queue.rb +0 -55
|
@@ -7,12 +7,14 @@ require_relative "file_ref"
|
|
|
7
7
|
require_relative "guardrails"
|
|
8
8
|
require_relative "vision_context"
|
|
9
9
|
require_relative "degrade"
|
|
10
|
+
require_relative "stream_event"
|
|
10
11
|
require_relative "log"
|
|
11
12
|
require_relative "edit_preview"
|
|
12
13
|
require_relative "tool_output_cut"
|
|
13
14
|
require_relative "tool_output_files"
|
|
14
15
|
require_relative "tools/output_guardrails"
|
|
15
16
|
require_relative "tools/tool_path"
|
|
17
|
+
require_relative "tools/tool_surface"
|
|
16
18
|
require_relative "tools/memory"
|
|
17
19
|
require_relative "config"
|
|
18
20
|
require_relative "memory_bundle/index_sync"
|
|
@@ -52,14 +54,20 @@ module Samagotchi
|
|
|
52
54
|
NOT_AN_EDIT = Object.new.freeze
|
|
53
55
|
private_constant :NOT_AN_EDIT
|
|
54
56
|
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
+
# What a call needs of its turn comes with it: #run's +context:+ (a
|
|
58
|
+
# TurnContext: the session's folders, the settings' vision, its
|
|
59
|
+
# ToolSurface, the participant's mode), passed on to the gate and the
|
|
60
|
+
# kernel's dispatch.
|
|
61
|
+
#
|
|
62
|
+
# @param kernel [KernelLoop] its services (KernelLoop::Services: hooks,
|
|
63
|
+
# tools, gate) are read per call: a spec's kernel may #attach new ones.
|
|
57
64
|
# @param clock [#call] monotonic seconds, for the approval wait and the
|
|
58
65
|
# dispatch time (after_tool_call's elapsed_ms)
|
|
59
|
-
# @param window [#call, nil] → ContextWindow::Resolved or nil:
|
|
60
|
-
# model's window
|
|
61
|
-
#
|
|
62
|
-
#
|
|
66
|
+
# @param window [#call, nil] (settings) → ContextWindow::Resolved or nil:
|
|
67
|
+
# the model's window under the turn's LLM::TurnSettings, for
|
|
68
|
+
# memory_read's overflow guard. nil: the kernel's own reader
|
|
69
|
+
# (KernelLoop#tool_output_window); a loop whose window comes from more
|
|
70
|
+
# than the kernel's client (LLM::ChatLoop's adapter) passes it.
|
|
63
71
|
def initialize(kernel, clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }, window: nil)
|
|
64
72
|
@kernel = kernel
|
|
65
73
|
@clock = clock
|
|
@@ -77,29 +85,30 @@ module Samagotchi
|
|
|
77
85
|
# RowRefs; saved likewise), and diff: what an edit/write changed in its
|
|
78
86
|
# file (EditPreview.change; also on tool_call_completed, never in the
|
|
79
87
|
# model's output)
|
|
80
|
-
|
|
88
|
+
# @param context [TurnContext] the call's turn
|
|
89
|
+
def run(call, context:, iteration:, call_index:, call_count:, on_stream_event:, max_tool_output_chars:)
|
|
81
90
|
# A call_tool of one of chi's own tools runs as that tool, from here
|
|
82
91
|
# on (the gate, the row, the result): rules, approvals and hooks see
|
|
83
92
|
# the real call, with via: (Tools::ToolIndex#unwrap).
|
|
84
|
-
call = unwrapped(call)
|
|
85
|
-
params = ToolActivity.tool_activity_params(call[:name], call, registry: tools)
|
|
93
|
+
call = unwrapped(call, context)
|
|
94
|
+
params = ToolActivity.tool_activity_params(call[:name], call, registry: tools, surface: surface(context))
|
|
86
95
|
# The gate runs first, so tool_call_started shows the call that runs.
|
|
87
96
|
# A tool the session's toolkit leaves out skips it: no before_tool_call
|
|
88
97
|
# hook, no approval card for a call that can't run (KernelLoop#dispatch
|
|
89
98
|
# answers it with the not-loaded error).
|
|
90
99
|
original = call.dup
|
|
91
|
-
refusal = unreached_group_refusal(call)
|
|
100
|
+
refusal = unreached_group_refusal(call, context)
|
|
92
101
|
verdict = if refusal
|
|
93
102
|
Guardrails::Verdict.new(call: call).deny!(refusal.message,
|
|
94
103
|
rule: Tools::ToolIndex::ToolkitGroupRefusal::RULE,
|
|
95
104
|
source: "core", decided_by: "core")
|
|
96
|
-
elsif left_out?(call)
|
|
105
|
+
elsif left_out?(call, context)
|
|
97
106
|
Guardrails::Verdict.new(call: call)
|
|
98
107
|
else
|
|
99
|
-
evaluate(call, iteration, params)
|
|
108
|
+
evaluate(call, context, iteration, params)
|
|
100
109
|
end
|
|
101
110
|
call = verdict.call
|
|
102
|
-
params = ToolActivity.tool_activity_params(call[:name], call, registry: tools)
|
|
111
|
+
params = ToolActivity.tool_activity_params(call[:name], call, registry: tools, surface: surface(context))
|
|
103
112
|
label = plugin_label(call[:name])
|
|
104
113
|
# Kept on the raw event for in-worker hooks and metrics, not sent to SSE clients.
|
|
105
114
|
started = { type: :tool_call_started, iteration: iteration, call_count: call_count, call_index: call_index,
|
|
@@ -109,7 +118,7 @@ module Samagotchi
|
|
|
109
118
|
# plugin tool's refs); the worker runs in its session's working
|
|
110
119
|
# directory.
|
|
111
120
|
fields = ToolRowFields.for(call[:name], call, cwd: Dir.pwd)
|
|
112
|
-
refs = RowRefs.for(tools && !call[:name].nil? ? tools[call[:name]]&.refs : nil, call, cwd: session_cwd)
|
|
121
|
+
refs = RowRefs.for(tools && !call[:name].nil? ? tools[call[:name]]&.refs : nil, call, cwd: session_cwd(context))
|
|
113
122
|
fields[:refs] = refs unless refs.empty?
|
|
114
123
|
started.merge!(fields)
|
|
115
124
|
emit(on_stream_event, started)
|
|
@@ -120,7 +129,7 @@ module Samagotchi
|
|
|
120
129
|
waited_ms = nil
|
|
121
130
|
if verdict.ask?
|
|
122
131
|
asked_at = @clock.call
|
|
123
|
-
settle_ask(verdict)
|
|
132
|
+
settle_ask(verdict, context)
|
|
124
133
|
waited_ms = ((@clock.call - asked_at) * 1000).round
|
|
125
134
|
end
|
|
126
135
|
before = verdict.deny? ? NOT_AN_EDIT : file_before(call)
|
|
@@ -128,10 +137,10 @@ module Samagotchi
|
|
|
128
137
|
entry_before = before.equal?(NOT_AN_EDIT) ? nil : MemoryBundle::EntrySize.measure_file(Tools::ToolPath.normalize(call[:path]))
|
|
129
138
|
elapsed_ms = nil
|
|
130
139
|
if verdict.deny?
|
|
131
|
-
result = denied(call, verdict)
|
|
140
|
+
result = denied(call, verdict, context)
|
|
132
141
|
else
|
|
133
142
|
dispatched_at = @clock.call
|
|
134
|
-
result = dispatch(call)
|
|
143
|
+
result = dispatch(call, context)
|
|
135
144
|
elapsed_ms = ((@clock.call - dispatched_at) * 1000).round
|
|
136
145
|
end
|
|
137
146
|
diff = file_change(call, before)
|
|
@@ -140,7 +149,7 @@ module Samagotchi
|
|
|
140
149
|
result = with_entry_note(result, entry_before, call)
|
|
141
150
|
end
|
|
142
151
|
result = approved(result, verdict) if verdict.allow? && verdict.decided_by
|
|
143
|
-
result, images = attach_images(call, result) if result[:images]
|
|
152
|
+
result, images = attach_images(call, result, context.settings&.vision) if result[:images]
|
|
144
153
|
|
|
145
154
|
output = scrub(result[:output].to_s)
|
|
146
155
|
# The tool's own text, for after_tool_call: no "[tool]" lead, no
|
|
@@ -148,12 +157,12 @@ module Samagotchi
|
|
|
148
157
|
tool_text = output.sub(/\A\[#{Regexp.escape(call[:name].to_s)}\](?:\n| )/, "")
|
|
149
158
|
# The gate may have replaced the call (known-names corrects a near
|
|
150
159
|
# miss): one leading line tells the model what actually ran.
|
|
151
|
-
if (changed = changed_call_line(original, call))
|
|
160
|
+
if (changed = changed_call_line(original, call, context))
|
|
152
161
|
output = "#{changed}\n#{output}"
|
|
153
162
|
end
|
|
154
|
-
cap = output_cap(call, max_tool_output_chars)
|
|
163
|
+
cap = output_cap(call, max_tool_output_chars, context)
|
|
155
164
|
truncated = !cap.nil? && output.length > cap
|
|
156
|
-
capped, saved = truncated ? cut(call, output, cap, result) : output
|
|
165
|
+
capped, saved = truncated ? cut(call, output, cap, result, context) : output
|
|
157
166
|
output_file = saved.path if saved.is_a?(ToolOutputFiles::Saved)
|
|
158
167
|
|
|
159
168
|
# The call's outcome as its activity line has it (worked out from the
|
|
@@ -179,7 +188,7 @@ module Samagotchi
|
|
|
179
188
|
images: images&.any? ? images : nil,
|
|
180
189
|
shown_params: params && plugin_tool?(call[:name]) ? params : nil,
|
|
181
190
|
shown_label: label, refs: refs.empty? ? nil : refs, diff: diff, output_file: output_file,
|
|
182
|
-
loaded: result[:loaded], stamp: status == "ok" ? file_stamp(call) : nil)
|
|
191
|
+
loaded: result[:loaded], stamp: status == "ok" ? file_stamp(call, context) : nil)
|
|
183
192
|
end
|
|
184
193
|
|
|
185
194
|
private
|
|
@@ -188,8 +197,8 @@ module Samagotchi
|
|
|
188
197
|
# {path:, mtime_ns:, size:}, the path as FileRef has it (against the
|
|
189
198
|
# session's folder, no realpath). nil for any other tool, a path it
|
|
190
199
|
# can't place and a stat that fails or isn't a file's.
|
|
191
|
-
def file_stamp(call)
|
|
192
|
-
ref = FileRef.for(call[:name], call, cwd: session_cwd)
|
|
200
|
+
def file_stamp(call, context)
|
|
201
|
+
ref = FileRef.for(call[:name], call, cwd: session_cwd(context))
|
|
193
202
|
return nil unless ref
|
|
194
203
|
|
|
195
204
|
stat = File.stat(ref.path)
|
|
@@ -208,8 +217,9 @@ module Samagotchi
|
|
|
208
217
|
# for task_wait and delegate; ref_stderr_path, a task's stderr log
|
|
209
218
|
# @return [Array(String, Object)] the text, and the saved file (or why
|
|
210
219
|
# there is none) when it was cut that way
|
|
211
|
-
def cut(call, output, cap, result)
|
|
212
|
-
ToolOutputCut.apply(output, cap: cap, tool: call[:name],
|
|
220
|
+
def cut(call, output, cap, result, context)
|
|
221
|
+
ToolOutputCut.apply(output, cap: cap, tool: call[:name],
|
|
222
|
+
save: ->(text, dry: false) { save_output(call, text, context.session_dir, dry: dry) },
|
|
213
223
|
line: (memory_cut_line(call) if call[:name] == MEMORY_READ),
|
|
214
224
|
ref: result[:ref_path], ref_stderr: result[:ref_stderr_path],
|
|
215
225
|
head_chars: Tools::OutputGuardrails.config_positive_int("execute.cut_head_chars",
|
|
@@ -222,11 +232,11 @@ module Samagotchi
|
|
|
222
232
|
# for every tool but memory_read, which memory.read_max_chars caps (0:
|
|
223
233
|
# no fixed cap) and, with none set, a known window's half (#window_guard).
|
|
224
234
|
# A nil +cap+ stays nil.
|
|
225
|
-
def output_cap(call, cap)
|
|
235
|
+
def output_cap(call, cap, context)
|
|
226
236
|
return cap unless cap && call[:name] == MEMORY_READ
|
|
227
237
|
|
|
228
238
|
fixed = Config.get("memory.read_max_chars").to_i
|
|
229
|
-
fixed.positive? ? fixed : window_guard
|
|
239
|
+
fixed.positive? ? fixed : window_guard(context.settings)
|
|
230
240
|
end
|
|
231
241
|
|
|
232
242
|
# Half the model's context window in characters, the most one memory_read
|
|
@@ -234,9 +244,9 @@ module Samagotchi
|
|
|
234
244
|
# and a retry repeats the read). nil when the window isn't known (the
|
|
235
245
|
# default isn't a window) or can't be read. It checks this result, not the
|
|
236
246
|
# context already used.
|
|
237
|
-
def window_guard
|
|
247
|
+
def window_guard(settings)
|
|
238
248
|
Degrade.call(:memory, "read_window_unknown") do
|
|
239
|
-
window = @window ? @window.call : (@kernel.tool_output_window if @kernel.respond_to?(:tool_output_window))
|
|
249
|
+
window = @window ? @window.call(settings) : (@kernel.tool_output_window(settings) if @kernel.respond_to?(:tool_output_window))
|
|
240
250
|
return nil unless window&.tokens&.positive? && window.source != :default
|
|
241
251
|
|
|
242
252
|
(window.tokens * MemoryBundle::IndexSize.chars_per_token / 2).floor
|
|
@@ -253,12 +263,12 @@ module Samagotchi
|
|
|
253
263
|
end
|
|
254
264
|
|
|
255
265
|
# The whole output in the session's tool_output folder
|
|
256
|
-
# (ToolOutputFiles), or why it
|
|
257
|
-
# or the error's class. Never
|
|
258
|
-
# nothing written
|
|
266
|
+
# (ToolOutputFiles; +dir+: the turn's TurnContext#session_dir), or why it
|
|
267
|
+
# isn't saved: no session (a bare kernel) or the error's class. Never
|
|
268
|
+
# fails the call. +dry+: where it would go, nothing written
|
|
269
|
+
# (ToolOutputFiles.planned).
|
|
259
270
|
# @return [ToolOutputFiles::Saved, String]
|
|
260
|
-
def save_output(call, text, dry: false)
|
|
261
|
-
dir = session_dir
|
|
271
|
+
def save_output(call, text, dir, dry: false)
|
|
262
272
|
return "no session" if dir.to_s.empty?
|
|
263
273
|
return ToolOutputFiles.planned(dir, text, tool: call[:name]) if dry
|
|
264
274
|
|
|
@@ -268,15 +278,6 @@ module Samagotchi
|
|
|
268
278
|
e.class.name
|
|
269
279
|
end
|
|
270
280
|
|
|
271
|
-
# The session's folder (KernelLoop#session_dir), nil for a bare kernel
|
|
272
|
-
# or a lookup that failed.
|
|
273
|
-
def session_dir
|
|
274
|
-
@kernel.respond_to?(:session_dir) ? @kernel.session_dir&.call : nil
|
|
275
|
-
rescue StandardError => e
|
|
276
|
-
Log.warn(:turn, "tool_output_save_failed", error: e.class.name)
|
|
277
|
-
nil
|
|
278
|
-
end
|
|
279
|
-
|
|
280
281
|
# edit/write only: the file just before the call runs. The row diffs it
|
|
281
282
|
# with the file after, whatever the result says, so an edit that fails
|
|
282
283
|
# after writing still shows its change and one that wrote nothing shows
|
|
@@ -350,8 +351,8 @@ module Samagotchi
|
|
|
350
351
|
# and keeps the text and the other images; for read, whose text only
|
|
351
352
|
# says the image is attached, that line replaces the text.
|
|
352
353
|
# @return [Array(Hash, Array<Hash>)] the result and its refs
|
|
353
|
-
|
|
354
|
-
|
|
354
|
+
# +vision+: the turn's VisionContext (TurnSettings#vision).
|
|
355
|
+
def attach_images(call, result, vision)
|
|
355
356
|
reason = if vision.nil? then "images can't be attached here"
|
|
356
357
|
elsif !vision.sendable? then ImagePlan::CANT_SEE
|
|
357
358
|
end
|
|
@@ -396,17 +397,19 @@ module Samagotchi
|
|
|
396
397
|
[result[:output], *notes].join("\n")
|
|
397
398
|
end
|
|
398
399
|
|
|
399
|
-
# The session's folder (the
|
|
400
|
+
# The session's folder (the turn's TurnContext#cwd), else the
|
|
400
401
|
# process's: a bare kernel (specs) has none.
|
|
401
|
-
def session_cwd
|
|
402
|
-
cwd =
|
|
402
|
+
def session_cwd(context)
|
|
403
|
+
cwd = context.cwd
|
|
403
404
|
cwd.to_s.empty? ? Dir.pwd : cwd
|
|
404
405
|
end
|
|
405
406
|
|
|
406
407
|
# The kernel's tools, for the activity line of a tool that isn't built in.
|
|
407
408
|
def tools = @kernel.tools
|
|
408
409
|
|
|
409
|
-
|
|
410
|
+
# The turn's Tools::ToolSurface (+context+'s), else one computed now
|
|
411
|
+
# from the kernel's tools (a runner outside an Engine turn).
|
|
412
|
+
def surface(context) = Tools::ToolSurface.of(context, tools)
|
|
410
413
|
|
|
411
414
|
# The call its tool's entry unwraps it to (call_tool on a built-in),
|
|
412
415
|
# else the call as it is. On a host that loads tools, a direct call of
|
|
@@ -414,12 +417,12 @@ module Samagotchi
|
|
|
414
417
|
# (Registry::Index#wrap): call_tool's row, rules and approvals. One
|
|
415
418
|
# that fails runs as it is: call_tool then refuses a built-in (fail
|
|
416
419
|
# closed).
|
|
417
|
-
def unwrapped(call)
|
|
418
|
-
|
|
419
|
-
return call unless registry.respond_to?(:offered) && !call[:name].nil?
|
|
420
|
+
def unwrapped(call, context)
|
|
421
|
+
return call if tools.nil? || call[:name].nil?
|
|
420
422
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
+
surface = surface(context)
|
|
424
|
+
call = wrapped(surface, call)
|
|
425
|
+
surface.declared_entry(call[:name])&.unwrap&.call(call, surface) || call
|
|
423
426
|
rescue StandardError => e
|
|
424
427
|
Log.warn(:turn, "tool_unwrap_failed", tool: call[:name].to_s, error: "#{e.class}: #{e.message}")
|
|
425
428
|
call
|
|
@@ -427,11 +430,11 @@ module Samagotchi
|
|
|
427
430
|
|
|
428
431
|
# +call+ as the call_tool call it runs as when it names a deferred
|
|
429
432
|
# source tool, on a host that loads tools; else as it is.
|
|
430
|
-
def wrapped(
|
|
431
|
-
wrap =
|
|
432
|
-
return call unless wrap && loads_tools
|
|
433
|
+
def wrapped(surface, call)
|
|
434
|
+
wrap = surface.index&.wrap
|
|
435
|
+
return call unless wrap && surface.loads_tools && !surface.registered?(call[:name])
|
|
433
436
|
|
|
434
|
-
wrap.call(call) || call
|
|
437
|
+
wrap.call(call, surface) || call
|
|
435
438
|
end
|
|
436
439
|
|
|
437
440
|
# A call that can't run in this turn as it is, by its name: a real tool
|
|
@@ -439,23 +442,25 @@ module Samagotchi
|
|
|
439
442
|
# (via:) that the toolkit doesn't reach that way (a hook's rewrite). On
|
|
440
443
|
# a host that loads tools, a built-in the toolkit reaches runs by its
|
|
441
444
|
# own name (KernelLoop#dispatch takes it too).
|
|
442
|
-
def left_out?(call)
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
return
|
|
445
|
+
def left_out?(call, context)
|
|
446
|
+
return false if tools.nil? || call[:name].nil?
|
|
447
|
+
|
|
448
|
+
surface = surface(context)
|
|
449
|
+
return surface.indexed_entry(call[:name]).nil? if call[:via]
|
|
450
|
+
return false if surface.loads_tools && surface.indexed_entry(call[:name])
|
|
447
451
|
|
|
448
|
-
|
|
452
|
+
surface.left_out?(call[:name])
|
|
449
453
|
end
|
|
450
454
|
|
|
451
|
-
def unreached_group_refusal(call)
|
|
455
|
+
def unreached_group_refusal(call, context)
|
|
452
456
|
# A review turn's boundary denies it first (and words why); a pair
|
|
453
457
|
# turn gets the owner's refusal.
|
|
454
|
-
return if
|
|
458
|
+
return if context.participant? && context.participant_mode != ParticipantMode::PAIR
|
|
459
|
+
return if tools.nil?
|
|
455
460
|
|
|
456
|
-
|
|
457
|
-
index =
|
|
458
|
-
return unless index
|
|
461
|
+
surface = surface(context)
|
|
462
|
+
index = surface.index
|
|
463
|
+
return unless index
|
|
459
464
|
|
|
460
465
|
name = if call[:name] == index.call_name
|
|
461
466
|
args = call[:args].is_a?(Hash) ? call[:args] : {}
|
|
@@ -463,24 +468,10 @@ module Samagotchi
|
|
|
463
468
|
else
|
|
464
469
|
call[:name]
|
|
465
470
|
end
|
|
466
|
-
tool = index.unreached_tool(name)
|
|
467
|
-
tool && Tools::ToolIndex::ToolkitGroupRefusal.for(tool,
|
|
471
|
+
tool = index.unreached_tool(name, surface)
|
|
472
|
+
tool && Tools::ToolIndex::ToolkitGroupRefusal.for(tool, surface)
|
|
468
473
|
end
|
|
469
474
|
|
|
470
|
-
def participant_turn?
|
|
471
|
-
@kernel.respond_to?(:participant_turn?) && @kernel.participant_turn?
|
|
472
|
-
end
|
|
473
|
-
|
|
474
|
-
# The running participant turn's mode (KernelLoop#participant_mode,
|
|
475
|
-
# GuardrailWiring#turn_mode), nil without one.
|
|
476
|
-
def participant_mode
|
|
477
|
-
lookup = @kernel.respond_to?(:participant_mode) ? @kernel.participant_mode : nil
|
|
478
|
-
lookup&.call
|
|
479
|
-
end
|
|
480
|
-
|
|
481
|
-
# Whether the turn's host loads tools (TurnSettings#loads_tools).
|
|
482
|
-
def loads_tools? = @kernel.respond_to?(:turn_settings) && @kernel.turn_settings&.loads_tools == true
|
|
483
|
-
|
|
484
475
|
# A tool the registry has from a plugin (not core, not unknown).
|
|
485
476
|
def plugin_tool?(name)
|
|
486
477
|
entry = tools && !name.nil? ? tools[name] : nil
|
|
@@ -494,29 +485,30 @@ module Samagotchi
|
|
|
494
485
|
# asked for: "ran as: <tool> <changed args>", or nil when nothing
|
|
495
486
|
# changed. The args are the changed call's activity params, so the line
|
|
496
487
|
# stays short.
|
|
497
|
-
def changed_call_line(original, call)
|
|
488
|
+
def changed_call_line(original, call, context)
|
|
498
489
|
return nil if original == call
|
|
499
490
|
|
|
500
|
-
params = ToolActivity.tool_activity_params(call[:name], call, registry: tools)
|
|
491
|
+
params = ToolActivity.tool_activity_params(call[:name], call, registry: tools, surface: surface(context))
|
|
501
492
|
["ran as: #{call[:name]}", params].compact.join(" ")
|
|
502
493
|
end
|
|
503
494
|
|
|
504
|
-
# The
|
|
505
|
-
# flow); a bare kernel (specs) gets one that only
|
|
495
|
+
# The kernel's gate (the Engine's, KernelLoop::Services#gate: its
|
|
496
|
+
# context, the approval flow); a bare kernel (specs) gets one that only
|
|
497
|
+
# runs its hooks, built per call from what the kernel holds now.
|
|
506
498
|
def gate
|
|
507
|
-
@kernel.guardrail_gate ||
|
|
499
|
+
@kernel.guardrail_gate || Guardrails::Gate.new(hooks: @kernel.hooks, tools: tools)
|
|
508
500
|
end
|
|
509
501
|
|
|
510
502
|
# A gate that fails denies the call (fail closed).
|
|
511
|
-
def evaluate(call, iteration, params)
|
|
512
|
-
gate.evaluate(call, iteration: iteration, params: params)
|
|
503
|
+
def evaluate(call, context, iteration, params)
|
|
504
|
+
gate.evaluate(call, context: context, iteration: iteration, params: params)
|
|
513
505
|
rescue StandardError => e
|
|
514
506
|
Guardrails::Verdict.new(call: call).deny!("the guardrail check failed: #{e.class}: #{e.message}",
|
|
515
507
|
decided_by: "core")
|
|
516
508
|
end
|
|
517
509
|
|
|
518
|
-
def settle_ask(verdict)
|
|
519
|
-
gate.settle_ask(verdict)
|
|
510
|
+
def settle_ask(verdict, context)
|
|
511
|
+
gate.settle_ask(verdict, context: context)
|
|
520
512
|
rescue StandardError => e
|
|
521
513
|
verdict.settle!(:deny, decided_by: "core", note: "The approval failed (#{e.class}: #{e.message}).")
|
|
522
514
|
end
|
|
@@ -530,10 +522,10 @@ module Samagotchi
|
|
|
530
522
|
|
|
531
523
|
# A legacy veto keeps its old text; a verdict's deny tells the model
|
|
532
524
|
# who decided and not to route around it.
|
|
533
|
-
def denied(call, verdict)
|
|
525
|
+
def denied(call, verdict, context)
|
|
534
526
|
if verdict.rule == Tools::ToolIndex::ToolkitGroupRefusal::RULE
|
|
535
527
|
output = "[#{call[:name]}] #{verdict.reason}"
|
|
536
|
-
activity = ToolActivity.tool_activity_event(call[:name], call, output, registry: tools)
|
|
528
|
+
activity = ToolActivity.tool_activity_event(call[:name], call, output, registry: tools, surface: surface(context))
|
|
537
529
|
return { output: output, activity: activity.merge(status: "blocked", guardrail: verdict.to_activity) }
|
|
538
530
|
end
|
|
539
531
|
|
|
@@ -543,14 +535,14 @@ module Samagotchi
|
|
|
543
535
|
else
|
|
544
536
|
"[#{call[:name]}] Error: #{verdict.deny_text}"
|
|
545
537
|
end
|
|
546
|
-
activity = ToolActivity.tool_activity_event(call[:name], call, output, registry: tools)
|
|
538
|
+
activity = ToolActivity.tool_activity_event(call[:name], call, output, registry: tools, surface: surface(context))
|
|
547
539
|
{ output: output, activity: activity.merge(status: "blocked", guardrail: verdict.to_activity) }
|
|
548
540
|
end
|
|
549
541
|
|
|
550
542
|
# KernelLoop#dispatch turns tool errors into "[name] Error: …" itself;
|
|
551
543
|
# this rescue only catches a failing dispatcher.
|
|
552
|
-
def dispatch(call)
|
|
553
|
-
@kernel.dispatch_tool_call(call)
|
|
544
|
+
def dispatch(call, context)
|
|
545
|
+
@kernel.dispatch_tool_call(call, context)
|
|
554
546
|
rescue StandardError => e
|
|
555
547
|
{ output: "[#{call[:name]}] Error: #{e.class}: #{e.message}", activity: nil }
|
|
556
548
|
end
|
|
@@ -562,7 +554,7 @@ module Samagotchi
|
|
|
562
554
|
|
|
563
555
|
# A failing stream callback (a UI gone) must not break the call either.
|
|
564
556
|
def emit(callback, event)
|
|
565
|
-
|
|
557
|
+
StreamEvent.emit(callback, event, failure: "tool_event_callback_failed")
|
|
566
558
|
end
|
|
567
559
|
end
|
|
568
560
|
end
|
|
@@ -5,6 +5,7 @@ require_relative "memory_paths"
|
|
|
5
5
|
require_relative "model_overlay"
|
|
6
6
|
require_relative "muted_memories"
|
|
7
7
|
require_relative "toolkit"
|
|
8
|
+
require_relative "tools/tool_surface"
|
|
8
9
|
require "yaml"
|
|
9
10
|
|
|
10
11
|
module Samagotchi
|
|
@@ -13,6 +14,12 @@ module Samagotchi
|
|
|
13
14
|
# ctx.use_toolkit (#use) and the turn-start apply into the tool Registry
|
|
14
15
|
# (#apply!), with its load warnings said once per process.
|
|
15
16
|
#
|
|
17
|
+
# Its readers off the turn thread (#explained: /toolkit, the snapshot,
|
|
18
|
+
# the plugin host's declared_tools; ctx.load_tools) read #published, the
|
|
19
|
+
# Registry as the turn thread last published it (#publish!: the Engine's
|
|
20
|
+
# build, a turn's start), never the live one. #kits' lazy reads may run
|
|
21
|
+
# on any thread: each one stores what any other would.
|
|
22
|
+
#
|
|
16
23
|
# What it needs from the Engine comes through lookups, called at use.
|
|
17
24
|
class ToolkitBinding
|
|
18
25
|
# @param tools [#call] → Tools::Registry
|
|
@@ -45,6 +52,21 @@ module Samagotchi
|
|
|
45
52
|
# @return [Array<Toolkit::Kit>] the plugins' kits (Plugin::Registries' toolkits:)
|
|
46
53
|
attr_reader :plugin_kits
|
|
47
54
|
|
|
55
|
+
# Publish the Registry as it is now (Tools::Registry#snapshot), on the
|
|
56
|
+
# turn thread or at the Engine's build only: after the tool index is
|
|
57
|
+
# declared, after a turn's staged tool sets and its toolkit apply.
|
|
58
|
+
# One assignment of a frozen value.
|
|
59
|
+
# @return [Tools::Registry] the snapshot
|
|
60
|
+
def publish!
|
|
61
|
+
@published = @tools.call.snapshot
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The Registry as last published (#publish!); before the first publish
|
|
65
|
+
# (a plugin reading while the Engine is built, on its thread), the
|
|
66
|
+
# Registry read now.
|
|
67
|
+
# @return [Tools::Registry]
|
|
68
|
+
def published = @published || @tools.call.snapshot
|
|
69
|
+
|
|
48
70
|
# The kits a session's toolkit resolves against: built-ins, config,
|
|
49
71
|
# plugins, then project and system memory kits. Config and memory kits
|
|
50
72
|
# are read once per worker, like the plugins': edits take effect when a
|
|
@@ -77,17 +99,18 @@ module Samagotchi
|
|
|
77
99
|
end
|
|
78
100
|
|
|
79
101
|
# +toolkit+ (the session's by default) as its next turn would declare
|
|
80
|
-
# it (/toolkit, the UIs' chip)
|
|
102
|
+
# it (/toolkit, the UIs' chip): the #published Registry read now as a
|
|
103
|
+
# Tools::ToolSurface under that toolkit.
|
|
81
104
|
# @return [Toolkit::Explained]
|
|
82
105
|
def explained(toolkit = nil)
|
|
83
106
|
toolkit ||= Toolkit.of(@session.call&.toolkit)
|
|
84
107
|
resolved = toolkit.resolve(kits)
|
|
85
108
|
filter = resolved.everything? ? nil : resolved
|
|
86
|
-
tools =
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
reachable:
|
|
90
|
-
removed:
|
|
109
|
+
tools = published
|
|
110
|
+
surface = Tools::ToolSurface.for(tools, toolkit: filter)
|
|
111
|
+
Toolkit::Explained.new(toolkit: toolkit, resolved: resolved, declared: surface.declared,
|
|
112
|
+
left_out: surface.left_out.keys, reachable: surface.index_only.map(&:name),
|
|
113
|
+
removed: surface.removed, pending: filter != tools.toolkit)
|
|
91
114
|
end
|
|
92
115
|
|
|
93
116
|
# The session's toolkit for the UIs (the snapshot, command_ran): nil on
|
|
@@ -130,7 +153,7 @@ module Samagotchi
|
|
|
130
153
|
true
|
|
131
154
|
end
|
|
132
155
|
|
|
133
|
-
# On mode's suggestion (
|
|
156
|
+
# On mode's suggestion (TurnStages::ToolkitSuggestion#settle), on the turn
|
|
134
157
|
# thread before #apply!: put +session+ on kit +name+ with origin
|
|
135
158
|
# suggested, saved, and tell the user through #use's notice path. Not
|
|
136
159
|
# when the session's toolkit isn't the default any more (a /toolkit or
|
|
@@ -157,8 +180,8 @@ module Samagotchi
|
|
|
157
180
|
true
|
|
158
181
|
end
|
|
159
182
|
|
|
160
|
-
# On mode's ask gave the turn no suggestion to judge
|
|
161
|
-
# #
|
|
183
|
+
# On mode's ask gave the turn no suggestion to judge
|
|
184
|
+
# (TurnStages::ToolkitSuggestion#settle: a timeout, no answer): say, as a warn
|
|
162
185
|
# notice, that +session+ runs its default (not once a toolkit was
|
|
163
186
|
# chosen meanwhile). +why+ is fallback_notice's.
|
|
164
187
|
def fell_back(session, why)
|
|
@@ -185,6 +208,7 @@ module Samagotchi
|
|
|
185
208
|
# again when what it declares (or its kit's prompt text) changed. A
|
|
186
209
|
# name that doesn't resolve (a typo, an uninstalled bundle) declares
|
|
187
210
|
# everything for now and says so once; the saved toolkit stays as it is.
|
|
211
|
+
# Publishes the Registry it leaves (#publish!).
|
|
188
212
|
# @return [Boolean] whether the declared tools (or the kit's prompt text) changed
|
|
189
213
|
def apply!(session, on_event = nil)
|
|
190
214
|
errors = []
|
|
@@ -200,6 +224,7 @@ module Samagotchi
|
|
|
200
224
|
@refresh_index.call
|
|
201
225
|
@reset_prompt.call
|
|
202
226
|
end
|
|
227
|
+
publish!
|
|
203
228
|
warn_stranded(tools)
|
|
204
229
|
changed
|
|
205
230
|
end
|
|
@@ -214,13 +239,14 @@ module Samagotchi
|
|
|
214
239
|
|
|
215
240
|
# A notice (by its bundle, once per tool and worker) for each plugin
|
|
216
241
|
# tool that arrived mid-session on a kit that leaves it out without
|
|
217
|
-
# the tool index to reach it (Tools::
|
|
242
|
+
# the tool index to reach it (Tools::ToolSurface#stranded, the
|
|
243
|
+
# Registry read now under the toolkit #apply! set).
|
|
218
244
|
def warn_stranded(tools)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
(tools.stranded_names - @stranded).each do |name|
|
|
245
|
+
surface = Tools::ToolSurface.for(tools)
|
|
246
|
+
(surface.stranded - @stranded).each do |name|
|
|
222
247
|
@stranded << name
|
|
223
|
-
@notify.call(self.class.stranded_notice(name,
|
|
248
|
+
@notify.call(self.class.stranded_notice(name, surface.toolkit.name), :warn,
|
|
249
|
+
surface.registered[name]&.source.to_s, extra: {})
|
|
224
250
|
end
|
|
225
251
|
end
|
|
226
252
|
private :warn_stranded
|
|
@@ -10,7 +10,7 @@ module Samagotchi
|
|
|
10
10
|
# session reaches but doesn't declare, so a find_tools hit can load it
|
|
11
11
|
# and the model calls it by its own name. In order:
|
|
12
12
|
# 1. the chi tools (built-ins and plugin tools) the toolkit leaves out
|
|
13
|
-
# but doesn't remove (
|
|
13
|
+
# but doesn't remove (ToolSurface#index_only), in registry order;
|
|
14
14
|
# 2. the tool index's source tools (an MCP server's, by acts_as), only
|
|
15
15
|
# while call_tool is declared (ToolIndex#deferrable: sorted by name);
|
|
16
16
|
# 3. every other name the conversation loaded (a find result's
|
|
@@ -28,18 +28,17 @@ module Samagotchi
|
|
|
28
28
|
|
|
29
29
|
module_function
|
|
30
30
|
|
|
31
|
-
# @param
|
|
31
|
+
# @param surface [ToolSurface] the turn's (ToolSurface#deferred_for)
|
|
32
32
|
# @param conversation [Array<Hash>, nil] the request's (engine format)
|
|
33
|
-
# @param layers [Array<Symbol>] the turn's LLM context layers
|
|
34
33
|
# @return [Array<Deferred>]
|
|
35
|
-
def list(
|
|
36
|
-
deferred = builtins(
|
|
34
|
+
def list(surface, conversation)
|
|
35
|
+
deferred = builtins(surface) + sourced(surface)
|
|
37
36
|
taken = deferred.map(&:name)
|
|
38
37
|
loaded_names(conversation).each do |name|
|
|
39
|
-
next if taken.include?(name) ||
|
|
38
|
+
next if taken.include?(name) || surface.declared_entry(name)
|
|
40
39
|
|
|
41
40
|
taken << name
|
|
42
|
-
deferred << Deferred.new(name: name, description: stub_description(
|
|
41
|
+
deferred << Deferred.new(name: name, description: stub_description(surface, name), schema: STUB_SCHEMA)
|
|
43
42
|
end
|
|
44
43
|
deferred
|
|
45
44
|
end
|
|
@@ -56,30 +55,27 @@ module Samagotchi
|
|
|
56
55
|
end.uniq
|
|
57
56
|
end
|
|
58
57
|
|
|
59
|
-
def builtins(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
schema = ToolDeclarations.chat_schemas([registry[name].schema]).first
|
|
64
|
-
Deferred.new(name: name, description: schema[:description].to_s, schema: schema[:parameters])
|
|
58
|
+
def builtins(surface)
|
|
59
|
+
surface.index_only.map do |entry|
|
|
60
|
+
schema = ToolDeclarations.chat_schemas([entry.schema]).first
|
|
61
|
+
Deferred.new(name: entry.name, description: schema[:description].to_s, schema: schema[:parameters])
|
|
65
62
|
end
|
|
66
63
|
end
|
|
67
64
|
|
|
68
|
-
def sourced(
|
|
69
|
-
index =
|
|
70
|
-
return [] unless index&.deferrable &&
|
|
65
|
+
def sourced(surface)
|
|
66
|
+
index = surface.index
|
|
67
|
+
return [] unless index&.deferrable && surface.declared_entry(index.call_name)
|
|
71
68
|
|
|
72
|
-
index.deferrable.call.map do |tool|
|
|
69
|
+
index.deferrable.call(surface).map do |tool|
|
|
73
70
|
Deferred.new(name: tool.acts_as, description: tool.description, schema: tool.schema)
|
|
74
71
|
end
|
|
75
72
|
end
|
|
76
73
|
|
|
77
|
-
def stub_description(
|
|
78
|
-
|
|
79
|
-
tool = index&.unreached_tool(name)
|
|
74
|
+
def stub_description(surface, name)
|
|
75
|
+
tool = surface.index&.unreached_tool(name, surface)
|
|
80
76
|
return STUB_DESCRIPTION unless tool
|
|
81
77
|
|
|
82
|
-
ToolIndex::ToolkitGroupRefusal.for(tool,
|
|
78
|
+
ToolIndex::ToolkitGroupRefusal.for(tool, surface).stub_description
|
|
83
79
|
end
|
|
84
80
|
|
|
85
81
|
private_class_method :builtins, :sourced, :stub_description
|