samagotchi 0.154.1 → 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 +16 -1
- data/docs/architecture.md +3 -2
- data/docs/bundles.md +21 -0
- data/docs/cli.md +1 -1
- 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/engine.md +220 -99
- 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 +9 -5
- data/docs/internals/web-frontend.md +1 -1
- data/docs/internals/working-set.md +2 -1
- data/docs/plugins.md +2 -0
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +2 -2
- data/docs/testing.md +1 -1
- data/lib/samagotchi/block_renderers.rb +5 -0
- data/lib/samagotchi/bundle_command/check.rb +1 -1
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/inline-widgets/manifest.yml +11 -0
- data/lib/samagotchi/bundles/inline-widgets/plugin.rb +76 -0
- data/lib/samagotchi/bundles/system/manifest.yml +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/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/inline_widgets.rb +325 -0
- data/lib/samagotchi/kernel_loop.rb +190 -213
- data/lib/samagotchi/llm/backend.rb +3 -1
- data/lib/samagotchi/llm/chat_loop.rb +63 -41
- 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/system_prompt.rb +37 -30
- 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 +91 -100
- 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_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/markdown_renderer.rb +39 -14
- data/lib/samagotchi/web/message_display.rb +1 -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/composer_fold.js +3 -0
- 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/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
- metadata +17 -1
|
@@ -34,14 +34,15 @@ module Samagotchi
|
|
|
34
34
|
# own go to +on_stream_event+. An Interrupt (Ctrl-C) mid-batch yields an
|
|
35
35
|
# #interrupted run for every call left (the one it cut short too), so
|
|
36
36
|
# the loop's conversation answers each call, then goes on up: the turn
|
|
37
|
-
# keeps its progress (Engine#ctrl_c_messages).
|
|
37
|
+
# keeps its progress (Engine#ctrl_c_messages). +context+: the calls'
|
|
38
|
+
# turn (TurnContext), each run's (ToolRunner#run).
|
|
38
39
|
# @return [Array<ToolRun>] the runs
|
|
39
|
-
def run_batch(runner, calls, iteration:, emit:, on_stream_event:, cap:)
|
|
40
|
+
def run_batch(runner, calls, context:, iteration:, emit:, on_stream_event:, cap:)
|
|
40
41
|
# Before anything can be interrupted: the rescue counts it.
|
|
41
42
|
runs = []
|
|
42
43
|
emit.call({ type: :tool_dispatch_started, iteration: iteration, call_count: calls.length })
|
|
43
44
|
calls.each_with_index do |call, index|
|
|
44
|
-
runs << runner.run(call, iteration: iteration, call_index: index + 1, call_count: calls.length,
|
|
45
|
+
runs << runner.run(call, context: context, iteration: iteration, call_index: index + 1, call_count: calls.length,
|
|
45
46
|
on_stream_event: on_stream_event, max_tool_output_chars: cap)
|
|
46
47
|
yield runs.last, index if block_given?
|
|
47
48
|
end
|
|
@@ -14,6 +14,7 @@ require_relative "tool_output_cut"
|
|
|
14
14
|
require_relative "tool_output_files"
|
|
15
15
|
require_relative "tools/output_guardrails"
|
|
16
16
|
require_relative "tools/tool_path"
|
|
17
|
+
require_relative "tools/tool_surface"
|
|
17
18
|
require_relative "tools/memory"
|
|
18
19
|
require_relative "config"
|
|
19
20
|
require_relative "memory_bundle/index_sync"
|
|
@@ -53,14 +54,20 @@ module Samagotchi
|
|
|
53
54
|
NOT_AN_EDIT = Object.new.freeze
|
|
54
55
|
private_constant :NOT_AN_EDIT
|
|
55
56
|
|
|
56
|
-
#
|
|
57
|
-
#
|
|
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.
|
|
58
64
|
# @param clock [#call] monotonic seconds, for the approval wait and the
|
|
59
65
|
# dispatch time (after_tool_call's elapsed_ms)
|
|
60
|
-
# @param window [#call, nil] → ContextWindow::Resolved or nil:
|
|
61
|
-
# model's window
|
|
62
|
-
#
|
|
63
|
-
#
|
|
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.
|
|
64
71
|
def initialize(kernel, clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }, window: nil)
|
|
65
72
|
@kernel = kernel
|
|
66
73
|
@clock = clock
|
|
@@ -78,29 +85,30 @@ module Samagotchi
|
|
|
78
85
|
# RowRefs; saved likewise), and diff: what an edit/write changed in its
|
|
79
86
|
# file (EditPreview.change; also on tool_call_completed, never in the
|
|
80
87
|
# model's output)
|
|
81
|
-
|
|
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:)
|
|
82
90
|
# A call_tool of one of chi's own tools runs as that tool, from here
|
|
83
91
|
# on (the gate, the row, the result): rules, approvals and hooks see
|
|
84
92
|
# the real call, with via: (Tools::ToolIndex#unwrap).
|
|
85
|
-
call = unwrapped(call)
|
|
86
|
-
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))
|
|
87
95
|
# The gate runs first, so tool_call_started shows the call that runs.
|
|
88
96
|
# A tool the session's toolkit leaves out skips it: no before_tool_call
|
|
89
97
|
# hook, no approval card for a call that can't run (KernelLoop#dispatch
|
|
90
98
|
# answers it with the not-loaded error).
|
|
91
99
|
original = call.dup
|
|
92
|
-
refusal = unreached_group_refusal(call)
|
|
100
|
+
refusal = unreached_group_refusal(call, context)
|
|
93
101
|
verdict = if refusal
|
|
94
102
|
Guardrails::Verdict.new(call: call).deny!(refusal.message,
|
|
95
103
|
rule: Tools::ToolIndex::ToolkitGroupRefusal::RULE,
|
|
96
104
|
source: "core", decided_by: "core")
|
|
97
|
-
elsif left_out?(call)
|
|
105
|
+
elsif left_out?(call, context)
|
|
98
106
|
Guardrails::Verdict.new(call: call)
|
|
99
107
|
else
|
|
100
|
-
evaluate(call, iteration, params)
|
|
108
|
+
evaluate(call, context, iteration, params)
|
|
101
109
|
end
|
|
102
110
|
call = verdict.call
|
|
103
|
-
params = ToolActivity.tool_activity_params(call[:name], call, registry: tools)
|
|
111
|
+
params = ToolActivity.tool_activity_params(call[:name], call, registry: tools, surface: surface(context))
|
|
104
112
|
label = plugin_label(call[:name])
|
|
105
113
|
# Kept on the raw event for in-worker hooks and metrics, not sent to SSE clients.
|
|
106
114
|
started = { type: :tool_call_started, iteration: iteration, call_count: call_count, call_index: call_index,
|
|
@@ -110,7 +118,7 @@ module Samagotchi
|
|
|
110
118
|
# plugin tool's refs); the worker runs in its session's working
|
|
111
119
|
# directory.
|
|
112
120
|
fields = ToolRowFields.for(call[:name], call, cwd: Dir.pwd)
|
|
113
|
-
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))
|
|
114
122
|
fields[:refs] = refs unless refs.empty?
|
|
115
123
|
started.merge!(fields)
|
|
116
124
|
emit(on_stream_event, started)
|
|
@@ -121,7 +129,7 @@ module Samagotchi
|
|
|
121
129
|
waited_ms = nil
|
|
122
130
|
if verdict.ask?
|
|
123
131
|
asked_at = @clock.call
|
|
124
|
-
settle_ask(verdict)
|
|
132
|
+
settle_ask(verdict, context)
|
|
125
133
|
waited_ms = ((@clock.call - asked_at) * 1000).round
|
|
126
134
|
end
|
|
127
135
|
before = verdict.deny? ? NOT_AN_EDIT : file_before(call)
|
|
@@ -129,10 +137,10 @@ module Samagotchi
|
|
|
129
137
|
entry_before = before.equal?(NOT_AN_EDIT) ? nil : MemoryBundle::EntrySize.measure_file(Tools::ToolPath.normalize(call[:path]))
|
|
130
138
|
elapsed_ms = nil
|
|
131
139
|
if verdict.deny?
|
|
132
|
-
result = denied(call, verdict)
|
|
140
|
+
result = denied(call, verdict, context)
|
|
133
141
|
else
|
|
134
142
|
dispatched_at = @clock.call
|
|
135
|
-
result = dispatch(call)
|
|
143
|
+
result = dispatch(call, context)
|
|
136
144
|
elapsed_ms = ((@clock.call - dispatched_at) * 1000).round
|
|
137
145
|
end
|
|
138
146
|
diff = file_change(call, before)
|
|
@@ -141,7 +149,7 @@ module Samagotchi
|
|
|
141
149
|
result = with_entry_note(result, entry_before, call)
|
|
142
150
|
end
|
|
143
151
|
result = approved(result, verdict) if verdict.allow? && verdict.decided_by
|
|
144
|
-
result, images = attach_images(call, result) if result[:images]
|
|
152
|
+
result, images = attach_images(call, result, context.settings&.vision) if result[:images]
|
|
145
153
|
|
|
146
154
|
output = scrub(result[:output].to_s)
|
|
147
155
|
# The tool's own text, for after_tool_call: no "[tool]" lead, no
|
|
@@ -149,12 +157,12 @@ module Samagotchi
|
|
|
149
157
|
tool_text = output.sub(/\A\[#{Regexp.escape(call[:name].to_s)}\](?:\n| )/, "")
|
|
150
158
|
# The gate may have replaced the call (known-names corrects a near
|
|
151
159
|
# miss): one leading line tells the model what actually ran.
|
|
152
|
-
if (changed = changed_call_line(original, call))
|
|
160
|
+
if (changed = changed_call_line(original, call, context))
|
|
153
161
|
output = "#{changed}\n#{output}"
|
|
154
162
|
end
|
|
155
|
-
cap = output_cap(call, max_tool_output_chars)
|
|
163
|
+
cap = output_cap(call, max_tool_output_chars, context)
|
|
156
164
|
truncated = !cap.nil? && output.length > cap
|
|
157
|
-
capped, saved = truncated ? cut(call, output, cap, result) : output
|
|
165
|
+
capped, saved = truncated ? cut(call, output, cap, result, context) : output
|
|
158
166
|
output_file = saved.path if saved.is_a?(ToolOutputFiles::Saved)
|
|
159
167
|
|
|
160
168
|
# The call's outcome as its activity line has it (worked out from the
|
|
@@ -180,7 +188,7 @@ module Samagotchi
|
|
|
180
188
|
images: images&.any? ? images : nil,
|
|
181
189
|
shown_params: params && plugin_tool?(call[:name]) ? params : nil,
|
|
182
190
|
shown_label: label, refs: refs.empty? ? nil : refs, diff: diff, output_file: output_file,
|
|
183
|
-
loaded: result[:loaded], stamp: status == "ok" ? file_stamp(call) : nil)
|
|
191
|
+
loaded: result[:loaded], stamp: status == "ok" ? file_stamp(call, context) : nil)
|
|
184
192
|
end
|
|
185
193
|
|
|
186
194
|
private
|
|
@@ -189,8 +197,8 @@ module Samagotchi
|
|
|
189
197
|
# {path:, mtime_ns:, size:}, the path as FileRef has it (against the
|
|
190
198
|
# session's folder, no realpath). nil for any other tool, a path it
|
|
191
199
|
# can't place and a stat that fails or isn't a file's.
|
|
192
|
-
def file_stamp(call)
|
|
193
|
-
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))
|
|
194
202
|
return nil unless ref
|
|
195
203
|
|
|
196
204
|
stat = File.stat(ref.path)
|
|
@@ -209,8 +217,9 @@ module Samagotchi
|
|
|
209
217
|
# for task_wait and delegate; ref_stderr_path, a task's stderr log
|
|
210
218
|
# @return [Array(String, Object)] the text, and the saved file (or why
|
|
211
219
|
# there is none) when it was cut that way
|
|
212
|
-
def cut(call, output, cap, result)
|
|
213
|
-
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) },
|
|
214
223
|
line: (memory_cut_line(call) if call[:name] == MEMORY_READ),
|
|
215
224
|
ref: result[:ref_path], ref_stderr: result[:ref_stderr_path],
|
|
216
225
|
head_chars: Tools::OutputGuardrails.config_positive_int("execute.cut_head_chars",
|
|
@@ -223,11 +232,11 @@ module Samagotchi
|
|
|
223
232
|
# for every tool but memory_read, which memory.read_max_chars caps (0:
|
|
224
233
|
# no fixed cap) and, with none set, a known window's half (#window_guard).
|
|
225
234
|
# A nil +cap+ stays nil.
|
|
226
|
-
def output_cap(call, cap)
|
|
235
|
+
def output_cap(call, cap, context)
|
|
227
236
|
return cap unless cap && call[:name] == MEMORY_READ
|
|
228
237
|
|
|
229
238
|
fixed = Config.get("memory.read_max_chars").to_i
|
|
230
|
-
fixed.positive? ? fixed : window_guard
|
|
239
|
+
fixed.positive? ? fixed : window_guard(context.settings)
|
|
231
240
|
end
|
|
232
241
|
|
|
233
242
|
# Half the model's context window in characters, the most one memory_read
|
|
@@ -235,9 +244,9 @@ module Samagotchi
|
|
|
235
244
|
# and a retry repeats the read). nil when the window isn't known (the
|
|
236
245
|
# default isn't a window) or can't be read. It checks this result, not the
|
|
237
246
|
# context already used.
|
|
238
|
-
def window_guard
|
|
247
|
+
def window_guard(settings)
|
|
239
248
|
Degrade.call(:memory, "read_window_unknown") do
|
|
240
|
-
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))
|
|
241
250
|
return nil unless window&.tokens&.positive? && window.source != :default
|
|
242
251
|
|
|
243
252
|
(window.tokens * MemoryBundle::IndexSize.chars_per_token / 2).floor
|
|
@@ -254,12 +263,12 @@ module Samagotchi
|
|
|
254
263
|
end
|
|
255
264
|
|
|
256
265
|
# The whole output in the session's tool_output folder
|
|
257
|
-
# (ToolOutputFiles), or why it
|
|
258
|
-
# or the error's class. Never
|
|
259
|
-
# 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).
|
|
260
270
|
# @return [ToolOutputFiles::Saved, String]
|
|
261
|
-
def save_output(call, text, dry: false)
|
|
262
|
-
dir = session_dir
|
|
271
|
+
def save_output(call, text, dir, dry: false)
|
|
263
272
|
return "no session" if dir.to_s.empty?
|
|
264
273
|
return ToolOutputFiles.planned(dir, text, tool: call[:name]) if dry
|
|
265
274
|
|
|
@@ -269,15 +278,6 @@ module Samagotchi
|
|
|
269
278
|
e.class.name
|
|
270
279
|
end
|
|
271
280
|
|
|
272
|
-
# The session's folder (KernelLoop#session_dir), nil for a bare kernel
|
|
273
|
-
# or a lookup that failed.
|
|
274
|
-
def session_dir
|
|
275
|
-
@kernel.respond_to?(:session_dir) ? @kernel.session_dir&.call : nil
|
|
276
|
-
rescue StandardError => e
|
|
277
|
-
Log.warn(:turn, "tool_output_save_failed", error: e.class.name)
|
|
278
|
-
nil
|
|
279
|
-
end
|
|
280
|
-
|
|
281
281
|
# edit/write only: the file just before the call runs. The row diffs it
|
|
282
282
|
# with the file after, whatever the result says, so an edit that fails
|
|
283
283
|
# after writing still shows its change and one that wrote nothing shows
|
|
@@ -351,8 +351,8 @@ module Samagotchi
|
|
|
351
351
|
# and keeps the text and the other images; for read, whose text only
|
|
352
352
|
# says the image is attached, that line replaces the text.
|
|
353
353
|
# @return [Array(Hash, Array<Hash>)] the result and its refs
|
|
354
|
-
|
|
355
|
-
|
|
354
|
+
# +vision+: the turn's VisionContext (TurnSettings#vision).
|
|
355
|
+
def attach_images(call, result, vision)
|
|
356
356
|
reason = if vision.nil? then "images can't be attached here"
|
|
357
357
|
elsif !vision.sendable? then ImagePlan::CANT_SEE
|
|
358
358
|
end
|
|
@@ -397,17 +397,19 @@ module Samagotchi
|
|
|
397
397
|
[result[:output], *notes].join("\n")
|
|
398
398
|
end
|
|
399
399
|
|
|
400
|
-
# The session's folder (the
|
|
400
|
+
# The session's folder (the turn's TurnContext#cwd), else the
|
|
401
401
|
# process's: a bare kernel (specs) has none.
|
|
402
|
-
def session_cwd
|
|
403
|
-
cwd =
|
|
402
|
+
def session_cwd(context)
|
|
403
|
+
cwd = context.cwd
|
|
404
404
|
cwd.to_s.empty? ? Dir.pwd : cwd
|
|
405
405
|
end
|
|
406
406
|
|
|
407
407
|
# The kernel's tools, for the activity line of a tool that isn't built in.
|
|
408
408
|
def tools = @kernel.tools
|
|
409
409
|
|
|
410
|
-
|
|
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)
|
|
411
413
|
|
|
412
414
|
# The call its tool's entry unwraps it to (call_tool on a built-in),
|
|
413
415
|
# else the call as it is. On a host that loads tools, a direct call of
|
|
@@ -415,12 +417,12 @@ module Samagotchi
|
|
|
415
417
|
# (Registry::Index#wrap): call_tool's row, rules and approvals. One
|
|
416
418
|
# that fails runs as it is: call_tool then refuses a built-in (fail
|
|
417
419
|
# closed).
|
|
418
|
-
def unwrapped(call)
|
|
419
|
-
|
|
420
|
-
return call unless registry.respond_to?(:offered) && !call[:name].nil?
|
|
420
|
+
def unwrapped(call, context)
|
|
421
|
+
return call if tools.nil? || call[:name].nil?
|
|
421
422
|
|
|
422
|
-
|
|
423
|
-
|
|
423
|
+
surface = surface(context)
|
|
424
|
+
call = wrapped(surface, call)
|
|
425
|
+
surface.declared_entry(call[:name])&.unwrap&.call(call, surface) || call
|
|
424
426
|
rescue StandardError => e
|
|
425
427
|
Log.warn(:turn, "tool_unwrap_failed", tool: call[:name].to_s, error: "#{e.class}: #{e.message}")
|
|
426
428
|
call
|
|
@@ -428,11 +430,11 @@ module Samagotchi
|
|
|
428
430
|
|
|
429
431
|
# +call+ as the call_tool call it runs as when it names a deferred
|
|
430
432
|
# source tool, on a host that loads tools; else as it is.
|
|
431
|
-
def wrapped(
|
|
432
|
-
wrap =
|
|
433
|
-
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])
|
|
434
436
|
|
|
435
|
-
wrap.call(call) || call
|
|
437
|
+
wrap.call(call, surface) || call
|
|
436
438
|
end
|
|
437
439
|
|
|
438
440
|
# A call that can't run in this turn as it is, by its name: a real tool
|
|
@@ -440,23 +442,25 @@ module Samagotchi
|
|
|
440
442
|
# (via:) that the toolkit doesn't reach that way (a hook's rewrite). On
|
|
441
443
|
# a host that loads tools, a built-in the toolkit reaches runs by its
|
|
442
444
|
# own name (KernelLoop#dispatch takes it too).
|
|
443
|
-
def left_out?(call)
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
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])
|
|
448
451
|
|
|
449
|
-
|
|
452
|
+
surface.left_out?(call[:name])
|
|
450
453
|
end
|
|
451
454
|
|
|
452
|
-
def unreached_group_refusal(call)
|
|
455
|
+
def unreached_group_refusal(call, context)
|
|
453
456
|
# A review turn's boundary denies it first (and words why); a pair
|
|
454
457
|
# turn gets the owner's refusal.
|
|
455
|
-
return if
|
|
458
|
+
return if context.participant? && context.participant_mode != ParticipantMode::PAIR
|
|
459
|
+
return if tools.nil?
|
|
456
460
|
|
|
457
|
-
|
|
458
|
-
index =
|
|
459
|
-
return unless index
|
|
461
|
+
surface = surface(context)
|
|
462
|
+
index = surface.index
|
|
463
|
+
return unless index
|
|
460
464
|
|
|
461
465
|
name = if call[:name] == index.call_name
|
|
462
466
|
args = call[:args].is_a?(Hash) ? call[:args] : {}
|
|
@@ -464,24 +468,10 @@ module Samagotchi
|
|
|
464
468
|
else
|
|
465
469
|
call[:name]
|
|
466
470
|
end
|
|
467
|
-
tool = index.unreached_tool(name)
|
|
468
|
-
tool && Tools::ToolIndex::ToolkitGroupRefusal.for(tool,
|
|
471
|
+
tool = index.unreached_tool(name, surface)
|
|
472
|
+
tool && Tools::ToolIndex::ToolkitGroupRefusal.for(tool, surface)
|
|
469
473
|
end
|
|
470
474
|
|
|
471
|
-
def participant_turn?
|
|
472
|
-
@kernel.respond_to?(:participant_turn?) && @kernel.participant_turn?
|
|
473
|
-
end
|
|
474
|
-
|
|
475
|
-
# The running participant turn's mode (KernelLoop#participant_mode,
|
|
476
|
-
# GuardrailWiring#turn_mode), nil without one.
|
|
477
|
-
def participant_mode
|
|
478
|
-
lookup = @kernel.respond_to?(:participant_mode) ? @kernel.participant_mode : nil
|
|
479
|
-
lookup&.call
|
|
480
|
-
end
|
|
481
|
-
|
|
482
|
-
# Whether the turn's host loads tools (TurnSettings#loads_tools).
|
|
483
|
-
def loads_tools? = @kernel.respond_to?(:turn_settings) && @kernel.turn_settings&.loads_tools == true
|
|
484
|
-
|
|
485
475
|
# A tool the registry has from a plugin (not core, not unknown).
|
|
486
476
|
def plugin_tool?(name)
|
|
487
477
|
entry = tools && !name.nil? ? tools[name] : nil
|
|
@@ -495,29 +485,30 @@ module Samagotchi
|
|
|
495
485
|
# asked for: "ran as: <tool> <changed args>", or nil when nothing
|
|
496
486
|
# changed. The args are the changed call's activity params, so the line
|
|
497
487
|
# stays short.
|
|
498
|
-
def changed_call_line(original, call)
|
|
488
|
+
def changed_call_line(original, call, context)
|
|
499
489
|
return nil if original == call
|
|
500
490
|
|
|
501
|
-
params = ToolActivity.tool_activity_params(call[:name], call, registry: tools)
|
|
491
|
+
params = ToolActivity.tool_activity_params(call[:name], call, registry: tools, surface: surface(context))
|
|
502
492
|
["ran as: #{call[:name]}", params].compact.join(" ")
|
|
503
493
|
end
|
|
504
494
|
|
|
505
|
-
# The
|
|
506
|
-
# 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.
|
|
507
498
|
def gate
|
|
508
|
-
@kernel.guardrail_gate ||
|
|
499
|
+
@kernel.guardrail_gate || Guardrails::Gate.new(hooks: @kernel.hooks, tools: tools)
|
|
509
500
|
end
|
|
510
501
|
|
|
511
502
|
# A gate that fails denies the call (fail closed).
|
|
512
|
-
def evaluate(call, iteration, params)
|
|
513
|
-
gate.evaluate(call, iteration: iteration, params: params)
|
|
503
|
+
def evaluate(call, context, iteration, params)
|
|
504
|
+
gate.evaluate(call, context: context, iteration: iteration, params: params)
|
|
514
505
|
rescue StandardError => e
|
|
515
506
|
Guardrails::Verdict.new(call: call).deny!("the guardrail check failed: #{e.class}: #{e.message}",
|
|
516
507
|
decided_by: "core")
|
|
517
508
|
end
|
|
518
509
|
|
|
519
|
-
def settle_ask(verdict)
|
|
520
|
-
gate.settle_ask(verdict)
|
|
510
|
+
def settle_ask(verdict, context)
|
|
511
|
+
gate.settle_ask(verdict, context: context)
|
|
521
512
|
rescue StandardError => e
|
|
522
513
|
verdict.settle!(:deny, decided_by: "core", note: "The approval failed (#{e.class}: #{e.message}).")
|
|
523
514
|
end
|
|
@@ -531,10 +522,10 @@ module Samagotchi
|
|
|
531
522
|
|
|
532
523
|
# A legacy veto keeps its old text; a verdict's deny tells the model
|
|
533
524
|
# who decided and not to route around it.
|
|
534
|
-
def denied(call, verdict)
|
|
525
|
+
def denied(call, verdict, context)
|
|
535
526
|
if verdict.rule == Tools::ToolIndex::ToolkitGroupRefusal::RULE
|
|
536
527
|
output = "[#{call[:name]}] #{verdict.reason}"
|
|
537
|
-
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))
|
|
538
529
|
return { output: output, activity: activity.merge(status: "blocked", guardrail: verdict.to_activity) }
|
|
539
530
|
end
|
|
540
531
|
|
|
@@ -544,14 +535,14 @@ module Samagotchi
|
|
|
544
535
|
else
|
|
545
536
|
"[#{call[:name]}] Error: #{verdict.deny_text}"
|
|
546
537
|
end
|
|
547
|
-
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))
|
|
548
539
|
{ output: output, activity: activity.merge(status: "blocked", guardrail: verdict.to_activity) }
|
|
549
540
|
end
|
|
550
541
|
|
|
551
542
|
# KernelLoop#dispatch turns tool errors into "[name] Error: …" itself;
|
|
552
543
|
# this rescue only catches a failing dispatcher.
|
|
553
|
-
def dispatch(call)
|
|
554
|
-
@kernel.dispatch_tool_call(call)
|
|
544
|
+
def dispatch(call, context)
|
|
545
|
+
@kernel.dispatch_tool_call(call, context)
|
|
555
546
|
rescue StandardError => e
|
|
556
547
|
{ output: "[#{call[:name]}] Error: #{e.class}: #{e.message}", activity: nil }
|
|
557
548
|
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
|