Skip to content

Commit ebd3e7a

Browse files
committed
feat(agentic): add fallback detection for failed agent executions
Implement robust failure detection in CodeGraphAgentOutput conversion: - Add FALLBACK_MESSAGE constant with retry guidance - Add detect_failure() with three detection criteria: 1. Agent never completed (done=false) 2. Empty/whitespace-only response 3. No tools used with minimal response (<100 chars) - Schema parse failures trigger fallback only when no tools were used - Raw response used when tools were executed but JSON parsing fails - Add CODEGRAPH_DISABLE_FALLBACK env var for debugging - Log failures at info level (regular occurrences) - Include 6 unit tests covering all scenarios When fallback is triggered, message guides the calling agent to: "Retry once with a rewritten question. If tool fails again, proceed normally without CodeGraph for this phase of the task."
1 parent 9b8f38b commit ebd3e7a

File tree

1 file changed

+206
-7
lines changed

1 file changed

+206
-7
lines changed
Lines changed: 206 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
// ABOUTME: CodeGraph agent definition for AutoAgents ReAct workflow
2-
// ABOUTME: Defines output format and behavior for graph analysis (tools registered manually)
2+
// ABOUTME: Defines output format, failure detection, and fallback behavior for graph analysis
33

44
use autoagents::core::agent::prebuilt::executor::ReActAgentOutput;
55
use autoagents_derive::AgentOutput;
66
use serde::{Deserialize, Serialize};
7+
use tracing::info;
8+
9+
/// Fallback message returned when the agent fails to complete its task properly.
10+
/// This guides the calling agent to retry or proceed without CodeGraph.
11+
pub const FALLBACK_MESSAGE: &str = "Tool failed to find answers to the question. Retry once with a rewritten question. If tool fails to find answers again, proceed normally without CodeGraph for this phase of the task.";
12+
13+
/// Minimum response length to be considered substantive (not a minimal/failed response)
14+
const MIN_RESPONSE_LENGTH: usize = 100;
715

816
/// CodeGraph agent output format
917
#[derive(Debug, Serialize, Deserialize, AgentOutput)]
@@ -18,25 +26,216 @@ pub struct CodeGraphAgentOutput {
1826
pub steps_taken: String,
1927
}
2028

29+
impl CodeGraphAgentOutput {
30+
/// Check if this output represents a failed/fallback agent execution
31+
pub fn is_failure(&self) -> bool {
32+
self.answer == FALLBACK_MESSAGE || self.answer.is_empty()
33+
}
34+
35+
/// Create a fallback response with debug information
36+
fn fallback(reason: &str, steps: usize) -> Self {
37+
CodeGraphAgentOutput {
38+
answer: FALLBACK_MESSAGE.to_string(),
39+
findings: format!("Fallback triggered: {}", reason),
40+
steps_taken: steps.to_string(),
41+
}
42+
}
43+
44+
/// Detect if the agent output represents a failure that should trigger fallback
45+
///
46+
/// Failure conditions:
47+
/// 1. Agent never completed (done=false)
48+
/// 2. Agent completed but didn't use tools and gave minimal response
49+
/// 3. Empty or whitespace-only response
50+
fn detect_failure(output: &ReActAgentOutput) -> Option<String> {
51+
// Check for environment variable to disable fallback (for debugging)
52+
if std::env::var("CODEGRAPH_DISABLE_FALLBACK").is_ok() {
53+
return None;
54+
}
55+
56+
let resp = &output.response;
57+
let tool_count = output.tool_calls.len();
58+
59+
// Case 1: Agent never completed
60+
if !output.done {
61+
return Some(format!(
62+
"agent did not complete (done=false, tools={}, response_len={})",
63+
tool_count,
64+
resp.len()
65+
));
66+
}
67+
68+
// Case 2: Empty or whitespace-only response
69+
if resp.trim().is_empty() {
70+
return Some(format!(
71+
"empty response (done=true, tools={})",
72+
tool_count
73+
));
74+
}
75+
76+
// Case 3: Agent completed but didn't use any tools and gave minimal response
77+
if tool_count == 0 && resp.len() < MIN_RESPONSE_LENGTH {
78+
return Some(format!(
79+
"no tools used with minimal response (done=true, tools=0, response_len={})",
80+
resp.len()
81+
));
82+
}
83+
84+
None
85+
}
86+
}
87+
2188
impl From<ReActAgentOutput> for CodeGraphAgentOutput {
2289
fn from(output: ReActAgentOutput) -> Self {
2390
let resp = output.response.clone();
2491
let num_steps = output.tool_calls.len();
2592

93+
// Step 1: Check for explicit failure conditions
94+
if let Some(reason) = CodeGraphAgentOutput::detect_failure(&output) {
95+
info!(
96+
target: "codegraph::agent::fallback",
97+
reason = %reason,
98+
done = output.done,
99+
tool_calls = num_steps,
100+
response_len = resp.len(),
101+
"Agent failed to complete task, returning fallback response"
102+
);
103+
return CodeGraphAgentOutput::fallback(&reason, num_steps);
104+
}
105+
106+
// Step 2: Try to parse as structured JSON (agent completed successfully)
26107
if output.done && !resp.trim().is_empty() {
27-
// Try to parse as structured JSON
28-
if let Ok(mut value) = serde_json::from_str::<CodeGraphAgentOutput>(&resp) {
29-
// Override steps_taken with actual count from ReActAgentOutput
30-
value.steps_taken = num_steps.to_string();
31-
return value;
108+
match serde_json::from_str::<CodeGraphAgentOutput>(&resp) {
109+
Ok(mut value) => {
110+
// Override steps_taken with actual count from ReActAgentOutput
111+
value.steps_taken = num_steps.to_string();
112+
return value;
113+
}
114+
Err(parse_err) => {
115+
// Schema parse failure - agent returned text but not in expected format
116+
// Only trigger fallback if the agent didn't use any tools
117+
// If tools were used, the raw response may still be valuable
118+
if num_steps == 0 {
119+
info!(
120+
target: "codegraph::agent::fallback",
121+
error = %parse_err,
122+
done = output.done,
123+
tool_calls = num_steps,
124+
response_preview = %resp.chars().take(100).collect::<String>(),
125+
"Agent response failed schema validation with no tool calls, returning fallback"
126+
);
127+
return CodeGraphAgentOutput::fallback(
128+
&format!("schema parse failure: {}", parse_err),
129+
num_steps,
130+
);
131+
}
132+
// Agent used tools but didn't format JSON - use raw response
133+
info!(
134+
target: "codegraph::agent",
135+
error = %parse_err,
136+
tool_calls = num_steps,
137+
"Agent response is not JSON but tools were used, using raw response"
138+
);
139+
}
32140
}
33141
}
34142

35-
// Fallback: create output from raw response with actual step count
143+
// If we reach here, agent completed with tool calls but non-JSON response
144+
// Use the raw response - the agent did work but didn't format output correctly
36145
CodeGraphAgentOutput {
37146
answer: resp,
38147
findings: String::new(),
39148
steps_taken: num_steps.to_string(),
40149
}
41150
}
42151
}
152+
153+
#[cfg(test)]
154+
mod tests {
155+
use super::*;
156+
157+
fn mock_tool_call() -> autoagents::core::tool::ToolCallResult {
158+
autoagents::core::tool::ToolCallResult {
159+
tool_name: "test_tool".to_string(),
160+
success: true,
161+
arguments: serde_json::json!({}),
162+
result: serde_json::json!({"status": "ok"}),
163+
}
164+
}
165+
166+
#[test]
167+
fn test_fallback_on_not_done() {
168+
let output = ReActAgentOutput {
169+
done: false,
170+
response: "partial answer that is long enough to pass length check".to_string(),
171+
tool_calls: vec![],
172+
};
173+
let result: CodeGraphAgentOutput = output.into();
174+
assert!(result.is_failure());
175+
assert!(result.answer.contains("Tool failed"));
176+
}
177+
178+
#[test]
179+
fn test_fallback_on_empty_response() {
180+
let output = ReActAgentOutput {
181+
done: true,
182+
response: " ".to_string(),
183+
tool_calls: vec![mock_tool_call()],
184+
};
185+
let result: CodeGraphAgentOutput = output.into();
186+
assert!(result.is_failure());
187+
}
188+
189+
#[test]
190+
fn test_fallback_on_no_tools_minimal_response() {
191+
let output = ReActAgentOutput {
192+
done: true,
193+
response: "ok".to_string(),
194+
tool_calls: vec![],
195+
};
196+
let result: CodeGraphAgentOutput = output.into();
197+
assert!(result.is_failure());
198+
}
199+
200+
#[test]
201+
fn test_fallback_on_schema_parse_failure() {
202+
let output = ReActAgentOutput {
203+
done: true,
204+
response: "This is a plain text response that is definitely long enough but not JSON formatted at all".to_string(),
205+
tool_calls: vec![], // No tools = fallback
206+
};
207+
let result: CodeGraphAgentOutput = output.into();
208+
assert!(result.is_failure());
209+
}
210+
211+
#[test]
212+
fn test_no_fallback_on_valid_json() {
213+
let valid_output = serde_json::json!({
214+
"answer": "Detailed analysis of the codebase showing authentication flow...",
215+
"findings": "Found 5 key components",
216+
"steps_taken": "3"
217+
});
218+
let output = ReActAgentOutput {
219+
done: true,
220+
response: valid_output.to_string(),
221+
tool_calls: vec![mock_tool_call()],
222+
};
223+
let result: CodeGraphAgentOutput = output.into();
224+
assert!(!result.is_failure());
225+
assert!(result.answer.contains("authentication"));
226+
}
227+
228+
#[test]
229+
fn test_raw_response_with_tool_calls_not_fallback() {
230+
// If agent used tools but gave non-JSON response, we use the raw response
231+
let output = ReActAgentOutput {
232+
done: true,
233+
response: "The authentication system uses JWT tokens stored in the database. Key files: auth.rs, token.rs".to_string(),
234+
tool_calls: vec![mock_tool_call(), mock_tool_call()],
235+
};
236+
let result: CodeGraphAgentOutput = output.into();
237+
// This should NOT be a fallback because the agent did use tools
238+
assert!(!result.is_failure());
239+
assert!(result.answer.contains("JWT"));
240+
}
241+
}

0 commit comments

Comments
 (0)