Skip to content

Commit cb51382

Browse files
authored
fix(tinyagent): Append tool_result message when tool not found (#893)
When the LLM calls a tool that doesn't exist, the error tool_result was not being appended to messages. This caused Anthropic API to return 400 errors about tool_use without corresponding tool_result.
1 parent 6d5fc6d commit cb51382

File tree

2 files changed

+125
-0
lines changed

2 files changed

+125
-0
lines changed

src/any_agent/frameworks/tinyagent.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ async def _run_async(self, prompt: str, **kwargs: Any) -> str | BaseModel:
215215
tool_message["content"] = (
216216
f"Error calling tool: No tool found with name: {tool_name}"
217217
)
218+
messages.append(tool_message)
218219
continue
219220

220221
tool_args = {}

tests/unit/frameworks/test_tinyagent.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)