@@ -267,6 +267,25 @@ impl MockLlmClient {
267267 }
268268 }
269269
270+ pub ( crate ) fn reasoning_only_response ( reasoning : & str ) -> LlmResponse {
271+ LlmResponse {
272+ message : Message {
273+ role : "assistant" . to_string ( ) ,
274+ content : Vec :: new ( ) ,
275+ reasoning_content : Some ( reasoning. to_string ( ) ) ,
276+ } ,
277+ usage : TokenUsage {
278+ prompt_tokens : 10 ,
279+ completion_tokens : 5 ,
280+ total_tokens : 15 ,
281+ cache_read_tokens : None ,
282+ cache_write_tokens : None ,
283+ } ,
284+ stop_reason : Some ( "stop" . to_string ( ) ) ,
285+ meta : None ,
286+ }
287+ }
288+
270289 /// Create a response with a tool call
271290 pub ( crate ) fn tool_call_response (
272291 tool_id : & str ,
@@ -404,6 +423,57 @@ async fn test_agent_simple_response() {
404423 assert_eq ! ( mock_client. call_count. load( Ordering :: SeqCst ) , 1 ) ;
405424}
406425
426+ #[ tokio:: test]
427+ async fn test_agent_repairs_reasoning_only_response_once ( ) {
428+ let mock_client = Arc :: new ( MockLlmClient :: new ( vec ! [
429+ MockLlmClient :: reasoning_only_response( "I have the answer but put it in reasoning." ) ,
430+ MockLlmClient :: text_response( "The answer is 42." ) ,
431+ ] ) ) ;
432+
433+ let tool_executor = Arc :: new ( ToolExecutor :: new ( "/tmp" . to_string ( ) ) ) ;
434+ let config = AgentConfig {
435+ max_continuation_turns : 0 ,
436+ ..Default :: default ( )
437+ } ;
438+
439+ let agent = AgentLoop :: new (
440+ mock_client. clone ( ) ,
441+ tool_executor,
442+ test_tool_context ( ) ,
443+ config,
444+ ) ;
445+ let result = agent. execute ( & [ ] , "Answer plainly" , None ) . await . unwrap ( ) ;
446+
447+ assert_eq ! ( result. text, "The answer is 42." ) ;
448+ assert_eq ! ( mock_client. call_count. load( Ordering :: SeqCst ) , 2 ) ;
449+ }
450+
451+ #[ tokio:: test]
452+ async fn test_agent_stops_after_repeated_reasoning_only_response ( ) {
453+ let mock_client = Arc :: new ( MockLlmClient :: new ( vec ! [
454+ MockLlmClient :: reasoning_only_response( "Thinking only, first pass." ) ,
455+ MockLlmClient :: reasoning_only_response( "Thinking only, second pass." ) ,
456+ MockLlmClient :: text_response( "This response should not be consumed." ) ,
457+ ] ) ) ;
458+
459+ let tool_executor = Arc :: new ( ToolExecutor :: new ( "/tmp" . to_string ( ) ) ) ;
460+ let config = AgentConfig :: default ( ) ;
461+
462+ let agent = AgentLoop :: new (
463+ mock_client. clone ( ) ,
464+ tool_executor,
465+ test_tool_context ( ) ,
466+ config,
467+ ) ;
468+ let result = agent. execute ( & [ ] , "Answer plainly" , None ) . await . unwrap ( ) ;
469+
470+ assert_eq ! (
471+ result. text,
472+ "The model completed but returned only reasoning content and did not provide a final answer."
473+ ) ;
474+ assert_eq ! ( mock_client. call_count. load( Ordering :: SeqCst ) , 2 ) ;
475+ }
476+
407477#[ tokio:: test]
408478async fn test_agent_with_tool_call ( ) {
409479 let mock_client = Arc :: new ( MockLlmClient :: new ( vec ! [
0 commit comments