1package subagents
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "strings"
8 "sync"
9)
10
11// Completion is one resident child's terminal turn outcome.
12type Completion struct {
13 AgentID string
14 TurnID string
15 Status string
16 Task string
17 Error string
18 Summary string
19}
20
21// Completion projects a typed resident outcome into the parent notification
22// shared by interactive and orchestrated modes.
23func (c ResidentCompletion) Completion() Completion {
24 result := Completion{AgentID: c.ChildID, TurnID: c.TurnID, Status: string(ResidentCompleted), Task: c.Task, Summary: c.Summary}
25 if c.Err != nil {
26 result.Status, result.Error = string(ResidentFailed), c.Err.Error()
27 if errors.Is(c.Err, context.Canceled) {
28 result.Status = string(ResidentInterrupted)
29 } else if errors.Is(c.Err, ErrBudgetExceeded) {
30 result.Status = string(ResidentBudgetExhausted)
31 }
32 }
33 return result
34}
35
36// CompletionTracker coordinates resident child completions with the parent
37// turn. It has no process or worker-event dependency.
38type CompletionTracker struct {
39 mu sync.Mutex
40 pending map[string]struct{}
41 ready []Completion
42 changed chan struct{}
43}
44
45func NewCompletionTracker() *CompletionTracker {
46 return &CompletionTracker{changed: make(chan struct{}), pending: make(map[string]struct{})}
47}
48
49// TrackResident records an accepted resident turn. It returns false when the
50// turn is incomplete or already tracked.
51func (t *CompletionTracker) TrackResident(agentID, turnID string) bool {
52 if t == nil {
53 return false
54 }
55 key := completionKey(agentID, turnID)
56 if key == "" {
57 return false
58 }
59 t.mu.Lock()
60 defer t.mu.Unlock()
61 if _, exists := t.pending[key]; exists {
62 return false
63 }
64 t.pending[key] = struct{}{}
65 t.signalLocked()
66 return true
67}
68
69// Report accepts one terminal completion for an accepted resident turn. It
70// returns false for duplicate or unknown terminal reports.
71func (t *CompletionTracker) Report(completion Completion) bool {
72 if t == nil {
73 return false
74 }
75 key := completionKey(completion.AgentID, completion.TurnID)
76 if key == "" {
77 return false
78 }
79 t.mu.Lock()
80 defer t.mu.Unlock()
81 if _, exists := t.pending[key]; !exists {
82 return false
83 }
84 delete(t.pending, key)
85 t.ready = append(t.ready, completion)
86 t.signalLocked()
87 return true
88}
89
90func completionKey(agentID, turnID string) string {
91 if agentID == "" || turnID == "" {
92 return ""
93 }
94 return agentID + "\x00" + turnID
95}
96
97func (t *CompletionTracker) Pending() int {
98 if t == nil {
99 return 0
100 }
101 t.mu.Lock()
102 defer t.mu.Unlock()
103 return len(t.pending)
104}
105
106// Reset drops buffered outcomes at a parent-turn cancellation boundary.
107func (t *CompletionTracker) Reset() {
108 if t == nil {
109 return
110 }
111 t.mu.Lock()
112 t.pending = make(map[string]struct{})
113 t.ready = nil
114 t.signalLocked()
115 t.mu.Unlock()
116}
117
118func (t *CompletionTracker) WaitIdle(ctx context.Context) ([]Completion, error) {
119 if t == nil {
120 return nil, nil
121 }
122 for {
123 t.mu.Lock()
124 if len(t.pending) == 0 {
125 ready := append([]Completion(nil), t.ready...)
126 t.ready = nil
127 t.mu.Unlock()
128 return ready, nil
129 }
130 changed := t.changed
131 t.mu.Unlock()
132 select {
133 case <-ctx.Done():
134 return nil, ctx.Err()
135 case <-changed:
136 }
137 }
138}
139
140func (t *CompletionTracker) signalLocked() { close(t.changed); t.changed = make(chan struct{}) }
141
142func FormatCompletionUpdate(batch []Completion, instruction string) string {
143 if len(batch) == 0 {
144 return ""
145 }
146 var b strings.Builder
147 b.WriteString("[auto-subagents update]\n")
148 for _, completion := range batch {
149 fmt.Fprintf(&b, "- %s: %s", completion.AgentID, completion.Status)
150 if completion.Error != "" {
151 fmt.Fprintf(&b, " (%s)", completion.Error)
152 }
153 if completion.Task != "" {
154 fmt.Fprintf(&b, " — %s", completion.Task)
155 }
156 if completion.Summary != "" {
157 label := "final"
158 if completion.Status != string(ResidentCompleted) {
159 label = "partial"
160 }
161 fmt.Fprintf(&b, "\n %s: %s", label, completion.Summary)
162 }
163 b.WriteByte('\n')
164 }
165 if instruction != "" {
166 b.WriteString(instruction)
167 }
168 return strings.TrimSpace(b.String())
169}