samagotchi 0.154.0 → 0.155.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -1
- data/docs/architecture.md +3 -2
- data/docs/bundles.md +21 -0
- data/docs/cli.md +10 -8
- data/docs/guardrails.md +2 -2
- data/docs/hooks.md +1 -1
- data/docs/internals/anthropic.md +4 -4
- data/docs/internals/attached-context.md +8 -6
- data/docs/internals/block-renderers.md +8 -1
- data/docs/internals/bridge.md +18 -5
- data/docs/internals/engine.md +221 -100
- data/docs/internals/inline-widgets.md +136 -0
- data/docs/internals/objectives.md +8 -6
- data/docs/internals/participants.md +5 -5
- data/docs/internals/prompt-caching.md +12 -6
- data/docs/internals/web-frontend.md +2 -2
- data/docs/internals/working-set.md +2 -1
- data/docs/plugins.md +2 -0
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +3 -3
- data/docs/testing.md +16 -1
- data/lib/samagotchi/answer_text.rb +1 -2
- data/lib/samagotchi/block_renderers.rb +5 -0
- data/lib/samagotchi/bridge/card_places.rb +56 -7
- data/lib/samagotchi/bridge/card_store.rb +24 -9
- data/lib/samagotchi/bridge.rb +9 -0
- data/lib/samagotchi/bridge_client.rb +9 -5
- data/lib/samagotchi/bundle_command/check.rb +1 -1
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
- data/lib/samagotchi/bundles/btw/plugin.rb +1 -1
- data/lib/samagotchi/bundles/coordinator/manifest.yml +2 -2
- data/lib/samagotchi/bundles/coordinator/plugin.rb +3 -2
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/github-pr/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/plugin.rb +3 -3
- data/lib/samagotchi/bundles/inline-widgets/manifest.yml +11 -0
- data/lib/samagotchi/bundles/inline-widgets/plugin.rb +76 -0
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +3 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +1 -1
- data/lib/samagotchi/engine/context_judge.rb +19 -125
- data/lib/samagotchi/engine/guardrails.rb +4 -2
- data/lib/samagotchi/engine/messages.rb +9 -6
- data/lib/samagotchi/engine/objectives.rb +22 -90
- data/lib/samagotchi/engine/participants.rb +15 -10
- data/lib/samagotchi/engine/plugins.rb +3 -1
- data/lib/samagotchi/engine/questions.rb +1 -1
- data/lib/samagotchi/engine/side_requests.rb +5 -2
- data/lib/samagotchi/engine/toolkit.rb +33 -190
- data/lib/samagotchi/engine/turn_end.rb +1 -1
- data/lib/samagotchi/engine/turn_helpers.rb +16 -39
- data/lib/samagotchi/engine/verify.rb +7 -143
- data/lib/samagotchi/engine.rb +162 -99
- data/lib/samagotchi/engine_peers.rb +1 -1
- data/lib/samagotchi/engine_snapshots.rb +9 -8
- data/lib/samagotchi/event_fanout.rb +18 -12
- data/lib/samagotchi/guardrail_wiring.rb +83 -82
- data/lib/samagotchi/guardrails/gate.rb +41 -40
- data/lib/samagotchi/guardrails/pair_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/review_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/targets.rb +6 -4
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/idle_recap.rb +3 -1
- data/lib/samagotchi/inline_widgets.rb +325 -0
- data/lib/samagotchi/kernel_loop.rb +194 -216
- data/lib/samagotchi/llm/backend.rb +3 -1
- data/lib/samagotchi/llm/chat_loop.rb +66 -42
- data/lib/samagotchi/llm/native_backend.rb +3 -2
- data/lib/samagotchi/llm_context_forget.rb +5 -3
- data/lib/samagotchi/model_binding.rb +3 -3
- data/lib/samagotchi/plugin/prompt_sections.rb +7 -0
- data/lib/samagotchi/question_desk.rb +1 -1
- data/lib/samagotchi/revise_anchor.rb +1 -1
- data/lib/samagotchi/session_manager.rb +8 -13
- data/lib/samagotchi/session_retention.rb +24 -4
- data/lib/samagotchi/steer.rb +12 -1
- data/lib/samagotchi/stream_event.rb +22 -0
- data/lib/samagotchi/system_prompt.rb +84 -61
- data/lib/samagotchi/terminal_ui/attached_view.rb +0 -9
- data/lib/samagotchi/terminal_ui/input_support.rb +16 -15
- data/lib/samagotchi/terminal_ui/join_replay.rb +16 -12
- data/lib/samagotchi/terminal_ui/reline_seam.rb +0 -10
- data/lib/samagotchi/terminal_ui/status_row.rb +0 -7
- data/lib/samagotchi/testing/boundary.rb +7 -0
- data/lib/samagotchi/testing/bundle.rb +7 -5
- data/lib/samagotchi/testing/install.rb +3 -2
- data/lib/samagotchi/tool_activity.rb +8 -6
- data/lib/samagotchi/tool_declarations.rb +4 -4
- data/lib/samagotchi/tool_response.rb +4 -3
- data/lib/samagotchi/tool_runner.rb +93 -101
- data/lib/samagotchi/toolkit_binding.rb +40 -14
- data/lib/samagotchi/tools/deferred_tools.rb +17 -21
- data/lib/samagotchi/tools/registry.rb +43 -40
- data/lib/samagotchi/tools/tool_index.rb +135 -88
- data/lib/samagotchi/tools/tool_surface.rb +173 -0
- data/lib/samagotchi/turn_context.rb +67 -0
- data/lib/samagotchi/turn_flow.rb +7 -2
- data/lib/samagotchi/turn_stages/context_judge.rb +170 -0
- data/lib/samagotchi/turn_stages/objectives.rb +146 -0
- data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +261 -0
- data/lib/samagotchi/turn_stages/verify_anchors.rb +169 -0
- data/lib/samagotchi/turn_stages/warmup.rb +83 -0
- data/lib/samagotchi/turn_state.rb +46 -4
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +3 -1
- data/lib/samagotchi/web/markdown_renderer.rb +39 -14
- data/lib/samagotchi/web/message_display.rb +8 -1
- data/lib/samagotchi/web/public/app.js +5 -1
- data/lib/samagotchi/web/public/block_host.js +74 -199
- data/lib/samagotchi/web/public/block_renderers.css +2 -0
- data/lib/samagotchi/web/public/block_source.js +7 -4
- data/lib/samagotchi/web/public/block_watch.js +256 -0
- data/lib/samagotchi/web/public/card_stamps.js +27 -0
- data/lib/samagotchi/web/public/cards_place.js +5 -3
- data/lib/samagotchi/web/public/composer_fold.js +3 -0
- data/lib/samagotchi/web/public/composer_mode.js +1 -1
- data/lib/samagotchi/web/public/format.js +3 -2
- data/lib/samagotchi/web/public/index.html +28 -9
- data/lib/samagotchi/web/public/inline_widgets.js +146 -0
- data/lib/samagotchi/web/public/owner_verify.js +1 -1
- data/lib/samagotchi/web/public/participant.js +1 -1
- data/lib/samagotchi/web/public/revise_stack.js +1 -1
- data/lib/samagotchi/web/public/stage_watch.js +6 -2
- data/lib/samagotchi/web/public/text_roots.js +1 -1
- data/lib/samagotchi/web/public/turn_html.js +8 -4
- data/lib/samagotchi/web/public/verify_target.js +1 -1
- data/lib/samagotchi/web/public/widget_picks.js +130 -0
- data/lib/samagotchi/web/public/widget_walk.js +230 -0
- data/lib/samagotchi/web/public/widgets.css +70 -0
- data/lib/samagotchi/web/session_access_routes.rb +8 -0
- data/lib/samagotchi/web/session_show.rb +4 -0
- data/lib/samagotchi/worker.rb +8 -5
- data/lib/samagotchi.rb +0 -1
- metadata +19 -2
- data/lib/samagotchi/pending_input_queue.rb +0 -55
|
@@ -19,9 +19,11 @@ module Samagotchi
|
|
|
19
19
|
# in +messages+ (Engine#run_turn resume_from:); the replies from there on
|
|
20
20
|
# keep the model's reasoning (thought blocks), as within one run. nil:
|
|
21
21
|
# the run starts here, as every other turn's does.
|
|
22
|
+
# +context+: the run's turn (TurnContext, Engine#generate's), handed down
|
|
23
|
+
# the tool path explicitly; nil: the backend's own (a bare kernel's).
|
|
22
24
|
class ModelBackend
|
|
23
25
|
def complete(messages:, max_iterations: IterationLimit::DEFAULT, on_stream_event: nil, cancel_controller: nil,
|
|
24
|
-
model_name: nil, max_tool_output_chars: nil, pending_input: nil, reasoning_since: nil)
|
|
26
|
+
model_name: nil, max_tool_output_chars: nil, pending_input: nil, reasoning_since: nil, context: nil)
|
|
25
27
|
raise NotImplementedError, "#{self.class}#complete must be implemented"
|
|
26
28
|
end
|
|
27
29
|
end
|
|
@@ -24,7 +24,7 @@ require_relative "../tool_runner"
|
|
|
24
24
|
require_relative "../tool_response"
|
|
25
25
|
require_relative "../tool_ids"
|
|
26
26
|
require_relative "../tool_declarations"
|
|
27
|
-
require_relative "../tools/
|
|
27
|
+
require_relative "../tools/tool_surface"
|
|
28
28
|
require_relative "../tool_loads"
|
|
29
29
|
require_relative "../vision_context"
|
|
30
30
|
require_relative "../log"
|
|
@@ -79,8 +79,11 @@ module Samagotchi
|
|
|
79
79
|
# prompt's part every session shares (SystemPrompt#stable_length), or nil
|
|
80
80
|
# @param sleeper [#call, nil] waits the given seconds before a dropped
|
|
81
81
|
# stream's step is asked again (specs pass one that doesn't sleep)
|
|
82
|
-
|
|
82
|
+
# @param context [TurnContext, nil] the turn its helpers read before any
|
|
83
|
+
# #complete (a bench building wire messages); a run's replaces it
|
|
84
|
+
def initialize(kernel:, adapter: nil, stable_length: nil, sleeper: nil, context: nil)
|
|
83
85
|
@kernel = kernel
|
|
86
|
+
@turn_context = context
|
|
84
87
|
@adapter = adapter
|
|
85
88
|
@stable_length = stable_length
|
|
86
89
|
@sleeper = sleeper || ->(seconds) { sleep(seconds) }
|
|
@@ -93,10 +96,14 @@ module Samagotchi
|
|
|
93
96
|
|
|
94
97
|
# +reasoning_since+ (ModelBackend#complete): the resumed run's start,
|
|
95
98
|
# the Run's own start then (Run#initialize, #reasoning_boundary).
|
|
99
|
+
# +context+: the run's turn (TurnContext, Engine#generate's): its
|
|
100
|
+
# settings are what the requests send, its tool calls run in it; a
|
|
101
|
+
# bare loop's (#turn_context) when none is given.
|
|
96
102
|
def complete(messages:, max_iterations: IterationLimit::DEFAULT, on_stream_event: nil, cancel_controller: nil,
|
|
97
|
-
model_name: nil, max_tool_output_chars: nil, pending_input: nil, reasoning_since: nil)
|
|
103
|
+
model_name: nil, max_tool_output_chars: nil, pending_input: nil, reasoning_since: nil, context: nil)
|
|
98
104
|
raise ArgumentError, "ChatLoop has no adapter" unless @adapter
|
|
99
105
|
|
|
106
|
+
@turn_context = context if context
|
|
100
107
|
conversation = Array(messages).map(&:dup)
|
|
101
108
|
Run.new(self, conversation, on_stream_event, cancel_controller, model_name, pending_input, start: reasoning_since)
|
|
102
109
|
.call(max_iterations: max_iterations || 1, cap: KernelLoop.resolve_output_char_cap(max_tool_output_chars))
|
|
@@ -108,14 +115,14 @@ module Samagotchi
|
|
|
108
115
|
end
|
|
109
116
|
|
|
110
117
|
# Tool definitions for the request: the schemas the native prompts are
|
|
111
|
-
# rendered from (the
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
# follows, deferred (
|
|
118
|
+
# rendered from (the turn's ToolSurface: what it declares), with the
|
|
119
|
+
# chat-only enums and closed parameters. On an adapter that loads
|
|
120
|
+
# tools for +model+ (#loads_tools?, asked per request: a refusal turns
|
|
121
|
+
# it off mid-turn), every tool the session reaches but doesn't declare
|
|
122
|
+
# follows, deferred (ToolSurface#deferred_for: +conversation+, the
|
|
116
123
|
# request's, names the ones it loaded); never without a declared one.
|
|
117
124
|
def tool_definitions(conversation = nil, model: nil)
|
|
118
|
-
declared = ToolDeclarations.chat_schemas(
|
|
125
|
+
declared = ToolDeclarations.chat_schemas(surface.schemas).map do |schema|
|
|
119
126
|
{ type: "function", function: schema.slice(:name, :description, :parameters) }
|
|
120
127
|
end
|
|
121
128
|
return declared if declared.empty? || !loads_tools?(model)
|
|
@@ -127,10 +134,13 @@ module Samagotchi
|
|
|
127
134
|
end
|
|
128
135
|
|
|
129
136
|
# The tools a request on a host that loads tools declares deferred
|
|
130
|
-
# (
|
|
131
|
-
def deferred_tools(conversation)
|
|
132
|
-
|
|
133
|
-
|
|
137
|
+
# (ToolSurface#deferred_for).
|
|
138
|
+
def deferred_tools(conversation) = surface.deferred_for(conversation)
|
|
139
|
+
|
|
140
|
+
# The turn's ToolSurface (#turn_context's), else one computed now from
|
|
141
|
+
# the kernel's tools (a bare loop).
|
|
142
|
+
# @return [Tools::ToolSurface]
|
|
143
|
+
def surface = Tools::ToolSurface.of(turn_context, tools)
|
|
134
144
|
|
|
135
145
|
# Whether the adapter takes tools it doesn't declare up front for
|
|
136
146
|
# +model+ (AnthropicMessages#loads_tools?); OpenAIChat doesn't.
|
|
@@ -147,22 +157,33 @@ module Samagotchi
|
|
|
147
157
|
# The kernel's tools.
|
|
148
158
|
def tools = @kernel.tools
|
|
149
159
|
|
|
150
|
-
# The
|
|
151
|
-
|
|
160
|
+
# The TurnContext of the run in progress, else of the last run
|
|
161
|
+
# (#complete's context:); before any, the one it was built with, else a
|
|
162
|
+
# context of the kernel's own settings (a bare loop). Read on the turn's thread only.
|
|
163
|
+
# @return [TurnContext]
|
|
164
|
+
def turn_context = @turn_context || TurnContext.none(settings: @kernel.turn_settings)
|
|
165
|
+
|
|
166
|
+
# The turn's LLM::TurnSettings (#turn_context's).
|
|
167
|
+
def settings = turn_context.settings
|
|
168
|
+
|
|
169
|
+
# The turn's LLM context layers (TurnContext#llm_context_layers).
|
|
170
|
+
def llm_context_layers = turn_context.llm_context_layers
|
|
152
171
|
|
|
153
172
|
# The turn's LLM context strategy (LLMContextStrategy::Resolved), nil
|
|
154
173
|
# when none was resolved.
|
|
155
|
-
def llm_context =
|
|
174
|
+
def llm_context = settings&.llm_context
|
|
156
175
|
|
|
157
176
|
# KernelLoop#reprefilled_tokens, the kernel's request history.
|
|
158
177
|
def reprefilled_tokens(model, **) = @kernel.reprefilled_tokens(model, **)
|
|
159
178
|
|
|
160
|
-
# The kernel's LLMContextView
|
|
161
|
-
def llm_context_view = @kernel.llm_context_view
|
|
179
|
+
# The kernel's LLMContextView under the turn's strategy.
|
|
180
|
+
def llm_context_view = @kernel.llm_context_view(llm_context)
|
|
162
181
|
|
|
163
182
|
# The strategy's own edits before a request and at turn end
|
|
164
|
-
# (KernelLoop#apply_llm_context!).
|
|
165
|
-
def apply_llm_context!(conversation, **)
|
|
183
|
+
# (KernelLoop#apply_llm_context!), under the turn's strategy.
|
|
184
|
+
def apply_llm_context!(conversation, **)
|
|
185
|
+
@kernel.apply_llm_context!(conversation, llm_context, model: settings&.model_name, **)
|
|
186
|
+
end
|
|
166
187
|
|
|
167
188
|
# The running turn, for forget_outputs (KernelLoop#llm_context_turn).
|
|
168
189
|
def llm_context_turn=(turn)
|
|
@@ -267,7 +288,7 @@ module Samagotchi
|
|
|
267
288
|
TOOL_IMAGES_TEXT = "[images from tool results]"
|
|
268
289
|
|
|
269
290
|
# Whether this turn's requests carry tool changes (the turn's model).
|
|
270
|
-
def tool_change_wire? = tool_changes?(
|
|
291
|
+
def tool_change_wire? = tool_changes?(settings&.model_name)
|
|
271
292
|
|
|
272
293
|
# The first index whose model entries carry their reasoning (nil:
|
|
273
294
|
# none do) under +level+. Under turn: +since+ when given, where a
|
|
@@ -298,14 +319,14 @@ module Samagotchi
|
|
|
298
319
|
conversation.drop(boundary).sum { |entry| entry[:role].to_s == "model" && thought?(entry) ? entry[:thinking].length : 0 }
|
|
299
320
|
end
|
|
300
321
|
|
|
301
|
-
# The turn's ReasoningHistory level (the
|
|
322
|
+
# The turn's ReasoningHistory level (the turn's settings); drop
|
|
302
323
|
# without settings, and for a +model+ whose host refused it
|
|
303
324
|
# (#reasoning_history_refused!). An adapter that sends its blocks back
|
|
304
325
|
# (#replays_content?) is floored at turn: the running run's thinking
|
|
305
326
|
# blocks always go back (a tool loop with thinking needs the last
|
|
306
327
|
# assistant turn's), whatever the level says.
|
|
307
328
|
def reasoning_history(model = nil)
|
|
308
|
-
level = model && @refusals.refused?(:reasoning_history, model) ? :drop :
|
|
329
|
+
level = model && @refusals.refused?(:reasoning_history, model) ? :drop : settings&.reasoning_history
|
|
309
330
|
level ||= :drop
|
|
310
331
|
level == :drop && replays_content? ? :turn : level
|
|
311
332
|
end
|
|
@@ -320,18 +341,17 @@ module Samagotchi
|
|
|
320
341
|
@refusals.remember(:reasoning_history, model)
|
|
321
342
|
end
|
|
322
343
|
|
|
323
|
-
# The turn's VisionContext (
|
|
324
|
-
|
|
325
|
-
def vision = @kernel.turn_settings.vision
|
|
344
|
+
# The turn's VisionContext (its settings'), or nil.
|
|
345
|
+
def vision = settings.vision
|
|
326
346
|
|
|
327
|
-
# The turn's configured price (
|
|
328
|
-
def price =
|
|
347
|
+
# The turn's configured price (its settings'), or nil.
|
|
348
|
+
def price = settings&.price
|
|
329
349
|
|
|
330
|
-
# The turn's request parameters (
|
|
331
|
-
def sampling =
|
|
350
|
+
# The turn's request parameters (its settings').
|
|
351
|
+
def sampling = settings.sampling || {}
|
|
332
352
|
|
|
333
|
-
# The turn's thinking level (
|
|
334
|
-
def thinking =
|
|
353
|
+
# The turn's thinking level (its settings').
|
|
354
|
+
def thinking = settings.thinking || Thinking::DEFAULT
|
|
335
355
|
|
|
336
356
|
# The request fields the thinking level adds, as the adapter's wire
|
|
337
357
|
# takes them (OpenAIChat#thinking_fields: Thinking.chat_fields); none
|
|
@@ -375,24 +395,24 @@ module Samagotchi
|
|
|
375
395
|
def strip_model_thought(text) = @kernel.strip_model_thought(text)
|
|
376
396
|
|
|
377
397
|
def tool_runner
|
|
378
|
-
@tool_runner ||= ToolRunner.new(@kernel, window: -> { context_window(
|
|
398
|
+
@tool_runner ||= ToolRunner.new(@kernel, window: ->(run_settings) { context_window(run_settings&.model_name, run_settings) })
|
|
379
399
|
end
|
|
380
400
|
|
|
381
401
|
# The window for +model+: the kernel's client probes the server it
|
|
382
402
|
# points at (Engine keeps it on the effective model's host; a local
|
|
383
403
|
# llama.cpp answers /props), then the host's model list, the model's
|
|
384
404
|
# or host's window_tokens, config, env and the default. A remote
|
|
385
|
-
# provider has no /props to probe.
|
|
386
|
-
|
|
405
|
+
# provider has no /props to probe. +run_settings+: the LLM::TurnSettings
|
|
406
|
+
# whose window_setting counts (the turn's by default).
|
|
407
|
+
def context_window(model, run_settings = settings)
|
|
387
408
|
client = @adapter.remote? ? nil : @kernel.client
|
|
388
|
-
ContextWindow.resolve(client: client, model: model, adapter: @adapter,
|
|
389
|
-
setting: (@kernel.turn_settings&.window_setting if @kernel.respond_to?(:turn_settings)))
|
|
409
|
+
ContextWindow.resolve(client: client, model: model, adapter: @adapter, setting: run_settings&.window_setting)
|
|
390
410
|
rescue StandardError
|
|
391
411
|
nil
|
|
392
412
|
end
|
|
393
413
|
|
|
394
|
-
# The kernel's generation guard (KernelLoop#guard_generation).
|
|
395
|
-
def guard_generation = @kernel.respond_to?(:guard_generation) ? @kernel.guard_generation : nil
|
|
414
|
+
# The kernel's generation guard for the turn (KernelLoop#guard_generation).
|
|
415
|
+
def guard_generation = @kernel.respond_to?(:guard_generation) ? @kernel.guard_generation(turn_context) : nil
|
|
396
416
|
|
|
397
417
|
# A hook failing must not break the turn (as in ToolRunner).
|
|
398
418
|
def fire_hook(name, event)
|
|
@@ -432,7 +452,8 @@ module Samagotchi
|
|
|
432
452
|
# Array), plus a model turn's tool_calls and thinking (the host's
|
|
433
453
|
# reasoning, sent back as #wire_messages says), a result's tool_call_id, the image
|
|
434
454
|
# refs of a user message or a tool result, a prompt's turn_id and its
|
|
435
|
-
# Verify/Revise anchors (Engine, never sent)
|
|
455
|
+
# Verify/Revise anchors (Engine, never sent), an opening's opening_id
|
|
456
|
+
# (TurnFlow#abort_continue!'s reason line, never sent) and a wake turn's report's turn_start (Steer, never sent), and a plugin tool result's tool_params and tool_labels
|
|
436
457
|
# (the live row's params line and label, never sent back), an
|
|
437
458
|
# edit/write result's tool_diffs (never sent), an empty-answer
|
|
438
459
|
# note's marker (TurnNote.empty, never sent), and an answer's display
|
|
@@ -448,6 +469,7 @@ module Samagotchi
|
|
|
448
469
|
message[:tool_call_id] = entry[:tool_call_id] if entry[:tool_call_id]
|
|
449
470
|
message[:images] = entry[:images] if entry[:images].is_a?(Array) && !entry[:images].empty?
|
|
450
471
|
message[:turn_id] = entry[:turn_id] if entry[:turn_id]
|
|
472
|
+
message[:opening_id] = entry[:opening_id] if entry[:opening_id].is_a?(String)
|
|
451
473
|
message[:turn_start] = true if entry[:turn_start] == true
|
|
452
474
|
message[:verify] = entry[:verify] if entry[:verify].is_a?(Hash)
|
|
453
475
|
message[:revises] = entry[:revises] if entry[:revises].is_a?(Hash)
|
|
@@ -607,8 +629,10 @@ module Samagotchi
|
|
|
607
629
|
@deferred = []
|
|
608
630
|
@context = ContextStatus.new(conversation: conversation, llm_context: loop.llm_context,
|
|
609
631
|
counted_chars_per_token: loop.counted_chars_per_token)
|
|
632
|
+
# The run's turn: its tool calls run in it.
|
|
633
|
+
@turn_context = loop.turn_context
|
|
610
634
|
loop.llm_context_turn = LLMContextForget::Turn.new(conversation: conversation, context: @context,
|
|
611
|
-
emit: method(:emit))
|
|
635
|
+
emit: method(:emit), turn_id: @turn_context.id)
|
|
612
636
|
end
|
|
613
637
|
|
|
614
638
|
def call(max_iterations:, cap:)
|
|
@@ -1092,7 +1116,7 @@ module Samagotchi
|
|
|
1092
1116
|
def dispatch(tool_calls, iteration, cap)
|
|
1093
1117
|
log_direct_deferred(tool_calls)
|
|
1094
1118
|
calls = tool_calls.map { |tool_call| NativeToolNormalizer.normalize(tool_call) }
|
|
1095
|
-
runs = ToolResponse.run_batch(@loop.tool_runner, calls, iteration: iteration, emit: method(:emit),
|
|
1119
|
+
runs = ToolResponse.run_batch(@loop.tool_runner, calls, context: @turn_context, iteration: iteration, emit: method(:emit),
|
|
1096
1120
|
on_stream_event: @on_stream_event, cap: cap) do |run, index|
|
|
1097
1121
|
@conversation << ToolResponse.single(run, tool_call_id: tool_calls[index].id,
|
|
1098
1122
|
ids: ToolIds.next_ids(@conversation, 1))
|
|
@@ -17,7 +17,7 @@ module Samagotchi
|
|
|
17
17
|
def provider = :native
|
|
18
18
|
|
|
19
19
|
def complete(messages:, max_iterations: IterationLimit::DEFAULT, on_stream_event: nil, cancel_controller: nil,
|
|
20
|
-
model_name: nil, max_tool_output_chars: nil, pending_input: nil, reasoning_since: nil)
|
|
20
|
+
model_name: nil, max_tool_output_chars: nil, pending_input: nil, reasoning_since: nil, context: nil)
|
|
21
21
|
@kernel.run(
|
|
22
22
|
messages,
|
|
23
23
|
max_iterations: max_iterations,
|
|
@@ -26,7 +26,8 @@ module Samagotchi
|
|
|
26
26
|
model_name: model_name,
|
|
27
27
|
max_tool_output_chars: max_tool_output_chars,
|
|
28
28
|
pending_input: pending_input,
|
|
29
|
-
reasoning_since: reasoning_since
|
|
29
|
+
reasoning_since: reasoning_since,
|
|
30
|
+
context: context || @kernel.context
|
|
30
31
|
)
|
|
31
32
|
end
|
|
32
33
|
end
|
|
@@ -52,9 +52,11 @@ module Samagotchi
|
|
|
52
52
|
# The turn a forget_outputs call runs in: its conversation (the
|
|
53
53
|
# entries the edits are saved on), its ContextStatus (nil: none) and
|
|
54
54
|
# its stream (+emit+, nil: none), which gets an applied batch's ✂ row
|
|
55
|
-
# (LLMContextNotice)
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
# (LLMContextNotice); +turn_id+: the Engine turn the run is for
|
|
56
|
+
# (TurnContext#id; nil: none), so a reader tells the running turn's
|
|
57
|
+
# conversation from the last one's (Engine#live_conversation).
|
|
58
|
+
Turn = Data.define(:conversation, :context, :emit, :turn_id) do
|
|
59
|
+
def initialize(emit: nil, turn_id: nil, **fields) = super
|
|
58
60
|
end
|
|
59
61
|
|
|
60
62
|
# One output a call may name: its entry, its ToolIds ref, its tool,
|
|
@@ -8,7 +8,7 @@ module Samagotchi
|
|
|
8
8
|
# An Engine's model choice (ModelChoice: the model as given and as typed)
|
|
9
9
|
# and the prompt profile resolved for it, under one mutex; the counterpart
|
|
10
10
|
# of ToolkitBinding. The memory overlay keys follow the choice
|
|
11
|
-
# (#sync_model_key!).
|
|
11
|
+
# (#sync_model_key!), which each turn's TurnContext takes.
|
|
12
12
|
#
|
|
13
13
|
# The choice changes in one assignment under the mutex (#switch!), so a
|
|
14
14
|
# reader on another thread sees the old pair or the new one. A resolution
|
|
@@ -126,13 +126,13 @@ module Samagotchi
|
|
|
126
126
|
# The memory overlay key follows the model sent (ModelOverlay.key_for
|
|
127
127
|
# its id). A model typed as an alias used to key overlays by the alias
|
|
128
128
|
# (before aliases were applied there): that key is read when the
|
|
129
|
-
# target's overlay is missing
|
|
129
|
+
# target's overlay is missing. A turn takes both in its TurnContext
|
|
130
|
+
# (Engine#generate): memory_read and the gate read them there.
|
|
130
131
|
def sync_model_key!
|
|
131
132
|
choice = @choice
|
|
132
133
|
@model_key = ModelOverlay.key_for(@bare_name.call(choice.name))
|
|
133
134
|
typed_key = ModelOverlay.key_for(@registry.parse_qualified_model(choice.typed).last)
|
|
134
135
|
@fallback_model_key = typed_key == @model_key ? nil : typed_key
|
|
135
|
-
@kernel.call.sync_model_key!(@model_key, fallback: @fallback_model_key)
|
|
136
136
|
end
|
|
137
137
|
end
|
|
138
138
|
end
|
|
@@ -63,6 +63,13 @@ module Samagotchi
|
|
|
63
63
|
@mutex.synchronize { @sections.any? { |section| section.bundle == bundle.to_s && section.name == name.to_s } }
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
# The names +bundle+ registered, in prompt order (what it registered, for
|
|
67
|
+
# Testing::Install and chi bundle check).
|
|
68
|
+
# @return [Array<String>]
|
|
69
|
+
def names(bundle)
|
|
70
|
+
@mutex.synchronize { @sections.select { |section| section.bundle == bundle.to_s }.map(&:name) }
|
|
71
|
+
end
|
|
72
|
+
|
|
66
73
|
# Run every block for +session_id+ and keep what they gave.
|
|
67
74
|
# @param session_id [String, nil] whose last good text a failure keeps
|
|
68
75
|
# @return [Array<Built>] in prompt order, the sections with text
|
|
@@ -118,7 +118,7 @@ module Samagotchi
|
|
|
118
118
|
end
|
|
119
119
|
|
|
120
120
|
# Ask the model's question (ask_user_question): the kernel's
|
|
121
|
-
# question_handler, called on the turn thread with the payload
|
|
121
|
+
# Services#question_handler, called on the turn thread with the payload
|
|
122
122
|
# Tools::AskUserQuestion.validate made. Opens it (#open_question) and
|
|
123
123
|
# returns the answer as JSON for the tool result.
|
|
124
124
|
# @param payload [Hash] {question:, options:, header:, multi_select:, allow_freeform:},
|
|
@@ -5,7 +5,7 @@ require_relative "verify_anchor"
|
|
|
5
5
|
module Samagotchi
|
|
6
6
|
# The earlier turn a Revise turn rewrites the answer of, carried only by
|
|
7
7
|
# a Revise turn (TurnAnchors). Its stamp on the turn's opening message
|
|
8
|
-
# is display metadata, never in the prompt (
|
|
8
|
+
# is display metadata, never in the prompt (TurnStages::VerifyAnchors#revision_target).
|
|
9
9
|
ReviseAnchor = Data.define(:turn_id) do
|
|
10
10
|
def self.from(value)
|
|
11
11
|
return value if value.is_a?(self)
|
|
@@ -33,6 +33,7 @@ require_relative "worker_idle_exit"
|
|
|
33
33
|
require_relative "worker_signals"
|
|
34
34
|
require_relative "chi_invocation"
|
|
35
35
|
require_relative "log"
|
|
36
|
+
require_relative "degrade"
|
|
36
37
|
require_relative "log_path"
|
|
37
38
|
require_relative "paths"
|
|
38
39
|
require_relative "project_env"
|
|
@@ -282,7 +283,7 @@ module Samagotchi
|
|
|
282
283
|
child_env[ModelProfile::MODEL_FROM_CLI_ENV] = "1" if child_env.key?(ModelProfile::MODEL_ENV)
|
|
283
284
|
# And the spawner's debug log, absolute: a relative log.file would
|
|
284
285
|
# otherwise land in the worker's (the session's) directory.
|
|
285
|
-
log_path = begin LogPath.resolve rescue nil end
|
|
286
|
+
log_path = begin LogPath.resolve rescue nil end # degrade-ok: no log to say it in; the worker's is off (below)
|
|
286
287
|
if log_path
|
|
287
288
|
child_env["SAMAGOTCHI_LOG_FILE"] = log_path
|
|
288
289
|
else
|
|
@@ -936,9 +937,7 @@ module Samagotchi
|
|
|
936
937
|
# session.keep_empty off: sessions left with nothing in them are deleted
|
|
937
938
|
# (the worker as it exits, the retention sweep).
|
|
938
939
|
def self.discard_empty?
|
|
939
|
-
Samagotchi::Config.get("session.keep_empty") != true
|
|
940
|
-
rescue StandardError
|
|
941
|
-
false
|
|
940
|
+
Degrade.call(:config, "keep_empty_unreadable", fallback: false) { Samagotchi::Config.get("session.keep_empty") != true }
|
|
942
941
|
end
|
|
943
942
|
|
|
944
943
|
# Whether a session being left (or found left) is deleted: session.keep_empty
|
|
@@ -977,9 +976,9 @@ module Samagotchi
|
|
|
977
976
|
return true if one.to_s == other.to_s
|
|
978
977
|
return false if one.nil? || other.nil?
|
|
979
978
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
979
|
+
Degrade.call(:config, "model_ref_unresolved", fallback: false) do
|
|
980
|
+
ConfigFile.model_ref(one).ref == ConfigFile.model_ref(other).ref
|
|
981
|
+
end
|
|
983
982
|
end
|
|
984
983
|
|
|
985
984
|
# Only the system prompt (an Engine seeds one, a saved session keeps
|
|
@@ -1281,9 +1280,7 @@ module Samagotchi
|
|
|
1281
1280
|
end
|
|
1282
1281
|
|
|
1283
1282
|
def self.config_idle_exit_minutes
|
|
1284
|
-
Samagotchi::Config.get("session.idle_exit_minutes")
|
|
1285
|
-
rescue StandardError
|
|
1286
|
-
nil
|
|
1283
|
+
Degrade.call(:config, "idle_exit_minutes_unreadable") { Samagotchi::Config.get("session.idle_exit_minutes") }
|
|
1287
1284
|
end
|
|
1288
1285
|
|
|
1289
1286
|
private_class_method def self.setup_session_directory(session_dir, session, state_dir:)
|
|
@@ -1640,9 +1637,7 @@ module Samagotchi
|
|
|
1640
1637
|
end
|
|
1641
1638
|
|
|
1642
1639
|
private_class_method def self.delivery_owner(manager, session_id, state_dir)
|
|
1643
|
-
manager.session_owner(session_id, state_dir: state_dir)
|
|
1644
|
-
rescue StandardError
|
|
1645
|
-
nil
|
|
1640
|
+
Degrade.call(:worker, "owner_unreadable", sid: session_id) { manager.session_owner(session_id, state_dir: state_dir) }
|
|
1646
1641
|
end
|
|
1647
1642
|
|
|
1648
1643
|
def self.stopped_on_disk?(session_id, state_dir:)
|
|
@@ -18,7 +18,7 @@ module Samagotchi
|
|
|
18
18
|
# (session.retention_days), count (session.max_count, counting only the
|
|
19
19
|
# sessions that stay), statuses kept (session.keep_status), sessions left
|
|
20
20
|
# empty (SessionManager.discardable?, an hour on), leftover scratch
|
|
21
|
-
# sessions and skeleton-only directories; a session with a live owner
|
|
21
|
+
# sessions (once past SCRATCH_START_GRACE) and skeleton-only directories; a session with a live owner
|
|
22
22
|
# always stays.
|
|
23
23
|
module SessionRetention
|
|
24
24
|
# The session.retention_days / max_count / keep_status defaults.
|
|
@@ -93,6 +93,16 @@ module Samagotchi
|
|
|
93
93
|
end
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
+
# How long after its save an unowned scratch session may still be
|
|
97
|
+
# waiting for the worker just spawned for it to take its owner lock.
|
|
98
|
+
SCRATCH_START_GRACE = SessionManager::WORKER_START_GRACE
|
|
99
|
+
|
|
100
|
+
private_class_method def self.scratch_starting?(path, now)
|
|
101
|
+
now - File.mtime(path) < SCRATCH_START_GRACE
|
|
102
|
+
rescue SystemCallError
|
|
103
|
+
false
|
|
104
|
+
end
|
|
105
|
+
|
|
96
106
|
# Lazy sweep guard: runs prune at most once per SWEEP_INTERVAL_HOURS
|
|
97
107
|
# (session.sweep_interval_hours).
|
|
98
108
|
MARKER = ".last_retention"
|
|
@@ -162,7 +172,9 @@ module Samagotchi
|
|
|
162
172
|
# count (`chi sessions clean` with no --days: test runs are throwaway).
|
|
163
173
|
#
|
|
164
174
|
# A scratch session nobody owns (its terminal and worker were killed) goes whatever its
|
|
165
|
-
# age, its status and the count; +test_only+ takes it too.
|
|
175
|
+
# age, its status and the count; +test_only+ takes it too. One saved
|
|
176
|
+
# within SCRATCH_START_GRACE (its file's mtime, against +now+) stays: its
|
|
177
|
+
# worker may not have taken the owner lock yet.
|
|
166
178
|
#
|
|
167
179
|
# +max_count+ counts the sessions that stay: one deleted whatever its
|
|
168
180
|
# place (scratch, left empty, expired, +any_age+) takes no slot.
|
|
@@ -173,7 +185,7 @@ module Samagotchi
|
|
|
173
185
|
# @return [Hash] { deleted: [ids], kept: [ids], skipped: [ids] }
|
|
174
186
|
def self.apply(state_dir: Session.default_state_dir, days: DEFAULT_RETENTION_DAYS, max_count: DEFAULT_MAX_COUNT,
|
|
175
187
|
keep_status: DEFAULT_KEEP_STATUS, dry_run: false, test_only: false, alive_check: nil,
|
|
176
|
-
empty_check: nil, any_age: false)
|
|
188
|
+
empty_check: nil, any_age: false, now: Time.now)
|
|
177
189
|
keep_status = Array(keep_status).map(&:to_s)
|
|
178
190
|
# Fetch all sessions sorted newest-first for count logic
|
|
179
191
|
all = Session.list(state_dir: state_dir, sort: "updated_at", order: "desc")
|
|
@@ -182,7 +194,6 @@ module Samagotchi
|
|
|
182
194
|
all = all.select { |session| session.test_run || session.scratch }
|
|
183
195
|
end
|
|
184
196
|
|
|
185
|
-
now = Time.now
|
|
186
197
|
cutoff = days.to_i.positive? ? now - (days.to_i * 86_400) : nil
|
|
187
198
|
max = max_count.to_i
|
|
188
199
|
|
|
@@ -217,6 +228,15 @@ module Samagotchi
|
|
|
217
228
|
end
|
|
218
229
|
end
|
|
219
230
|
|
|
231
|
+
# A scratch session saved within SCRATCH_START_GRACE: `chi scratch`
|
|
232
|
+
# saves it, then spawns the worker that takes its owner lock, so no
|
|
233
|
+
# owner yet doesn't mean a leftover. Kept, it takes no count slot.
|
|
234
|
+
if session.scratch && scratch_starting?(path, now)
|
|
235
|
+
kept << session.id
|
|
236
|
+
passed_over += 1
|
|
237
|
+
next
|
|
238
|
+
end
|
|
239
|
+
|
|
220
240
|
left_empty = begin
|
|
221
241
|
empty_check&.call(session.id)
|
|
222
242
|
rescue StandardError
|
data/lib/samagotchi/steer.rb
CHANGED
|
@@ -28,6 +28,12 @@ module Samagotchi
|
|
|
28
28
|
module Steer
|
|
29
29
|
KIND = "steer"
|
|
30
30
|
INPUT_KIND = "input"
|
|
31
|
+
# KernelLoop's line asking a Qwen model to finish the tool call it left
|
|
32
|
+
# open: a user line inside the running turn, the model's alone. No turn
|
|
33
|
+
# opens with it (#turn_prompt?), and the page draws nothing for it, as it
|
|
34
|
+
# showed nothing live. Its kind adds no header (#header): its bytes go
|
|
35
|
+
# out as before it had one.
|
|
36
|
+
RECOVERY_KIND = "recovery"
|
|
31
37
|
|
|
32
38
|
# A drain item for an input line with its sender (source nil = the user).
|
|
33
39
|
# A plain String is still a valid item: the user's line. +mark+: fields
|
|
@@ -317,6 +323,11 @@ module Samagotchi
|
|
|
317
323
|
message.is_a?(Hash) && (message[:role] || message["role"]).to_s == "user" && !steer?(message)
|
|
318
324
|
end
|
|
319
325
|
|
|
326
|
+
# KernelLoop's recovery line (RECOVERY_KIND).
|
|
327
|
+
def recovery?(message)
|
|
328
|
+
message.is_a?(Hash) && (message[:kind] || message["kind"]).to_s == RECOVERY_KIND
|
|
329
|
+
end
|
|
330
|
+
|
|
320
331
|
# The user's lines merged into a running turn (Merge#messages).
|
|
321
332
|
def input?(message)
|
|
322
333
|
message.is_a?(Hash) && (message[:kind] || message["kind"]).to_s == INPUT_KIND
|
|
@@ -333,7 +344,7 @@ module Samagotchi
|
|
|
333
344
|
start = (message[:turn_start] || message["turn_start"]) == true
|
|
334
345
|
return start if (message[:role] || message["role"]).to_s == "system"
|
|
335
346
|
|
|
336
|
-
prompt?(message) && (!input?(message) || start)
|
|
347
|
+
prompt?(message) && !recovery?(message) && (!input?(message) || start)
|
|
337
348
|
end
|
|
338
349
|
|
|
339
350
|
# A stored `!shell` line: "!(cmd)\n<output>", no turn id. A user message,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "degrade"
|
|
4
|
+
|
|
5
|
+
module Samagotchi
|
|
6
|
+
# The one rule for handing a loop event to its on_stream_event callback
|
|
7
|
+
# (KernelLoop#emit_stream_event, ToolRunner#emit): no callback is fine, and
|
|
8
|
+
# a failing one (a UI gone) must not break the turn or the tool call. Its
|
|
9
|
+
# failure is a Degrade record under :turn, named by the caller (+failure+,
|
|
10
|
+
# so a log still says which side lost the event), with the event's type.
|
|
11
|
+
module StreamEvent
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# @param callback [#call, nil] the loop's on_stream_event
|
|
15
|
+
# @param event [Hash] the event
|
|
16
|
+
# @param failure [String] the Degrade event slug when +callback+ raises
|
|
17
|
+
# @return the callback's value, nil when it raised or there is none
|
|
18
|
+
def emit(callback, event, failure:)
|
|
19
|
+
Degrade.call(:turn, failure, type: (event[:type] if event.is_a?(Hash))) { callback&.call(event) }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|