fix(gemini): stop splitting one tool call into two when thoughtSignature arrives late - #15739
Closed
Ricardo-M-L wants to merge 1 commit into
Closed
Conversation
…ives late
translate_stream_event() in agent/gemini_native_adapter.py keys
tool_call slots on (part_index, name, thought_signature). Because the
thought_signature is part of the dedup key, a single tool call whose
chunks carry the signature inconsistently (e.g., empty on early chunks,
present on a later one — which Gemini 3 thinking models do) is split
into two separate slots:
- slot 0: built from the early chunks → no signature, partial args
- slot 1: built from the later chunk → has signature, fuller args
Both slots are emitted as deltas, so the agent records *two* tool calls
for what was logically one. On the next turn the slot without a
signature is replayed back to Gemini, which 400s with:
Function call is missing a thought_signature in functionCall parts.
Fix: dedup on (part_index, name) only. The signature is still surfaced
through the per-chunk extra_content field, and the downstream
streaming accumulator (run_agent.py) already does latest-non-None-wins
on extra_content per slot — so whichever chunk carried the signature
gets it merged into the single slot.
Adds a regression test that fails on main and passes here:
test_stream_event_translation_does_not_split_slot_when_signature_arrives_late
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Ricardo-M-L
force-pushed
the
fix/gemini-stream-signature-dedup-key
branch
from
April 27, 2026 14:52
0115735 to
d94469f
Compare
2 tasks
Contributor
Author
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.
Summary
`translate_stream_event()` in `agent/gemini_native_adapter.py` keys streaming tool-call slots on `(part_index, name, thought_signature)`:
```python
before
thought_signature = part.get("thoughtSignature") if isinstance(part.get("thoughtSignature"), str) else ""
call_key = json.dumps(
{
"part_index": part_index,
"name": name,
"thought_signature": thought_signature, # ← splits one call into two
},
sort_keys=True,
)
```
Because `thought_signature` is part of the dedup key, a single tool call whose chunks carry the signature inconsistently is split into two separate slots:
Both slots are emitted as deltas, so the agent records two tool calls for what was logically one. On the next turn the slot without a signature is replayed back to Gemini, which then 400s with:
```
Function call is missing a thought_signature in functionCall parts.
```
How this bug surfaces in the wild
Reported by a user running `gemini-3.1-flash-lite-preview` through Hermes Desktop:
```
HTTP 400: Gemini HTTP 400 (INVALID_ARGUMENT): Function call is missing a
thought_signature in functionCall parts. ... Additional data, function call
`default_api:terminal` , position 2.
```
Position 2 == the third part of the assistant content. With the bug, after one logical `terminal` tool call the model's content has:
Gemini sees the part-1 functionCall without a signature and rejects.
Fix
Dedup on `(part_index, name)` only. The signature is still surfaced through the per-chunk `extra_content` field, and the downstream streaming accumulator in `run_agent.py` already does latest-non-None-wins on `extra_content` per slot:
```python
run_agent.py — existing behavior, unchanged
extra = getattr(tc_delta, "extra_content", None)
...
if extra is not None:
if hasattr(extra, "model_dump"):
extra = extra.model_dump()
entry["extra_content"] = extra # late-arriving signature wins, single slot
```
So whichever chunk carries the signature gets it merged into the single slot.
Test plan
Adds a regression test:
```python
def test_stream_event_translation_does_not_split_slot_when_signature_arrives_late():
...
# Chunk 1 — no signature yet.
event_a = {"candidates": [{"content": {"parts": [
{"functionCall": {"name": "terminal", "args": {}}}
]}}]}
# Chunk 2 — same tool call, signature now present.
event_b = {"candidates": [{"content": {"parts": [{
"functionCall": {"name": "terminal", "args": {"cmd": "ls"}},
"thoughtSignature": "sig-late",
}]}}]}
...
# Both chunks must address the SAME slot.
assert a_tool.index == b_tool.index == 0
assert a_tool.id == b_tool.id
# The chunk that carried the signature surfaces it via extra_content.
assert b_tool.extra_content == {"google": {"thought_signature": "sig-late"}}
```
🤖 Generated with Claude Code