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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user