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
@@ -2,137 +2,31 @@
2
2
 
3
3
  module Samagotchi
4
4
  class Engine
5
- # The attached-context candidate judge (ContextJudge): its pass at an
6
- # owner turn's start and for a late candidate, the asks it starts, and
7
- # the cards it shows after the turn. Reads @context_cards_after_turn
8
- # under @context_cards_mutex, @side_judgments, @host_registry, @session.
5
+ # The attached-context candidate judge (TurnStages::ContextJudge,
6
+ # forwarded): prepare_turn starts its pass, run_turn's ensure shows the
7
+ # cards it held, and a plugin's late candidate asks it. Reads
8
+ # @context_judge, @side_judgments, @host_registry.
9
9
  module ContextJudgePart
10
10
  private
11
11
 
12
- # The candidate judge (ContextJudge) at an owner turn's start, after
13
- # the toolkit's ask settled: the session's candidates' status changes
14
- # are logged (ContextCandidatesLog#look) and, unless
15
- # context.candidates.mode is off, the proposed candidates due an ask
16
- # are asked about the owner's prompts so far, this one included. Never
17
- # a participant's turn, a review session or a Continue (no new prompt).
18
- def start_context_judge(turn)
19
- session = turn.session
20
- return unless turn.prompt.is_a?(String) && OwnerPrompt.turn?(session: session, author: turn.origin.author)
21
-
22
- context_judge_pass(session, ContextCandidates.owner_texts(session.messages) + [turn.prompt], phase: "turn")
23
- end
24
- private :start_context_judge
12
+ # The judge's stage (TurnStages::ContextJudge).
13
+ attr_reader :context_judge
25
14
 
26
15
  # A late proposal no owner prompt mentions (Plugin::Host
27
- # #candidate_proposed: github-pr's init task runs beside the first
28
- # turn): judged on the owner's prompts so far, the running turn's too.
29
- def judge_late_candidates
30
- session = @session
31
- return if session.nil? || session.review?
32
-
33
- texts = ContextCandidates.owner_texts(plugin_messages)
34
- context_judge_pass(session, texts, phase: "late") unless texts.empty?
35
- end
36
- private :judge_late_candidates
37
-
38
- # One look at +session+'s candidates; the asks go to SideJudgments'
39
- # :context kind, one thread asking each due candidate in turn.
40
- def context_judge_pass(session, texts, phase:)
41
- return if session.id.nil? || session.scratch
42
-
43
- rows = ContextCandidates.with_status(session.id, project_root: session.project_root, state_dir: session_state_dir)
44
- return if rows.empty?
45
-
46
- log = ContextCandidatesLog.log_for(session_state_dir)
47
- records = ContextCandidatesLog.records_for(log, session.id)
48
- ContextCandidatesLog.look(log, session.id, rows, records) if phase == "turn"
49
- settings = ContextJudge::Settings.from_config
50
- return if settings.mode == "off"
51
-
52
- jobs = ContextJudge.jobs(rows, records, session_id: session.id, texts: texts, settings: settings, phase: phase,
53
- parent_attached: context_parent_attached(session))
54
- return if jobs.empty?
55
-
56
- client = system_one_recording(SystemOne::Client.from_config(host_registry: @host_registry, mode: "on", tag: :context),
57
- ContextJudge::JUDGMENT, session, mode: settings.mode)
58
- return unless client
59
-
60
- start_context_asks(client, jobs, log, settings, context_choice(session, log, settings))
61
- rescue StandardError => e
62
- ContextJudge.warn(e)
63
- end
64
- private :context_judge_pass
65
-
66
- # @return [Thread, nil] the asks' thread, nil when none started
67
- def start_context_asks(client, jobs, log, settings, choice)
68
- if @side_judgments.in_flight?(:context)
69
- Log.info(:context, "context_judge_in_flight_skipped", session_id: jobs.first.session_id)
70
- return nil
71
- end
72
-
73
- @side_judgments.start(:context, warn: ContextJudge.method(:warn)) do
74
- jobs.each { |job| choice.call(job, ContextJudge.ask(client: client, job: job, log: log, settings: settings)) }
75
- end
76
- end
77
- private :start_context_asks
78
-
79
- # What an answer does on the asks' thread (ContextCandidateChoice
80
- # #after_judged: on mode attaches or cards; audit nothing). Taken on
81
- # the caller's thread. The attach only writes the session's folder: its
82
- # note comes with the next between-turn delivery, never into a running
83
- # turn. A card shows only while this Engine is still on that session,
84
- # and not in a running turn (it isn't the turn's: no "waiting for you"
85
- # on it), but once it has ended.
86
- # @return [#call] (job, fields)
87
- def context_choice(session, log, settings)
88
- session_id = session.id
89
- project_root = session.project_root
90
- state_dir = session_state_dir
91
- show = lambda do |**card|
92
- current = @session
93
- next nil unless current.nil? || current.id == session_id
94
-
95
- @context_cards_mutex.synchronize do
96
- next @context_cards_after_turn << card if turn_running?
97
-
98
- show_card(**card)
99
- end
100
- end
101
- lambda do |job, fields|
102
- ContextCandidateChoice.after_judged(job: job, fields: fields, settings: settings, project_root: project_root,
103
- state_dir: state_dir, log: log, show_card: show)
104
- end
105
- end
106
- private :context_choice
107
-
108
- # The cards #context_choice held while a turn ran (#run_turn's ensure,
109
- # after the turn flag is released).
110
- def show_context_cards_after_turn
111
- cards = @context_cards_mutex.synchronize { @context_cards_after_turn.slice!(0..) }
112
- cards.each { |card| show_card(**card) }
113
- rescue StandardError => e
114
- ContextJudge.warn(e)
115
- end
116
- private :show_context_cards_after_turn
117
-
118
- # A delegate child's parent's sources, by title (else name): the
119
- # judge's started_from. nil for any other session (a chi send --new
120
- # session has no parent; a fork isn't a delegate).
121
- def context_parent_attached(session)
122
- return nil unless session.delegate? && session.parent_id
123
-
124
- parent_root = if Session.exist?(session.parent_id, state_dir: session_state_dir)
125
- Session.load(session.parent_id, state_dir: session_state_dir).project_root
126
- else
127
- session.project_root
128
- end
129
- ContextSources.attached(session.parent_id, project_root: parent_root, state_dir: session_state_dir)
130
- .map { |attached| attached.source.title || attached.name }
131
- rescue StandardError => e
132
- Log.warn(:context, "context_parent_attached_failed", error: e.class.name, msg: e.message.to_s[0, 200])
133
- []
16
+ # #candidate_proposed): TurnStages::ContextJudge#late_candidates.
17
+ def judge_late_candidates = @context_judge.late_candidates
18
+
19
+ # The stage over this Engine's side asks and its session (built in
20
+ # #build_tools_and_plugins, before the plugins load: one may propose a
21
+ # candidate as it loads).
22
+ def build_context_judge
23
+ TurnStages::ContextJudge.new(
24
+ side_judgments: @side_judgments, host_registry: @host_registry, state_dir: -> { session_state_dir },
25
+ recording: ->(client, judgment, session, mode:) { system_one_recording(client, judgment, session, mode: mode) },
26
+ session: -> { session }, messages: -> { plugin_messages }, turn_running: -> { turn_running? },
27
+ show_card: ->(**card) { show_card(**card) }
28
+ )
134
29
  end
135
- private :context_parent_attached
136
30
  end
137
31
  end
138
32
  end
@@ -76,11 +76,13 @@ module Samagotchi
76
76
  end
77
77
 
78
78
  # Ask the user to approve a call the gate voted `ask` on
79
- # (GuardrailWiring#request_approval).
79
+ # (GuardrailWiring#request_approval), for the running turn (its
80
+ # author), else the last one. The gate's own asks come with their
81
+ # call's TurnContext instead.
80
82
  # @param verdict [Guardrails::Verdict]
81
83
  # @return [Guardrails::Verdict]
82
84
  def request_approval(verdict)
83
- @guardrail_wiring.request_approval(verdict)
85
+ @guardrail_wiring.request_approval(verdict, turn_context || last_turn_context)
84
86
  end
85
87
  end
86
88
  end
@@ -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)
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.
@@ -2,30 +2,15 @@
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.
16
- def announce_objectives(session = @session)
17
- return unless session
18
-
19
- 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 })
25
- end
26
- rescue StandardError => e
27
- Log.warn(:turn, "objectives_announce_failed", error: e.class.name, msg: e.message)
28
- end
11
+ # The session's objectives as the page draws them, when that differs
12
+ # from the last announced (TurnStages::Objectives#announce).
13
+ def announce_objectives(session = @session) = @objectives_stage.announce(session)
29
14
 
30
15
  # Check this (ObjectivesCheck)'s one side request. Its usage is in the
31
16
  # session's side line. Raises as ctx.ask_model's request does.
@@ -38,79 +23,26 @@ module Samagotchi
38
23
  end
39
24
 
40
25
  # 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.
26
+ # is over (TurnStages::Objectives#propose).
44
27
  # @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
28
+ def propose_objective(proposal) = @objectives_stage.propose(proposal)
82
29
 
83
30
  private
84
31
 
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
93
-
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)
98
- 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
-
108
- system_one_recording(SystemOne::Client.from_config(host_registry: @host_registry, mode: ObjectivesAudit::MODE, tag: :system_one),
109
- judgment, session)
110
- end
32
+ # The objectives' stage (TurnStages::Objectives).
33
+ attr_reader :objectives_stage
34
+
35
+ # The stage over this Engine's side asks and its event lock (built in
36
+ # #initialize, once the host registry is).
37
+ def build_objectives_stage
38
+ TurnStages::Objectives.new(
39
+ side_judgments: @side_judgments, host_registry: @host_registry, state_dir: -> { session_state_dir },
40
+ recording: ->(client, judgment, session, mode:) { system_one_recording(client, judgment, session, mode: mode) },
41
+ runs: -> { system_one_runs }, synchronize: ->(&block) { synchronize_events(&block) },
42
+ announce: ->(event) { announce(event) }, running: -> { live_conversation },
43
+ show_card: ->(**card) { show_card(**card) }
111
44
  )
112
45
  end
113
- private :objectives_audit
114
46
  end
115
47
  end
116
48
  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.
@@ -42,9 +42,12 @@ module Samagotchi
42
42
  def side_author(kind)
43
43
  # Check this is only ever the owner's, whichever turn runs as it ends.
44
44
  return nil if kind.to_s == ObjectivesCheck::SOURCE
45
- return nil unless turn_running? || kind.to_s == RECAP_SIDE_KIND
46
45
 
47
- @guardrail_wiring.turn_author
46
+ # One read each of the published contexts (TurnState): any thread.
47
+ running = turn_context
48
+ return running.author if running
49
+
50
+ kind.to_s == RECAP_SIDE_KIND ? last_turn_context&.author : nil
48
51
  end
49
52
 
50
53
  private