From a8adfcbf6dc5411e977f22ac6b6ba023f563d7c9 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Wed, 15 Jul 2026 02:18:30 +0700 Subject: [PATCH] feat(subagent): include tool call arguments in ToolResult events and progress formatting --- src/app/subagent/engine.rs | 2 + src/app/subagent/event.rs | 1 + src/app/workflow/engine.rs | 93 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 92 insertions(+), 4 deletions(-) diff --git a/src/app/subagent/engine.rs b/src/app/subagent/engine.rs index c5c484a..70f8d08 100644 --- a/src/app/subagent/engine.rs +++ b/src/app/subagent/engine.rs @@ -582,6 +582,7 @@ pub fn run_subagent(ctx: &SubagentContext, tx: &mpsc::Sender) -> messages.push(ChatMessage::tool_result(tool_call.id.clone(), output_text.clone())); let _ = tx.blocking_send(SubagentEvent::ToolResult { tool: tool_name.clone(), + args: args.clone(), output: output_text, }); } @@ -598,6 +599,7 @@ pub fn run_subagent(ctx: &SubagentContext, tx: &mpsc::Sender) -> messages.push(ChatMessage::tool_result(tool_call.id.clone(), msg.clone())); let _ = tx.blocking_send(SubagentEvent::ToolResult { tool: tool_name.clone(), + args: args.clone(), output: msg, }); } diff --git a/src/app/subagent/event.rs b/src/app/subagent/event.rs index ff70b74..c97df96 100644 --- a/src/app/subagent/event.rs +++ b/src/app/subagent/event.rs @@ -28,6 +28,7 @@ pub enum SubagentEvent { }, ToolResult { tool: String, + args: Value, #[allow(dead_code)] output: String, }, diff --git a/src/app/workflow/engine.rs b/src/app/workflow/engine.rs index 98a740b..48ea85d 100644 --- a/src/app/workflow/engine.rs +++ b/src/app/workflow/engine.rs @@ -79,6 +79,89 @@ impl WorkflowEngine { /// display purposes (e.g. a hive-mind node's designation, `"Node-0-1"`). pub type LiveStateFn = Arc; +/// Spawn a single synchronous subagent with the given prompt, passing it +/// any findings from earlier sibling agents. Updates live state before and +/// after to reflect Running → Completed/Failed transitions. +/// +/// Flow: push agent as `Running` → build `SubagentContext` with prompt + +/// findings preamble, linking the `workflow_findings` Arc so the subagent's +/// `note_finding` tool pushes into the same vec → call `run_subagent` +/// (draining the event channel into a consumer so events are not blocked) +/// → push `Completed` or `Failed`. +/// +/// Why: the `workflow_findings` Arc is shared by all agents within the same +/// `execute_primitive` scope, so pipeline stages can pass data between each +/// other while different workflow invocations remain isolated. +/// +/// When `timeout_ms` is `Some`, the subagent is killed (abandoned on a +/// separate thread) if it does not complete within the deadline, preventing +/// a stuck stage from blocking the entire pipeline forever. +/// +/// Return: the agent's text output, or an error on failure. +fn format_tool_call_progress(prefix: &str, tool: &str, args: &serde_json::Value) -> String { + let details = match tool { + "read" | "view_file" | "write" | "write_to_file" | "edit" | "replace_file_content" | "multi_replace_file_content" | "delete" => { + args.get("path") + .or_else(|| args.get("TargetFile")) + .or_else(|| args.get("AbsolutePath")) + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string() + } + "grep" | "grep_search" => { + let pattern = args.get("pattern").or_else(|| args.get("Query")).and_then(|v| v.as_str()).unwrap_or(""); + let path = args.get("path").or_else(|| args.get("SearchPath")).and_then(|v| v.as_str()).unwrap_or(""); + if path.is_empty() { + format!("\"{pattern}\"") + } else { + format!("\"{pattern}\" in {path}") + } + } + "glob" => { + let pattern = args.get("pattern").and_then(|v| v.as_str()).unwrap_or(""); + let path = args.get("path").and_then(|v| v.as_str()).unwrap_or(""); + if path.is_empty() { + pattern.to_string() + } else { + format!("{pattern} in {path}") + } + } + "bash" | "run_command" => { + let cmd = args.get("command").or_else(|| args.get("CommandLine")).and_then(|v| v.as_str()).unwrap_or(""); + if cmd.len() > 60 { + format!("\"{}...\"", &cmd[..57]) + } else { + format!("\"{cmd}\"") + } + } + "recall" => { + args.get("query").and_then(|v| v.as_str()).unwrap_or("").to_string() + } + "remember" => { + args.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string() + } + "dir_list" | "list_dir" => { + args.get("DirectoryPath").or_else(|| args.get("path")).and_then(|v| v.as_str()).unwrap_or("").to_string() + } + _ => { + if args.is_object() && !args.as_object().unwrap().is_empty() { + args.as_object().unwrap().values() + .find_map(|v| v.as_str()) + .unwrap_or("") + .to_string() + } else { + String::new() + } + } + }; + + if details.is_empty() { + format!("{prefix}: {tool}") + } else { + format!("{prefix}: {tool} {details}") + } +} + /// Spawn a single synchronous subagent with the given prompt, passing it /// any findings from earlier sibling agents. Updates live state before and /// after to reflect Running → Completed/Failed transitions. @@ -178,10 +261,11 @@ fn spawn_single_agent( let mut rx = rx; while let Some(event) = rx.blocking_recv() { match &event { - SubagentEvent::ToolCall { tool, .. } => { + SubagentEvent::ToolCall { tool, args } => { tracing::debug!("[subagent] tool call: {}", tool); // Push intra-division progress: which tool is running if let Some(ref f) = drain_live { + let formatted = format_tool_call_progress("tool", tool, args); f( drain_agent_id.clone(), drain_agent_name.clone(), @@ -190,14 +274,15 @@ fn spawn_single_agent( started_at: Some(drain_started_at), completed_at: None, error: None, - progress: Some(format!("tool: {tool}")), + progress: Some(formatted), }, ); } } - SubagentEvent::ToolResult { tool, .. } => { + SubagentEvent::ToolResult { tool, args, .. } => { tracing::debug!("[subagent] tool result: {}", tool); if let Some(ref f) = drain_live { + let formatted = format_tool_call_progress("done", tool, args); f( drain_agent_id.clone(), drain_agent_name.clone(), @@ -206,7 +291,7 @@ fn spawn_single_agent( started_at: Some(drain_started_at), completed_at: None, error: None, - progress: Some(format!("done: {tool}")), + progress: Some(formatted), }, ); }