Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 18 additions & 26 deletions astrbot/core/agent/runners/tool_loop_agent_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -1076,15 +1076,13 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None:
)

_final_resp: CallToolResult | None = None
tool_result_parts: list[str] = []
async for resp in self._iter_tool_executor_results(executor): # type: ignore
if isinstance(resp, CallToolResult):
res = resp
_final_resp = resp
if not res.content:
_append_tool_call_result(
func_tool_id,
"The tool returned no content.",
)
tool_result_parts.append("The tool returned no content.")
continue

result_parts: list[str] = []
Expand Down Expand Up @@ -1140,18 +1138,7 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None:
"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(
tool_call_id=func_tool_id,
content=inline_result,
)
_append_tool_call_result(
func_tool_id,
inline_result
+ self._build_repeated_tool_call_guidance(
func_tool_name, tool_call_streak
),
)
tool_result_parts.append("\n\n".join(result_parts))

elif resp is None:
# Tool 直接请求发送消息给用户
Expand All @@ -1162,26 +1149,31 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None:
)
self._transition_state(AgentState.DONE)
self.stats.end_time = time.time()
_append_tool_call_result(
func_tool_id,
tool_result_parts.append(
"The tool has no return value, or has sent the result directly to the user."
+ self._build_repeated_tool_call_guidance(
func_tool_name, tool_call_streak
),
)
else:
# 不应该出现其他类型
logger.warning(
f"Tool 返回了不支持的类型: {type(resp)}。",
)
_append_tool_call_result(
func_tool_id,
tool_result_parts.append(
"*The tool has returned an unsupported type. Please tell the user to check the definition and implementation of this tool.*"
+ self._build_repeated_tool_call_guidance(
func_tool_name, tool_call_streak
),
)

if tool_result_parts:
inline_result = await self._materialize_large_tool_result(
tool_call_id=func_tool_id,
content="\n\n".join(tool_result_parts),
)
_append_tool_call_result(
func_tool_id,
inline_result
+ self._build_repeated_tool_call_guidance(
func_tool_name, tool_call_streak
),
)

try:
await self.agent_hooks.on_tool_end(
self.run_context,
Expand Down
51 changes: 51 additions & 0 deletions tests/test_tool_loop_agent_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,22 @@ async def generator():
return generator()


class MultiYieldToolExecutor:
@classmethod
def execute(cls, tool, run_context, **tool_args):
async def generator():
from mcp.types import CallToolResult, TextContent

yield CallToolResult(
content=[TextContent(type="text", text="first partial result")]
)
yield CallToolResult(
content=[TextContent(type="text", text="second partial result")]
)

return generator()


class LargeTextToolExecutor:
"""模拟返回超长文本的工具执行器"""

Expand Down Expand Up @@ -635,6 +651,41 @@ def fake_save_image(
]


@pytest.mark.asyncio
async def test_async_generator_tool_results_share_one_tool_call_id(
runner, mock_provider, provider_request, mock_hooks
):
"""Multiple streamed tool results should be merged into one provider result."""

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=MultiYieldToolExecutor,
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"
]
assert len(tool_messages) == 1
assert tool_messages[0].tool_call_id == "call_123"

content = str(tool_messages[0].content)
assert "first partial result" in content
assert "second partial result" in content
assert content.index("first partial result") < content.index(
"second partial result"
)


@pytest.mark.asyncio
async def test_runner_replaces_runtime_image_context_before_provider_call(
runner, provider_request, mock_hooks
Expand Down
Loading