@@ -84,18 +84,20 @@ const (
8484 taskFailed
8585)
8686
87- var taskStatusStrings = map [taskStatus ]string {
88- taskRunning : "running" ,
89- taskCompleted : "completed" ,
90- taskStopped : "stopped" ,
91- taskFailed : "failed" ,
92- }
93-
94- func statusToString (s taskStatus ) string {
95- if str , ok := taskStatusStrings [s ]; ok {
96- return str
87+ // String returns a human-readable name for the status.
88+ func (s taskStatus ) String () string {
89+ switch s {
90+ case taskRunning :
91+ return "running"
92+ case taskCompleted :
93+ return "completed"
94+ case taskStopped :
95+ return "stopped"
96+ case taskFailed :
97+ return "failed"
98+ default :
99+ return "unknown"
97100 }
98- return "unknown"
99101}
100102
101103// task tracks a single background sub-agent execution.
@@ -104,18 +106,16 @@ type task struct {
104106 agentName string
105107 taskDesc string
106108
107- cancel context.CancelFunc
108- outputMu sync.RWMutex
109- output strings.Builder
110- outputBytes int
111- startTime time.Time
112- status atomic.Int32
113- result string
114- errMsg string
115-
116- // viewCount tracks how many consecutive HandleView calls observed no
117- // new output. It is reset whenever the buffered output grows.
118- // Protected by outputMu.
109+ cancel context.CancelFunc
110+ startTime time.Time
111+ status atomic.Int32
112+ result string
113+ errMsg string
114+
115+ // outputMu protects output, outputBytes, viewCount, and lastViewedOutputBytes.
116+ outputMu sync.RWMutex
117+ output strings.Builder
118+ outputBytes int
119119 viewCount int
120120 lastViewedOutputBytes int
121121}
@@ -132,6 +132,79 @@ func (t *task) casStatus(old, next taskStatus) bool {
132132 return t .status .CompareAndSwap (int32 (old ), int32 (next ))
133133}
134134
135+ // writeOutput appends content to the task's live output buffer, respecting the
136+ // maxOutputBytes cap. It is safe for concurrent use.
137+ func (t * task ) writeOutput (content string ) {
138+ t .outputMu .Lock ()
139+ defer t .outputMu .Unlock ()
140+
141+ if t .outputBytes < maxOutputBytes {
142+ n , _ := t .output .WriteString (content )
143+ t .outputBytes += n
144+ }
145+ }
146+
147+ // formatView builds the human-readable output section for HandleView.
148+ // It covers all terminal and in-progress states. The caller supplies the
149+ // pre-loaded status and elapsed duration.
150+ func (t * task ) formatView (status taskStatus , elapsed time.Duration ) string {
151+ var out strings.Builder
152+ fmt .Fprintf (& out , "Task ID: %s\n " , t .id )
153+ fmt .Fprintf (& out , "Agent: %s\n " , t .agentName )
154+ fmt .Fprintf (& out , "Status: %s\n " , status )
155+ fmt .Fprintf (& out , "Runtime: %s\n " , elapsed )
156+ out .WriteString ("\n --- Output ---\n " )
157+
158+ switch status {
159+ case taskCompleted :
160+ if t .result != "" {
161+ out .WriteString (t .result )
162+ } else {
163+ out .WriteString ("<no output>" )
164+ }
165+
166+ case taskFailed :
167+ out .WriteString ("<task failed>" )
168+ if t .errMsg != "" {
169+ fmt .Fprintf (& out , "\n Error: %s" , t .errMsg )
170+ }
171+
172+ case taskStopped :
173+ out .WriteString ("<task was stopped>" )
174+
175+ default : // taskRunning (or any unexpected value)
176+ t .outputMu .Lock ()
177+ progress := t .output .String ()
178+ truncated := t .outputBytes >= maxOutputBytes
179+ currentBytes := t .outputBytes
180+
181+ if currentBytes == t .lastViewedOutputBytes {
182+ t .viewCount ++
183+ } else {
184+ t .viewCount = 1
185+ t .lastViewedOutputBytes = currentBytes
186+ }
187+ viewCount := t .viewCount
188+ t .outputMu .Unlock ()
189+
190+ if progress != "" {
191+ out .WriteString (progress )
192+ if truncated {
193+ out .WriteString ("\n \n [output truncated at 10MB limit — still running...]" )
194+ } else {
195+ out .WriteString ("\n \n [still running...]" )
196+ }
197+ } else {
198+ out .WriteString ("<no output yet — still running>" )
199+ }
200+ if viewCount > 1 {
201+ fmt .Fprintf (& out , "\n \n [No new output since last check — poll #%d]" , viewCount )
202+ }
203+ }
204+
205+ return out .String ()
206+ }
207+
135208// Handler owns all background agent tasks and provides tool handlers.
136209type Handler struct {
137210 runner Runner
@@ -169,8 +242,7 @@ func (h *Handler) totalTaskCount() int {
169242func (h * Handler ) pruneCompleted () {
170243 var toDelete []string
171244 h .tasks .Range (func (id string , t * task ) bool {
172- s := t .loadStatus ()
173- if s == taskCompleted || s == taskStopped || s == taskFailed {
245+ if s := t .loadStatus (); s != taskRunning {
174246 toDelete = append (toDelete , id )
175247 }
176248 return true
@@ -195,8 +267,7 @@ func (h *Handler) HandleRun(ctx context.Context, sess *session.Session, toolCall
195267 }
196268
197269 subAgentNames := h .runner .CurrentAgentSubAgentNames ()
198- valid := slices .Contains (subAgentNames , params .Agent )
199- if ! valid {
270+ if ! slices .Contains (subAgentNames , params .Agent ) {
200271 if len (subAgentNames ) > 0 {
201272 return tools .ResultError (fmt .Sprintf ("agent %q is not in the sub-agents list. Available: %s" , params .Agent , strings .Join (subAgentNames , ", " ))), nil
202273 }
@@ -218,7 +289,11 @@ func (h *Handler) HandleRun(ctx context.Context, sess *session.Session, toolCall
218289
219290 taskID := newTaskID ()
220291
221- taskCtx , cancel := context .WithCancel (ctx )
292+ // Use WithoutCancel so the background task is not killed when the
293+ // parent message context is cancelled (e.g. the user sends a new
294+ // message in the TUI). The task can still be explicitly stopped
295+ // via HandleStop which calls cancel().
296+ taskCtx , cancel := context .WithCancel (context .WithoutCancel (ctx ))
222297
223298 t := & task {
224299 id : taskID ,
@@ -240,14 +315,7 @@ func (h *Handler) HandleRun(ctx context.Context, sess *session.Session, toolCall
240315 Task : params .Task ,
241316 ExpectedOutput : params .ExpectedOutput ,
242317 ParentSession : sess ,
243- OnContent : func (content string ) {
244- t .outputMu .Lock ()
245- if t .outputBytes < maxOutputBytes {
246- n , _ := t .output .WriteString (content )
247- t .outputBytes += n
248- }
249- t .outputMu .Unlock ()
250- },
318+ OnContent : t .writeOutput ,
251319 })
252320
253321 if result .ErrMsg != "" {
@@ -283,11 +351,10 @@ func (h *Handler) HandleList(_ context.Context, _ *session.Session, _ tools.Tool
283351 var count int
284352 h .tasks .Range (func (_ string , t * task ) bool {
285353 count ++
286- status := t .loadStatus ()
287354 elapsed := time .Since (t .startTime ).Round (time .Second )
288355 fmt .Fprintf (& out , "ID: %s\n " , t .id )
289356 fmt .Fprintf (& out , " Agent: %s\n " , t .agentName )
290- fmt .Fprintf (& out , " Status: %s\n " , statusToString ( status ))
357+ fmt .Fprintf (& out , " Status: %s\n " , t . loadStatus ( ))
291358 fmt .Fprintf (& out , " Runtime: %s\n " , elapsed )
292359 out .WriteString ("\n " )
293360 return true
@@ -315,59 +382,7 @@ func (h *Handler) HandleView(_ context.Context, _ *session.Session, toolCall too
315382 status := t .loadStatus ()
316383 elapsed := time .Since (t .startTime ).Round (time .Second )
317384
318- var out strings.Builder
319- fmt .Fprintf (& out , "Task ID: %s\n " , t .id )
320- fmt .Fprintf (& out , "Agent: %s\n " , t .agentName )
321- fmt .Fprintf (& out , "Status: %s\n " , statusToString (status ))
322- fmt .Fprintf (& out , "Runtime: %s\n " , elapsed )
323- out .WriteString ("\n --- Output ---\n " )
324-
325- switch status {
326- case taskCompleted :
327- if t .result != "" {
328- out .WriteString (t .result )
329- } else {
330- out .WriteString ("<no output>" )
331- }
332- case taskFailed :
333- out .WriteString ("<task failed>" )
334- if t .errMsg != "" {
335- fmt .Fprintf (& out , "\n Error: %s" , t .errMsg )
336- }
337- case taskStopped :
338- out .WriteString ("<task was stopped>" )
339- default :
340- t .outputMu .Lock ()
341- progress := t .output .String ()
342- truncated := t .outputBytes >= maxOutputBytes
343- currentBytes := t .outputBytes
344-
345- // Track whether output has changed since the last view.
346- if currentBytes == t .lastViewedOutputBytes {
347- t .viewCount ++
348- } else {
349- t .viewCount = 1
350- t .lastViewedOutputBytes = currentBytes
351- }
352- viewCount := t .viewCount
353- t .outputMu .Unlock ()
354-
355- if progress != "" {
356- out .WriteString (progress )
357- if truncated {
358- out .WriteString ("\n \n [output truncated at 10MB limit — still running...]" )
359- } else {
360- out .WriteString ("\n \n [still running...]" )
361- }
362- } else {
363- out .WriteString ("<no output yet — still running>" )
364- }
365- if viewCount > 1 {
366- fmt .Fprintf (& out , "\n \n [No new output since last check — poll #%d]" , viewCount )
367- }
368- }
369-
370- return tools .ResultSuccess (out .String ()), nil
385+ return tools .ResultSuccess (t .formatView (status , elapsed )), nil
371386}
372387
373388// HandleStop cancels a running background agent task.
@@ -383,8 +398,7 @@ func (h *Handler) HandleStop(_ context.Context, _ *session.Session, toolCall too
383398 }
384399
385400 if ! t .casStatus (taskRunning , taskStopped ) {
386- current := t .loadStatus ()
387- return tools .ResultError (fmt .Sprintf ("task %s is not running (status: %s)" , params .TaskID , statusToString (current ))), nil
401+ return tools .ResultError (fmt .Sprintf ("task %s is not running (status: %s)" , params .TaskID , t .loadStatus ())), nil
388402 }
389403
390404 t .cancel ()
@@ -424,7 +438,7 @@ func NewToolSet() tools.ToolSet {
424438type toolSet struct {}
425439
426440func (t * toolSet ) Tools (context.Context ) ([]tools.Tool , error ) {
427- return backgroundAgentTools ()
441+ return backgroundAgentTools (), nil
428442}
429443
430444func (t * toolSet ) Instructions () string {
@@ -440,7 +454,7 @@ Use background agent tasks to dispatch work to sub-agents concurrently.
440454**Notes**: Output capped at 10MB per task. All tasks auto-terminate when the agent stops.`
441455}
442456
443- func backgroundAgentTools () ( []tools.Tool , error ) {
457+ func backgroundAgentTools () []tools.Tool {
444458 return []tools.Tool {
445459 {
446460 Name : ToolNameRunBackgroundAgent ,
@@ -480,5 +494,5 @@ view_background_agent and collect results once the task is complete.`,
480494 Title : "Stop Background Agent" ,
481495 },
482496 },
483- }, nil
497+ }
484498}
0 commit comments