fix(agent): recover tool_call arguments that arrived wrapped - #79333
fix(agent): recover tool_call arguments that arrived wrapped#79333Drexuxux wants to merge 1 commit into
Conversation
_repair_tool_call_arguments is a repair ladder for the malformed argument
JSON local models emit, falling back to "{}" only when nothing parses.
Two shapes reached that fallback carrying a complete, valid payload:
```json\n{"path": "a.txt"}\n``` -> {}
{"path": "a.txt"} trailing prose -> {}
Both are ordinary model behaviour — a fenced object, or an object followed
by an explanation — and both hold the arguments the model actually chose.
Discarding them runs the tool with NO arguments, so the call fails on a
missing required parameter and the turn is spent on a confusing error
instead of the requested action.
Add one pass before the character-surgery repairs: strip a surrounding
markdown fence, then take the leading JSON value via JSONDecoder.raw_decode
(which also covers trailing prose). Well-formed input is untouched — it
still returns from the existing pass 0 — and input that genuinely does not
parse still ends at "{}".
|
Thanks for the work here — reviewed against current The premise is technically true (both shapes currently fall through the repair ladder to There are also concrete defects that would need addressing even if we wanted this: Closing per policy — appreciate the thorough investigation regardless. |
What
_repair_tool_call_argumentsis a repair ladder for the malformed argument JSON local models emit — its docstring names truncated JSON, trailing commas and PythonNone, and it falls back to"{}"only when nothing parses, "better than crashing the session."Two shapes were reaching that fallback while carrying a complete, valid payload:
Both are ordinary model behaviour — a fenced object, or an object followed by an explanatory sentence — and both hold exactly the arguments the model chose. Discarding them runs the tool with no arguments at all, so the call fails on a missing required parameter (
write_filewith nopath,terminalwith nocommand) and the turn is spent on a confusing error instead of the requested action. The log line even saysUnrepairable tool_call arguments … replaced with empty objectfor a payload that parses fine once unwrapped.Found by fuzzing the function: 14 well-formed and 13 malformed argument strings, checking that the result parses and that a well-formed payload survives unchanged. These two were the only recoverable inputs being thrown away.
The fix
One pass added before the existing character-surgery repairs: strip a surrounding markdown fence, then take the leading JSON value with
json.JSONDecoder().raw_decode(), which covers the trailing-prose case in the same step. The recovery is logged like every other repair in this function, including how many surrounding characters were dropped.Ordering keeps the change inert for everything that already worked:
json.loads(..., strict=False)) returns first"{}"Verified on the same fuzz corpus: the two wrapped cases now return
{"path":"a.txt"}, every other input's output is byte-identical to before, and 0/14 well-formed payloads are damaged.Tests
Added
TestWrappedArgumentRecoverytotests/run_agent/test_repair_tool_call_arguments.py:```jsonfenced object is recovered with both keys intact```fence with no language tag is recovered{}Results:
That is this file plus
test_streaming_tool_call_repair.py,test_repair_tool_call_name.pyandtests/agent/test_canon_args_memo_parity.py, with no pre-existing failures.Without the source change, on the same files:
25 + 3 = 28, so the delta is exactly the new tests flipping green.
tests/run_agent/test_dict_tool_call_args.py,test_dropped_tool_call_recovery.pyandtest_message_sequence_repair.pyalso pass (19) unchanged, andscripts/check-windows-footguns.pyis clean on both files.