Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,39 @@
# ============================================================
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 link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AI fluff

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

# ============================================================
# Stage 2: Minimal runtime image
# ============================================================
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"]
10 changes: 8 additions & 2 deletions pkg/agent/loop.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Comment on lines +634 to +636
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.

ThoughtSignture should be included in ExtraContent. Why are we pulling it out to its own field?

Have you tested this with non Google LLMs?

})
}
messages = append(messages, assistantMsg)
Expand Down
36 changes: 30 additions & 6 deletions pkg/providers/http_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"`
Expand All @@ -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
}
Comment on lines +170 to +174
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.

seems like extra lines of code when tc.ExtraContent can just be passed to ToolCall struct


if tc.Type == "function" && tc.Function != nil {
name = tc.Function.Name
if tc.Function.Arguments != "" {
Expand All @@ -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
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.

Whats this for? Seems to be the wrong structure of a tool call, Thought Signature is an ExtraContent field based by google.

Since this is the HTTP Provider other services other than google will use it.

}

if thoughtSignature != "" {
toolCall.ExtraContent = &ExtraContent{
Google: &GoogleExtra{
ThoughtSignature: thoughtSignature,
},
}
}
Comment on lines +201 to +207
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.

Extra assignment when line 194 set tool call fields


toolCalls = append(toolCalls, toolCall)
}

return &LLMResponse{
Expand Down
25 changes: 18 additions & 7 deletions pkg/providers/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
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.

Remove

ExtraContent *ExtraContent `json:"extra_content,omitempty"`
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.

Why is this a pointer?

}

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 {
Expand Down