Skip to content

Commit e11ba49

Browse files
committed
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.
1 parent d5f7371 commit e11ba49

2 files changed

Lines changed: 69 additions & 0 deletions

File tree

astrbot/core/agent/runners/tool_loop_agent_runner.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1266,6 +1266,13 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None:
12661266
result_parts.append(
12671267
"The tool has returned a data type that is not supported."
12681268
)
1269+
else:
1270+
logger.warning(
1271+
f"Unsupported tool result content type: {type(content_item).__name__}"
1272+
)
1273+
result_parts.append(
1274+
"The tool has returned a data type that is not supported."
1275+
)
12691276
if result_parts:
12701277
inline_result = "\n\n".join(result_parts)
12711278
inline_result = await self._materialize_large_tool_result(

tests/test_tool_loop_agent_runner.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,28 @@ async def generator():
142142
return generator()
143143

144144

145+
class MockUnrecognizedContentToolExecutor:
146+
"""Tool executor returning a content type the dispatch does not recognize."""
147+
148+
@classmethod
149+
def execute(cls, tool, run_context, **tool_args):
150+
async def generator():
151+
from mcp.types import AudioContent, CallToolResult
152+
153+
result = CallToolResult(
154+
content=[
155+
AudioContent(
156+
type="audio",
157+
data="dGVzdA==",
158+
mimeType="audio/wav",
159+
),
160+
]
161+
)
162+
yield result
163+
164+
return generator()
165+
166+
145167
class VaryingUsageProvider(MockProvider):
146168
"""Return distinct token usage values for each tool-loop request."""
147169

@@ -906,6 +928,46 @@ def fake_save_image(
906928
]
907929

908930

931+
@pytest.mark.asyncio
932+
async def test_tool_result_falls_back_for_unrecognized_content(
933+
runner, mock_provider, provider_request, mock_hooks
934+
):
935+
"""A tool result must still pair with its tool_call when no content item is recognized."""
936+
937+
mock_provider.should_call_tools = True
938+
mock_provider.max_calls_before_normal_response = 1
939+
940+
await runner.reset(
941+
provider=mock_provider,
942+
request=provider_request,
943+
run_context=ContextWrapper(context=None),
944+
tool_executor=MockUnrecognizedContentToolExecutor,
945+
agent_hooks=mock_hooks,
946+
streaming=False,
947+
)
948+
949+
async for _ in runner.step_until_done(3):
950+
pass
951+
952+
tool_messages = [
953+
m for m in runner.run_context.messages if getattr(m, "role", None) == "tool"
954+
]
955+
requested_tool_call_ids = {
956+
tool_call.id
957+
for m in runner.run_context.messages
958+
if getattr(m, "role", None) == "assistant"
959+
for tool_call in (getattr(m, "tool_calls", None) or [])
960+
}
961+
answered_tool_call_ids = {m.tool_call_id for m in tool_messages}
962+
963+
assert requested_tool_call_ids
964+
assert requested_tool_call_ids == answered_tool_call_ids
965+
assert len(tool_messages) == 1
966+
assert "The tool has returned a data type that is not supported." in str(
967+
tool_messages[0].content
968+
)
969+
970+
909971
@pytest.mark.asyncio
910972
async def test_runner_replaces_runtime_image_context_before_provider_call(
911973
runner, provider_request, mock_hooks

0 commit comments

Comments
 (0)