From 67e0081c6d573b701ed931f96e14dbe5b4258a17 Mon Sep 17 00:00:00 2001 From: Claude Sonnet 5 Date: Fri, 3 Jul 2026 08:49:43 +0000 Subject: refactor(executor): extract provider-neutral tool-use loop (Phase 1) Splits LocalRunner's OpenAI-specific agentic loop into reusable, provider- agnostic pieces so later phases can add native Anthropic/OpenAI/Google/Groq/ OpenRouter adapters without duplicating the control flow: - internal/provider: neutral Provider/ChatRequest/ChatResponse types, plus an openaicompat adapter wrapping the existing internal/llm.Client unchanged - internal/sandbox: Sandbox interface + HostSandbox (git clone/push/cleanup, read_file/write_file/run_bash/glob), lifted verbatim from local.go/localtools.go - internal/agentloop: the extracted tool-use loop (request/response/tool- dispatch/loop, ask_user blocking, stream-json envelope, summary fallback) - internal/agentchannel: AgentChannel/SubtaskSpec/BlockedError/ErrAgentBlocked moved out of internal/executor so agentloop can use them without an import cycle; internal/executor re-exports via type aliases, so no call site changes - internal/executor/nativerunner.go: NativeRunner replaces LocalRunner, wiring agentloop.Loop + openaicompat + HostSandbox together - config.Providers map[string]ProviderConfig added (unused until Phase 2+) Zero intended behavior change: go test -race ./... passes across all packages, and end-to-end stream-json/summary/changestats output was verified byte-compatible against a fake OpenAI-compatible server. Adds test coverage for sandbox tool-dispatch (git clone/push, read/write/bash/glob) that LocalRunner never had. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs --- internal/executor/channel.go | 51 +++++++++++++++++--------------------------- 1 file changed, 19 insertions(+), 32 deletions(-) (limited to 'internal/executor/channel.go') diff --git a/internal/executor/channel.go b/internal/executor/channel.go index 8605ffa..debe21d 100644 --- a/internal/executor/channel.go +++ b/internal/executor/channel.go @@ -3,46 +3,33 @@ package executor import ( "context" "encoding/json" - "errors" "sync" "time" + "github.com/thepeterstone/claudomator/internal/agentchannel" "github.com/thepeterstone/claudomator/internal/event" "github.com/thepeterstone/claudomator/internal/task" "github.com/google/uuid" ) -// ErrAgentBlocked is returned by AgentChannel.AskUser when the transport cannot -// deliver an answer within the current run — e.g. the file transport, which can -// only write-and-exit. Runners translate it into a *BlockedError so the task -// blocks and resumes later. Transports that can suspend the agent in-session -// (MCP, Phase 2) instead block and return the answer. -var ErrAgentBlocked = errors.New("agent blocked awaiting user input") - -// SubtaskSpec describes a child task an agent wants to spawn. -type SubtaskSpec struct { - Name string - Instructions string - Model string - MaxBudgetUSD float64 -} - -// AgentChannel is how a Runner reports agent-originated signals to the rest of -// the system. Implementations translate these into stored artifacts and events. -// The transport by which a Runner detects these signals — post-exit files -// today, MCP/tool-use in Phase 2 — is the Runner's private concern. -type AgentChannel interface { - // AskUser records that the agent needs human input. Transports that can - // suspend the agent in-session return the answer; those that cannot return - // ErrAgentBlocked. - AskUser(ctx context.Context, questionJSON string) (answer string, err error) - // ReportSummary records the agent's final summary text. - ReportSummary(ctx context.Context, summary string) error - // SpawnSubtask creates a child task and returns its ID. - SpawnSubtask(ctx context.Context, spec SubtaskSpec) (taskID string, err error) - // RecordProgress records a free-form progress note from the agent. - RecordProgress(ctx context.Context, message string) error -} +// AgentChannel, SubtaskSpec, BlockedError, and ErrAgentBlocked used to be +// defined directly in this package. Phase 1 of the multi-provider refactor +// moved them to internal/agentchannel (a small leaf package with no +// executor dependency) so that internal/agentloop — the new provider-neutral +// tool-use loop — can construct/inspect them without importing +// internal/executor. That matters because internal/executor now imports +// internal/agentloop (via nativerunner.go, which wires an agentloop.Loop up +// to a Runner); if agentloop imported executor too, that would be a direct +// import cycle. Re-exporting via type aliases (and a var assignment for the +// sentinel error) here means every existing reference to +// executor.AgentChannel / executor.SubtaskSpec / executor.BlockedError / +// executor.ErrAgentBlocked keeps compiling and behaving identically — an +// alias is the same type, not a copy. +type AgentChannel = agentchannel.AgentChannel +type SubtaskSpec = agentchannel.SubtaskSpec +type BlockedError = agentchannel.BlockedError + +var ErrAgentBlocked = agentchannel.ErrAgentBlocked // channelStore is the subset of storage the default channel needs. type channelStore interface { -- cgit v1.2.3