summaryrefslogtreecommitdiff
path: root/internal/agentchannel/agentchannel.go
blob: 433b76278c0ee506a93d4caf7170cdc2b23a797e (plain)
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
// 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
}

// 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) }