Module: Pikuri::Agent::Synthesizer
- Defined in:
- lib/pikuri/agent/synthesizer.rb
Overview
Step-exhaustion rescue. When an Agent‘s Listener::StepLimit trips, Agent#run_loop catches the Exceeded exception and hands off to Synthesizer.run so the run still produces something useful — a tools-free assistant turn that answers the user’s question from whatever evidence the failed agent collected before running out of budget.
Why this exists
Without a rescue, a step-exhausted run just raises a stack trace past bin/pikuri-chat and the user gets nothing despite the agent having gathered useful information in the first N-1 steps. The observed failure mode is the “wait, but what about X?” death-loop: the agent collects sound evidence in the first few rounds, then spends the rest of the budget second-guessing. By the time the cap trips, the answer is largely in the messages — it just needs a tools-free pass to synthesize.
Seam discipline
Synthesizer.run does not reference RubyLLM::*. Agent constructs the synth chat itself (the one RubyLLM.chat call lives in lib/agent.rb, same as the parent chat) and passes it in. Synthesizer only calls instance methods on whatever chat it receives — #with_instructions and #ask — so the seam stays at three files.
Constant Summary collapse
- SYSTEM_PROMPT =
The synthesizer’s system prompt. Strict and short: use the evidence, don’t apologize, admit gaps when present.
<<~PROMPT You are given evidence another agent collected before running out of steps. Answer the user's question using only this evidence. You have no tools. If the evidence is insufficient, state plainly what's missing and what partial answer you can give. Do not apologize or comment on the previous agent. PROMPT
Class Method Summary collapse
-
.build_prompt(parent_messages:, user_message:) ⇒ String
Render the user’s question plus an “Evidence gathered” section built from
parent_messagesas a single prompt string. -
.run(chat:, parent_messages:, user_message:, listeners:) ⇒ String?
Configure
chatfor synthesis, run one turn against it, and return the final assistant content.
Class Method Details
.build_prompt(parent_messages:, user_message:) ⇒ String
Render the user’s question plus an “Evidence gathered” section built from parent_messages as a single prompt string. Pure function — no I/O, safe to test directly with fixture messages.
77 78 79 80 |
# File 'lib/pikuri/agent/synthesizer.rb', line 77 def self.build_prompt(parent_messages:, user_message:) transcript = format_evidence() "Question: #{}\n\nEvidence gathered:\n#{transcript}" end |
.run(chat:, parent_messages:, user_message:, listeners:) ⇒ String?
Configure chat for synthesis, run one turn against it, and return the final assistant content. Listeners are attached so the synth’s reasoning and answer flow through the same surface the parent agent uses — terminal renders them inline, an in-memory recorder picks them up, and a future web sink sees them as normal Message variants.
63 64 65 66 67 68 |
# File 'lib/pikuri/agent/synthesizer.rb', line 63 def self.run(chat:, parent_messages:, user_message:, listeners:) chat.with_instructions(SYSTEM_PROMPT) listeners.attach(chat) chat.ask(build_prompt(parent_messages: , user_message: )) chat..reverse.find { |m| m.role == :assistant }&.content end |