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
|
package executor
import (
"context"
"encoding/json"
"errors"
"time"
"github.com/thepeterstone/claudomator/internal/event"
"github.com/thepeterstone/claudomator/internal/storage"
"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
}
// channelStore is the subset of storage the default channel needs.
type channelStore interface {
CreateTask(t *task.Task) error
CreateEvent(e *event.Event) error
}
// storeChannel is the default AgentChannel backed by storage. Summary reports
// are buffered onto the execution record so the pool persists them once (with
// its extract/synthesize fallbacks); other signals are written immediately.
type storeChannel struct {
store channelStore
taskID string
exec *storage.Execution
}
var _ AgentChannel = (*storeChannel)(nil)
func newStoreChannel(store channelStore, taskID string, exec *storage.Execution) *storeChannel {
return &storeChannel{store: store, taskID: taskID, exec: exec}
}
// AskUser on the default (file-transport) channel cannot deliver an answer
// in-session, so it always reports the run as blocked. Question persistence is
// owned by the pool's BlockedError handling.
func (c *storeChannel) AskUser(_ context.Context, _ string) (string, error) {
return "", ErrAgentBlocked
}
// ReportSummary buffers the summary onto the execution record. The pool persists
// it (preferring this value over its extract/synthesize fallbacks).
func (c *storeChannel) ReportSummary(_ context.Context, summary string) error {
if c.exec != nil {
c.exec.Summary = summary
}
return nil
}
func (c *storeChannel) SpawnSubtask(_ context.Context, spec SubtaskSpec) (string, error) {
now := time.Now().UTC()
child := &task.Task{
ID: uuid.NewString(),
Name: spec.Name,
ParentTaskID: c.taskID,
Agent: task.AgentConfig{
Type: "claude",
Model: spec.Model,
Instructions: spec.Instructions,
MaxBudgetUSD: spec.MaxBudgetUSD,
},
Priority: task.PriorityNormal,
State: task.StatePending,
CreatedAt: now,
UpdatedAt: now,
}
if err := c.store.CreateTask(child); err != nil {
return "", err
}
return child.ID, nil
}
func (c *storeChannel) RecordProgress(_ context.Context, message string) error {
payload, _ := json.Marshal(struct {
Message string `json:"message"`
}{Message: message})
return c.store.CreateEvent(&event.Event{
TaskID: c.taskID,
Kind: event.KindAgentMessage,
Actor: event.ActorAgent,
Payload: payload,
})
}
|