Skip to content

Commit 643eae7

Browse files
committed
fix: preserve multi-yield tool results
1 parent 3dbd608 commit 643eae7

2 files changed

Lines changed: 55 additions & 1 deletion

File tree

astrbot/core/agent/runners/tool_loop_agent_runner.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1096,11 +1096,19 @@ async def _handle_function_tools(
10961096
logger.info(f"Agent 使用工具: {llm_response.tools_call_name}")
10971097

10981098
def _append_tool_call_result(tool_call_id: str, content: str) -> None:
1099+
content = self._merge_follow_up_notice(content)
1100+
if (
1101+
tool_call_result_blocks
1102+
and tool_call_result_blocks[-1].tool_call_id == tool_call_id
1103+
):
1104+
previous = tool_call_result_blocks[-1]
1105+
previous.content = f"{previous.content}\n\n{content}"
1106+
return
10991107
tool_call_result_blocks.append(
11001108
ToolCallMessageSegment(
11011109
role="tool",
11021110
tool_call_id=tool_call_id,
1103-
content=self._merge_follow_up_notice(content),
1111+
content=content,
11041112
),
11051113
)
11061114

tests/test_tool_loop_agent_runner.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,24 @@ async def generator():
9999
return generator()
100100

101101

102+
class MultiYieldToolExecutor:
103+
"""Simulate a local tool that yields multiple results for one call."""
104+
105+
@classmethod
106+
def execute(cls, tool, run_context, **tool_args):
107+
async def generator():
108+
from mcp.types import CallToolResult, TextContent
109+
110+
yield CallToolResult(
111+
content=[TextContent(type="text", text="first streamed result")]
112+
)
113+
yield CallToolResult(
114+
content=[TextContent(type="text", text="second streamed result")]
115+
)
116+
117+
return generator()
118+
119+
102120
class LargeTextToolExecutor:
103121
"""模拟返回超长文本的工具执行器"""
104122

@@ -621,6 +639,34 @@ async def snapshot_context_manager(messages, trusted_token_usage=0):
621639
assert "工具执行结果" in tool_messages[0].content
622640

623641

642+
@pytest.mark.asyncio
643+
async def test_tool_loop_merges_multiple_results_from_one_tool_call(
644+
runner, provider_request, mock_hooks
645+
):
646+
"""Multiple executor yields must remain one complete provider tool result."""
647+
provider = CapturingToolLoopProvider("test_tool")
648+
649+
await runner.reset(
650+
provider=provider,
651+
request=provider_request,
652+
run_context=ContextWrapper(context=None),
653+
tool_executor=MultiYieldToolExecutor,
654+
agent_hooks=mock_hooks,
655+
streaming=False,
656+
)
657+
658+
async for _ in runner.step_until_done(3):
659+
pass
660+
661+
assert provider.call_count == 2
662+
second_contexts = provider.received_contexts[1]
663+
tool_messages = [msg for msg in second_contexts if msg.role == "tool"]
664+
assert len(tool_messages) == 1
665+
assert tool_messages[0].tool_call_id == "call_context_refresh"
666+
assert "first streamed result" in tool_messages[0].content
667+
assert "second streamed result" in tool_messages[0].content
668+
669+
624670
@pytest.mark.asyncio
625671
async def test_normal_completion_without_max_step(
626672
runner, mock_provider, provider_request, mock_tool_executor, mock_hooks

0 commit comments

Comments
 (0)