samagotchi 0.154.1 → 0.156.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 +29 -1
- data/bin/chi +1 -1
- data/docs/architecture.md +12 -7
- 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 +5 -5
- data/docs/internals/attached-context.md +9 -6
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/block-renderers.md +8 -1
- data/docs/internals/engine.md +642 -831
- data/docs/internals/inline-widgets.md +136 -0
- data/docs/internals/llm-context-forget.md +8 -3
- data/docs/internals/objectives.md +8 -6
- data/docs/internals/participants.md +5 -5
- data/docs/internals/prompt-caching.md +13 -6
- data/docs/internals/remote-sessions.md +3 -1
- 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 +4 -4
- data/docs/system-one.md +3 -2
- data/docs/testing.md +1 -1
- data/lib/samagotchi/block_renderers.rb +5 -0
- data/lib/samagotchi/bootstrap/config_writer.rb +1 -1
- data/lib/samagotchi/broadcast_command.rb +3 -3
- data/lib/samagotchi/bundle_command/check.rb +1 -1
- data/lib/samagotchi/bundle_command/try/sandbox.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/config_modification_protocol.md +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/child_move.rb +7 -51
- data/lib/samagotchi/children_status.rb +1 -1
- data/lib/samagotchi/cli/parent_wait.rb +1 -1
- data/lib/samagotchi/config.rb +17 -112
- data/lib/samagotchi/context_command.rb +1 -2
- data/lib/samagotchi/context_window.rb +3 -3
- data/lib/samagotchi/desktop/macos.rb +1 -1
- data/lib/samagotchi/engine/context_judge.rb +21 -119
- data/lib/samagotchi/engine/guardrails.rb +4 -2
- data/lib/samagotchi/engine/messages.rb +9 -6
- data/lib/samagotchi/engine/models.rb +5 -2
- data/lib/samagotchi/engine/objectives.rb +30 -80
- 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 +26 -13
- data/lib/samagotchi/engine/toolkit.rb +35 -191
- 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 +166 -98
- 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 +28 -11
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/host_config.rb +72 -1
- data/lib/samagotchi/host_model.rb +2 -6
- data/lib/samagotchi/host_registry.rb +1 -1
- data/lib/samagotchi/inline_widgets.rb +325 -0
- data/lib/samagotchi/kernel_loop.rb +215 -313
- data/lib/samagotchi/llm/anthropic_wire.rb +2 -2
- data/lib/samagotchi/llm/backend.rb +3 -1
- data/lib/samagotchi/llm/chat_loop.rb +87 -49
- data/lib/samagotchi/llm/native_backend.rb +3 -2
- data/lib/samagotchi/llm_context_apply.rb +54 -1
- data/lib/samagotchi/llm_context_forget.rb +5 -3
- data/lib/samagotchi/llm_context_notice.rb +1 -1
- data/lib/samagotchi/llm_context_strategy.rb +7 -9
- data/lib/samagotchi/llm_context_view.rb +9 -0
- data/lib/samagotchi/model_binding.rb +3 -3
- data/lib/samagotchi/model_price.rb +4 -10
- data/lib/samagotchi/model_profile.rb +6 -6
- data/lib/samagotchi/model_settings.rb +37 -2
- data/lib/samagotchi/note_command.rb +1 -2
- data/lib/samagotchi/objectives_audit.rb +4 -3
- data/lib/samagotchi/participants.rb +2 -2
- data/lib/samagotchi/plugin/context.rb +1 -1
- data/lib/samagotchi/plugin/prompt_sections.rb +7 -0
- data/lib/samagotchi/plugin/sessions.rb +2 -5
- data/lib/samagotchi/plugin/tool_spec.rb +5 -3
- data/lib/samagotchi/project_env.rb +1 -1
- data/lib/samagotchi/question_desk.rb +1 -1
- data/lib/samagotchi/reasoning_history.rb +3 -3
- data/lib/samagotchi/reminder_carry.rb +1 -1
- data/lib/samagotchi/reprefill_meter.rb +45 -0
- data/lib/samagotchi/review_command.rb +1 -1
- data/lib/samagotchi/revise_anchor.rb +1 -1
- data/lib/samagotchi/sampling_settings.rb +3 -3
- data/lib/samagotchi/self_report.rb +7 -7
- data/lib/samagotchi/session.rb +50 -7
- data/lib/samagotchi/session_access/local.rb +7 -5
- data/lib/samagotchi/session_access/remote.rb +17 -3
- data/lib/samagotchi/session_archive_command.rb +2 -2
- data/lib/samagotchi/session_chain.rb +7 -6
- data/lib/samagotchi/session_continue.rb +6 -5
- data/lib/samagotchi/session_delete_command.rb +1 -1
- data/lib/samagotchi/session_manager/delivery.rb +254 -0
- data/lib/samagotchi/session_manager/emptiness.rb +138 -0
- data/lib/samagotchi/session_manager/lifecycle.rb +274 -0
- data/lib/samagotchi/session_manager/listing.rb +189 -0
- data/lib/samagotchi/session_manager/removal.rb +190 -0
- data/lib/samagotchi/session_manager/spawn.rb +242 -0
- data/lib/samagotchi/session_manager/worker_command.rb +157 -0
- data/lib/samagotchi/session_manager.rb +26 -1540
- data/lib/samagotchi/session_metrics.rb +1 -1
- data/lib/samagotchi/sessions_command.rb +1 -1
- data/lib/samagotchi/share_command.rb +1 -1
- data/lib/samagotchi/starting_marker.rb +65 -0
- data/lib/samagotchi/steer_cut_after.rb +3 -3
- data/lib/samagotchi/system_one/recorder.rb +49 -0
- data/lib/samagotchi/system_prompt.rb +37 -30
- data/lib/samagotchi/terminal_ui/exit_lines.rb +1 -1
- data/lib/samagotchi/terminal_ui.rb +2 -6
- 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/testing/sessions.rb +1 -1
- data/lib/samagotchi/thinking.rb +3 -3
- data/lib/samagotchi/token_usage.rb +1 -1
- data/lib/samagotchi/tool_activity.rb +15 -7
- data/lib/samagotchi/tool_declarations.rb +6 -6
- data/lib/samagotchi/tool_output_cut.rb +17 -0
- data/lib/samagotchi/tool_response.rb +4 -3
- data/lib/samagotchi/tool_runner.rb +96 -102
- data/lib/samagotchi/toolkit_binding.rb +40 -14
- data/lib/samagotchi/toolkit_command.rb +1 -1
- data/lib/samagotchi/tools/deferred_tools.rb +17 -21
- data/lib/samagotchi/tools/delegate.rb +10 -5
- data/lib/samagotchi/tools/delegate_result.rb +4 -4
- data/lib/samagotchi/tools/index_hint.rb +14 -0
- data/lib/samagotchi/tools/registry.rb +64 -44
- data/lib/samagotchi/tools/send_note.rb +4 -6
- data/lib/samagotchi/tools/tool_index.rb +150 -102
- data/lib/samagotchi/tools/tool_surface.rb +187 -0
- data/lib/samagotchi/turn_context.rb +68 -0
- data/lib/samagotchi/turn_stages/context_judge.rb +172 -0
- data/lib/samagotchi/turn_stages/objectives.rb +150 -0
- data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +257 -0
- data/lib/samagotchi/turn_stages/verify_anchors.rb +168 -0
- data/lib/samagotchi/turn_stages/warmup.rb +82 -0
- data/lib/samagotchi/turn_state.rb +46 -4
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +3 -3
- data/lib/samagotchi/web/app.rb +15 -8
- data/lib/samagotchi/web/markdown_renderer.rb +39 -14
- data/lib/samagotchi/web/message_display.rb +1 -1
- data/lib/samagotchi/web/participant_access.rb +2 -2
- 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
- data/lib/samagotchi/web/session_access_routes.rb +3 -3
- data/lib/samagotchi/web/session_control.rb +3 -3
- data/lib/samagotchi/web/session_create.rb +3 -3
- data/lib/samagotchi/web/session_hub.rb +1 -1
- data/lib/samagotchi/web/session_show.rb +3 -3
- data/lib/samagotchi/web/session_summary.rb +3 -4
- data/lib/samagotchi/worker.rb +55 -27
- data/lib/samagotchi/worker_boot.rb +184 -0
- data/lib/samagotchi/worker_signals.rb +1 -1
- data/lib/samagotchi/worker_task_wakes.rb +2 -2
- metadata +29 -1
|
@@ -24,8 +24,11 @@ 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"
|
|
30
|
+
require_relative "reprefill_meter"
|
|
31
|
+
require_relative "tool_output_cut"
|
|
29
32
|
require_relative "tool_runner"
|
|
30
33
|
require_relative "tool_response"
|
|
31
34
|
require_relative "tool_ids"
|
|
@@ -38,6 +41,7 @@ require_relative "answer_display"
|
|
|
38
41
|
require_relative "message_time"
|
|
39
42
|
require_relative "llm/model_result"
|
|
40
43
|
require_relative "llm/turn_settings"
|
|
44
|
+
require_relative "turn_context"
|
|
41
45
|
require_relative "llm/stream_drops"
|
|
42
46
|
|
|
43
47
|
module Samagotchi
|
|
@@ -59,30 +63,34 @@ module Samagotchi
|
|
|
59
63
|
# The built-in tool classes (Tools::Builtins registers them).
|
|
60
64
|
TOOLS = Tools::Builtins::CLASSES
|
|
61
65
|
|
|
62
|
-
# What a tool handler (call, kctx) gets
|
|
63
|
-
#
|
|
64
|
-
#
|
|
66
|
+
# What a tool handler (call, kctx) gets: the kernel's reminder store
|
|
67
|
+
# and peers, the call's turn (+context+, a TurnContext: the participant's
|
|
68
|
+
# mode, the model key), and the memory read and ask-user flows that need
|
|
69
|
+
# them. One per dispatched call.
|
|
65
70
|
class ToolContext
|
|
66
|
-
def initialize(kernel)
|
|
71
|
+
def initialize(kernel, context)
|
|
67
72
|
@kernel = kernel
|
|
73
|
+
@context = context
|
|
68
74
|
end
|
|
69
75
|
|
|
70
76
|
def reminder_store = @kernel.reminder_store
|
|
71
|
-
def peers = @kernel.peers
|
|
77
|
+
def peers = @kernel.services.peers
|
|
72
78
|
|
|
73
|
-
# The
|
|
74
|
-
# :
|
|
75
|
-
def participant_mode = @
|
|
76
|
-
|
|
79
|
+
# The participant turn's mode (TurnContext#participant_mode: :pair,
|
|
80
|
+
# :review), nil for the owner's turns.
|
|
81
|
+
def participant_mode = @context.participant_mode
|
|
82
|
+
# The memory overlay key (TurnContext#model_key, frozen at the turn's
|
|
83
|
+
# generate).
|
|
84
|
+
def model_key = @context.model_key
|
|
77
85
|
|
|
78
86
|
# memory_read with the session's mutes applied: a blank name (the index)
|
|
79
87
|
# loses the muted memories' lines; a muted name in a comma list is
|
|
80
88
|
# refused with its own error line and the rest is read as usual.
|
|
81
89
|
def muted_memory_read(call)
|
|
82
|
-
muted = Array(@kernel.muted_memory_names)
|
|
90
|
+
muted = Array(@kernel.services.muted_memory_names)
|
|
83
91
|
content = call[:content].to_s
|
|
84
92
|
read = lambda do |names|
|
|
85
|
-
Tools::MemoryRead.call(names, scope: call[:scope], model_key: model_key, fallback_model_key: @
|
|
93
|
+
Tools::MemoryRead.call(names, scope: call[:scope], model_key: model_key, fallback_model_key: @context.fallback_model_key)
|
|
86
94
|
end
|
|
87
95
|
return read.call(content) if muted.empty?
|
|
88
96
|
return MutedMemories.filter_index(read.call(content), muted) if content.strip.empty?
|
|
@@ -98,14 +106,14 @@ module Samagotchi
|
|
|
98
106
|
end
|
|
99
107
|
|
|
100
108
|
# ask_user_question: validate the call, then hand the payload to the
|
|
101
|
-
# Engine's question flow (the kernel's question_handler, which blocks
|
|
109
|
+
# Engine's question flow (the kernel's Services#question_handler, which blocks
|
|
102
110
|
# until the user answers). Without one (headless), the payload as JSON
|
|
103
111
|
# so the model sees the options and can ask in plain text.
|
|
104
112
|
def ask_user_question(call)
|
|
105
113
|
payload = Tools::AskUserQuestion.validate(call)
|
|
106
114
|
return payload if payload.is_a?(String)
|
|
107
115
|
|
|
108
|
-
handler = @kernel.question_handler
|
|
116
|
+
handler = @kernel.services.question_handler
|
|
109
117
|
return JSON.pretty_generate(payload) unless handler
|
|
110
118
|
|
|
111
119
|
handler.call(payload).to_s
|
|
@@ -115,44 +123,102 @@ module Samagotchi
|
|
|
115
123
|
|
|
116
124
|
# forget_outputs: the forget layer's call on the running turn's
|
|
117
125
|
# conversation (KernelLoop#forget_outputs).
|
|
118
|
-
def forget_outputs(call) = @kernel.forget_outputs(call)
|
|
126
|
+
def forget_outputs(call) = @kernel.forget_outputs(call, @context)
|
|
127
|
+
|
|
128
|
+
# The turn's Tools::ToolSurface (TurnContext#surface; a bare kernel's
|
|
129
|
+
# own context has one too, KernelLoop#own_context): what the tool
|
|
130
|
+
# index's handlers (find_tools, call_tool) read.
|
|
131
|
+
def surface = Tools::ToolSurface.of(@context)
|
|
119
132
|
end
|
|
120
133
|
|
|
121
|
-
DEFAULT_MAX_TOOL_OUTPUT_CHARS = 10_000
|
|
122
134
|
QWEN_INCOMPLETE_TOOL_CALL_RECOVERY_LIMIT = 2
|
|
123
135
|
# How much of a malformed generation's end the warning keeps.
|
|
124
136
|
MALFORMED_TAIL_CHARS = 400
|
|
125
137
|
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
138
|
|
|
127
|
-
#
|
|
128
|
-
#
|
|
139
|
+
# What a kernel holds of its Engine for the Engine's life, one value
|
|
140
|
+
# (Engine#wire_kernel builds it; a spec's kernel takes one through
|
|
141
|
+
# #attach): nothing per turn (a turn's TurnContext goes down #run).
|
|
142
|
+
#
|
|
143
|
+
# - hooks: the Hooks::Registry the loop fires (nil: none); the Engine's
|
|
144
|
+
# - tools: the Tools::Registry #dispatch runs (the built-ins by default)
|
|
145
|
+
# - reminder_store: the ReminderStore the reminder tools write (nil: none)
|
|
146
|
+
# - muted_memory_names: the session's muted memories (normalized names,
|
|
147
|
+
# MutedMemories), refused by memory_read; nil: none
|
|
148
|
+
# - question_handler: answers ask_user_question (payload → answer
|
|
149
|
+
# string; the Engine's blocking request_question); nil: headless
|
|
150
|
+
# - peers: Tools::Peers (or the Engine's PeerView): the session
|
|
151
|
+
# list_sessions and send_note speak for; nil outside a session
|
|
152
|
+
# - gate: the Guardrails::Gate ToolRunner asks before each call; nil:
|
|
153
|
+
# ToolRunner's own, hooks only
|
|
154
|
+
# - warmup: the Engine's PromptWarmup, asked for a slot pin per request
|
|
155
|
+
# (#take_pin) while the turn-end warm-up runs; nil: never pinned
|
|
156
|
+
# - generation_guard: called before each generation with the turn's
|
|
157
|
+
# TurnContext, before the before_generation hook; the Engine's check
|
|
158
|
+
# that may stop the turn (a pair turn past its participant's cap,
|
|
159
|
+
# Engine#guard_participant_spend); nil: none
|
|
160
|
+
#
|
|
161
|
+
# The client is not one of them: a model switch swaps it
|
|
162
|
+
# (#client=, Engine#sync_kernel_client!).
|
|
163
|
+
Services = Data.define(:hooks, :tools, :reminder_store, :muted_memory_names, :question_handler, :peers, :gate,
|
|
164
|
+
:warmup, :generation_guard) do
|
|
165
|
+
# A bare kernel's (specs, a kernel built without its Engine): the
|
|
166
|
+
# built-in tools and nothing else, or the +fields+ given.
|
|
167
|
+
# @return [Services]
|
|
168
|
+
def self.none(**fields)
|
|
169
|
+
new(hooks: nil, tools: Tools::Builtins.default, reminder_store: nil, muted_memory_names: nil,
|
|
170
|
+
question_handler: nil, peers: nil, gate: nil, warmup: nil, generation_guard: nil).with(**fields)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# @param services [Services, nil] what the kernel holds of its Engine
|
|
175
|
+
# (Services.none by default: the built-in tools alone)
|
|
176
|
+
# @param reprefill [ReprefillMeter, nil] the re-prefill baseline its
|
|
177
|
+
# requests count against (the Engine's, shared with its ChatLoop; its
|
|
178
|
+
# own by default)
|
|
129
179
|
# @param sleeper [#call, nil] waits the given seconds before a dropped
|
|
130
180
|
# 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)
|
|
181
|
+
def initialize(client: nil, profile: nil, model_name: nil, model_key: nil, services: nil, reprefill: nil, sleeper: nil)
|
|
133
182
|
@client = client || Client.new
|
|
134
183
|
@sleeper = sleeper || ->(seconds) { sleep(seconds) }
|
|
135
|
-
@
|
|
184
|
+
@services = services || Services.none
|
|
185
|
+
@reprefill = reprefill || ReprefillMeter.new
|
|
136
186
|
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)
|
|
187
|
+
# The context of the run in progress, else of the last run (#context):
|
|
188
|
+
# a kernel that ran none has its own (no turn, no settings but its
|
|
189
|
+
# model name, its model key). #run takes the one it is given.
|
|
190
|
+
@context = TurnContext.none(settings: LLM::TurnSettings.none.with(model_name: resolved_model_name),
|
|
191
|
+
model_key: model_key)
|
|
143
192
|
@profile = profile ? ModelProfile.normalize(profile) : ModelProfile.from_model_name(resolved_model_name)
|
|
144
193
|
# Where @profile came from, as /stats shows it (a Resolution's label
|
|
145
194
|
# once the Engine resolves one, see #use_profile!).
|
|
146
195
|
@profile_source = profile ? "given" : "name"
|
|
147
|
-
@hooks = hooks
|
|
148
|
-
@reminder_store = reminder_store
|
|
149
|
-
@model_key = model_key
|
|
150
|
-
@model_key_fallback = nil
|
|
151
196
|
end
|
|
152
197
|
|
|
153
|
-
# @return [
|
|
154
|
-
|
|
155
|
-
|
|
198
|
+
# @return [Services] what the kernel holds of its Engine (#attach)
|
|
199
|
+
attr_reader :services
|
|
200
|
+
|
|
201
|
+
# Hand the kernel its Engine's services, all at once, and its
|
|
202
|
+
# re-prefill meter (nil keeps the one it has): the Engine does for a
|
|
203
|
+
# kernel it is given (specs) what KernelLoop.new does for its own.
|
|
204
|
+
# @param services [Services]
|
|
205
|
+
# @param reprefill [ReprefillMeter, nil]
|
|
206
|
+
# @return [Services]
|
|
207
|
+
def attach(services, reprefill: nil)
|
|
208
|
+
@reprefill = reprefill if reprefill
|
|
209
|
+
@services = services
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# @return [Hooks::Registry, nil] the hooks the loop fires (Services#hooks)
|
|
213
|
+
def hooks = @services.hooks
|
|
214
|
+
# @return [Tools::Registry] the tools #dispatch runs (Services#tools)
|
|
215
|
+
def tools = @services.tools
|
|
216
|
+
# @return [ReminderStore, nil] the store the reminder tools write
|
|
217
|
+
# (Services#reminder_store)
|
|
218
|
+
def reminder_store = @services.reminder_store
|
|
219
|
+
# @return [Guardrails::Gate, nil] the gate ToolRunner asks before each
|
|
220
|
+
# call (Services#gate)
|
|
221
|
+
def guardrail_gate = @services.gate
|
|
156
222
|
|
|
157
223
|
# @return [Float, nil] the last counted request's chars per token
|
|
158
224
|
# (ContextStatus#counted_chars_per_token), for the next turn's first
|
|
@@ -162,79 +228,43 @@ module Samagotchi
|
|
|
162
228
|
# @return [ModelProfile] the active prompt profile
|
|
163
229
|
attr_reader :profile
|
|
164
230
|
|
|
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.
|
|
231
|
+
# The model client; a model switch swaps it (Engine#sync_kernel_client!).
|
|
232
|
+
attr_accessor :client
|
|
233
|
+
|
|
234
|
+
# @return [String, nil] the model key a kernel was built with (its own
|
|
235
|
+
# #context's, for a run given none)
|
|
236
|
+
def model_key = @context.model_key
|
|
237
|
+
|
|
197
238
|
# 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.
|
|
239
|
+
# on the client and +settings+, a turn's LLM::TurnSettings): ToolRunner
|
|
240
|
+
# halves it for memory_read's overflow guard. nil when it can't be read.
|
|
200
241
|
# @return [ContextWindow::Resolved, nil]
|
|
201
|
-
def tool_output_window
|
|
242
|
+
def tool_output_window(settings = turn_settings)
|
|
202
243
|
Degrade.call(:model, "context_window_unresolved") do
|
|
203
|
-
ContextWindow.resolve(client: @client, model:
|
|
244
|
+
ContextWindow.resolve(client: @client, model: settings&.model_name, setting: settings&.window_setting)
|
|
204
245
|
end
|
|
205
246
|
end
|
|
206
247
|
|
|
207
|
-
|
|
208
|
-
|
|
248
|
+
# The generation guard for +context+'s turn, if any; one that raises
|
|
249
|
+
# never breaks the turn.
|
|
250
|
+
def guard_generation(context = @context)
|
|
251
|
+
@services.generation_guard&.call(context)
|
|
209
252
|
rescue StandardError => e
|
|
210
253
|
Log.warn(:turn, "generation_guard_failed", error: "#{e.class}: #{e.message}")
|
|
211
254
|
nil
|
|
212
255
|
end
|
|
213
256
|
|
|
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
|
|
257
|
+
# @return [TurnContext] the context of the run in progress, else of the
|
|
258
|
+
# last run (the kernel's own before any): its turn's settings,
|
|
259
|
+
# session folders and participant mode. Read on the turn's thread
|
|
260
|
+
# only; another thread reads the Engine's (Engine#turn_context).
|
|
261
|
+
attr_reader :context
|
|
262
|
+
|
|
263
|
+
# @return [LLM::TurnSettings] the vision, sampling, thinking and model
|
|
264
|
+
# name of the run in progress, else of the last one (#context's; #run
|
|
265
|
+
# sets the model name); the thinking level is its own field, since the
|
|
266
|
+
# native path sends the sampling as is.
|
|
267
|
+
def turn_settings = @context.settings
|
|
238
268
|
# The turn a forget_outputs call works on (LLMContextForget::Turn: its
|
|
239
269
|
# conversation and ContextStatus); both loops set it as a turn starts.
|
|
240
270
|
attr_accessor :llm_context_turn
|
|
@@ -265,13 +295,17 @@ module Samagotchi
|
|
|
265
295
|
# run it resumes started in +messages+; that run's model messages keep
|
|
266
296
|
# their thought blocks (#prepare_conversation), as within one run. nil:
|
|
267
297
|
# every earlier thought is stripped.
|
|
298
|
+
# @param context [TurnContext] the run's turn (Engine#generate's); a
|
|
299
|
+
# bare kernel's own (#own_context) by default. Its settings take the
|
|
300
|
+
# run's model name: this run's own copy, never published.
|
|
268
301
|
# @return [LLM::ModelResult] final visible response with continuation metadata
|
|
269
302
|
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
|
-
|
|
303
|
+
reasoning_since: nil, context: own_context)
|
|
304
|
+
settings = context.settings || LLM::TurnSettings.none
|
|
305
|
+
@context = context.with(settings: settings.with(model_name: completion_model_name(model_name)))
|
|
272
306
|
@last_slot = nil
|
|
273
307
|
turn = start_turn(messages, on_stream_event: on_stream_event, cancel_controller: cancel_controller,
|
|
274
|
-
pending_input: pending_input, cap:
|
|
308
|
+
pending_input: pending_input, cap: ToolOutputCut.max_chars(max_tool_output_chars),
|
|
275
309
|
reasoning_since: reasoning_since)
|
|
276
310
|
|
|
277
311
|
max_iterations.times do |iteration_index|
|
|
@@ -304,28 +338,6 @@ module Samagotchi
|
|
|
304
338
|
@profile_source = resolution.label
|
|
305
339
|
end
|
|
306
340
|
|
|
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
|
-
# Resolve the per-output character cap for the tool outputs the model
|
|
315
|
-
# gets back and the emitted tool call events.
|
|
316
|
-
#
|
|
317
|
-
# Precedence: an explicit override wins, then max_tool_output_chars
|
|
318
|
-
# (Config). A non-positive value falls back to
|
|
319
|
-
# DEFAULT_MAX_TOOL_OUTPUT_CHARS (there is intentionally no "unlimited" — live UIs get a
|
|
320
|
-
# bounded `output:` plus a truthful `output_truncated:` flag). The one
|
|
321
|
-
# exception is memory_read, which ToolRunner leaves out of this cap
|
|
322
|
-
# (memory.read_max_chars, 0: no fixed cap; a memory is read whole).
|
|
323
|
-
# Class-level so the chat loop resolves it the same way.
|
|
324
|
-
def self.resolve_output_char_cap(override)
|
|
325
|
-
parsed = (override || Samagotchi::Config.get("max_tool_output_chars")).to_i
|
|
326
|
-
parsed.positive? ? parsed : DEFAULT_MAX_TOOL_OUTPUT_CHARS
|
|
327
|
-
end
|
|
328
|
-
|
|
329
341
|
private
|
|
330
342
|
|
|
331
343
|
# ── One turn (#run) ────────────────────────────────────────────────────────
|
|
@@ -354,10 +366,11 @@ module Samagotchi
|
|
|
354
366
|
|
|
355
367
|
def start_turn(messages, on_stream_event:, cancel_controller:, pending_input:, cap:, reasoning_since: nil)
|
|
356
368
|
conversation = prepare_conversation(messages, keep_thought_from: ReasoningHistory.run_start(messages, reasoning_since))
|
|
357
|
-
context = ContextStatus.new(conversation: conversation, llm_context:
|
|
369
|
+
context = ContextStatus.new(conversation: conversation, llm_context: turn_settings.llm_context,
|
|
358
370
|
counted_chars_per_token: @counted_chars_per_token)
|
|
359
371
|
emit = ->(event) { emit_stream_event(on_stream_event, event) }
|
|
360
|
-
@llm_context_turn = LLMContextForget::Turn.new(conversation: conversation, context: context, emit: emit
|
|
372
|
+
@llm_context_turn = LLMContextForget::Turn.new(conversation: conversation, context: context, emit: emit,
|
|
373
|
+
turn_id: @context.id)
|
|
361
374
|
Turn.new(
|
|
362
375
|
conversation: conversation, context: context,
|
|
363
376
|
empty_retry: EmptyAnswerRetry.new, empty_steps: [], tool_activity: [], buffer: +"", streamed_thinking: +"",
|
|
@@ -366,8 +379,8 @@ module Samagotchi
|
|
|
366
379
|
# answers at once; on, an open one where the server's template has
|
|
367
380
|
# it. Kept in the turn's model messages, so each tool-loop prompt
|
|
368
381
|
# starts with what the server already has cached.
|
|
369
|
-
prefill: native_prefill(
|
|
370
|
-
pending_tool_calls: false, model_name:
|
|
382
|
+
prefill: native_prefill(turn_settings.thinking || Thinking::DEFAULT),
|
|
383
|
+
pending_tool_calls: false, model_name: turn_settings.model_name, pending_input: pending_input,
|
|
371
384
|
on_stream_event: on_stream_event, cancel_controller: cancel_controller, cap: cap,
|
|
372
385
|
emit: emit
|
|
373
386
|
)
|
|
@@ -385,7 +398,7 @@ module Samagotchi
|
|
|
385
398
|
|
|
386
399
|
def model_props
|
|
387
400
|
Degrade.call(:model, "server_props_unreadable") do
|
|
388
|
-
@client.respond_to?(:server_props) ? @client.server_props(model:
|
|
401
|
+
@client.respond_to?(:server_props) ? @client.server_props(model: turn_settings.model_name) : nil
|
|
389
402
|
end
|
|
390
403
|
end
|
|
391
404
|
|
|
@@ -435,7 +448,7 @@ module Samagotchi
|
|
|
435
448
|
apply_llm_context!(turn.conversation, context: turn.context, emit: turn.emit)
|
|
436
449
|
prompt, images = format_prompt(turn)
|
|
437
450
|
image_tokens = images.empty? ? 0 : ImagePlan.estimated_tokens(turn.conversation)
|
|
438
|
-
window = ContextWindow.resolve(client: @client, model: turn.model_name, setting:
|
|
451
|
+
window = ContextWindow.resolve(client: @client, model: turn.model_name, setting: turn_settings.window_setting)
|
|
439
452
|
emit_context_status_event(turn.on_stream_event, turn.context, prompt, iteration_index: turn.iteration - 1,
|
|
440
453
|
window: window, image_tokens: image_tokens)
|
|
441
454
|
if (line = turn.context.take_guidance)
|
|
@@ -450,7 +463,7 @@ module Samagotchi
|
|
|
450
463
|
|
|
451
464
|
def format_prompt(turn)
|
|
452
465
|
Prompt.format_with_images(llm_context_view.messages(turn.conversation), profile: @profile,
|
|
453
|
-
vision:
|
|
466
|
+
vision: turn_settings.vision, prefill: turn.prefill)
|
|
454
467
|
end
|
|
455
468
|
|
|
456
469
|
# One streamed request, under the generation's own controller: a
|
|
@@ -460,8 +473,8 @@ module Samagotchi
|
|
|
460
473
|
def generate(turn, request)
|
|
461
474
|
# Before the generation starts: a hook that waits (a question) isn't
|
|
462
475
|
# 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
|
|
476
|
+
guard_generation(@context)
|
|
477
|
+
fire_hook(:before_generation, { type: :before_generation, iteration: turn.iteration }) if hooks
|
|
465
478
|
raise Client::RequestCancelled, turn.cancel_controller.reason if turn.cancel_controller&.cancelled?
|
|
466
479
|
|
|
467
480
|
# Each ask splits its own stream (run_generation rebuilds the
|
|
@@ -469,7 +482,7 @@ module Samagotchi
|
|
|
469
482
|
# leave the next one "inside" it.
|
|
470
483
|
emit(turn, type: :generation_started, iteration: turn.iteration, context_window_tokens: request.window.tokens,
|
|
471
484
|
context_window_source: request.window.source, profile: @profile.name, profile_source: @profile_source,
|
|
472
|
-
price:
|
|
485
|
+
price: turn_settings.price&.to_h)
|
|
473
486
|
# This generation's own server counts (the run-long ones in the
|
|
474
487
|
# context tracker can be an earlier one's), and the thinking it
|
|
475
488
|
# streamed, for the log (a stuck thinking generation shows as
|
|
@@ -505,7 +518,7 @@ module Samagotchi
|
|
|
505
518
|
# The empty-retry sampling is taken once, before the ask-again loop:
|
|
506
519
|
# a drop's restart must still carry the nudge's 0.6 (as ChatLoop does
|
|
507
520
|
# per step), not lose it when request_sampling is asked again.
|
|
508
|
-
sampling = turn.empty_retry.request_sampling(
|
|
521
|
+
sampling = turn.empty_retry.request_sampling(turn_settings.sampling)
|
|
509
522
|
count = LLM::StreamDrops::DropCount.new(0, 0)
|
|
510
523
|
begin
|
|
511
524
|
# Each ask splits its own stream: a drop mid tool call (or mid
|
|
@@ -639,7 +652,7 @@ module Samagotchi
|
|
|
639
652
|
def cache_fields(cache) = cache ? TokenUsage.cache_fields(**cache) : {}
|
|
640
653
|
|
|
641
654
|
def reprefill_field(model, cache)
|
|
642
|
-
tokens = cache &&
|
|
655
|
+
tokens = cache && @reprefill.tokens(model, client: @client, prompt: cache[:prompt], cached: cache[:cached])
|
|
643
656
|
tokens ? { reprefill_tokens: tokens } : {}
|
|
644
657
|
end
|
|
645
658
|
|
|
@@ -666,7 +679,7 @@ module Samagotchi
|
|
|
666
679
|
dump_log("response", generation.response, iteration: turn.iteration)
|
|
667
680
|
# The after_generation hook (after the model returns, before the tool
|
|
668
681
|
# parse), with a read-only copy of the conversation as sent.
|
|
669
|
-
return unless
|
|
682
|
+
return unless hooks
|
|
670
683
|
|
|
671
684
|
fire_hook(:after_generation, { type: :after_generation, iteration: turn.iteration, response: generation.response,
|
|
672
685
|
messages: AnswerDisplay.strip_all(turn.conversation).map(&:dup).freeze })
|
|
@@ -780,7 +793,7 @@ module Samagotchi
|
|
|
780
793
|
turn.qwen_partial = nil
|
|
781
794
|
runs = []
|
|
782
795
|
begin
|
|
783
|
-
ToolResponse.run_batch(tool_runner, calls, iteration: turn.iteration, emit: turn.emit,
|
|
796
|
+
ToolResponse.run_batch(tool_runner, calls, context: @context, iteration: turn.iteration, emit: turn.emit,
|
|
784
797
|
on_stream_event: turn.on_stream_event, cap: turn.cap) { |run, _| runs << run }
|
|
785
798
|
rescue Interrupt
|
|
786
799
|
# A Ctrl-C mid-batch: the entry still answers every call (the
|
|
@@ -848,29 +861,21 @@ module Samagotchi
|
|
|
848
861
|
end
|
|
849
862
|
|
|
850
863
|
def fire_hook(name, event)
|
|
851
|
-
return unless
|
|
864
|
+
return unless hooks
|
|
852
865
|
|
|
853
866
|
# A failing hook must not break the turn.
|
|
854
|
-
Degrade.call(:hooks, "hook_fire_failed", hook: name) {
|
|
867
|
+
Degrade.call(:hooks, "hook_fire_failed", hook: name) { hooks.fire(name, event) }
|
|
855
868
|
end
|
|
856
869
|
|
|
857
870
|
# Coerce a value to boolean — handles true/false, nil, and string "true"/"false".
|
|
858
871
|
def truthy?(val) = Tools::Builtins.truthy?(val)
|
|
859
872
|
|
|
860
|
-
def tool_context = @tool_context ||= ToolContext.new(self)
|
|
861
|
-
|
|
862
|
-
# ── Output char cap resolution ─────────────────────────────────────────────
|
|
863
|
-
|
|
864
|
-
def resolve_output_char_cap(override)
|
|
865
|
-
self.class.resolve_output_char_cap(override)
|
|
866
|
-
end
|
|
867
|
-
|
|
868
873
|
# One request, cancelled by +generation_controller+ (the turn's child)
|
|
869
874
|
# when there is one.
|
|
870
875
|
def request_generation(prompt, generation_controller:, cancel_controller:, **)
|
|
871
876
|
kwargs = complete_kwargs(cancel_controller: generation_controller || cancel_controller, **)
|
|
872
877
|
# Behind a turn-end warm-up still running on its slot (PromptWarmup).
|
|
873
|
-
slot = @warmup&.take_pin(@client)
|
|
878
|
+
slot = @services.warmup&.take_pin(@client)
|
|
874
879
|
kwargs[:slot] = slot if slot
|
|
875
880
|
@client.complete(prompt, **kwargs)
|
|
876
881
|
end
|
|
@@ -940,10 +945,10 @@ module Samagotchi
|
|
|
940
945
|
|
|
941
946
|
# A payload dump (model response, tool call/result, context status): the
|
|
942
947
|
# debug level only, tagged with the model it came from.
|
|
943
|
-
def dump_log(event, payload, **fields)
|
|
948
|
+
def dump_log(event, payload, model: turn_settings.model_name, **fields)
|
|
944
949
|
return unless Log.level?(:debug)
|
|
945
950
|
|
|
946
|
-
Log.debug(:model, event, payload: payload, model:
|
|
951
|
+
Log.debug(:model, event, payload: payload, model: model, **fields)
|
|
947
952
|
end
|
|
948
953
|
|
|
949
954
|
# Estimate context usage for this iteration's prompt (ContextStatus#observe)
|
|
@@ -971,12 +976,14 @@ module Samagotchi
|
|
|
971
976
|
# for the model the next turn runs on, after a /model switch too),
|
|
972
977
|
# else the last turn's. Returns [prompt, images], or nil when the cut
|
|
973
978
|
# isn't found.
|
|
974
|
-
|
|
979
|
+
# +vision+: the next turn's VisionContext (the last turn's settings'),
|
|
980
|
+
# else the last run's.
|
|
981
|
+
def warmup_prompt(messages, llm_context: turn_settings&.llm_context, vision: turn_settings.vision)
|
|
975
982
|
conversation = prepare_conversation(messages)
|
|
976
983
|
apply_llm_context!(conversation, llm_context, warmup: true)
|
|
977
984
|
conversation << { role: "user", content: WARMUP_CUT }
|
|
978
985
|
prompt, images = Prompt.format_with_images(llm_context_view(llm_context).messages(conversation),
|
|
979
|
-
profile: @profile, vision:
|
|
986
|
+
profile: @profile, vision: vision)
|
|
980
987
|
head = prompt[0, prompt.index(WARMUP_CUT) || 0]
|
|
981
988
|
cut = head.rindex(user_opener)
|
|
982
989
|
cut&.positive? ? [head[0, cut], images] : nil
|
|
@@ -985,119 +992,52 @@ module Samagotchi
|
|
|
985
992
|
WARMUP_CUT = "chi-warmup-cut"
|
|
986
993
|
private_constant :WARMUP_CUT
|
|
987
994
|
|
|
988
|
-
# What
|
|
989
|
-
# turn's strategy
|
|
990
|
-
#
|
|
991
|
-
|
|
992
|
-
# the prompt the next turn sends.
|
|
993
|
-
def llm_context_view(llm_context = @turn_settings&.llm_context)
|
|
994
|
-
LLMContextView.new(strategy: llm_context&.strategy || LLMContextView::NONE)
|
|
995
|
-
end
|
|
996
|
-
|
|
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 || []
|
|
995
|
+
# What the native prompt and the warm-up send of the conversation
|
|
996
|
+
# (LLMContextView.for), under the turn's strategy or the one given (the
|
|
997
|
+
# warm-up's, the next turn's).
|
|
998
|
+
def llm_context_view(llm_context = turn_settings&.llm_context) = LLMContextView.for(llm_context)
|
|
1000
999
|
|
|
1001
1000
|
# The model's forget_outputs call (LLMContextForget) on the running
|
|
1002
|
-
# turn, under
|
|
1003
|
-
#
|
|
1004
|
-
#
|
|
1001
|
+
# turn (the loop's llm_context_turn), under +context+'s strategy (the
|
|
1002
|
+
# call's TurnContext); the apply rule runs on the turn's conversation at
|
|
1003
|
+
# once, as at a request, so the result says whether the stubs go with
|
|
1004
|
+
# the next request or wait for the turn's end.
|
|
1005
1005
|
# @return [String] the tool result
|
|
1006
|
-
def forget_outputs(call)
|
|
1007
|
-
llm_context =
|
|
1006
|
+
def forget_outputs(call, context)
|
|
1007
|
+
llm_context = context.settings&.llm_context
|
|
1008
1008
|
turn = @llm_context_turn
|
|
1009
1009
|
unless turn && llm_context&.active_layers&.include?(:forget)
|
|
1010
1010
|
return "Error: forget_outputs is off (the llm_context strategy has no forget layer)"
|
|
1011
1011
|
end
|
|
1012
1012
|
|
|
1013
|
-
apply =
|
|
1014
|
-
|
|
1013
|
+
apply = lambda {
|
|
1014
|
+
apply_llm_context!(turn.conversation, llm_context, context: turn.context, emit: turn.emit,
|
|
1015
|
+
model: context.settings.model_name)
|
|
1016
|
+
}
|
|
1017
|
+
LLMContextForget.call(turn, call, llm_context: llm_context, apply: apply,
|
|
1018
|
+
pinned: ->(conversation) { pinned_ids(conversation, context) })
|
|
1015
1019
|
end
|
|
1016
1020
|
|
|
1017
|
-
# The ids of +conversation+'s outputs the session's pins hold
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
WorkingSet.pinned_ids(conversation, pins:
|
|
1021
|
+
# The ids of +conversation+'s outputs the session's pins hold (the
|
|
1022
|
+
# working-set pins of +context+'s session, read now).
|
|
1023
|
+
def pinned_ids(conversation, context)
|
|
1024
|
+
WorkingSet.pinned_ids(conversation, pins: context.working_set_pins, cwd: context.cwd)
|
|
1021
1025
|
end
|
|
1022
1026
|
private :pinned_ids
|
|
1023
1027
|
|
|
1024
|
-
#
|
|
1025
|
-
#
|
|
1026
|
-
#
|
|
1027
|
-
# stage their edits on +conversation+ and the apply rule
|
|
1028
|
-
# (LLMContextApply, llm_context.apply) saves the batch it lets through
|
|
1029
|
-
# as applied, so the request about to be sent (or the next turn's
|
|
1030
|
-
# first, and the warm-up) is the first with the stubs. +context+: the
|
|
1031
|
-
# turn's ContextStatus (payoff applies in its top bucket; an applied
|
|
1032
|
-
# batch rebases its estimate, ContextStatus#edited!). none changes
|
|
1033
|
-
# nothing. Logged as llm_context_apply when a batch is applied, and
|
|
1034
|
-
# shown to the user as a ✂ row (+emit+ takes the :llm_context_edited
|
|
1035
|
-
# event, LLMContextNotice; the warm-up's own copy has none).
|
|
1028
|
+
# The strategy's edits on +conversation+ (LLMContextApply.apply!), under
|
|
1029
|
+
# the turn's strategy and model name by default: the native run's
|
|
1030
|
+
# requests and turn end, the warm-up's copy, a forget_outputs call.
|
|
1036
1031
|
# @return [LLMContextApply::Outcome]
|
|
1037
|
-
def apply_llm_context!(conversation, llm_context =
|
|
1038
|
-
|
|
1039
|
-
view = llm_context_view(llm_context)
|
|
1040
|
-
return LLMContextApply::Outcome.none if view.none?
|
|
1041
|
-
|
|
1042
|
-
outcome = LLMContextApply.run!(conversation, layers: view.layers, rule: llm_context.apply, moment: moment,
|
|
1043
|
-
protect_steps: llm_context.protect_steps,
|
|
1044
|
-
top_bucket: context&.top_bucket? || false,
|
|
1045
|
-
changes: llm_context.stale_edits)
|
|
1046
|
-
if outcome.applied?
|
|
1047
|
-
# The prompt just shrank: the context estimate counts from it.
|
|
1048
|
-
context&.edited!
|
|
1049
|
-
unless warmup
|
|
1050
|
-
log_llm_context(outcome, llm_context, moment)
|
|
1051
|
-
emit_llm_context(emit, conversation, outcome, moment) if emit
|
|
1052
|
-
end
|
|
1053
|
-
end
|
|
1054
|
-
outcome
|
|
1055
|
-
end
|
|
1056
|
-
|
|
1057
|
-
def log_llm_context(outcome, llm_context, moment)
|
|
1058
|
-
Log.info(:model, "llm_context_apply", rule: llm_context.apply, at: moment, why: outcome.why,
|
|
1059
|
-
applied: outcome.applied.size, ids: outcome.applied.map(&:id).join(","),
|
|
1060
|
-
staged: outcome.staged, freed: (outcome.freed_chars / TokenUsage::CHARS_PER_TOKEN).ceil,
|
|
1061
|
-
tail: (outcome.tail_chars / TokenUsage::CHARS_PER_TOKEN).ceil,
|
|
1062
|
-
model: @turn_settings&.model_name)
|
|
1063
|
-
end
|
|
1064
|
-
private :log_llm_context
|
|
1065
|
-
|
|
1066
|
-
# The batch's ✂ row; a row that can't be built never breaks the turn.
|
|
1067
|
-
def emit_llm_context(emit, conversation, outcome, moment)
|
|
1068
|
-
emit.call(LLMContextNotice.event(conversation, outcome, moment: moment))
|
|
1069
|
-
rescue StandardError => e
|
|
1070
|
-
Log.warn(:model, "llm_context_notice_failed", error: e.class.name, message: e.message)
|
|
1071
|
-
end
|
|
1072
|
-
private :emit_llm_context
|
|
1073
|
-
|
|
1074
|
-
# A request's re-prefilled tokens, for both loops: of the prompt the
|
|
1075
|
-
# last request on +model+ through this client (the host) sent (this
|
|
1076
|
-
# kernel's, any turn), what the server didn't reuse from its cache this
|
|
1077
|
-
# time (that prompt less +cached+), so an LLM context edit's cache break
|
|
1078
|
-
# shows, and so does anything else that rewrote the prompt's middle or
|
|
1079
|
-
# lost the cache. nil under TokenUsage::REPREFILL_MIN_TOKENS (a cache
|
|
1080
|
-
# block's rounding), for a server that reports no cached count (or 0: a
|
|
1081
|
-
# cold cache isn't told from no cache), the first request, a model
|
|
1082
|
-
# switch (another bare model, or the same one on another host: the
|
|
1083
|
-
# Engine points #client at the new host's; SessionMetrics counts that
|
|
1084
|
-
# prefill as the switch's), and the first request after a warm-up
|
|
1085
|
-
# (#warmed_up!).
|
|
1086
|
-
# @return [Integer, nil]
|
|
1087
|
-
def reprefilled_tokens(model, prompt:, cached:)
|
|
1088
|
-
previous = @reprefill_baseline
|
|
1089
|
-
@reprefill_baseline = { model: model, client: @client, prompt: prompt.to_i } if prompt.to_i.positive?
|
|
1090
|
-
return nil unless previous && previous[:model] == model && previous[:client].equal?(@client) && cached.to_i.positive?
|
|
1091
|
-
|
|
1092
|
-
tokens = previous[:prompt] - cached.to_i
|
|
1093
|
-
tokens >= TokenUsage::REPREFILL_MIN_TOKENS ? tokens : nil
|
|
1032
|
+
def apply_llm_context!(conversation, llm_context = turn_settings&.llm_context, model: turn_settings&.model_name, **)
|
|
1033
|
+
LLMContextApply.apply!(conversation, llm_context, model: model, **)
|
|
1094
1034
|
end
|
|
1095
1035
|
|
|
1096
1036
|
# The Engine started a turn-end warm-up: it prefills the next prompt
|
|
1097
1037
|
# off the clock, so the next request's cache counts are against it, not
|
|
1098
|
-
# the last request (
|
|
1038
|
+
# the last request (the re-prefill meter starts over, ReprefillMeter#reset!).
|
|
1099
1039
|
def warmed_up!
|
|
1100
|
-
@
|
|
1040
|
+
@reprefill.reset!
|
|
1101
1041
|
end
|
|
1102
1042
|
|
|
1103
1043
|
# Public wrapper so other loops (e.g. the chat loop) can strip
|
|
@@ -1188,78 +1128,40 @@ module Samagotchi
|
|
|
1188
1128
|
# unknown-tool handling, and activity events are shared, not duplicated. Callers
|
|
1189
1129
|
# are responsible for normalizing the provider's native call into this shape
|
|
1190
1130
|
# first (see Samagotchi::LLM::NativeToolNormalizer); dispatch itself never
|
|
1191
|
-
# parses provider text.
|
|
1192
|
-
|
|
1193
|
-
|
|
1131
|
+
# parses provider text. +context+: the call's turn (TurnContext: its
|
|
1132
|
+
# ToolSurface, the handler's ToolContext); a bare kernel's own
|
|
1133
|
+
# (#own_context) by default.
|
|
1134
|
+
def dispatch_tool_call(call, context = own_context)
|
|
1135
|
+
dispatch(call, context)
|
|
1194
1136
|
end
|
|
1195
1137
|
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
#
|
|
1199
|
-
#
|
|
1200
|
-
#
|
|
1201
|
-
|
|
1202
|
-
SHELL_LIKE_TOOL_NAMES = %w[
|
|
1203
|
-
bash sh zsh shell terminal cmd command run run_command run_shell exec execute_command
|
|
1204
|
-
].freeze
|
|
1205
|
-
|
|
1206
|
-
# The error an unknown tool name gets back. A shell-like name with
|
|
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
|
|
1138
|
+
# The context a bare kernel (no Engine: a spec, a bench) runs in when
|
|
1139
|
+
# given none: #context, with a Tools::ToolSurface built now from the
|
|
1140
|
+
# kernel's tools when it has none (Tools::ToolSurface.bare). An
|
|
1141
|
+
# Engine's turn always hands its own down (Engine#generate's surface).
|
|
1142
|
+
# @return [TurnContext]
|
|
1143
|
+
def own_context = Tools::ToolSurface.bare(@context, tools)
|
|
1224
1144
|
|
|
1225
|
-
|
|
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
|
|
1145
|
+
private
|
|
1243
1146
|
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
end
|
|
1147
|
+
# The call runs on the entry the turn's surface gives it
|
|
1148
|
+
# (Tools::ToolSurface#entry_for: a call_tool-reached one only while the
|
|
1149
|
+
# toolkit reaches it that way, a reachable one by its name on a host
|
|
1150
|
+
# that loads tools); without one it gets ToolSurface#unknown_tool_error.
|
|
1151
|
+
# A context without a surface raises Tools::MissingSurface.
|
|
1152
|
+
def dispatch(call, context)
|
|
1153
|
+
model = context.settings&.model_name
|
|
1154
|
+
surface = Tools::ToolSurface.of(context)
|
|
1155
|
+
entry = surface.entry_for(call)
|
|
1254
1156
|
unless entry
|
|
1255
|
-
result = unknown_tool_error(call[:name],
|
|
1157
|
+
result = surface.unknown_tool_error(call[:name], via: call[:via])
|
|
1256
1158
|
return {
|
|
1257
1159
|
output: result,
|
|
1258
|
-
activity: ToolActivity.tool_activity_event(call[:name], call, result, registry:
|
|
1160
|
+
activity: ToolActivity.tool_activity_event(call[:name], call, result, registry: tools, surface: surface)
|
|
1259
1161
|
}
|
|
1260
1162
|
end
|
|
1261
1163
|
|
|
1262
|
-
dump_log("tool_call", call[:content], tool: call[:name], path: call[:path], scope: call[:scope])
|
|
1164
|
+
dump_log("tool_call", call[:content], model: model, tool: call[:name], path: call[:path], scope: call[:scope])
|
|
1263
1165
|
|
|
1264
1166
|
if call[:called_as]
|
|
1265
1167
|
# The model's spelling for a tool an alias ran (bash → execute); its
|
|
@@ -1268,12 +1170,12 @@ module Samagotchi
|
|
|
1268
1170
|
Log.info(:turn, "tool_alias", name: call[:called_as], as: call[:name])
|
|
1269
1171
|
end
|
|
1270
1172
|
|
|
1271
|
-
result = entry.handler.call(call,
|
|
1173
|
+
result = entry.handler.call(call, ToolContext.new(self, context))
|
|
1272
1174
|
|
|
1273
|
-
dump_log("tool_result", result, tool: call[:name])
|
|
1175
|
+
dump_log("tool_result", result, model: model, tool: call[:name])
|
|
1274
1176
|
dispatched = {
|
|
1275
1177
|
output: "[#{call[:name]}]\n#{result}",
|
|
1276
|
-
activity: ToolActivity.tool_activity_event(call[:name], call, result, registry:
|
|
1178
|
+
activity: ToolActivity.tool_activity_event(call[:name], call, result, registry: tools, surface: surface)
|
|
1277
1179
|
}
|
|
1278
1180
|
# Images the tool returned (read's ImageResult, a plugin's ToolResult):
|
|
1279
1181
|
# ToolRunner attaches them (or says why not).
|
|
@@ -1292,12 +1194,12 @@ module Samagotchi
|
|
|
1292
1194
|
end
|
|
1293
1195
|
dispatched
|
|
1294
1196
|
rescue StandardError => e
|
|
1295
|
-
dump_log("tool_error", e.message, tool: call[:name], error: e.class.name)
|
|
1197
|
+
dump_log("tool_error", e.message, model: context.settings&.model_name, tool: call[:name], error: e.class.name)
|
|
1296
1198
|
result = "Error: #{e.message}"
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
}
|
|
1199
|
+
# No surface (Tools::MissingSurface from ToolSurface.of): no row either,
|
|
1200
|
+
# it would read the surface again.
|
|
1201
|
+
activity = surface && ToolActivity.tool_activity_event(call[:name], call, result, registry: tools, surface: surface)
|
|
1202
|
+
{ output: "[#{call[:name]}] #{result}", activity: activity }
|
|
1301
1203
|
end
|
|
1302
1204
|
end
|
|
1303
1205
|
end
|