samagotchi 0.154.1 → 0.156.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -1
  3. data/bin/chi +1 -1
  4. data/docs/architecture.md +12 -7
  5. data/docs/bundles.md +21 -0
  6. data/docs/cli.md +1 -1
  7. data/docs/guardrails.md +2 -2
  8. data/docs/hooks.md +1 -1
  9. data/docs/internals/anthropic.md +5 -5
  10. data/docs/internals/attached-context.md +9 -6
  11. data/docs/internals/background-tasks.md +1 -1
  12. data/docs/internals/block-renderers.md +8 -1
  13. data/docs/internals/engine.md +642 -831
  14. data/docs/internals/inline-widgets.md +136 -0
  15. data/docs/internals/llm-context-forget.md +8 -3
  16. data/docs/internals/objectives.md +8 -6
  17. data/docs/internals/participants.md +5 -5
  18. data/docs/internals/prompt-caching.md +13 -6
  19. data/docs/internals/remote-sessions.md +3 -1
  20. data/docs/internals/web-frontend.md +1 -1
  21. data/docs/internals/working-set.md +2 -1
  22. data/docs/plugins.md +2 -0
  23. data/docs/releasing.md +1 -1
  24. data/docs/sessions.md +4 -4
  25. data/docs/system-one.md +3 -2
  26. data/docs/testing.md +1 -1
  27. data/lib/samagotchi/block_renderers.rb +5 -0
  28. data/lib/samagotchi/bootstrap/config_writer.rb +1 -1
  29. data/lib/samagotchi/broadcast_command.rb +3 -3
  30. data/lib/samagotchi/bundle_command/check.rb +1 -1
  31. data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
  32. data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
  33. data/lib/samagotchi/bundles/inline-widgets/manifest.yml +11 -0
  34. data/lib/samagotchi/bundles/inline-widgets/plugin.rb +76 -0
  35. data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
  36. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  37. data/lib/samagotchi/child_move.rb +7 -51
  38. data/lib/samagotchi/children_status.rb +1 -1
  39. data/lib/samagotchi/cli/parent_wait.rb +1 -1
  40. data/lib/samagotchi/config.rb +17 -112
  41. data/lib/samagotchi/context_command.rb +1 -2
  42. data/lib/samagotchi/context_window.rb +3 -3
  43. data/lib/samagotchi/desktop/macos.rb +1 -1
  44. data/lib/samagotchi/engine/context_judge.rb +21 -119
  45. data/lib/samagotchi/engine/guardrails.rb +4 -2
  46. data/lib/samagotchi/engine/messages.rb +9 -6
  47. data/lib/samagotchi/engine/models.rb +5 -2
  48. data/lib/samagotchi/engine/objectives.rb +30 -80
  49. data/lib/samagotchi/engine/participants.rb +15 -10
  50. data/lib/samagotchi/engine/plugins.rb +3 -1
  51. data/lib/samagotchi/engine/questions.rb +1 -1
  52. data/lib/samagotchi/engine/side_requests.rb +26 -13
  53. data/lib/samagotchi/engine/toolkit.rb +35 -191
  54. data/lib/samagotchi/engine/turn_end.rb +1 -1
  55. data/lib/samagotchi/engine/turn_helpers.rb +16 -39
  56. data/lib/samagotchi/engine/verify.rb +7 -143
  57. data/lib/samagotchi/engine.rb +166 -98
  58. data/lib/samagotchi/engine_peers.rb +1 -1
  59. data/lib/samagotchi/event_fanout.rb +18 -12
  60. data/lib/samagotchi/guardrail_wiring.rb +83 -82
  61. data/lib/samagotchi/guardrails/gate.rb +41 -40
  62. data/lib/samagotchi/guardrails/pair_boundary.rb +2 -1
  63. data/lib/samagotchi/guardrails/review_boundary.rb +2 -1
  64. data/lib/samagotchi/guardrails/targets.rb +28 -11
  65. data/lib/samagotchi/guardrails_command.rb +1 -1
  66. data/lib/samagotchi/host_config.rb +72 -1
  67. data/lib/samagotchi/host_model.rb +2 -6
  68. data/lib/samagotchi/host_registry.rb +1 -1
  69. data/lib/samagotchi/inline_widgets.rb +325 -0
  70. data/lib/samagotchi/kernel_loop.rb +215 -313
  71. data/lib/samagotchi/llm/anthropic_wire.rb +2 -2
  72. data/lib/samagotchi/llm/backend.rb +3 -1
  73. data/lib/samagotchi/llm/chat_loop.rb +87 -49
  74. data/lib/samagotchi/llm/native_backend.rb +3 -2
  75. data/lib/samagotchi/llm_context_apply.rb +54 -1
  76. data/lib/samagotchi/llm_context_forget.rb +5 -3
  77. data/lib/samagotchi/llm_context_notice.rb +1 -1
  78. data/lib/samagotchi/llm_context_strategy.rb +7 -9
  79. data/lib/samagotchi/llm_context_view.rb +9 -0
  80. data/lib/samagotchi/model_binding.rb +3 -3
  81. data/lib/samagotchi/model_price.rb +4 -10
  82. data/lib/samagotchi/model_profile.rb +6 -6
  83. data/lib/samagotchi/model_settings.rb +37 -2
  84. data/lib/samagotchi/note_command.rb +1 -2
  85. data/lib/samagotchi/objectives_audit.rb +4 -3
  86. data/lib/samagotchi/participants.rb +2 -2
  87. data/lib/samagotchi/plugin/context.rb +1 -1
  88. data/lib/samagotchi/plugin/prompt_sections.rb +7 -0
  89. data/lib/samagotchi/plugin/sessions.rb +2 -5
  90. data/lib/samagotchi/plugin/tool_spec.rb +5 -3
  91. data/lib/samagotchi/project_env.rb +1 -1
  92. data/lib/samagotchi/question_desk.rb +1 -1
  93. data/lib/samagotchi/reasoning_history.rb +3 -3
  94. data/lib/samagotchi/reminder_carry.rb +1 -1
  95. data/lib/samagotchi/reprefill_meter.rb +45 -0
  96. data/lib/samagotchi/review_command.rb +1 -1
  97. data/lib/samagotchi/revise_anchor.rb +1 -1
  98. data/lib/samagotchi/sampling_settings.rb +3 -3
  99. data/lib/samagotchi/self_report.rb +7 -7
  100. data/lib/samagotchi/session.rb +50 -7
  101. data/lib/samagotchi/session_access/local.rb +7 -5
  102. data/lib/samagotchi/session_access/remote.rb +17 -3
  103. data/lib/samagotchi/session_archive_command.rb +2 -2
  104. data/lib/samagotchi/session_chain.rb +7 -6
  105. data/lib/samagotchi/session_continue.rb +6 -5
  106. data/lib/samagotchi/session_delete_command.rb +1 -1
  107. data/lib/samagotchi/session_manager/delivery.rb +254 -0
  108. data/lib/samagotchi/session_manager/emptiness.rb +138 -0
  109. data/lib/samagotchi/session_manager/lifecycle.rb +274 -0
  110. data/lib/samagotchi/session_manager/listing.rb +189 -0
  111. data/lib/samagotchi/session_manager/removal.rb +190 -0
  112. data/lib/samagotchi/session_manager/spawn.rb +242 -0
  113. data/lib/samagotchi/session_manager/worker_command.rb +157 -0
  114. data/lib/samagotchi/session_manager.rb +26 -1540
  115. data/lib/samagotchi/session_metrics.rb +1 -1
  116. data/lib/samagotchi/sessions_command.rb +1 -1
  117. data/lib/samagotchi/share_command.rb +1 -1
  118. data/lib/samagotchi/starting_marker.rb +65 -0
  119. data/lib/samagotchi/steer_cut_after.rb +3 -3
  120. data/lib/samagotchi/system_one/recorder.rb +49 -0
  121. data/lib/samagotchi/system_prompt.rb +37 -30
  122. data/lib/samagotchi/terminal_ui/exit_lines.rb +1 -1
  123. data/lib/samagotchi/terminal_ui.rb +2 -6
  124. data/lib/samagotchi/testing/boundary.rb +7 -0
  125. data/lib/samagotchi/testing/bundle.rb +7 -5
  126. data/lib/samagotchi/testing/install.rb +3 -2
  127. data/lib/samagotchi/testing/sessions.rb +1 -1
  128. data/lib/samagotchi/thinking.rb +3 -3
  129. data/lib/samagotchi/token_usage.rb +1 -1
  130. data/lib/samagotchi/tool_activity.rb +15 -7
  131. data/lib/samagotchi/tool_declarations.rb +6 -6
  132. data/lib/samagotchi/tool_output_cut.rb +17 -0
  133. data/lib/samagotchi/tool_response.rb +4 -3
  134. data/lib/samagotchi/tool_runner.rb +96 -102
  135. data/lib/samagotchi/toolkit_binding.rb +40 -14
  136. data/lib/samagotchi/toolkit_command.rb +1 -1
  137. data/lib/samagotchi/tools/deferred_tools.rb +17 -21
  138. data/lib/samagotchi/tools/delegate.rb +10 -5
  139. data/lib/samagotchi/tools/delegate_result.rb +4 -4
  140. data/lib/samagotchi/tools/index_hint.rb +14 -0
  141. data/lib/samagotchi/tools/registry.rb +64 -44
  142. data/lib/samagotchi/tools/send_note.rb +4 -6
  143. data/lib/samagotchi/tools/tool_index.rb +150 -102
  144. data/lib/samagotchi/tools/tool_surface.rb +187 -0
  145. data/lib/samagotchi/turn_context.rb +68 -0
  146. data/lib/samagotchi/turn_stages/context_judge.rb +172 -0
  147. data/lib/samagotchi/turn_stages/objectives.rb +150 -0
  148. data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +257 -0
  149. data/lib/samagotchi/turn_stages/verify_anchors.rb +168 -0
  150. data/lib/samagotchi/turn_stages/warmup.rb +82 -0
  151. data/lib/samagotchi/turn_state.rb +46 -4
  152. data/lib/samagotchi/version.rb +1 -1
  153. data/lib/samagotchi/vision_support.rb +3 -3
  154. data/lib/samagotchi/web/app.rb +15 -8
  155. data/lib/samagotchi/web/markdown_renderer.rb +39 -14
  156. data/lib/samagotchi/web/message_display.rb +1 -1
  157. data/lib/samagotchi/web/participant_access.rb +2 -2
  158. data/lib/samagotchi/web/public/app.js +5 -1
  159. data/lib/samagotchi/web/public/block_host.js +74 -199
  160. data/lib/samagotchi/web/public/block_renderers.css +2 -0
  161. data/lib/samagotchi/web/public/block_source.js +7 -4
  162. data/lib/samagotchi/web/public/block_watch.js +256 -0
  163. data/lib/samagotchi/web/public/composer_fold.js +3 -0
  164. data/lib/samagotchi/web/public/index.html +28 -9
  165. data/lib/samagotchi/web/public/inline_widgets.js +146 -0
  166. data/lib/samagotchi/web/public/owner_verify.js +1 -1
  167. data/lib/samagotchi/web/public/participant.js +1 -1
  168. data/lib/samagotchi/web/public/revise_stack.js +1 -1
  169. data/lib/samagotchi/web/public/stage_watch.js +6 -2
  170. data/lib/samagotchi/web/public/text_roots.js +1 -1
  171. data/lib/samagotchi/web/public/verify_target.js +1 -1
  172. data/lib/samagotchi/web/public/widget_picks.js +130 -0
  173. data/lib/samagotchi/web/public/widget_walk.js +230 -0
  174. data/lib/samagotchi/web/public/widgets.css +70 -0
  175. data/lib/samagotchi/web/session_access_routes.rb +3 -3
  176. data/lib/samagotchi/web/session_control.rb +3 -3
  177. data/lib/samagotchi/web/session_create.rb +3 -3
  178. data/lib/samagotchi/web/session_hub.rb +1 -1
  179. data/lib/samagotchi/web/session_show.rb +3 -3
  180. data/lib/samagotchi/web/session_summary.rb +3 -4
  181. data/lib/samagotchi/worker.rb +55 -27
  182. data/lib/samagotchi/worker_boot.rb +184 -0
  183. data/lib/samagotchi/worker_signals.rb +1 -1
  184. data/lib/samagotchi/worker_task_wakes.rb +2 -2
  185. metadata +29 -1
@@ -27,9 +27,11 @@ require_relative "thinking_notices"
27
27
  require_relative "cancellation_controller"
28
28
  require_relative "context_window"
29
29
  require_relative "kernel_loop"
30
+ require_relative "reprefill_meter"
30
31
  require_relative "tools/builtins"
31
32
  require_relative "tools/task_runtime"
32
33
  require_relative "tools/tool_index"
34
+ require_relative "tools/tool_surface"
33
35
  require_relative "peer_sources"
34
36
  require_relative "session_commands"
35
37
  require_relative "plugin/loader"
@@ -54,7 +56,13 @@ require_relative "context_judge"
54
56
  require_relative "objectives_audit"
55
57
  require_relative "owner_prompt"
56
58
  require_relative "side_judgments"
59
+ require_relative "system_one/recorder"
57
60
  require_relative "toolkit_suggest"
61
+ require_relative "turn_stages/context_judge"
62
+ require_relative "turn_stages/objectives"
63
+ require_relative "turn_stages/toolkit_suggestion"
64
+ require_relative "turn_stages/verify_anchors"
65
+ require_relative "turn_stages/warmup"
58
66
  require_relative "tool_loads"
59
67
  require_relative "answer_tail"
60
68
  require_relative "archive_store"
@@ -79,6 +87,7 @@ require_relative "tools/memory"
79
87
  require_relative "muted_memories"
80
88
  require_relative "used_memories"
81
89
  require_relative "turn_state"
90
+ require_relative "turn_context"
82
91
  require_relative "steer_cut"
83
92
  require_relative "engine_peers"
84
93
  require_relative "engine_snapshots"
@@ -206,8 +215,6 @@ module Samagotchi
206
215
  # steers) and the idle layer's activity clock. Built first: a plugin
207
216
  # may steer or ask whether a turn runs while it loads.
208
217
  @turn_state = TurnState.new(clock: -> { monotonic_now })
209
- @objective_proposals = []
210
- @objective_proposals_mutex = Mutex.new
211
218
  # A message for the running turn cutting its generation.
212
219
  @steer_cut = SteerCut.new(clock: -> { monotonic_now }, controller: -> { active_cancel_controller },
213
220
  cut_after: -> { steer_cut_after })
@@ -223,12 +230,11 @@ module Samagotchi
223
230
  @chat_backend_mutex = Mutex.new
224
231
  @side_judgments = SideJudgments.new
225
232
  @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
233
  @host_registry = host_registry || HostRegistry.new
234
+ # The turn stages over the objectives (its audit, view and proposals)
235
+ # and the Verify/Revise anchors (TurnStages).
236
+ @objectives_stage = build_objectives_stage
237
+ @verify_anchors = build_verify_anchors
232
238
  # A model given here (a worker's session model) is the one it runs,
233
239
  # so its host is checked; the config default is only checked once it
234
240
  # is used (the one-shot checks the model it starts on, #switch_model!).
@@ -302,6 +308,8 @@ module Samagotchi
302
308
  # The kernel (built here, or a spec's) and what it reads of the Engine;
303
309
  # then the native backend over it.
304
310
  wire_kernel(kernel, muted_memories)
311
+ # The turn-end warm-up's stage, over that kernel.
312
+ @turn_warmup = build_turn_warmup
305
313
  Log.debug(:model, "backend", provider: backend.provider) if Log.level?(:debug)
306
314
  @resume_session = session_id ? Session.load(session_id) : nil
307
315
  @prompt_builder = build_system_prompt(memories, appended_prompt_files)
@@ -379,6 +387,11 @@ module Samagotchi
379
387
  # The session's toolkit: the kits (the plugins' too, chi.toolkit),
380
388
  # what its next turn declares, the turn-start apply.
381
389
  @toolkit_binding = build_toolkit_binding
390
+ # The turn stages over it and the side asks (TurnStages): System 1's
391
+ # toolkit suggestion and the attached-context candidate judge, which a
392
+ # plugin may reach as it loads.
393
+ @toolkit_suggestion = build_toolkit_suggestion
394
+ @context_judge = build_context_judge
382
395
  # The tools plugins publish without declaring (chi.tool_source), and
383
396
  # the search and call tools that reach them (Tools::ToolIndex).
384
397
  @tool_index = Tools::ToolIndex.new
@@ -396,6 +409,8 @@ module Samagotchi
396
409
  # with no plugins too: a toolkit's left-out tools are reachable
397
410
  # either way (declared per toolkit, Tools::Registry).
398
411
  @tool_index.declare(@tools, failures: @plugin_failures)
412
+ # What readers off the turn thread see until the first turn's start.
413
+ publish_tools!
399
414
  end
400
415
  private :build_tools_and_plugins
401
416
 
@@ -424,45 +439,42 @@ module Samagotchi
424
439
  end
425
440
  private :subscribe_session_observers
426
441
 
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.
442
+ # The kernel and what it holds of the Engine for its life, one
443
+ # KernelLoop::Services (stores, hooks, tools, the mutes, the question
444
+ # flow, peers, the gate, warm-up, the generation guard), and the native
445
+ # backend over it. After the plugins load. Nothing per turn: a turn's
446
+ # TurnContext goes down the backend's complete (#generate).
430
447
  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.
448
+ # The mutes never change during a session: the kernel's memory_read
449
+ # guard reads the same list for every turn.
442
450
  @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.
451
+ # Where the approvals live; also where list_sessions and send_note
452
+ # look for other sessions (a worker moves it, Worker).
448
453
  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
454
  # The turn-end warm-up; the kernel asks it for a slot pin per request.
464
455
  @prompt_warmup = PromptWarmup.new
465
- @kernel.warmup = @prompt_warmup
456
+ # One re-prefill baseline for both loops (the native kernel's and the
457
+ # chat loop's requests), reset by the warm-up (KernelLoop#warmed_up!).
458
+ @reprefill = ReprefillMeter.new
459
+ services = KernelLoop::Services.new(
460
+ hooks: @hooks, tools: @tools, reminder_store: @reminder_store, muted_memory_names: @muted_memory_names,
461
+ # ask_user_question blocks on the Engine's question flow (TUI/Web answer it).
462
+ question_handler: proc { |payload| request_question(payload) },
463
+ # list_sessions and send_note speak for whichever session runs now.
464
+ peers: PeerView.new(self),
465
+ # Every tool call asks this gate first. The kernel is never rebuilt,
466
+ # so it holds across model switches.
467
+ gate: @guardrail_wiring.gate(hooks: @hooks, tools: @tools),
468
+ warmup: @prompt_warmup,
469
+ # A pair turn stops once its participant passed their cap.
470
+ generation_guard: ->(context) { guard_participant_spend(context) }
471
+ )
472
+ # A given kernel (specs) gets the same services as the one built here.
473
+ @kernel = kernel || KernelLoop.new(client: @client, profile: @model_binding.given_profile,
474
+ model_name: effective_model_name, services: services, reprefill: @reprefill)
475
+ @kernel.attach(services, reprefill: @reprefill) if kernel
476
+ sync_kernel_client!
477
+ sync_model_key!
466
478
  # What a hook can do beyond reading its event (event[:notify],
467
479
  # event[:ask_user], event[:stop_turn]): the Engine's routes to the UIs.
468
480
  @hooks.runtime = hook_runtime
@@ -476,11 +488,9 @@ module Samagotchi
476
488
  def build_guardrail_wiring
477
489
  GuardrailWiring.new(
478
490
  scratch: @scratch,
479
- hooks: -> { @hooks },
491
+ # Built before the tools (the hooks load into its failures).
480
492
  tools: -> { @tools },
481
493
  session: -> { @session },
482
- model_key: -> { @model_binding.model_key },
483
- model_name: -> { guardrail_model_name },
484
494
  cancelled: -> { !!active_cancel_controller&.cancelled? },
485
495
  # An approval of the model's call: :question_asked first (QuestionAsked).
486
496
  ask: ->(fields) { open_question(question_asked(QuestionAsked::KIND_APPROVAL, fields)) },
@@ -495,7 +505,7 @@ module Samagotchi
495
505
  system_one_runs: -> { system_one_runs },
496
506
  cancel_controller: -> { active_cancel_controller },
497
507
  # A participant's turn: their name on the owner's card, their
498
- # grant's mode for the turn's (GuardrailWiring#turn_mode).
508
+ # grant's mode for the turn's (GuardrailWiring#begin_turn).
499
509
  participant: ->(author) { @participants.entry_for(author, session: @session) }
500
510
  )
501
511
  end
@@ -511,7 +521,8 @@ module Samagotchi
511
521
  thinking_notices: -> { @thinking_notices },
512
522
  metrics: -> { @metrics },
513
523
  hooks: -> { @hooks },
514
- kernel: -> { @kernel },
524
+ turn_context: -> { turn_context },
525
+ last_turn_context: -> { last_turn_context },
515
526
  model_settings: -> { @model_settings },
516
527
  session: -> { @session },
517
528
  live_conversation: -> { live_conversation },
@@ -683,7 +694,7 @@ module Samagotchi
683
694
  end
684
695
  @side_judgments.join(SIDE_JUDGMENTS_JOIN_SECONDS)
685
696
  # The warm-up's answer is dropped anyway.
686
- stop_toolkit_warmup
697
+ @toolkit_suggestion.stop_warmup
687
698
  left = threads.count(&:alive?)
688
699
  Log.warn(:plugins, "anytime_commands_left", count: left) if left.positive?
689
700
  @plugin_tasks.kill_leftovers
@@ -715,9 +726,9 @@ module Samagotchi
715
726
  kept, event = yield(seconds)
716
727
  event[:turn_id] = turn.id
717
728
  event[:verify] = true if turn.verify
718
- revises = stamp_opening(kept, turn, outcome)
729
+ revises = @verify_anchors.stamp_opening(kept, turn, outcome)
719
730
  replace_session_messages(turn.session, kept) if kept
720
- verdict = record_verdict(turn, outcome)
731
+ verdict = @verify_anchors.record_verdict(turn, outcome)
721
732
  event[:verdict] = verdict if verdict
722
733
  event[:revises] = revises.to_h if revises
723
734
  emit_working_set(turn.on_event, turn.session, turn.session.messages)
@@ -778,14 +789,15 @@ module Samagotchi
778
789
  end
779
790
  private :build_recap
780
791
 
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.
792
+ # One turn's lifecycle state, for #run_turn and its endings. Per turn
793
+ # and single-threaded: the cross-thread turn state (the published
794
+ # TurnContext, the cancel slot, the sink, steers) stays in TurnState.
795
+ # +context+: the turn's TurnContext, the one source of truth for its
796
+ # id, session, origin and controller (read through #id, #session,
797
+ # #origin, #controller); #generate replaces it with the settings in.
784
798
  # +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.
799
+ # +prepared_settings+: the LLM::TurnSettings #prepare_turn makes (before
800
+ # the hooks); #generate completes them into the context's.
789
801
  # +limit+: the iteration limit #generate ran it with.
790
802
  # +resumed+: a step-limit Continue (run_turn's resumed:).
791
803
  # +resume_from+: where its reasoning starts, the run it resumes
@@ -794,8 +806,15 @@ module Samagotchi
794
806
  # +wall+: the wall time it started (MessageTime.now, once): the
795
807
  # :turn_started event's at and its user message's (and reminders') stamp.
796
808
  # +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
809
+ Turn = Struct.new(:context, :prompt, :continue, :on_event, :started_at, :messages, :ended, :prepared_settings,
810
+ :limit, :resumed, :resume_from, :wall, :verify, :revises, :outcome) do
811
+ # The turn's id (:turn_started's turn_id, its metrics record's id and
812
+ # its saved prompt's), session, origin and controller: its context's.
813
+ def id = context.id
814
+ def session = context.session
815
+ def origin = context.origin
816
+ def controller = context.controller
817
+
799
818
  def resumed? = resumed ? true : false
800
819
 
801
820
  # The turn ended as a completed one (not canceled, not failed): the
@@ -866,7 +885,7 @@ module Samagotchi
866
885
  # wake's note) names it with this id
867
886
  # @param verify [VerifyAnchor, nil] a Verify turn's claim (its verdict)
868
887
  # @param revises [ReviseAnchor, nil] a Revise turn's draft: stamped on
869
- # the prompt at the turn's end, metadata only (#stamp_opening)
888
+ # the prompt at the turn's end, metadata only (TurnStages::VerifyAnchors#stamp_opening)
870
889
  #
871
890
  # An Interrupt (SIGINT) cancels the turn: what it got to is kept in the
872
891
  # session as after a Stop (the prompt, the loop's finished tool
@@ -897,7 +916,7 @@ module Samagotchi
897
916
  pending_input: pending_input)
898
917
  publish_used_memories(session, on_event)
899
918
  complete_turn(turn, result)
900
- warm_up_next_turn(turn, result)
919
+ @turn_warmup.after(turn, result, next_turn_waiting: next_turn_waiting)
901
920
  result
902
921
  rescue Interrupt => e
903
922
  # After the end event (in the after_turn/session_end hooks) the
@@ -924,24 +943,23 @@ module Samagotchi
924
943
  raise
925
944
  ensure
926
945
  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)
946
+ @context_judge.show_cards_after_turn
947
+ # Only a completed turn shows its proposal cards; any other's are
948
+ # dropped, so the next turn does not show them either.
949
+ show_objective_proposals(turn)
932
950
  end
933
951
  end
934
952
 
935
953
  # 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.
954
+ # its TurnContext, the turn flag, its cancel controller and sink. The
955
+ # cross-thread state is set here and cleared in #release_turn only.
938
956
  def begin_turn(session, prompt, on_event:, cancel_controller:, origin:, continue:, id: nil, verify: nil, revises: nil,
939
957
  input_waiting: nil)
940
958
  origin = TurnOrigin.of(origin)
941
959
  # A participant's Verify fresh child: every turn is theirs (their
942
960
  # author, boundary and cap), whoever started it.
943
961
  origin = session.verify_fresh.origin_for(origin) if session.respond_to?(:verify_fresh) && session.verify_fresh
944
- verify = verified_anchor(session, VerifyAnchor.from(verify))
962
+ verify = @verify_anchors.verified(session, VerifyAnchor.from(verify), state_dir: session_state_dir)
945
963
  # Track the active session for recap and status snapshot.
946
964
  @session = session
947
965
  # status is turn state: running now, idle again before the turn's end
@@ -950,25 +968,62 @@ module Samagotchi
950
968
  @used_memories.absorb(session)
951
969
  # Provide a cancellable controller for this turn (cross-process cancel via file flag)
952
970
  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)
971
+ # The turn's context, whole before the turn is published: who queued
972
+ # it, a participant's mode (participants.json), git asked afresh.
973
+ context = new_turn_context(session, origin, effective_controller, id || SecureRandom.uuid)
974
+ # The turn runs, with its context, controller and sink, in one step:
975
+ # a Stop, a plugin's card or its ctx.turn_author from another thread
976
+ # never sees a running turn without them (or with the last turn's).
977
+ # The idle recap detector does not fire (or render an invalidated
978
+ # recap) while the model is working. A hook's notice goes where the
979
+ # turn's events go (a one-shot prints only its sink; a worker's
980
+ # observers carry it to the bridge).
981
+ @turn_state.begin!(context: context, controller: effective_controller, sink: on_event, input_waiting: input_waiting)
960
982
  # Drop a recap already in flight: the turn makes it stale.
961
983
  @recap&.invalidate!
962
- # The gate's context: who queued this turn, and git asked afresh.
963
- @guardrail_wiring.begin_turn(origin)
964
984
 
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))
985
+ Turn.new(context: context, prompt: continue ? nil : prompt, continue: continue, on_event: on_event,
986
+ started_at: Process.clock_gettime(Process::CLOCK_MONOTONIC), wall: MessageTime.now,
987
+ verify: VerifyAnchor.from(verify), revises: ReviseAnchor.from(revises))
969
988
  end
970
989
  private :begin_turn
971
990
 
991
+ # A new turn's TurnContext (#begin_turn): the gate's turn facts
992
+ # (GuardrailWiring#begin_turn), the session's folders. Its settings and
993
+ # model come in #generate.
994
+ def new_turn_context(session, origin, controller, id)
995
+ start = @guardrail_wiring.begin_turn(origin, session: session)
996
+ TurnContext.new(id: id, session: session, origin: origin, author: origin.participant_author,
997
+ participant_mode: start.mode, participant: start.participant, controller: controller,
998
+ cwd: (session.working_directory if session.respond_to?(:working_directory)), session_dir: turn_session_dir(session),
999
+ state_dir: session_state_dir, git: start.git, settings: nil, model_key: nil,
1000
+ fallback_model_key: nil, model_name: nil, surface: nil)
1001
+ end
1002
+ private :new_turn_context
1003
+
1004
+ # The session's folder, nil when it can't be named (an id that isn't
1005
+ # one, a lookup that raised): a cut tool output then says it wasn't saved.
1006
+ def turn_session_dir(session)
1007
+ Session.session_dir(session.id, state_dir: session_state_dir)
1008
+ rescue StandardError => e
1009
+ Log.warn(:turn, "tool_output_save_failed", error: e.class.name)
1010
+ nil
1011
+ end
1012
+ private :turn_session_dir
1013
+
1014
+ # The running turn's TurnContext as other threads see it (TurnState),
1015
+ # nil between turns; its settings are nil until #generate.
1016
+ # @return [TurnContext, nil]
1017
+ def turn_context = @turn_state.context
1018
+ private :turn_context
1019
+
1020
+ # The last finished turn's TurnContext (TurnState#last_context), nil
1021
+ # before any: its author (the recap's), its settings (the working set's
1022
+ # between turns).
1023
+ # @return [TurnContext, nil]
1024
+ def last_turn_context = @turn_state.last_context
1025
+ private :last_turn_context
1026
+
972
1027
  # Everything before the model is asked: :turn_started, the kernel's
973
1028
  # per-turn settings, the plugins' setup, the session_start/before_turn
974
1029
  # hooks, then the messages to send (#turn_messages). False when a Stop
@@ -987,14 +1042,14 @@ module Samagotchi
987
1042
  # A participant's turn names them (author, author_name) for the UIs'
988
1043
  # badge; the owner's turn_started stays as it was.
989
1044
  turn_started.merge!(author_fields(turn.origin.author, session: session),
990
- participant_mode_fields(turn.origin.author))
1045
+ participant_mode_fields(turn.context))
991
1046
  emit_event(turn.on_event, turn.tag(turn_started))
992
1047
  raise image_error if image_error
993
1048
 
994
1049
  # On mode's ask starts here too; the turn joins it before
995
1050
  # apply_toolkit! below, so its latency hides behind the probes and
996
1051
  # the init wait.
997
- toolkit_suggestion = start_toolkit_suggestion(turn)
1052
+ @toolkit_suggestion.start(turn, recorder: system_one_recorder)
998
1053
 
999
1054
  # The window and the profile probes run after run_turn's probe-cancel
1000
1055
  # swap, so a Stop cuts them; a failure ends the turn as turn_failed.
@@ -1006,12 +1061,12 @@ module Samagotchi
1006
1061
  refresh_profile!
1007
1062
 
1008
1063
  # 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?
1064
+ turn.prepared_settings = turn_settings(session)
1065
+ refuse_images!(turn.prepared_settings.vision) unless image_refs.empty?
1011
1066
  # A Stop during the probes: the turn ends before the load-failure
1012
1067
  # warnings, which show once, so the next turn still shows them.
1013
1068
  if turn.controller.cancelled?
1014
- toolkit_suggestion&.abandon(:canceled)
1069
+ @toolkit_suggestion.abandon(turn, :canceled)
1015
1070
  return false
1016
1071
  end
1017
1072
 
@@ -1028,11 +1083,11 @@ module Samagotchi
1028
1083
  apply_staged_tools!
1029
1084
  # On mode: the suggested kit, before anything is declared (a Stop
1030
1085
  # during the wait is caught by the check below).
1031
- settle_toolkit_suggestion(turn, toolkit_suggestion)
1086
+ @toolkit_suggestion.settle(turn)
1032
1087
  # The candidate judge: its asks run beside the turn; on mode's
1033
1088
  # attaches reach the model at the next between-turn delivery.
1034
- start_context_judge(turn)
1035
- audit_objectives_incoming(turn)
1089
+ @context_judge.start(turn, recorder: system_one_recorder)
1090
+ @objectives_stage.incoming(turn, recorder: system_one_recorder)
1036
1091
  apply_toolkit!(session, turn.on_event)
1037
1092
  refresh_participants!(session)
1038
1093
  # A Stop during the waits above: the turn ends here,
@@ -1055,7 +1110,8 @@ module Samagotchi
1055
1110
  ensure
1056
1111
  # A turn that ended (a failure, an Interrupt) before it settled the
1057
1112
  # ask: its line says so. Nothing once it was settled.
1058
- toolkit_suggestion&.abandon(:failed)
1113
+ @toolkit_suggestion.abandon(turn, :failed)
1114
+ @toolkit_suggestion.finish(turn)
1059
1115
  end
1060
1116
  private :prepare_turn
1061
1117
 
@@ -1087,7 +1143,7 @@ module Samagotchi
1087
1143
 
1088
1144
  unless turn.continue
1089
1145
  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 }
1146
+ **participant_mode_fields(turn.context), MessageTime::KEY => turn.at }
1091
1147
  user_message[:verify] = turn.verify.to_h if turn.verify
1092
1148
  user_message[:images] = image_refs unless image_refs.empty?
1093
1149
  turn.messages << user_message
@@ -1106,17 +1162,28 @@ module Samagotchi
1106
1162
  # Route model name as bare (without host prefix) to the server;
1107
1163
  # host selection already done via active client.
1108
1164
  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.
1165
+ # The turn's settings, set once here into its context (with the model
1166
+ # key and name the gate and memory_read use: frozen now, after the
1167
+ # hooks that may switch the model), which goes down the backend's
1168
+ # complete to every tool call, and is published (TurnState).
1112
1169
  explained = model.llm_context_explained(@model_settings.llm_context_session_args)
1113
1170
  model_backend = backend(model)
1114
1171
  # A host that loads tools runs a direct call of one the session
1115
1172
  # reaches but doesn't declare (ToolRunner, KernelLoop#dispatch).
1116
1173
  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)
1174
+ settings = turn.prepared_settings.with(model_name: bare_for_backend, window_setting: model.window_setting,
1175
+ llm_context: explained&.resolved, price: model.price,
1176
+ loads_tools: loads_tools)
1177
+ # The turn's tools (Tools::ToolSurface), built once from the Registry
1178
+ # under those settings' layers and loads_tools: what the loops declare,
1179
+ # defer and dispatch until the turn ends; a change meanwhile (a
1180
+ # /toolkit change, a staged chi.replace_tools) is the next turn's.
1181
+ surface = Tools::ToolSurface.for(@tools, layers: settings.llm_context&.active_layers || [],
1182
+ loads_tools: loads_tools)
1183
+ turn.context = turn.context.with(settings: settings, model_key: @model_binding.model_key,
1184
+ fallback_model_key: @model_binding.fallback_model_key,
1185
+ model_name: guardrail_model_name, surface: surface)
1186
+ @turn_state.replace_context(turn.context)
1120
1187
  log_llm_context(explained)
1121
1188
 
1122
1189
  turn.limit = @no_interrupt ? IterationLimit.for(no_interrupt: true) : max_iterations || IterationLimit.for
@@ -1133,7 +1200,8 @@ module Samagotchi
1133
1200
  model_name: bare_for_backend,
1134
1201
  max_tool_output_chars: max_tool_output_chars,
1135
1202
  pending_input: turn_drain(pending_input),
1136
- reasoning_since: reasoning_since
1203
+ reasoning_since: reasoning_since,
1204
+ context: turn.context
1137
1205
  )
1138
1206
  result.run_start = run_start
1139
1207
  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