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