11package main
22
33import (
4+ "bufio"
45 "context"
56 "encoding/json"
67 "fmt"
@@ -24,6 +25,11 @@ type delegateTasksTool struct {
2425 maxConcurrency int
2526 odekPath string // path to the odek binary
2627 timeout time.Duration
28+
29+ // OnSubagentLog, if set, is called with each NDJSON progress line
30+ // emitted by a sub-agent. taskIdx is the index within the current
31+ // batch. Used by the WebUI for live log streaming.
32+ OnSubagentLog func (taskIdx int , line string )
2733}
2834
2935func (t * delegateTasksTool ) Name () string { return "delegate_tasks" }
@@ -112,7 +118,7 @@ func (t *delegateTasksTool) Call(args string) (string, error) {
112118 sem <- struct {}{}
113119 go func (i int , goal , ctx , sys string ) {
114120 defer func () { <- sem }()
115- r := t .runTask (goal , ctx , sys )
121+ r := t .runTask (i , goal , ctx , sys )
116122 mu .Lock ()
117123 results [i ] = r
118124 mu .Unlock ()
@@ -135,7 +141,7 @@ func (t *delegateTasksTool) Call(args string) (string, error) {
135141 return buf .String (), nil
136142}
137143
138- func (t * delegateTasksTool ) runTask (goal , taskContext , system string ) string {
144+ func (t * delegateTasksTool ) runTask (taskIdx int , goal , taskContext , system string ) string {
139145 ctx , cancel := context .WithTimeout (context .Background (), t .timeout )
140146 defer cancel ()
141147
@@ -163,6 +169,7 @@ func (t *delegateTasksTool) runTask(goal, taskContext, system string) string {
163169 "subagent" ,
164170 "--task" , taskPath ,
165171 "--quiet" ,
172+ "--stream" ,
166173 )
167174
168175 stdout , err := cmd .StdoutPipe ()
@@ -178,30 +185,65 @@ func (t *delegateTasksTool) runTask(goal, taskContext, system string) string {
178185 return fmt .Sprintf (`{"error":"start: %v"}` , err )
179186 }
180187
188+ // Read stdout line-by-line — NDJSON progress lines followed by final JSON result
181189 var result map [string ]any
182- if err := json .NewDecoder (stdout ).Decode (& result ); err != nil {
183- // Wait for process to finish, then check for timeout
184- cmd .Wait ()
185- if ctx .Err () != nil {
186- return fmt .Sprintf (`{"error":"timeout after %v"}` , t .timeout )
190+ var lastLine string
191+ scanner := bufio .NewScanner (stdout )
192+ for scanner .Scan () {
193+ line := scanner .Text ()
194+ lastLine = line
195+
196+ // Check if this is a progress line (NDJSON with "type":"tool_call" or "type":"tool_result")
197+ var event struct {
198+ Type string `json:"type"`
199+ }
200+ if err := json .Unmarshal ([]byte (line ), & event ); err == nil {
201+ if event .Type == "tool_call" || event .Type == "tool_result" {
202+ if t .OnSubagentLog != nil {
203+ t .OnSubagentLog (taskIdx , line )
204+ }
205+ continue
206+ }
207+ }
208+
209+ // Not a progress line — parse as result JSON
210+ var r map [string ]any
211+ if err := json .Unmarshal ([]byte (line ), & r ); err == nil {
212+ result = r
187213 }
188- return fmt .Sprintf (`{"error":"parse result: %v"}` , err )
189214 }
215+ scannerErr := scanner .Err ()
190216
191217 if err := cmd .Wait (); err != nil {
192- // Process exited with error — result may still be valid JSON
218+ // Process exited with error — result may still be valid
193219 if result != nil {
194220 summary , _ := json .MarshalIndent (result , "" , " " )
195221 return string (summary )
196222 }
197223 if ctx .Err () != nil {
198224 return fmt .Sprintf (`{"error":"timeout after %v"}` , t .timeout )
199225 }
226+ if scannerErr != nil {
227+ return fmt .Sprintf (`{"error":"read stdout: %v"}` , scannerErr )
228+ }
200229 return fmt .Sprintf (`{"error":"exit: %v"}` , err )
201230 }
202231
203- summary , _ := json .MarshalIndent (result , "" , " " )
204- return string (summary )
232+ if result != nil {
233+ summary , _ := json .MarshalIndent (result , "" , " " )
234+ return string (summary )
235+ }
236+
237+ // Last resort: try parsing the last line as JSON
238+ if lastLine != "" {
239+ var r map [string ]any
240+ if err := json .Unmarshal ([]byte (lastLine ), & r ); err == nil {
241+ summary , _ := json .MarshalIndent (r , "" , " " )
242+ return string (summary )
243+ }
244+ }
245+
246+ return fmt .Sprintf (`{"error":"no result from sub-agent"}` )
205247}
206248
207249// Ensure delegateTasksTool implements odek.Tool
0 commit comments