From e11ba4936dccf54daa463308ed078e8bd98623bf Mon Sep 17 00:00:00 2001 From: Trainingcqy Date: Thu, 13 Aug 2026 16:58:35 +0900 Subject: [PATCH] fix(agent): fall back for unrecognized CallToolResult content types CallToolResult.content items are typed as the ContentBlock union, which includes TextContent, ImageContent, AudioContent, ResourceLink and EmbeddedResource. The content item dispatch in _handle_function_tools only handles TextContent, ImageContent and EmbeddedResource and has no default branch, so AudioContent and ResourceLink items contribute nothing to result_parts. When no content item matches any branch, result_parts is empty and no tool result is appended, which leaves the assistant tool_calls message unanswered and causes the provider to reject the next request. Append the unsupported-type notice this function already uses for any content item that matches no branch, and log its type name. --- .../agent/runners/tool_loop_agent_runner.py | 7 +++ tests/test_tool_loop_agent_runner.py | 62 +++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/astrbot/core/agent/runners/tool_loop_agent_runner.py b/astrbot/core/agent/runners/tool_loop_agent_runner.py index 8c91adbbfd..c026bd12b8 100644 --- a/astrbot/core/agent/runners/tool_loop_agent_runner.py +++ b/astrbot/core/agent/runners/tool_loop_agent_runner.py @@ -1266,6 +1266,13 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None: result_parts.append( "The tool has returned a data type that is not supported." ) + else: + logger.warning( + f"Unsupported tool result content type: {type(content_item).__name__}" + ) + result_parts.append( + "The tool has returned a data type that is not supported." + ) if result_parts: inline_result = "\n\n".join(result_parts) inline_result = await self._materialize_large_tool_result( diff --git a/tests/test_tool_loop_agent_runner.py b/tests/test_tool_loop_agent_runner.py index 1e679de4aa..63c5a177f5 100644 --- a/tests/test_tool_loop_agent_runner.py +++ b/tests/test_tool_loop_agent_runner.py @@ -142,6 +142,28 @@ async def generator(): return generator() +class MockUnrecognizedContentToolExecutor: + """Tool executor returning a content type the dispatch does not recognize.""" + + @classmethod + def execute(cls, tool, run_context, **tool_args): + async def generator(): + from mcp.types import AudioContent, CallToolResult + + result = CallToolResult( + content=[ + AudioContent( + type="audio", + data="dGVzdA==", + mimeType="audio/wav", + ), + ] + ) + yield result + + return generator() + + class VaryingUsageProvider(MockProvider): """Return distinct token usage values for each tool-loop request.""" @@ -906,6 +928,46 @@ def fake_save_image( ] +@pytest.mark.asyncio +async def test_tool_result_falls_back_for_unrecognized_content( + runner, mock_provider, provider_request, mock_hooks +): + """A tool result must still pair with its tool_call when no content item is recognized.""" + + mock_provider.should_call_tools = True + mock_provider.max_calls_before_normal_response = 1 + + await runner.reset( + provider=mock_provider, + request=provider_request, + run_context=ContextWrapper(context=None), + tool_executor=MockUnrecognizedContentToolExecutor, + agent_hooks=mock_hooks, + streaming=False, + ) + + async for _ in runner.step_until_done(3): + pass + + tool_messages = [ + m for m in runner.run_context.messages if getattr(m, "role", None) == "tool" + ] + requested_tool_call_ids = { + tool_call.id + for m in runner.run_context.messages + if getattr(m, "role", None) == "assistant" + for tool_call in (getattr(m, "tool_calls", None) or []) + } + answered_tool_call_ids = {m.tool_call_id for m in tool_messages} + + assert requested_tool_call_ids + assert requested_tool_call_ids == answered_tool_call_ids + assert len(tool_messages) == 1 + assert "The tool has returned a data type that is not supported." in str( + tool_messages[0].content + ) + + @pytest.mark.asyncio async def test_runner_replaces_runtime_image_context_before_provider_call( runner, provider_request, mock_hooks