feat(subagent): add progress event handling and formatting for subagent execution

This commit is contained in:
asepharyana
2026-07-15 02:25:15 +07:00
parent 472c597c5e
commit 558908aef2
5 changed files with 52 additions and 7 deletions
+1
View File
@@ -368,6 +368,7 @@ pub fn trigger_review(state: &mut AppStateRest) {
SubagentEvent::StepFailed { step, error } => { SubagentEvent::StepFailed { step, error } => {
tracing::warn!("[review] step {} failed: {}", step, error); tracing::warn!("[review] step {} failed: {}", step, error);
} }
SubagentEvent::Progress(_) => {}
SubagentEvent::Completed { .. } => { SubagentEvent::Completed { .. } => {
tracing::debug!("[review] completed"); tracing::debug!("[review] completed");
} }
+29 -3
View File
@@ -296,6 +296,17 @@ fn generate_workspace_tree(roots: &[std::path::PathBuf]) -> String {
out out
} }
fn format_subagent_progress(prefix: &str, text: &str) -> String {
let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
if lines.is_empty() {
format!("{prefix}...")
} else if lines.len() == 1 {
format!("{prefix}: {}", lines[0])
} else {
lines[lines.len() - 2..].join("\n")
}
}
/// Synchronous subagent entry point: run up to `ctx.max_steps` iterations /// Synchronous subagent entry point: run up to `ctx.max_steps` iterations
/// of the LLM tool loop. /// of the LLM tool loop.
/// ///
@@ -369,6 +380,10 @@ pub fn run_subagent(ctx: &SubagentContext, tx: &mpsc::Sender<SubagentEvent>) ->
anyhow::bail!("subagent aborted by parent at step {step}"); anyhow::bail!("subagent aborted by parent at step {step}");
} }
let tx_clone = tx.clone();
let mut current_thinking = String::new();
let mut current_token = String::new();
// Use streaming API so the abort flag is checked per SSE event, // Use streaming API so the abort flag is checked per SSE event,
// making the subagent responsive to cancellation even during an // making the subagent responsive to cancellation even during an
// LLM call (non-streaming would block for 10-30s unchecked). // LLM call (non-streaming would block for 10-30s unchecked).
@@ -377,13 +392,24 @@ pub fn run_subagent(ctx: &SubagentContext, tx: &mpsc::Sender<SubagentEvent>) ->
tdefs_opt.clone(), tdefs_opt.clone(),
Some(0.7), Some(0.7),
Some(4096), Some(4096),
|_event| -> bool { |event| -> bool {
// Check abort on every SSE event for responsive cancellation. // Check abort on every SSE event for responsive cancellation.
if ctx.abort_flag.as_ref().is_some_and(|f| f.load(std::sync::atomic::Ordering::SeqCst)) { if ctx.abort_flag.as_ref().is_some_and(|f| f.load(std::sync::atomic::Ordering::SeqCst)) {
return false; // signals provider to abort return false; // signals provider to abort
} }
// We don't stream tokens to the UI for subagents — just match event {
// need the assembled message at the end. crate::app::runtime::stream::StreamEvent::Reasoning(text) => {
current_thinking.push_str(text);
let prog = format_subagent_progress("thinking", &current_thinking);
let _ = tx_clone.blocking_send(SubagentEvent::Progress(prog));
}
crate::app::runtime::stream::StreamEvent::Token(text) => {
current_token.push_str(text);
let prog = format_subagent_progress("replying", &current_token);
let _ = tx_clone.blocking_send(SubagentEvent::Progress(prog));
}
_ => {}
}
true true
}, },
); );
+1
View File
@@ -31,4 +31,5 @@ pub enum SubagentEvent {
#[allow(dead_code)] #[allow(dead_code)]
output: String, output: String,
}, },
Progress(String),
} }
+15
View File
@@ -238,6 +238,21 @@ fn spawn_single_agent(
SubagentEvent::StepFailed { step, error } => { SubagentEvent::StepFailed { step, error } => {
tracing::warn!("[subagent] step {} failed: {}", step, error); tracing::warn!("[subagent] step {} failed: {}", step, error);
} }
SubagentEvent::Progress(prog) => {
if let Some(ref f) = drain_live {
f(
drain_agent_id.clone(),
drain_agent_name.clone(),
AgentStatus {
state: AgentState::Running,
started_at: Some(drain_started_at),
completed_at: None,
error: None,
progress: Some(prog.clone()),
},
);
}
}
SubagentEvent::Completed { .. } => { SubagentEvent::Completed { .. } => {
tracing::debug!("[subagent] completed"); tracing::debug!("[subagent] completed");
} }
+6 -4
View File
@@ -141,10 +141,12 @@ pub fn draw_workflow_panel(frame: &mut Frame, area: Rect, state: &crate::app::st
Span::styled(err.clone(), Style::default().fg(Theme::ERROR)), Span::styled(err.clone(), Style::default().fg(Theme::ERROR)),
])); ]));
} else if let Some(ref prog) = agent.status.progress { } else if let Some(ref prog) = agent.status.progress {
card_lines.push(Line::from(vec![ for line in prog.lines().take(2) {
Span::styled(" ", Style::default()), card_lines.push(Line::from(vec![
Span::styled(prog.clone(), Style::default().fg(Theme::TEXT_DIM).add_modifier(Modifier::ITALIC)), Span::styled(" ", Style::default()),
])); Span::styled(line.to_string(), Style::default().fg(Theme::TEXT_DIM).add_modifier(Modifier::ITALIC)),
]));
}
} }
} }