|
5 | 5 | "context" |
6 | 6 | "encoding/json" |
7 | 7 | "fmt" |
| 8 | + "io" |
8 | 9 | "os" |
9 | 10 | "os/exec" |
10 | 11 | "strings" |
@@ -241,34 +242,15 @@ func (t *delegateTasksTool) runTask(taskIdx int, goal, taskContext, guidance, tr |
241 | 242 | return fmt.Sprintf(`{"error":"start: %v"}`, err) |
242 | 243 | } |
243 | 244 |
|
244 | | - // Read stdout line-by-line — NDJSON progress lines followed by final JSON result |
245 | | - var result map[string]any |
246 | | - var lastLine string |
247 | | - scanner := bufio.NewScanner(stdout) |
248 | | - for scanner.Scan() { |
249 | | - line := scanner.Text() |
250 | | - lastLine = line |
251 | | - |
252 | | - // Check if this is a progress line (NDJSON with "type":"tool_call" or "type":"tool_result") |
253 | | - var event struct { |
254 | | - Type string `json:"type"` |
255 | | - } |
256 | | - if err := json.Unmarshal([]byte(line), &event); err == nil { |
257 | | - if event.Type == "tool_call" || event.Type == "tool_result" { |
258 | | - if t.OnSubagentLog != nil { |
259 | | - t.OnSubagentLog(taskIdx, line) |
260 | | - } |
261 | | - continue |
262 | | - } |
263 | | - } |
264 | | - |
265 | | - // Not a progress line — parse as result JSON |
266 | | - var r map[string]any |
267 | | - if err := json.Unmarshal([]byte(line), &r); err == nil { |
268 | | - result = r |
269 | | - } |
| 245 | + // Read stdout line-by-line — NDJSON progress lines followed by final JSON |
| 246 | + // result. A streamed tool_call event can embed full tool arguments (e.g. a |
| 247 | + // large write_file), so lines routinely exceed bufio.Scanner's default 64KB |
| 248 | + // token cap; scanSubagentStream raises the cap to avoid losing the result. |
| 249 | + var onLog func(line string) |
| 250 | + if t.OnSubagentLog != nil { |
| 251 | + onLog = func(line string) { t.OnSubagentLog(taskIdx, line) } |
270 | 252 | } |
271 | | - scannerErr := scanner.Err() |
| 253 | + result, lastLine, scannerErr := scanSubagentStream(stdout, onLog) |
272 | 254 |
|
273 | 255 | if err := cmd.Wait(); err != nil { |
274 | 256 | // Process exited with error — result may still be valid |
@@ -302,5 +284,49 @@ func (t *delegateTasksTool) runTask(taskIdx int, goal, taskContext, guidance, tr |
302 | 284 | return `{"error":"no result from sub-agent"}` |
303 | 285 | } |
304 | 286 |
|
| 287 | +// maxSubagentLine caps a single NDJSON line read from a sub-agent's stdout. |
| 288 | +// Streamed tool_call events embed full tool arguments (e.g. a large write_file |
| 289 | +// or patch), which routinely exceed bufio.Scanner's default 64KB token cap. |
| 290 | +// Without a raised cap the scanner returns bufio.ErrTooLong, the reader stops, |
| 291 | +// the child blocks writing to a full stdout pipe, and cmd.Wait hangs until the |
| 292 | +// timeout kills a task that actually completed successfully. |
| 293 | +const maxSubagentLine = 10 << 20 // 10 MiB |
| 294 | + |
| 295 | +// scanSubagentStream reads a sub-agent's NDJSON stdout: zero or more progress |
| 296 | +// lines (objects with "type":"tool_call" or "type":"tool_result") followed by |
| 297 | +// the final JSON result object. Progress lines are forwarded to onLog (when |
| 298 | +// non-nil); the last line that parses as a JSON object is returned as result. |
| 299 | +// It returns the result map (nil if none parsed), the last raw line seen, and |
| 300 | +// any scanner error. The scan buffer is sized to maxSubagentLine so large |
| 301 | +// streamed events do not truncate the stream. |
| 302 | +func scanSubagentStream(r io.Reader, onLog func(line string)) (result map[string]any, lastLine string, err error) { |
| 303 | + scanner := bufio.NewScanner(r) |
| 304 | + scanner.Buffer(make([]byte, 0, 64*1024), maxSubagentLine) |
| 305 | + for scanner.Scan() { |
| 306 | + line := scanner.Text() |
| 307 | + lastLine = line |
| 308 | + |
| 309 | + // Check if this is a progress line (NDJSON with "type":"tool_call" or "type":"tool_result") |
| 310 | + var event struct { |
| 311 | + Type string `json:"type"` |
| 312 | + } |
| 313 | + if uerr := json.Unmarshal([]byte(line), &event); uerr == nil { |
| 314 | + if event.Type == "tool_call" || event.Type == "tool_result" { |
| 315 | + if onLog != nil { |
| 316 | + onLog(line) |
| 317 | + } |
| 318 | + continue |
| 319 | + } |
| 320 | + } |
| 321 | + |
| 322 | + // Not a progress line — parse as result JSON |
| 323 | + var rmap map[string]any |
| 324 | + if uerr := json.Unmarshal([]byte(line), &rmap); uerr == nil { |
| 325 | + result = rmap |
| 326 | + } |
| 327 | + } |
| 328 | + return result, lastLine, scanner.Err() |
| 329 | +} |
| 330 | + |
305 | 331 | // Ensure delegateTasksTool implements odek.Tool |
306 | 332 | var _ odek.Tool = (*delegateTasksTool)(nil) |
0 commit comments