Capture Gemini thought_signature carried on function-call response parts for multi-turn tool round-trips#693
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes Gemini 3 multi-turn tool round-trips by preserving thought_signature when it’s attached to a functionCall response part (with Thought=false and empty Text), ensuring the signature is captured into framework ProtectedData and can be replayed on the next request turn.
Changes:
- In
buildResponsePart, emit aTextReasoningContentcontaining base64-encodedThoughtSignatureimmediately beforeFunctionCallContentwhen a function-call part carries a signature. - Add
TestResponseWithFunctionCallThoughtSignatureto assert signature capture and ordering (reasoning content precedes function call).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| provider/geminiprovider/agent.go | Captures ThoughtSignature on function-call parts by emitting a preceding TextReasoningContent for replay in subsequent turns. |
| provider/geminiprovider/agent_test.go | Adds coverage for Gemini 3 behavior where thoughtSignature is attached to the function-call part and must round-trip correctly. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This comment has been minimized.
This comment has been minimized.
36fd9f8 to
a5f3f36
Compare
This comment has been minimized.
This comment has been minimized.
Gemini 3 attaches the opaque thought_signature to the same part that carries the function_call (Thought false, Text empty). buildResponsePart only read the signature inside the Thought branch, so for these parts it was silently dropped and could never be replayed on the next turn. Emit a TextReasoningContent (ProtectedData=base64(thought_signature)) immediately before the FunctionCallContent, matching the Python reference. The existing replay path already decodes ProtectedData back into part.ThoughtSignature, closing the round-trip.
a5f3f36 to
0ff44f0
Compare
Parity Review — ✅ ApprovedScope: What changed: When a Gemini response part carries a Cross-repo parity: The PR correctly identifies this as mirroring the Python reference, which emits API label: Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "awmgmcpg"See Network Configuration for more information.
|
What
In
provider/geminiprovider/agent.go,buildResponsePartonly readpart.ThoughtSignatureinside theif part.Thoughtbranch. Gemini 3 attaches the opaquethought_signatureto the same part that carries thefunction_call(Thoughtis false,Textis empty,ThoughtSignatureis set). For such a part theThoughtbranch is skipped, theelse if part.Text != ""branch is skipped, and theFunctionCallbranch fired without capturing the signature, so it was silently dropped. Because it was never parsed intoProtectedData, the replay path inbuildRequestParts(which decodesTextReasoningContent.ProtectedDataback intopart.ThoughtSignature) could never send it back, breaking multi-turn tool round-trips.The fix emits a
TextReasoningContent{ProtectedData: base64(part.ThoughtSignature)}immediately before theFunctionCallContentwhenever a function-call part carries a signature.Why (parity)
This mirrors the Python reference, which emits a text-reasoning content (
protected_data=base64(part.thought_signature)) immediately preceding the function call. The existing replay path already decodesProtectedDataback intopart.ThoughtSignature, so the round-trip closes with no other change.Testing
Added
TestResponseWithFunctionCallThoughtSignaturein the canonicalagent_test.go: it feeds a response whose single part hasfunctionCallset (nothought, notext) plus athoughtSignature, and asserts the returned contents include a*message.TextReasoningContentwhoseProtectedDataequals the base64 signature, ordered before the*message.FunctionCallContent. The test fails before the fix and passes after.go build ./...,go vet ./provider/geminiprovider/..., andgo test ./provider/geminiprovider/...all pass.