Skip to content

fix(run_agent): split concatenated streamed tool-call args - #25346

Closed
LeonSGP43 wants to merge 1 commit into
NousResearch:mainfrom
LeonSGP43:hermes/fix-25333-gemini-parallel-tool-calls
Closed

fix(run_agent): split concatenated streamed tool-call args#25346
LeonSGP43 wants to merge 1 commit into
NousResearch:mainfrom
LeonSGP43:hermes/fix-25333-gemini-parallel-tool-calls

Conversation

@LeonSGP43

Copy link
Copy Markdown
Contributor

Summary

  • split fully decodable concatenated tool-call argument blobs like {"a":1}{"b":2} into separate streamed tool calls
  • keep existing truncation handling for partial or otherwise non-lossless payloads
  • add regression coverage for both the splitter and streaming assembly path

Testing

  • uv run --frozen pytest -o addopts='' tests/run_agent/test_repair_tool_call_arguments.py tests/run_agent/test_run_agent.py::TestStreamingApiCall::test_concatenated_json_arguments_split_into_parallel_tool_calls tests/run_agent/test_run_agent.py::TestStreamingApiCall::test_ollama_reused_index_separate_tool_calls tests/run_agent/test_run_agent.py::TestStreamingApiCall::test_truncated_tool_call_args_upgrade_finish_reason_to_length
  • git diff --check
  • uv run --frozen ruff check run_agent.py tests/run_agent/test_repair_tool_call_arguments.py tests/run_agent/test_run_agent.py

Attribution

Known red Tests on recent company PRs and origin/main is treated as preexisting_unrelated baseline noise; this candidate's targeted local gate is green.

Closes #25333.

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

Copy link
Copy Markdown
Collaborator

Competes with #25347 — both fix #25333 (concatenated JSON tool_call arguments from Gemini parallel calls). This PR splits all decodable concatenated objects into separate tool calls; #25347 extracts only the first via raw_decode().

@hbentel

hbentel commented May 26, 2026

Copy link
Copy Markdown
Contributor

Confirmed reproducing on stable gemini-3.5-flash (not just gemini-3-flash-preview) and confirmed this PR fixes it. Tested on macOS / Hermes v0.14.0, Google AI Studio direct (provider: gemini, default: gemini-3.5-flash).

Repro (run 5x per arm)

hermes chat -q "In a SINGLE response with FOUR parallel write_file tool calls (no sequential calls allowed), create: /tmp/gpr_a.txt containing 'Don Bowman is the CEO of Agilicus', /tmp/gpr_b.txt containing 'Pranab Sarkar reported issue 25333', /tmp/gpr_c.txt containing 'Gemini emits chained JSON objects', /tmp/gpr_d.txt containing 'Streaming chunks lack delimiters'. Issue all four write_file calls in parallel in one assistant turn."

Baseline (without this PR)

$ grep -c "Unrepairable tool_call arguments" ~/.hermes/profiles/gemini35/logs/agent.log
9

Sample warning (note the }{ pattern at the boundary — exactly #25333):

WARNING agent.message_sanitization: Unrepairable tool_call arguments for write_file
  — replaced with empty object
  (was: {"content": "Pranab Sarkar reported issue 25333", "path": "/tmp/gpr_b.txt"}{"con)

Across 5 trials: most prompts dropped multiple write_file calls to {}; the agent's user-visible result was "Truncated tool call detected — refusing to execute incomplete tool arguments."

With this PR

$ grep -c "Unrepairable tool_call arguments" ~/.hermes/profiles/gemini35/logs/agent.log
0

$ grep "Split concatenated" ~/.hermes/profiles/gemini35/logs/agent.log
WARNING run_agent: Split concatenated tool_call arguments for write_file into 3 calls
WARNING run_agent: Split concatenated tool_call arguments for write_file into 3 calls

All 4 write_file calls executed cleanly in both fully-completed trials (the remaining 3 hit the free-tier 429 rate limit before reaching the splitter — unrelated to this fix). Log excerpt:

20:40:55  WARNING  Split concatenated tool_call arguments for write_file into 3 calls
20:40:56  INFO     tool write_file completed (0.92s, 113 chars)
20:40:56  INFO     tool write_file completed (0.92s, 113 chars)
20:40:56  INFO     tool write_file completed (0.92s, 113 chars)
20:40:56  INFO     tool write_file completed (0.92s, 113 chars)

(The "split into 3" + 1 standalone = 4 total tool calls — matches the prompt.)

Conclusion

This bug is not limited to the preview model — the stable gemini-3.5-flash family produces the same }{ concatenated-args pattern. The splitter approach in this PR is the correct fix: lossless recovery of all N calls, not just the first. LGTM from a usability standpoint — strongly worth landing for Hermes Gemini 3.5-flash users.

Happy to provide more repro data on request.

benclawbot pushed a commit to benclawbot/hermes-agent that referenced this pull request Jun 3, 2026
…nonfinite, coerce booleans, port content-channel promotion

Six pre-existing failure modes in the tool-call argument pipeline, all
surfaced by adversarial probing and online research into upstream
Hermes PRs. 241/241 tests passing in the repair+coerce+transport surface.

A. Concatenated streamed tool-call args — Gemini-3-flash-preview and
   some Ollama routes emit ``{"a":1}{"b":2}`` with no delimiter. The
   repair layer was falling through to ``{}`` and losing every call.
   Fixed by adding ``_split_concatenated_tool_call_arguments`` (matches
   upstream PR NousResearch#25346/NousResearch#36039 verbatim so a future merge is conflict-
   free), which uses ``json.JSONDecoder().raw_decode(pos=...)`` to
   walk left-to-right and peel off complete top-level dicts.

B. JSON buried in surrounding noise — U+2028/U+2029 line separators,
   BOM markers, model preamble/postscript, extra ``true``/``null``/
   ``"excess"`` after valid JSON, etc. All made ``json.loads`` reject
   the whole payload. Fixed by adding ``_extract_first_json_object``,
   which uses ``raw_decode`` from every ``{`` position in the string
   and rescues the first complete dict it finds.

C. NaN / Infinity pass-through — ``json.loads(strict=False)`` accepts
   the literal tokens, but re-serialising produces ``NaN``/``Infinity``
   literals that strict-validating providers (Anthropic, AWS Bedrock,
   Google Vertex) reject with HTTP 400. Fixed by adding
   ``_scrub_nonfinite_numbers`` which recursively replaces them with
   ``None``.

D. Integer overflow silent corruption in ``_coerce_number`` —
   ``'99999999999999999999'`` was being routed through ``float()`` then
   ``int()``, producing ``100000000000000000000`` (off by ~9 orders of
   magnitude). Python's ``int()`` has arbitrary precision, so for
   ``integer_only`` fields we now try ``int(value)`` first; the float
   path is the fallback for decimal strings like ``"3.0"``.

E. ``_coerce_boolean`` accepted only ``"true"`` / ``"false"`` while
   M3, DeepSeek, Qwen, and GLM routinely emit ``"1"`` / ``"0"`` /
   ``"yes"`` / ``"no"`` / ``"on"`` / ``"off"`` for boolean fields.
   Extended the match set. Two pre-existing tests asserted the old
   wrong behaviour; updated with comments explaining the change.

F. Content-channel tool calls (M3 / MiniMax, Kimi K2, Ollama
   qwen2.5-coder, GLM, Gemma) — models emit tool calls in the
   response ``content`` field as ``<invoke name="…">…</invoke>`` or
   ``<tool_call>{…}</tool_call>`` instead of the structured
   ``tool_calls`` field, causing the call to leak as chat text. Ported
   the leaf parser from upstream Hermes PR NousResearch#35129 into a new module
   ``agent/transports/content_tool_calls.py`` (309 lines, 6 parsers,
   exact-name gate, fail-closed, env kill-switch
   ``HERMES_PROMOTE_TOOLCALLS``) and added a single promotion seam
   in ``agent/conversation_loop.py`` at the post-normalization point
   in the response flow. The seam is a strict no-op when structured
   ``tool_calls`` already exist, so native tool-calling paths are
   untouched.

Tests:
- 4 new classes in test_repair_tool_call_arguments.py (53 tests) for
  the split/extract/scrub helpers and end-to-end coverage of the new
  repair stages.
- 1 new class in test_tool_arg_coercion.py (8 tests) for integer
  overflow precision.
- 2 pre-existing tests in test_tool_arg_coercion.py updated to assert
  the new correct boolean coercion behaviour.
- New file tests/agent/transports/test_content_tool_calls.py (17 tests)
  covering all 6 content-channel parsers, dedup, env gates, and
  fail-closed behaviour on unknown tool names.

Files changed: 6 modified, 2 new. +506/-13.

Upstream: PRs NousResearch#25346, NousResearch#36039, NousResearch#35129 are still open. If the user
wants to push these locally, the diffs are designed to merge near-zero-
conflict with those PRs.
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the careful lossless parsing approach and the focused regression coverage.

This automated hermes-sweeper review is closing this as not planned because the underlying issue has a maintainer product-boundary decision: malformed or concatenated provider/model tool-call output must not be inferred, split, or repaired into executable calls. The owner’s closure of #25333 explicitly identifies splitter-style recovery as out of scope.

A focused provider or adapter fix that prevents invalid structured tool-call output at its source would be the appropriate direction.


Closed as not-planned per standing maintainer policy (model-output-repair). This is a design-direction decision, not a code-quality judgment — see the Contribution Rubric in AGENTS.md for what the project is looking for. If you believe this policy was misapplied to your change, comment here and a maintainer will take a look.

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) sweeper:not-planned Sweeper: closed per standing maintainer policy (design direction) 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

4 participants