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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
|
// 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
// DependsOn, when non-empty, names sibling task IDs (returned by a prior
// SpawnSubtask call in the same decomposition) this new subtask must wait
// for — set directly on the created child's task.DependsOn, so the
// existing dispatch-gating and cascade-fail-on-dependency-failure
// machinery (already built for top-level tasks) applies unchanged. Empty
// (the default) preserves today's behavior: every spawned subtask is
// structurally independent/parallel.
DependsOn []string
// AcceptanceCriteria, when non-empty, states what this subtask's work
// must satisfy — set directly on the created child's
// task.Task.AcceptanceCriteria. Empty (the default) leaves the child with
// no criteria of its own; a later phase's arbitrated-review
// generalization falls back to the enclosing story's AcceptanceCriteria
// in that case.
AcceptanceCriteria []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)
// ReportVerdict lets an evaluating agent (e.g. an arbitration-role task)
// report a structured approve/reject decision, instead of leaving it to
// be inferred by parsing the task's free-text summary later.
// Implementations record this as an event attached to the calling task's
// own ID; internal/scheduler.StoryOrchestrator's finalizeArbitration
// reads it back once the reporting task completes to decide REVIEW_READY
// vs NEEDS_FIX. A task that never calls this leaves no such event —
// callers reading it back must treat "no event found" as a distinct,
// backward-compatible case, not an error.
ReportVerdict(ctx context.Context, approved bool, reasoning string) 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) }
|