samagotchi 0.154.1 → 0.155.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -1
- data/docs/architecture.md +3 -2
- data/docs/bundles.md +21 -0
- data/docs/cli.md +1 -1
- data/docs/guardrails.md +2 -2
- data/docs/hooks.md +1 -1
- data/docs/internals/anthropic.md +4 -4
- data/docs/internals/attached-context.md +8 -6
- data/docs/internals/block-renderers.md +8 -1
- data/docs/internals/engine.md +220 -99
- data/docs/internals/inline-widgets.md +136 -0
- data/docs/internals/objectives.md +8 -6
- data/docs/internals/participants.md +5 -5
- data/docs/internals/prompt-caching.md +9 -5
- data/docs/internals/web-frontend.md +1 -1
- data/docs/internals/working-set.md +2 -1
- data/docs/plugins.md +2 -0
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +2 -2
- data/docs/testing.md +1 -1
- data/lib/samagotchi/block_renderers.rb +5 -0
- data/lib/samagotchi/bundle_command/check.rb +1 -1
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/inline-widgets/manifest.yml +11 -0
- data/lib/samagotchi/bundles/inline-widgets/plugin.rb +76 -0
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/engine/context_judge.rb +19 -125
- data/lib/samagotchi/engine/guardrails.rb +4 -2
- data/lib/samagotchi/engine/messages.rb +9 -6
- data/lib/samagotchi/engine/objectives.rb +22 -90
- data/lib/samagotchi/engine/participants.rb +15 -10
- data/lib/samagotchi/engine/plugins.rb +3 -1
- data/lib/samagotchi/engine/questions.rb +1 -1
- data/lib/samagotchi/engine/side_requests.rb +5 -2
- data/lib/samagotchi/engine/toolkit.rb +33 -190
- data/lib/samagotchi/engine/turn_end.rb +1 -1
- data/lib/samagotchi/engine/turn_helpers.rb +16 -39
- data/lib/samagotchi/engine/verify.rb +7 -143
- data/lib/samagotchi/engine.rb +162 -99
- data/lib/samagotchi/engine_peers.rb +1 -1
- data/lib/samagotchi/event_fanout.rb +18 -12
- data/lib/samagotchi/guardrail_wiring.rb +83 -82
- data/lib/samagotchi/guardrails/gate.rb +41 -40
- data/lib/samagotchi/guardrails/pair_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/review_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/targets.rb +6 -4
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/inline_widgets.rb +325 -0
- data/lib/samagotchi/kernel_loop.rb +190 -213
- data/lib/samagotchi/llm/backend.rb +3 -1
- data/lib/samagotchi/llm/chat_loop.rb +63 -41
- data/lib/samagotchi/llm/native_backend.rb +3 -2
- data/lib/samagotchi/llm_context_forget.rb +5 -3
- data/lib/samagotchi/model_binding.rb +3 -3
- data/lib/samagotchi/plugin/prompt_sections.rb +7 -0
- data/lib/samagotchi/question_desk.rb +1 -1
- data/lib/samagotchi/revise_anchor.rb +1 -1
- data/lib/samagotchi/system_prompt.rb +37 -30
- data/lib/samagotchi/testing/boundary.rb +7 -0
- data/lib/samagotchi/testing/bundle.rb +7 -5
- data/lib/samagotchi/testing/install.rb +3 -2
- data/lib/samagotchi/tool_activity.rb +8 -6
- data/lib/samagotchi/tool_declarations.rb +4 -4
- data/lib/samagotchi/tool_response.rb +4 -3
- data/lib/samagotchi/tool_runner.rb +91 -100
- data/lib/samagotchi/toolkit_binding.rb +40 -14
- data/lib/samagotchi/tools/deferred_tools.rb +17 -21
- data/lib/samagotchi/tools/registry.rb +43 -40
- data/lib/samagotchi/tools/tool_index.rb +135 -88
- data/lib/samagotchi/tools/tool_surface.rb +173 -0
- data/lib/samagotchi/turn_context.rb +67 -0
- data/lib/samagotchi/turn_stages/context_judge.rb +170 -0
- data/lib/samagotchi/turn_stages/objectives.rb +146 -0
- data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +261 -0
- data/lib/samagotchi/turn_stages/verify_anchors.rb +169 -0
- data/lib/samagotchi/turn_stages/warmup.rb +83 -0
- data/lib/samagotchi/turn_state.rb +46 -4
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/markdown_renderer.rb +39 -14
- data/lib/samagotchi/web/message_display.rb +1 -1
- data/lib/samagotchi/web/public/app.js +5 -1
- data/lib/samagotchi/web/public/block_host.js +74 -199
- data/lib/samagotchi/web/public/block_renderers.css +2 -0
- data/lib/samagotchi/web/public/block_source.js +7 -4
- data/lib/samagotchi/web/public/block_watch.js +256 -0
- data/lib/samagotchi/web/public/composer_fold.js +3 -0
- data/lib/samagotchi/web/public/index.html +28 -9
- data/lib/samagotchi/web/public/inline_widgets.js +146 -0
- data/lib/samagotchi/web/public/owner_verify.js +1 -1
- data/lib/samagotchi/web/public/participant.js +1 -1
- data/lib/samagotchi/web/public/revise_stack.js +1 -1
- data/lib/samagotchi/web/public/stage_watch.js +6 -2
- data/lib/samagotchi/web/public/text_roots.js +1 -1
- data/lib/samagotchi/web/public/verify_target.js +1 -1
- data/lib/samagotchi/web/public/widget_picks.js +130 -0
- data/lib/samagotchi/web/public/widget_walk.js +230 -0
- data/lib/samagotchi/web/public/widgets.css +70 -0
- metadata +17 -1
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -30,6 +30,7 @@ require_relative "kernel_loop"
|
|
|
30
30
|
require_relative "tools/builtins"
|
|
31
31
|
require_relative "tools/task_runtime"
|
|
32
32
|
require_relative "tools/tool_index"
|
|
33
|
+
require_relative "tools/tool_surface"
|
|
33
34
|
require_relative "peer_sources"
|
|
34
35
|
require_relative "session_commands"
|
|
35
36
|
require_relative "plugin/loader"
|
|
@@ -55,6 +56,11 @@ require_relative "objectives_audit"
|
|
|
55
56
|
require_relative "owner_prompt"
|
|
56
57
|
require_relative "side_judgments"
|
|
57
58
|
require_relative "toolkit_suggest"
|
|
59
|
+
require_relative "turn_stages/context_judge"
|
|
60
|
+
require_relative "turn_stages/objectives"
|
|
61
|
+
require_relative "turn_stages/toolkit_suggestion"
|
|
62
|
+
require_relative "turn_stages/verify_anchors"
|
|
63
|
+
require_relative "turn_stages/warmup"
|
|
58
64
|
require_relative "tool_loads"
|
|
59
65
|
require_relative "answer_tail"
|
|
60
66
|
require_relative "archive_store"
|
|
@@ -79,6 +85,7 @@ require_relative "tools/memory"
|
|
|
79
85
|
require_relative "muted_memories"
|
|
80
86
|
require_relative "used_memories"
|
|
81
87
|
require_relative "turn_state"
|
|
88
|
+
require_relative "turn_context"
|
|
82
89
|
require_relative "steer_cut"
|
|
83
90
|
require_relative "engine_peers"
|
|
84
91
|
require_relative "engine_snapshots"
|
|
@@ -206,8 +213,6 @@ module Samagotchi
|
|
|
206
213
|
# steers) and the idle layer's activity clock. Built first: a plugin
|
|
207
214
|
# may steer or ask whether a turn runs while it loads.
|
|
208
215
|
@turn_state = TurnState.new(clock: -> { monotonic_now })
|
|
209
|
-
@objective_proposals = []
|
|
210
|
-
@objective_proposals_mutex = Mutex.new
|
|
211
216
|
# A message for the running turn cutting its generation.
|
|
212
217
|
@steer_cut = SteerCut.new(clock: -> { monotonic_now }, controller: -> { active_cancel_controller },
|
|
213
218
|
cut_after: -> { steer_cut_after })
|
|
@@ -223,12 +228,11 @@ module Samagotchi
|
|
|
223
228
|
@chat_backend_mutex = Mutex.new
|
|
224
229
|
@side_judgments = SideJudgments.new
|
|
225
230
|
@judgments = build_judgments
|
|
226
|
-
# On mode's cards an answer brought while a turn ran: shown once it
|
|
227
|
-
# has ended (#show_context_cards_after_turn).
|
|
228
|
-
@context_cards_after_turn = []
|
|
229
|
-
@context_cards_mutex = Mutex.new
|
|
230
|
-
@toolkit_warmup_thread = nil
|
|
231
231
|
@host_registry = host_registry || HostRegistry.new
|
|
232
|
+
# The turn stages over the objectives (its audit, view and proposals)
|
|
233
|
+
# and the Verify/Revise anchors (TurnStages).
|
|
234
|
+
@objectives_stage = build_objectives_stage
|
|
235
|
+
@verify_anchors = build_verify_anchors
|
|
232
236
|
# A model given here (a worker's session model) is the one it runs,
|
|
233
237
|
# so its host is checked; the config default is only checked once it
|
|
234
238
|
# is used (the one-shot checks the model it starts on, #switch_model!).
|
|
@@ -302,6 +306,8 @@ module Samagotchi
|
|
|
302
306
|
# The kernel (built here, or a spec's) and what it reads of the Engine;
|
|
303
307
|
# then the native backend over it.
|
|
304
308
|
wire_kernel(kernel, muted_memories)
|
|
309
|
+
# The turn-end warm-up's stage, over that kernel.
|
|
310
|
+
@turn_warmup = build_turn_warmup
|
|
305
311
|
Log.debug(:model, "backend", provider: backend.provider) if Log.level?(:debug)
|
|
306
312
|
@resume_session = session_id ? Session.load(session_id) : nil
|
|
307
313
|
@prompt_builder = build_system_prompt(memories, appended_prompt_files)
|
|
@@ -379,6 +385,11 @@ module Samagotchi
|
|
|
379
385
|
# The session's toolkit: the kits (the plugins' too, chi.toolkit),
|
|
380
386
|
# what its next turn declares, the turn-start apply.
|
|
381
387
|
@toolkit_binding = build_toolkit_binding
|
|
388
|
+
# The turn stages over it and the side asks (TurnStages): System 1's
|
|
389
|
+
# toolkit suggestion and the attached-context candidate judge, which a
|
|
390
|
+
# plugin may reach as it loads.
|
|
391
|
+
@toolkit_suggestion = build_toolkit_suggestion
|
|
392
|
+
@context_judge = build_context_judge
|
|
382
393
|
# The tools plugins publish without declaring (chi.tool_source), and
|
|
383
394
|
# the search and call tools that reach them (Tools::ToolIndex).
|
|
384
395
|
@tool_index = Tools::ToolIndex.new
|
|
@@ -396,6 +407,8 @@ module Samagotchi
|
|
|
396
407
|
# with no plugins too: a toolkit's left-out tools are reachable
|
|
397
408
|
# either way (declared per toolkit, Tools::Registry).
|
|
398
409
|
@tool_index.declare(@tools, failures: @plugin_failures)
|
|
410
|
+
# What readers off the turn thread see until the first turn's start.
|
|
411
|
+
publish_tools!
|
|
399
412
|
end
|
|
400
413
|
private :build_tools_and_plugins
|
|
401
414
|
|
|
@@ -424,45 +437,39 @@ module Samagotchi
|
|
|
424
437
|
end
|
|
425
438
|
private :subscribe_session_observers
|
|
426
439
|
|
|
427
|
-
# The kernel and what it holds of the Engine
|
|
428
|
-
#
|
|
429
|
-
#
|
|
440
|
+
# The kernel and what it holds of the Engine for its life, one
|
|
441
|
+
# KernelLoop::Services (stores, hooks, tools, the mutes, the question
|
|
442
|
+
# flow, peers, the gate, warm-up, the generation guard), and the native
|
|
443
|
+
# backend over it. After the plugins load. Nothing per turn: a turn's
|
|
444
|
+
# TurnContext goes down the backend's complete (#generate).
|
|
430
445
|
def wire_kernel(kernel, muted_memories)
|
|
431
|
-
#
|
|
432
|
-
# the
|
|
433
|
-
@kernel = kernel || KernelLoop.new(client: @client, profile: @model_binding.given_profile, model_name: effective_model_name,
|
|
434
|
-
hooks: @hooks, reminder_store: @reminder_store, tools: @tools)
|
|
435
|
-
@kernel.reminder_store = @reminder_store
|
|
436
|
-
@kernel.hooks = @hooks
|
|
437
|
-
@kernel.tools = @tools
|
|
438
|
-
sync_kernel_client!
|
|
439
|
-
sync_model_key!
|
|
440
|
-
# The mutes never change during a session, so no re-sync: the kernel's
|
|
441
|
-
# memory_read guard reads the same list for every turn.
|
|
446
|
+
# The mutes never change during a session: the kernel's memory_read
|
|
447
|
+
# guard reads the same list for every turn.
|
|
442
448
|
@muted_memory_names = MutedMemories.normalize_list(muted_memories)
|
|
443
|
-
|
|
444
|
-
#
|
|
445
|
-
@kernel.question_handler = proc { |payload| request_question(payload) }
|
|
446
|
-
# Every tool call asks this gate first. The kernel is never rebuilt, so
|
|
447
|
-
# it holds across model switches.
|
|
449
|
+
# Where the approvals live; also where list_sessions and send_note
|
|
450
|
+
# look for other sessions (a worker moves it, Worker).
|
|
448
451
|
self.guardrail_state_dir = Session.default_state_dir
|
|
449
|
-
# list_sessions and send_note speak for whichever session runs now.
|
|
450
|
-
@kernel.peers = PeerView.new(self)
|
|
451
|
-
# A plugin tool's row refs resolve against the session's folder, as
|
|
452
|
-
# answer refs do (AnswerRefs), not the process's.
|
|
453
|
-
@kernel.session_cwd = -> { @session&.working_directory }
|
|
454
|
-
# forget_outputs refuses what the user pinned in the working set.
|
|
455
|
-
@kernel.working_set_pins = -> { @session&.working_set_pins || [] }
|
|
456
|
-
# A cut tool output is saved whole in the session's folder.
|
|
457
|
-
@kernel.session_dir = -> { @session&.id && Session.session_dir(@session.id, state_dir: session_state_dir) }
|
|
458
|
-
@kernel.guardrail_gate = @guardrail_wiring.gate
|
|
459
|
-
@kernel.participant_turn = -> { Steer.participant_author?(turn_author) }
|
|
460
|
-
@kernel.participant_mode = -> { @guardrail_wiring.turn_mode }
|
|
461
|
-
# A pair turn stops once its participant passed their cap.
|
|
462
|
-
@kernel.generation_guard = -> { guard_participant_spend }
|
|
463
452
|
# The turn-end warm-up; the kernel asks it for a slot pin per request.
|
|
464
453
|
@prompt_warmup = PromptWarmup.new
|
|
465
|
-
|
|
454
|
+
services = KernelLoop::Services.new(
|
|
455
|
+
hooks: @hooks, tools: @tools, reminder_store: @reminder_store, muted_memory_names: @muted_memory_names,
|
|
456
|
+
# ask_user_question blocks on the Engine's question flow (TUI/Web answer it).
|
|
457
|
+
question_handler: proc { |payload| request_question(payload) },
|
|
458
|
+
# list_sessions and send_note speak for whichever session runs now.
|
|
459
|
+
peers: PeerView.new(self),
|
|
460
|
+
# Every tool call asks this gate first. The kernel is never rebuilt,
|
|
461
|
+
# so it holds across model switches.
|
|
462
|
+
gate: @guardrail_wiring.gate(hooks: @hooks, tools: @tools),
|
|
463
|
+
warmup: @prompt_warmup,
|
|
464
|
+
# A pair turn stops once its participant passed their cap.
|
|
465
|
+
generation_guard: ->(context) { guard_participant_spend(context) }
|
|
466
|
+
)
|
|
467
|
+
# A given kernel (specs) gets the same services as the one built here.
|
|
468
|
+
@kernel = kernel || KernelLoop.new(client: @client, profile: @model_binding.given_profile,
|
|
469
|
+
model_name: effective_model_name, services: services)
|
|
470
|
+
@kernel.attach(services) if kernel
|
|
471
|
+
sync_kernel_client!
|
|
472
|
+
sync_model_key!
|
|
466
473
|
# What a hook can do beyond reading its event (event[:notify],
|
|
467
474
|
# event[:ask_user], event[:stop_turn]): the Engine's routes to the UIs.
|
|
468
475
|
@hooks.runtime = hook_runtime
|
|
@@ -476,11 +483,9 @@ module Samagotchi
|
|
|
476
483
|
def build_guardrail_wiring
|
|
477
484
|
GuardrailWiring.new(
|
|
478
485
|
scratch: @scratch,
|
|
479
|
-
|
|
486
|
+
# Built before the tools (the hooks load into its failures).
|
|
480
487
|
tools: -> { @tools },
|
|
481
488
|
session: -> { @session },
|
|
482
|
-
model_key: -> { @model_binding.model_key },
|
|
483
|
-
model_name: -> { guardrail_model_name },
|
|
484
489
|
cancelled: -> { !!active_cancel_controller&.cancelled? },
|
|
485
490
|
# An approval of the model's call: :question_asked first (QuestionAsked).
|
|
486
491
|
ask: ->(fields) { open_question(question_asked(QuestionAsked::KIND_APPROVAL, fields)) },
|
|
@@ -495,7 +500,7 @@ module Samagotchi
|
|
|
495
500
|
system_one_runs: -> { system_one_runs },
|
|
496
501
|
cancel_controller: -> { active_cancel_controller },
|
|
497
502
|
# A participant's turn: their name on the owner's card, their
|
|
498
|
-
# grant's mode for the turn's (GuardrailWiring#
|
|
503
|
+
# grant's mode for the turn's (GuardrailWiring#begin_turn).
|
|
499
504
|
participant: ->(author) { @participants.entry_for(author, session: @session) }
|
|
500
505
|
)
|
|
501
506
|
end
|
|
@@ -511,7 +516,8 @@ module Samagotchi
|
|
|
511
516
|
thinking_notices: -> { @thinking_notices },
|
|
512
517
|
metrics: -> { @metrics },
|
|
513
518
|
hooks: -> { @hooks },
|
|
514
|
-
|
|
519
|
+
turn_context: -> { turn_context },
|
|
520
|
+
last_turn_context: -> { last_turn_context },
|
|
515
521
|
model_settings: -> { @model_settings },
|
|
516
522
|
session: -> { @session },
|
|
517
523
|
live_conversation: -> { live_conversation },
|
|
@@ -683,7 +689,7 @@ module Samagotchi
|
|
|
683
689
|
end
|
|
684
690
|
@side_judgments.join(SIDE_JUDGMENTS_JOIN_SECONDS)
|
|
685
691
|
# The warm-up's answer is dropped anyway.
|
|
686
|
-
|
|
692
|
+
@toolkit_suggestion.stop_warmup
|
|
687
693
|
left = threads.count(&:alive?)
|
|
688
694
|
Log.warn(:plugins, "anytime_commands_left", count: left) if left.positive?
|
|
689
695
|
@plugin_tasks.kill_leftovers
|
|
@@ -715,13 +721,13 @@ module Samagotchi
|
|
|
715
721
|
kept, event = yield(seconds)
|
|
716
722
|
event[:turn_id] = turn.id
|
|
717
723
|
event[:verify] = true if turn.verify
|
|
718
|
-
revises = stamp_opening(kept, turn, outcome)
|
|
724
|
+
revises = @verify_anchors.stamp_opening(kept, turn, outcome)
|
|
719
725
|
replace_session_messages(turn.session, kept) if kept
|
|
720
|
-
verdict = record_verdict(turn, outcome)
|
|
726
|
+
verdict = @verify_anchors.record_verdict(turn, outcome)
|
|
721
727
|
event[:verdict] = verdict if verdict
|
|
722
728
|
event[:revises] = revises.to_h if revises
|
|
723
729
|
emit_working_set(turn.on_event, turn.session, turn.session.messages)
|
|
724
|
-
|
|
730
|
+
@objectives_stage.announce(turn.session)
|
|
725
731
|
turn.session.status = Session::STATUS_IDLE
|
|
726
732
|
record_last_turn(turn.session, outcome, seconds, turn.origin, event, limit: exhausted ? turn.limit : nil)
|
|
727
733
|
emit_event(turn.on_event, turn.tag(event))
|
|
@@ -778,14 +784,15 @@ module Samagotchi
|
|
|
778
784
|
end
|
|
779
785
|
private :build_recap
|
|
780
786
|
|
|
781
|
-
# One turn's
|
|
782
|
-
# single-threaded: the cross-thread turn state (the
|
|
783
|
-
# sink, steers) stays
|
|
787
|
+
# One turn's lifecycle state, for #run_turn and its endings. Per turn
|
|
788
|
+
# and single-threaded: the cross-thread turn state (the published
|
|
789
|
+
# TurnContext, the cancel slot, the sink, steers) stays in TurnState.
|
|
790
|
+
# +context+: the turn's TurnContext, the one source of truth for its
|
|
791
|
+
# id, session, origin and controller (read through #id, #session,
|
|
792
|
+
# #origin, #controller); #generate replaces it with the settings in.
|
|
784
793
|
# +ended+: the end event is out (what follows is post-turn work).
|
|
785
|
-
# +
|
|
786
|
-
#
|
|
787
|
-
# +id+: names the turn on :turn_started (its metrics record's id) and on
|
|
788
|
-
# its saved prompt, so a UI pairs the two whatever left the conversation.
|
|
794
|
+
# +prepared_settings+: the LLM::TurnSettings #prepare_turn makes (before
|
|
795
|
+
# the hooks); #generate completes them into the context's.
|
|
789
796
|
# +limit+: the iteration limit #generate ran it with.
|
|
790
797
|
# +resumed+: a step-limit Continue (run_turn's resumed:).
|
|
791
798
|
# +resume_from+: where its reasoning starts, the run it resumes
|
|
@@ -794,8 +801,15 @@ module Samagotchi
|
|
|
794
801
|
# +wall+: the wall time it started (MessageTime.now, once): the
|
|
795
802
|
# :turn_started event's at and its user message's (and reminders') stamp.
|
|
796
803
|
# +verify+, +revises+: a Verify turn's claim, a Revise turn's draft.
|
|
797
|
-
Turn = Struct.new(:
|
|
798
|
-
:
|
|
804
|
+
Turn = Struct.new(:context, :prompt, :continue, :on_event, :started_at, :messages, :ended, :prepared_settings,
|
|
805
|
+
:limit, :resumed, :resume_from, :wall, :verify, :revises, :outcome) do
|
|
806
|
+
# The turn's id (:turn_started's turn_id, its metrics record's id and
|
|
807
|
+
# its saved prompt's), session, origin and controller: its context's.
|
|
808
|
+
def id = context.id
|
|
809
|
+
def session = context.session
|
|
810
|
+
def origin = context.origin
|
|
811
|
+
def controller = context.controller
|
|
812
|
+
|
|
799
813
|
def resumed? = resumed ? true : false
|
|
800
814
|
|
|
801
815
|
# The turn ended as a completed one (not canceled, not failed): the
|
|
@@ -866,7 +880,7 @@ module Samagotchi
|
|
|
866
880
|
# wake's note) names it with this id
|
|
867
881
|
# @param verify [VerifyAnchor, nil] a Verify turn's claim (its verdict)
|
|
868
882
|
# @param revises [ReviseAnchor, nil] a Revise turn's draft: stamped on
|
|
869
|
-
# the prompt at the turn's end, metadata only (#stamp_opening)
|
|
883
|
+
# the prompt at the turn's end, metadata only (TurnStages::VerifyAnchors#stamp_opening)
|
|
870
884
|
#
|
|
871
885
|
# An Interrupt (SIGINT) cancels the turn: what it got to is kept in the
|
|
872
886
|
# session as after a Stop (the prompt, the loop's finished tool
|
|
@@ -897,7 +911,7 @@ module Samagotchi
|
|
|
897
911
|
pending_input: pending_input)
|
|
898
912
|
publish_used_memories(session, on_event)
|
|
899
913
|
complete_turn(turn, result)
|
|
900
|
-
|
|
914
|
+
@turn_warmup.after(turn, result)
|
|
901
915
|
result
|
|
902
916
|
rescue Interrupt => e
|
|
903
917
|
# After the end event (in the after_turn/session_end hooks) the
|
|
@@ -924,24 +938,23 @@ module Samagotchi
|
|
|
924
938
|
raise
|
|
925
939
|
ensure
|
|
926
940
|
release_turn(probe_cancel_before)
|
|
927
|
-
|
|
928
|
-
# Only a completed turn shows its proposal cards
|
|
929
|
-
#
|
|
930
|
-
|
|
931
|
-
turn.completed? ? show_objective_proposals : discard_objective_proposals(turn)
|
|
941
|
+
@context_judge.show_cards_after_turn
|
|
942
|
+
# Only a completed turn shows its proposal cards; any other's are
|
|
943
|
+
# dropped, so the next turn does not show them either.
|
|
944
|
+
@objectives_stage.finally(turn)
|
|
932
945
|
end
|
|
933
946
|
end
|
|
934
947
|
|
|
935
948
|
# The turn's state before anything can fail it: the session running,
|
|
936
|
-
# the turn flag, its cancel controller and sink. The
|
|
937
|
-
# is set here and cleared in #release_turn only.
|
|
949
|
+
# its TurnContext, the turn flag, its cancel controller and sink. The
|
|
950
|
+
# cross-thread state is set here and cleared in #release_turn only.
|
|
938
951
|
def begin_turn(session, prompt, on_event:, cancel_controller:, origin:, continue:, id: nil, verify: nil, revises: nil,
|
|
939
952
|
input_waiting: nil)
|
|
940
953
|
origin = TurnOrigin.of(origin)
|
|
941
954
|
# A participant's Verify fresh child: every turn is theirs (their
|
|
942
955
|
# author, boundary and cap), whoever started it.
|
|
943
956
|
origin = session.verify_fresh.origin_for(origin) if session.respond_to?(:verify_fresh) && session.verify_fresh
|
|
944
|
-
verify =
|
|
957
|
+
verify = @verify_anchors.verified(session, VerifyAnchor.from(verify))
|
|
945
958
|
# Track the active session for recap and status snapshot.
|
|
946
959
|
@session = session
|
|
947
960
|
# status is turn state: running now, idle again before the turn's end
|
|
@@ -950,25 +963,62 @@ module Samagotchi
|
|
|
950
963
|
@used_memories.absorb(session)
|
|
951
964
|
# Provide a cancellable controller for this turn (cross-process cancel via file flag)
|
|
952
965
|
effective_controller = cancel_controller || CancellationController.new
|
|
953
|
-
# The turn
|
|
954
|
-
#
|
|
955
|
-
|
|
956
|
-
#
|
|
957
|
-
#
|
|
958
|
-
#
|
|
959
|
-
|
|
966
|
+
# The turn's context, whole before the turn is published: who queued
|
|
967
|
+
# it, a participant's mode (participants.json), git asked afresh.
|
|
968
|
+
context = new_turn_context(session, origin, effective_controller, id || SecureRandom.uuid)
|
|
969
|
+
# The turn runs, with its context, controller and sink, in one step:
|
|
970
|
+
# a Stop, a plugin's card or its ctx.turn_author from another thread
|
|
971
|
+
# never sees a running turn without them (or with the last turn's).
|
|
972
|
+
# The idle recap detector does not fire (or render an invalidated
|
|
973
|
+
# recap) while the model is working. A hook's notice goes where the
|
|
974
|
+
# turn's events go (a one-shot prints only its sink; a worker's
|
|
975
|
+
# observers carry it to the bridge).
|
|
976
|
+
@turn_state.begin!(context: context, controller: effective_controller, sink: on_event, input_waiting: input_waiting)
|
|
960
977
|
# Drop a recap already in flight: the turn makes it stale.
|
|
961
978
|
@recap&.invalidate!
|
|
962
|
-
# The gate's context: who queued this turn, and git asked afresh.
|
|
963
|
-
@guardrail_wiring.begin_turn(origin)
|
|
964
979
|
|
|
965
|
-
Turn.new(
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
revises: ReviseAnchor.from(revises))
|
|
980
|
+
Turn.new(context: context, prompt: continue ? nil : prompt, continue: continue, on_event: on_event,
|
|
981
|
+
started_at: Process.clock_gettime(Process::CLOCK_MONOTONIC), wall: MessageTime.now,
|
|
982
|
+
verify: VerifyAnchor.from(verify), revises: ReviseAnchor.from(revises))
|
|
969
983
|
end
|
|
970
984
|
private :begin_turn
|
|
971
985
|
|
|
986
|
+
# A new turn's TurnContext (#begin_turn): the gate's turn facts
|
|
987
|
+
# (GuardrailWiring#begin_turn), the session's folders. Its settings and
|
|
988
|
+
# model come in #generate.
|
|
989
|
+
def new_turn_context(session, origin, controller, id)
|
|
990
|
+
start = @guardrail_wiring.begin_turn(origin, session: session)
|
|
991
|
+
TurnContext.new(id: id, session: session, origin: origin, author: origin.participant_author,
|
|
992
|
+
participant_mode: start.mode, participant: start.participant, controller: controller,
|
|
993
|
+
cwd: (session.working_directory if session.respond_to?(:working_directory)), session_dir: turn_session_dir(session),
|
|
994
|
+
state_dir: session_state_dir, git: start.git, settings: nil, model_key: nil,
|
|
995
|
+
fallback_model_key: nil, model_name: nil, surface: nil)
|
|
996
|
+
end
|
|
997
|
+
private :new_turn_context
|
|
998
|
+
|
|
999
|
+
# The session's folder, nil when it can't be named (an id that isn't
|
|
1000
|
+
# one, a lookup that raised): a cut tool output then says it wasn't saved.
|
|
1001
|
+
def turn_session_dir(session)
|
|
1002
|
+
Session.session_dir(session.id, state_dir: session_state_dir)
|
|
1003
|
+
rescue StandardError => e
|
|
1004
|
+
Log.warn(:turn, "tool_output_save_failed", error: e.class.name)
|
|
1005
|
+
nil
|
|
1006
|
+
end
|
|
1007
|
+
private :turn_session_dir
|
|
1008
|
+
|
|
1009
|
+
# The running turn's TurnContext as other threads see it (TurnState),
|
|
1010
|
+
# nil between turns; its settings are nil until #generate.
|
|
1011
|
+
# @return [TurnContext, nil]
|
|
1012
|
+
def turn_context = @turn_state.context
|
|
1013
|
+
private :turn_context
|
|
1014
|
+
|
|
1015
|
+
# The last finished turn's TurnContext (TurnState#last_context), nil
|
|
1016
|
+
# before any: its author (the recap's), its settings (the working set's
|
|
1017
|
+
# between turns).
|
|
1018
|
+
# @return [TurnContext, nil]
|
|
1019
|
+
def last_turn_context = @turn_state.last_context
|
|
1020
|
+
private :last_turn_context
|
|
1021
|
+
|
|
972
1022
|
# Everything before the model is asked: :turn_started, the kernel's
|
|
973
1023
|
# per-turn settings, the plugins' setup, the session_start/before_turn
|
|
974
1024
|
# hooks, then the messages to send (#turn_messages). False when a Stop
|
|
@@ -987,14 +1037,14 @@ module Samagotchi
|
|
|
987
1037
|
# A participant's turn names them (author, author_name) for the UIs'
|
|
988
1038
|
# badge; the owner's turn_started stays as it was.
|
|
989
1039
|
turn_started.merge!(author_fields(turn.origin.author, session: session),
|
|
990
|
-
participant_mode_fields(turn.
|
|
1040
|
+
participant_mode_fields(turn.context))
|
|
991
1041
|
emit_event(turn.on_event, turn.tag(turn_started))
|
|
992
1042
|
raise image_error if image_error
|
|
993
1043
|
|
|
994
1044
|
# On mode's ask starts here too; the turn joins it before
|
|
995
1045
|
# apply_toolkit! below, so its latency hides behind the probes and
|
|
996
1046
|
# the init wait.
|
|
997
|
-
toolkit_suggestion
|
|
1047
|
+
@toolkit_suggestion.start(turn)
|
|
998
1048
|
|
|
999
1049
|
# The window and the profile probes run after run_turn's probe-cancel
|
|
1000
1050
|
# swap, so a Stop cuts them; a failure ends the turn as turn_failed.
|
|
@@ -1006,12 +1056,12 @@ module Samagotchi
|
|
|
1006
1056
|
refresh_profile!
|
|
1007
1057
|
|
|
1008
1058
|
# Before anything of the turn is kept or a reminder is used up.
|
|
1009
|
-
turn.
|
|
1010
|
-
refuse_images!(turn.
|
|
1059
|
+
turn.prepared_settings = turn_settings(session)
|
|
1060
|
+
refuse_images!(turn.prepared_settings.vision) unless image_refs.empty?
|
|
1011
1061
|
# A Stop during the probes: the turn ends before the load-failure
|
|
1012
1062
|
# warnings, which show once, so the next turn still shows them.
|
|
1013
1063
|
if turn.controller.cancelled?
|
|
1014
|
-
toolkit_suggestion
|
|
1064
|
+
@toolkit_suggestion.abandon(turn, :canceled)
|
|
1015
1065
|
return false
|
|
1016
1066
|
end
|
|
1017
1067
|
|
|
@@ -1028,11 +1078,11 @@ module Samagotchi
|
|
|
1028
1078
|
apply_staged_tools!
|
|
1029
1079
|
# On mode: the suggested kit, before anything is declared (a Stop
|
|
1030
1080
|
# during the wait is caught by the check below).
|
|
1031
|
-
|
|
1081
|
+
@toolkit_suggestion.settle(turn)
|
|
1032
1082
|
# The candidate judge: its asks run beside the turn; on mode's
|
|
1033
1083
|
# attaches reach the model at the next between-turn delivery.
|
|
1034
|
-
|
|
1035
|
-
|
|
1084
|
+
@context_judge.start(turn)
|
|
1085
|
+
@objectives_stage.incoming(turn)
|
|
1036
1086
|
apply_toolkit!(session, turn.on_event)
|
|
1037
1087
|
refresh_participants!(session)
|
|
1038
1088
|
# A Stop during the waits above: the turn ends here,
|
|
@@ -1055,7 +1105,8 @@ module Samagotchi
|
|
|
1055
1105
|
ensure
|
|
1056
1106
|
# A turn that ended (a failure, an Interrupt) before it settled the
|
|
1057
1107
|
# ask: its line says so. Nothing once it was settled.
|
|
1058
|
-
toolkit_suggestion
|
|
1108
|
+
@toolkit_suggestion.abandon(turn, :failed)
|
|
1109
|
+
@toolkit_suggestion.finish(turn)
|
|
1059
1110
|
end
|
|
1060
1111
|
private :prepare_turn
|
|
1061
1112
|
|
|
@@ -1087,7 +1138,7 @@ module Samagotchi
|
|
|
1087
1138
|
|
|
1088
1139
|
unless turn.continue
|
|
1089
1140
|
user_message = { role: "user", content: turn.prompt, turn_id: turn.id, **author_fields(turn.origin.author),
|
|
1090
|
-
**participant_mode_fields(turn.
|
|
1141
|
+
**participant_mode_fields(turn.context), MessageTime::KEY => turn.at }
|
|
1091
1142
|
user_message[:verify] = turn.verify.to_h if turn.verify
|
|
1092
1143
|
user_message[:images] = image_refs unless image_refs.empty?
|
|
1093
1144
|
turn.messages << user_message
|
|
@@ -1106,17 +1157,28 @@ module Samagotchi
|
|
|
1106
1157
|
# Route model name as bare (without host prefix) to the server;
|
|
1107
1158
|
# host selection already done via active client.
|
|
1108
1159
|
bare_for_backend = bare_model_name(model.name)
|
|
1109
|
-
# The turn's settings, set once here
|
|
1110
|
-
#
|
|
1111
|
-
#
|
|
1160
|
+
# The turn's settings, set once here into its context (with the model
|
|
1161
|
+
# key and name the gate and memory_read use: frozen now, after the
|
|
1162
|
+
# hooks that may switch the model), which goes down the backend's
|
|
1163
|
+
# complete to every tool call, and is published (TurnState).
|
|
1112
1164
|
explained = model.llm_context_explained(@model_settings.llm_context_session_args)
|
|
1113
1165
|
model_backend = backend(model)
|
|
1114
1166
|
# A host that loads tools runs a direct call of one the session
|
|
1115
1167
|
# reaches but doesn't declare (ToolRunner, KernelLoop#dispatch).
|
|
1116
1168
|
loads_tools = model_backend.respond_to?(:loads_tools?) && model_backend.loads_tools?(bare_for_backend)
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1169
|
+
settings = turn.prepared_settings.with(model_name: bare_for_backend, window_setting: model.window_setting,
|
|
1170
|
+
llm_context: explained&.resolved, price: model.price,
|
|
1171
|
+
loads_tools: loads_tools)
|
|
1172
|
+
# The turn's tools (Tools::ToolSurface), built once from the Registry
|
|
1173
|
+
# under those settings' layers and loads_tools: what the loops declare,
|
|
1174
|
+
# defer and dispatch until the turn ends; a change meanwhile (a
|
|
1175
|
+
# /toolkit change, a staged chi.replace_tools) is the next turn's.
|
|
1176
|
+
surface = Tools::ToolSurface.for(@tools, layers: settings.llm_context&.active_layers || [],
|
|
1177
|
+
loads_tools: loads_tools)
|
|
1178
|
+
turn.context = turn.context.with(settings: settings, model_key: @model_binding.model_key,
|
|
1179
|
+
fallback_model_key: @model_binding.fallback_model_key,
|
|
1180
|
+
model_name: guardrail_model_name, surface: surface)
|
|
1181
|
+
@turn_state.replace_context(turn.context)
|
|
1120
1182
|
log_llm_context(explained)
|
|
1121
1183
|
|
|
1122
1184
|
turn.limit = @no_interrupt ? IterationLimit.for(no_interrupt: true) : max_iterations || IterationLimit.for
|
|
@@ -1133,7 +1195,8 @@ module Samagotchi
|
|
|
1133
1195
|
model_name: bare_for_backend,
|
|
1134
1196
|
max_tool_output_chars: max_tool_output_chars,
|
|
1135
1197
|
pending_input: turn_drain(pending_input),
|
|
1136
|
-
reasoning_since: reasoning_since
|
|
1198
|
+
reasoning_since: reasoning_since,
|
|
1199
|
+
context: turn.context
|
|
1137
1200
|
)
|
|
1138
1201
|
result.run_start = run_start
|
|
1139
1202
|
result
|
|
@@ -4,7 +4,7 @@ require_relative "objectives"
|
|
|
4
4
|
|
|
5
5
|
module Samagotchi
|
|
6
6
|
# The views of an Engine its peers get: the kernel's tools
|
|
7
|
-
# (Engine#
|
|
7
|
+
# (KernelLoop::Services#peers, Engine#wire_kernel) and a delegate's approval relay
|
|
8
8
|
# (Engine#relay_peer).
|
|
9
9
|
class Engine
|
|
10
10
|
# The kernel's Tools::Peers, following the current session. cancelled?
|
|
@@ -26,7 +26,8 @@ module Samagotchi
|
|
|
26
26
|
# @param thinking_notices [#call] → ThinkingNotices
|
|
27
27
|
# @param metrics [#call] → SessionMetrics
|
|
28
28
|
# @param hooks [#call] → Hooks::Registry
|
|
29
|
-
# @param
|
|
29
|
+
# @param turn_context [#call] → TurnContext, nil: the running turn's (Engine#turn_context)
|
|
30
|
+
# @param last_turn_context [#call] → TurnContext, nil: the last finished turn's
|
|
30
31
|
# @param model_settings [#call] → SessionModelSettings
|
|
31
32
|
# @param session [#call] → Session, nil
|
|
32
33
|
# @param live_conversation [#call] → Array<Hash>, the running turn's conversation
|
|
@@ -35,8 +36,8 @@ module Samagotchi
|
|
|
35
36
|
# @param working_set_strategy [#call] → the strategy the working set is built under
|
|
36
37
|
# @param turn_running [#call] → Boolean
|
|
37
38
|
# @param announce_or_hold [#call] (event) → announces the event, or holds it
|
|
38
|
-
def initialize(observer:, used_memories:, muted:, steer_cut:, thinking_notices:, metrics:, hooks:,
|
|
39
|
-
model_settings:, session:, live_conversation:, used_memory_names:, served_expected_by:,
|
|
39
|
+
def initialize(observer:, used_memories:, muted:, steer_cut:, thinking_notices:, metrics:, hooks:, turn_context:,
|
|
40
|
+
last_turn_context:, model_settings:, session:, live_conversation:, used_memory_names:, served_expected_by:,
|
|
40
41
|
working_set_strategy:, turn_running:, announce_or_hold:)
|
|
41
42
|
@observer = observer
|
|
42
43
|
@used_memories = used_memories
|
|
@@ -45,7 +46,8 @@ module Samagotchi
|
|
|
45
46
|
@thinking_notices = thinking_notices
|
|
46
47
|
@metrics = metrics
|
|
47
48
|
@hooks = hooks
|
|
48
|
-
@
|
|
49
|
+
@turn_context = turn_context
|
|
50
|
+
@last_turn_context = last_turn_context
|
|
49
51
|
@model_settings = model_settings
|
|
50
52
|
@session = session
|
|
51
53
|
@live_conversation = live_conversation
|
|
@@ -149,15 +151,18 @@ module Samagotchi
|
|
|
149
151
|
end
|
|
150
152
|
|
|
151
153
|
# The strategy the set is built under: the running turn's
|
|
152
|
-
# (TurnSettings#llm_context: what its requests send)
|
|
153
|
-
# the
|
|
154
|
-
#
|
|
155
|
-
#
|
|
154
|
+
# (TurnSettings#llm_context: what its requests send), the last turn's
|
|
155
|
+
# while the running one has no settings yet (before Engine#generate);
|
|
156
|
+
# between turns the next turn's (as TurnStages::Warmup#after: the last turn's
|
|
157
|
+
# settings are stale after a /llm-context or /model change), the last
|
|
158
|
+
# turn's when it can't be resolved. One read of each published context
|
|
159
|
+
# (TurnState): any thread may ask (/working-set's anytime list).
|
|
156
160
|
def working_set_llm_context
|
|
157
|
-
|
|
158
|
-
|
|
161
|
+
running = turn_context
|
|
162
|
+
last = last_turn_context&.settings&.llm_context
|
|
163
|
+
return running.settings ? running.settings.llm_context : last if running
|
|
159
164
|
|
|
160
|
-
model_settings.turn_llm_context ||
|
|
165
|
+
model_settings.turn_llm_context || last
|
|
161
166
|
end
|
|
162
167
|
|
|
163
168
|
# A :generation_completed with the generation's decode speed (`speed:`
|
|
@@ -222,7 +227,8 @@ module Samagotchi
|
|
|
222
227
|
def thinking_notices = @thinking_notices.call
|
|
223
228
|
def metrics = @metrics.call
|
|
224
229
|
def hooks = @hooks.call
|
|
225
|
-
def
|
|
230
|
+
def turn_context = @turn_context.call
|
|
231
|
+
def last_turn_context = @last_turn_context.call
|
|
226
232
|
def model_settings = @model_settings.call
|
|
227
233
|
def session = @session.call
|
|
228
234
|
def live_conversation = @live_conversation.call
|