// Package agentchannel holds the runner-agnostic types describing how a Runner // reports agent-originated signals (summary, questions, subtasks, progress) // back to the rest of the system, plus the sentinel/blocked-run error types // that flow through that channel. // // These types used to live in internal/executor. They were extracted here in // Phase 1 of the multi-provider refactor so that internal/agentloop (which // needs to construct/inspect them while running the provider-neutral tool-use // loop) does not have to import internal/executor — and internal/executor, in // turn, can import internal/agentloop (via NativeRunner) without creating an // import cycle. internal/executor re-exports everything here via type // aliases/var assignments (see internal/executor/channel.go) so existing code // that refers to executor.AgentChannel, executor.SubtaskSpec, // executor.BlockedError, and executor.ErrAgentBlocked keeps compiling // unchanged. package agentchannel import ( "context" "errors" "fmt" "github.com/thepeterstone/claudomator/internal/role" ) // 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 // Role, when non-empty, names an internal/role.RoleConfig the child task // should dispatch through (task.AgentConfig.Role — see Phase 5's // role-based dispatch in internal/executor.Pool.execute()). When set, the // storeChannel implementation leaves Agent.Type/Model empty on the child // so tier 0 of the role's escalation ladder resolves them, exactly like // any other role-typed task. Empty (the default) preserves the prior // hardcoded "claude" behavior for backward compatibility. Role string } // EpicProposal describes an epic a discovery/planner-role agent wants to // group one or more stories under (internal/story.Epic/Story — the planning // layer above the flat task tree). Matching an existing epic is by exact // Name (see storeChannel.ProposeEpic in internal/executor/channel.go) — the // simplest reasonable de-dup, not fuzzy matching. type EpicProposal struct { Name string Description string // StoryIDs are the story IDs to group under this epic. An ID that // doesn't resolve to an existing story is skipped rather than failing // the whole call — one bad ID shouldn't block grouping the rest. StoryIDs []string } // 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 // ProposeEpic groups spec.StoryIDs under a new or existing epic (matched // by name), returning the epic's ID. Called by a discovery/planner-role // agent that has independently judged a set of stories to form a // cohesive initiative — the judgment itself is the calling agent's job // (via its instructions/model); this just gives it a mechanism to act on // it. ProposeEpic(ctx context.Context, spec EpicProposal) (epicID string, err error) // ProposeRoleConfig lets a retro-role agent (internal/scheduler. // StoryOrchestrator's Phase 8 retro stage) propose a new draft // role_configs version for a role, after reflecting on a completed // story's history (task tree, escalations, cost, evaluator/arbitration // feedback). Implementations create a new "draft"-status role_configs // row and return its version number — they never touch whatever version // is currently "active" for that role; promoting a draft to active stays // a human action via the existing POST /api/roles/{role}/activate. ProposeRoleConfig(ctx context.Context, config role.RoleConfig) (version int, err error) } // BlockedError is returned by Run when the agent asked the user a question and // the run stopped to await an answer. The pool transitions the task to BLOCKED // and stores the question for the user. type BlockedError struct { QuestionJSON string // raw JSON from the question SessionID string // claude session to resume once the user answers SandboxDir string // preserved sandbox path; resume must run here so the agent finds its session files } func (e *BlockedError) Error() string { return fmt.Sprintf("task blocked: %s", e.QuestionJSON) }