Skip to content

fix: preserve thought_signature across tool-call turns (fixes Gemini 3.x tool use) - #21

Open
FritzHeider wants to merge 1 commit into
CursorTouch:mainfrom
FritzHeider:fix/gemini-thought-signature
Open

fix: preserve thought_signature across tool-call turns (fixes Gemini 3.x tool use)#21
FritzHeider wants to merge 1 commit into
CursorTouch:mainfrom
FritzHeider:fix/gemini-thought-signature

Conversation

@FritzHeider

Copy link
Copy Markdown

Gemini 3.x rejects a replayed function call whose thought_signature is missing, so the agent dies on its second step:

400 INVALID_ARGUMENT: Function call is missing a thought_signature in
functionCall parts. This is required for tools to work correctly.
Additional data, function call `default_api:goto_tool`, position 2.

Step 1 succeeds (the browser navigates), then every retry fails and the run aborts. This makes the agent unusable on current Gemini models — gemini-2.5-flash and gemini-2.0-flash now return 404 ... no longer available to new users for new API keys, so new users land on a 3.x model by default.

Root cause

The Google provider already handles both ends of this:

  • extracts the signature from the response — src/providers/google/llm.py:321 (_extract_thought_signature, into Thinking(signature=...))
  • replays it onto the function-call Part when rebuilding history — src/providers/google/llm.py:121-126

The gap is between them. src/agent/service.py built its ToolMessage from the LLMEvent without thinking or thinking_signature, so the extracted signature was discarded before it could ever be replayed. ToolMessage.thinking_signature is declared in src/messages/service.py:150 but nothing assigned it anywhere in the codebase.

Fix

Thread both fields from the LLMEvent into the ToolMessage (2 lines).

Also affects Anthropic

src/providers/anthropic/llm.py:148,160 reads the same msg.thinking_signature that nothing previously set, so extended thinking with tools was likely broken in the same way. This fix is provider-agnostic and should repair that path too — though I verified on Gemini only.

Verification

With gemini-flash-latest:

  • before: fails at step 2 with the error above, all 3 retries exhausted
  • after: a multi-step navigate-and-extract task completes and returns its final answer

Happy to add a regression test if you would like one — a fake provider returning a signature and asserting it survives the history round-trip would cover it.

Gemini 3.x rejects a replayed function call whose thought_signature is
missing, so the agent dies on its second step with:

  400 INVALID_ARGUMENT: Function call is missing a thought_signature in
  functionCall parts. This is required for tools to work correctly.
  Additional data, function call `default_api:goto_tool`, position 2.

The Google provider already handles both ends of this: it extracts the
signature from the response (providers/google/llm.py:321) and replays it
onto the function-call Part when rebuilding history (:121). The gap was in
between — the agent constructed its ToolMessage without thinking or
thinking_signature, so the extracted value was discarded before it could
be replayed, and ToolMessage.thinking_signature was never populated by
anything.

Thread both fields from the LLMEvent into the ToolMessage.

This also affects the Anthropic provider, which reads the same
msg.thinking_signature (providers/anthropic/llm.py:148,160) that nothing
previously set, so extended thinking with tools should benefit too
(verified on Gemini only).

Verified with gemini-flash-latest: before this change the agent fails at
step 2; after it, a multi-step navigate-and-extract task completes.
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Fix: preserve thought_signature when recording tool-call turns

🐞 Bug fix 🕐 10-20 Minutes

Grey Divider

AI Description

• Propagate LLM thinking and thought_signature into ToolMessage tool-call turns.
• Prevent Gemini 3.x tool-call replay failures caused by missing thought_signature.
• Unblocks Anthropic extended-thinking tool use by preserving the same signature field.
Diagram

graph TD
A["Agent loop (service.py)"] --> B["LLM provider ainvoke()"] --> C["LLMEvent (thinking+signature)"] --> D["ToolMessage history"] --> E["Provider _convert_messages()"] --> F{{"Gemini/Anthropic API"}}
Loading
High-Level Assessment

This is the right minimal fix: the providers already extract and replay thought_signature, so the correct place to preserve it is when the agent records the tool-call turn (ToolMessage). The only meaningful follow-up to consider is adding a regression test that asserts thinking_signature survives an LLMEvent → ToolMessage → provider history rebuild round-trip.

Files changed (1) +2 / -0

Bug fix (1) +2 / -0
service.pyPreserve thinking and thought_signature in ToolMessage tool-call turns +2/-0

Preserve thinking and thought_signature in ToolMessage tool-call turns

• When the LLM returns a TOOL_CALL event with Thinking metadata, the agent now copies both thinking content and thinking_signature into the ToolMessage it records. This prevents signature loss across turns so providers can correctly replay signed tool calls (required by Gemini 3.x; also used by Anthropic extended thinking).

src/agent/service.py

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant