samagotchi 0.64.0 → 0.66.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 +88 -1
- data/README.md +2 -1
- data/bin/chi +51 -2
- data/docs/architecture.md +20 -3
- data/docs/cli.md +36 -2
- data/docs/configuration.md +177 -8
- data/docs/context.md +4 -1
- data/docs/internals/engine.md +19 -4
- data/docs/internals/prompt-caching.md +10 -2
- data/docs/memory.md +1 -1
- data/docs/plugins.md +163 -2
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +13 -2
- data/docs/sub-agent.md +3 -1
- data/lib/samagotchi/bridge.rb +11 -1
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/claude-peer/plugin.rb +18 -1
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +21 -0
- data/lib/samagotchi/bundles/codex-peer/plugin.rb +464 -0
- data/lib/samagotchi/bundles/codex-peer/scripts/chi-peer-SKILL.md +131 -0
- data/lib/samagotchi/bundles/codex-peer/scripts/pair +92 -0
- data/lib/samagotchi/bundles/codex-peer/skill_codex_peer.md +68 -0
- data/lib/samagotchi/bundles/coordinator/manifest.yml +5 -5
- data/lib/samagotchi/bundles/coordinator/plugin.rb +24 -2
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -1
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
- data/lib/samagotchi/bundles/system/delegated.md +2 -1
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/cache_confirm.rb +9 -6
- data/lib/samagotchi/client.rb +4 -2
- data/lib/samagotchi/commands/registry.rb +25 -8
- data/lib/samagotchi/config.rb +13 -5
- data/lib/samagotchi/engine.rb +151 -11
- data/lib/samagotchi/generation_phase.rb +17 -0
- data/lib/samagotchi/host_config.rb +27 -6
- data/lib/samagotchi/host_registry.rb +34 -11
- data/lib/samagotchi/idle_client.rb +62 -25
- data/lib/samagotchi/idle_recap.rb +5 -2
- data/lib/samagotchi/idle_target.rb +30 -7
- data/lib/samagotchi/kernel_loop.rb +9 -1
- data/lib/samagotchi/llm/adapters.rb +41 -0
- data/lib/samagotchi/llm/anthropic_assembly.rb +212 -0
- data/lib/samagotchi/llm/anthropic_messages.rb +416 -0
- data/lib/samagotchi/llm/anthropic_wire.rb +238 -0
- data/lib/samagotchi/llm/api_key.rb +26 -9
- data/lib/samagotchi/llm/chat_loop.rb +113 -14
- data/lib/samagotchi/llm/dialect.rb +57 -0
- data/lib/samagotchi/llm/errors.rb +81 -12
- data/lib/samagotchi/llm/model_listing.rb +111 -0
- data/lib/samagotchi/llm/model_result.rb +9 -2
- data/lib/samagotchi/llm/openai_chat.rb +46 -93
- data/lib/samagotchi/model_profile.rb +1 -1
- data/lib/samagotchi/plugin/api.rb +80 -7
- data/lib/samagotchi/plugin/context.rb +22 -1
- data/lib/samagotchi/plugin/loader.rb +3 -2
- data/lib/samagotchi/reasoning_history.rb +14 -4
- data/lib/samagotchi/recap_setup.rb +2 -1
- data/lib/samagotchi/self_report.rb +18 -28
- data/lib/samagotchi/send_command.rb +23 -7
- data/lib/samagotchi/session.rb +21 -3
- data/lib/samagotchi/session_commands.rb +16 -1
- data/lib/samagotchi/session_manager.rb +4 -1
- data/lib/samagotchi/session_metrics.rb +6 -11
- data/lib/samagotchi/session_setup.rb +14 -7
- data/lib/samagotchi/steer_cut.rb +4 -0
- data/lib/samagotchi/system_prompt.rb +16 -1
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +19 -9
- data/lib/samagotchi/terminal_ui/attached_loop.rb +8 -2
- data/lib/samagotchi/terminal_ui/event_renderer.rb +2 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +4 -3
- data/lib/samagotchi/terminal_ui/status_row.rb +24 -3
- data/lib/samagotchi/terminal_ui.rb +18 -2
- data/lib/samagotchi/tool_runner.rb +13 -1
- data/lib/samagotchi/toolkit.rb +246 -0
- data/lib/samagotchi/toolkit_command.rb +117 -0
- data/lib/samagotchi/tools/registry.rb +43 -6
- data/lib/samagotchi/turn_note.rb +22 -5
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +14 -1
- data/lib/samagotchi/web/public/activity.js +3 -0
- data/lib/samagotchi/web/public/annotate_actions.js +109 -0
- data/lib/samagotchi/web/public/annotate_bar.js +84 -2
- data/lib/samagotchi/web/public/app.js +111 -20
- data/lib/samagotchi/web/public/commands.js +4 -2
- data/lib/samagotchi/web/public/delivery_line.js +228 -0
- data/lib/samagotchi/web/public/index.html +38 -0
- data/lib/samagotchi/web/public/info_bar.js +5 -3
- data/lib/samagotchi/web/public/live_turn.js +8 -0
- data/lib/samagotchi/web/public/session_state.js +3 -0
- data/lib/samagotchi/web/public/stage_model.js +27 -7
- data/lib/samagotchi/web/public/stage_view.js +32 -10
- data/lib/samagotchi/web/public/stream_handlers.js +21 -2
- data/lib/samagotchi/web/public/toolkit_chip.js +29 -0
- data/lib/samagotchi/web/public/turn_events.js +22 -8
- data/lib/samagotchi/web/public/turn_html.js +20 -4
- data/lib/samagotchi/web/public/turn_model.js +16 -9
- data/lib/samagotchi/web/public/turn_view.js +6 -4
- data/lib/samagotchi/worker.rb +2 -0
- metadata +17 -1
|
@@ -39,7 +39,7 @@ module Samagotchi
|
|
|
39
39
|
|
|
40
40
|
USAGE = <<~TEXT
|
|
41
41
|
Usage: chi send [-m TEXT] [--image PATH]... [--cut | --queue] (ID|PREFIX)...
|
|
42
|
-
chi send --new [--dir DIR] [--model M] [--llm-context LAYERS] [--thinking LEVEL] [-m TEXT] [--image PATH]...
|
|
42
|
+
chi send --new [--dir DIR] [--model M] [--llm-context LAYERS] [--thinking LEVEL] [--toolkit NAME] [-m TEXT] [--image PATH]...
|
|
43
43
|
chi send --new --continues (ID|PREFIX|last:ID) [-m TEXT] [--image PATH]...
|
|
44
44
|
chi send --wait [--timeout S] [--format json] [-m TEXT] [--image PATH]... (--new | ID)
|
|
45
45
|
chi send --queue --wait [--timeout S] [--format json] [-m TEXT] ID
|
|
@@ -63,8 +63,8 @@ module Samagotchi
|
|
|
63
63
|
the current one
|
|
64
64
|
--continues ID
|
|
65
65
|
(--new) start the next link of that session's chain:
|
|
66
|
-
in its folder, on its model, LLM context
|
|
67
|
-
thinking level, with a note carrying its recap; it
|
|
66
|
+
in its folder, on its model, LLM context,
|
|
67
|
+
thinking level and toolkit, with a note carrying its recap; it
|
|
68
68
|
is archived. last:ID names the chain's latest link.
|
|
69
69
|
Without -m the new session waits idle. Refused for a
|
|
70
70
|
session continued already (naming the next link) or
|
|
@@ -77,6 +77,11 @@ module Samagotchi
|
|
|
77
77
|
(--new) its own thinking level (off, low, medium,
|
|
78
78
|
high; default: none of its own), above a process
|
|
79
79
|
default such as SAMAGOTCHI_THINKING_LEVEL
|
|
80
|
+
--toolkit NAME, --tool NAME, --no-tool NAME
|
|
81
|
+
(--new) its toolkit, the tools its prompts declare
|
|
82
|
+
(full, lean, child, a config or bundle one; default
|
|
83
|
+
config's toolkit.default), and tools declared on top
|
|
84
|
+
of it or left out (repeatable, a comma list too)
|
|
80
85
|
--wait wait for the answer and print it (one session); the
|
|
81
86
|
other lines go to stderr. Exit 3: it waits for an
|
|
82
87
|
answer from you: the question, its options and the
|
|
@@ -126,6 +131,9 @@ module Samagotchi
|
|
|
126
131
|
f.value "--llm-context-apply", key: :llm_apply
|
|
127
132
|
f.value "--llm-context-budget", key: :llm_budget
|
|
128
133
|
f.value "--thinking"
|
|
134
|
+
f.value "--toolkit"
|
|
135
|
+
f.value "--tool", key: :tools, repeat: true
|
|
136
|
+
f.value "--no-tool", key: :no_tools, repeat: true
|
|
129
137
|
f.value "--image", key: :images, repeat: true
|
|
130
138
|
f.switch "--wait"
|
|
131
139
|
f.switch "--cut"
|
|
@@ -262,7 +270,8 @@ module Samagotchi
|
|
|
262
270
|
return usage_error("at most #{MAX_IMAGES} images") if options[:images].size > MAX_IMAGES
|
|
263
271
|
return new_options(options) if options[:new]
|
|
264
272
|
|
|
265
|
-
%i[dir model continues thinking].each { |key| return usage_error("--#{key} needs --new") if options[key] }
|
|
273
|
+
%i[dir model continues thinking toolkit].each { |key| return usage_error("--#{key} needs --new") if options[key] }
|
|
274
|
+
TOOLKIT_FLAGS.each { |key, flag| return usage_error("#{flag} needs --new") unless Array(options[key]).empty? }
|
|
266
275
|
LLM_CONTEXT_FLAGS.each { |key, flag| return usage_error("#{flag} needs --new") if options[key] }
|
|
267
276
|
return usage_error("give session ids") if options[:ids].empty?
|
|
268
277
|
|
|
@@ -273,15 +282,19 @@ module Samagotchi
|
|
|
273
282
|
LLM_CONTEXT_FLAGS = { llm_strategy: "--llm-context", llm_apply: "--llm-context-apply",
|
|
274
283
|
llm_budget: "--llm-context-budget" }.freeze
|
|
275
284
|
LLM_CONTEXT_FIELDS = { llm_strategy: :strategy, llm_apply: :apply, llm_budget: :budget_tokens }.freeze
|
|
285
|
+
# --tool / --no-tool's option keys.
|
|
286
|
+
TOOLKIT_FLAGS = { tools: "--tool", no_tools: "--no-tool" }.freeze
|
|
276
287
|
|
|
277
288
|
def new_options(options)
|
|
278
289
|
return usage_error("--new takes no session ids: it starts one session") unless options[:ids].empty?
|
|
279
290
|
|
|
280
291
|
if options[:continues]
|
|
281
292
|
taken = [("--dir" if options[:dir]), ("--model" if options[:model])] +
|
|
282
|
-
LLM_CONTEXT_FLAGS.filter_map { |key, flag| flag if options[key] } + [("--thinking" if options[:thinking])]
|
|
293
|
+
LLM_CONTEXT_FLAGS.filter_map { |key, flag| flag if options[key] } + [("--thinking" if options[:thinking])] +
|
|
294
|
+
[("--toolkit" if options[:toolkit])] + TOOLKIT_FLAGS.filter_map { |key, flag| flag unless Array(options[key]).empty? }
|
|
283
295
|
unless taken.compact.empty?
|
|
284
|
-
return usage_error("--continues takes the previous session's folder, model, LLM context
|
|
296
|
+
return usage_error("--continues takes the previous session's folder, model, LLM context, thinking level and " \
|
|
297
|
+
"toolkit; " \
|
|
285
298
|
"leave out #{taken.compact.join(", ")}")
|
|
286
299
|
end
|
|
287
300
|
end
|
|
@@ -301,6 +314,8 @@ module Samagotchi
|
|
|
301
314
|
|
|
302
315
|
options[:thinking] = Thinking.session_level(word)
|
|
303
316
|
end
|
|
317
|
+
toolkit = Toolkit.from_flags(name: options[:toolkit], add: options[:tools], remove: options[:no_tools])
|
|
318
|
+
options[:toolkit_value] = toolkit if toolkit
|
|
304
319
|
|
|
305
320
|
if options[:dir]
|
|
306
321
|
options[:dir] = File.expand_path(options[:dir])
|
|
@@ -348,7 +363,8 @@ module Samagotchi
|
|
|
348
363
|
SessionManager.spawn_session(**start, working_directory: options[:dir],
|
|
349
364
|
model_name: options[:model],
|
|
350
365
|
setup: SessionSetup.new(llm_context: options[:llm_context],
|
|
351
|
-
thinking: options[:thinking]
|
|
366
|
+
thinking: options[:thinking],
|
|
367
|
+
toolkit: options[:toolkit_value]),
|
|
352
368
|
state_dir: @state_dir)
|
|
353
369
|
end
|
|
354
370
|
rescue SessionContinue::Refused, SessionManager::ArchiveRefused, SessionManager::OwnedByTUI => e
|
data/lib/samagotchi/session.rb
CHANGED
|
@@ -184,6 +184,19 @@ module Samagotchi
|
|
|
184
184
|
# couldn't read; nil when there is neither.
|
|
185
185
|
def reasoning_history_file = @reasoning_history&.to_s || @reasoning_history_unread
|
|
186
186
|
|
|
187
|
+
# The session's toolkit (Toolkit: the tools its prompts declare), as
|
|
188
|
+
# given when it was created; nil for a session from before toolkits,
|
|
189
|
+
# which is on Toolkit::FULL (Toolkit.of). Saved and never resolved
|
|
190
|
+
# again from config, so a respawned worker declares the same tools; a
|
|
191
|
+
# continue and a fork copy it (SessionSetup.of), a delegate child gets
|
|
192
|
+
# toolkit.child (SessionManager.spawn_session).
|
|
193
|
+
attr_reader :toolkit
|
|
194
|
+
|
|
195
|
+
# @param value [Toolkit, Hash, String, nil] the session file's form too
|
|
196
|
+
def toolkit=(value)
|
|
197
|
+
@toolkit = value.is_a?(Toolkit) ? value : Toolkit.from_file(value)
|
|
198
|
+
end
|
|
199
|
+
|
|
187
200
|
# The hook that stopped the last turn (stop_turn, or a cut with no retry
|
|
188
201
|
# left), e.g. "loop-guard"; nil when it ended otherwise.
|
|
189
202
|
def stopped_by
|
|
@@ -213,7 +226,7 @@ module Samagotchi
|
|
|
213
226
|
preloaded_memory_names: [], muted_memory_names: [], parent_id: nil, scratch: false,
|
|
214
227
|
last_turn: nil, model_typed: nil, delegate: false, llm_context: nil, prompt_notes: [],
|
|
215
228
|
continues: nil, thinking: nil, reasoning_history: nil, appended_prompt_files: [],
|
|
216
|
-
interrupted_turn: nil)
|
|
229
|
+
toolkit: nil, interrupted_turn: nil)
|
|
217
230
|
@id = id
|
|
218
231
|
@metadata_version = metadata_version
|
|
219
232
|
@mode = mode
|
|
@@ -242,6 +255,7 @@ module Samagotchi
|
|
|
242
255
|
self.thinking = thinking
|
|
243
256
|
self.reasoning_history = reasoning_history
|
|
244
257
|
self.appended_prompt_files = appended_prompt_files
|
|
258
|
+
self.toolkit = toolkit
|
|
245
259
|
self.interrupted_turn = interrupted_turn
|
|
246
260
|
@turn_checkpoint = nil
|
|
247
261
|
@archived = false
|
|
@@ -336,6 +350,7 @@ module Samagotchi
|
|
|
336
350
|
thinking: setup.thinking,
|
|
337
351
|
reasoning_history: setup.reasoning_history,
|
|
338
352
|
appended_prompt_files: setup.appended_prompt_files,
|
|
353
|
+
toolkit: setup.toolkit,
|
|
339
354
|
continues: continues
|
|
340
355
|
)
|
|
341
356
|
end
|
|
@@ -400,6 +415,7 @@ module Samagotchi
|
|
|
400
415
|
"thinking" => nil,
|
|
401
416
|
"reasoning_history" => nil,
|
|
402
417
|
"appended_prompt_files" => [],
|
|
418
|
+
"toolkit" => nil,
|
|
403
419
|
"interrupted_turn" => nil
|
|
404
420
|
}.freeze
|
|
405
421
|
|
|
@@ -527,6 +543,7 @@ module Samagotchi
|
|
|
527
543
|
when "llm_context" then llm_context_file
|
|
528
544
|
when "thinking" then thinking_file
|
|
529
545
|
when "reasoning_history" then reasoning_history_file
|
|
546
|
+
when "toolkit" then @toolkit&.to_file
|
|
530
547
|
when "prompt_notes" then @prompt_notes.map(&:to_file)
|
|
531
548
|
when "last_turn" then @last_turn&.to_file
|
|
532
549
|
when "used_memory_names", "preloaded_memory_names", "muted_memory_names", "appended_prompt_files"
|
|
@@ -722,14 +739,15 @@ module Samagotchi
|
|
|
722
739
|
symbolize_empty_answer(message)
|
|
723
740
|
end
|
|
724
741
|
|
|
725
|
-
# An empty-answer note's marker (TurnNote.empty)
|
|
726
|
-
# they were saved.
|
|
742
|
+
# An empty-answer note's marker (TurnNote.empty), its steps and its
|
|
743
|
+
# refusal, as they were saved.
|
|
727
744
|
def symbolize_empty_answer(message)
|
|
728
745
|
marker = message[:empty_answer]
|
|
729
746
|
return message unless marker.is_a?(Hash)
|
|
730
747
|
|
|
731
748
|
marker = marker.transform_keys(&:to_sym)
|
|
732
749
|
marker[:steps] = marker[:steps].map { |step| step.is_a?(Hash) ? step.transform_keys(&:to_sym) : step } if marker[:steps].is_a?(Array)
|
|
750
|
+
marker[:refusal] = marker[:refusal].transform_keys(&:to_sym) if marker[:refusal].is_a?(Hash)
|
|
733
751
|
message.merge(empty_answer: marker)
|
|
734
752
|
end
|
|
735
753
|
|
|
@@ -12,6 +12,7 @@ require_relative "tools/peers"
|
|
|
12
12
|
require_relative "commands/registry"
|
|
13
13
|
require_relative "llm_context_command"
|
|
14
14
|
require_relative "thinking_command"
|
|
15
|
+
require_relative "toolkit_command"
|
|
15
16
|
require_relative "reasoning_history_command"
|
|
16
17
|
|
|
17
18
|
module Samagotchi
|
|
@@ -32,6 +33,7 @@ module Samagotchi
|
|
|
32
33
|
HELP_COMMAND = "/help"
|
|
33
34
|
LLM_CONTEXT_COMMAND = LLMContextCommand::NAME
|
|
34
35
|
THINKING_COMMAND = ThinkingCommand::NAME
|
|
36
|
+
TOOLKIT_COMMAND = ToolkitCommand::NAME
|
|
35
37
|
REASONING_HISTORY_COMMAND = ReasoningHistoryCommand::NAME
|
|
36
38
|
# A remote catalog has hundreds of ids (OpenRouter ~380): plain /models
|
|
37
39
|
# shows this many per host; /models <text> lists every match.
|
|
@@ -48,7 +50,7 @@ module Samagotchi
|
|
|
48
50
|
|
|
49
51
|
# The commands that ask before a change that restarts the prompt cache
|
|
50
52
|
# (CacheConfirm): #run passes them its confirm flag.
|
|
51
|
-
CONFIRMING = %i[thinking reasoning_history].freeze
|
|
53
|
+
CONFIRMING = %i[thinking reasoning_history toolkit].freeze
|
|
52
54
|
|
|
53
55
|
# @!attribute status [Symbol] :ok, or :error when the line was refused
|
|
54
56
|
# @!attribute output [String, nil] what to tell the user
|
|
@@ -103,6 +105,9 @@ module Samagotchi
|
|
|
103
105
|
registry.register(REASONING_HISTORY_COMMAND, "show or set which of its reasoning this session's model gets back (drop|turn|all|default)",
|
|
104
106
|
id: :reasoning_history, mid_turn: show_mid_turn(REASONING_HISTORY_COMMAND),
|
|
105
107
|
match: ->(text) { text.match?(%r{\A/reasoning-history(?:\s+.*)?\z}) }) { |text| reasoning_history(text) }
|
|
108
|
+
registry.register(TOOLKIT_COMMAND, "show or change the tools this session declares (NAME, add TOOL, remove TOOL)",
|
|
109
|
+
id: :toolkit, mid_turn: show_mid_turn(TOOLKIT_COMMAND),
|
|
110
|
+
match: ->(text) { text.match?(%r{\A/toolkit(?:\s+.*)?\z}) }) { |text| toolkit(text) }
|
|
106
111
|
registry.register(HELP_COMMAND, "list the commands, the bundles' too", anytime: true) { |_text| reply(help_listing) }
|
|
107
112
|
registry.register("/stats", "show the session's stats", local: true)
|
|
108
113
|
registry.register("/recap", "show the session's recap", local: true)
|
|
@@ -467,6 +472,16 @@ module Samagotchi
|
|
|
467
472
|
reply(e.message, status: :error)
|
|
468
473
|
end
|
|
469
474
|
|
|
475
|
+
# /toolkit (ToolkitCommand); :toolkit in changed when it set the
|
|
476
|
+
# session's toolkit.
|
|
477
|
+
def toolkit(text, confirm: true)
|
|
478
|
+
output, changed, ask = ToolkitCommand.new(engine: @engine, save: ->(session) { save_setup(session) })
|
|
479
|
+
.run(text.delete_prefix(TOOLKIT_COMMAND), confirm: confirm)
|
|
480
|
+
reply(output, changed: changed ? [:toolkit] : [], confirm: ask)
|
|
481
|
+
rescue ArgumentError => e
|
|
482
|
+
reply(e.message, status: :error)
|
|
483
|
+
end
|
|
484
|
+
|
|
470
485
|
# A session setting changed (/llm-context, /thinking, /reasoning-history):
|
|
471
486
|
# saved with its model.
|
|
472
487
|
def save_setup(session)
|
|
@@ -140,7 +140,9 @@ module Samagotchi
|
|
|
140
140
|
# @param title [String, nil] what the lists show before the first turn
|
|
141
141
|
# (the prompt's preview by default, else the seed's first user message)
|
|
142
142
|
# @param setup [SessionSetup] the session's own settings (chi
|
|
143
|
-
# --llm-context; a fork and a continue copy their session's)
|
|
143
|
+
# --llm-context; a fork and a continue copy their session's); without
|
|
144
|
+
# a toolkit the session is stored on config's toolkit.default, a
|
|
145
|
+
# delegate child always on toolkit.child (Toolkit.configured)
|
|
144
146
|
# @param continues [String, nil] the session this one continues (a
|
|
145
147
|
# session field; SessionContinue)
|
|
146
148
|
# @param note [String, nil] a context note the session starts with,
|
|
@@ -177,6 +179,7 @@ module Samagotchi
|
|
|
177
179
|
setup: setup,
|
|
178
180
|
continues: continues
|
|
179
181
|
)
|
|
182
|
+
session.toolkit = Toolkit.configured(delegate: delegate) if delegate || session.toolkit.nil?
|
|
180
183
|
session.model_warning = model_warning
|
|
181
184
|
# With no prompt there is no first turn to run (an attaching UI sends
|
|
182
185
|
# the prompts), so the session starts idle.
|
|
@@ -883,7 +883,7 @@ module Samagotchi
|
|
|
883
883
|
payload = event[:payload]
|
|
884
884
|
payload = {} unless payload.is_a?(Hash)
|
|
885
885
|
|
|
886
|
-
@mutex.synchronize { note_first_chunk(
|
|
886
|
+
@mutex.synchronize { note_first_chunk(event) }
|
|
887
887
|
usage = TokenUsage.from_payload(payload)
|
|
888
888
|
if usage
|
|
889
889
|
@mutex.synchronize { note_server_usage(usage) if @turn }
|
|
@@ -915,21 +915,16 @@ module Samagotchi
|
|
|
915
915
|
turn.gen_had_server = true
|
|
916
916
|
end
|
|
917
917
|
|
|
918
|
-
# The first chunk with content, reasoning or a tool call
|
|
919
|
-
#
|
|
920
|
-
# holds the mutex.
|
|
921
|
-
def note_first_chunk(
|
|
918
|
+
# The first chunk with content, reasoning or a tool call (the loop marks
|
|
919
|
+
# its event tool_call: true, whatever the wire) starts the generation's
|
|
920
|
+
# decode clock (an estimated speed runs from it). Caller holds the mutex.
|
|
921
|
+
def note_first_chunk(event)
|
|
922
922
|
return unless @turn && @turn.gen_open && @turn.gen_first_chunk_at.nil?
|
|
923
|
-
return unless !event[:content].to_s.empty? ||
|
|
923
|
+
return unless !event[:content].to_s.empty? || event[:tool_call]
|
|
924
924
|
|
|
925
925
|
@turn.gen_first_chunk_at = monotonic_time
|
|
926
926
|
end
|
|
927
927
|
|
|
928
|
-
def tool_call_delta?(payload)
|
|
929
|
-
choice = payload["choices"].is_a?(Array) ? payload["choices"].first : nil
|
|
930
|
-
choice.is_a?(Hash) && choice["delta"].is_a?(Hash) && !Array(choice["delta"]["tool_calls"]).empty?
|
|
931
|
-
end
|
|
932
|
-
|
|
933
928
|
def event_content_chars(payload, event)
|
|
934
929
|
len = payload_content(payload, event).length
|
|
935
930
|
len.positive? ? len : nil
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "llm_context_override"
|
|
4
4
|
require_relative "reasoning_history"
|
|
5
5
|
require_relative "thinking"
|
|
6
|
+
require_relative "toolkit"
|
|
6
7
|
|
|
7
8
|
module Samagotchi
|
|
8
9
|
# A session's own settings that come before its model's, carried as one
|
|
@@ -12,27 +13,33 @@ module Samagotchi
|
|
|
12
13
|
# +thinking+ a level Symbol (Thinking.session_level) or nil,
|
|
13
14
|
# +reasoning_history+ a level Symbol (ReasoningHistory.session_level) or nil,
|
|
14
15
|
# +appended_prompt_files+ the absolute paths chi --append-system-prompt
|
|
15
|
-
# named (their text goes into the system prompt; [] with none)
|
|
16
|
+
# named (their text goes into the system prompt; [] with none),
|
|
17
|
+
# +toolkit+ a Toolkit or nil (SessionManager.spawn_session gives the
|
|
18
|
+
# session config's toolkit.default then).
|
|
16
19
|
#
|
|
17
20
|
# It doesn't save itself: the session keeps each setting in its own field
|
|
18
21
|
# and file key, and resolves it on its own. A delegate child starts
|
|
19
22
|
# without one; a continue and a fork copy their session's (.of).
|
|
20
|
-
SessionSetup = Data.define(:llm_context, :thinking, :reasoning_history, :appended_prompt_files) do
|
|
21
|
-
def initialize(llm_context: nil, thinking: nil, reasoning_history: nil, appended_prompt_files: [])
|
|
23
|
+
SessionSetup = Data.define(:llm_context, :thinking, :reasoning_history, :appended_prompt_files, :toolkit) do
|
|
24
|
+
def initialize(llm_context: nil, thinking: nil, reasoning_history: nil, appended_prompt_files: [], toolkit: nil)
|
|
22
25
|
super(llm_context: llm_context, thinking: thinking, reasoning_history: reasoning_history,
|
|
23
|
-
appended_prompt_files: Array(appended_prompt_files).map(&:to_s).freeze)
|
|
26
|
+
appended_prompt_files: Array(appended_prompt_files).map(&:to_s).freeze, toolkit: toolkit)
|
|
24
27
|
end
|
|
25
28
|
|
|
26
|
-
def empty?
|
|
29
|
+
def empty?
|
|
30
|
+
llm_context.nil? && thinking.nil? && reasoning_history.nil? && appended_prompt_files.empty? && toolkit.nil?
|
|
31
|
+
end
|
|
27
32
|
|
|
28
|
-
# A session's own setup, for a session that starts from it.
|
|
33
|
+
# A session's own setup, for a session that starts from it. Its
|
|
34
|
+
# toolkit always: one from before toolkits is full (Toolkit.of), not
|
|
35
|
+
# whatever config's default is now.
|
|
29
36
|
# @param session [Session, nil]
|
|
30
37
|
# @return [SessionSetup]
|
|
31
38
|
def self.of(session)
|
|
32
39
|
return new if session.nil?
|
|
33
40
|
|
|
34
41
|
new(llm_context: session.llm_context, thinking: session.thinking, reasoning_history: session.reasoning_history,
|
|
35
|
-
appended_prompt_files: session.appended_prompt_files)
|
|
42
|
+
appended_prompt_files: session.appended_prompt_files, toolkit: Toolkit.of(session.toolkit))
|
|
36
43
|
end
|
|
37
44
|
end
|
|
38
45
|
end
|
data/lib/samagotchi/steer_cut.rb
CHANGED
|
@@ -82,6 +82,10 @@ module Samagotchi
|
|
|
82
82
|
@waiting.epoch
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
+
# The running generation's phase (GenerationPhase::Snapshot), nil with
|
|
86
|
+
# none running.
|
|
87
|
+
def generation_snapshot = @phase.snapshot
|
|
88
|
+
|
|
85
89
|
# The turn's stream: the generation's phase follows it, and a thinking
|
|
86
90
|
# chunk rechecks a waiting message. +progress+: a chunk's lanes
|
|
87
91
|
# (.lanes), read from it when not given.
|
|
@@ -317,7 +317,7 @@ module Samagotchi
|
|
|
317
317
|
"Project memories:\n#{indexes[:project]}",
|
|
318
318
|
"System memories:\n#{indexes[:system]}"
|
|
319
319
|
].join("\n\n")
|
|
320
|
-
stable = [thinking_token + base, rg_guidance, system_identity_section, model_notes_section, explicit_memory_section,
|
|
320
|
+
stable = [thinking_token + base, not_loaded_line(layers), rg_guidance, system_identity_section, model_notes_section, explicit_memory_section,
|
|
321
321
|
project_specific_description, appended_section, memory_sections]
|
|
322
322
|
stable_text = stable.compact.join("\n")
|
|
323
323
|
volatile = [current_model, project_location, current_session].compact
|
|
@@ -328,6 +328,21 @@ module Samagotchi
|
|
|
328
328
|
with_tail_declarations(prompt, chat: chat, layers: layers)
|
|
329
329
|
end
|
|
330
330
|
|
|
331
|
+
# The tools the session's toolkit leaves out, named once right after the
|
|
332
|
+
# base prompt: sessions on another toolkit already differ at the tools,
|
|
333
|
+
# and nothing later (a memory write) moves it. nil when none are (full):
|
|
334
|
+
# the prompt is what it was before toolkits.
|
|
335
|
+
def not_loaded_line(layers)
|
|
336
|
+
registry = @tools_lookup.call
|
|
337
|
+
return nil unless registry.respond_to?(:left_out_names)
|
|
338
|
+
|
|
339
|
+
names = registry.left_out_names(layers: layers)
|
|
340
|
+
return nil if names.empty?
|
|
341
|
+
|
|
342
|
+
"Not loaded in this session (toolkit #{registry.toolkit&.name}): #{names.join(", ")}. " \
|
|
343
|
+
"Only the user can load one (/toolkit add <name>)."
|
|
344
|
+
end
|
|
345
|
+
|
|
331
346
|
def with_tail_declarations(prompt, chat:, layers:)
|
|
332
347
|
chat || !gemma_tail_declarations? ? prompt : prompt.rstrip + tool_declarations(layers)
|
|
333
348
|
end
|
|
@@ -46,17 +46,23 @@ module Samagotchi
|
|
|
46
46
|
# @param thinking [String, nil] --thinking as typed: a new session
|
|
47
47
|
# starts with it as its own level; a resumed or attached one's
|
|
48
48
|
# worker gets a /thinking, after those
|
|
49
|
+
# @param toolkit [Toolkit, nil] --toolkit/--tool/--no-tool: a new
|
|
50
|
+
# session's toolkit (config's toolkit.default without); an existing
|
|
51
|
+
# session keeps its own
|
|
52
|
+
# @param toolkit_flags [Array<String>] which of those were given, for
|
|
53
|
+
# the line an existing session's run prints
|
|
49
54
|
# @param state_dir [String, nil] where the sessions are (the default
|
|
50
55
|
# state dir by default): the worker's, for the attached loop too
|
|
51
56
|
# @return [Symbol] :detached, :closed when the worker went away,
|
|
52
57
|
# :failed when the --model switch didn't go through, or (input from a
|
|
53
58
|
# pipe) :turn_failed / :empty_answer / :unanswered (AttachedLoop#run)
|
|
54
59
|
def run(attach: nil, shared: false, resume: nil, prompt: nil, model: nil, no_interrupt: false, default_input: true,
|
|
55
|
-
memories: [], muted_memories: [], llm_context: {}, thinking: nil, appended_prompt_files: [],
|
|
60
|
+
memories: [], muted_memories: [], llm_context: {}, thinking: nil, appended_prompt_files: [], toolkit: nil,
|
|
61
|
+
toolkit_flags: [], state_dir: nil)
|
|
56
62
|
state_dir ||= Session.default_state_dir
|
|
57
63
|
client = connect(attach: attach, shared: shared, resume: resume, model: model, state_dir: state_dir,
|
|
58
64
|
memories: memories, muted_memories: muted_memories, llm_context: llm_context, thinking: thinking,
|
|
59
|
-
appended_prompt_files: appended_prompt_files)
|
|
65
|
+
appended_prompt_files: appended_prompt_files, toolkit: toolkit, toolkit_flags: toolkit_flags)
|
|
60
66
|
commands = []
|
|
61
67
|
commands << "/model #{model}" if model && (attach || resume)
|
|
62
68
|
commands << LLMContextOverride.command_line(llm_context) if (attach || resume) && !llm_context.empty?
|
|
@@ -123,10 +129,12 @@ module Samagotchi
|
|
|
123
129
|
# @return [BridgeClient] a client for the live Bridge of the session
|
|
124
130
|
# @raise [Error]
|
|
125
131
|
def connect(attach: nil, shared: false, resume: nil, model: nil, state_dir: nil, wait: BRIDGE_WAIT,
|
|
126
|
-
memories: [], muted_memories: [], llm_context: {}, thinking: nil, appended_prompt_files: [],
|
|
132
|
+
memories: [], muted_memories: [], llm_context: {}, thinking: nil, appended_prompt_files: [], toolkit: nil,
|
|
133
|
+
toolkit_flags: [], err: $stderr)
|
|
127
134
|
sd = state_dir || Session.default_state_dir
|
|
128
135
|
if attach || resume
|
|
129
|
-
warn_memory_flags_ignored(attach || resume, memories, muted_memories, err, appended_prompt_files: appended_prompt_files
|
|
136
|
+
warn_memory_flags_ignored(attach || resume, memories, muted_memories, err, appended_prompt_files: appended_prompt_files,
|
|
137
|
+
toolkit_flags: toolkit_flags)
|
|
130
138
|
end
|
|
131
139
|
if attach
|
|
132
140
|
live = connect_existing(attach, sd)
|
|
@@ -143,7 +151,8 @@ module Samagotchi
|
|
|
143
151
|
memories: memories, muted_memories: muted_memories,
|
|
144
152
|
setup: SessionSetup.new(llm_context: llm_context.empty? ? nil : LLMContextOverride.update(nil, llm_context),
|
|
145
153
|
thinking: Thinking.session_level(thinking),
|
|
146
|
-
appended_prompt_files: appended_prompt_files
|
|
154
|
+
appended_prompt_files: appended_prompt_files,
|
|
155
|
+
toolkit: toolkit))
|
|
147
156
|
end
|
|
148
157
|
client = BridgeClient.wait_for(session.id, session_dir: Session.session_dir(session.id, state_dir: sd), timeout: wait)
|
|
149
158
|
client || raise(Error, "the worker for session #{session.id} did not start its Bridge in time")
|
|
@@ -159,14 +168,15 @@ module Samagotchi
|
|
|
159
168
|
end
|
|
160
169
|
|
|
161
170
|
# An existing session's prompt is built from its own session fields:
|
|
162
|
-
# --memory/--mute/--append-system-prompt
|
|
163
|
-
# --resume are ignored, with a line saying so (printed
|
|
164
|
-
# region opens).
|
|
165
|
-
def warn_memory_flags_ignored(session_id, memories, muted_memories, err, appended_prompt_files: [])
|
|
171
|
+
# --memory/--mute/--append-system-prompt and the toolkit flags given
|
|
172
|
+
# with --attach or --resume are ignored, with a line saying so (printed
|
|
173
|
+
# before the live region opens).
|
|
174
|
+
def warn_memory_flags_ignored(session_id, memories, muted_memories, err, appended_prompt_files: [], toolkit_flags: [])
|
|
166
175
|
flags = []
|
|
167
176
|
flags << "--memory" unless Array(memories).empty?
|
|
168
177
|
flags << "--mute" unless Array(muted_memories).empty?
|
|
169
178
|
flags << "--append-system-prompt" unless Array(appended_prompt_files).empty?
|
|
179
|
+
flags.concat(toolkit_flags)
|
|
170
180
|
return if flags.empty?
|
|
171
181
|
|
|
172
182
|
verb = flags.size == 1 ? "applies" : "apply"
|
|
@@ -260,13 +260,17 @@ module Samagotchi
|
|
|
260
260
|
take_session_state(event[:session_state_snapshot] || {})
|
|
261
261
|
return render_snapshot(event[:snapshot] || {}, reset: event[:type] == :reset)
|
|
262
262
|
when :turn_enqueued then show_enqueued(event)
|
|
263
|
-
when :turn_started
|
|
263
|
+
when :turn_started
|
|
264
|
+
@status.take_event(event)
|
|
265
|
+
start_turn(event)
|
|
264
266
|
when :turn_completed
|
|
267
|
+
@status.take_event(event)
|
|
265
268
|
complete_turn(event)
|
|
266
269
|
refresh_children
|
|
267
270
|
own_turn_ended(event)
|
|
268
271
|
# The renderer says how it ended (the REPL's words too).
|
|
269
272
|
when :turn_canceled, :turn_failed
|
|
273
|
+
@status.take_event(event)
|
|
270
274
|
@renderer.call(event)
|
|
271
275
|
end_turn
|
|
272
276
|
refresh_children
|
|
@@ -1239,7 +1243,7 @@ module Samagotchi
|
|
|
1239
1243
|
# A turn that ended with no answer: its notice, as live.
|
|
1240
1244
|
empty = messages.drop(from + 1).filter_map { |m| TurnNote.empty_answer(m) }.last
|
|
1241
1245
|
if empty
|
|
1242
|
-
@screen.commit(format_empty_answer_line(empty[:retries] || empty["retries"]))
|
|
1246
|
+
@screen.commit(format_empty_answer_line(empty[:retries] || empty["retries"], empty[:refusal] || empty["refusal"]))
|
|
1243
1247
|
elsif failed
|
|
1244
1248
|
# A failed turn whose steps stayed: the line the live view left,
|
|
1245
1249
|
# and no step text as if it were the answer (the web keeps a
|
|
@@ -1379,6 +1383,8 @@ module Samagotchi
|
|
|
1379
1383
|
def render_current_turn(turn)
|
|
1380
1384
|
@running = !turn.nil?
|
|
1381
1385
|
@joined_mid_turn = @running
|
|
1386
|
+
# A turn running at the join: the status row's steer hint.
|
|
1387
|
+
@status.update(running: @running)
|
|
1382
1388
|
return unless turn
|
|
1383
1389
|
|
|
1384
1390
|
continues = turn[:prompt].nil?
|
|
@@ -179,7 +179,8 @@ module Samagotchi
|
|
|
179
179
|
end
|
|
180
180
|
# No answer: chi's muted notice, never an empty answer line.
|
|
181
181
|
if summary[:empty_answer]
|
|
182
|
-
|
|
182
|
+
empty = summary[:empty_answer]
|
|
183
|
+
@view.print_line(@view.format_empty_answer_line(empty[:retries], empty[:refusal]))
|
|
183
184
|
else
|
|
184
185
|
@view.print_line(summary[:output])
|
|
185
186
|
end
|
|
@@ -135,9 +135,10 @@ module Samagotchi
|
|
|
135
135
|
end
|
|
136
136
|
|
|
137
137
|
# "no answer: the model returned nothing (after 1 retry)", dim like the
|
|
138
|
-
# retry row: a turn that ended with no answer
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
# retry row: a turn that ended with no answer, or "no answer: refused
|
|
139
|
+
# by the provider (cyber)" for a +refusal+ (TurnNote.empty_answer_line).
|
|
140
|
+
def format_empty_answer_line(retries, refusal = nil)
|
|
141
|
+
paint(TurnNote.empty_answer_line(retries, refusal: refusal), 90)
|
|
141
142
|
end
|
|
142
143
|
|
|
143
144
|
# "↻ retrying (503) in 4.0s, 2/5": the live status while the provider
|
|
@@ -28,7 +28,15 @@ module Samagotchi
|
|
|
28
28
|
# preloaded its --memory list (shown until a turn
|
|
29
29
|
# records them as used)
|
|
30
30
|
# muted its --mute list
|
|
31
|
-
|
|
31
|
+
# running a turn runs: the row trails STEER_HINT
|
|
32
|
+
FIELDS = %i[model default_model served parent_id children context used_memories preloaded muted running].freeze
|
|
33
|
+
|
|
34
|
+
# The tail while a turn runs: where a typed line goes (the next step)
|
|
35
|
+
# and the two other choices. Dropped whole first when the row is cut.
|
|
36
|
+
STEER_HINT = " · typing goes in at the next step (/cut, /queue)"
|
|
37
|
+
|
|
38
|
+
# The turn events that end a turn (the hint goes).
|
|
39
|
+
TURN_END_EVENTS = %i[turn_completed turn_canceled turn_failed].freeze
|
|
32
40
|
|
|
33
41
|
# @param surface [Surface] with #columns
|
|
34
42
|
def initialize(surface)
|
|
@@ -77,13 +85,16 @@ module Samagotchi
|
|
|
77
85
|
|
|
78
86
|
# Take what a turn event says about the row (the Engine's events in
|
|
79
87
|
# the REPL, the Bridge's in attached mode): ctx, the memories used,
|
|
80
|
-
# the model the server served
|
|
88
|
+
# the model the server served, whether a turn runs (the steer
|
|
89
|
+
# hint). Other events change nothing.
|
|
81
90
|
def take_event(event)
|
|
82
91
|
case event[:type]
|
|
83
92
|
when :context_status then update(context: { est_pct: event.dig(:usage, :estimated_pct), bucket: event[:bucket] })
|
|
84
93
|
when :used_memories_updated then update(used_memories: Array(event[:used_memory_names]))
|
|
85
94
|
when :generation_completed
|
|
86
95
|
update(served: [event[:served_model], event[:requested_model], event[:served_expected_by]]) if event[:served_model]
|
|
96
|
+
when :turn_started then update(running: true)
|
|
97
|
+
when *TURN_END_EVENTS then update(running: false)
|
|
87
98
|
end
|
|
88
99
|
end
|
|
89
100
|
|
|
@@ -119,11 +130,21 @@ module Samagotchi
|
|
|
119
130
|
status_context_text(estimate: @values[:context]),
|
|
120
131
|
status_memory_text(Array(@values[:used_memories]) | Array(@values[:preloaded]), MEMORY_LIMIT),
|
|
121
132
|
status_memory_text(@values[:muted], MEMORY_LIMIT, label: "muted")].reject(&:empty?)
|
|
122
|
-
status_rows(segments, width)
|
|
133
|
+
status_rows(with_steer_hint(segments, width), width)
|
|
123
134
|
end
|
|
124
135
|
|
|
125
136
|
private
|
|
126
137
|
|
|
138
|
+
# +segments+ with STEER_HINT after the last while a turn runs, when
|
|
139
|
+
# the whole row fits +width+ with it: on a narrow terminal the hint
|
|
140
|
+
# goes first, whole, before anything else is cut.
|
|
141
|
+
def with_steer_hint(segments, width)
|
|
142
|
+
return segments unless @values[:running] && segments.any?
|
|
143
|
+
return segments if "status> #{segments.join(" | ")}#{STEER_HINT}".length > width
|
|
144
|
+
|
|
145
|
+
segments[0..-2] + ["#{segments.last}#{STEER_HINT}"]
|
|
146
|
+
end
|
|
147
|
+
|
|
127
148
|
# `⑂ 2 running · 1 waiting`, the counts there are; "" with neither (no
|
|
128
149
|
# children, or all ended). Not "not reported": a REPL parent is never
|
|
129
150
|
# given its children's replies (no wakes there), and a worker moves
|
|
@@ -97,10 +97,15 @@ module Samagotchi
|
|
|
97
97
|
# @param appended_prompt_files [Array<String>] --append-system-prompt's
|
|
98
98
|
# files (absolute): a new session saves them; a resumed one's own come
|
|
99
99
|
# first, and the merge goes only to the Engine (as --memory's does)
|
|
100
|
+
# @param toolkit [Toolkit, nil] --toolkit/--tool/--no-tool: a new
|
|
101
|
+
# session's toolkit (config's toolkit.default without); a resumed one
|
|
102
|
+
# keeps its own, with a line naming +toolkit_flags+ (the flags given)
|
|
100
103
|
def initialize(prompt: nil, client: nil, host_registry: nil, profile: nil, session_id: nil, no_interrupt: false, no_default_input: false, model_name: nil, memories: [], muted_memories: [], non_interactive: false, surface: nil,
|
|
101
104
|
spinner_tick_interval: AttachedView::TICK_INTERVAL, spinner_clock: nil, scratch: false, llm_context: {}, thinking: nil,
|
|
102
|
-
appended_prompt_files: [])
|
|
105
|
+
appended_prompt_files: [], toolkit: nil, toolkit_flags: [])
|
|
103
106
|
@scratch = scratch
|
|
107
|
+
@toolkit = toolkit
|
|
108
|
+
@toolkit_flags = toolkit_flags
|
|
104
109
|
@thinking_word = thinking
|
|
105
110
|
@prompt = prompt
|
|
106
111
|
@llm_context_words = llm_context
|
|
@@ -269,7 +274,7 @@ module Samagotchi
|
|
|
269
274
|
preloaded_memory_names: @requested_memories,
|
|
270
275
|
muted_memory_names: @muted_memory_names,
|
|
271
276
|
scratch: @scratch,
|
|
272
|
-
setup: SessionSetup.new(appended_prompt_files: @appended_prompt_files)
|
|
277
|
+
setup: SessionSetup.new(appended_prompt_files: @appended_prompt_files, toolkit: @toolkit || Toolkit.configured)
|
|
273
278
|
))
|
|
274
279
|
# --llm-context*: the session's own values, over a resumed one's.
|
|
275
280
|
# Saved at once on a resumed one: leaving before a turn keeps them.
|
|
@@ -279,6 +284,7 @@ module Samagotchi
|
|
|
279
284
|
session.thinking = Thinking.session_level(@thinking_word) if @thinking_word
|
|
280
285
|
session.save if @resume_session
|
|
281
286
|
end
|
|
287
|
+
warn_toolkit_flags_ignored(session) if @resume_session
|
|
282
288
|
claim_session!(session.id) unless @owner_lock
|
|
283
289
|
# A turn a worker died in (TurnCheckpoint): ended as canceled, with its
|
|
284
290
|
# note, before this REPL's first turn appends after it.
|
|
@@ -714,6 +720,16 @@ module Samagotchi
|
|
|
714
720
|
end
|
|
715
721
|
end
|
|
716
722
|
|
|
723
|
+
# The toolkit flags are a new session's: a resumed one keeps its own
|
|
724
|
+
# toolkit (as attached mode says it, AttachLauncher).
|
|
725
|
+
def warn_toolkit_flags_ignored(session)
|
|
726
|
+
return if @toolkit_flags.empty?
|
|
727
|
+
|
|
728
|
+
names = @toolkit_flags.size > 1 ? "#{@toolkit_flags[0..-2].join(", ")} and #{@toolkit_flags.last}" : @toolkit_flags.first
|
|
729
|
+
warn "(#{names} #{@toolkit_flags.size == 1 ? "applies" : "apply"} to a new session; #{session.id}'s prompt is already built)"
|
|
730
|
+
end
|
|
731
|
+
private :warn_toolkit_flags_ignored
|
|
732
|
+
|
|
717
733
|
private
|
|
718
734
|
|
|
719
735
|
# Interactive REPL loop. Session seed + messages are built by #run and
|
|
@@ -49,8 +49,11 @@ module Samagotchi
|
|
|
49
49
|
def run(call, iteration:, call_index:, call_count:, on_stream_event:, max_tool_output_chars:)
|
|
50
50
|
params = ToolActivity.tool_activity_params(call[:name], call, registry: tools)
|
|
51
51
|
# The gate runs first, so tool_call_started shows the call that runs.
|
|
52
|
+
# A tool the session's toolkit leaves out skips it: no before_tool_call
|
|
53
|
+
# hook, no approval card for a call that can't run (KernelLoop#dispatch
|
|
54
|
+
# answers it with the not-loaded error).
|
|
52
55
|
original = call.dup
|
|
53
|
-
verdict = evaluate(call, iteration, params)
|
|
56
|
+
verdict = left_out?(call) ? Guardrails::Verdict.new(call: call) : evaluate(call, iteration, params)
|
|
54
57
|
call = verdict.call
|
|
55
58
|
params = ToolActivity.tool_activity_params(call[:name], call, registry: tools)
|
|
56
59
|
label = plugin_label(call[:name])
|
|
@@ -265,6 +268,15 @@ module Samagotchi
|
|
|
265
268
|
# The kernel's tools, for the activity line of a tool that isn't built in.
|
|
266
269
|
def tools = @kernel.tools
|
|
267
270
|
|
|
271
|
+
# A real tool the session's toolkit leaves out of this turn.
|
|
272
|
+
def left_out?(call)
|
|
273
|
+
registry = tools
|
|
274
|
+
return false unless registry.respond_to?(:left_out) && !call[:name].nil?
|
|
275
|
+
|
|
276
|
+
layers = @kernel.respond_to?(:llm_context_layers) ? @kernel.llm_context_layers : []
|
|
277
|
+
!registry.left_out(call[:name], layers: layers).nil?
|
|
278
|
+
end
|
|
279
|
+
|
|
268
280
|
# A tool the registry has from a plugin (not core, not unknown).
|
|
269
281
|
def plugin_tool?(name)
|
|
270
282
|
entry = tools && !name.nil? ? tools[name] : nil
|