summaryrefslogtreecommitdiff
path: root/internal/llm/client.go
diff options
context:
space:
mode:
authorClaude <noreply@anthropic.com>2026-05-26 07:11:59 +0000
committerClaude <noreply@anthropic.com>2026-05-26 07:11:59 +0000
commit301e7a66387f99ab76754d08bca42f4a9930d3b1 (patch)
tree35815d99028d53f99b800a91437b455f043830bc /internal/llm/client.go
parent65cd7ea65d9c6fe0fad39bb2c5cac70d61153444 (diff)
feat(executor,llm): LocalRunner agent-channel via OpenAI tool-use (Phase 5)
LocalRunner previously ignored the AgentChannel and produced a single fire-and-forget completion. It now declares the four agent back-channel tools (ask_user/report_summary/spawn_subtask/record_progress) as OpenAI function-calling definitions and runs a tool-use loop: each turn feeds tool results back as message history (re-feed) until the model stops calling tools, bounded by maxLocalToolTurns. ask_user converts a buffered question into a *BlockedError so the task blocks like the container runners. Adds tool-use support to the llm client (Tool/ToolCall/ToolFunction types, Tools on ChatRequest, ToolCalls on ChatResponse + wire request/response). The loop uses non-streaming Chat (tool_calls don't stream cleanly); assistant text is still written to stdout.log in the Claude stream-json envelope so summary/ changestats parsing is unchanged. Fully tested against a mock OpenAI endpoint + storeChannel: spawn/summary/ progress dispatch, ask_user blocking, token accumulation, and the llm tools round-trip. NOTE: local resume re-feeds conversation state (Decision #8) — not yet wired, so a blocked local task resumes fresh for now. https://claude.ai/code/session_01SESwn7kQ7oP62trWw6pc39
Diffstat (limited to 'internal/llm/client.go')
-rw-r--r--internal/llm/client.go37
1 files changed, 37 insertions, 0 deletions
diff --git a/internal/llm/client.go b/internal/llm/client.go
index 613ebe5..e3a6102 100644
--- a/internal/llm/client.go
+++ b/internal/llm/client.go
@@ -34,6 +34,38 @@ type Client struct {
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
+ // ToolCalls is set on assistant messages that request tool invocations.
+ ToolCalls []ToolCall `json:"tool_calls,omitempty"`
+ // ToolCallID links a role:"tool" result message to the call it answers.
+ ToolCallID string `json:"tool_call_id,omitempty"`
+ // Name carries the tool name on role:"tool" result messages.
+ Name string `json:"name,omitempty"`
+}
+
+// Tool declares a function the model may call (OpenAI function-calling format).
+type Tool struct {
+ Type string `json:"type"` // always "function"
+ Function ToolFunction `json:"function"`
+}
+
+// ToolFunction describes a callable function and its JSON-schema parameters.
+type ToolFunction struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Parameters map[string]any `json:"parameters,omitempty"`
+}
+
+// ToolCall is a function invocation the model emitted in its response.
+type ToolCall struct {
+ ID string `json:"id"`
+ Type string `json:"type"` // "function"
+ Function ToolCallFunction `json:"function"`
+}
+
+// ToolCallFunction holds the called function's name and raw JSON arguments.
+type ToolCallFunction struct {
+ Name string `json:"name"`
+ Arguments string `json:"arguments"`
}
// ChatRequest captures the parameters of a single Chat or ChatStream call.
@@ -45,6 +77,7 @@ type ChatRequest struct {
Temperature *float64
MaxTokens int
ResponseJSON bool
+ Tools []Tool
}
// ChatResponse is the aggregated result of a chat completion.
@@ -54,6 +87,7 @@ type ChatResponse struct {
OutputTokens int
Model string
FinishReason string
+ ToolCalls []ToolCall
}
// Chat performs a non-streaming chat completion. Rate-limit errors (HTTP 429,
@@ -87,6 +121,7 @@ func (c *Client) Chat(ctx context.Context, req ChatRequest) (*ChatResponse, erro
OutputTokens: oai.Usage.CompletionTokens,
Model: oai.Model,
FinishReason: oai.Choices[0].FinishReason,
+ ToolCalls: oai.Choices[0].Message.ToolCalls,
}
return nil
})
@@ -133,6 +168,7 @@ func (c *Client) buildRequestBody(req ChatRequest, stream bool) ([]byte, error)
Model: model,
Messages: req.Messages,
Stream: stream,
+ Tools: req.Tools,
}
if req.Temperature != nil {
payload.Temperature = req.Temperature
@@ -301,6 +337,7 @@ type openAIRequest struct {
Stream bool `json:"stream,omitempty"`
StreamOptions *streamOptions `json:"stream_options,omitempty"`
ResponseFormat *responseFormat `json:"response_format,omitempty"`
+ Tools []Tool `json:"tools,omitempty"`
}
type streamOptions struct {