Skip to content

fix(run_agent): recover first object from concatenated tool_call args (#25333) - #26053

Closed
spranab wants to merge 1 commit into
NousResearch:mainfrom
spranab:fix/repair-concatenated-tool-call-objects
Closed

fix(run_agent): recover first object from concatenated tool_call args (#25333)#26053
spranab wants to merge 1 commit into
NousResearch:mainfrom
spranab:fix/repair-concatenated-tool-call-objects

Conversation

@spranab

@spranab spranab commented May 15, 2026

Copy link
Copy Markdown

Summary

gemini-3-flash-preview occasionally emits two parallel tool-call argument deltas merged into one streaming buffer with no delimiter, producing inputs like:

{"entity": "X", "relationship": "works_at", "target": "Y"}{"entity": "X", "relationship": "ceo_of", "target": "Y"}

The existing _repair_tool_call_arguments cascade (strict=False → trailing-comma strip → brace-balance heuristic → control-char escape) doesn't handle this shape:

  • strict=False json.loads fails on the extra data ("Extra data: line 1 column N")
  • The brace counts are already balanced (2 { and 2 }), so the brace-balance heuristic is a no-op
  • Input falls through to the {} fallback — both tool calls are dropped

This PR adds a repair pass that uses json.JSONDecoder.raw_decode() to extract the first complete top-level JSON value. If anything non-whitespace follows the parsed boundary, we log a warning naming #25333 and return the first object. At least one of the two parallel tool calls then lands instead of both being dropped.

raw_decode respects JSON string semantics, so the boundary detector is not fooled by }{ characters appearing inside string values (test test_concatenated_objects_with_braces_inside_strings covers this).

Relationship to PR #24676

This is the parser-side counterpart to the streaming-accumulator fix in #24676. That PR fixes the upstream cause (the accumulator shouldn't merge parallel deltas in the first place); this PR makes the parser graceful when the upstream cause does slip through. The two are complementary; both can land independently.

Test Plan

  • 7 new test cases in tests/run_agent/test_repair_tool_call_arguments.py covering:
  • All 27 cases pass: python -m pytest tests/run_agent/test_repair_tool_call_arguments.py -p no:xdist -o addopts= -v

Reproducer

The bug surfaced in yantrikos/yantrikdb-hermes-plugin#5, filed as #25333 here. With model=gemini-3-flash-preview and the message "import the builtin memories into yantrikdb", Hermes logs:

2026-05-13 21:01:30,386 WARNING run_agent: Unrepairable tool_call arguments for yantrikdb_relate — replaced with empty object (was: {"entity": "Don Bowman", "relationship": "ceo_of", "target": "Agilicus"}{"entity)

The trailing {"entity is the start of a second yantrikdb_relate call. With this patch the first call now lands cleanly; the second is dropped with a clear log line so operators can investigate further. Closing #24676 will additionally fix the upstream merge so neither call is ever dropped.

Refs:

…NousResearch#25333)

`gemini-3-flash-preview` occasionally emits two parallel tool_call
argument deltas merged into one streaming buffer with no delimiter,
producing inputs like:

  {"entity": "X", "relationship": "works_at", "target": "Y"}{"entity": "X", ...}

The existing repair cascade (strict=False, trailing-comma strip,
brace-balance heuristic, control-char escape) does not handle this
shape — strict=False fails on the extra data, the brace counts are
balanced (so the heuristic is a no-op), and the input falls through
to the {} fallback. Both tool calls are dropped.

Add a new repair pass between strict=False and the brace-balance
heuristic that uses json.JSONDecoder.raw_decode to extract the first
complete top-level JSON value. If anything non-whitespace follows
that boundary, we log a warning naming NousResearch#25333 and return the first
object; at least one of the two parallel tool calls then lands
instead of both being dropped.

raw_decode respects JSON string semantics, so the boundary detector
is not fooled by `}{` characters that appear inside string values
(test covers this).

This is the parser-side counterpart to the streaming-accumulator fix
in NousResearch#24676 — that PR fixes the upstream cause (the accumulator
shouldn't merge parallel deltas in the first place); this PR makes
the parser graceful when the upstream cause does slip through.

Tests: 7 new cases covering two/three concatenated objects, braces-
inside-strings (no false positive), trailing-whitespace (no false
positive), array-shaped values, and the malformed-first-object
fallback. 27 total cases pass.

Reported and reproducible from yantrikos/yantrikdb-hermes-plugin#5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint provider/gemini Google Gemini (AI Studio, Cloud Code) P2 Medium — degraded but workaround exists labels May 15, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Duplicate of #25347 — both use json.JSONDecoder.raw_decode() to extract the first complete JSON object from concatenated tool_call arguments. Also competes with #25346 (splits all objects instead of keeping only first). Fixes #25333.

@spranab

spranab commented May 15, 2026

Copy link
Copy Markdown
Author

Thanks for the cross-reference — agreed, #26053 is a duplicate of #25347 (same raw_decode approach) and a weaker version of #25346.

#25346's approach is strictly better for the actual symptom in #25333:

  • It applies the split at the streaming-accumulator boundary, not just the parser
  • N concatenated objects → N separate tool_calls instead of N-1 dropped
  • That's what donbowman's repro in yantrikos/yantrikdb-hermes-plugin#5 actually needs — the message "import the builtin memories into yantrikdb" landed exactly one yantrikdb_relate call instead of both ceo_of and works_at, which is the user-visible failure

Closing this PR. One small thing from this branch that may be worth pulling forward (against #25346 or its review feedback): the test case test_concatenated_objects_with_braces_inside_strings — covers the case where }{ appears literally inside a JSON string value (e.g. {"snippet": "if (cfg) { run(); }"}) and shouldn't trip a false boundary. Happy to send it as a small follow-up if it's not already covered in #25346's tests.

Apologies for not checking existing PRs against #25333 before filing — won't repeat the mistake.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists provider/gemini Google Gemini (AI Studio, Cloud Code) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

run_agent: 'Unrepairable tool_call arguments' when Gemini-3-Flash-Preview emits parallel tool calls as }{ — JSON parser drops them

2 participants