fix(gemini): Implement Gemini 3 thought_signature handling for stateful reasoning

This commit is contained in:
Andrew 2026-02-16 11:59:10 +00:00
parent 13e4028d42
commit 5ac9a46f59
4 changed files with 67 additions and 21 deletions

View file

@ -3,16 +3,19 @@
# ============================================================ # ============================================================
FROM golang:1.26.0-alpine AS builder FROM golang:1.26.0-alpine AS builder
RUN apk add --no-cache git make # Install build dependencies
RUN apk add --no-cache git make gcc musl-dev
WORKDIR /src WORKDIR /src
# Cache dependencies # Cache dependencies for faster subsequent builds
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
# Copy source and build # Copy your local source code (where you'll add the Thought Signature fix)
COPY . . COPY . .
# Compile the binary
RUN make build RUN make build
# ============================================================ # ============================================================
@ -20,17 +23,19 @@ RUN make build
# ============================================================ # ============================================================
FROM alpine:3.23 FROM alpine:3.23
# Install runtime essentials
RUN apk add --no-cache ca-certificates tzdata curl RUN apk add --no-cache ca-certificates tzdata curl
# Health check # Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -q --spider http://localhost:18790/health || exit 1 CMD wget -q --spider http://localhost:18790/health || exit 1
# Copy binary # Copy the compiled binary from the builder stage
COPY --from=builder /src/build/picoclaw /usr/local/bin/picoclaw COPY --from=builder /src/build/picoclaw /usr/local/bin/picoclaw
# Create picoclaw home directory # Create necessary directories and initialize
RUN /usr/local/bin/picoclaw onboard RUN /usr/local/bin/picoclaw onboard
# Set the binary as the entrypoint
ENTRYPOINT ["picoclaw"] ENTRYPOINT ["picoclaw"]
CMD ["gateway"] CMD ["gateway"]

View file

@ -621,6 +621,9 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, messages []providers.M
} }
for _, tc := range response.ToolCalls { for _, tc := range response.ToolCalls {
argumentsJSON, _ := json.Marshal(tc.Arguments) argumentsJSON, _ := json.Marshal(tc.Arguments)
// Copy ExtraContent to ensure thought_signature is persisted
extraContent := tc.ExtraContent
assistantMsg.ToolCalls = append(assistantMsg.ToolCalls, providers.ToolCall{ assistantMsg.ToolCalls = append(assistantMsg.ToolCalls, providers.ToolCall{
ID: tc.ID, ID: tc.ID,
Type: "function", Type: "function",
@ -628,6 +631,9 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, messages []providers.M
Name: tc.Name, Name: tc.Name,
Arguments: string(argumentsJSON), Arguments: string(argumentsJSON),
}, },
ExtraContent: extraContent,
// We also set internal ThoughtSignature, but ExtraContent is what matters for serialization
ThoughtSignature: tc.ThoughtSignature,
}) })
} }
messages = append(messages, assistantMsg) messages = append(messages, assistantMsg)

View file

@ -61,6 +61,8 @@ func (p *HTTPProvider) Chat(ctx context.Context, messages []Message, tools []Too
} }
} }
// Pre-process messages loop removed - relying on ExtraContent persistence in Agent Loop.
requestBody := map[string]interface{}{ requestBody := map[string]interface{}{
"model": model, "model": model,
"messages": messages, "messages": messages,
@ -135,6 +137,11 @@ func (p *HTTPProvider) parseResponse(body []byte) (*LLMResponse, error) {
Name string `json:"name"` Name string `json:"name"`
Arguments string `json:"arguments"` Arguments string `json:"arguments"`
} `json:"function"` } `json:"function"`
ExtraContent *struct {
Google *struct {
ThoughtSignature string `json:"thought_signature"`
} `json:"google"`
} `json:"extra_content"`
} `json:"tool_calls"` } `json:"tool_calls"`
} `json:"message"` } `json:"message"`
FinishReason string `json:"finish_reason"` FinishReason string `json:"finish_reason"`
@ -160,7 +167,12 @@ func (p *HTTPProvider) parseResponse(body []byte) (*LLMResponse, error) {
arguments := make(map[string]interface{}) arguments := make(map[string]interface{})
name := "" name := ""
// Handle OpenAI format with nested function object // Extract thought_signature from Gemini/Google-specific extra content
thoughtSignature := ""
if tc.ExtraContent != nil && tc.ExtraContent.Google != nil {
thoughtSignature = tc.ExtraContent.Google.ThoughtSignature
}
if tc.Type == "function" && tc.Function != nil { if tc.Type == "function" && tc.Function != nil {
name = tc.Function.Name name = tc.Function.Name
if tc.Function.Arguments != "" { if tc.Function.Arguments != "" {
@ -178,11 +190,23 @@ func (p *HTTPProvider) parseResponse(body []byte) (*LLMResponse, error) {
} }
} }
toolCalls = append(toolCalls, ToolCall{ // Correctly map extracted ExtraContent to ToolCall struct
toolCall := ToolCall{
ID: tc.ID, ID: tc.ID,
Name: name, Name: name,
Arguments: arguments, Arguments: arguments,
}) ThoughtSignature: thoughtSignature, // Populating internal field for convenience
}
if thoughtSignature != "" {
toolCall.ExtraContent = &ExtraContent{
Google: &GoogleExtra{
ThoughtSignature: thoughtSignature,
},
}
}
toolCalls = append(toolCalls, toolCall)
} }
return &LLMResponse{ return &LLMResponse{

View file

@ -8,11 +8,22 @@ type ToolCall struct {
Function *FunctionCall `json:"function,omitempty"` Function *FunctionCall `json:"function,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Arguments map[string]interface{} `json:"arguments,omitempty"` Arguments map[string]interface{} `json:"arguments,omitempty"`
ThoughtSignature string `json:"-"` // Internal use only
ExtraContent *ExtraContent `json:"extra_content,omitempty"`
}
type ExtraContent struct {
Google *GoogleExtra `json:"google,omitempty"`
}
type GoogleExtra struct {
ThoughtSignature string `json:"thought_signature,omitempty"`
} }
type FunctionCall struct { type FunctionCall struct {
Name string `json:"name"` Name string `json:"name"`
Arguments string `json:"arguments"` Arguments string `json:"arguments"`
ThoughtSignature string `json:"-"` // Internal use only
} }
type LLMResponse struct { type LLMResponse struct {