From 301e7a66387f99ab76754d08bca42f4a9930d3b1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 07:11:59 +0000 Subject: feat(executor,llm): LocalRunner agent-channel via OpenAI tool-use (Phase 5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- internal/llm/client.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) (limited to 'internal/llm/client.go') 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 { -- cgit v1.2.3