fix(gemini): disambiguate parallel function calls across stream events - #24676
Open
cdbartholomew wants to merge 1 commit into
Open
Conversation
translate_stream_event keys its tool-call accumulator slots on the
tuple (part_index, name, thought_signature). Gemini's streaming SSE
emits each function call in its own event with `parts[0]` only — so
`part_index` is always 0. When the model emits multiple parallel
function calls that share the same `name` across separate events
(common with provider-side dispatch patterns and with models that
fan out per-target tool calls), all of them collide on a single
slot. The delta-emitting logic below the lookup then concatenates
the args of the colliding calls because
`args_str.startswith(last_arguments)` is False for distinct
payloads, producing invalid JSON like
`{"q":"a"}{"q":"b"}{"q":"c"}`.
Downstream consumers that JSON-validate the accumulated args (e.g.
the run_agent.py tool-call validator at the truncation handler)
read the malformed args as truncated and convert Gemini's
`finishReason=STOP` into `finish_reason="length"` — surfacing as
a spurious "Response truncated due to output length limit" failure
on a perfectly successful Gemini response.
The fix replaces the rigid `call_key` -> slot lookup with a
value-based match: find any existing slot for this
(part_index, name, thought_signature) whose accumulated args either
equals args_str or is a prefix of it (delta continuation). When
no such slot exists, allocate a fresh one. This preserves both
existing invariants:
- Streamed delta accumulation for the SAME logical call across
events (Test 1: `*_emits_tool_call_delta_with_stable_index`)
- Distinct slots for identical calls in separate parts of one
event (Test 2:
`*_keeps_identical_calls_in_distinct_parts`)
while fixing the parallel-different-args-across-events case.
Two regression tests added:
- test_stream_event_translation_distinct_parallel_calls_across_events_same_name
Reproduces the bug exactly — three events, each `parts[0]` only,
name="search" with args {"q":"a"}, {"q":"b"}, {"q":"c"}. Without
the fix, all three collide on slot 0 and the third event's args
become the concatenation `{"q":"a"}{"q":"b"}{"q":"c"}`. With the
fix, each gets a distinct slot with its own index, id, and
isolated args.
- test_stream_event_translation_streamed_arg_delta_still_merges
Asserts that consecutive events for the SAME logical call still
collapse to one slot (Test 1's invariant, preserved by the new
prefix-match path).
Observed in production with `gemini-3-flash-preview` + tool use:
prompts that elicit several parallel function calls with
overlapping names (e.g. an agent investigating with `skill_view`
called for each of several skills) failed with "Response truncated"
in under a second despite the model returning a valid response.
|
I can confirm this fixes my problems with tool calling and the false |
1 task
|
I would love this pr to be merged |
1 task
Contributor
|
Closing — deferring to #25346 by @LeonSGP43 which addresses the same. Reopen if that PR stalls. |
This was referenced Jun 28, 2026
Contributor
|
Thanks for targeting the collision at the Gemini adapter boundary. Current Problems
Suggested changes
Automated hermes-sweeper review. |
Open
19 tasks
This was referenced Jul 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Symptom
translate_stream_eventinagent/gemini_native_adapter.pymishandles parallel function calls when each call arrives in its own SSE event withparts[0]only — a common shape forstreamGenerateContentresponses. Downstream consumers that JSON-validate the accumulated tool-call args read the malformed accumulator as truncated, and Hermes' top-level handler converts Gemini'sfinishReason=STOPintofinish_reason="length". The user seesResponse truncated due to output length limitin under a second despite a perfectly successful Gemini response.Observed in production with
gemini-3-flash-preview+ tool use: prompts that elicit several parallel function calls with overlapping names (e.g. an agent investigating withskill_viewcalled for each of several skills) consistently fail.Root cause
The accumulator slot's key is the tuple
(part_index, name, thought_signature). When each function call is in its own event withparts[0]only,part_indexis always0. Parallel calls that share the samename(different args) collide on a single slot. The delta-emitting logic below the lookup:…concatenates the args of distinct calls because
args_str.startswith(last_arguments)isFalsefor different payloads. After three events with args{"q":"a"},{"q":"b"},{"q":"c"}, the slot's emitted args are the concatenation{"q":"a"}{"q":"b"}{"q":"c"}— invalid JSON.Fix
Replace the rigid
call_key → slotlookup with a value-based match: find any existing slot for this(part_index, name, thought_signature)whose accumulated args either equalsargs_stror is a prefix of it (i.e. a delta continuation). When no such slot exists, allocate a fresh one.This preserves both existing invariants:
test_stream_event_translation_emits_tool_call_delta_with_stable_indexstill passes.part_index) —test_stream_event_translation_keeps_identical_calls_in_distinct_partsstill passes.…while fixing the parallel-different-args-across-events case.
Tests
Two regression tests added in
tests/agent/test_gemini_native_adapter.py:test_stream_event_translation_distinct_parallel_calls_across_events_same_name— reproduces the bug exactly. Three events, eachparts[0]only,name="search"with args{"q":"a"},{"q":"b"},{"q":"c"}. Asserts each call gets a distinct slot index/id and that each slot's args are isolated (not concatenated).test_stream_event_translation_streamed_arg_delta_still_merges— asserts consecutive events for the SAME logical call still collapse to one slot (Test 1's invariant, preserved by the new prefix-match path).Verified the regression test FAILS without the fix (all three calls collapse to slot 0; emitted args become the concatenation) and PASSES with the fix. All 13 existing tests in
tests/agent/test_gemini_native_adapter.pystill pass.Notes
_match_part_index, etc.) to make it clear they're not part of any wire-level slot contract._GeminiStreamChunkshape — downstream consumers are unaffected.