fix: suppress subagent error and warning output to stderr/stdout

This commit is contained in:
Dark-Alex-17 committed 2026-09-08 11:24:43 -06:00
1 parent a0dc4550b1
commit 4651658b34
2 files changed
+74 -24

No files matched your search

+1
View File
@@ -623,6 +623,7 @@ impl Agent {
vec!["_instructions".into(), "{}".into()], vec!["_instructions".into(), "{}".into()],
self.variable_envs(), self.variable_envs(),
Some(self.name().to_string()), Some(self.name().to_string()),
false,
)?; )?;
match value { match value {
Some(v) => Ok(v), Some(v) => Ok(v),
+73 -24
View File
@@ -1484,6 +1484,7 @@ impl ToolCall {
async fn eval_mcp(&self, ctx: &RequestContext) -> Result<Value> { async fn eval_mcp(&self, ctx: &RequestContext) -> Result<Value> {
let json_data = self.parse_arguments()?; let json_data = self.parse_arguments()?;
let cmd_name = self.name.as_str(); let cmd_name = self.name.as_str();
let quiet = ctx.current_depth > 0;
if *IS_STDOUT_TERMINAL && ctx.current_depth == 0 && !HEADLESS.load(Ordering::SeqCst) { if *IS_STDOUT_TERMINAL && ctx.current_depth == 0 && !HEADLESS.load(Ordering::SeqCst) {
println!( println!(
"{}", "{}",
@@ -1495,7 +1496,7 @@ impl ToolCall {
.await .await
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
let error_msg = format!("MCP search failed: {e}"); let error_msg = format!("MCP search failed: {e}");
eprintln!("{}", muted_warning_text(&mcp_error_display(&error_msg))); emit_tool_warning(quiet, &mcp_error_display(&error_msg), &error_msg);
json!({"tool_call_error": error_msg}) json!({"tool_call_error": error_msg})
}) })
} else if cmd_name.starts_with(MCP_DESCRIBE_META_FUNCTION_NAME_PREFIX) { } else if cmd_name.starts_with(MCP_DESCRIBE_META_FUNCTION_NAME_PREFIX) {
@@ -1503,7 +1504,7 @@ impl ToolCall {
.await .await
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
let error_msg = format!("MCP describe failed: {e}"); let error_msg = format!("MCP describe failed: {e}");
eprintln!("{}", muted_warning_text(&mcp_error_display(&error_msg))); emit_tool_warning(quiet, &mcp_error_display(&error_msg), &error_msg);
json!({"tool_call_error": error_msg}) json!({"tool_call_error": error_msg})
}) })
} else if cmd_name.starts_with(MCP_READ_META_FUNCTION_NAME_PREFIX) { } else if cmd_name.starts_with(MCP_READ_META_FUNCTION_NAME_PREFIX) {
@@ -1511,7 +1512,7 @@ impl ToolCall {
.await .await
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
let error_msg = format!("MCP read failed: {e}"); let error_msg = format!("MCP read failed: {e}");
eprintln!("{}", muted_warning_text(&mcp_error_display(&error_msg))); emit_tool_warning(quiet, &mcp_error_display(&error_msg), &error_msg);
json!({"tool_call_error": error_msg}) json!({"tool_call_error": error_msg})
}) })
} else if cmd_name.starts_with(MCP_PROMPT_META_FUNCTION_NAME_PREFIX) { } else if cmd_name.starts_with(MCP_PROMPT_META_FUNCTION_NAME_PREFIX) {
@@ -1519,7 +1520,7 @@ impl ToolCall {
.await .await
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
let error_msg = format!("MCP prompt failed: {e}"); let error_msg = format!("MCP prompt failed: {e}");
eprintln!("{}", muted_warning_text(&mcp_error_display(&error_msg))); emit_tool_warning(quiet, &mcp_error_display(&error_msg), &error_msg);
json!({"tool_call_error": error_msg}) json!({"tool_call_error": error_msg})
}) })
} else { } else {
@@ -1527,7 +1528,7 @@ impl ToolCall {
.await .await
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
let error_msg = format!("MCP tool invocation failed: {e}"); let error_msg = format!("MCP tool invocation failed: {e}");
eprintln!("{}", muted_warning_text(&mcp_error_display(&error_msg))); emit_tool_warning(quiet, &mcp_error_display(&error_msg), &error_msg);
json!({"tool_call_error": error_msg}) json!({"tool_call_error": error_msg})
}) })
}; };
@@ -1538,6 +1539,7 @@ impl ToolCall {
let agent = ctx.agent.clone(); let agent = ctx.agent.clone();
let functions = ctx.tool_scope.functions.clone(); let functions = ctx.tool_scope.functions.clone();
let current_depth = ctx.current_depth; let current_depth = ctx.current_depth;
let quiet = current_depth > 0;
let agent_name = agent.as_ref().map(|agent| agent.name().to_owned()); let agent_name = agent.as_ref().map(|agent| agent.name().to_owned());
let (call_name, cmd_name, mut cmd_args, envs) = match agent.as_ref() { let (call_name, cmd_name, mut cmd_args, envs) = match agent.as_ref() {
Some(agent) => self.extract_call_config_from_agent(&functions, agent)?, Some(agent) => self.extract_call_config_from_agent(&functions, agent)?,
@@ -1570,7 +1572,7 @@ impl ToolCall {
.await .await
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
let error_msg = format!("MCP search failed: {e}"); let error_msg = format!("MCP search failed: {e}");
eprintln!("{}", muted_warning_text(&mcp_error_display(&error_msg))); emit_tool_warning(quiet, &mcp_error_display(&error_msg), &error_msg);
json!({"tool_call_error": error_msg}) json!({"tool_call_error": error_msg})
}) })
} }
@@ -1579,7 +1581,7 @@ impl ToolCall {
.await .await
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
let error_msg = format!("MCP describe failed: {e}"); let error_msg = format!("MCP describe failed: {e}");
eprintln!("{}", muted_warning_text(&mcp_error_display(&error_msg))); emit_tool_warning(quiet, &mcp_error_display(&error_msg), &error_msg);
json!({"tool_call_error": error_msg}) json!({"tool_call_error": error_msg})
}) })
} }
@@ -1588,7 +1590,7 @@ impl ToolCall {
.await .await
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
let error_msg = format!("MCP read failed: {e}"); let error_msg = format!("MCP read failed: {e}");
eprintln!("{}", muted_warning_text(&mcp_error_display(&error_msg))); emit_tool_warning(quiet, &mcp_error_display(&error_msg), &error_msg);
json!({"tool_call_error": error_msg}) json!({"tool_call_error": error_msg})
}) })
} }
@@ -1597,7 +1599,7 @@ impl ToolCall {
.await .await
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
let error_msg = format!("MCP prompt failed: {e}"); let error_msg = format!("MCP prompt failed: {e}");
eprintln!("{}", muted_warning_text(&mcp_error_display(&error_msg))); emit_tool_warning(quiet, &mcp_error_display(&error_msg), &error_msg);
json!({"tool_call_error": error_msg}) json!({"tool_call_error": error_msg})
}) })
} }
@@ -1606,21 +1608,21 @@ impl ToolCall {
.await .await
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
let error_msg = format!("MCP tool invocation failed: {e}"); let error_msg = format!("MCP tool invocation failed: {e}");
eprintln!("{}", muted_warning_text(&mcp_error_display(&error_msg))); emit_tool_warning(quiet, &mcp_error_display(&error_msg), &error_msg);
json!({"tool_call_error": error_msg}) json!({"tool_call_error": error_msg})
}) })
} }
_ if cmd_name.starts_with(TODO_FUNCTION_PREFIX) => { _ if cmd_name.starts_with(TODO_FUNCTION_PREFIX) => {
todo::handle_todo_tool(ctx, &cmd_name, &json_data).unwrap_or_else(|e| { todo::handle_todo_tool(ctx, &cmd_name, &json_data).unwrap_or_else(|e| {
let error_msg = format!("Todo tool failed: {e}"); let error_msg = format!("Todo tool failed: {e}");
eprintln!("{}", muted_warning_text(&format!("⚠️ {error_msg} ⚠️"))); emit_tool_warning(quiet, &format!("⚠️ {error_msg} ⚠️"), &error_msg);
json!({"tool_call_error": error_msg}) json!({"tool_call_error": error_msg})
}) })
} }
_ if cmd_name.starts_with(MEMORY_FUNCTION_PREFIX) => { _ if cmd_name.starts_with(MEMORY_FUNCTION_PREFIX) => {
memory::handle_memory_tool(ctx, &cmd_name, &json_data).unwrap_or_else(|e| { memory::handle_memory_tool(ctx, &cmd_name, &json_data).unwrap_or_else(|e| {
let error_msg = format!("Memory tool failed: {e}"); let error_msg = format!("Memory tool failed: {e}");
eprintln!("{}", muted_warning_text(&format!("⚠️ {error_msg} ⚠️"))); emit_tool_warning(quiet, &format!("⚠️ {error_msg} ⚠️"), &error_msg);
json!({"tool_call_error": error_msg}) json!({"tool_call_error": error_msg})
}) })
} }
@@ -1629,7 +1631,7 @@ impl ToolCall {
.await .await
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
let error_msg = format!("Skill tool failed: {e}"); let error_msg = format!("Skill tool failed: {e}");
eprintln!("{}", muted_warning_text(&format!("⚠️ {error_msg} ⚠️"))); emit_tool_warning(quiet, &format!("⚠️ {error_msg} ⚠️"), &error_msg);
json!({"tool_call_error": error_msg}) json!({"tool_call_error": error_msg})
}) })
} }
@@ -1638,7 +1640,7 @@ impl ToolCall {
.await .await
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
let error_msg = format!("Agent tool failed: {e}"); let error_msg = format!("Agent tool failed: {e}");
eprintln!("{}", muted_warning_text(&format!("⚠️ {error_msg} ⚠️"))); emit_tool_warning(quiet, &format!("⚠️ {error_msg} ⚠️"), &error_msg);
json!({"tool_call_error": error_msg}) json!({"tool_call_error": error_msg})
}) })
} }
@@ -1647,7 +1649,7 @@ impl ToolCall {
.await .await
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
let error_msg = format!("User interaction failed: {e}"); let error_msg = format!("User interaction failed: {e}");
eprintln!("{}", muted_warning_text(&format!("⚠️ {error_msg} ⚠️"))); emit_tool_warning(quiet, &format!("⚠️ {error_msg} ⚠️"), &error_msg);
json!({"tool_call_error": error_msg}) json!({"tool_call_error": error_msg})
}) })
} }
@@ -1656,7 +1658,7 @@ impl ToolCall {
.await .await
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
let error_msg = format!("RAG query failed: {e}"); let error_msg = format!("RAG query failed: {e}");
eprintln!("{}", muted_warning_text(&format!("⚠️ {error_msg} ⚠️"))); emit_tool_warning(quiet, &format!("⚠️ {error_msg} ⚠️"), &error_msg);
json!({"tool_call_error": error_msg}) json!({"tool_call_error": error_msg})
}) })
} }
@@ -1665,11 +1667,11 @@ impl ToolCall {
.await .await
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
let error_msg = format!("Job tool failed: {e}"); let error_msg = format!("Job tool failed: {e}");
eprintln!("{}", muted_warning_text(&format!("⚠️ {error_msg} ⚠️"))); emit_tool_warning(quiet, &format!("⚠️ {error_msg} ⚠️"), &error_msg);
json!({"tool_call_error": error_msg}) json!({"tool_call_error": error_msg})
}) })
} }
_ => match run_llm_function(cmd_name, cmd_args, envs, agent_name) { _ => match run_llm_function(cmd_name, cmd_args, envs, agent_name, quiet) {
Ok(Some(contents)) => serde_json::from_str(&contents) Ok(Some(contents)) => serde_json::from_str(&contents)
.ok() .ok()
.unwrap_or_else(|| json!({"output": contents})), .unwrap_or_else(|| json!({"output": contents})),
@@ -2108,6 +2110,16 @@ fn mcp_error_display(error_msg: &str) -> String {
sanitize_display_text(&format!("⚠️ {error_msg} ⚠️")) sanitize_display_text(&format!("⚠️ {error_msg} ⚠️"))
} }
// Subagent (depth > 0) tool calls run while the parent session owns the
// terminal; route their warnings to the debug log instead of stderr.
fn emit_tool_warning(quiet: bool, display: &str, plain: &str) {
if quiet {
debug!("{plain}");
} else {
eprintln!("{}", muted_warning_text(display));
}
}
/// Bounds a raw `CallToolResult` JSON value: oversized text is sliced, /// Bounds a raw `CallToolResult` JSON value: oversized text is sliced,
/// base64 blob content is routed through the blob renderer instead of /// base64 blob content is routed through the blob renderer instead of
/// reaching model context, and oversized structured content is replaced with /// reaching model context, and oversized structured content is replaced with
@@ -2258,6 +2270,7 @@ pub fn run_llm_function(
cmd_args: Vec<String>, cmd_args: Vec<String>,
mut envs: HashMap<String, String>, mut envs: HashMap<String, String>,
agent_name: Option<String>, agent_name: Option<String>,
quiet: bool,
) -> Result<Option<String>> { ) -> Result<Option<String>> {
let mut bin_dirs: Vec<PathBuf> = vec![]; let mut bin_dirs: Vec<PathBuf> = vec![];
let mut command_name = cmd_name.clone(); let mut command_name = cmd_name.clone();
@@ -2327,6 +2340,9 @@ pub fn run_llm_function(
} }
let chunk = &buffer[0..n]; let chunk = &buffer[0..n];
buf.extend_from_slice(chunk); buf.extend_from_slice(chunk);
if quiet {
continue;
}
let mut last_pos = 0; let mut last_pos = 0;
for (i, &byte) in chunk.iter().enumerate() { for (i, &byte) in chunk.iter().enumerate() {
if byte == b'\n' { if byte == b'\n' {
@@ -2354,6 +2370,9 @@ pub fn run_llm_function(
} }
let chunk = &buffer[0..n]; let chunk = &buffer[0..n];
buf.extend_from_slice(chunk); buf.extend_from_slice(chunk);
if quiet {
continue;
}
let mut last_pos = 0; let mut last_pos = 0;
for (i, &byte) in chunk.iter().enumerate() { for (i, &byte) in chunk.iter().enumerate() {
if byte == b'\n' { if byte == b'\n' {
@@ -2391,9 +2410,10 @@ pub fn run_llm_function(
let tool_error_message = format!( let tool_error_message = format!(
"Tool call '{command_name}' timed out after {timeout_secs}s and was killed (set COYOTE_TOOL_TIMEOUT to adjust; 0 = unlimited)" "Tool call '{command_name}' timed out after {timeout_secs}s and was killed (set COYOTE_TOOL_TIMEOUT to adjust; 0 = unlimited)"
); );
eprintln!( emit_tool_warning(
"{}", quiet,
muted_warning_text(&format!("⚠️ {tool_error_message} ⚠️")) &format!("⚠️ {tool_error_message} ⚠️"),
&tool_error_message,
); );
let error_json = json!({"tool_call_error": tool_error_message}); let error_json = json!({"tool_call_error": tool_error_message});
@@ -2411,9 +2431,10 @@ pub fn run_llm_function(
let stderr = String::from_utf8_lossy(&stderr_bytes).trim().to_string(); let stderr = String::from_utf8_lossy(&stderr_bytes).trim().to_string();
let stdout = String::from_utf8_lossy(&stdout_bytes).trim().to_string(); let stdout = String::from_utf8_lossy(&stdout_bytes).trim().to_string();
let tool_error_message = format!("Tool call '{command_name}' exited with code {exit_code}"); let tool_error_message = format!("Tool call '{command_name}' exited with code {exit_code}");
eprintln!( emit_tool_warning(
"{}", quiet,
muted_warning_text(&format!("⚠️ {tool_error_message} ⚠️")) &format!("⚠️ {tool_error_message} ⚠️"),
&tool_error_message,
); );
let mut error_json = json!({"tool_call_error": tool_error_message}); let mut error_json = json!({"tool_call_error": tool_error_message});
if !stderr.is_empty() { if !stderr.is_empty() {
@@ -4519,6 +4540,34 @@ mod tests {
], ],
HashMap::new(), HashMap::new(),
None, None,
false,
)
.unwrap()
.expect("nonzero exit must return an error payload");
let json: serde_json::Value = serde_json::from_str(&result).unwrap();
assert!(
json["tool_call_error"]
.as_str()
.unwrap()
.contains("exited with code 3")
);
assert_eq!(json["stderr"], "err-text");
assert_eq!(json["output"], "partial-output\n");
}
#[cfg(unix)]
#[test]
fn run_llm_function_quiet_still_collects_output_buffers() {
let result = run_llm_function(
"bash".into(),
vec![
"-c".into(),
"echo partial-output >> \"$LLM_OUTPUT\"; echo err-text >&2; exit 3".into(),
],
HashMap::new(),
None,
true,
) )
.unwrap() .unwrap()
.expect("nonzero exit must return an error payload"); .expect("nonzero exit must return an error payload");