From 33d93ae798183540d6774f1a8165f3bb06fb51ad Mon Sep 17 00:00:00 2001 From: Luna-channel Date: Tue, 24 Feb 2026 23:52:15 +0800 Subject: [PATCH 1/2] fix: ensure tool call/response pairing in context truncation --- astrbot/core/agent/context/truncator.py | 66 +++++++++++++++++++++---- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/astrbot/core/agent/context/truncator.py b/astrbot/core/agent/context/truncator.py index 8d1da6f569..4b4aa3a460 100644 --- a/astrbot/core/agent/context/truncator.py +++ b/astrbot/core/agent/context/truncator.py @@ -4,19 +4,65 @@ class ContextTruncator: """Context truncator.""" + def _has_tool_calls(self, message: Message) -> bool: + """Check if a message contains tool calls.""" + return ( + message.role == "assistant" + and message.tool_calls is not None + and len(message.tool_calls) > 0 + ) + def fix_messages(self, messages: list[Message]) -> list[Message]: - fixed_messages = [] - for message in messages: - if message.role == "tool": - # tool block 前面必须要有 user 和 assistant block - if len(fixed_messages) < 2: - # 这种情况可能是上下文被截断导致的 - # 我们直接将之前的上下文都清空 - fixed_messages = [] + """修复消息列表,确保 tool call 和 tool response 的配对关系有效。 + + 此方法确保: + 1. 每个 `tool` 消息前面都有一个包含 tool_calls 的 `assistant` 消息 + 2. 每个包含 tool_calls 的 `assistant` 消息后面都有对应的 `tool` 响应 + + 这是 OpenAI Chat Completions API 规范的要求(Gemini 对此执行严格检查)。 + """ + if not messages: + return messages + + # First pass: identify which assistant(tool_calls) have valid tool responses + # Build a set of indices for assistant messages that have valid tool responses + valid_tool_call_indices: set[int] = set() + i = 0 + while i < len(messages): + msg = messages[i] + if self._has_tool_calls(msg): + # Check if next message(s) are tool responses + j = i + 1 + has_tool_response = False + while j < len(messages) and messages[j].role == "tool": + has_tool_response = True + j += 1 + if has_tool_response: + valid_tool_call_indices.add(i) + i += 1 + + # Second pass: build fixed message list + fixed_messages: list[Message] = [] + in_valid_tool_chain = False # 是否处于有效的 tool call 链中 + + for i, msg in enumerate(messages): + if msg.role == "tool": + # tool 消息:只有在有效的 tool call 链中才保留 + if in_valid_tool_chain: + fixed_messages.append(msg) + # else: 孤立的 tool 消息,跳过 + elif self._has_tool_calls(msg): + # assistant(tool_calls):只保留有效的(后面有 tool response 的) + if i in valid_tool_call_indices: + fixed_messages.append(msg) + in_valid_tool_chain = True # 进入有效的 tool call 链 else: - fixed_messages.append(message) + in_valid_tool_chain = False # 孤立的 tool_calls,跳过并重置状态 else: - fixed_messages.append(message) + # system, user, 或不含 tool_calls 的 assistant + fixed_messages.append(msg) + in_valid_tool_chain = False # 退出 tool call 链 + return fixed_messages def truncate_by_turns( From a7754c02be267da6d193486c37cd2ed21507c5d2 Mon Sep 17 00:00:00 2001 From: Luna-channel Date: Wed, 25 Feb 2026 00:00:04 +0800 Subject: [PATCH 2/2] refactor: simplify fix_messages to single-pass state machine --- astrbot/core/agent/context/truncator.py | 67 ++++++++++++------------- 1 file changed, 31 insertions(+), 36 deletions(-) diff --git a/astrbot/core/agent/context/truncator.py b/astrbot/core/agent/context/truncator.py index 4b4aa3a460..afd89f2bed 100644 --- a/astrbot/core/agent/context/truncator.py +++ b/astrbot/core/agent/context/truncator.py @@ -24,44 +24,39 @@ def fix_messages(self, messages: list[Message]) -> list[Message]: if not messages: return messages - # First pass: identify which assistant(tool_calls) have valid tool responses - # Build a set of indices for assistant messages that have valid tool responses - valid_tool_call_indices: set[int] = set() - i = 0 - while i < len(messages): - msg = messages[i] - if self._has_tool_calls(msg): - # Check if next message(s) are tool responses - j = i + 1 - has_tool_response = False - while j < len(messages) and messages[j].role == "tool": - has_tool_response = True - j += 1 - if has_tool_response: - valid_tool_call_indices.add(i) - i += 1 - - # Second pass: build fixed message list fixed_messages: list[Message] = [] - in_valid_tool_chain = False # 是否处于有效的 tool call 链中 - - for i, msg in enumerate(messages): + pending_assistant: Message | None = None + pending_tools: list[Message] = [] + + def flush_pending_if_valid() -> None: + nonlocal pending_assistant, pending_tools + if pending_assistant is not None and pending_tools: + fixed_messages.append(pending_assistant) + fixed_messages.extend(pending_tools) + pending_assistant = None + pending_tools = [] + + for msg in messages: if msg.role == "tool": - # tool 消息:只有在有效的 tool call 链中才保留 - if in_valid_tool_chain: - fixed_messages.append(msg) - # else: 孤立的 tool 消息,跳过 - elif self._has_tool_calls(msg): - # assistant(tool_calls):只保留有效的(后面有 tool response 的) - if i in valid_tool_call_indices: - fixed_messages.append(msg) - in_valid_tool_chain = True # 进入有效的 tool call 链 - else: - in_valid_tool_chain = False # 孤立的 tool_calls,跳过并重置状态 - else: - # system, user, 或不含 tool_calls 的 assistant - fixed_messages.append(msg) - in_valid_tool_chain = False # 退出 tool call 链 + # 只有在有挂起的 assistant(tool_calls) 时才记录 tool 响应 + if pending_assistant is not None: + pending_tools.append(msg) + # else: 孤立的 tool 消息,直接忽略 + continue + + if self._has_tool_calls(msg): + # 遇到新的 assistant(tool_calls) 前,先处理旧的 pending 链 + flush_pending_if_valid() + pending_assistant = msg + continue + + # 非 tool,且不含 tool_calls 的消息 + # 先结束任何 pending 链,再正常追加 + flush_pending_if_valid() + fixed_messages.append(msg) + + # 结束时处理最后一个 pending 链 + flush_pending_if_valid() return fixed_messages