Describe the bug
generateRequestConfirmationEvent in internal/llminternal/functions.go creates synthetic adk_request_confirmation FunctionCall parts but does not copy the ThoughtSignature from the original model-generated FunctionCall part onto them. This causes Gemini 3 models (and 2.5 with thinking enabled) to reject subsequent requests with a 400 INVALID_ARGUMENT error: "function call … is missing a thought_signature".
The stream aggregator (internal/llminternal/stream_aggregator.go) already handles this correctly — it tracks currentFunctionThoughtSignature and copies it onto emitted parts. The confirmation flow simply missed it.
To Reproduce
- Create an
LlmAgent with a tool wrapped in tool.WithConfirmation (or using require_confirmation)
- Use a Gemini model that emits thought signatures (e.g.
gemini-3-flash-preview)
- Trigger the tool so the model emits a
FunctionCall with a ThoughtSignature
- The confirmation flow calls
generateRequestConfirmationEvent, which builds a new Part{FunctionCall: ...} without setting ThoughtSignature
- This event is appended to session history. On the next model call, the Gemini API rejects the request with:
400 INVALID_ARGUMENT: Unable to submit request because function call `adk_request_confirmation`
in the N. content block is missing a `thought_signature`.
Expected behavior
generateRequestConfirmationEvent should copy the ThoughtSignature from the original model-generated FunctionCall part (found in functionCallEvent.Content.Parts) onto the synthetic adk_request_confirmation part it creates, so that the Gemini API thought signature validation passes.
Screenshots
N/A — API-level error, no UI involved.
Versions
- OS: Linux (also reproducible on Mac)
- ADK version:
v0.5.0 / main at commit d8a05b5 (2026-03-17).
- Python version: N/A (this is ADK Go). Go 1.23+.
Additional context
The fix is likely small. In generateRequestConfirmationEvent, the loop already looks up originalFunctionCall by ID from functionCallEvent.Content.Parts. It just needs to also look up the corresponding ThoughtSignature from the original Part and set it on the new part:
// Find the original Part's ThoughtSignature
var originalThoughtSig []byte
for _, p := range functionCallEvent.Content.Parts {
if p.FunctionCall != nil && p.FunctionCall.ID == funcID {
originalThoughtSig = p.ThoughtSignature
break
}
}
part := &genai.Part{
FunctionCall: requestConfirmationFC,
}
if len(originalThoughtSig) > 0 {
part.ThoughtSignature = originalThoughtSig
}
parts = append(parts, part)
This mirrors how stream_aggregator.go already handles ThoughtSignature propagation.
Describe the bug
generateRequestConfirmationEventininternal/llminternal/functions.gocreates syntheticadk_request_confirmationFunctionCallparts but does not copy theThoughtSignaturefrom the original model-generatedFunctionCallpart onto them. This causes Gemini 3 models (and 2.5 with thinking enabled) to reject subsequent requests with a400 INVALID_ARGUMENTerror: "function call … is missing athought_signature".The stream aggregator (
internal/llminternal/stream_aggregator.go) already handles this correctly — it trackscurrentFunctionThoughtSignatureand copies it onto emitted parts. The confirmation flow simply missed it.To Reproduce
LlmAgentwith a tool wrapped intool.WithConfirmation(or usingrequire_confirmation)gemini-3-flash-preview)FunctionCallwith aThoughtSignaturegenerateRequestConfirmationEvent, which builds a newPart{FunctionCall: ...}without settingThoughtSignatureExpected behavior
generateRequestConfirmationEventshould copy theThoughtSignaturefrom the original model-generatedFunctionCallpart (found infunctionCallEvent.Content.Parts) onto the syntheticadk_request_confirmationpart it creates, so that the Gemini API thought signature validation passes.Screenshots
N/A — API-level error, no UI involved.
Versions
v0.5.0/mainat commitd8a05b5(2026-03-17).Additional context
The fix is likely small. In
generateRequestConfirmationEvent, the loop already looks uporiginalFunctionCallby ID fromfunctionCallEvent.Content.Parts. It just needs to also look up the correspondingThoughtSignaturefrom the originalPartand set it on the new part:This mirrors how
stream_aggregator.goalready handlesThoughtSignaturepropagation.