Skip to content

Commit c1ff7bc

Browse files
authored
fix(agents): stop dropping self-paired tool messages during sanitation (agentscope-ai#5792)
1 parent a3be71b commit c1ff7bc

2 files changed

Lines changed: 56 additions & 5 deletions

File tree

src/qwenpaw/agents/utils/tool_message_utils.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,17 @@ def _reorder_tool_results(msgs: list) -> list:
8787
result_msg_ids: set[int] = set()
8888
for msg in msgs:
8989
if isinstance(msg.content, list):
90+
# Keep a message that carries its own tool_call in place (e.g. an
91+
# AgentScope 2.0 self-paired assistant msg
92+
# [text, tool_call, tool_result]); only standalone tool_result
93+
# messages are movable. Skipping a self-carrying msg here would
94+
# drop it, since its own tool_call can never re-insert it.
95+
has_own_call = any(
96+
_is_tool_call(block) and _block_attr(block, "id")
97+
for block in msg.content
98+
)
99+
if has_own_call:
100+
continue
90101
for block in msg.content:
91102
if _is_tool_result(block) and _block_attr(block, "id"):
92103
results_by_id.setdefault(
@@ -131,17 +142,25 @@ def _remove_unpaired_tool_messages(msgs: list) -> list:
131142

132143
i = 0
133144
while i < len(msgs):
134-
use_ids, _ = extract_tool_ids(msgs[i])
145+
use_ids, own_results = extract_tool_ids(msgs[i])
135146
if not use_ids:
136147
i += 1
137148
continue
138-
required = set(use_ids)
149+
# A self-paired message satisfies (some of) its own tool_calls with the
150+
# tool_results in the same message; only the remainder must be covered
151+
# by following messages.
152+
required = set(use_ids) - own_results
139153
j = i + 1
140154
result_indices: list[int] = []
141155
while j < len(msgs) and required:
142-
_, r = extract_tool_ids(msgs[j])
156+
uj, r = extract_tool_ids(msgs[j])
143157
if not r:
144158
break
159+
# A following message that also carries tool_calls (e.g. another
160+
# self-paired turn) is not a result-provider for this one; stop
161+
# rather than consuming it.
162+
if uj:
163+
break
145164
required -= r
146165
result_indices.append(j)
147166
j += 1
@@ -160,8 +179,10 @@ def _remove_unpaired_tool_messages(msgs: list) -> list:
160179
for idx, msg in enumerate(msgs):
161180
if idx in to_remove:
162181
continue
163-
_, r = extract_tool_ids(msg)
164-
if r and not r.issubset(surviving_use_ids):
182+
u, r = extract_tool_ids(msg)
183+
# A tool_result is paired if some surviving tool_call uses its id --
184+
# including a tool_call in the same (self-paired) message.
185+
if r and not r.issubset(surviving_use_ids | u):
165186
to_remove.add(idx)
166187

167188
return [msg for idx, msg in enumerate(msgs) if idx not in to_remove]

tests/unit/agents/utils/test_tool_message_utils.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,3 +520,33 @@ def test_removes_unpaired_tool_use(self):
520520
def test_empty_messages_returns_empty(self):
521521
result = _sanitize_tool_messages([])
522522
assert result == []
523+
524+
def test_self_paired_message_kept_when_another_block_unpaired(self):
525+
# An AgentScope 2.0 self-paired assistant message carries its own
526+
# tool_use and matching tool_result (plus text). When an *unrelated*
527+
# unpaired tool_use elsewhere triggers sanitation, the valid
528+
# self-paired turn must NOT be dropped (previously it was silently
529+
# removed, losing the text and leaving an unpaired tool_use).
530+
self_paired = _msg(
531+
[
532+
{"type": "text", "text": "keep me"},
533+
_tool_use("paired"),
534+
_tool_result("paired"),
535+
],
536+
)
537+
msgs = [
538+
_msg([_tool_use("orphan")]), # unpaired -> triggers sanitation
539+
self_paired,
540+
_msg("regular message"),
541+
]
542+
543+
result = _sanitize_tool_messages(msgs)
544+
545+
assert self_paired in result, "self-paired message must be preserved"
546+
# The unpaired orphan tool_use is still removed.
547+
remaining_uses: set = set()
548+
for m in result:
549+
u, _ = extract_tool_ids(m)
550+
remaining_uses |= u
551+
assert "orphan" not in remaining_uses
552+
assert "paired" in remaining_uses

0 commit comments

Comments
 (0)