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.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -1
  3. data/docs/architecture.md +3 -2
  4. data/docs/bundles.md +21 -0
  5. data/docs/cli.md +1 -1
  6. data/docs/guardrails.md +2 -2
  7. data/docs/hooks.md +1 -1
  8. data/docs/internals/anthropic.md +4 -4
  9. data/docs/internals/attached-context.md +8 -6
  10. data/docs/internals/block-renderers.md +8 -1
  11. data/docs/internals/engine.md +220 -99
  12. data/docs/internals/inline-widgets.md +136 -0
  13. data/docs/internals/objectives.md +8 -6
  14. data/docs/internals/participants.md +5 -5
  15. data/docs/internals/prompt-caching.md +9 -5
  16. data/docs/internals/web-frontend.md +1 -1
  17. data/docs/internals/working-set.md +2 -1
  18. data/docs/plugins.md +2 -0
  19. data/docs/releasing.md +1 -1
  20. data/docs/sessions.md +2 -2
  21. data/docs/testing.md +1 -1
  22. data/lib/samagotchi/block_renderers.rb +5 -0
  23. data/lib/samagotchi/bundle_command/check.rb +1 -1
  24. data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
  25. data/lib/samagotchi/bundles/inline-widgets/manifest.yml +11 -0
  26. data/lib/samagotchi/bundles/inline-widgets/plugin.rb +76 -0
  27. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  28. data/lib/samagotchi/engine/context_judge.rb +19 -125
  29. data/lib/samagotchi/engine/guardrails.rb +4 -2
  30. data/lib/samagotchi/engine/messages.rb +9 -6
  31. data/lib/samagotchi/engine/objectives.rb +22 -90
  32. data/lib/samagotchi/engine/participants.rb +15 -10
  33. data/lib/samagotchi/engine/plugins.rb +3 -1
  34. data/lib/samagotchi/engine/questions.rb +1 -1
  35. data/lib/samagotchi/engine/side_requests.rb +5 -2
  36. data/lib/samagotchi/engine/toolkit.rb +33 -190
  37. data/lib/samagotchi/engine/turn_end.rb +1 -1
  38. data/lib/samagotchi/engine/turn_helpers.rb +16 -39
  39. data/lib/samagotchi/engine/verify.rb +7 -143
  40. data/lib/samagotchi/engine.rb +162 -99
  41. data/lib/samagotchi/engine_peers.rb +1 -1
  42. data/lib/samagotchi/event_fanout.rb +18 -12
  43. data/lib/samagotchi/guardrail_wiring.rb +83 -82
  44. data/lib/samagotchi/guardrails/gate.rb +41 -40
  45. data/lib/samagotchi/guardrails/pair_boundary.rb +2 -1
  46. data/lib/samagotchi/guardrails/review_boundary.rb +2 -1
  47. data/lib/samagotchi/guardrails/targets.rb +6 -4
  48. data/lib/samagotchi/guardrails_command.rb +1 -1
  49. data/lib/samagotchi/inline_widgets.rb +325 -0
  50. data/lib/samagotchi/kernel_loop.rb +190 -213
  51. data/lib/samagotchi/llm/backend.rb +3 -1
  52. data/lib/samagotchi/llm/chat_loop.rb +63 -41
  53. data/lib/samagotchi/llm/native_backend.rb +3 -2
  54. data/lib/samagotchi/llm_context_forget.rb +5 -3
  55. data/lib/samagotchi/model_binding.rb +3 -3
  56. data/lib/samagotchi/plugin/prompt_sections.rb +7 -0
  57. data/lib/samagotchi/question_desk.rb +1 -1
  58. data/lib/samagotchi/revise_anchor.rb +1 -1
  59. data/lib/samagotchi/system_prompt.rb +37 -30
  60. data/lib/samagotchi/testing/boundary.rb +7 -0
  61. data/lib/samagotchi/testing/bundle.rb +7 -5
  62. data/lib/samagotchi/testing/install.rb +3 -2
  63. data/lib/samagotchi/tool_activity.rb +8 -6
  64. data/lib/samagotchi/tool_declarations.rb +4 -4
  65. data/lib/samagotchi/tool_response.rb +4 -3
  66. data/lib/samagotchi/tool_runner.rb +91 -100
  67. data/lib/samagotchi/toolkit_binding.rb +40 -14
  68. data/lib/samagotchi/tools/deferred_tools.rb +17 -21
  69. data/lib/samagotchi/tools/registry.rb +43 -40
  70. data/lib/samagotchi/tools/tool_index.rb +135 -88
  71. data/lib/samagotchi/tools/tool_surface.rb +173 -0
  72. data/lib/samagotchi/turn_context.rb +67 -0
  73. data/lib/samagotchi/turn_stages/context_judge.rb +170 -0
  74. data/lib/samagotchi/turn_stages/objectives.rb +146 -0
  75. data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +261 -0
  76. data/lib/samagotchi/turn_stages/verify_anchors.rb +169 -0
  77. data/lib/samagotchi/turn_stages/warmup.rb +83 -0
  78. data/lib/samagotchi/turn_state.rb +46 -4
  79. data/lib/samagotchi/version.rb +1 -1
  80. data/lib/samagotchi/web/markdown_renderer.rb +39 -14
  81. data/lib/samagotchi/web/message_display.rb +1 -1
  82. data/lib/samagotchi/web/public/app.js +5 -1
  83. data/lib/samagotchi/web/public/block_host.js +74 -199
  84. data/lib/samagotchi/web/public/block_renderers.css +2 -0
  85. data/lib/samagotchi/web/public/block_source.js +7 -4
  86. data/lib/samagotchi/web/public/block_watch.js +256 -0
  87. data/lib/samagotchi/web/public/composer_fold.js +3 -0
  88. data/lib/samagotchi/web/public/index.html +28 -9
  89. data/lib/samagotchi/web/public/inline_widgets.js +146 -0
  90. data/lib/samagotchi/web/public/owner_verify.js +1 -1
  91. data/lib/samagotchi/web/public/participant.js +1 -1
  92. data/lib/samagotchi/web/public/revise_stack.js +1 -1
  93. data/lib/samagotchi/web/public/stage_watch.js +6 -2
  94. data/lib/samagotchi/web/public/text_roots.js +1 -1
  95. data/lib/samagotchi/web/public/verify_target.js +1 -1
  96. data/lib/samagotchi/web/public/widget_picks.js +130 -0
  97. data/lib/samagotchi/web/public/widget_walk.js +230 -0
  98. data/lib/samagotchi/web/public/widgets.css +70 -0
  99. metadata +17 -1
@@ -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 (stores, hooks, tools, the
428
- # mutes, the question flow, the gate, peers, warm-up, the hook runtime),
429
- # and the native backend over it. After the plugins load.
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
- # A given kernel (specs) gets the Engine's store, hooks and tools, as
432
- # the one built here does.
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
- @kernel.muted_memory_names = @muted_memory_names
444
- # ask_user_question blocks on the Engine's question flow (TUI/Web answer it).
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
- @kernel.warmup = @prompt_warmup
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
- hooks: -> { @hooks },
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#turn_mode).
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
- kernel: -> { @kernel },
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
- stop_toolkit_warmup
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
- announce_objectives(turn.session)
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 values, for #run_turn and its endings. Per turn and
782
- # single-threaded: the cross-thread turn state (the cancel slot, the
783
- # sink, steers) stays on the Engine.
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
- # +settings+: the kernel's LLM::TurnSettings, made in #prepare_turn and
786
- # set on the kernel in #generate.
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(:session, :prompt, :continue, :on_event, :controller, :origin, :started_at, :messages, :ended,
798
- :settings, :id, :limit, :resumed, :resume_from, :wall, :verify, :revises, :outcome) do
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
- warm_up_next_turn(turn, result)
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
- show_context_cards_after_turn
928
- # Only a completed turn shows its proposal cards: a canceled, failed
929
- # or rolled-back turn keeps none, and its proposals are dropped here
930
- # so the next turn does not show them either.
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 cross-thread state
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 = verified_anchor(session, VerifyAnchor.from(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 runs, with its controller and sink, in one step: a Stop
954
- # or a plugin's card from another thread never sees a running turn
955
- # without them. The idle recap detector does not fire (or render an
956
- # invalidated recap) while the model is working. A hook's notice goes
957
- # where the turn's events go (a one-shot prints only its sink; a
958
- # worker's observers carry it to the bridge).
959
- @turn_state.begin!(controller: effective_controller, sink: on_event, input_waiting: input_waiting)
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(session: session, prompt: continue ? nil : prompt, continue: continue, on_event: on_event,
966
- controller: effective_controller, origin: origin, started_at: Process.clock_gettime(Process::CLOCK_MONOTONIC),
967
- id: id || SecureRandom.uuid, wall: MessageTime.now, verify: VerifyAnchor.from(verify),
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.origin.author))
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 = start_toolkit_suggestion(turn)
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.settings = turn_settings(session)
1010
- refuse_images!(turn.settings.vision) unless image_refs.empty?
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&.abandon(:canceled)
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
- settle_toolkit_suggestion(turn, toolkit_suggestion)
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
- start_context_judge(turn)
1035
- audit_objectives_incoming(turn)
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&.abandon(:failed)
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.origin.author), MessageTime::KEY => turn.at }
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. The chat loop dispatches tools
1110
- # through the kernel without its #run: tag those dumps with this
1111
- # turn's model, not the last native one.
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
- @kernel.turn_settings = turn.settings.with(model_name: bare_for_backend, window_setting: model.window_setting,
1118
- llm_context: explained&.resolved, price: model.price,
1119
- loads_tools: loads_tools)
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#initialize sets @kernel.peers) and a delegate's approval relay
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 kernel [#call] → KernelLoop
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:, kernel:,
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
- @kernel = kernel
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); between turns
153
- # the next turn's (as #warm_up_next_turn: the turn's settings keep the
154
- # last turn's, stale after a /llm-context or /model change), the last
155
- # turn's when it can't be resolved.
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
- turn = kernel.respond_to?(:turn_settings) ? kernel.turn_settings&.llm_context : nil
158
- return turn if turn_running?
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 || turn
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 kernel = @kernel.call
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