From 86709834e2c5af8e7c810c358cf74349f334763b Mon Sep 17 00:00:00 2001 From: "Dhanji R. Prasanna" Date: Sat, 10 Jan 2026 20:50:43 +1100 Subject: [PATCH] Improve research tool error reporting for scout agent failures When the scout agent fails (e.g., context window exhaustion), now: - Captures both stdout and stderr from the scout process - Detects context window exhaustion errors with specific patterns - Provides detailed, actionable error messages to the user - Shows suggestions for how to work around the issue - Includes technical details (exit code, error output) for debugging Handles two failure modes: 1. Scout agent exits with non-zero status 2. Scout agent exits successfully but doesn't produce valid report markers Both cases now surface clear error messages instead of cryptic failures. --- crates/g3-core/src/tools/research.rs | 98 +++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 3 deletions(-) diff --git a/crates/g3-core/src/tools/research.rs b/crates/g3-core/src/tools/research.rs index 6032b36..f98a49c 100644 --- a/crates/g3-core/src/tools/research.rs +++ b/crates/g3-core/src/tools/research.rs @@ -143,6 +143,12 @@ fn truncate_command_snippet(cmd: &str, max_len: usize) -> String { } } +/// Error patterns that indicate context window exhaustion +const CONTEXT_ERROR_PATTERNS: &[&str] = &[ + "context length", "context_length_exceeded", "maximum context", "token limit", + "too many tokens", "exceeds the model", "context window", "max_tokens", +]; + pub async fn execute_research( tool_call: &ToolCall, ctx: &mut ToolContext<'_, W>, @@ -181,10 +187,24 @@ pub async fn execute_research( let stdout = child.stdout.take() .ok_or_else(|| anyhow::anyhow!("Failed to capture scout agent stdout"))?; + // Also capture stderr for error messages + let stderr = child.stderr.take() + .ok_or_else(|| anyhow::anyhow!("Failed to capture scout agent stderr"))?; + let mut reader = BufReader::new(stdout).lines(); let mut all_output = Vec::new(); + + // Spawn a task to collect stderr + let stderr_handle = tokio::spawn(async move { + let mut stderr_reader = BufReader::new(stderr).lines(); + let mut stderr_output = Vec::new(); + while let Some(line) = stderr_reader.next_line().await.ok().flatten() { + stderr_output.push(line); + } + stderr_output + }); - // Collect all lines, showing only translated progress messages + // Collect stdout lines, showing only translated progress messages while let Some(line) = reader.next_line().await? { all_output.push(line.clone()); @@ -194,19 +214,91 @@ pub async fn execute_research( ctx.ui_writer.update_tool_output_line(&progress_msg); } } + + // Collect stderr output + let stderr_output = stderr_handle.await.unwrap_or_default(); // Wait for the process to complete let status = child.wait().await .map_err(|e| anyhow::anyhow!("Failed to wait for scout agent: {}", e))?; if !status.success() { - return Ok(format!("❌ Scout agent failed with exit code: {:?}", status.code())); + // Build detailed error message + let exit_code = status.code().map(|c| c.to_string()).unwrap_or_else(|| "unknown".to_string()); + let full_output = all_output.join("\n"); + let stderr_text = stderr_output.join("\n"); + + // Check for context window exhaustion + let combined_output = format!("{} {}", full_output, stderr_text).to_lowercase(); + let is_context_error = CONTEXT_ERROR_PATTERNS.iter() + .any(|pattern| combined_output.contains(pattern)); + + if is_context_error { + let error_msg = format!( + "❌ **Scout Agent Error: Context Window Exhausted**\n\n\ + The research query required more context than the model supports.\n\n\ + **Suggestions:**\n\ + - Try a more specific, narrower query\n\ + - Break the research into smaller sub-questions\n\ + - Use a model with a larger context window\n\n\ + **Technical Details:**\n\ + Exit code: {}\n\ + {}", + exit_code, + if !stderr_text.is_empty() { format!("Error output: {}", stderr_text.chars().take(500).collect::()) } else { String::new() } + ); + ctx.ui_writer.println(&error_msg); + return Ok(error_msg); + } + + // Generic error with details + let error_msg = format!( + "❌ **Scout Agent Failed**\n\n\ + Exit code: {}\n\n\ + {}{}", + exit_code, + if !stderr_text.is_empty() { format!("**Error output:**\n{}\n\n", stderr_text.chars().take(1000).collect::()) } else { String::new() }, + if all_output.len() > 0 { format!("**Last output lines:**\n{}", all_output.iter().rev().take(10).rev().cloned().collect::>().join("\n")) } else { String::new() } + ); + ctx.ui_writer.println(&error_msg); + return Ok(error_msg); } // Join all output and extract the report between markers let full_output = all_output.join("\n"); - let report = extract_report(&full_output)?; + let report = match extract_report(&full_output) { + Ok(r) => r, + Err(e) => { + // Check if this looks like a context exhaustion issue + let combined = format!("{} {}", full_output, stderr_output.join(" ")).to_lowercase(); + let is_context_error = CONTEXT_ERROR_PATTERNS.iter() + .any(|pattern| combined.contains(pattern)); + + let error_msg = if is_context_error { + format!( + "❌ **Scout Agent Error: Context Window Exhausted**\n\n\ + The scout agent ran out of context before completing the research report.\n\n\ + **Suggestions:**\n\ + - Try a more specific, narrower query\n\ + - Break the research into smaller sub-questions\n\n\ + **Technical Details:**\n\ + {}", + e + ) + } else { + format!( + "❌ **Scout Agent Error: Report Extraction Failed**\n\n\ + {}\n\n\ + The scout agent completed but did not produce a valid report.\n\ + This may indicate the agent encountered an error during research.", + e + ) + }; + ctx.ui_writer.println(&error_msg); + return Ok(error_msg); + } + }; // Print the research brief to the console for scrollback reference // The report is printed without stripping ANSI codes to preserve formatting