From 5ac9a46f596bddd88b0cf658f01b9415224adf98 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 16 Feb 2026 11:59:10 +0000 Subject: [PATCH] fix(gemini): Implement Gemini 3 thought_signature handling for stateful reasoning --- Dockerfile | 17 ++++++++++------ pkg/agent/loop.go | 10 ++++++++-- pkg/providers/http_provider.go | 36 ++++++++++++++++++++++++++++------ pkg/providers/types.go | 25 ++++++++++++++++------- 4 files changed, 67 insertions(+), 21 deletions(-) diff --git a/Dockerfile b/Dockerfile index dd98ec0bd..08a768b77 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,16 +3,19 @@ # ============================================================ 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 -# Cache dependencies +# Cache dependencies for faster subsequent builds COPY go.mod go.sum ./ RUN go mod download -# Copy source and build +# Copy your local source code (where you'll add the Thought Signature fix) COPY . . + +# Compile the binary RUN make build # ============================================================ @@ -20,17 +23,19 @@ RUN make build # ============================================================ FROM alpine:3.23 +# Install runtime essentials RUN apk add --no-cache ca-certificates tzdata curl # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ 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 -# Create picoclaw home directory +# Create necessary directories and initialize RUN /usr/local/bin/picoclaw onboard +# Set the binary as the entrypoint ENTRYPOINT ["picoclaw"] -CMD ["gateway"] +CMD ["gateway"] \ No newline at end of file diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index cd4276155..4c3313d18 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -621,13 +621,19 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, messages []providers.M } for _, tc := range response.ToolCalls { argumentsJSON, _ := json.Marshal(tc.Arguments) + // Copy ExtraContent to ensure thought_signature is persisted + extraContent := tc.ExtraContent + assistantMsg.ToolCalls = append(assistantMsg.ToolCalls, providers.ToolCall{ ID: tc.ID, Type: "function", Function: &providers.FunctionCall{ - Name: tc.Name, - Arguments: string(argumentsJSON), + Name: tc.Name, + Arguments: string(argumentsJSON), }, + ExtraContent: extraContent, + // We also set internal ThoughtSignature, but ExtraContent is what matters for serialization + ThoughtSignature: tc.ThoughtSignature, }) } messages = append(messages, assistantMsg) diff --git a/pkg/providers/http_provider.go b/pkg/providers/http_provider.go index 4cf2c6db2..b0e92f1e1 100644 --- a/pkg/providers/http_provider.go +++ b/pkg/providers/http_provider.go @@ -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{}{ "model": model, "messages": messages, @@ -135,6 +137,11 @@ func (p *HTTPProvider) parseResponse(body []byte) (*LLMResponse, error) { Name string `json:"name"` Arguments string `json:"arguments"` } `json:"function"` + ExtraContent *struct { + Google *struct { + ThoughtSignature string `json:"thought_signature"` + } `json:"google"` + } `json:"extra_content"` } `json:"tool_calls"` } `json:"message"` FinishReason string `json:"finish_reason"` @@ -160,7 +167,12 @@ func (p *HTTPProvider) parseResponse(body []byte) (*LLMResponse, error) { arguments := make(map[string]interface{}) 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 { name = tc.Function.Name if tc.Function.Arguments != "" { @@ -178,11 +190,23 @@ func (p *HTTPProvider) parseResponse(body []byte) (*LLMResponse, error) { } } - toolCalls = append(toolCalls, ToolCall{ - ID: tc.ID, - Name: name, - Arguments: arguments, - }) + // Correctly map extracted ExtraContent to ToolCall struct + toolCall := ToolCall{ + ID: tc.ID, + Name: name, + Arguments: arguments, + ThoughtSignature: thoughtSignature, // Populating internal field for convenience + } + + if thoughtSignature != "" { + toolCall.ExtraContent = &ExtraContent{ + Google: &GoogleExtra{ + ThoughtSignature: thoughtSignature, + }, + } + } + + toolCalls = append(toolCalls, toolCall) } return &LLMResponse{ diff --git a/pkg/providers/types.go b/pkg/providers/types.go index 88b62e975..6df6c9d2d 100644 --- a/pkg/providers/types.go +++ b/pkg/providers/types.go @@ -3,16 +3,27 @@ package providers import "context" type ToolCall struct { - ID string `json:"id"` - Type string `json:"type,omitempty"` - Function *FunctionCall `json:"function,omitempty"` - Name string `json:"name,omitempty"` - Arguments map[string]interface{} `json:"arguments,omitempty"` + ID string `json:"id"` + Type string `json:"type,omitempty"` + Function *FunctionCall `json:"function,omitempty"` + Name string `json:"name,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 { - Name string `json:"name"` - Arguments string `json:"arguments"` + Name string `json:"name"` + Arguments string `json:"arguments"` + ThoughtSignature string `json:"-"` // Internal use only } type LLMResponse struct {