@@ -266,3 +266,127 @@ def test_uses_openai_handles_gateway_provider(
266266 agent : TinyAgent = AnyAgent .create (AgentFramework .TINYAGENT , config ) # type: ignore[assignment]
267267
268268 assert agent .uses_openai is expected_uses_openai
269+
270+
271+ @pytest .mark .asyncio
272+ async def test_tool_result_appended_when_tool_not_found () -> None :
273+ """Test that tool_result message is appended when a tool is not found.
274+
275+ This test verifies that when the LLM calls a tool that doesn't exist,
276+ the error message is properly appended to the messages list. Without this,
277+ Anthropic API returns 400 errors about tool_use without tool_result.
278+ """
279+ nonexistent_tool_name = "nonexistent_tool"
280+ nonexistent_tool_call_id = "call_nonexistent"
281+ nonexistent_tool_args = '{"query": "test"}'
282+ final_tool_call_id = "call_final"
283+ final_answer_text = "Done"
284+ final_tool_args = f'{{"answer": "{ final_answer_text } "}}'
285+
286+ config = AgentConfig (model_id = DEFAULT_SMALL_MODEL_ID , tools = [sample_tool_function ])
287+ agent : TinyAgent = await AnyAgent .create_async (AgentFramework .TINYAGENT , config ) # type: ignore[assignment]
288+
289+ def create_mock_nonexistent_tool_response () -> MagicMock :
290+ """Mock a tool call response for a non-existent tool.
291+
292+ The LLM expects the response to contain a tool_result, even if that
293+ result is an error. A response with it missing causes the Anthropic
294+ API to return 400 errors about tool_use without tool_result.
295+ """
296+ mock_message = MagicMock ()
297+ mock_message .content = None
298+ mock_message .role = "assistant"
299+
300+ mock_tool_call = MagicMock ()
301+ mock_tool_call .id = nonexistent_tool_call_id
302+ mock_function = MagicMock ()
303+ mock_function .name = nonexistent_tool_name
304+ mock_function .arguments = nonexistent_tool_args
305+ mock_tool_call .function = mock_function
306+ mock_message .tool_calls = [mock_tool_call ]
307+
308+ mock_message .model_dump .return_value = {
309+ "content" : None ,
310+ "role" : "assistant" ,
311+ "tool_calls" : [
312+ {
313+ "id" : nonexistent_tool_call_id ,
314+ "function" : {
315+ "name" : nonexistent_tool_name ,
316+ "arguments" : nonexistent_tool_args ,
317+ },
318+ "type" : "function" ,
319+ }
320+ ],
321+ }
322+ return MagicMock (choices = [MagicMock (message = mock_message )])
323+
324+ def create_mock_final_response () -> MagicMock :
325+ """Mock a final_answer tool call to end the agent loop.
326+
327+ This allows the test to complete successfully after the nonexistent
328+ tool error is handled.
329+ """
330+ mock_message = MagicMock ()
331+ mock_message .content = None
332+ mock_message .role = "assistant"
333+
334+ mock_tool_call = MagicMock ()
335+ mock_tool_call .id = final_tool_call_id
336+ mock_function = MagicMock ()
337+ mock_function .name = "final_answer"
338+ mock_function .arguments = final_tool_args
339+ mock_tool_call .function = mock_function
340+ mock_message .tool_calls = [mock_tool_call ]
341+
342+ mock_message .model_dump .return_value = {
343+ "content" : None ,
344+ "role" : "assistant" ,
345+ "tool_calls" : [
346+ {
347+ "id" : final_tool_call_id ,
348+ "function" : {
349+ "name" : "final_answer" ,
350+ "arguments" : final_tool_args ,
351+ },
352+ "type" : "function" ,
353+ }
354+ ],
355+ }
356+ return MagicMock (choices = [MagicMock (message = mock_message )])
357+
358+ with patch (LLM_IMPORT_PATHS [AgentFramework .TINYAGENT ]) as mock_acompletion :
359+ mock_acompletion .side_effect = [
360+ create_mock_nonexistent_tool_response (),
361+ create_mock_final_response (),
362+ ]
363+
364+ result = await agent .run_async ("Call a tool" )
365+
366+ assert result .final_output == final_answer_text
367+ assert mock_acompletion .call_count == 2
368+
369+ # Verify the second call includes the tool_result for the nonexistent tool.
370+ second_call_messages = mock_acompletion .call_args_list [1 ][1 ]["messages" ]
371+
372+ # Find the assistant message containing the tool_use for the nonexistent tool.
373+ assistant_msg_index = None
374+ for i , msg in enumerate (second_call_messages ):
375+ if msg .get ("role" ) == "assistant" :
376+ tool_calls = msg .get ("tool_calls" , [])
377+ if tool_calls and tool_calls [0 ].get ("id" ) == nonexistent_tool_call_id :
378+ assistant_msg_index = i
379+ break
380+
381+ assert assistant_msg_index is not None
382+
383+ # Verify tool_result immediately follows the assistant message.
384+ # Anthropic requires tool_result blocks immediately after tool_use.
385+ tool_result_msg = second_call_messages [assistant_msg_index + 1 ]
386+ assert tool_result_msg .get ("role" ) == "tool"
387+ assert tool_result_msg .get ("tool_call_id" ) == nonexistent_tool_call_id
388+ assert tool_result_msg .get ("name" ) == nonexistent_tool_name
389+ assert (
390+ f"No tool found with name: { nonexistent_tool_name } "
391+ in tool_result_msg ["content" ]
392+ )
0 commit comments