fix: suppress subagent error and warning output to stderr/stdout
This commit is contained in:
2 files changed
+74
-24
No files matched your search
@@ -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
@@ -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");
|
||||||
|
|||||||
Reference in new issue
Block a user