@@ -792,6 +792,22 @@ def test_exit_condition_exits(self, monkeypatch, weather_tool):
792792 assert isinstance (result ["last_message" ], ChatMessage )
793793 assert result ["messages" ][- 1 ] == result ["last_message" ]
794794
795+ def test_does_not_exit_on_empty_assistant_message (self , monkeypatch , weather_tool ):
796+ monkeypatch .setenv ("OPENAI_API_KEY" , "fake-key" )
797+ agent = Agent (chat_generator = OpenAIChatGenerator (), tools = [weather_tool ], exit_conditions = ["text" ])
798+
799+ # The first reply simulates the LLM producing an invalid tool call that our code discards,leaving an assistant
800+ # message with empty text and no tool calls. This must not be treated as a "text" exit condition, so the agent
801+ # keeps looping and recovers on the second reply.
802+ empty_reply = {"replies" : [ChatMessage .from_assistant (text = "" )]}
803+ recovered_reply = {"replies" : [ChatMessage .from_assistant (text = "The weather is sunny." )]}
804+ agent .chat_generator .run = MagicMock (side_effect = [empty_reply , recovered_reply ])
805+
806+ result = agent .run ([ChatMessage .from_user ("What's the weather?" )])
807+
808+ assert agent .chat_generator .run .call_count == 2
809+ assert result ["last_message" ].text == "The weather is sunny."
810+
795811 def test_check_exit_conditions_parallel_tool_calls (self , monkeypatch , weather_tool ):
796812 monkeypatch .setenv ("OPENAI_API_KEY" , "fake-key" )
797813 agent = Agent (chat_generator = OpenAIChatGenerator (), tools = [weather_tool ], exit_conditions = ["weather_tool" ])
@@ -1077,6 +1093,23 @@ def streaming_callback(chunk: StreamingChunk) -> None:
10771093 assert result ["last_message" ] is not None
10781094 assert streaming_callback_called
10791095
1096+ @pytest .mark .asyncio
1097+ async def test_does_not_exit_on_empty_assistant_message_async (self , monkeypatch , weather_tool ):
1098+ monkeypatch .setenv ("OPENAI_API_KEY" , "fake-key" )
1099+ agent = Agent (chat_generator = OpenAIChatGenerator (), tools = [weather_tool ], exit_conditions = ["text" ])
1100+
1101+ # The first reply simulates the LLM producing an invalid tool call that our code discards,leaving an assistant
1102+ # message with empty text and no tool calls. This must not be treated as a "text" exit condition, so the agent
1103+ # keeps looping and recovers on the second reply.
1104+ empty_reply = {"replies" : [ChatMessage .from_assistant (text = "" )]}
1105+ recovered_reply = {"replies" : [ChatMessage .from_assistant (text = "The weather is sunny." )]}
1106+ agent .chat_generator .run_async = AsyncMock (side_effect = [empty_reply , recovered_reply ])
1107+
1108+ result = await agent .run_async ([ChatMessage .from_user ("What's the weather?" )])
1109+
1110+ assert agent .chat_generator .run_async .call_count == 2
1111+ assert result ["last_message" ].text == "The weather is sunny."
1112+
10801113 @pytest .mark .asyncio
10811114 async def test_run_async_with_async_streaming_callback (self , weather_tool ):
10821115 chat_generator = MockChatGenerator ()
0 commit comments