fix(agent): harden canonical tool call deduplication
This commit is contained in:
15
run_agent.py
15
run_agent.py
@@ -4879,19 +4879,28 @@ class AIAgent:
|
||||
|
||||
Valid JSON arguments are canonicalized so equivalent objects do not
|
||||
evade deduplication merely because their keys or whitespace differ.
|
||||
Malformed arguments retain their raw representation rather than being
|
||||
repaired here. Only the first occurrence of each unique pair is kept.
|
||||
Duplicate object keys use the parser's last-value-wins semantics,
|
||||
matching downstream argument parsing. Malformed or excessively nested
|
||||
arguments retain their raw representation rather than being repaired
|
||||
here. Only the first occurrence of each unique pair is kept.
|
||||
Returns the original list if no duplicates were found.
|
||||
"""
|
||||
seen_raw: set = set()
|
||||
seen: set = set()
|
||||
unique: list = []
|
||||
for tc in tool_calls:
|
||||
raw_key = (tc.function.name, tc.function.arguments)
|
||||
if raw_key in seen_raw:
|
||||
logger.warning("Removed duplicate tool call: %s", tc.function.name)
|
||||
continue
|
||||
seen_raw.add(raw_key)
|
||||
|
||||
arguments = tc.function.arguments
|
||||
try:
|
||||
arguments = json.dumps(
|
||||
json.loads(arguments), separators=(",", ":"), sort_keys=True
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
except (RecursionError, TypeError, ValueError):
|
||||
pass
|
||||
key = (tc.function.name, arguments)
|
||||
if key not in seen:
|
||||
|
||||
@@ -7,6 +7,7 @@ Covers three static methods on AIAgent (inspired by PR #1321 — @alireza78a):
|
||||
- _uniquify_tool_call_ids() — Phase 2c: duplicate-id repair (lossless pairing)
|
||||
"""
|
||||
|
||||
import json
|
||||
import types
|
||||
|
||||
import pytest
|
||||
@@ -212,6 +213,36 @@ class TestDeduplicateToolCalls:
|
||||
|
||||
assert out == [first, distinct]
|
||||
|
||||
def test_recursion_error_uses_raw_string_for_deduplication(self, monkeypatch):
|
||||
def raise_recursion_error(_arguments):
|
||||
raise RecursionError("maximum recursion depth exceeded")
|
||||
|
||||
monkeypatch.setattr("run_agent.json.loads", raise_recursion_error)
|
||||
first = make_tc("terminal", '{"command":"one"}')
|
||||
duplicate = make_tc("terminal", '{"command":"one"}')
|
||||
|
||||
out = AIAgent._deduplicate_tool_calls([first, duplicate])
|
||||
|
||||
assert out == [first]
|
||||
|
||||
def test_byte_identical_duplicate_is_parsed_only_once(self, monkeypatch):
|
||||
original_loads = json.loads
|
||||
calls = 0
|
||||
|
||||
def track_loads(arguments):
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
return original_loads(arguments)
|
||||
|
||||
monkeypatch.setattr("run_agent.json.loads", track_loads)
|
||||
first = make_tc("terminal", '{"command":"one"}')
|
||||
duplicate = make_tc("terminal", '{"command":"one"}')
|
||||
|
||||
out = AIAgent._deduplicate_tool_calls([first, duplicate])
|
||||
|
||||
assert out == [first]
|
||||
assert calls == 1
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user