Restore progressive agent output and reduce repeated narration

Forward text and reasoning events as they arrive while retaining canonical history without replaying output. Tighten shared communication guidance and add streaming-order regression coverage. Include the staged dependency refresh.
This commit is contained in:
2026-10-04 19:12:58 -05:00
parent b29882d56c
commit 447113bdd0
5 changed files with 450 additions and 383 deletions
Generated
+318 -329
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1724,7 +1724,7 @@ pub(crate) fn tool_name_is_supported(name: &str, supported: &HashSet<api::ToolTy
/// Shared by direct-provider prompts and compatibility/background request prompts. /// Shared by direct-provider prompts and compatibility/background request prompts.
pub(crate) const CODING_DELEGATION_GUIDANCE: &str = "## Coding Delegation\nFor coding tasks, actively look for bounded implementation or review work to offload to available subagents early. Prefer delegation for substantial independent changes or a focused review of completed work; give the child the known files, just enough context, an explicit implementation or research role, the expected deliverable, and a validation check. The parent owns integration and reviews the result, returning corrections to the same child when needed. Do not duplicate delegated implementation while it runs. Keep trivial edits and tightly dependent steps local when delegation would add delay. Research and plan-mode assignments remain read-only. Delegate only when delegation tools are advertised; child agents are leaf workers and must not spawn other agents.\n\n## Dueling Models Review\nAfter a substantial coherent set of authorized code changes, the parent must request an independent read-only subagent review before reporting completion. Skip this workflow for trivial edits. Always select an available reviewer model; provider or model diversity is a preference, never a reason to skip review. Prefer a capable reviewer with a different model and a different provider from the implementer, using only the advertised model catalog. If no suitable different-provider option exists, select the next best capable different model; if only one model is available, use a separate reviewer conversation and disclose that limitation. Do not invent model IDs or provider diversity. Give the reviewer the requirements, changed files and diff scope, implementation model ID, completed checks, and known uncertainties. Ask for concrete correctness, regression, security, or missing-validation findings with file references and evidence; distinguish blocking findings from optional suggestions. The reviewer must inspect the current code and return findings or explicit LGTM, never rubber-stamp a summary. The parent evaluates findings, sends actionable corrections to the same implementer, and sends the revised diff and validation evidence back to the same reviewer. Continue repair and re-review until the reviewer returns LGTM for the current changes. Changes after approval need review again. Do not force stylistic churn, hide disagreements, or claim LGTM without an actual reviewer response. If verification is unavailable or a disagreement requires user input, report the concrete blocker and remaining findings honestly. The parent coordinates this loop; leaf implementers report readiness for parent review and never spawn reviewers themselves.\n\n"; pub(crate) const CODING_DELEGATION_GUIDANCE: &str = "## Coding Delegation\nFor coding tasks, actively look for bounded implementation or review work to offload to available subagents early. Prefer delegation for substantial independent changes or a focused review of completed work; give the child the known files, just enough context, an explicit implementation or research role, the expected deliverable, and a validation check. The parent owns integration and reviews the result, returning corrections to the same child when needed. Do not duplicate delegated implementation while it runs. Keep trivial edits and tightly dependent steps local when delegation would add delay. Research and plan-mode assignments remain read-only. Delegate only when delegation tools are advertised; child agents are leaf workers and must not spawn other agents.\n\n## Dueling Models Review\nAfter a substantial coherent set of authorized code changes, the parent must request an independent read-only subagent review before reporting completion. Skip this workflow for trivial edits. Always select an available reviewer model; provider or model diversity is a preference, never a reason to skip review. Prefer a capable reviewer with a different model and a different provider from the implementer, using only the advertised model catalog. If no suitable different-provider option exists, select the next best capable different model; if only one model is available, use a separate reviewer conversation and disclose that limitation. Do not invent model IDs or provider diversity. Give the reviewer the requirements, changed files and diff scope, implementation model ID, completed checks, and known uncertainties. Ask for concrete correctness, regression, security, or missing-validation findings with file references and evidence; distinguish blocking findings from optional suggestions. The reviewer must inspect the current code and return findings or explicit LGTM, never rubber-stamp a summary. The parent evaluates findings, sends actionable corrections to the same implementer, and sends the revised diff and validation evidence back to the same reviewer. Continue repair and re-review until the reviewer returns LGTM for the current changes. Changes after approval need review again. Do not force stylistic churn, hide disagreements, or claim LGTM without an actual reviewer response. If verification is unavailable or a disagreement requires user input, report the concrete blocker and remaining findings honestly. The parent coordinates this loop; leaf implementers report readiness for parent review and never spawn reviewers themselves.\n\n";
pub(crate) const PROGRESS_COMMUNICATION_GUIDANCE: &str = "## Progress Updates\nWork quietly while tools run. Tool panes provide activity visibility; prose updates are not required to prove that work continues. Before sending any update, compare it with recent assistant messages and the conversation summary. Speak during work only when you need a user decision, encounter a consequential blocker, or have a material finding the user needs to act on now. A completed internal step, tool result, poll, or resumed context alone is not a reason to speak. Never repeat an acknowledgement, promise, plan, or previous status in different words. Continue with the next necessary tool call when nothing actionable needs saying. Answer direct user status questions, and give a self-contained final result when the task ends.\n\n"; pub(crate) const PROGRESS_COMMUNICATION_GUIDANCE: &str = "## Progress Updates\nFor an active implementation task, work quietly: the normal response to a tool result is the next necessary tool call, with no assistant text. Tool panes show activity. Do not greet, acknowledge instructions, announce intentions, restate the request or plan, recap each completed step, or reassure the user that work continues. This applies equally to the parent and subagents, follow-up instructions, tool results, command polls, and restored or compacted context. These all continue the same task; they do not start a new introduction.\nDuring work, send prose only to ask a necessary user decision, report a concrete blocker that requires user action, or flag a newly discovered urgent consequence the user must act on now. Otherwise save findings for the final result. Before speaking, compare the intended factual content with prior assistant messages and the conversation summary. If it adds no new actionable information, omit it entirely; paraphrasing a previous update still counts as repetition. Track what has already been communicated using that history, without extra tracking tools or files.\nExamples: after a read/search result, call the next tool without saying 'Got it' or 'I will inspect the files'; after a successful edit, run the relevant check without saying 'The change is in place'; while waiting for a command or subagent, use its wait tool without saying 'Still working'; after approval or a follow-up correction, perform the authorized work without another acknowledgement. Do not stop with a promise, an update, or a repeated question when work can continue. Answer explicit user questions, including status requests, briefly and directly, then continue. When finished, report the final outcome once.\n\n";
pub(crate) const IMPLEMENTATION_PROGRESS_GUIDANCE: &str = "## Incremental Implementation\nFor an authorized implementation task, reuse the parent handoff's paths, findings, constraints, and acceptance criteria. Search until you have just enough context to make the requested change. Read the immediate implementation and a relevant example or test as needed, then make the smallest coherent change. The parent can review and request follow-up changes; do not try to anticipate every possible review concern before editing, and still validate your own work. Validate that slice before expanding the investigation. Do not inventory every related subsystem before editing. Further discovery must resolve a concrete uncertainty that affects the next change. When a local code change is reversible and a focused build or test can settle an uncertainty, make a small, evidence-based attempt and use the result to refine it instead of searching for complete certainty. Do not guess APIs or edit files you have not inspected. Read known files directly and batch independent focused reads; avoid repeating discovery already supplied by the parent. Prefer the nearest relevant check before a full build. Investigate first when a mistake could destroy data, affect production, expose credentials, or create irreversible external effects. For review-only work, inspect the changed code and its immediate dependencies, then report actionable findings; expand the search only to test a specific suspected defect. For delegated implementation work, include known paths and symbols, established findings, changes to preserve, a concrete first change, and its acceptance check; distinguish known facts from unanswered questions. Do not invent missing discoveries. Research-only, review, and planning requests remain read-only unless the user authorizes changes.\n\n"; pub(crate) const IMPLEMENTATION_PROGRESS_GUIDANCE: &str = "## Incremental Implementation\nFor an authorized implementation task, reuse the parent handoff's paths, findings, constraints, and acceptance criteria. Search until you have just enough context to make the requested change. Read the immediate implementation and a relevant example or test as needed, then make the smallest coherent change. The parent can review and request follow-up changes; do not try to anticipate every possible review concern before editing, and still validate your own work. Validate that slice before expanding the investigation. Do not inventory every related subsystem before editing. Further discovery must resolve a concrete uncertainty that affects the next change. When a local code change is reversible and a focused build or test can settle an uncertainty, make a small, evidence-based attempt and use the result to refine it instead of searching for complete certainty. Do not guess APIs or edit files you have not inspected. Read known files directly and batch independent focused reads; avoid repeating discovery already supplied by the parent. Prefer the nearest relevant check before a full build. Investigate first when a mistake could destroy data, affect production, expose credentials, or create irreversible external effects. For review-only work, inspect the changed code and its immediate dependencies, then report actionable findings; expand the search only to test a specific suspected defect. For delegated implementation work, include known paths and symbols, established findings, changes to preserve, a concrete first change, and its acceptance check; distinguish known facts from unanswered questions. Do not invent missing discoveries. Research-only, review, and planning requests remain read-only unless the user authorizes changes.\n\n";
+38 -41
View File
@@ -753,6 +753,18 @@ impl ProviderRunCoordinator {
return Ok(()); return Ok(());
} }
buffer.text.push_str(&text); buffer.text.push_str(&text);
if !self
.project_or_fail_acknowledged(
ProviderRunProjection::ModelEvent {
work_id: call.work_id.clone(),
event: AgentEvent::TextDelta { text },
},
project,
)
.await?
{
return Ok(());
}
} }
AgentEvent::ReasoningDelta { text } => { AgentEvent::ReasoningDelta { text } => {
if !self if !self
@@ -764,6 +776,18 @@ impl ProviderRunCoordinator {
return Ok(()); return Ok(());
} }
buffer.reasoning.push_str(&text); buffer.reasoning.push_str(&text);
if !self
.project_or_fail_acknowledged(
ProviderRunProjection::ModelEvent {
work_id: call.work_id.clone(),
event: AgentEvent::ReasoningDelta { text },
},
project,
)
.await?
{
return Ok(());
}
} }
AgentEvent::ReasoningCompleted { text, signature } AgentEvent::ReasoningCompleted { text, signature }
if text.is_empty() && signature.is_none() => if text.is_empty() && signature.is_none() =>
@@ -783,6 +807,18 @@ impl ProviderRunCoordinator {
} }
buffer.reasoning.clone_from(&text); buffer.reasoning.clone_from(&text);
buffer.reasoning_signature.clone_from(&signature); buffer.reasoning_signature.clone_from(&signature);
if !self
.project_or_fail_acknowledged(
ProviderRunProjection::ModelEvent {
work_id: call.work_id.clone(),
event: AgentEvent::ReasoningCompleted { text, signature },
},
project,
)
.await?
{
return Ok(());
}
} }
AgentEvent::Tool { AgentEvent::Tool {
event: ToolEvent::Proposed { call: tool_call }, event: ToolEvent::Proposed { call: tool_call },
@@ -829,47 +865,8 @@ impl ProviderRunCoordinator {
{ {
return Ok(()); return Ok(());
} }
// A model can speak before its tool call arrives. Wait until the turn // Output is projected as it arrives. The buffer is only for canonical
// finishes so routine tool-turn narration never reaches the conversation. // history; replaying it here would duplicate the streamed response.
// Keep it in the canonical turn for provider continuity.
if buffer.tool_calls.is_empty()
&& !matches!(
&reason,
StopReason::Cancelled | StopReason::ContextWindowExceeded
)
{
if !buffer.reasoning.is_empty()
&& !self
.project_or_fail_acknowledged(
ProviderRunProjection::ModelEvent {
work_id: call.work_id.clone(),
event: AgentEvent::ReasoningCompleted {
text: buffer.reasoning.clone(),
signature: buffer.reasoning_signature.clone(),
},
},
project,
)
.await?
{
return Ok(());
}
if !buffer.text.is_empty()
&& !self
.project_or_fail_acknowledged(
ProviderRunProjection::ModelEvent {
work_id: call.work_id.clone(),
event: AgentEvent::TextDelta {
text: buffer.text.clone(),
},
},
project,
)
.await?
{
return Ok(());
}
}
if !self if !self
.project_or_fail_acknowledged( .project_or_fail_acknowledged(
ProviderRunProjection::ModelTurnFinished { ProviderRunProjection::ModelTurnFinished {
@@ -20,6 +20,7 @@ type ScriptedTurn = Result<Vec<ScriptedEvent>, AgentError>;
struct ScriptedRuntime { struct ScriptedRuntime {
descriptor: RuntimeDescriptor, descriptor: RuntimeDescriptor,
turns: Mutex<VecDeque<ScriptedTurn>>, turns: Mutex<VecDeque<ScriptedTurn>>,
events_consumed: Arc<AtomicUsize>,
requests: Mutex<Vec<TurnRequest>>, requests: Mutex<Vec<TurnRequest>>,
} }
@@ -37,6 +38,7 @@ impl ScriptedRuntime {
capabilities: RuntimeCapabilities::provider(), capabilities: RuntimeCapabilities::provider(),
}, },
turns: Mutex::new(turns.into()), turns: Mutex::new(turns.into()),
events_consumed: Arc::new(AtomicUsize::new(0)),
requests: Mutex::new(Vec::new()), requests: Mutex::new(Vec::new()),
} }
} }
@@ -64,7 +66,10 @@ impl AgentRuntime for ScriptedRuntime {
.unwrap() .unwrap()
.pop_front() .pop_front()
.expect("scripted runtime ran out of turns")?; .expect("scripted runtime ran out of turns")?;
Ok(Box::pin(futures::stream::iter(turn))) let events_consumed = self.events_consumed.clone();
Ok(Box::pin(futures::stream::iter(turn).inspect(move |_| {
events_consumed.fetch_add(1, Ordering::Relaxed);
})))
} }
} }
@@ -302,7 +307,7 @@ fn answer_turn() -> ScriptedTurn {
} }
#[tokio::test] #[tokio::test]
async fn tool_turn_narration_stays_in_history_but_out_of_visible_output() { async fn tool_turn_text_is_streamed_and_retained_in_history() {
let runtime = Arc::new(ScriptedRuntime::new(vec![tool_turn()])); let runtime = Arc::new(ScriptedRuntime::new(vec![tool_turn()]));
let mut coordinator = coordinator(runtime); let mut coordinator = coordinator(runtime);
let mut projections = Vec::new(); let mut projections = Vec::new();
@@ -319,15 +324,91 @@ async fn tool_turn_narration_stays_in_history_but_out_of_visible_output() {
.transcript() .transcript()
.iter() .iter()
.any(|message| format!("{:?}", message.content).contains("I will inspect it."))); .any(|message| format!("{:?}", message.content).contains("I will inspect it.")));
assert!(!projections.iter().any(|projection| matches!( let text: Vec<_> = projections
projection, .iter()
ProviderRunProjection::ModelEvent { .filter_map(|projection| match projection {
event: AgentEvent::TextDelta { .. } ProviderRunProjection::ModelEvent {
| AgentEvent::ReasoningDelta { .. } event: AgentEvent::TextDelta { text },
| AgentEvent::ReasoningCompleted { .. }, ..
.. } => Some(text.as_str()),
} _ => None,
))); })
.collect();
assert_eq!(text, ["I will inspect it."]);
}
#[tokio::test]
async fn text_and_reasoning_are_projected_before_reading_the_next_stream_event() {
let runtime = Arc::new(ScriptedRuntime::new(vec![Ok(vec![
started("request-streaming"),
Ok(AgentEvent::TextDelta {
text: "First ".to_string(),
}),
Ok(AgentEvent::ReasoningDelta {
text: "Thinking".to_string(),
}),
Ok(AgentEvent::ReasoningCompleted {
text: "Thinking".to_string(),
signature: Some("signature".to_string()),
}),
Ok(AgentEvent::TextDelta {
text: "second.".to_string(),
}),
usage(10, 4),
stopped(StopReason::Completed),
])]));
let events_consumed = runtime.events_consumed.clone();
let mut coordinator = coordinator(runtime);
let mut output = Vec::new();
let (_sender, control) = turn_control();
coordinator
.drive_until_blocked(control, |projection| {
if let ProviderRunProjection::ModelEvent { event, .. } = projection {
if matches!(
event,
AgentEvent::TextDelta { .. }
| AgentEvent::ReasoningDelta { .. }
| AgentEvent::ReasoningCompleted { .. }
) {
output.push((events_consumed.load(Ordering::Relaxed), event));
}
}
Ok(())
})
.await
.unwrap();
assert_eq!(
output,
vec![
(
2,
AgentEvent::TextDelta {
text: "First ".to_string()
}
),
(
3,
AgentEvent::ReasoningDelta {
text: "Thinking".to_string()
}
),
(
4,
AgentEvent::ReasoningCompleted {
text: "Thinking".to_string(),
signature: Some("signature".to_string()),
}
),
(
5,
AgentEvent::TextDelta {
text: "second.".to_string()
}
),
]
);
} }
#[tokio::test] #[tokio::test]
+1 -1
View File
@@ -832,7 +832,7 @@ fn build_system_prompt(
"You are Galaxy, a local-first software-engineering and terminal agent. Complete the user's task through inspection, implementation, and proportionate validation. Galaxy owns tool permissions and execution; use only the tools advertised in this request and treat every result as authoritative evidence.\n\n", "You are Galaxy, a local-first software-engineering and terminal agent. Complete the user's task through inspection, implementation, and proportionate validation. Galaxy owns tool permissions and execution; use only the tools advertised in this request and treat every result as authoritative evidence.\n\n",
); );
prompt.push_str( prompt.push_str(
"## Communication Style\nSpeak naturally, warmly, and directly, like a thoughtful collaborator working alongside the user. Work quietly by default while using tools; tool panes already show the activity. Do not send an introductory acknowledgement, plan, or generic progress message. Send prose during work only for a question needing the user's input, a consequential blocker, or a material finding the user needs to act on now. Tool results and compaction checkpoints continue the same request: resume work without another greeting or restatement. Supply the short purpose title requested by each tool schema. Do not narrate routine reads, repeat the plan, or end a turn with only a progress update when useful work can continue. In the final response, lead with the outcome and keep the handoff compact.\n\n", "## Communication Style\nUse plain, direct language. During implementation, default to tool calls without accompanying prose; follow the Progress Updates contract below. Supply the short purpose title requested by each tool schema. Answer conversational questions naturally. When the task is complete, give one compact, self-contained result with the outcome, relevant validation, and any remaining blocker.\n\n",
); );
prompt.push_str( prompt.push_str(
"## Execution Contract\nBefore another read or search, identify the missing fact it will resolve. Reuse findings already present in the conversation. Once enough evidence exists for the requested change, implement it and validate; additional inspection should resolve a specific uncertainty. Batch independent reads within the tool limits and wait for results before dependent work. Continue until the user's requested outcome is complete and validated. Do not stop at an intermediate analysis, plan, status update, or promise of future work, and do not ask the user to say \"continue\". After each tool result, choose and perform the next necessary step. Stop only when the request is fulfilled or a concrete blocker requires user input; identify that blocker explicitly.\n\n", "## Execution Contract\nBefore another read or search, identify the missing fact it will resolve. Reuse findings already present in the conversation. Once enough evidence exists for the requested change, implement it and validate; additional inspection should resolve a specific uncertainty. Batch independent reads within the tool limits and wait for results before dependent work. Continue until the user's requested outcome is complete and validated. Do not stop at an intermediate analysis, plan, status update, or promise of future work, and do not ask the user to say \"continue\". After each tool result, choose and perform the next necessary step. Stop only when the request is fulfilled or a concrete blocker requires user input; identify that blocker explicitly.\n\n",