Skip to content

Python: preserve Gemini 3 thought_signature across function-call replays#7095

Open
giles17 wants to merge 3 commits into
microsoft:mainfrom
giles17:fix/gemini-thought-signature
Open

Python: preserve Gemini 3 thought_signature across function-call replays#7095
giles17 wants to merge 3 commits into
microsoft:mainfrom
giles17:fix/gemini-thought-signature

Conversation

@giles17

@giles17 giles17 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Motivation & Context

Gemini 3 models return an opaque, cryptographically-signed thought_signature on each functionCall part and require it echoed back on every later turn that replays that call (Google docs). A request that replays a function call without its signature is rejected with 400 INVALID_ARGUMENT.

agent_framework_gemini preserved the signature only when the replayed FunctionCallContent still carried its original types.Part as raw_representation. Any layer that reconstructs a call from call_id/name/arguments — the harness ToolApprovalMiddleware releasing an auto-approved tool, an always_require MCP tool, a history rebuilt from a transcript — produced a fresh FunctionCallContent without that raw part, so the client emitted a signature-less functionCall part on the next step and the run died. In practice a Gemini 3 harness agent broke on the second step of essentially any tool loop. Disabling thinking does not help — the signature is required regardless of ThinkingConfig.

Description & Review Guide

  • What are the major changes?
    • _parse_parts: when a functionCall part carries a thought_signature (a field on types.Part, not FunctionCall), capture it into Content.additional_properties["thought_signature"] so it survives content transformations that drop raw_representation.
    • _convert_message_contents: replay the signature from additional_properties when building the Gemini Part — in the reconstructed-call path, and as a backfill when a raw Part is present but lacks one. Applied unconditionally, independent of ThinkingConfig.
    • Added unit tests covering capture-on-parse, no-op when absent, replay from additional_properties (the harness reconstruction path), no-op replay, and a full parse -> rebuild -> replay round-trip.
  • What is the impact of these changes? Gemini 3 tool loops that go through call reconstruction (harness tool approval, MCP always_require, rebuilt history) no longer fail with 400 INVALID_ARGUMENT on the continuation step. No public API change; the signature is stored under a bare additional_properties key, matching the existing OpenAI (server_label, fc_id) convention.
  • What do you want reviewers to focus on? The signature key is stored using an inline string literal to match peer provider clients. Cross-request resume through a wire format (AG-UI MESSAGES_SNAPSHOT) is out of scope here — thought_signature is bytes, so serialized hosts need the ag-ui adapters to carry it separately; tracked as a follow-up. This client-level fix covers the common in-process multi-step tool loop.

Related Issue

Fixes #6963

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

Gemini 3 requires the opaque thought_signature attached to each functionCall
part to be echoed back on every replay of that call, or the request is rejected
with 400 INVALID_ARGUMENT. The signature previously survived only via
raw_representation, so any layer that reconstructs a FunctionCallContent (e.g.
harness tool approval) dropped it and broke the next step of the tool loop.

Capture the signature into additional_properties on parse and replay it when
building the Gemini Part, independent of raw_representation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 33233834-bc6e-4ad2-a6f3-6f1d6e57b1d2
Copilot AI review requested due to automatic review settings July 13, 2026 20:33
@giles17 giles17 added the python Usage: [Issues, PRs], Target: Python label Jul 13, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 5 | Confidence: 91% | Result: All clear

Reviewed: Correctness, Security Reliability, Test Coverage, Failure Modes, Design Approach


Automated review by giles17's agents

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the Python Gemini chat client to preserve Gemini 3 thought_signature metadata across function-call replays, so reconstructed tool calls (e.g., harness approval flows) don’t fail with 400 INVALID_ARGUMENT when the prior functionCall is replayed.

Changes:

  • Capture thought_signature from incoming Gemini Part objects during _parse_parts into Content.additional_properties.
  • Replay thought_signature from Content.additional_properties when rebuilding outgoing Gemini functionCall parts in _convert_message_contents.
  • Add unit tests covering capture/replay behavior and a parse→rebuild round-trip.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
python/packages/gemini/agent_framework_gemini/_chat_client.py Captures and replays thought_signature for function-call parts across reconstructed-call paths.
python/packages/gemini/tests/test_gemini_client.py Adds unit tests validating thought signature capture and replay semantics.

Comment thread python/packages/gemini/agent_framework_gemini/_chat_client.py Outdated
Comment thread python/packages/gemini/agent_framework_gemini/_chat_client.py
@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/gemini/agent_framework_gemini
   _chat_client.py4491995%402, 709, 730–731, 749–750, 759–760, 763–764, 796, 803, 923, 934, 1109–1110, 1114, 1125–1126
TOTAL44155526588% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
8868 33 💤 0 ❌ 0 🔥 2m 13s ⏱️

Content.additional_properties is serialized via json.dumps(message.to_dict())
by history providers (e.g. RedisHistoryProvider), which fails on raw bytes.
Store the thought_signature as a base64 string on parse and decode it back to
bytes when building the Gemini Part. Also narrow call_id/name in the round-trip
test to satisfy the type checkers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 33233834-bc6e-4ad2-a6f3-6f1d6e57b1d2
@giles17 giles17 marked this pull request as ready for review July 13, 2026 21:05

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 5 | Confidence: 85%

✓ Correctness

The implementation is correct. It properly captures Gemini 3's thought_signature as a base64-encoded string during parsing (ensuring JSON serializability for history providers), and decodes it back to bytes when constructing the Gemini Part for replay. The backfill logic for raw Parts that lack a signature, the new-Part construction path, and the no-op paths when no signature is present are all handled correctly. The previous review feedback about bytes vs base64 serialization has been fully addressed.

✓ Security Reliability

The PR correctly preserves Gemini 3 thought_signature across function-call replays using base64 encoding in additional_properties. The implementation addresses the previously-resolved review comments about JSON serializability. The main reliability concern is that base64.b64decode at line 698 has no error handling — a corrupted value in persisted history would crash the entire agent tool loop rather than gracefully degrading, unlike similar decode operations elsewhere in this file that use try/except.

✓ Test Coverage

The test coverage for the new thought_signature preservation logic is comprehensive and well-structured. Tests cover: capture on parse, JSON serialization round-trip, no-op when absent, replay from additional_properties (reconstructed calls), no-op replay, and full parse→reconstruct→replay round-trip. Assertions are meaningful (checking specific decoded values rather than just 'no exception'). The only untested branch is the defensive backfill at line 701-702 in _convert_message_contents, where a raw Part exists with function_call but has lost its thought_signature while additional_properties still carries one — this cannot occur in normal _parse_parts flow and is purely protective code.

✓ Failure Modes

The PR correctly implements thought_signature preservation across function-call replays using base64 encoding for JSON serializability. The parse path stores signatures as base64 strings, and the replay path decodes them back to bytes. Both the raw-Part-present and raw-Part-absent code paths are handled correctly with appropriate None guards. The backfill logic (line 701-702) correctly defers to the raw Part's own signature when available. No silent failures, swallowed exceptions, or operational failure modes are introduced by this diff.

✓ Design Approach

The fix is targeted at the right layer, but the replay path currently trusts Content.additional_properties["thought_signature"] too loosely. Because Content.additional_properties is explicitly untyped (dict[str, Any]), decoding any truthy value with base64.b64decode(...) can silently turn malformed persisted data into the wrong bytes (for example, non-base64 text can decode to b'' or arbitrary bytes) and reintroduce the same invalid replay this PR is trying to prevent.


Automated review by giles17's agents

Comment thread python/packages/gemini/agent_framework_gemini/_chat_client.py Outdated
Guard the untyped additional_properties value with an isinstance(str) check and
decode with validate=True, degrading gracefully (warn + drop the signature) on
malformed data instead of raising binascii.Error mid tool loop. Matches the
defensive base64 handling already used for data URIs in this file.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 33233834-bc6e-4ad2-a6f3-6f1d6e57b1d2
@giles17 giles17 enabled auto-merge July 13, 2026 22:21
# Capture Gemini 3's thought_signature (on the Part, not the FunctionCall) as a base64
# string so it stays JSON-serializable when messages are persisted and can be replayed.
additional_properties = (
{"thought_signature": base64.b64encode(part.thought_signature).decode("utf-8")}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should use protected_data as part of reasoning_content for this.

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

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: Gemini 3 thought_signature dropped on replayed function calls

4 participants