Skip to content

fix(gemini): stop splitting one tool call into two when thoughtSignature arrives late - #15739

Closed
Ricardo-M-L wants to merge 1 commit into
NousResearch:mainfrom
Ricardo-M-L:fix/gemini-stream-signature-dedup-key
Closed

fix(gemini): stop splitting one tool call into two when thoughtSignature arrives late#15739
Ricardo-M-L wants to merge 1 commit into
NousResearch:mainfrom
Ricardo-M-L:fix/gemini-stream-signature-dedup-key

Conversation

@Ricardo-M-L

Copy link
Copy Markdown
Contributor

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:

  • slot 0: built from the early chunks → no signature, partial args
  • slot 1: built from a 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 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:

  • part 0: text (assistant prose)
  • part 1: functionCall (slot 0 — partial args, no signature)
  • part 2: functionCall (slot 1 — full args, has signature)

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"}}
```

  • Fails on main with `assert 0 == 1` (slot 1 created with the late signature, slot 0 left signature-less).
  • Passes here.
  • All 12 tests in `tests/agent/test_gemini_native_adapter.py` continue to pass.

🤖 Generated with Claude Code

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists provider/gemini Google Gemini (AI Studio, Cloud Code) comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint labels Apr 25, 2026
…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

Copy link
Copy Markdown
Contributor Author

Closing in favor of #28438, which is the same fix rebased onto current main (this branch was 2K+ commits behind and would conflict). #28438 has the same one-key dedup change plus a regression test that fails on main and passes with the fix.

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.

2 participants