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
|
@@ -24,6 +24,7 @@ require_relative "reasoning_history"
|
|
|
24
24
|
require_relative "thought_stream_splitter"
|
|
25
25
|
require_relative "tools/builtins"
|
|
26
26
|
require_relative "tools/builtin_calls"
|
|
27
|
+
require_relative "tools/tool_surface"
|
|
27
28
|
require_relative "muted_memories"
|
|
28
29
|
require_relative "tool_activity"
|
|
29
30
|
require_relative "tool_runner"
|
|
@@ -38,6 +39,7 @@ require_relative "answer_display"
|
|
|
38
39
|
require_relative "message_time"
|
|
39
40
|
require_relative "llm/model_result"
|
|
40
41
|
require_relative "llm/turn_settings"
|
|
42
|
+
require_relative "turn_context"
|
|
41
43
|
require_relative "llm/stream_drops"
|
|
42
44
|
|
|
43
45
|
module Samagotchi
|
|
@@ -59,30 +61,34 @@ module Samagotchi
|
|
|
59
61
|
# The built-in tool classes (Tools::Builtins registers them).
|
|
60
62
|
TOOLS = Tools::Builtins::CLASSES
|
|
61
63
|
|
|
62
|
-
# What a tool handler (call, kctx) gets
|
|
63
|
-
#
|
|
64
|
-
#
|
|
64
|
+
# What a tool handler (call, kctx) gets: the kernel's reminder store
|
|
65
|
+
# and peers, the call's turn (+context+, a TurnContext: the participant's
|
|
66
|
+
# mode, the model key), and the memory read and ask-user flows that need
|
|
67
|
+
# them. One per dispatched call.
|
|
65
68
|
class ToolContext
|
|
66
|
-
def initialize(kernel)
|
|
69
|
+
def initialize(kernel, context)
|
|
67
70
|
@kernel = kernel
|
|
71
|
+
@context = context
|
|
68
72
|
end
|
|
69
73
|
|
|
70
74
|
def reminder_store = @kernel.reminder_store
|
|
71
|
-
def peers = @kernel.peers
|
|
75
|
+
def peers = @kernel.services.peers
|
|
72
76
|
|
|
73
|
-
# The
|
|
74
|
-
# :
|
|
75
|
-
def participant_mode = @
|
|
76
|
-
|
|
77
|
+
# The participant turn's mode (TurnContext#participant_mode: :pair,
|
|
78
|
+
# :review), nil for the owner's turns.
|
|
79
|
+
def participant_mode = @context.participant_mode
|
|
80
|
+
# The memory overlay key (TurnContext#model_key, frozen at the turn's
|
|
81
|
+
# generate).
|
|
82
|
+
def model_key = @context.model_key
|
|
77
83
|
|
|
78
84
|
# memory_read with the session's mutes applied: a blank name (the index)
|
|
79
85
|
# loses the muted memories' lines; a muted name in a comma list is
|
|
80
86
|
# refused with its own error line and the rest is read as usual.
|
|
81
87
|
def muted_memory_read(call)
|
|
82
|
-
muted = Array(@kernel.muted_memory_names)
|
|
88
|
+
muted = Array(@kernel.services.muted_memory_names)
|
|
83
89
|
content = call[:content].to_s
|
|
84
90
|
read = lambda do |names|
|
|
85
|
-
Tools::MemoryRead.call(names, scope: call[:scope], model_key: model_key, fallback_model_key: @
|
|
91
|
+
Tools::MemoryRead.call(names, scope: call[:scope], model_key: model_key, fallback_model_key: @context.fallback_model_key)
|
|
86
92
|
end
|
|
87
93
|
return read.call(content) if muted.empty?
|
|
88
94
|
return MutedMemories.filter_index(read.call(content), muted) if content.strip.empty?
|
|
@@ -98,14 +104,14 @@ module Samagotchi
|
|
|
98
104
|
end
|
|
99
105
|
|
|
100
106
|
# ask_user_question: validate the call, then hand the payload to the
|
|
101
|
-
# Engine's question flow (the kernel's question_handler, which blocks
|
|
107
|
+
# Engine's question flow (the kernel's Services#question_handler, which blocks
|
|
102
108
|
# until the user answers). Without one (headless), the payload as JSON
|
|
103
109
|
# so the model sees the options and can ask in plain text.
|
|
104
110
|
def ask_user_question(call)
|
|
105
111
|
payload = Tools::AskUserQuestion.validate(call)
|
|
106
112
|
return payload if payload.is_a?(String)
|
|
107
113
|
|
|
108
|
-
handler = @kernel.question_handler
|
|
114
|
+
handler = @kernel.services.question_handler
|
|
109
115
|
return JSON.pretty_generate(payload) unless handler
|
|
110
116
|
|
|
111
117
|
handler.call(payload).to_s
|
|
@@ -115,7 +121,12 @@ module Samagotchi
|
|
|
115
121
|
|
|
116
122
|
# forget_outputs: the forget layer's call on the running turn's
|
|
117
123
|
# conversation (KernelLoop#forget_outputs).
|
|
118
|
-
def forget_outputs(call) = @kernel.forget_outputs(call)
|
|
124
|
+
def forget_outputs(call) = @kernel.forget_outputs(call, @context)
|
|
125
|
+
|
|
126
|
+
# The turn's Tools::ToolSurface (TurnContext#surface), else one
|
|
127
|
+
# computed now from the kernel's tools (a bare kernel): what the tool
|
|
128
|
+
# index's handlers (find_tools, call_tool) read.
|
|
129
|
+
def surface = Tools::ToolSurface.of(@context, @kernel.tools)
|
|
119
130
|
end
|
|
120
131
|
|
|
121
132
|
DEFAULT_MAX_TOOL_OUTPUT_CHARS = 10_000
|
|
@@ -124,35 +135,82 @@ module Samagotchi
|
|
|
124
135
|
MALFORMED_TAIL_CHARS = 400
|
|
125
136
|
QWEN_INCOMPLETE_TOOL_CALL_RECOVERY_PROMPT = "Continue the previous assistant message by finishing the open <tool_call> XML block. Output only the remaining XML needed to complete the tool call."
|
|
126
137
|
|
|
127
|
-
#
|
|
128
|
-
#
|
|
138
|
+
# What a kernel holds of its Engine for the Engine's life, one value
|
|
139
|
+
# (Engine#wire_kernel builds it; a spec's kernel takes one through
|
|
140
|
+
# #attach): nothing per turn (a turn's TurnContext goes down #run).
|
|
141
|
+
#
|
|
142
|
+
# - hooks: the Hooks::Registry the loop fires (nil: none); the Engine's
|
|
143
|
+
# - tools: the Tools::Registry #dispatch runs (the built-ins by default)
|
|
144
|
+
# - reminder_store: the ReminderStore the reminder tools write (nil: none)
|
|
145
|
+
# - muted_memory_names: the session's muted memories (normalized names,
|
|
146
|
+
# MutedMemories), refused by memory_read; nil: none
|
|
147
|
+
# - question_handler: answers ask_user_question (payload → answer
|
|
148
|
+
# string; the Engine's blocking request_question); nil: headless
|
|
149
|
+
# - peers: Tools::Peers (or the Engine's PeerView): the session
|
|
150
|
+
# list_sessions and send_note speak for; nil outside a session
|
|
151
|
+
# - gate: the Guardrails::Gate ToolRunner asks before each call; nil:
|
|
152
|
+
# ToolRunner's own, hooks only
|
|
153
|
+
# - warmup: the Engine's PromptWarmup, asked for a slot pin per request
|
|
154
|
+
# (#take_pin) while the turn-end warm-up runs; nil: never pinned
|
|
155
|
+
# - generation_guard: called before each generation with the turn's
|
|
156
|
+
# TurnContext, before the before_generation hook; the Engine's check
|
|
157
|
+
# that may stop the turn (a pair turn past its participant's cap,
|
|
158
|
+
# Engine#guard_participant_spend); nil: none
|
|
159
|
+
#
|
|
160
|
+
# The client is not one of them: a model switch swaps it
|
|
161
|
+
# (#client=, Engine#sync_kernel_client!).
|
|
162
|
+
Services = Data.define(:hooks, :tools, :reminder_store, :muted_memory_names, :question_handler, :peers, :gate,
|
|
163
|
+
:warmup, :generation_guard) do
|
|
164
|
+
# A bare kernel's (specs, a kernel built without its Engine): the
|
|
165
|
+
# built-in tools and nothing else, or the +fields+ given.
|
|
166
|
+
# @return [Services]
|
|
167
|
+
def self.none(**fields)
|
|
168
|
+
new(hooks: nil, tools: Tools::Builtins.default, reminder_store: nil, muted_memory_names: nil,
|
|
169
|
+
question_handler: nil, peers: nil, gate: nil, warmup: nil, generation_guard: nil).with(**fields)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# @param services [Services, nil] what the kernel holds of its Engine
|
|
174
|
+
# (Services.none by default: the built-in tools alone)
|
|
129
175
|
# @param sleeper [#call, nil] waits the given seconds before a dropped
|
|
130
176
|
# stream's step is asked again (specs pass one that doesn't sleep)
|
|
131
|
-
def initialize(client: nil, profile: nil, model_name: nil,
|
|
132
|
-
sleeper: nil)
|
|
177
|
+
def initialize(client: nil, profile: nil, model_name: nil, model_key: nil, services: nil, sleeper: nil)
|
|
133
178
|
@client = client || Client.new
|
|
134
179
|
@sleeper = sleeper || ->(seconds) { sleep(seconds) }
|
|
135
|
-
@
|
|
180
|
+
@services = services || Services.none
|
|
136
181
|
resolved_model_name = ModelProfile.required_model_name(model_name)
|
|
137
|
-
# The
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
@turn_settings = LLM::TurnSettings.none.with(model_name: resolved_model_name)
|
|
182
|
+
# The context of the run in progress, else of the last run (#context):
|
|
183
|
+
# a kernel that ran none has its own (no turn, no settings but its
|
|
184
|
+
# model name, its model key). #run takes the one it is given.
|
|
185
|
+
@context = TurnContext.none(settings: LLM::TurnSettings.none.with(model_name: resolved_model_name),
|
|
186
|
+
model_key: model_key)
|
|
143
187
|
@profile = profile ? ModelProfile.normalize(profile) : ModelProfile.from_model_name(resolved_model_name)
|
|
144
188
|
# Where @profile came from, as /stats shows it (a Resolution's label
|
|
145
189
|
# once the Engine resolves one, see #use_profile!).
|
|
146
190
|
@profile_source = profile ? "given" : "name"
|
|
147
|
-
@hooks = hooks
|
|
148
|
-
@reminder_store = reminder_store
|
|
149
|
-
@model_key = model_key
|
|
150
|
-
@model_key_fallback = nil
|
|
151
191
|
end
|
|
152
192
|
|
|
153
|
-
# @return [
|
|
154
|
-
|
|
155
|
-
|
|
193
|
+
# @return [Services] what the kernel holds of its Engine (#attach)
|
|
194
|
+
attr_reader :services
|
|
195
|
+
|
|
196
|
+
# Hand the kernel its Engine's services, all at once: the Engine does
|
|
197
|
+
# for a kernel it is given (specs) what KernelLoop.new does for its own.
|
|
198
|
+
# @param services [Services]
|
|
199
|
+
# @return [Services]
|
|
200
|
+
def attach(services)
|
|
201
|
+
@services = services
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# @return [Hooks::Registry, nil] the hooks the loop fires (Services#hooks)
|
|
205
|
+
def hooks = @services.hooks
|
|
206
|
+
# @return [Tools::Registry] the tools #dispatch runs (Services#tools)
|
|
207
|
+
def tools = @services.tools
|
|
208
|
+
# @return [ReminderStore, nil] the store the reminder tools write
|
|
209
|
+
# (Services#reminder_store)
|
|
210
|
+
def reminder_store = @services.reminder_store
|
|
211
|
+
# @return [Guardrails::Gate, nil] the gate ToolRunner asks before each
|
|
212
|
+
# call (Services#gate)
|
|
213
|
+
def guardrail_gate = @services.gate
|
|
156
214
|
|
|
157
215
|
# @return [Float, nil] the last counted request's chars per token
|
|
158
216
|
# (ContextStatus#counted_chars_per_token), for the next turn's first
|
|
@@ -162,79 +220,43 @@ module Samagotchi
|
|
|
162
220
|
# @return [ModelProfile] the active prompt profile
|
|
163
221
|
attr_reader :profile
|
|
164
222
|
|
|
165
|
-
#
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
#
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
attr_accessor :client, :model_key
|
|
173
|
-
# @return [String, nil] the key whose overlay memory_read takes when
|
|
174
|
-
# #model_key has none (#sync_model_key!)
|
|
175
|
-
attr_reader :model_key_fallback
|
|
176
|
-
# @return [Array<String>, nil] the session's muted memories (normalized
|
|
177
|
-
# names, see MutedMemories); memory_read refuses them. The Engine sets it.
|
|
178
|
-
attr_accessor :muted_memory_names
|
|
179
|
-
# @return [Proc, nil] answers ask_user_question (payload → answer string);
|
|
180
|
-
# Engine sets it to its blocking request_question.
|
|
181
|
-
attr_accessor :question_handler
|
|
182
|
-
# The Guardrails::Gate ToolRunner asks before each call; the Engine sets
|
|
183
|
-
# it (nil: ToolRunner's own, hooks only).
|
|
184
|
-
attr_accessor :guardrail_gate
|
|
185
|
-
# Whether the current turn belongs to a participant, so pre-gate
|
|
186
|
-
# refusals can defer to ReviewBoundary.
|
|
187
|
-
attr_accessor :participant_turn
|
|
188
|
-
# The running participant turn's mode (a callable → :pair, :review or
|
|
189
|
-
# nil: GuardrailWiring#turn_mode): ToolRunner reads it. The Engine sets it.
|
|
190
|
-
attr_accessor :participant_mode
|
|
191
|
-
# Called before each generation, before the before_generation hook (a
|
|
192
|
-
# callable, nil: none): the Engine's check that may stop the turn (a
|
|
193
|
-
# pair turn past its participant's cap, Engine#guard_participant_spend).
|
|
194
|
-
attr_accessor :generation_guard
|
|
195
|
-
|
|
196
|
-
# The generation guard, if any; one that raises never breaks the turn.
|
|
223
|
+
# The model client; a model switch swaps it (Engine#sync_kernel_client!).
|
|
224
|
+
attr_accessor :client
|
|
225
|
+
|
|
226
|
+
# @return [String, nil] the model key a kernel was built with (its own
|
|
227
|
+
# #context's, for a run given none)
|
|
228
|
+
def model_key = @context.model_key
|
|
229
|
+
|
|
197
230
|
# The model's context window as this loop knows it (ContextWindow.resolve
|
|
198
|
-
# on the client and
|
|
199
|
-
# memory_read's overflow guard. nil when it can't be read.
|
|
231
|
+
# on the client and +settings+, a turn's LLM::TurnSettings): ToolRunner
|
|
232
|
+
# halves it for memory_read's overflow guard. nil when it can't be read.
|
|
200
233
|
# @return [ContextWindow::Resolved, nil]
|
|
201
|
-
def tool_output_window
|
|
234
|
+
def tool_output_window(settings = turn_settings)
|
|
202
235
|
Degrade.call(:model, "context_window_unresolved") do
|
|
203
|
-
ContextWindow.resolve(client: @client, model:
|
|
236
|
+
ContextWindow.resolve(client: @client, model: settings&.model_name, setting: settings&.window_setting)
|
|
204
237
|
end
|
|
205
238
|
end
|
|
206
239
|
|
|
207
|
-
|
|
208
|
-
|
|
240
|
+
# The generation guard for +context+'s turn, if any; one that raises
|
|
241
|
+
# never breaks the turn.
|
|
242
|
+
def guard_generation(context = @context)
|
|
243
|
+
@services.generation_guard&.call(context)
|
|
209
244
|
rescue StandardError => e
|
|
210
245
|
Log.warn(:turn, "generation_guard_failed", error: "#{e.class}: #{e.message}")
|
|
211
246
|
nil
|
|
212
247
|
end
|
|
213
248
|
|
|
214
|
-
|
|
215
|
-
#
|
|
216
|
-
#
|
|
217
|
-
#
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
#
|
|
221
|
-
#
|
|
222
|
-
#
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
# which forget_outputs refuses (LLMContextForget); the Engine sets it.
|
|
226
|
-
# nil: none.
|
|
227
|
-
attr_accessor :working_set_pins
|
|
228
|
-
# The session's folder (a callable → String or nil), where ToolRunner
|
|
229
|
-
# saves a cut output whole (ToolOutputFiles); the Engine sets it. nil:
|
|
230
|
-
# none (a bare kernel), the cut says the output wasn't saved.
|
|
231
|
-
attr_accessor :session_dir
|
|
232
|
-
# Tools::Peers (or the Engine's live view of it): the session
|
|
233
|
-
# list_sessions and send_note speak for; nil outside a session.
|
|
234
|
-
attr_accessor :peers
|
|
235
|
-
# The Engine's PromptWarmup: a request asks it for a slot pin
|
|
236
|
-
# (#take_pin) while the turn-end warm-up still runs; nil: never pinned.
|
|
237
|
-
attr_accessor :warmup
|
|
249
|
+
# @return [TurnContext] the context of the run in progress, else of the
|
|
250
|
+
# last run (the kernel's own before any): its turn's settings,
|
|
251
|
+
# session folders and participant mode. Read on the turn's thread
|
|
252
|
+
# only; another thread reads the Engine's (Engine#turn_context).
|
|
253
|
+
attr_reader :context
|
|
254
|
+
|
|
255
|
+
# @return [LLM::TurnSettings] the vision, sampling, thinking and model
|
|
256
|
+
# name of the run in progress, else of the last one (#context's; #run
|
|
257
|
+
# sets the model name); the thinking level is its own field, since the
|
|
258
|
+
# native path sends the sampling as is.
|
|
259
|
+
def turn_settings = @context.settings
|
|
238
260
|
# The turn a forget_outputs call works on (LLMContextForget::Turn: its
|
|
239
261
|
# conversation and ContextStatus); both loops set it as a turn starts.
|
|
240
262
|
attr_accessor :llm_context_turn
|
|
@@ -265,10 +287,14 @@ module Samagotchi
|
|
|
265
287
|
# run it resumes started in +messages+; that run's model messages keep
|
|
266
288
|
# their thought blocks (#prepare_conversation), as within one run. nil:
|
|
267
289
|
# every earlier thought is stripped.
|
|
290
|
+
# @param context [TurnContext] the run's turn (Engine#generate's); a
|
|
291
|
+
# bare kernel's last (#context) by default. Its settings take the
|
|
292
|
+
# run's model name: this run's own copy, never published.
|
|
268
293
|
# @return [LLM::ModelResult] final visible response with continuation metadata
|
|
269
294
|
def run(messages, max_iterations: IterationLimit::DEFAULT, on_stream_event: nil, cancel_controller: nil, model_name: nil, max_tool_output_chars: nil, pending_input: nil,
|
|
270
|
-
reasoning_since: nil)
|
|
271
|
-
|
|
295
|
+
reasoning_since: nil, context: @context)
|
|
296
|
+
settings = context.settings || LLM::TurnSettings.none
|
|
297
|
+
@context = context.with(settings: settings.with(model_name: completion_model_name(model_name)))
|
|
272
298
|
@last_slot = nil
|
|
273
299
|
turn = start_turn(messages, on_stream_event: on_stream_event, cancel_controller: cancel_controller,
|
|
274
300
|
pending_input: pending_input, cap: resolve_output_char_cap(max_tool_output_chars),
|
|
@@ -304,13 +330,6 @@ module Samagotchi
|
|
|
304
330
|
@profile_source = resolution.label
|
|
305
331
|
end
|
|
306
332
|
|
|
307
|
-
# @param fallback [String, nil] a key whose overlay memory_read takes
|
|
308
|
-
# when +key+ has none (the alias a model was typed as)
|
|
309
|
-
def sync_model_key!(key, fallback: nil)
|
|
310
|
-
@model_key = key
|
|
311
|
-
@model_key_fallback = fallback
|
|
312
|
-
end
|
|
313
|
-
|
|
314
333
|
# Resolve the per-output character cap for the tool outputs the model
|
|
315
334
|
# gets back and the emitted tool call events.
|
|
316
335
|
#
|
|
@@ -354,10 +373,11 @@ module Samagotchi
|
|
|
354
373
|
|
|
355
374
|
def start_turn(messages, on_stream_event:, cancel_controller:, pending_input:, cap:, reasoning_since: nil)
|
|
356
375
|
conversation = prepare_conversation(messages, keep_thought_from: ReasoningHistory.run_start(messages, reasoning_since))
|
|
357
|
-
context = ContextStatus.new(conversation: conversation, llm_context:
|
|
376
|
+
context = ContextStatus.new(conversation: conversation, llm_context: turn_settings.llm_context,
|
|
358
377
|
counted_chars_per_token: @counted_chars_per_token)
|
|
359
378
|
emit = ->(event) { emit_stream_event(on_stream_event, event) }
|
|
360
|
-
@llm_context_turn = LLMContextForget::Turn.new(conversation: conversation, context: context, emit: emit
|
|
379
|
+
@llm_context_turn = LLMContextForget::Turn.new(conversation: conversation, context: context, emit: emit,
|
|
380
|
+
turn_id: @context.id)
|
|
361
381
|
Turn.new(
|
|
362
382
|
conversation: conversation, context: context,
|
|
363
383
|
empty_retry: EmptyAnswerRetry.new, empty_steps: [], tool_activity: [], buffer: +"", streamed_thinking: +"",
|
|
@@ -366,8 +386,8 @@ module Samagotchi
|
|
|
366
386
|
# answers at once; on, an open one where the server's template has
|
|
367
387
|
# it. Kept in the turn's model messages, so each tool-loop prompt
|
|
368
388
|
# starts with what the server already has cached.
|
|
369
|
-
prefill: native_prefill(
|
|
370
|
-
pending_tool_calls: false, model_name:
|
|
389
|
+
prefill: native_prefill(turn_settings.thinking || Thinking::DEFAULT),
|
|
390
|
+
pending_tool_calls: false, model_name: turn_settings.model_name, pending_input: pending_input,
|
|
371
391
|
on_stream_event: on_stream_event, cancel_controller: cancel_controller, cap: cap,
|
|
372
392
|
emit: emit
|
|
373
393
|
)
|
|
@@ -385,7 +405,7 @@ module Samagotchi
|
|
|
385
405
|
|
|
386
406
|
def model_props
|
|
387
407
|
Degrade.call(:model, "server_props_unreadable") do
|
|
388
|
-
@client.respond_to?(:server_props) ? @client.server_props(model:
|
|
408
|
+
@client.respond_to?(:server_props) ? @client.server_props(model: turn_settings.model_name) : nil
|
|
389
409
|
end
|
|
390
410
|
end
|
|
391
411
|
|
|
@@ -435,7 +455,7 @@ module Samagotchi
|
|
|
435
455
|
apply_llm_context!(turn.conversation, context: turn.context, emit: turn.emit)
|
|
436
456
|
prompt, images = format_prompt(turn)
|
|
437
457
|
image_tokens = images.empty? ? 0 : ImagePlan.estimated_tokens(turn.conversation)
|
|
438
|
-
window = ContextWindow.resolve(client: @client, model: turn.model_name, setting:
|
|
458
|
+
window = ContextWindow.resolve(client: @client, model: turn.model_name, setting: turn_settings.window_setting)
|
|
439
459
|
emit_context_status_event(turn.on_stream_event, turn.context, prompt, iteration_index: turn.iteration - 1,
|
|
440
460
|
window: window, image_tokens: image_tokens)
|
|
441
461
|
if (line = turn.context.take_guidance)
|
|
@@ -450,7 +470,7 @@ module Samagotchi
|
|
|
450
470
|
|
|
451
471
|
def format_prompt(turn)
|
|
452
472
|
Prompt.format_with_images(llm_context_view.messages(turn.conversation), profile: @profile,
|
|
453
|
-
vision:
|
|
473
|
+
vision: turn_settings.vision, prefill: turn.prefill)
|
|
454
474
|
end
|
|
455
475
|
|
|
456
476
|
# One streamed request, under the generation's own controller: a
|
|
@@ -460,8 +480,8 @@ module Samagotchi
|
|
|
460
480
|
def generate(turn, request)
|
|
461
481
|
# Before the generation starts: a hook that waits (a question) isn't
|
|
462
482
|
# the generation's time, and a Stop there leaves none started.
|
|
463
|
-
guard_generation
|
|
464
|
-
fire_hook(:before_generation, { type: :before_generation, iteration: turn.iteration }) if
|
|
483
|
+
guard_generation(@context)
|
|
484
|
+
fire_hook(:before_generation, { type: :before_generation, iteration: turn.iteration }) if hooks
|
|
465
485
|
raise Client::RequestCancelled, turn.cancel_controller.reason if turn.cancel_controller&.cancelled?
|
|
466
486
|
|
|
467
487
|
# Each ask splits its own stream (run_generation rebuilds the
|
|
@@ -469,7 +489,7 @@ module Samagotchi
|
|
|
469
489
|
# leave the next one "inside" it.
|
|
470
490
|
emit(turn, type: :generation_started, iteration: turn.iteration, context_window_tokens: request.window.tokens,
|
|
471
491
|
context_window_source: request.window.source, profile: @profile.name, profile_source: @profile_source,
|
|
472
|
-
price:
|
|
492
|
+
price: turn_settings.price&.to_h)
|
|
473
493
|
# This generation's own server counts (the run-long ones in the
|
|
474
494
|
# context tracker can be an earlier one's), and the thinking it
|
|
475
495
|
# streamed, for the log (a stuck thinking generation shows as
|
|
@@ -505,7 +525,7 @@ module Samagotchi
|
|
|
505
525
|
# The empty-retry sampling is taken once, before the ask-again loop:
|
|
506
526
|
# a drop's restart must still carry the nudge's 0.6 (as ChatLoop does
|
|
507
527
|
# per step), not lose it when request_sampling is asked again.
|
|
508
|
-
sampling = turn.empty_retry.request_sampling(
|
|
528
|
+
sampling = turn.empty_retry.request_sampling(turn_settings.sampling)
|
|
509
529
|
count = LLM::StreamDrops::DropCount.new(0, 0)
|
|
510
530
|
begin
|
|
511
531
|
# Each ask splits its own stream: a drop mid tool call (or mid
|
|
@@ -666,7 +686,7 @@ module Samagotchi
|
|
|
666
686
|
dump_log("response", generation.response, iteration: turn.iteration)
|
|
667
687
|
# The after_generation hook (after the model returns, before the tool
|
|
668
688
|
# parse), with a read-only copy of the conversation as sent.
|
|
669
|
-
return unless
|
|
689
|
+
return unless hooks
|
|
670
690
|
|
|
671
691
|
fire_hook(:after_generation, { type: :after_generation, iteration: turn.iteration, response: generation.response,
|
|
672
692
|
messages: AnswerDisplay.strip_all(turn.conversation).map(&:dup).freeze })
|
|
@@ -780,7 +800,7 @@ module Samagotchi
|
|
|
780
800
|
turn.qwen_partial = nil
|
|
781
801
|
runs = []
|
|
782
802
|
begin
|
|
783
|
-
ToolResponse.run_batch(tool_runner, calls, iteration: turn.iteration, emit: turn.emit,
|
|
803
|
+
ToolResponse.run_batch(tool_runner, calls, context: @context, iteration: turn.iteration, emit: turn.emit,
|
|
784
804
|
on_stream_event: turn.on_stream_event, cap: turn.cap) { |run, _| runs << run }
|
|
785
805
|
rescue Interrupt
|
|
786
806
|
# A Ctrl-C mid-batch: the entry still answers every call (the
|
|
@@ -848,17 +868,15 @@ module Samagotchi
|
|
|
848
868
|
end
|
|
849
869
|
|
|
850
870
|
def fire_hook(name, event)
|
|
851
|
-
return unless
|
|
871
|
+
return unless hooks
|
|
852
872
|
|
|
853
873
|
# A failing hook must not break the turn.
|
|
854
|
-
Degrade.call(:hooks, "hook_fire_failed", hook: name) {
|
|
874
|
+
Degrade.call(:hooks, "hook_fire_failed", hook: name) { hooks.fire(name, event) }
|
|
855
875
|
end
|
|
856
876
|
|
|
857
877
|
# Coerce a value to boolean — handles true/false, nil, and string "true"/"false".
|
|
858
878
|
def truthy?(val) = Tools::Builtins.truthy?(val)
|
|
859
879
|
|
|
860
|
-
def tool_context = @tool_context ||= ToolContext.new(self)
|
|
861
|
-
|
|
862
880
|
# ── Output char cap resolution ─────────────────────────────────────────────
|
|
863
881
|
|
|
864
882
|
def resolve_output_char_cap(override)
|
|
@@ -870,7 +888,7 @@ module Samagotchi
|
|
|
870
888
|
def request_generation(prompt, generation_controller:, cancel_controller:, **)
|
|
871
889
|
kwargs = complete_kwargs(cancel_controller: generation_controller || cancel_controller, **)
|
|
872
890
|
# Behind a turn-end warm-up still running on its slot (PromptWarmup).
|
|
873
|
-
slot = @warmup&.take_pin(@client)
|
|
891
|
+
slot = @services.warmup&.take_pin(@client)
|
|
874
892
|
kwargs[:slot] = slot if slot
|
|
875
893
|
@client.complete(prompt, **kwargs)
|
|
876
894
|
end
|
|
@@ -940,10 +958,10 @@ module Samagotchi
|
|
|
940
958
|
|
|
941
959
|
# A payload dump (model response, tool call/result, context status): the
|
|
942
960
|
# debug level only, tagged with the model it came from.
|
|
943
|
-
def dump_log(event, payload, **fields)
|
|
961
|
+
def dump_log(event, payload, model: turn_settings.model_name, **fields)
|
|
944
962
|
return unless Log.level?(:debug)
|
|
945
963
|
|
|
946
|
-
Log.debug(:model, event, payload: payload, model:
|
|
964
|
+
Log.debug(:model, event, payload: payload, model: model, **fields)
|
|
947
965
|
end
|
|
948
966
|
|
|
949
967
|
# Estimate context usage for this iteration's prompt (ContextStatus#observe)
|
|
@@ -971,12 +989,14 @@ module Samagotchi
|
|
|
971
989
|
# for the model the next turn runs on, after a /model switch too),
|
|
972
990
|
# else the last turn's. Returns [prompt, images], or nil when the cut
|
|
973
991
|
# isn't found.
|
|
974
|
-
|
|
992
|
+
# +vision+: the next turn's VisionContext (the last turn's settings'),
|
|
993
|
+
# else the last run's.
|
|
994
|
+
def warmup_prompt(messages, llm_context: turn_settings&.llm_context, vision: turn_settings.vision)
|
|
975
995
|
conversation = prepare_conversation(messages)
|
|
976
996
|
apply_llm_context!(conversation, llm_context, warmup: true)
|
|
977
997
|
conversation << { role: "user", content: WARMUP_CUT }
|
|
978
998
|
prompt, images = Prompt.format_with_images(llm_context_view(llm_context).messages(conversation),
|
|
979
|
-
profile: @profile, vision:
|
|
999
|
+
profile: @profile, vision: vision)
|
|
980
1000
|
head = prompt[0, prompt.index(WARMUP_CUT) || 0]
|
|
981
1001
|
cut = head.rindex(user_opener)
|
|
982
1002
|
cut&.positive? ? [head[0, cut], images] : nil
|
|
@@ -990,34 +1010,35 @@ module Samagotchi
|
|
|
990
1010
|
# one given (the warm-up's, the next turn's): the prompt, the warm-up
|
|
991
1011
|
# and the chat messages all format through it, so the warm-up warms
|
|
992
1012
|
# the prompt the next turn sends.
|
|
993
|
-
def llm_context_view(llm_context =
|
|
1013
|
+
def llm_context_view(llm_context = turn_settings&.llm_context)
|
|
994
1014
|
LLMContextView.new(strategy: llm_context&.strategy || LLMContextView::NONE)
|
|
995
1015
|
end
|
|
996
1016
|
|
|
997
|
-
# The turn's LLM context layers ([] under none): the tools they add
|
|
998
|
-
# (Tools::Registry#entries) are the turn's to offer and dispatch.
|
|
999
|
-
def llm_context_layers = @turn_settings&.llm_context&.active_layers || []
|
|
1000
|
-
|
|
1001
1017
|
# The model's forget_outputs call (LLMContextForget) on the running
|
|
1002
|
-
# turn, under
|
|
1003
|
-
#
|
|
1004
|
-
#
|
|
1018
|
+
# turn (the loop's llm_context_turn), under +context+'s strategy (the
|
|
1019
|
+
# call's TurnContext); the apply rule runs on the turn's conversation at
|
|
1020
|
+
# once, as at a request, so the result says whether the stubs go with
|
|
1021
|
+
# the next request or wait for the turn's end.
|
|
1005
1022
|
# @return [String] the tool result
|
|
1006
|
-
def forget_outputs(call)
|
|
1007
|
-
llm_context =
|
|
1023
|
+
def forget_outputs(call, context)
|
|
1024
|
+
llm_context = context.settings&.llm_context
|
|
1008
1025
|
turn = @llm_context_turn
|
|
1009
1026
|
unless turn && llm_context&.active_layers&.include?(:forget)
|
|
1010
1027
|
return "Error: forget_outputs is off (the llm_context strategy has no forget layer)"
|
|
1011
1028
|
end
|
|
1012
1029
|
|
|
1013
|
-
apply =
|
|
1014
|
-
|
|
1030
|
+
apply = lambda {
|
|
1031
|
+
apply_llm_context!(turn.conversation, llm_context, context: turn.context, emit: turn.emit,
|
|
1032
|
+
model: context.settings.model_name)
|
|
1033
|
+
}
|
|
1034
|
+
LLMContextForget.call(turn, call, llm_context: llm_context, apply: apply,
|
|
1035
|
+
pinned: ->(conversation) { pinned_ids(conversation, context) })
|
|
1015
1036
|
end
|
|
1016
1037
|
|
|
1017
|
-
# The ids of +conversation+'s outputs the session's pins hold
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
WorkingSet.pinned_ids(conversation, pins:
|
|
1038
|
+
# The ids of +conversation+'s outputs the session's pins hold (the
|
|
1039
|
+
# working-set pins of +context+'s session, read now).
|
|
1040
|
+
def pinned_ids(conversation, context)
|
|
1041
|
+
WorkingSet.pinned_ids(conversation, pins: context.working_set_pins, cwd: context.cwd)
|
|
1021
1042
|
end
|
|
1022
1043
|
private :pinned_ids
|
|
1023
1044
|
|
|
@@ -1034,8 +1055,9 @@ module Samagotchi
|
|
|
1034
1055
|
# shown to the user as a ✂ row (+emit+ takes the :llm_context_edited
|
|
1035
1056
|
# event, LLMContextNotice; the warm-up's own copy has none).
|
|
1036
1057
|
# @return [LLMContextApply::Outcome]
|
|
1037
|
-
|
|
1038
|
-
|
|
1058
|
+
# +model+: the run's model name, in the log line (the chat loop's own).
|
|
1059
|
+
def apply_llm_context!(conversation, llm_context = turn_settings&.llm_context, moment: :request, context: nil,
|
|
1060
|
+
warmup: false, emit: nil, model: turn_settings&.model_name)
|
|
1039
1061
|
view = llm_context_view(llm_context)
|
|
1040
1062
|
return LLMContextApply::Outcome.none if view.none?
|
|
1041
1063
|
|
|
@@ -1047,19 +1069,19 @@ module Samagotchi
|
|
|
1047
1069
|
# The prompt just shrank: the context estimate counts from it.
|
|
1048
1070
|
context&.edited!
|
|
1049
1071
|
unless warmup
|
|
1050
|
-
log_llm_context(outcome, llm_context, moment)
|
|
1072
|
+
log_llm_context(outcome, llm_context, moment, model)
|
|
1051
1073
|
emit_llm_context(emit, conversation, outcome, moment) if emit
|
|
1052
1074
|
end
|
|
1053
1075
|
end
|
|
1054
1076
|
outcome
|
|
1055
1077
|
end
|
|
1056
1078
|
|
|
1057
|
-
def log_llm_context(outcome, llm_context, moment)
|
|
1079
|
+
def log_llm_context(outcome, llm_context, moment, model)
|
|
1058
1080
|
Log.info(:model, "llm_context_apply", rule: llm_context.apply, at: moment, why: outcome.why,
|
|
1059
1081
|
applied: outcome.applied.size, ids: outcome.applied.map(&:id).join(","),
|
|
1060
1082
|
staged: outcome.staged, freed: (outcome.freed_chars / TokenUsage::CHARS_PER_TOKEN).ceil,
|
|
1061
1083
|
tail: (outcome.tail_chars / TokenUsage::CHARS_PER_TOKEN).ceil,
|
|
1062
|
-
model:
|
|
1084
|
+
model: model)
|
|
1063
1085
|
end
|
|
1064
1086
|
private :log_llm_context
|
|
1065
1087
|
|
|
@@ -1188,78 +1210,33 @@ module Samagotchi
|
|
|
1188
1210
|
# unknown-tool handling, and activity events are shared, not duplicated. Callers
|
|
1189
1211
|
# are responsible for normalizing the provider's native call into this shape
|
|
1190
1212
|
# first (see Samagotchi::LLM::NativeToolNormalizer); dispatch itself never
|
|
1191
|
-
# parses provider text.
|
|
1192
|
-
|
|
1193
|
-
|
|
1213
|
+
# parses provider text. +context+: the call's turn (TurnContext: its
|
|
1214
|
+
# ToolSurface, the handler's ToolContext); a bare kernel's own
|
|
1215
|
+
# (#context) by default.
|
|
1216
|
+
def dispatch_tool_call(call, context = @context)
|
|
1217
|
+
dispatch(call, context)
|
|
1194
1218
|
end
|
|
1195
1219
|
|
|
1196
1220
|
private
|
|
1197
1221
|
|
|
1198
|
-
#
|
|
1199
|
-
#
|
|
1200
|
-
#
|
|
1201
|
-
#
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
# `execute` offered this turn points at `execute`; every other unknown
|
|
1208
|
-
# name lists what is offered. Neither echoes the wrong name.
|
|
1209
|
-
#
|
|
1210
|
-
# A tool the session's toolkit leaves out is a real one, so its error
|
|
1211
|
-
# does name it (deliberately, against the rule above): the model learns
|
|
1212
|
-
# it exists, and how to reach it (call_tool, when the toolkit reaches
|
|
1213
|
-
# it that way) or that only the user can load it. A call reached
|
|
1214
|
-
# through call_tool (+via+) of a tool the toolkit declares is told to
|
|
1215
|
-
# call it directly: the model's call_tool(tool: "chi/read") with read
|
|
1216
|
-
# declared, or a hook's rewrite (a direct call can't carry via:).
|
|
1217
|
-
# The built-in +name+ names when the turn's host loads tools and the
|
|
1218
|
-
# toolkit reaches it (Registry#indexed): a direct call runs it.
|
|
1219
|
-
def loaded_entry(name)
|
|
1220
|
-
return nil unless @turn_settings&.loads_tools && @tools.respond_to?(:indexed)
|
|
1221
|
-
|
|
1222
|
-
@tools.indexed(name, layers: llm_context_layers)
|
|
1223
|
-
end
|
|
1224
|
-
|
|
1225
|
-
def unknown_tool_error(name, layers, via: nil)
|
|
1226
|
-
names = @tools.names(layers: layers)
|
|
1227
|
-
toolkit = @tools.toolkit&.name
|
|
1228
|
-
in_toolkit = " in this session#{" (toolkit #{toolkit})" if toolkit}"
|
|
1229
|
-
if via && @tools.offered(name, layers: layers)
|
|
1230
|
-
"Error: #{name} is declared#{in_toolkit}; call it directly. Available: #{names.join(", ")}"
|
|
1231
|
-
elsif !via && @tools.respond_to?(:indexed) && @tools.indexed(name, layers: layers)
|
|
1232
|
-
"Error: #{name} isn't declared#{in_toolkit}; call it with #{Tools::BuiltinCalls::VIA}(tool: \"chi/#{name}\", args: {…}) " \
|
|
1233
|
-
"(find_tools gives its inputSchema). Available: #{names.join(", ")}"
|
|
1234
|
-
elsif @tools.respond_to?(:left_out) && @tools.left_out(name, layers: layers)
|
|
1235
|
-
"Error: #{name} isn't loaded#{in_toolkit} and can't be loaded from " \
|
|
1236
|
-
"here; only the user can (/toolkit add #{name}). Available: #{names.join(", ")}"
|
|
1237
|
-
elsif SHELL_LIKE_TOOL_NAMES.include?(name.to_s.downcase) && names.include?("execute")
|
|
1238
|
-
"Error: no such tool. Shell commands run with `execute` (same arguments)."
|
|
1239
|
-
else
|
|
1240
|
-
"Error: no such tool. Available: #{names.join(", ")}"
|
|
1241
|
-
end
|
|
1242
|
-
end
|
|
1243
|
-
|
|
1244
|
-
def dispatch(call)
|
|
1245
|
-
# A built-in reached through call_tool runs only while the toolkit
|
|
1246
|
-
# reaches it that way (Registry#indexed); any other call by its name,
|
|
1247
|
-
# on a host that loads tools (TurnSettings#loads_tools) a reachable
|
|
1248
|
-
# one too.
|
|
1249
|
-
entry = if call[:via]
|
|
1250
|
-
@tools.respond_to?(:indexed) ? @tools.indexed(call[:name], layers: llm_context_layers) : nil
|
|
1251
|
-
else
|
|
1252
|
-
@tools.offered(call[:name], layers: llm_context_layers) || loaded_entry(call[:name])
|
|
1253
|
-
end
|
|
1222
|
+
# The call runs on the entry the turn's surface gives it
|
|
1223
|
+
# (Tools::ToolSurface#entry_for: a call_tool-reached one only while the
|
|
1224
|
+
# toolkit reaches it that way, a reachable one by its name on a host
|
|
1225
|
+
# that loads tools); without one it gets ToolSurface#unknown_tool_error.
|
|
1226
|
+
# A context without a surface (a bare kernel) computes one now.
|
|
1227
|
+
def dispatch(call, context)
|
|
1228
|
+
model = context.settings&.model_name
|
|
1229
|
+
surface = Tools::ToolSurface.of(context, tools)
|
|
1230
|
+
entry = surface.entry_for(call)
|
|
1254
1231
|
unless entry
|
|
1255
|
-
result = unknown_tool_error(call[:name],
|
|
1232
|
+
result = surface.unknown_tool_error(call[:name], via: call[:via])
|
|
1256
1233
|
return {
|
|
1257
1234
|
output: result,
|
|
1258
|
-
activity: ToolActivity.tool_activity_event(call[:name], call, result, registry:
|
|
1235
|
+
activity: ToolActivity.tool_activity_event(call[:name], call, result, registry: tools, surface: surface)
|
|
1259
1236
|
}
|
|
1260
1237
|
end
|
|
1261
1238
|
|
|
1262
|
-
dump_log("tool_call", call[:content], tool: call[:name], path: call[:path], scope: call[:scope])
|
|
1239
|
+
dump_log("tool_call", call[:content], model: model, tool: call[:name], path: call[:path], scope: call[:scope])
|
|
1263
1240
|
|
|
1264
1241
|
if call[:called_as]
|
|
1265
1242
|
# The model's spelling for a tool an alias ran (bash → execute); its
|
|
@@ -1268,12 +1245,12 @@ module Samagotchi
|
|
|
1268
1245
|
Log.info(:turn, "tool_alias", name: call[:called_as], as: call[:name])
|
|
1269
1246
|
end
|
|
1270
1247
|
|
|
1271
|
-
result = entry.handler.call(call,
|
|
1248
|
+
result = entry.handler.call(call, ToolContext.new(self, context))
|
|
1272
1249
|
|
|
1273
|
-
dump_log("tool_result", result, tool: call[:name])
|
|
1250
|
+
dump_log("tool_result", result, model: model, tool: call[:name])
|
|
1274
1251
|
dispatched = {
|
|
1275
1252
|
output: "[#{call[:name]}]\n#{result}",
|
|
1276
|
-
activity: ToolActivity.tool_activity_event(call[:name], call, result, registry:
|
|
1253
|
+
activity: ToolActivity.tool_activity_event(call[:name], call, result, registry: tools, surface: surface)
|
|
1277
1254
|
}
|
|
1278
1255
|
# Images the tool returned (read's ImageResult, a plugin's ToolResult):
|
|
1279
1256
|
# ToolRunner attaches them (or says why not).
|
|
@@ -1292,11 +1269,11 @@ module Samagotchi
|
|
|
1292
1269
|
end
|
|
1293
1270
|
dispatched
|
|
1294
1271
|
rescue StandardError => e
|
|
1295
|
-
dump_log("tool_error", e.message, tool: call[:name], error: e.class.name)
|
|
1272
|
+
dump_log("tool_error", e.message, model: context.settings&.model_name, tool: call[:name], error: e.class.name)
|
|
1296
1273
|
result = "Error: #{e.message}"
|
|
1297
1274
|
{
|
|
1298
1275
|
output: "[#{call[:name]}] #{result}",
|
|
1299
|
-
activity: ToolActivity.tool_activity_event(call[:name], call, result, registry:
|
|
1276
|
+
activity: ToolActivity.tool_activity_event(call[:name], call, result, registry: tools, surface: surface)
|
|
1300
1277
|
}
|
|
1301
1278
|
end
|
|
1302
1279
|
end
|