@ferris1225/pi-subagents 4.1.7 → 4.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -65
- package/agents/cleaner.md +13 -14
- package/agents/documenter.md +10 -17
- package/agents/explorer.md +6 -16
- package/agents/reviewer.md +28 -29
- package/agents/worker.md +14 -33
- package/package.json +1 -1
- package/src/announcements.ts +30 -67
- package/src/background.ts +25 -12
- package/src/config.ts +9 -170
- package/src/dispatch.ts +721 -747
- package/src/durable.ts +336 -0
- package/src/fixloop.ts +37 -37
- package/src/format.ts +1 -8
- package/src/index.ts +8 -1
- package/src/models.ts +16 -0
- package/src/monitor.ts +28 -29
- package/src/prompt.ts +7 -8
- package/src/rpc-run.ts +22 -228
- package/src/runtime.ts +72 -50
- package/src/session-fork.ts +7 -2
- package/src/setup.ts +0 -41
- package/src/spawn.ts +32 -29
- package/src/temp-hygiene.ts +194 -0
- package/src/thread-lifecycle.ts +1410 -1327
- package/src/tools.ts +21 -108
- package/src/widget.ts +3 -3
- package/src/worktree.ts +144 -4
package/src/dispatch.ts
CHANGED
|
@@ -1,747 +1,721 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `subagent` tool: dispatches explorer/worker/cleaner/documenter/reviewer agents as isolated pi
|
|
3
|
-
* child processes, single or parallel. Owns the public dispatch contract,
|
|
4
|
-
* per-run status tracking, managed worker/cleaner → reviewer workflows with a
|
|
5
|
-
* conditional documenter, bounded worker/reviewer fix rounds, and internal
|
|
6
|
-
* step launching. Stable
|
|
7
|
-
* thread generations, final integration, and completion ownership live in
|
|
8
|
-
* thread-lifecycle.ts.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { StringEnum } from "@earendil-works/pi-ai";
|
|
12
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
type
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
agent: Type.
|
|
79
|
-
task: Type.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process
|
|
84
|
-
isolation: IsolationSchema,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
]
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
//
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
|
|
723
|
-
const usage = formatUsage(r.usage);
|
|
724
|
-
const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (main after ${r.modelFallbackFrom} failed)` : ""}`;
|
|
725
|
-
const isolation = r.isolation === "worktree" ? ` · worktree ${r.integrationStatus ?? "active"}` : "";
|
|
726
|
-
const runId = r.runId === undefined ? "" : `${theme.fg("dim", `#${r.runId}`)} `;
|
|
727
|
-
const line = `${theme.fg("toolTitle", theme.bold("subagent "))}${icon} ${runId}${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${r.thinking ? ` · thinking ${r.thinking}` : ""}${isolation}${pending ? " · background" : ""}${usage ? ` · ${usage}` : ""}`)}`;
|
|
728
|
-
return new Text(line, 0, 0);
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
// Parallel mode: header + one compact line per agent
|
|
732
|
-
const lines: string[] = [
|
|
733
|
-
`${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", `parallel (${details.results.length})`)}`,
|
|
734
|
-
];
|
|
735
|
-
for (const r of details.results) {
|
|
736
|
-
const pending = r.exitCode === -1;
|
|
737
|
-
const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
|
|
738
|
-
const usage = formatUsage(r.usage);
|
|
739
|
-
const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (main after ${r.modelFallbackFrom} failed)` : ""}`;
|
|
740
|
-
const isolation = r.isolation === "worktree" ? ` · worktree ${r.integrationStatus ?? "active"}` : "";
|
|
741
|
-
const runId = r.runId === undefined ? "" : `${theme.fg("dim", `#${r.runId}`)} `;
|
|
742
|
-
lines.push(` ${icon} ${runId}${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${r.thinking ? ` · thinking ${r.thinking}` : ""}${isolation}${pending ? " · background" : ""}${usage ? ` · ${usage}` : ""}`)}`);
|
|
743
|
-
}
|
|
744
|
-
return new Text(lines.join("\n"), 0, 0);
|
|
745
|
-
},
|
|
746
|
-
});
|
|
747
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The `subagent` tool: dispatches explorer/worker/cleaner/documenter/reviewer agents as isolated pi
|
|
3
|
+
* child processes, single or parallel. Owns the public dispatch contract,
|
|
4
|
+
* per-run status tracking, managed worker/cleaner → reviewer workflows with a
|
|
5
|
+
* conditional documenter, bounded worker/reviewer fix rounds, and internal
|
|
6
|
+
* step launching. Stable
|
|
7
|
+
* thread generations, final integration, and completion ownership live in
|
|
8
|
+
* thread-lifecycle.ts.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
12
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
14
|
+
import { resolve } from "node:path";
|
|
15
|
+
import { Type } from "typebox";
|
|
16
|
+
import { discoverAgents, resolveAgentTools, type AgentConfig } from "./agents.ts";
|
|
17
|
+
import { MAX_CONCURRENT_SUBAGENTS } from "./background.ts";
|
|
18
|
+
import { getStateRoot } from "./durable.ts";
|
|
19
|
+
import { loadConfig } from "./config.ts";
|
|
20
|
+
import { formatUsage, queuedResult } from "./format.ts";
|
|
21
|
+
import {
|
|
22
|
+
buildFinalDocumenterBrief,
|
|
23
|
+
buildFinalReviewBrief,
|
|
24
|
+
buildFixTaskBrief,
|
|
25
|
+
buildReReviewBrief,
|
|
26
|
+
documentationDisposition,
|
|
27
|
+
MAX_FIX_ROUNDS,
|
|
28
|
+
type ChainStep,
|
|
29
|
+
type ManagedWorkflowOutcome,
|
|
30
|
+
} from "./fixloop.ts";
|
|
31
|
+
import {
|
|
32
|
+
formatTaskSummary,
|
|
33
|
+
formatToolActivity,
|
|
34
|
+
monitor,
|
|
35
|
+
statusIcon,
|
|
36
|
+
type RunChainMeta,
|
|
37
|
+
type WorkflowStage,
|
|
38
|
+
type WorkflowStageStatus,
|
|
39
|
+
} from "./monitor.ts";
|
|
40
|
+
import type { SubagentRuntime } from "./runtime.ts";
|
|
41
|
+
import { persistThreadCheckpoint } from "./thread-lifecycle.ts";
|
|
42
|
+
import {
|
|
43
|
+
getResultOutput,
|
|
44
|
+
isFailedResult,
|
|
45
|
+
reviewVerdict,
|
|
46
|
+
runSingleAgentWithMainFallback,
|
|
47
|
+
type SingleResult,
|
|
48
|
+
type SubagentDetails,
|
|
49
|
+
type SubagentLiveEvent,
|
|
50
|
+
} from "./spawn.ts";
|
|
51
|
+
import {
|
|
52
|
+
createBackgroundDispatcher,
|
|
53
|
+
resolveDispatchModelRoute,
|
|
54
|
+
runInManagedRepositoryLane,
|
|
55
|
+
withWorktreeSystemPrompt,
|
|
56
|
+
type DispatchEnvironment,
|
|
57
|
+
type ManagedWorkflowRequest,
|
|
58
|
+
} from "./thread-lifecycle.ts";
|
|
59
|
+
import type { IsolationMode } from "./worktree.ts";
|
|
60
|
+
|
|
61
|
+
export { isWorktreeCapableAgent, runInManagedRepositoryLane } from "./thread-lifecycle.ts";
|
|
62
|
+
|
|
63
|
+
const NON_BLANK_TASK_OPTIONS = { minLength: 1, pattern: "\\S" } as const;
|
|
64
|
+
|
|
65
|
+
const ISOLATION_DESCRIPTION =
|
|
66
|
+
"Filesystem isolation: shared uses the caller's working tree; worktree creates a detached temporary Git worktree (write-capable agents, including worker, cleaner, and documenter, only)";
|
|
67
|
+
|
|
68
|
+
const IsolationSchema = Type.Optional(
|
|
69
|
+
StringEnum(["shared", "worktree"] as const, { description: ISOLATION_DESCRIPTION }),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const ADVISORY_DESCRIPTION =
|
|
73
|
+
"Report-only reviewer dispatch: findings return to you for the decision; a verdict (if emitted anyway) never starts the auto-fix chain. Use when re-verifying work you already fixed yourself.";
|
|
74
|
+
|
|
75
|
+
const AdvisorySchema = Type.Optional(Type.Boolean({ description: ADVISORY_DESCRIPTION }));
|
|
76
|
+
|
|
77
|
+
const TaskItem = Type.Object({
|
|
78
|
+
agent: Type.String({ description: "Name of the agent to invoke" }),
|
|
79
|
+
task: Type.String({
|
|
80
|
+
...NON_BLANK_TASK_OPTIONS,
|
|
81
|
+
description: "Self-contained task to delegate (the agent has no memory of this conversation)",
|
|
82
|
+
}),
|
|
83
|
+
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
|
|
84
|
+
isolation: IsolationSchema,
|
|
85
|
+
advisory: AdvisorySchema,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const SubagentParams = Type.Object({
|
|
89
|
+
agent: Type.Optional(Type.String({ description: "Name of the agent to invoke (single mode)" })),
|
|
90
|
+
task: Type.Optional(
|
|
91
|
+
Type.String({ ...NON_BLANK_TASK_OPTIONS, description: "Self-contained task to delegate (single mode)" }),
|
|
92
|
+
),
|
|
93
|
+
tasks: Type.Optional(Type.Array(TaskItem, { description: "Array of {agent, task} for parallel execution" })),
|
|
94
|
+
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process (single mode)" })),
|
|
95
|
+
isolation: IsolationSchema,
|
|
96
|
+
advisory: AdvisorySchema,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
export function defaultIsolationMode(mode: "single" | "parallel", agentName: string, requested?: IsolationMode): IsolationMode {
|
|
100
|
+
if (requested) return requested;
|
|
101
|
+
return mode === "parallel" && agentName === "worker" ? "worktree" : "shared";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function workflowStageStatus(result: SingleResult): WorkflowStageStatus {
|
|
105
|
+
if (isFailedResult(result)) return "failed";
|
|
106
|
+
if (result.agent !== "reviewer") return "done";
|
|
107
|
+
const verdict = reviewVerdict(getResultOutput(result));
|
|
108
|
+
if (verdict === "fail") return "changes";
|
|
109
|
+
return verdict === "pass" ? "done" : "failed";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime): void {
|
|
113
|
+
// Latest dispatch environment. The dispatcher is created once per process so
|
|
114
|
+
// restored threads can resume before any dispatch has run; each execute
|
|
115
|
+
// refreshes the fallback context, config, and agent catalog it resolves.
|
|
116
|
+
const environmentRef: { current: DispatchEnvironment | undefined } = { current: undefined };
|
|
117
|
+
|
|
118
|
+
// Finished runs leave the active monitor immediately. Their final findings
|
|
119
|
+
// are sent as a custom message that starts a follow-up turn.
|
|
120
|
+
const finishRun = (
|
|
121
|
+
runId: number,
|
|
122
|
+
status: "done" | "failed",
|
|
123
|
+
opts?: { silent?: boolean },
|
|
124
|
+
): void => {
|
|
125
|
+
monitor.setStatus(runId, status); // stamps endedAt for the elapsed time
|
|
126
|
+
const run = monitor.removeRun(runId);
|
|
127
|
+
if (!run) return; // already finished — stay idempotent
|
|
128
|
+
if (opts?.silent || !runtime.sessionActive) return;
|
|
129
|
+
const icon = status === "done" ? "✓" : "✗";
|
|
130
|
+
environmentRef.current?.ctx.ui.notify(`${icon} #${run.id} ${monitor.summarize(run)}`, status === "done" ? "info" : "error");
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Live sub-agent activity → concise one-line status ("thinking",
|
|
134
|
+
// "read src/index.ts", ...), never a raw args blob. The live handler
|
|
135
|
+
// only updates monitor state; the queue task / launchInWorkflow owns
|
|
136
|
+
// terminal removal, notification, and downstream workflow decisions.
|
|
137
|
+
const makeLiveHandler =
|
|
138
|
+
(runId: number, generation?: number) =>
|
|
139
|
+
(e: SubagentLiveEvent): void => {
|
|
140
|
+
if (generation !== undefined && runtime.threads.get(runId)?.generation !== generation) return;
|
|
141
|
+
switch (e.kind) {
|
|
142
|
+
case "status":
|
|
143
|
+
monitor.setStatus(runId, e.status);
|
|
144
|
+
// A fresh running segment refreshes the durable checkpoint (session
|
|
145
|
+
// path plus child pids) so a crash mid-generation still restores.
|
|
146
|
+
if (e.status === "running") {
|
|
147
|
+
const thread = runtime.threads.get(runId);
|
|
148
|
+
if (thread?.sessionId && thread.sessionDir) {
|
|
149
|
+
persistThreadCheckpoint(runtime, thread, "parked");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
break;
|
|
153
|
+
case "model":
|
|
154
|
+
monitor.setModel(runId, e.model, e.fallbackFrom);
|
|
155
|
+
monitor.setThinking(runId, e.thinking);
|
|
156
|
+
break;
|
|
157
|
+
case "usage":
|
|
158
|
+
monitor.setUsage(runId, e.usage, e.model);
|
|
159
|
+
break;
|
|
160
|
+
case "session": {
|
|
161
|
+
runtime.retainSession({ sessionDir: e.sessionDir });
|
|
162
|
+
const thread = runtime.threads.get(runId);
|
|
163
|
+
if (thread && (generation === undefined || runtime.threads.get(runId)?.generation === generation)) {
|
|
164
|
+
thread.sessionId = e.sessionId;
|
|
165
|
+
thread.sessionDir = e.sessionDir;
|
|
166
|
+
persistThreadCheckpoint(runtime, thread, "parked");
|
|
167
|
+
}
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
case "tool_start":
|
|
171
|
+
monitor.recordToolStart(runId, e.toolName, formatToolActivity(e.toolName, e.args));
|
|
172
|
+
break;
|
|
173
|
+
case "tool_end":
|
|
174
|
+
monitor.recordToolEnd(runId, e.toolName, e.isError);
|
|
175
|
+
break;
|
|
176
|
+
case "thinking":
|
|
177
|
+
monitor.setActivity(runId, "thinking");
|
|
178
|
+
break;
|
|
179
|
+
case "text":
|
|
180
|
+
// A text delta is model output, not a filesystem write.
|
|
181
|
+
monitor.setActivity(runId, "responding");
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const makeDetails =
|
|
187
|
+
(mode: "single" | "parallel", background = false) =>
|
|
188
|
+
(results: SingleResult[]): SubagentDetails => ({ mode, results, background });
|
|
189
|
+
|
|
190
|
+
/** Launch one workflow-internal child in a fresh model context. It sees the
|
|
191
|
+
* parent's exact repository/worktree state and is registered by its own id,
|
|
192
|
+
* but never enters top-level lifecycle policy or completion delivery. */
|
|
193
|
+
const launchInWorkflow = async (
|
|
194
|
+
request: ManagedWorkflowRequest,
|
|
195
|
+
agentName: string,
|
|
196
|
+
task: string,
|
|
197
|
+
meta: RunChainMeta,
|
|
198
|
+
): Promise<{ runId: number; result: SingleResult }> => {
|
|
199
|
+
const discoveredAgent = request.agents.find((candidate) => candidate.name === agentName);
|
|
200
|
+
if (!discoveredAgent) {
|
|
201
|
+
throw new Error(`Managed workflow requires enabled agent "${agentName}", but discovery did not provide it.`);
|
|
202
|
+
}
|
|
203
|
+
const resolveLiveAgentTools = (candidate: AgentConfig): AgentConfig =>
|
|
204
|
+
resolveAgentTools({ ...candidate, tools: discoveredAgent.tools }, runtime.getActiveTools());
|
|
205
|
+
const agent = resolveLiveAgentTools(discoveredAgent);
|
|
206
|
+
// Workflow policy (fix-round caps, agents) stays fixed for the chain,
|
|
207
|
+
// but model/thinking routes are re-read per stage so config edits
|
|
208
|
+
// apply to stages that have not launched yet.
|
|
209
|
+
const stageConfig = await loadConfig(runtime.configPath).catch(() => request.config);
|
|
210
|
+
const resolvedRoute = resolveDispatchModelRoute(agent, stageConfig, request.ctx);
|
|
211
|
+
const route = request.isolation === "worktree"
|
|
212
|
+
? { ...resolvedRoute, agent: withWorktreeSystemPrompt(resolvedRoute.agent) }
|
|
213
|
+
: resolvedRoute;
|
|
214
|
+
const thinkingLevel = route.thinkingLevel;
|
|
215
|
+
const runId = monitor.addRun(agent.name, task, route.agent.model, thinkingLevel, {
|
|
216
|
+
...meta,
|
|
217
|
+
isolation: request.isolation,
|
|
218
|
+
...(request.worktreeId ? { worktreeId: request.worktreeId } : {}),
|
|
219
|
+
});
|
|
220
|
+
const onLive = makeLiveHandler(runId);
|
|
221
|
+
try {
|
|
222
|
+
const result = await runSingleAgentWithMainFallback(
|
|
223
|
+
{
|
|
224
|
+
defaultCwd: request.executionCwd,
|
|
225
|
+
cwd: request.executionCwd,
|
|
226
|
+
agent: route.agent,
|
|
227
|
+
resolveAgentForAttempt: resolveLiveAgentTools,
|
|
228
|
+
agentName,
|
|
229
|
+
task,
|
|
230
|
+
thinkingLevel,
|
|
231
|
+
thinkingLevelForModel: route.thinkingLevelForModel,
|
|
232
|
+
signal: request.signal,
|
|
233
|
+
onLive,
|
|
234
|
+
makeDetails: makeDetails("single", true),
|
|
235
|
+
idleTimeoutMs: stageConfig.idleTimeoutSec * 1000,
|
|
236
|
+
sessionRoot: getStateRoot(runtime.configPath),
|
|
237
|
+
},
|
|
238
|
+
route.mainFallbackRef,
|
|
239
|
+
);
|
|
240
|
+
result.runId = runId;
|
|
241
|
+
result.projectCwd = request.projectCwd;
|
|
242
|
+
result.isolation = request.isolation;
|
|
243
|
+
runtime.retainSession(result);
|
|
244
|
+
monitor.setModel(runId, result.model, result.modelFallbackFrom);
|
|
245
|
+
monitor.setThinking(runId, result.thinking);
|
|
246
|
+
finishRun(runId, isFailedResult(result) ? "failed" : "done", { silent: true });
|
|
247
|
+
runtime.registerRunResult(runId, result);
|
|
248
|
+
return { runId, result };
|
|
249
|
+
} catch (error) {
|
|
250
|
+
finishRun(runId, "failed", { silent: true });
|
|
251
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
252
|
+
const crashed: SingleResult = {
|
|
253
|
+
...queuedResult(route.agent, task, thinkingLevel),
|
|
254
|
+
runId,
|
|
255
|
+
projectCwd: request.projectCwd,
|
|
256
|
+
isolation: request.isolation,
|
|
257
|
+
exitCode: 1,
|
|
258
|
+
stderr: errorMessage,
|
|
259
|
+
stopReason: request.signal.aborted ? "aborted" : "error",
|
|
260
|
+
errorMessage,
|
|
261
|
+
dispatchFailed: true,
|
|
262
|
+
};
|
|
263
|
+
runtime.registerRunResult(runId, crashed);
|
|
264
|
+
return { runId, result: crashed };
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/** Drop any in-flight internal row. Normal internal settlement already
|
|
269
|
+
* removes rows; this is a cancellation/crash guard. */
|
|
270
|
+
const removeWorkflowGroup = (groupId: string): void => {
|
|
271
|
+
for (const run of [...monitor.getRuns()]) {
|
|
272
|
+
if (run.groupId === groupId) monitor.removeRun(run.id);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
/** Run every downstream role inline under the parent generation's queue
|
|
277
|
+
* controller. That gives park/stop/shutdown one lifecycle owner and keeps
|
|
278
|
+
* isolated worktrees unintegrated until the final reviewer settles. */
|
|
279
|
+
const runManagedWorkflow = async (
|
|
280
|
+
request: ManagedWorkflowRequest,
|
|
281
|
+
): Promise<ManagedWorkflowOutcome> => {
|
|
282
|
+
const initialStepRunId = monitor.reserveRunId();
|
|
283
|
+
const initialStepResult: SingleResult = {
|
|
284
|
+
...request.initialResult,
|
|
285
|
+
runId: initialStepRunId,
|
|
286
|
+
};
|
|
287
|
+
runtime.registerRunResult(initialStepRunId, initialStepResult);
|
|
288
|
+
const steps: ChainStep[] = [{
|
|
289
|
+
runId: initialStepRunId,
|
|
290
|
+
result: initialStepResult,
|
|
291
|
+
relation: request.plan.initialRelation,
|
|
292
|
+
}];
|
|
293
|
+
const enabled = (name: string): boolean =>
|
|
294
|
+
request.agents.some((candidate) => candidate.name === name);
|
|
295
|
+
const canContinue = (): boolean => runtime.sessionActive && !request.signal.aborted;
|
|
296
|
+
|
|
297
|
+
// Keep a live parent-owned projection because settled internal rows are
|
|
298
|
+
// intentionally removed. Only real/currently planned stages enter it.
|
|
299
|
+
const initialStageRelation = initialStepResult.agent === "worker"
|
|
300
|
+
? "implement"
|
|
301
|
+
: initialStepResult.agent === "cleaner"
|
|
302
|
+
? "cleanup"
|
|
303
|
+
: "review";
|
|
304
|
+
const workflowStages: WorkflowStage[] = [{
|
|
305
|
+
agent: initialStepResult.agent,
|
|
306
|
+
relation: initialStageRelation,
|
|
307
|
+
status: workflowStageStatus(initialStepResult),
|
|
308
|
+
}];
|
|
309
|
+
let reviewStage: WorkflowStage | undefined;
|
|
310
|
+
if (request.plan.kind === "post-writer" && enabled("reviewer")) {
|
|
311
|
+
reviewStage = { agent: "reviewer", relation: "review", status: "pending" };
|
|
312
|
+
workflowStages.push(reviewStage);
|
|
313
|
+
}
|
|
314
|
+
let documentationStage: WorkflowStage | undefined;
|
|
315
|
+
if (enabled("documenter")) {
|
|
316
|
+
documentationStage = { agent: "documenter", relation: "docs", status: "pending" };
|
|
317
|
+
workflowStages.push(documentationStage);
|
|
318
|
+
}
|
|
319
|
+
const publishWorkflowStages = (): void => {
|
|
320
|
+
monitor.setWorkflowStages(request.parentRunId, workflowStages);
|
|
321
|
+
};
|
|
322
|
+
const insertBeforeDocumentation = (stage: WorkflowStage): void => {
|
|
323
|
+
const documentationIndex = documentationStage
|
|
324
|
+
? workflowStages.indexOf(documentationStage)
|
|
325
|
+
: -1;
|
|
326
|
+
if (documentationIndex === -1) workflowStages.push(stage);
|
|
327
|
+
else workflowStages.splice(documentationIndex, 0, stage);
|
|
328
|
+
};
|
|
329
|
+
const removeDocumentationStage = (): void => {
|
|
330
|
+
if (!documentationStage) return;
|
|
331
|
+
const index = workflowStages.indexOf(documentationStage);
|
|
332
|
+
if (index !== -1) workflowStages.splice(index, 1);
|
|
333
|
+
documentationStage = undefined;
|
|
334
|
+
publishWorkflowStages();
|
|
335
|
+
};
|
|
336
|
+
publishWorkflowStages();
|
|
337
|
+
|
|
338
|
+
const launchStep = async (
|
|
339
|
+
agentName: string,
|
|
340
|
+
task: string,
|
|
341
|
+
relation: string,
|
|
342
|
+
projection: {
|
|
343
|
+
stage?: WorkflowStage;
|
|
344
|
+
timelineRelation?: string;
|
|
345
|
+
childRelation?: string;
|
|
346
|
+
} = {},
|
|
347
|
+
): Promise<SingleResult> => {
|
|
348
|
+
if (!enabled(agentName)) {
|
|
349
|
+
throw new Error(`Managed workflow cannot launch disabled or missing agent "${agentName}".`);
|
|
350
|
+
}
|
|
351
|
+
const stage: WorkflowStage = projection.stage ?? {
|
|
352
|
+
agent: agentName,
|
|
353
|
+
relation: projection.timelineRelation ?? relation,
|
|
354
|
+
status: "pending",
|
|
355
|
+
};
|
|
356
|
+
if (!projection.stage) insertBeforeDocumentation(stage);
|
|
357
|
+
stage.status = "active";
|
|
358
|
+
publishWorkflowStages();
|
|
359
|
+
try {
|
|
360
|
+
const step = await launchInWorkflow(request, agentName, task, {
|
|
361
|
+
groupId: request.groupId,
|
|
362
|
+
relationLabel: projection.childRelation ?? relation,
|
|
363
|
+
parentRunId: request.parentRunId,
|
|
364
|
+
});
|
|
365
|
+
stage.status = workflowStageStatus(step.result);
|
|
366
|
+
publishWorkflowStages();
|
|
367
|
+
request.rememberLatest(step.result);
|
|
368
|
+
steps.push({ ...step, relation });
|
|
369
|
+
return step.result;
|
|
370
|
+
} catch (error) {
|
|
371
|
+
stage.status = "failed";
|
|
372
|
+
publishWorkflowStages();
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
/** Bounded worker → reviewer fix rounds. A conditional documentation sync
|
|
378
|
+
* deliberately stays out of the rounds: code fixes would invalidate it,
|
|
379
|
+
* and the terminal review classifies whether the settled diff needs one. */
|
|
380
|
+
const runFixRounds = async (
|
|
381
|
+
triggeringReviewer: SingleResult,
|
|
382
|
+
): Promise<{ lastReview?: SingleResult; lastWorker?: SingleResult }> => {
|
|
383
|
+
let lastReviewer = triggeringReviewer;
|
|
384
|
+
const outcome: { lastReview?: SingleResult; lastWorker?: SingleResult } = {};
|
|
385
|
+
for (let round = 1; round <= MAX_FIX_ROUNDS; round++) {
|
|
386
|
+
if (!canContinue()) break;
|
|
387
|
+
const fixRelation = `fix ${round}/${MAX_FIX_ROUNDS}`;
|
|
388
|
+
const workerResult = await launchStep(
|
|
389
|
+
"worker",
|
|
390
|
+
buildFixTaskBrief(lastReviewer, round, MAX_FIX_ROUNDS),
|
|
391
|
+
`fix round ${round}`,
|
|
392
|
+
{ timelineRelation: fixRelation, childRelation: fixRelation },
|
|
393
|
+
);
|
|
394
|
+
if (isFailedResult(workerResult) || !canContinue()) break;
|
|
395
|
+
outcome.lastWorker = workerResult;
|
|
396
|
+
|
|
397
|
+
const reReviewRelation = `re-review ${round}/${MAX_FIX_ROUNDS}`;
|
|
398
|
+
const reviewResult = await launchStep(
|
|
399
|
+
"reviewer",
|
|
400
|
+
buildReReviewBrief(lastReviewer, round, workerResult, {
|
|
401
|
+
documenterPending: enabled("documenter"),
|
|
402
|
+
}),
|
|
403
|
+
`re-review round ${round}`,
|
|
404
|
+
{ timelineRelation: reReviewRelation, childRelation: reReviewRelation },
|
|
405
|
+
);
|
|
406
|
+
if (isFailedResult(reviewResult) || !canContinue()) break;
|
|
407
|
+
outcome.lastReview = reviewResult;
|
|
408
|
+
const verdict = reviewVerdict(getResultOutput(reviewResult));
|
|
409
|
+
// REVIEW_PASS settles. No verdict is advisory/malformed and must never
|
|
410
|
+
// trigger another writer. Only an explicit REVIEW_FAIL consumes a fix.
|
|
411
|
+
if (verdict !== "fail") break;
|
|
412
|
+
lastReviewer = reviewResult;
|
|
413
|
+
}
|
|
414
|
+
return outcome;
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
/** Run the low-cost final documentation sync only when the terminal REVIEW_PASS
|
|
418
|
+
* reports drift or omits the new marker. A failed process, missing verdict, or
|
|
419
|
+
* REVIEW_FAIL never writes docs. With no reviewer, retain the conservative
|
|
420
|
+
* writer → documenter fallback. */
|
|
421
|
+
const runFinalDocumentation = async (
|
|
422
|
+
lastWriterResult: SingleResult | undefined,
|
|
423
|
+
finalReviewResult: SingleResult | undefined,
|
|
424
|
+
): Promise<void> => {
|
|
425
|
+
if (!canContinue() || !enabled("documenter")) return;
|
|
426
|
+
if (lastWriterResult?.agent === "documenter") {
|
|
427
|
+
removeDocumentationStage();
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (finalReviewResult) {
|
|
431
|
+
if (isFailedResult(finalReviewResult)) {
|
|
432
|
+
removeDocumentationStage();
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const reviewOutput = getResultOutput(finalReviewResult);
|
|
436
|
+
if (
|
|
437
|
+
reviewVerdict(reviewOutput) !== "pass" ||
|
|
438
|
+
documentationDisposition(reviewOutput) === "clean"
|
|
439
|
+
) {
|
|
440
|
+
removeDocumentationStage();
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
documentationStage ??= { agent: "documenter", relation: "docs", status: "pending" };
|
|
445
|
+
if (!workflowStages.includes(documentationStage)) workflowStages.push(documentationStage);
|
|
446
|
+
await launchStep(
|
|
447
|
+
"documenter",
|
|
448
|
+
buildFinalDocumenterBrief(lastWriterResult, finalReviewResult),
|
|
449
|
+
"final documentation sync",
|
|
450
|
+
{ stage: documentationStage },
|
|
451
|
+
);
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
// Park/stop/shutdown may win after the top-level child settles but
|
|
456
|
+
// before this continuation starts. Preserve that stable checkpoint and
|
|
457
|
+
// never create an already-aborted downstream child.
|
|
458
|
+
if (!canContinue()) return { kind: request.plan.kind, steps };
|
|
459
|
+
if (request.plan.kind === "auto-fix") {
|
|
460
|
+
const fixOutcome = await runFixRounds(initialStepResult);
|
|
461
|
+
await runFinalDocumentation(fixOutcome.lastWorker, fixOutcome.lastReview ?? initialStepResult);
|
|
462
|
+
} else if (request.plan.kind === "review-pass-sync") {
|
|
463
|
+
// The direct passing review already gated the pending code. Its
|
|
464
|
+
// disposition requested (or conservatively defaulted to) one docs sync.
|
|
465
|
+
await runFinalDocumentation(undefined, initialStepResult);
|
|
466
|
+
} else if (enabled("reviewer")) {
|
|
467
|
+
const gateReview = await launchStep(
|
|
468
|
+
"reviewer",
|
|
469
|
+
buildFinalReviewBrief(initialStepResult, { documenterPending: enabled("documenter") }),
|
|
470
|
+
"final review",
|
|
471
|
+
{ stage: reviewStage },
|
|
472
|
+
);
|
|
473
|
+
let fixOutcome: Awaited<ReturnType<typeof runFixRounds>> = {};
|
|
474
|
+
if (
|
|
475
|
+
!isFailedResult(gateReview) &&
|
|
476
|
+
canContinue() &&
|
|
477
|
+
reviewVerdict(getResultOutput(gateReview)) === "fail" &&
|
|
478
|
+
enabled("worker")
|
|
479
|
+
) {
|
|
480
|
+
fixOutcome = await runFixRounds(gateReview);
|
|
481
|
+
}
|
|
482
|
+
await runFinalDocumentation(
|
|
483
|
+
fixOutcome.lastWorker ?? initialStepResult,
|
|
484
|
+
fixOutcome.lastReview ?? gateReview,
|
|
485
|
+
);
|
|
486
|
+
} else {
|
|
487
|
+
// No gate configured: the documenter is the only downstream stage.
|
|
488
|
+
await runFinalDocumentation(initialStepResult, undefined);
|
|
489
|
+
}
|
|
490
|
+
return { kind: request.plan.kind, steps };
|
|
491
|
+
} finally {
|
|
492
|
+
removeWorkflowGroup(request.groupId);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const startBackground = createBackgroundDispatcher({
|
|
497
|
+
runtime,
|
|
498
|
+
getEnvironment: () => {
|
|
499
|
+
if (!environmentRef.current) {
|
|
500
|
+
throw new Error("pi-subagents dispatch environment is not ready yet.");
|
|
501
|
+
}
|
|
502
|
+
return environmentRef.current;
|
|
503
|
+
},
|
|
504
|
+
finishRun,
|
|
505
|
+
makeLiveHandler,
|
|
506
|
+
makeDetails,
|
|
507
|
+
runManagedWorkflow,
|
|
508
|
+
});
|
|
509
|
+
runtime.dispatcher = startBackground;
|
|
510
|
+
|
|
511
|
+
pi.registerTool({
|
|
512
|
+
name: "subagent",
|
|
513
|
+
label: "Subagent",
|
|
514
|
+
description: [
|
|
515
|
+
"Dispatch enabled specialized agents as isolated leaf Pi child processes, singly or in parallel; keep small known-target work in the main thread with direct tools.",
|
|
516
|
+
"Built-ins: explorer for broad read-only reconnaissance (a retrieval index, never a gate); worker for implementation; cleaner as a separate explicitly authorized cleanup/removal/simplification/deduplication entry; documenter for explicit docs/comments work or conditional final diff sync; reviewer for generic read-only assessments and independent code gates.",
|
|
517
|
+
"Work starts in the background. Successful worker/cleaner runs keep one enabled reviewer gate and bounded fix loop; documenter runs afterward only when REVIEW_PASS reports DOCUMENTATION: NEEDED or omits the marker, with a reviewer-disabled fallback. A top-level documenter delivers directly. Results resume the main agent and are already shown, so do not poll, duplicate downstream roles, or restate them.",
|
|
518
|
+
"Single tasks default to shared; parallel workers default to detached Git worktrees. Only write-capable agents can use worktree isolation, and failures never fall back silently to shared.",
|
|
519
|
+
"A selected-model or provider failure continues the retained session on the current main model; ordinary tool/task failures do not.",
|
|
520
|
+
"Use subagent_control to resume a parked or settled thread's retained context by stable run id; use subagent_stop for destructive cancellation.",
|
|
521
|
+
].join(" "),
|
|
522
|
+
promptSnippet:
|
|
523
|
+
"Dispatch isolated background agents for broad recon, self-contained implementation, authorized cleanup, explicit docs, or independent review; keep small known-target work on direct tools. Worker/cleaner gates and only needed/conservative docs sync run automatically, results resume automatically, and each workflow delivers once.",
|
|
524
|
+
parameters: SubagentParams,
|
|
525
|
+
|
|
526
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
527
|
+
monitor.beginTurn();
|
|
528
|
+
const config = await loadConfig(runtime.configPath);
|
|
529
|
+
|
|
530
|
+
const discovery = discoverAgents(ctx.cwd, {
|
|
531
|
+
scope: config.agentScope,
|
|
532
|
+
enabledNames: config.enabledAgents,
|
|
533
|
+
projectTrusted: ctx.isProjectTrusted?.() === true,
|
|
534
|
+
});
|
|
535
|
+
const agents = discovery.agents;
|
|
536
|
+
// Refresh the dispatcher's fallback environment so control operations
|
|
537
|
+
// (resume of restored threads) never run on a stale context.
|
|
538
|
+
environmentRef.current = { ctx, config, agents };
|
|
539
|
+
|
|
540
|
+
const hasTasks = (params.tasks?.length ?? 0) > 0;
|
|
541
|
+
const hasSingle = Boolean(params.agent) && params.task !== undefined;
|
|
542
|
+
|
|
543
|
+
const catalog = agents.map((a) => a.name).join(", ") || "none";
|
|
544
|
+
|
|
545
|
+
if (Number(hasTasks) + Number(hasSingle) !== 1) {
|
|
546
|
+
return {
|
|
547
|
+
content: [
|
|
548
|
+
{
|
|
549
|
+
type: "text",
|
|
550
|
+
text: `Invalid parameters. Provide exactly one mode: single {agent, task} or parallel {tasks: [...]}. Enabled agents: ${catalog}.`,
|
|
551
|
+
},
|
|
552
|
+
],
|
|
553
|
+
details: makeDetails("single")([]),
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (hasTasks) {
|
|
558
|
+
const blankTaskIndex = params.tasks?.findIndex(({ task }) => task.trim().length === 0) ?? -1;
|
|
559
|
+
if (blankTaskIndex !== -1) {
|
|
560
|
+
return {
|
|
561
|
+
content: [
|
|
562
|
+
{
|
|
563
|
+
type: "text",
|
|
564
|
+
text: `Invalid parameters. tasks[${blankTaskIndex}].task must contain at least one non-whitespace character. No background tasks were started. Enabled agents: ${catalog}.`,
|
|
565
|
+
},
|
|
566
|
+
],
|
|
567
|
+
details: makeDetails("parallel")([]),
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
} else if (params.task?.trim().length === 0) {
|
|
571
|
+
return {
|
|
572
|
+
content: [
|
|
573
|
+
{
|
|
574
|
+
type: "text",
|
|
575
|
+
text: `Invalid parameters. task must contain at least one non-whitespace character. Enabled agents: ${catalog}.`,
|
|
576
|
+
},
|
|
577
|
+
],
|
|
578
|
+
details: makeDetails("single")([]),
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Sub-agents intentionally detach from the foreground turn. This makes the
|
|
583
|
+
// editor available immediately; completion messages later wake the main agent.
|
|
584
|
+
if (params.tasks && params.tasks.length > 0) {
|
|
585
|
+
if (params.tasks.length > MAX_CONCURRENT_SUBAGENTS) {
|
|
586
|
+
return {
|
|
587
|
+
content: [
|
|
588
|
+
{
|
|
589
|
+
type: "text",
|
|
590
|
+
text: `Too many parallel tasks (${params.tasks.length}). Max is ${MAX_CONCURRENT_SUBAGENTS}.`,
|
|
591
|
+
},
|
|
592
|
+
],
|
|
593
|
+
details: makeDetails("parallel", true)([]),
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const results: SingleResult[] = [];
|
|
598
|
+
// Preserve caller order (and deterministic completion batching) while
|
|
599
|
+
// preparing each isolated filesystem before its queue entry can start.
|
|
600
|
+
for (const item of params.tasks) {
|
|
601
|
+
results.push(await startBackground(
|
|
602
|
+
item.agent,
|
|
603
|
+
item.task,
|
|
604
|
+
item.cwd,
|
|
605
|
+
defaultIsolationMode("parallel", item.agent, item.isolation as IsolationMode | undefined),
|
|
606
|
+
undefined,
|
|
607
|
+
false,
|
|
608
|
+
undefined,
|
|
609
|
+
undefined,
|
|
610
|
+
undefined,
|
|
611
|
+
{ advisoryReview: item.advisory === true },
|
|
612
|
+
));
|
|
613
|
+
}
|
|
614
|
+
const startedRuns = results.filter((result) => result.exitCode === -1);
|
|
615
|
+
const started = startedRuns.length;
|
|
616
|
+
const startedRefs = startedRuns.map((result) =>
|
|
617
|
+
result.runId === undefined ? result.agent : `#${result.runId} ${result.agent}`,
|
|
618
|
+
);
|
|
619
|
+
const failureLines = results.flatMap((result, index) => {
|
|
620
|
+
if (result.exitCode === -1) return [];
|
|
621
|
+
const reason = getResultOutput(result).trim() || "unknown startup failure";
|
|
622
|
+
return [
|
|
623
|
+
`- tasks[${index}] (${params.tasks![index]!.agent}) failed to start: ${reason.replace(/\n/g, "\n ")}`,
|
|
624
|
+
];
|
|
625
|
+
});
|
|
626
|
+
if (started === 0) {
|
|
627
|
+
// Pi marks custom-tool failures only when execute throws; returning an
|
|
628
|
+
// `isError` property is still a successful AgentToolResult.
|
|
629
|
+
throw new Error(`No background subagents were started.\n${failureLines.join("\n")}`);
|
|
630
|
+
}
|
|
631
|
+
const text = [
|
|
632
|
+
`Started ${started} background subagent${started === 1 ? "" : "s"}: ${startedRefs.join(", ")}. Results will automatically resume the main agent when ready.`,
|
|
633
|
+
...(failureLines.length > 0
|
|
634
|
+
? [`${failureLines.length} task${failureLines.length === 1 ? "" : "s"} failed before launch:`, ...failureLines]
|
|
635
|
+
: []),
|
|
636
|
+
].join("\n");
|
|
637
|
+
return {
|
|
638
|
+
content: [{ type: "text", text }],
|
|
639
|
+
details: makeDetails("parallel", true)(results),
|
|
640
|
+
terminate: true,
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const result = await startBackground(
|
|
645
|
+
params.agent as string,
|
|
646
|
+
params.task as string,
|
|
647
|
+
params.cwd,
|
|
648
|
+
defaultIsolationMode("single", params.agent as string, params.isolation as IsolationMode | undefined),
|
|
649
|
+
undefined,
|
|
650
|
+
false,
|
|
651
|
+
undefined,
|
|
652
|
+
undefined,
|
|
653
|
+
undefined,
|
|
654
|
+
{ advisoryReview: params.advisory === true },
|
|
655
|
+
);
|
|
656
|
+
if (result.exitCode !== -1) {
|
|
657
|
+
throw new Error(getResultOutput(result));
|
|
658
|
+
}
|
|
659
|
+
const runRef = result.runId === undefined ? result.agent : `#${result.runId} ${result.agent}`;
|
|
660
|
+
return {
|
|
661
|
+
content: [{ type: "text", text: `Started ${runRef} in the background. Its result will automatically resume the main agent when ready.` }],
|
|
662
|
+
details: makeDetails("single", true)([result]),
|
|
663
|
+
terminate: true,
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
},
|
|
667
|
+
|
|
668
|
+
renderCall(args, theme) {
|
|
669
|
+
if (args.tasks && args.tasks.length > 0) {
|
|
670
|
+
let text = `${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", `parallel (${args.tasks.length})`)}`;
|
|
671
|
+
for (const t of args.tasks.slice(0, 4)) {
|
|
672
|
+
const preview = formatTaskSummary(t.task, 48);
|
|
673
|
+
const isolation = defaultIsolationMode("parallel", t.agent, t.isolation) === "worktree" ? " [worktree]" : "";
|
|
674
|
+
text += `\n ${theme.fg("accent", t.agent)}${theme.fg("dim", isolation)} ${theme.fg("dim", preview)}`;
|
|
675
|
+
}
|
|
676
|
+
if (args.tasks.length > 4) text += `\n ${theme.fg("dim", `… +${args.tasks.length - 4} more`)}`;
|
|
677
|
+
return new Text(text, 0, 0);
|
|
678
|
+
}
|
|
679
|
+
const task: string = args.task ?? "";
|
|
680
|
+
const preview = formatTaskSummary(task, 60);
|
|
681
|
+
const isolation = args.isolation === "worktree" ? " [worktree]" : "";
|
|
682
|
+
return new Text(
|
|
683
|
+
`${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", args.agent ?? "?")}${theme.fg("dim", isolation)} ${theme.fg("dim", preview)}`,
|
|
684
|
+
0,
|
|
685
|
+
0,
|
|
686
|
+
);
|
|
687
|
+
},
|
|
688
|
+
|
|
689
|
+
renderResult(result, _options, theme) {
|
|
690
|
+
const details = result.details as SubagentDetails | undefined;
|
|
691
|
+
if (!details || details.results.length === 0) return new Text(theme.fg("dim", "(no output)"), 0, 0);
|
|
692
|
+
|
|
693
|
+
if (details.mode === "single") {
|
|
694
|
+
const r = details.results[0];
|
|
695
|
+
const pending = r.exitCode === -1;
|
|
696
|
+
const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
|
|
697
|
+
const usage = formatUsage(r.usage);
|
|
698
|
+
const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (main after ${r.modelFallbackFrom} failed)` : ""}`;
|
|
699
|
+
const isolation = r.isolation === "worktree" ? ` · worktree ${r.integrationStatus ?? "active"}` : "";
|
|
700
|
+
const runId = r.runId === undefined ? "" : `${theme.fg("dim", `#${r.runId}`)} `;
|
|
701
|
+
const line = `${theme.fg("toolTitle", theme.bold("subagent "))}${icon} ${runId}${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${r.thinking ? ` · thinking ${r.thinking}` : ""}${isolation}${pending ? " · background" : ""}${usage ? ` · ${usage}` : ""}`)}`;
|
|
702
|
+
return new Text(line, 0, 0);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Parallel mode: header + one compact line per agent
|
|
706
|
+
const lines: string[] = [
|
|
707
|
+
`${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", `parallel (${details.results.length})`)}`,
|
|
708
|
+
];
|
|
709
|
+
for (const r of details.results) {
|
|
710
|
+
const pending = r.exitCode === -1;
|
|
711
|
+
const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
|
|
712
|
+
const usage = formatUsage(r.usage);
|
|
713
|
+
const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (main after ${r.modelFallbackFrom} failed)` : ""}`;
|
|
714
|
+
const isolation = r.isolation === "worktree" ? ` · worktree ${r.integrationStatus ?? "active"}` : "";
|
|
715
|
+
const runId = r.runId === undefined ? "" : `${theme.fg("dim", `#${r.runId}`)} `;
|
|
716
|
+
lines.push(` ${icon} ${runId}${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${r.thinking ? ` · thinking ${r.thinking}` : ""}${isolation}${pending ? " · background" : ""}${usage ? ` · ${usage}` : ""}`)}`);
|
|
717
|
+
}
|
|
718
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
719
|
+
},
|
|
720
|
+
});
|
|
721
|
+
}
|