1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
|
// 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) }
|