From bc06bc12dbbc3b56dc1c9057f1d018c247e154d4 Mon Sep 17 00:00:00 2001 From: renjianbo <18691577328@163.com> Date: Thu, 7 May 2026 23:59:42 +0800 Subject: [PATCH] fix: trim_messages prevents orphaned tool messages after history truncation When conversation history exceeds max_history, the blind slice in trim_messages could cut between an assistant(tool_calls) message and its tool result messages. DeepSeek/OpenAI requires every role:"tool" message to follow a preceding assistant message with matching tool_calls. The fix detects when the slice boundary lands on a tool message, then scans backwards to include the parent assistant(tool_calls) message(s). If the parent cannot be found (e.g. cut at a user boundary), the orphaned tool messages are removed. This fixes the "Messages with role 'tool' must be a response to a preceding message with 'tool_calls'" error from DeepSeek. Co-Authored-By: Claude Opus 4.6 --- backend/app/agent_runtime/memory.py | 36 ++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/backend/app/agent_runtime/memory.py b/backend/app/agent_runtime/memory.py index 88250c6..98cf030 100644 --- a/backend/app/agent_runtime/memory.py +++ b/backend/app/agent_runtime/memory.py @@ -514,6 +514,10 @@ class AgentMemory: def trim_messages(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]: """ 裁剪消息列表:保留最近的 N 条,但始终保留第一条 system 消息。 + + 同时保证 assistant(tool_calls) 与 tool 消息的配对完整性: + 如果裁剪边界落在 assistant(tool_calls) 和其 tool 结果之间, + 则向前扩展窗口包含该 assistant 消息,避免孤立的 tool 消息。 """ if len(messages) <= self.max_history: return messages @@ -521,7 +525,37 @@ class AgentMemory: system_msgs = [m for m in messages if m.get("role") == "system"] other_msgs = [m for m in messages if m.get("role") != "system"] - trimmed = other_msgs[-(self.max_history - len(system_msgs)):] + max_keep = max(1, self.max_history - len(system_msgs)) + start_idx = max(0, len(other_msgs) - max_keep) + + # 如果裁剪后第一条是 tool 消息,向前找到其父 assistant(tool_calls) + if start_idx > 0 and start_idx < len(other_msgs) and other_msgs[start_idx].get("role") == "tool": + # 收集从 start_idx 开始连续的所有 tool 消息 + tool_count = 0 + for i in range(start_idx, len(other_msgs)): + if other_msgs[i].get("role") == "tool": + tool_count += 1 + else: + break + # 向前查找对应的 assistant(tool_calls),一个 assistant 可包含多个 tool_calls + needed = tool_count + cursor = start_idx - 1 + while cursor >= 0 and needed > 0: + role = other_msgs[cursor].get("role") + if role == "assistant" and other_msgs[cursor].get("tool_calls"): + needed -= len(other_msgs[cursor]["tool_calls"]) + elif role == "user": + # 遇到 user 说明上一轮已结束,放弃扩展 + break + cursor -= 1 + if needed <= 0: + # 找到了所有父 assistant 消息,扩展窗口 + start_idx = cursor + 1 + + trimmed = other_msgs[start_idx:] + # 最终安全检查:移除开头仍存在的孤立 tool 消息 + while trimmed and trimmed[0].get("role") == "tool": + trimmed.pop(0) return system_msgs + trimmed @staticmethod