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
44use autoagents:: core:: agent:: prebuilt:: executor:: ReActAgentOutput ;
55use autoagents_derive:: AgentOutput ;
66use 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+
2188impl 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