From fcd2f4eb18a2bb7085cf227a106feed3d0b902ac Mon Sep 17 00:00:00 2001 From: pei711 <1259636180@qq.com> Date: Wed, 14 Oct 2026 06:44:10 +0000 Subject: [PATCH] fix(agent): keep replayed tool call IDs stable (#43796) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: zjp <你的邮箱> --- api/core/agent/base_agent_runner.py | 6 ++-- .../core/agent/test_base_agent_runner.py | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/api/core/agent/base_agent_runner.py b/api/core/agent/base_agent_runner.py index 806f7c6590f..0bbc3880c58 100644 --- a/api/core/agent/base_agent_runner.py +++ b/api/core/agent/base_agent_runner.py @@ -408,9 +408,9 @@ class BaseAgentRunner(AppRunner): else: tool_responses = dict.fromkeys(tool_names, observation_payload) - for tool in tool_names: - # generate a uuid for tool call - tool_call_id = str(uuid.uuid4()) + for index, tool in enumerate(tool_names): + # Keep IDs stable across turns so replayed history can use prompt caching. + tool_call_id = str(uuid.uuid5(uuid.NAMESPACE_OID, f"{agent_thought.id}:{index}:{tool}")) tool_calls.append( AssistantPromptMessage.ToolCall( id=tool_call_id, diff --git a/api/tests/unit_tests/core/agent/test_base_agent_runner.py b/api/tests/unit_tests/core/agent/test_base_agent_runner.py index 71ad572cce1..c1bbdf8e2d9 100644 --- a/api/tests/unit_tests/core/agent/test_base_agent_runner.py +++ b/api/tests/unit_tests/core/agent/test_base_agent_runner.py @@ -611,6 +611,41 @@ def test_organize_history_reconstructs_tool_flows( assert any(isinstance(item, module.AssistantPromptMessage) for item in result) +def test_organize_history_reuses_stable_tool_call_ids( + runner: BaseAgentRunner, + sqlite_session: Session, +) -> None: + message = _message(message_id="m2") + thought = _thought( + thought_id="thought-1", + message_id=message.id, + tool="tool1;tool2", + tool_input=json.dumps({"tool1": {}, "tool2": {}}), + observation=json.dumps({"tool1": "o1", "tool2": "o2"}), + ) + _persist_history(sqlite_session, message, thoughts=[thought]) + + first = runner.organize_agent_history([], session=sqlite_session) + second = runner.organize_agent_history([], session=sqlite_session) + + def tool_call_ids(messages: list[module.PromptMessage]) -> tuple[list[str], list[str]]: + call_ids = [ + tool_call.id + for prompt in messages + if isinstance(prompt, module.AssistantPromptMessage) + for tool_call in prompt.tool_calls or [] + ] + response_ids = [prompt.tool_call_id for prompt in messages if isinstance(prompt, module.ToolPromptMessage)] + return call_ids, response_ids + + first_call_ids, first_response_ids = tool_call_ids(first) + second_call_ids, second_response_ids = tool_call_ids(second) + + assert first_call_ids == second_call_ids + assert first_call_ids == first_response_ids == second_response_ids + assert len(set(first_call_ids)) == 2 + + def test_organize_history_without_tool_name(runner: BaseAgentRunner, sqlite_session: Session) -> None: message = _message(message_id="m3") thought = _thought(thought_id="thought-1", message_id=message.id, tool=None)