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
@@ -46,7 +46,7 @@ module Samagotchi
46
46
  from_cwd: note[:from_cwd], text: note[:text], context_source: note[:context_source],
47
47
  created_at: note[:created_at], objectives: entry }.compact)
48
48
  announce_objectives(session) if entry
49
- objectives_audit.note(session, note)
49
+ @objectives_stage.note(session, note, recorder: system_one_recorder, state_dir: session_state_dir)
50
50
  message
51
51
  end
52
52
  end
@@ -76,14 +76,17 @@ module Samagotchi
76
76
  # register it on the kernel's llm_context_turn): the same array the loop
77
77
  # appends to, not a copy, so read it on the turn's thread (the pending
78
78
  # input drain, at an iteration boundary: TurnCheckpoint), or from another
79
- # only as a dup, read only (/objectives). nil when no
80
- # turn runs (llm_context_turn is never cleared, and still holds the
81
- # last turn's then).
79
+ # only as a dup, read only (/objectives). nil when no turn runs, and
80
+ # while the running turn's loop has yet to register its own
81
+ # (llm_context_turn is never cleared, and still holds the last turn's
82
+ # then: its turn_id tells them apart).
82
83
  # @return [Array<Hash>, nil]
83
84
  def live_conversation
84
- return nil unless turn_running? && @kernel.respond_to?(:llm_context_turn)
85
+ running = turn_context
86
+ return nil unless running && @kernel.respond_to?(:llm_context_turn)
85
87
 
86
- @kernel.llm_context_turn&.conversation
88
+ forget = @kernel.llm_context_turn
89
+ forget&.turn_id == running.id ? forget.conversation : nil
87
90
  end
88
91
 
89
92
  # Append messages to the current session's conversation.
@@ -228,13 +228,16 @@ module Samagotchi
228
228
  private
229
229
 
230
230
  # Pick the loop for +model+ (EffectiveModel); the chat loop is pointed
231
- # at its host's adapter (one per host, kept for its cached model list).
231
+ # at its host's adapter (one per host, kept for its cached model list)
232
+ # and client (the window probe, the re-prefill's host).
232
233
  def backend_for(model)
233
234
  return @native_backend unless model.chat?
234
235
 
235
236
  @chat_backend_mutex.synchronize do
236
- @chat_backend ||= LLM::ChatLoop.new(kernel: @kernel, stable_length: ->(text) { @prompt_builder.stable_length(text) })
237
+ @chat_backend ||= LLM::ChatLoop.new(kernel: @kernel, reprefill: @reprefill,
238
+ stable_length: ->(text) { @prompt_builder.stable_length(text) })
237
239
  @chat_backend.adapter = @host_registry.adapter_for(model.entry)
240
+ @chat_backend.client = model.client
238
241
  @chat_backend.session_id = session&.id
239
242
  @chat_backend
240
243
  end
@@ -2,26 +2,25 @@
2
2
 
3
3
  module Samagotchi
4
4
  class Engine
5
- # The session's objectives: their view announced when it changed, Check
6
- # this's side request, the model's proposals kept as cards until the turn
7
- # is over, System 1's audit of them (ObjectivesAudit). Writes
8
- # @objectives_announced, @objectives_audit; reads @objective_proposals
9
- # under @objective_proposals_mutex, @session, @side_judgments.
5
+ # The session's objectives (TurnStages::Objectives, forwarded): the view
6
+ # announced when it changed, the model's proposals kept as cards until
7
+ # the turn is over, System 1's audit of them (ObjectivesAudit); and
8
+ # Check this's side request. Reads @objectives_stage, @side_judgments,
9
+ # @host_registry, @session.
10
10
  module ObjectivesPart
11
- # The session's objectives as the page draws them (Objectives.view), when
12
- # that differs from the last announced: after an edit (its file written,
13
- # pending), once a list note is in, at a turn's end (its moves) and after
14
- # a rollback. A session with no list announces nothing. It only informs:
15
- # a failure is logged, never the turn's.
11
+ # The session's objectives as the page draws them, when that differs
12
+ # from the last announced (TurnStages::Objectives#announce), from any
13
+ # thread: end_turn's step, a command, Check this's thread, a note,
14
+ # a rollback. Under the event lock, which guards the last view: the
15
+ # sessions dir and the running turn's conversation read there, the
16
+ # event announced in the same step. It only informs: a failure is
17
+ # logged, never raised.
16
18
  def announce_objectives(session = @session)
17
19
  return unless session
18
20
 
19
21
  synchronize_events do
20
- view = Objectives.view(session.id, session.messages, state_dir: session_state_dir, running: live_conversation&.dup)
21
- next if view.nil? || view == @objectives_announced
22
-
23
- @objectives_announced = view
24
- announce({ type: :objectives_updated, session_id: session.id, **view })
22
+ event = @objectives_stage.announce(session, state_dir: session_state_dir, running: live_conversation)
23
+ announce(event) if event
25
24
  end
26
25
  rescue StandardError => e
27
26
  Log.warn(:turn, "objectives_announce_failed", error: e.class.name, msg: e.message)
@@ -38,79 +37,30 @@ module Samagotchi
38
37
  end
39
38
 
40
39
  # A change the model proposes (the objectives tool), kept until the turn
41
- # is over, as #context_choice's cards are: a card shown then is a quiet one, between turns, with its Apply kept
42
- # over a restart (CardStore), not a running turn's that says it waits on the
43
- # user. The same change twice in a turn is one card.
40
+ # is over (TurnStages::Objectives#propose).
44
41
  # @return [Boolean] false past Objectives::PROPOSALS_PER_TURN
45
- def propose_objective(proposal)
46
- @objective_proposals_mutex.synchronize do
47
- next true if @objective_proposals.any? { |p| p.line == proposal.line }
48
- next false if @objective_proposals.size >= Objectives::PROPOSALS_PER_TURN
49
-
50
- @objective_proposals << proposal
51
- true
52
- end
53
- end
54
-
55
- def show_objective_proposals
56
- drain_objective_proposals.each do |proposal|
57
- id = "objectives-#{SecureRandom.hex(4)}"
58
- show_card(source: Objectives::SOURCE, id: id, title: proposal.title, body: proposal.body,
59
- actions: [{ label: "Apply", command: proposal.action_line(id) },
60
- { label: "Dismiss", command: "/objectives --card #{id} dismiss" }])
61
- end
62
- rescue StandardError => e
63
- Log.warn(:turn, "objectives_proposal_failed", error: e.class.name, msg: e.message)
64
- end
65
- private :show_objective_proposals
66
-
67
- # A turn that did not complete (canceled, failed) takes its proposals
68
- # off the buffer without showing them: their Apply card would sit after a
69
- # turn whose `objectives propose` call is not in the history (a rollback
70
- # after Ctrl-C drops it), and leaving them in the buffer would show them
71
- # at the end of the next turn.
72
- def discard_objective_proposals(turn)
73
- dropped = drain_objective_proposals
74
- Log.info(:turn, "objectives_proposals_dropped", count: dropped.size, outcome: turn.outcome.to_s) unless dropped.empty?
75
- end
76
- private :discard_objective_proposals
77
-
78
- def drain_objective_proposals
79
- @objective_proposals_mutex.synchronize { @objective_proposals.slice!(0..) }
80
- end
81
- private :drain_objective_proposals
42
+ def propose_objective(proposal) = @objectives_stage.propose(proposal)
82
43
 
83
44
  private
84
45
 
85
- # System 1 on the objectives, in audit (ObjectivesAudit): the owner's turns
86
- # only, and for a message a person's: a reminder's or a delegate's prompt isn't one.
87
- def audit_objectives_incoming(turn)
88
- return unless turn.prompt.is_a?(String) && turn.origin.human? && OwnerPrompt.turn?(session: turn.session, author: turn.origin.author)
89
-
90
- objectives_audit.incoming(turn.session, turn.prompt)
91
- end
92
- private :audit_objectives_incoming
46
+ # The objectives' stage (TurnStages::Objectives).
47
+ attr_reader :objectives_stage
93
48
 
94
- def audit_objectives_outgoing(turn, result)
95
- return unless OwnerPrompt.turn?(session: turn.session, author: turn.origin.author)
96
-
97
- objectives_audit.outgoing(turn.session, result.text)
49
+ # However +turn+ ended (run_turn's ensure, after the turn flag is
50
+ # released): a completed turn's proposal cards shown
51
+ # (TurnStages::Objectives#finally). A card that fails is logged.
52
+ def show_objective_proposals(turn)
53
+ @objectives_stage.finally(turn).each { |card| show_card(**card) }
54
+ rescue StandardError => e
55
+ Log.warn(:turn, "objectives_proposal_failed", error: e.class.name, msg: e.message)
98
56
  end
99
- private :audit_objectives_outgoing
100
-
101
- # With saving off nothing would come of an ask, so none is made.
102
- def objectives_audit
103
- @objectives_audit ||= ObjectivesAudit.new(
104
- side: @side_judgments, state_dir: -> { session_state_dir },
105
- client_for: lambda do |judgment, session|
106
- next nil unless system_one_runs.on?
107
57
 
108
- system_one_recording(SystemOne::Client.from_config(host_registry: @host_registry, mode: ObjectivesAudit::MODE, tag: :system_one),
109
- judgment, session)
110
- end
111
- )
58
+ # The stage over this Engine's side asks (built in #initialize, once
59
+ # the host registry is). Each call takes its values: the recorder,
60
+ # the sessions dir, the running conversation.
61
+ def build_objectives_stage
62
+ TurnStages::Objectives.new(side_judgments: @side_judgments, host_registry: @host_registry)
112
63
  end
113
- private :objectives_audit
114
64
  end
115
65
  end
116
66
  end
@@ -5,13 +5,16 @@ module Samagotchi
5
5
  # The session's participants: the running turn's author, a pair turn's
6
6
  # stop check, who came or went, the system prompt's participants line and
7
7
  # a participant's author fields. Writes @participant_stop,
8
- # @participants_line; reads @participants, @participants_watch,
9
- # @guardrail_wiring.
8
+ # @participants_line; reads @participants, @participants_watch and the
9
+ # turn's TurnContext.
10
10
  module ParticipantsPart
11
11
  # The running turn's author when a participant wrote it, else nil
12
- # (ctx.turn_author, the hooks' author).
12
+ # (ctx.turn_author, the hooks' author, the judgments' participant:,
13
+ # ObjectivesCheck): one read of the published TurnContext, so any
14
+ # thread sees the running turn's author from its first moment, never
15
+ # the last turn's.
13
16
  # @return [String, nil]
14
- def turn_author = turn_running? ? @guardrail_wiring.turn_author : nil
17
+ def turn_author = turn_context&.author
15
18
 
16
19
  # Whether a participant's pair turn must stop, read before each of its
17
20
  # generations (#guard_participant_spend): author ("p:<id>") → why (their
@@ -20,7 +23,7 @@ module Samagotchi
20
23
  # nothing is checked mid-turn.
21
24
  attr_writer :participant_stop
22
25
 
23
- # Before each generation (KernelLoop#generation_guard): a pair turn whose
26
+ # Before each generation (KernelLoop::Services#generation_guard): a pair turn whose
24
27
  # participant's link ended, or whose spend (Engine#metrics: their
25
28
  # finished turns, this one's finished generations, their side requests)
26
29
  # passed their cap, stops on the plugins' stop path (cost-cap's,
@@ -28,10 +31,11 @@ module Samagotchi
28
31
  # once it ends, so a turn can pass the cap by its last generation. The
29
32
  # turn-start check is the Worker's (Worker#participant_refusal); a
30
33
  # review turn only reads, and keeps to it.
31
- def guard_participant_spend
32
- author = turn_author
34
+ # @param context [TurnContext] the running turn's (the loop's)
35
+ def guard_participant_spend(context)
36
+ author = context&.author
33
37
  return if author.nil? || @participant_stop.nil?
34
- return unless @guardrail_wiring.turn_mode == ParticipantMode::PAIR
38
+ return unless context.participant_mode == ParticipantMode::PAIR
35
39
 
36
40
  reason = @participant_stop.call(author) or return
37
41
  hook_stop_turn(reason, PARTICIPANTS_HOOK)
@@ -77,8 +81,9 @@ module Samagotchi
77
81
  # A participant's opening message keeps the rules it was delivered under
78
82
  # (ParticipantMode), stamped before any request carries it, so its header
79
83
  # never changes when the session's mode does.
80
- def participant_mode_fields(author)
81
- mode = Steer.participant_author?(author) && @guardrail_wiring.turn_mode
84
+ # @param context [TurnContext] the turn's
85
+ def participant_mode_fields(context)
86
+ mode = context.participant? && context.participant_mode
82
87
  mode ? { participant_mode: mode.to_s } : {}
83
88
  end
84
89
  end
@@ -101,11 +101,13 @@ module Samagotchi
101
101
  # Apply the staged tool sets, on the turn thread, before the turn's
102
102
  # system prompt is built (PluginTasks#apply_staged_tools!). A changed
103
103
  # set may change the chi group the search tool's description counts
104
- # (a plugin tool the toolkit leaves out).
104
+ # (a plugin tool the toolkit leaves out). Publishes the Registry it
105
+ # leaves (#publish_tools!).
105
106
  # @return [Boolean] whether the tools changed
106
107
  def apply_staged_tools!
107
108
  changed = @plugin_tasks.apply_staged_tools!
108
109
  @tool_index.refresh! if changed
110
+ publish_tools!
109
111
  changed
110
112
  end
111
113
  public :apply_staged_tools!
@@ -15,7 +15,7 @@ module Samagotchi
15
15
  end
16
16
 
17
17
  # Ask the model's question (ask_user_question): the kernel's
18
- # question_handler, called on the turn thread with the payload
18
+ # Services#question_handler, called on the turn thread with the payload
19
19
  # Tools::AskUserQuestion.validate made (QuestionDesk#request), after
20
20
  # the :question_asked hooks (QuestionAsked) gave the step its refs; a
21
21
  # --non-interactive run has no one to ask, so they don't fire there.
@@ -6,6 +6,7 @@ module Samagotchi
6
6
  # IdleClient, their usage in the session's side line, and System 1's
7
7
  # recording of judged asks. Writes @side_client/@side_client_key (under
8
8
  # @side_client_mutex); reads @metrics, @session, @host_registry.
9
+ # The recorder (SystemOne::Recorder) is the turn stages' recording seam.
9
10
  module SideRequestsPart
10
11
  # Count one side request (a recap, a plugin's ctx.ask_model: IdleClient's
11
12
  # on_usage) in the session's side line, apart from its turns' totals.
@@ -42,31 +43,43 @@ module Samagotchi
42
43
  def side_author(kind)
43
44
  # Check this is only ever the owner's, whichever turn runs as it ends.
44
45
  return nil if kind.to_s == ObjectivesCheck::SOURCE
45
- return nil unless turn_running? || kind.to_s == RECAP_SIDE_KIND
46
46
 
47
- @guardrail_wiring.turn_author
47
+ # One read each of the published contexts (TurnState): any thread.
48
+ running = turn_context
49
+ return running.author if running
50
+
51
+ kind.to_s == RECAP_SIDE_KIND ? last_turn_context&.author : nil
48
52
  end
49
53
 
50
54
  private
51
55
 
52
- # +client+ (nil for none) saving its asks as +judgment+'s runs. Anything
53
- # going wrong here leaves the client as it was: a run is never worth a
54
- # judgment.
55
- #
56
- # A scratch session's asks aren't saved; a participant's turn is never
57
- # judged, so it never comes here.
56
+ # +client+ (nil for none) saving its asks as +judgment+'s runs
57
+ # (SystemOne::Recorder#wrap, over a recorder built now): the
58
+ # guardrails' System 1 client. No client or a scratch session: the
59
+ # client as it is, with nothing read (no recorder built).
58
60
  # @param mode [String, nil] the use's mode, for its runs (Recording.wrap)
59
61
  def system_one_recording(client, judgment, session, mode: nil)
60
62
  return client if client.nil? || session&.scratch
61
63
 
62
- context = SystemOne::Recording::Context.of(session, session_model: session && effective_model_name)
63
- SystemOne::Recording.wrap(client, judgment: judgment, runs: system_one_runs, context: context, recorded_mode: mode)
64
- rescue StandardError => e
65
- Log.warn(:system_one, "system_one_recording_failed", error: e.class.name, msg: e.message.to_s[0, 200])
66
- client
64
+ system_one_recorder.wrap(client, judgment, session, mode: mode)
67
65
  end
68
66
  private :system_one_recording
69
67
 
68
+ # What a turn stage saves its System 1 asks with (SystemOne::Recorder):
69
+ # this Engine's runs and the model it names now, read at this call and
70
+ # never kept, so each stage call sees the model as it is then (an
71
+ # outgoing ask after a before_turn hook's switch, the new one). Never
72
+ # raises: on a failure, SystemOne::Recorder::NONE (no runs, the plain
73
+ # client), said once per failure as before.
74
+ # @return [SystemOne::Recorder]
75
+ def system_one_recorder
76
+ SystemOne::Recorder.new(runs: system_one_runs, session_model: effective_model_name)
77
+ rescue StandardError => e
78
+ SystemOne::Recorder.failed(e)
79
+ SystemOne::Recorder::NONE
80
+ end
81
+ private :system_one_recorder
82
+
70
83
  # Where this Engine's runs are saved; read each time, as the settings are.
71
84
  def system_one_runs = SystemOne::Runs.for_sessions_dir(session_state_dir)
72
85
  private :system_one_runs
@@ -4,9 +4,9 @@ module Samagotchi
4
4
  class Engine
5
5
  # The session's toolkit: the kits and what a turn declares
6
6
  # (ToolkitBinding, forwarded), ctx.load_tools and the staged loads a turn
7
- # appends, and System 1's toolkit suggestion (start, settle, warm-up).
8
- # Writes @toolkit_warmup_thread; reads @toolkit_binding, @tools,
9
- # @tool_index, @tool_loads, @side_judgments, @session.
7
+ # appends, and System 1's toolkit suggestion (TurnStages::ToolkitSuggestion,
8
+ # forwarded). Reads @toolkit_binding, @tools, @tool_index, @tool_loads,
9
+ # @toolkit_suggestion, @session.
10
10
  module ToolkitPart
11
11
  private
12
12
 
@@ -17,22 +17,33 @@ module Samagotchi
17
17
  def toolkit_explained(toolkit = nil) = @toolkit_binding.explained(toolkit)
18
18
  def toolkit_summary = @toolkit_binding.summary
19
19
 
20
+ # The Registry published for readers off the turn thread
21
+ # (ToolkitBinding#publish!): at the end of the Engine's build, after a
22
+ # turn's staged tool sets and its toolkit apply.
23
+ def publish_tools! = @toolkit_binding.publish!
24
+
25
+ # That published Registry (ToolkitBinding#published): what a reader
26
+ # off the turn thread (/toolkit) reads of the tools.
27
+ def published_tools = @toolkit_binding.published
28
+
20
29
  # The tools this session registered (Tools::Registry), declared or not.
21
30
  attr_reader :tools
22
31
 
23
- public :tools, :toolkit_kits, :resolved_toolkit, :toolkit_explained, :toolkit_summary
32
+ public :tools, :published_tools, :toolkit_kits, :resolved_toolkit, :toolkit_explained, :toolkit_summary
24
33
 
25
34
  # A plugin's ctx.load_tools: stage the reachable tools +names+ names
26
- # (Tools::ToolIndex#deferred_name) that the session neither declares
27
- # nor loaded with an earlier tool change, for bundle +bundle+. A name
28
- # that names no tool at all is logged; a declared or removed one is
29
- # skipped quietly.
35
+ # (Tools::ToolIndex#deferred_name, on the published Registry) that the
36
+ # session neither declares nor loaded with an earlier tool change, for
37
+ # bundle +bundle+. A name that names no tool at all is logged; a
38
+ # declared or removed one is skipped quietly.
30
39
  # @return [Array<String>] the wire names staged
31
40
  def load_tools(names, bundle)
32
41
  added = ToolLoads.added(@session&.messages)
42
+ published = published_tools
43
+ surface = Tools::ToolSurface.for(published)
33
44
  wanted = names.filter_map do |name|
34
- wire = @tool_index.deferred_name(name)
35
- Log.info(:plugins, "load_tools_skipped", bundle: bundle, tool: name) unless wire || @tools.key?(name)
45
+ wire = @tool_index.deferred_name(name, surface)
46
+ Log.info(:plugins, "load_tools_skipped", bundle: bundle, tool: name) unless wire || published.key?(name)
36
47
  wire
37
48
  end
38
49
  @tool_loads.stage(wanted - added, by: bundle)
@@ -79,191 +90,24 @@ module Samagotchi
79
90
  def apply_toolkit!(session, on_event = nil) = @toolkit_binding.apply!(session, on_event)
80
91
  private :apply_toolkit!
81
92
 
82
- # Start the toolkit classifier after :turn_started, for audit and on
83
- # mode alike (one eligibility, one snapshot, one thread). All session
84
- # and tool-registry reads happen here; the thread receives only this
85
- # snapshot. Audit never blocks: the thread logs its line. On mode
86
- # returns the ask's Pending, which the turn joins before apply_toolkit!
87
- # (#settle_toolkit_suggestion), with the turn's controller as the
88
- # request's cancel controller.
89
- # @return [ToolkitSuggest::Pending, nil] nil unless the turn joins an ask
90
- def start_toolkit_suggestion(turn)
91
- mode = Config.get("toolkit.suggest.mode").to_s
92
- return if mode == "off"
93
-
94
- joined = mode == "on"
95
- session = turn.session
96
- default = Toolkit.of(session.toolkit)
97
- log = toolkit_suggestion_log
98
- # The predicate checks the session first; the kits (memory kits are
99
- # read from disk) and the log (read whole) only when it gets there.
100
- kits = nil
101
- candidates = nil
102
- return unless ToolkitSuggest.eligible?(
103
- session: session, prompt: turn.prompt, author: turn.origin.author,
104
- candidate_count: lambda {
105
- kits = @toolkit_binding.kits
106
- candidates = ToolkitSuggest.candidates(kits, default_name: default.name)
107
- candidates.size
108
- },
109
- last_suggestion: -> { ToolkitSuggest.last_suggestion(log, session.id) },
110
- defer_turns: Config.get("toolkit.suggest.defer_turns"),
111
- defer_tokens: Config.get("toolkit.suggest.defer_tokens")
112
- )
113
-
114
- # One ask at a time: an eligible turn while the last one still runs
115
- # goes unasked, and says so.
116
- if @side_judgments.in_flight?(:toolkit)
117
- Log.info(:toolkit, "toolkit_suggest_in_flight_skipped", session_id: session.id)
118
- return
119
- end
120
-
121
- client = system_one_recording(SystemOne::Client.from_config(host_registry: @host_registry, mode: "on", tag: :toolkit),
122
- ToolkitSuggest::JUDGMENT, session, mode: Config.get("toolkit.suggest.mode").to_s)
123
- return unless client
124
-
125
- # The warm-up's only value is the cache: the real ask never queues
126
- # behind it.
127
- stop_toolkit_warmup
93
+ # System 1's toolkit suggestion (TurnStages::ToolkitSuggestion):
94
+ # prepare_turn starts, settles and abandons it; the Worker warms it.
95
+ attr_reader :toolkit_suggestion
96
+ private :toolkit_suggestion
128
97
 
129
- # Rules, not tool lists: this runs before await_init_tasks and
130
- # apply_staged_tools!, so a first turn's plugin/MCP tools aren't
131
- # registered yet; the report expands the rules (ToolkitSuggest.rules).
132
- snapshots = candidates.map do |kit|
133
- ToolkitSuggest::Candidate.new(name: kit.name.dup.freeze, description: kit.description.dup.freeze,
134
- rules: ToolkitSuggest.rules(Toolkit.resolve_kit(kit.name, kits)))
135
- end.freeze
136
- default_snapshot = ToolkitSuggest::Candidate.new(name: default.name.dup.freeze, description: nil,
137
- rules: ToolkitSuggest.rules(default.resolve(kits)))
138
- registered = @tools.entries(toolkit: nil).reject { |entry| entry.source == Tools::ToolIndex::SOURCE }
139
- core_tools, plugin_tools = registered.partition(&:core?).map { |entries| entries.map { |entry| entry.name.dup.freeze }.freeze }
140
- state = ToolkitSuggest.question(
141
- default: default, kits: kits, project: File.basename(session.project_root.to_s),
142
- prompt: turn.prompt
143
- ).last
144
- snapshot = ToolkitSuggest::Snapshot.new(
145
- session_id: session.id.dup.freeze, turn_number: ToolkitSuggest.turn_number(session.messages),
146
- default: default_snapshot, candidates: snapshots, core_tools: core_tools, plugin_tools: plugin_tools,
147
- state: { project: state[:project].dup.freeze, prompt: state[:prompt].dup.freeze }.freeze
148
- )
149
- threshold = Config.get("toolkit.suggest.threshold")
150
- runs = session.scratch ? nil : system_one_runs
151
- pending = ToolkitSuggest::Pending.new(log: log, joined: joined,
152
- acted: runs && ->(judged, did) { runs.acted(judged.fields[:run_id], did) },
153
- timeout_seconds: SystemOne::Client.configured_timeout_ms / 1000.0)
154
- # On mode: cut by a Stop while the turn waits (Pending#wait), not
155
- # by one after it went on; audit's ask is never cut.
156
- controller = joined ? pending.cancel_controller : nil
157
- @side_judgments.start(:toolkit, warn: ToolkitSuggest.method(:warn), finish: pending.method(:finish)) do
158
- ToolkitSuggest.judge(client: client, snapshot: snapshot, threshold: threshold,
159
- cancel_controller: controller)
160
- end
161
- joined ? pending : nil
162
- rescue StandardError => e
163
- ToolkitSuggest.warn(e)
164
- nil
165
- end
166
- private :start_toolkit_suggestion
167
-
168
- # On mode, before apply_toolkit!: wait for the ask (#start_toolkit_
169
- # suggestion) up to System 1's timeout from its start, or until a Stop,
170
- # and put the session on a candidate picked with confidence ≥ threshold
171
- # (ToolkitBinding#suggest: origin suggested, saved, the notice). Any
172
- # other outcome (the default, below threshold, undecided, no judgment,
173
- # a timeout, a Stop) leaves the session on its default. Nothing here
174
- # sends a request; prepare_turn's cancel check ends a stopped turn. A
175
- # Stop wins over an answer already in (Pending#wait checks it first).
176
- def settle_toolkit_suggestion(turn, pending)
177
- return if pending.nil?
178
-
179
- outcome, judged = pending.wait(turn.controller)
180
- if outcome == :timeout
181
- # The session starts on its default without a word otherwise: a
182
- # slow System 1 host (a local one) is the usual cause.
183
- timeout_ms = SystemOne::Client.configured_timeout_ms
184
- Log.warn(:toolkit, "toolkit_suggest_timeout", session_id: turn.session.id, timeout_ms: timeout_ms,
185
- reason: "System 1 gave no answer within system_one.timeout_ms")
186
- @toolkit_binding.fell_back(turn.session, "System 1 took over #{timeout_ms} ms")
187
- return
188
- end
189
- unless outcome == :answered
190
- Log.info(:toolkit, "toolkit_suggest_#{outcome}", session_id: turn.session.id)
191
- return
192
- end
193
- return if judged.nil?
194
-
195
- pending.record(toolkit_suggestion_outcome(turn.session, judged))
196
- rescue StandardError => e
197
- ToolkitSuggest.warn(e)
198
- pending&.record("failed")
199
- end
200
- private :settle_toolkit_suggestion
201
-
202
- # What the turn does with an answered ask, as the log line's did:
203
- # suggested (switched), chosen (the toolkit isn't the default any
204
- # more), unavailable (the kit is gone), defer or default.
205
- def toolkit_suggestion_outcome(session, judged)
206
- case judged.would
207
- when "suggested"
208
- return "chosen" unless session.toolkit_origin == "default"
209
- return "unavailable" unless @toolkit_binding.kits.key?(judged.suggestion.kit)
210
-
211
- switched = @toolkit_binding.suggest(session, judged.suggestion.kit, confidence: judged.suggestion.confidence,
212
- deferred: ToolkitSuggest.model_reply?(session.messages))
213
- switched ? "suggested" : "chosen"
214
- when "defer" then "defer"
215
- when "no_judgment"
216
- # System 1 failed (its own warn) or answered nothing usable.
217
- @toolkit_binding.fell_back(session, "System 1 gave no answer")
218
- "default"
219
- else "default"
220
- end
221
- end
222
- private :toolkit_suggestion_outcome
223
-
224
- # On mode, as an idle worker starts (Worker#start_background): one ask
225
- # with the real question and a stand-in prompt, on its own thread,
226
- # never logged and never waited for, so System 1's server has the
227
- # question's prefix cached for the first prompt. Only for a session a
228
- # first prompt could be asked for: origin default, no model reply, not
229
- # a delegate child or a review session, at least two candidates; and
230
- # not when the worker starts on a first prompt (that ask comes at once).
231
- def warm_toolkit_suggestion!
232
- return unless Config.get("toolkit.suggest.mode").to_s == "on"
233
-
234
- session = @session
235
- return if session.nil? || session.toolkit_origin != "default" || session.delegate? || session.review?
236
- return if ToolkitSuggest.model_reply?(session.messages) || !session.last_prompt.to_s.strip.empty?
237
-
238
- default = Toolkit.of(session.toolkit)
239
- kits = @toolkit_binding.kits
240
- return if ToolkitSuggest.candidates(kits, default_name: default.name).size < 2
241
-
242
- client = SystemOne::Client.from_config(host_registry: @host_registry, mode: "on", tag: :toolkit)
243
- return unless client
244
-
245
- project = File.basename(session.project_root.to_s)
246
- @toolkit_warmup_thread = Thread.new do
247
- ToolkitSuggest.warm(client: client, default: default, kits: kits, project: project)
248
- end
249
- rescue StandardError => e
250
- ToolkitSuggest.warn(e)
251
- nil
252
- end
98
+ # On mode, as an idle worker starts (Worker#start_background): the
99
+ # warm-up ask (TurnStages::ToolkitSuggestion#warm!).
100
+ def warm_toolkit_suggestion! = @toolkit_suggestion.warm!(@session)
253
101
  public :warm_toolkit_suggestion!
254
102
 
255
- # A still-running warm-up, stopped: a real ask (or the worker leaving)
256
- # doesn't wait behind it.
257
- def stop_toolkit_warmup
258
- @toolkit_warmup_thread&.kill
259
- @toolkit_warmup_thread = nil
260
- end
261
- private :stop_toolkit_warmup
262
-
263
- def toolkit_suggestion_log
264
- SideJudgments.log(ToolkitSuggest.log_path(session_state_dir), tag: :toolkit, event: "toolkit_suggest_log_failed")
103
+ # The stage over this Engine's side asks, binding and tools (built in
104
+ # #build_tools_and_plugins, before the plugins load). Its turn's start
105
+ # takes the recorder (#system_one_recorder) per call.
106
+ def build_toolkit_suggestion
107
+ TurnStages::ToolkitSuggestion.new(side_judgments: @side_judgments, host_registry: @host_registry,
108
+ toolkit_binding: @toolkit_binding, tools: @tools)
265
109
  end
266
- private :toolkit_suggestion_log
110
+ private :build_toolkit_suggestion
267
111
  end
268
112
  end
269
113
  end
@@ -117,7 +117,7 @@ module Samagotchi
117
117
  # a display version of the answer (AnswerDisplay) and event[:refs]
118
118
  # for refs on it (AnswerRefs).
119
119
  session = turn.session
120
- audit_objectives_outgoing(turn, result) unless canceled || result.exhausted?
120
+ @objectives_stage.outgoing(turn, result, recorder: system_one_recorder) unless canceled || result.exhausted?
121
121
  display = AnswerDisplay.new(session.messages)
122
122
  refs = AnswerRefs.new(session.messages, cwd: session.working_directory)
123
123
  after_turn = { type: :after_turn, status: canceled ? "canceled" : "completed",