Skip to content

fix(gemini-native): disambiguate parallel same-name tool calls by id - #102953

Closed
Sangoku wants to merge 1 commit into
NousResearch:mainfrom
Sangoku:fix/gemini-native-parallel-same-name-tool-calls
Closed

Sangoku wants to merge 1 commit into
NousResearch:mainfrom
Sangoku:fix/gemini-native-parallel-same-name-tool-calls

Conversation

@Sangoku

@Sangoku Sangoku commented Sep 4, 2026

Copy link
Copy Markdown

Problem

translate_stream_event() in agent/gemini_native_adapter.py correlates
streamed functionCall parts across SSE chunks using a call_key of
(part_index, name, thought_signature).

part_index is the position within the current chunk's parts[]
array, which is 0 whenever a chunk carries a single part — the common
case when Gemini streams several tool calls, each arriving in its own
chunk. When a model turn contains multiple different calls to the
same tool name (e.g. several ha_call_service calls in one turn, one
per Home-Assistant entity being updated), every call collides onto the
identical call_key and gets treated as the same call growing
incrementally.

The startswith-based diffing then fails to reconcile unrelated JSON
payloads and falls through to re-emitting the full args each time,
which the downstream OpenAI-style delta accumulator appends (+=) onto
the same slot — concatenating multiple complete, individually-valid JSON
objects into one unparseable arguments string.

Observed symptom in production (Discord platform, gemini-3.1-flash-lite):

Unrepairable tool_call arguments for ha_call_service — replaced with empty object
(was: {"data": "...", "entity_id": "input_number.a", ...}{"data": "...", "entity_id": "input_number.b", ...}...)
Response truncated (finish_reason='length') - model hit max output tokens

...repeating across 4 retries, ending in "Response truncated due to
output length limit" — even though the model was behaving correctly and
sending well-formed, complete tool calls.

Fix

Gemini 3+ already stamps each distinct function call with a stable,
unique id (see gemini_requires_tool_call_ids()) — the adapter already
read fc["id"], but only to seed the outgoing tool-call id, never to
correlate calls across streaming chunks.

call_key now prefers fc["id"] when present, falling back to the
previous (part_index, name, thought_signature) heuristic only when
id is absent (Gemini 2.x, which doesn't advertise per-call ids and
isn't exercised for parallel same-name calls in practice — no behavior
change there).

Testing

  • Added test_stream_event_translation_disambiguates_parallel_same_name_calls_by_id,
    replicating the exact collision: three distinct calls to the same tool
    name, each in its own chunk, each with a Gemini-3-style per-call id.
    Confirms each call now lands in its own slot with independently valid
    (non-concatenated) JSON arguments.
  • Confirmed the existing test_stream_event_translation_emits_tool_call_delta_with_stable_index
    (same call resent twice, no id, Gemini 2.x path) still passes
    unchanged.
  • pytest tests/agent/test_gemini_native_adapter.py — 30 passed.

Notes

Found and fixed while diagnosing a real-world failure where a Discord
user reported several aquarium water-parameter readings at once and the
agent tried to batch the corresponding input_number.set_value calls
(via parallel_tool_call_guidance) into a single turn.

translate_stream_event() correlated streamed functionCall parts across
SSE chunks using a call_key of (part_index, name, thought_signature).

part_index is the position within the CURRENT chunk's parts[] array,
which is 0 whenever a chunk carries a single part -- the common case
when Gemini streams several tool calls, each in its own chunk. When a
model turn contains multiple DIFFERENT calls to the SAME tool name
(e.g. several ha_call_service calls in one turn, one per entity), every
call collided onto the identical call_key and was treated as the same
call growing incrementally. The startswith-based diffing then failed to
reconcile unrelated JSON payloads and fell through to re-emitting the
full args each time, which the downstream OpenAI-style delta
accumulator appends (+=) onto the same slot -- concatenating multiple
complete, individually-valid JSON objects into one unparseable
"arguments" string. Symptom: "Unrepairable tool_call arguments ...
replaced with empty object" followed by finish_reason='length' and,
after 4 retries, "Response truncated due to output length limit".

Gemini 3+ already stamps each distinct function call with a stable,
unique id (see gemini_requires_tool_call_ids()) -- the adapter already
read fc["id"], but only to seed the outgoing tool-call id, never to
correlate calls across chunks. Fix: prefer fc["id"] in call_key when
present, falling back to the previous (part_index, name,
thought_signature) heuristic only when id is absent (Gemini 2.x, which
doesn't advertise per-call ids and isn't used for parallel calls in
practice).

Adds a regression test replicating the exact collision (three distinct
calls to the same tool name, each in its own chunk, with per-call ids)
and confirms the existing same-call-resent-twice test (no id, Gemini
2.x path) still passes unchanged.
@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) area/streaming Streaming responses: gateway delivery, provider wire P2 Medium — degraded but workaround exists duplicate This issue or pull request already exists labels Sep 4, 2026
@alt-glitch

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Duplicate of #75528 — same fix: key Gemini streaming tool-call slots on the provider functionCall.id when present and fall back to the (part_index, name, thought_signature) heuristic only for id-less models. #75528 (earliest, independently reproduced) and #101654 already implement this; #24676 / #57941 / #99273 are the earlier heuristic-only attempts. Your Home-Assistant same-name parallel-call regression test would be a useful addition to #75528 if it is not already covered.

@kshitijk4poor

Copy link
Copy Markdown
Contributor

The Gemini parallel tool-call slot collision is landing via #111686 (salvage of #75528 by @jmiguellucas, co-credited to the first submitter @cdbartholomew #24676; armed to auto-merge on green): Gemini 3 ids are the slot identity, no-id calls disambiguate by value with reachable key#N slots for resends. Closing this one as a duplicate — thank you for the fix; if #111686 does not merge I'll reopen.

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

Labels

area/streaming Streaming responses: gateway delivery, provider wire comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint duplicate This issue or pull request already exists 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.

3 participants