@ferris1225/pi-subagents 4.1.1 → 4.1.2
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 +506 -481
- package/agents/cleaner.md +14 -4
- package/agents/documenter.md +44 -0
- package/agents/reviewer.md +3 -2
- package/agents/worker.md +4 -1
- package/package.json +2 -2
- package/src/agents.ts +12 -0
- package/src/announcements.ts +18 -1
- package/src/config.ts +43 -13
- package/src/dispatch.ts +637 -704
- package/src/fixloop.ts +266 -52
- package/src/index.ts +3 -3
- package/src/monitor.ts +12 -3
- package/src/prompt.ts +47 -12
- package/src/rpc-run.ts +23 -7
- package/src/runtime.ts +8 -7
- package/src/setup.ts +24 -7
- package/src/spawn.ts +45 -11
- package/src/thread-lifecycle.ts +203 -49
- package/src/tools.ts +23 -9
- package/src/widget.ts +4 -4
- package/src/worktree.ts +27 -4
package/src/dispatch.ts
CHANGED
|
@@ -1,704 +1,637 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `subagent` tool: dispatches explorer/worker/cleaner/reviewer agents as isolated pi
|
|
3
|
-
* child processes, single or parallel. Owns the public dispatch contract,
|
|
4
|
-
* per-run status tracking,
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
type
|
|
28
|
-
} from "./fixloop.ts";
|
|
29
|
-
import {
|
|
30
|
-
formatTaskSummary,
|
|
31
|
-
formatToolActivity,
|
|
32
|
-
monitor,
|
|
33
|
-
statusIcon,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
} from "./
|
|
37
|
-
import
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
type
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
} from "./thread-lifecycle.ts";
|
|
52
|
-
import {
|
|
53
|
-
|
|
54
|
-
export { FORK_CONTINUATION_PROMPT, isWorktreeCapableAgent } from "./thread-lifecycle.ts";
|
|
55
|
-
|
|
56
|
-
const NON_BLANK_TASK_OPTIONS = { minLength: 1, pattern: "\\S" } as const;
|
|
57
|
-
|
|
58
|
-
const ISOLATION_DESCRIPTION =
|
|
59
|
-
"Filesystem isolation: shared uses the caller's working tree; worktree creates a detached temporary Git worktree (write-capable agents, including worker and
|
|
60
|
-
|
|
61
|
-
const IsolationSchema = Type.Optional(
|
|
62
|
-
StringEnum(["shared", "worktree"] as const, { description: ISOLATION_DESCRIPTION }),
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
const TaskItem = Type.Object({
|
|
66
|
-
agent: Type.String({ description: "Name of the agent to invoke" }),
|
|
67
|
-
task: Type.String({
|
|
68
|
-
...NON_BLANK_TASK_OPTIONS,
|
|
69
|
-
description: "Self-contained task to delegate (the agent has no memory of this conversation)",
|
|
70
|
-
}),
|
|
71
|
-
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
|
|
72
|
-
isolation: IsolationSchema,
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
const SubagentParams = Type.Object({
|
|
76
|
-
agent: Type.Optional(Type.String({ description: "Name of the agent to invoke (single mode)" })),
|
|
77
|
-
task: Type.Optional(
|
|
78
|
-
Type.String({ ...NON_BLANK_TASK_OPTIONS, description: "Self-contained task to delegate (single mode)" }),
|
|
79
|
-
),
|
|
80
|
-
tasks: Type.Optional(Type.Array(TaskItem, { description: "Array of {agent, task} for parallel execution" })),
|
|
81
|
-
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process (single mode)" })),
|
|
82
|
-
isolation: IsolationSchema,
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
export function defaultIsolationMode(mode: "single" | "parallel", agentName: string, requested?: IsolationMode): IsolationMode {
|
|
86
|
-
if (requested) return requested;
|
|
87
|
-
return mode === "parallel" && agentName === "worker" ? "worktree" : "shared";
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
async function
|
|
93
|
-
try {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return resolve(cwd);
|
|
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
|
-
const
|
|
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
|
-
const
|
|
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
|
-
if (result.exitCode !== -1) {
|
|
640
|
-
throw new Error(getResultOutput(result));
|
|
641
|
-
}
|
|
642
|
-
const runRef = result.runId === undefined ? result.agent : `#${result.runId} ${result.agent}`;
|
|
643
|
-
return {
|
|
644
|
-
content: [{ type: "text", text: `Started ${runRef} in the background. Its result will automatically resume the main agent when ready.` }],
|
|
645
|
-
details: makeDetails("single", true)([result]),
|
|
646
|
-
terminate: true,
|
|
647
|
-
};
|
|
648
|
-
|
|
649
|
-
},
|
|
650
|
-
|
|
651
|
-
renderCall(args, theme) {
|
|
652
|
-
if (args.tasks && args.tasks.length > 0) {
|
|
653
|
-
let text = `${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", `parallel (${args.tasks.length})`)}`;
|
|
654
|
-
for (const t of args.tasks.slice(0, 4)) {
|
|
655
|
-
const preview = formatTaskSummary(t.task, 48);
|
|
656
|
-
const isolation = defaultIsolationMode("parallel", t.agent, t.isolation) === "worktree" ? " [worktree]" : "";
|
|
657
|
-
text += `\n ${theme.fg("accent", t.agent)}${theme.fg("dim", isolation)} ${theme.fg("dim", preview)}`;
|
|
658
|
-
}
|
|
659
|
-
if (args.tasks.length > 4) text += `\n ${theme.fg("dim", `… +${args.tasks.length - 4} more`)}`;
|
|
660
|
-
return new Text(text, 0, 0);
|
|
661
|
-
}
|
|
662
|
-
const task: string = args.task ?? "";
|
|
663
|
-
const preview = formatTaskSummary(task, 60);
|
|
664
|
-
const isolation = args.isolation === "worktree" ? " [worktree]" : "";
|
|
665
|
-
return new Text(
|
|
666
|
-
`${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", args.agent ?? "?")}${theme.fg("dim", isolation)} ${theme.fg("dim", preview)}`,
|
|
667
|
-
0,
|
|
668
|
-
0,
|
|
669
|
-
);
|
|
670
|
-
},
|
|
671
|
-
|
|
672
|
-
renderResult(result, _options, theme) {
|
|
673
|
-
const details = result.details as SubagentDetails | undefined;
|
|
674
|
-
if (!details || details.results.length === 0) return new Text(theme.fg("dim", "(no output)"), 0, 0);
|
|
675
|
-
|
|
676
|
-
if (details.mode === "single") {
|
|
677
|
-
const r = details.results[0];
|
|
678
|
-
const pending = r.exitCode === -1;
|
|
679
|
-
const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
|
|
680
|
-
const usage = formatUsage(r.usage);
|
|
681
|
-
const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (main after ${r.modelFallbackFrom} failed)` : ""}`;
|
|
682
|
-
const isolation = r.isolation === "worktree" ? ` · worktree ${r.integrationStatus ?? "active"}` : "";
|
|
683
|
-
const runId = r.runId === undefined ? "" : `${theme.fg("dim", `#${r.runId}`)} `;
|
|
684
|
-
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}` : ""}`)}`;
|
|
685
|
-
return new Text(line, 0, 0);
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
// Parallel mode: header + one compact line per agent
|
|
689
|
-
const lines: string[] = [
|
|
690
|
-
`${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", `parallel (${details.results.length})`)}`,
|
|
691
|
-
];
|
|
692
|
-
for (const r of details.results) {
|
|
693
|
-
const pending = r.exitCode === -1;
|
|
694
|
-
const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
|
|
695
|
-
const usage = formatUsage(r.usage);
|
|
696
|
-
const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (main after ${r.modelFallbackFrom} failed)` : ""}`;
|
|
697
|
-
const isolation = r.isolation === "worktree" ? ` · worktree ${r.integrationStatus ?? "active"}` : "";
|
|
698
|
-
const runId = r.runId === undefined ? "" : `${theme.fg("dim", `#${r.runId}`)} `;
|
|
699
|
-
lines.push(` ${icon} ${runId}${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${r.thinking ? ` · thinking ${r.thinking}` : ""}${isolation}${pending ? " · background" : ""}${usage ? ` · ${usage}` : ""}`)}`);
|
|
700
|
-
}
|
|
701
|
-
return new Text(lines.join("\n"), 0, 0);
|
|
702
|
-
},
|
|
703
|
-
});
|
|
704
|
-
}
|
|
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 writer → documenter → reviewer workflows,
|
|
5
|
+
* reviewer auto-fix rounds, and internal step launching. Stable thread
|
|
6
|
+
* generations, final integration, and completion ownership live in
|
|
7
|
+
* thread-lifecycle.ts.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
11
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
13
|
+
import { realpath } from "node:fs/promises";
|
|
14
|
+
import { resolve } from "node:path";
|
|
15
|
+
import { Type } from "typebox";
|
|
16
|
+
import { discoverAgents } from "./agents.ts";
|
|
17
|
+
import { loadConfig } from "./config.ts";
|
|
18
|
+
import { formatUsage, queuedResult } from "./format.ts";
|
|
19
|
+
import {
|
|
20
|
+
buildDocumenterTaskBrief,
|
|
21
|
+
buildFinalReviewBrief,
|
|
22
|
+
buildFixTaskBrief,
|
|
23
|
+
buildPostWriterDocumenterBrief,
|
|
24
|
+
buildReReviewBrief,
|
|
25
|
+
buildReviewPassDocumenterBrief,
|
|
26
|
+
type ChainStep,
|
|
27
|
+
type ManagedWorkflowOutcome,
|
|
28
|
+
} from "./fixloop.ts";
|
|
29
|
+
import {
|
|
30
|
+
formatTaskSummary,
|
|
31
|
+
formatToolActivity,
|
|
32
|
+
monitor,
|
|
33
|
+
statusIcon,
|
|
34
|
+
type RunChainMeta,
|
|
35
|
+
} from "./monitor.ts";
|
|
36
|
+
import type { SubagentRuntime } from "./runtime.ts";
|
|
37
|
+
import {
|
|
38
|
+
getResultOutput,
|
|
39
|
+
isFailedResult,
|
|
40
|
+
reviewVerdict,
|
|
41
|
+
runSingleAgentWithMainFallback,
|
|
42
|
+
type SingleResult,
|
|
43
|
+
type SubagentDetails,
|
|
44
|
+
type SubagentLiveEvent,
|
|
45
|
+
} from "./spawn.ts";
|
|
46
|
+
import {
|
|
47
|
+
createBackgroundDispatcher,
|
|
48
|
+
resolveDispatchModelRoute,
|
|
49
|
+
withWorktreeSystemPrompt,
|
|
50
|
+
type ManagedWorkflowRequest,
|
|
51
|
+
} from "./thread-lifecycle.ts";
|
|
52
|
+
import { resolveRepositoryRoot, type IsolationMode } from "./worktree.ts";
|
|
53
|
+
|
|
54
|
+
export { FORK_CONTINUATION_PROMPT, isWorktreeCapableAgent } from "./thread-lifecycle.ts";
|
|
55
|
+
|
|
56
|
+
const NON_BLANK_TASK_OPTIONS = { minLength: 1, pattern: "\\S" } as const;
|
|
57
|
+
|
|
58
|
+
const ISOLATION_DESCRIPTION =
|
|
59
|
+
"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)";
|
|
60
|
+
|
|
61
|
+
const IsolationSchema = Type.Optional(
|
|
62
|
+
StringEnum(["shared", "worktree"] as const, { description: ISOLATION_DESCRIPTION }),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const TaskItem = Type.Object({
|
|
66
|
+
agent: Type.String({ description: "Name of the agent to invoke" }),
|
|
67
|
+
task: Type.String({
|
|
68
|
+
...NON_BLANK_TASK_OPTIONS,
|
|
69
|
+
description: "Self-contained task to delegate (the agent has no memory of this conversation)",
|
|
70
|
+
}),
|
|
71
|
+
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
|
|
72
|
+
isolation: IsolationSchema,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const SubagentParams = Type.Object({
|
|
76
|
+
agent: Type.Optional(Type.String({ description: "Name of the agent to invoke (single mode)" })),
|
|
77
|
+
task: Type.Optional(
|
|
78
|
+
Type.String({ ...NON_BLANK_TASK_OPTIONS, description: "Self-contained task to delegate (single mode)" }),
|
|
79
|
+
),
|
|
80
|
+
tasks: Type.Optional(Type.Array(TaskItem, { description: "Array of {agent, task} for parallel execution" })),
|
|
81
|
+
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process (single mode)" })),
|
|
82
|
+
isolation: IsolationSchema,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export function defaultIsolationMode(mode: "single" | "parallel", agentName: string, requested?: IsolationMode): IsolationMode {
|
|
86
|
+
if (requested) return requested;
|
|
87
|
+
return mode === "parallel" && agentName === "worker" ? "worktree" : "shared";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const managedRepositoryRootTails = new Map<string, Promise<void>>();
|
|
91
|
+
|
|
92
|
+
async function canonicalManagedRepositoryRoot(cwd: string): Promise<string> {
|
|
93
|
+
try {
|
|
94
|
+
// Repository identity does not depend on HEAD: empty repositories must
|
|
95
|
+
// serialize root and nested cwd requests under the same lane too.
|
|
96
|
+
return await resolveRepositoryRoot(cwd);
|
|
97
|
+
} catch {
|
|
98
|
+
try {
|
|
99
|
+
return await realpath(resolve(cwd));
|
|
100
|
+
} catch {
|
|
101
|
+
return resolve(cwd);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Run one operation under the canonical original-repository lane.
|
|
107
|
+
*
|
|
108
|
+
* Shared managed generations use the abortable overload for their complete
|
|
109
|
+
* writer/reviewer workflow. Isolated generations use the non-abortable overload
|
|
110
|
+
* only for their final worktree apply, so model work remains parallel while the
|
|
111
|
+
* original checkout mutation cannot race a shared writer or reviewer snapshot.
|
|
112
|
+
*/
|
|
113
|
+
async function runInManagedRepositoryLane<T>(
|
|
114
|
+
cwd: string,
|
|
115
|
+
task: () => Promise<T>,
|
|
116
|
+
): Promise<T>;
|
|
117
|
+
async function runInManagedRepositoryLane<T>(
|
|
118
|
+
cwd: string,
|
|
119
|
+
task: () => Promise<T>,
|
|
120
|
+
signal: AbortSignal,
|
|
121
|
+
): Promise<T | undefined>;
|
|
122
|
+
async function runInManagedRepositoryLane<T>(
|
|
123
|
+
cwd: string,
|
|
124
|
+
task: () => Promise<T>,
|
|
125
|
+
signal?: AbortSignal,
|
|
126
|
+
): Promise<T | undefined> {
|
|
127
|
+
if (signal?.aborted) return undefined;
|
|
128
|
+
const root = await canonicalManagedRepositoryRoot(cwd);
|
|
129
|
+
const key = process.platform === "win32" ? root.toLowerCase() : root;
|
|
130
|
+
const previous = managedRepositoryRootTails.get(key) ?? Promise.resolve();
|
|
131
|
+
let release!: () => void;
|
|
132
|
+
const gate = new Promise<void>((resolveGate) => {
|
|
133
|
+
release = resolveGate;
|
|
134
|
+
});
|
|
135
|
+
const tail = previous.catch(() => undefined).then(() => gate);
|
|
136
|
+
managedRepositoryRootTails.set(key, tail);
|
|
137
|
+
let onAbort: (() => void) | undefined;
|
|
138
|
+
try {
|
|
139
|
+
if (signal) {
|
|
140
|
+
await Promise.race([
|
|
141
|
+
previous.catch(() => undefined),
|
|
142
|
+
new Promise<void>((resolveAborted) => {
|
|
143
|
+
if (signal.aborted) resolveAborted();
|
|
144
|
+
else {
|
|
145
|
+
onAbort = resolveAborted;
|
|
146
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
147
|
+
}
|
|
148
|
+
}),
|
|
149
|
+
]);
|
|
150
|
+
} else {
|
|
151
|
+
await previous.catch(() => undefined);
|
|
152
|
+
}
|
|
153
|
+
if (signal?.aborted) return undefined;
|
|
154
|
+
return await task();
|
|
155
|
+
} finally {
|
|
156
|
+
if (signal && onAbort) signal.removeEventListener("abort", onAbort);
|
|
157
|
+
release();
|
|
158
|
+
// An aborted waiter may finish before the prior owner. Keep its chained
|
|
159
|
+
// tail installed until that owner also settles, otherwise a newcomer could
|
|
160
|
+
// observe an empty map and race the still-running workflow.
|
|
161
|
+
void tail.then(() => {
|
|
162
|
+
if (managedRepositoryRootTails.get(key) === tail) managedRepositoryRootTails.delete(key);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime): void {
|
|
168
|
+
pi.registerTool({
|
|
169
|
+
name: "subagent",
|
|
170
|
+
label: "Subagent",
|
|
171
|
+
description: [
|
|
172
|
+
"Dispatch enabled specialized agents as isolated leaf Pi child processes, singly or in parallel.",
|
|
173
|
+
"Built-ins: explorer for broad read-only reconnaissance; worker for implementation; cleaner for explicitly authorized cleanup, removal, simplification, and duplicate-code consolidation; documenter for pre-commit diff sync or explicitly requested whole-codebase comment/README/docs maintenance; reviewer for generic read-only assessments and final gates.",
|
|
174
|
+
"Work starts in the background; successful top-level writers automatically continue through enabled documenter/reviewer stages and return one final completion. Results resume the main agent and are already shown to the user, so do not poll, duplicate downstream roles, or restate them. Give each child a self-contained brief because it has no conversation memory.",
|
|
175
|
+
"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.",
|
|
176
|
+
"A selected-model or provider failure continues the retained session on the current main model; ordinary tool/task failures do not.",
|
|
177
|
+
"Use subagent_control to steer/retarget an active top-level child, park/stop a managed downstream stage, or resume/fork retained context by stable run id.",
|
|
178
|
+
].join(" "),
|
|
179
|
+
promptSnippet:
|
|
180
|
+
"Dispatch isolated background agents: explorer (recon), worker (implementation), cleaner (authorized cleanup/deduplication), documenter (docs sync), reviewer (read-only assessment/gate); enabled post-writer stages run automatically, results resume automatically, and the workflow delivers once. Use direct tools for trivial work.",
|
|
181
|
+
parameters: SubagentParams,
|
|
182
|
+
|
|
183
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
184
|
+
monitor.beginTurn();
|
|
185
|
+
const config = await loadConfig(runtime.configPath);
|
|
186
|
+
// Pick up concurrency changes from /subagents-setup without a restart.
|
|
187
|
+
runtime.backgroundQueue.setConcurrency(config.maxConcurrency);
|
|
188
|
+
|
|
189
|
+
// Finished runs leave the active monitor immediately. Their final findings
|
|
190
|
+
// are sent as a custom message that starts a follow-up turn.
|
|
191
|
+
const finishRun = (
|
|
192
|
+
runId: number,
|
|
193
|
+
status: "done" | "failed",
|
|
194
|
+
opts?: { silent?: boolean },
|
|
195
|
+
): void => {
|
|
196
|
+
monitor.setStatus(runId, status); // stamps endedAt for the elapsed time
|
|
197
|
+
const run = monitor.removeRun(runId);
|
|
198
|
+
if (!run) return; // already finished — stay idempotent
|
|
199
|
+
if (opts?.silent || !runtime.sessionActive) return;
|
|
200
|
+
const icon = status === "done" ? "✓" : "✗";
|
|
201
|
+
ctx.ui.notify(`${icon} ${monitor.summarize(run)}`, status === "done" ? "info" : "error");
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Live sub-agent activity → concise one-line status ("thinking",
|
|
205
|
+
// "read src/index.ts", ...), never a raw args blob. The live handler
|
|
206
|
+
// only updates monitor state; finishing (removeRun + notify) is owned
|
|
207
|
+
// by the queue task / launchInWorkflow. That keeps a startup retry —
|
|
208
|
+
// which fires a transient "failed" status before relaunching — from
|
|
209
|
+
// ripping the row out early, and lets the queue task decide between
|
|
210
|
+
// delivering a reviewer's result and starting an auto-fix chain.
|
|
211
|
+
const makeLiveHandler =
|
|
212
|
+
(runId: number, generation?: number) =>
|
|
213
|
+
(e: SubagentLiveEvent): void => {
|
|
214
|
+
if (generation !== undefined && runtime.threads.get(runId)?.generation !== generation) return;
|
|
215
|
+
switch (e.kind) {
|
|
216
|
+
case "status":
|
|
217
|
+
// Only update monitor status here. Finishing (removeRun + notify) is
|
|
218
|
+
// owned by the queue task / launchInWorkflow so that a startup retry — which
|
|
219
|
+
// fires a transient "failed" status before relaunching the child — never
|
|
220
|
+
// rips the row out from under the retry or emits a premature "✗" toast.
|
|
221
|
+
monitor.setStatus(runId, e.status);
|
|
222
|
+
break;
|
|
223
|
+
case "model":
|
|
224
|
+
monitor.setModel(runId, e.model, e.fallbackFrom);
|
|
225
|
+
monitor.setThinking(runId, e.thinking);
|
|
226
|
+
break;
|
|
227
|
+
case "usage":
|
|
228
|
+
monitor.setUsage(runId, e.usage, e.model);
|
|
229
|
+
break;
|
|
230
|
+
case "tool_start":
|
|
231
|
+
monitor.recordToolStart(runId, e.toolName, formatToolActivity(e.toolName, e.args));
|
|
232
|
+
break;
|
|
233
|
+
case "tool_end":
|
|
234
|
+
monitor.recordToolEnd(runId, e.toolName, e.isError);
|
|
235
|
+
break;
|
|
236
|
+
case "thinking":
|
|
237
|
+
monitor.setActivity(runId, "thinking");
|
|
238
|
+
break;
|
|
239
|
+
case "text":
|
|
240
|
+
// A text delta is model output, not a filesystem write.
|
|
241
|
+
monitor.setActivity(runId, "responding");
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const discovery = discoverAgents(ctx.cwd, {
|
|
247
|
+
scope: config.agentScope,
|
|
248
|
+
enabledNames: config.enabledAgents,
|
|
249
|
+
projectTrusted: ctx.isProjectTrusted?.() === true,
|
|
250
|
+
});
|
|
251
|
+
const agents = discovery.agents;
|
|
252
|
+
|
|
253
|
+
const hasTasks = (params.tasks?.length ?? 0) > 0;
|
|
254
|
+
const hasSingle = Boolean(params.agent) && params.task !== undefined;
|
|
255
|
+
|
|
256
|
+
const makeDetails =
|
|
257
|
+
(mode: "single" | "parallel", background = false) =>
|
|
258
|
+
(results: SingleResult[]): SubagentDetails => ({ mode, results, background });
|
|
259
|
+
|
|
260
|
+
const catalog = agents.map((a) => a.name).join(", ") || "none";
|
|
261
|
+
|
|
262
|
+
if (Number(hasTasks) + Number(hasSingle) !== 1) {
|
|
263
|
+
return {
|
|
264
|
+
content: [
|
|
265
|
+
{
|
|
266
|
+
type: "text",
|
|
267
|
+
text: `Invalid parameters. Provide exactly one mode: single {agent, task} or parallel {tasks: [...]}. Enabled agents: ${catalog}.`,
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
details: makeDetails("single")([]),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (hasTasks) {
|
|
275
|
+
const blankTaskIndex = params.tasks?.findIndex(({ task }) => task.trim().length === 0) ?? -1;
|
|
276
|
+
if (blankTaskIndex !== -1) {
|
|
277
|
+
return {
|
|
278
|
+
content: [
|
|
279
|
+
{
|
|
280
|
+
type: "text",
|
|
281
|
+
text: `Invalid parameters. tasks[${blankTaskIndex}].task must contain at least one non-whitespace character. No background tasks were started. Enabled agents: ${catalog}.`,
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
details: makeDetails("parallel")([]),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
} else if (params.task?.trim().length === 0) {
|
|
288
|
+
return {
|
|
289
|
+
content: [
|
|
290
|
+
{
|
|
291
|
+
type: "text",
|
|
292
|
+
text: `Invalid parameters. task must contain at least one non-whitespace character. Enabled agents: ${catalog}.`,
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
details: makeDetails("single")([]),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Launch one workflow-internal child in a fresh model context. It sees the
|
|
300
|
+
* parent's exact repository/worktree state and is registered by its own id,
|
|
301
|
+
* but never enters top-level lifecycle policy or completion delivery. */
|
|
302
|
+
const launchInWorkflow = async (
|
|
303
|
+
request: ManagedWorkflowRequest,
|
|
304
|
+
agentName: string,
|
|
305
|
+
task: string,
|
|
306
|
+
meta: RunChainMeta,
|
|
307
|
+
): Promise<{ runId: number; result: SingleResult }> => {
|
|
308
|
+
const agent = request.agents.find((candidate) => candidate.name === agentName);
|
|
309
|
+
if (!agent) {
|
|
310
|
+
throw new Error(`Managed workflow requires enabled agent "${agentName}", but discovery did not provide it.`);
|
|
311
|
+
}
|
|
312
|
+
const resolvedRoute = resolveDispatchModelRoute(agent, request.config, request.ctx);
|
|
313
|
+
const route = request.isolation === "worktree"
|
|
314
|
+
? { ...resolvedRoute, agent: withWorktreeSystemPrompt(resolvedRoute.agent) }
|
|
315
|
+
: resolvedRoute;
|
|
316
|
+
const thinkingLevel = route.thinkingLevel;
|
|
317
|
+
const runId = monitor.addRun(agent.name, task, route.agent.model, thinkingLevel, {
|
|
318
|
+
...meta,
|
|
319
|
+
isolation: request.isolation,
|
|
320
|
+
});
|
|
321
|
+
const onLive = makeLiveHandler(runId);
|
|
322
|
+
try {
|
|
323
|
+
const result = await runSingleAgentWithMainFallback(
|
|
324
|
+
{
|
|
325
|
+
defaultCwd: request.executionCwd,
|
|
326
|
+
cwd: request.executionCwd,
|
|
327
|
+
agent: route.agent,
|
|
328
|
+
agentName,
|
|
329
|
+
task,
|
|
330
|
+
thinkingLevel,
|
|
331
|
+
thinkingLevelForModel: route.thinkingLevelForModel,
|
|
332
|
+
signal: request.signal,
|
|
333
|
+
onLive,
|
|
334
|
+
makeDetails: makeDetails("single", true),
|
|
335
|
+
idleTimeoutMs: request.config.idleTimeoutSec * 1000,
|
|
336
|
+
},
|
|
337
|
+
route.mainFallbackRef,
|
|
338
|
+
);
|
|
339
|
+
result.runId = runId;
|
|
340
|
+
result.projectCwd = request.projectCwd;
|
|
341
|
+
result.isolation = request.isolation;
|
|
342
|
+
runtime.retainSession(result);
|
|
343
|
+
monitor.setModel(runId, result.model, result.modelFallbackFrom);
|
|
344
|
+
monitor.setThinking(runId, result.thinking);
|
|
345
|
+
finishRun(runId, isFailedResult(result) ? "failed" : "done", { silent: true });
|
|
346
|
+
runtime.registerRunResult(runId, result);
|
|
347
|
+
return { runId, result };
|
|
348
|
+
} catch (error) {
|
|
349
|
+
finishRun(runId, "failed", { silent: true });
|
|
350
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
351
|
+
const crashed: SingleResult = {
|
|
352
|
+
...queuedResult(route.agent, task, thinkingLevel),
|
|
353
|
+
runId,
|
|
354
|
+
projectCwd: request.projectCwd,
|
|
355
|
+
isolation: request.isolation,
|
|
356
|
+
exitCode: 1,
|
|
357
|
+
stderr: errorMessage,
|
|
358
|
+
stopReason: request.signal.aborted ? "aborted" : "error",
|
|
359
|
+
errorMessage,
|
|
360
|
+
dispatchFailed: true,
|
|
361
|
+
};
|
|
362
|
+
runtime.registerRunResult(runId, crashed);
|
|
363
|
+
return { runId, result: crashed };
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
/** Drop any in-flight internal row. Normal internal settlement already
|
|
368
|
+
* removes rows; this is a cancellation/crash guard. */
|
|
369
|
+
const removeWorkflowGroup = (groupId: string): void => {
|
|
370
|
+
for (const run of [...monitor.getRuns()]) {
|
|
371
|
+
if (run.groupId === groupId) monitor.removeRun(run.id);
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
/** Run every downstream role inline under the parent generation's queue
|
|
376
|
+
* controller. That gives park/stop/shutdown one lifecycle owner and keeps
|
|
377
|
+
* isolated worktrees unintegrated until the final reviewer settles. */
|
|
378
|
+
const runManagedWorkflow = async (
|
|
379
|
+
request: ManagedWorkflowRequest,
|
|
380
|
+
): Promise<ManagedWorkflowOutcome> => {
|
|
381
|
+
const initialStepRunId = monitor.reserveRunId();
|
|
382
|
+
const initialStepResult: SingleResult = {
|
|
383
|
+
...request.initialResult,
|
|
384
|
+
runId: initialStepRunId,
|
|
385
|
+
};
|
|
386
|
+
runtime.registerRunResult(initialStepRunId, initialStepResult);
|
|
387
|
+
const steps: ChainStep[] = [{
|
|
388
|
+
runId: initialStepRunId,
|
|
389
|
+
result: initialStepResult,
|
|
390
|
+
relation: request.plan.initialRelation,
|
|
391
|
+
}];
|
|
392
|
+
const enabled = (name: string): boolean =>
|
|
393
|
+
request.agents.some((candidate) => candidate.name === name);
|
|
394
|
+
const canContinue = (): boolean => runtime.sessionActive && !request.signal.aborted;
|
|
395
|
+
const launchStep = async (
|
|
396
|
+
agentName: string,
|
|
397
|
+
task: string,
|
|
398
|
+
relation: string,
|
|
399
|
+
): Promise<SingleResult> => {
|
|
400
|
+
if (!enabled(agentName)) {
|
|
401
|
+
throw new Error(`Managed workflow cannot launch disabled or missing agent "${agentName}".`);
|
|
402
|
+
}
|
|
403
|
+
const step = await launchInWorkflow(request, agentName, task, {
|
|
404
|
+
groupId: request.groupId,
|
|
405
|
+
relationLabel: relation,
|
|
406
|
+
parentRunId: request.parentRunId,
|
|
407
|
+
});
|
|
408
|
+
request.rememberLatest(step.result);
|
|
409
|
+
steps.push({ ...step, relation });
|
|
410
|
+
return step.result;
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const runFixRounds = async (triggeringReviewer: SingleResult): Promise<void> => {
|
|
414
|
+
let lastReviewer = triggeringReviewer;
|
|
415
|
+
for (let round = 1; round <= request.config.maxFixRounds; round++) {
|
|
416
|
+
if (!canContinue()) break;
|
|
417
|
+
const workerResult = await launchStep(
|
|
418
|
+
"worker",
|
|
419
|
+
buildFixTaskBrief(lastReviewer, round, request.config.maxFixRounds),
|
|
420
|
+
`fix round ${round}`,
|
|
421
|
+
);
|
|
422
|
+
if (!canContinue() || isFailedResult(workerResult)) break;
|
|
423
|
+
|
|
424
|
+
let documenterResult: SingleResult | undefined;
|
|
425
|
+
if (enabled("documenter")) {
|
|
426
|
+
documenterResult = await launchStep(
|
|
427
|
+
"documenter",
|
|
428
|
+
buildDocumenterTaskBrief(workerResult, round, lastReviewer),
|
|
429
|
+
`docs round ${round}`,
|
|
430
|
+
);
|
|
431
|
+
if (!canContinue() || isFailedResult(documenterResult)) break;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const reviewResult = await launchStep(
|
|
435
|
+
"reviewer",
|
|
436
|
+
buildReReviewBrief(lastReviewer, round, workerResult, documenterResult),
|
|
437
|
+
`re-review round ${round}`,
|
|
438
|
+
);
|
|
439
|
+
if (!canContinue() || isFailedResult(reviewResult)) break;
|
|
440
|
+
const verdict = reviewVerdict(getResultOutput(reviewResult));
|
|
441
|
+
// REVIEW_PASS settles. No verdict is advisory/malformed and must never
|
|
442
|
+
// trigger another writer. Only an explicit REVIEW_FAIL consumes a fix.
|
|
443
|
+
if (verdict !== "fail") break;
|
|
444
|
+
lastReviewer = reviewResult;
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
// Park/stop/shutdown may win after the top-level child settles but
|
|
450
|
+
// before this continuation starts. Preserve that stable checkpoint and
|
|
451
|
+
// never create an already-aborted downstream child.
|
|
452
|
+
if (!canContinue()) return { kind: request.plan.kind, steps };
|
|
453
|
+
if (request.plan.kind === "auto-fix") {
|
|
454
|
+
await runFixRounds(initialStepResult);
|
|
455
|
+
} else {
|
|
456
|
+
let documenterResult: SingleResult | undefined;
|
|
457
|
+
if (request.plan.kind === "review-pass-sync") {
|
|
458
|
+
documenterResult = await launchStep(
|
|
459
|
+
"documenter",
|
|
460
|
+
buildReviewPassDocumenterBrief(initialStepResult),
|
|
461
|
+
"documentation sync",
|
|
462
|
+
);
|
|
463
|
+
} else if (initialStepResult.agent !== "documenter" && enabled("documenter")) {
|
|
464
|
+
documenterResult = await launchStep(
|
|
465
|
+
"documenter",
|
|
466
|
+
buildPostWriterDocumenterBrief(initialStepResult),
|
|
467
|
+
"documentation sync",
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (
|
|
472
|
+
canContinue() &&
|
|
473
|
+
(!documenterResult || !isFailedResult(documenterResult)) &&
|
|
474
|
+
enabled("reviewer")
|
|
475
|
+
) {
|
|
476
|
+
const reviewResult = await launchStep(
|
|
477
|
+
"reviewer",
|
|
478
|
+
buildFinalReviewBrief(initialStepResult, documenterResult),
|
|
479
|
+
"final review",
|
|
480
|
+
);
|
|
481
|
+
if (
|
|
482
|
+
canContinue() &&
|
|
483
|
+
!isFailedResult(reviewResult) &&
|
|
484
|
+
reviewVerdict(getResultOutput(reviewResult)) === "fail" &&
|
|
485
|
+
enabled("worker") &&
|
|
486
|
+
request.config.maxFixRounds > 0
|
|
487
|
+
) {
|
|
488
|
+
await runFixRounds(reviewResult);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return { kind: request.plan.kind, steps };
|
|
493
|
+
} finally {
|
|
494
|
+
removeWorkflowGroup(request.groupId);
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
const startBackground = createBackgroundDispatcher({
|
|
499
|
+
runtime,
|
|
500
|
+
ctx,
|
|
501
|
+
config,
|
|
502
|
+
agents,
|
|
503
|
+
finishRun,
|
|
504
|
+
makeLiveHandler,
|
|
505
|
+
makeDetails,
|
|
506
|
+
runManagedWorkflow,
|
|
507
|
+
runInManagedRepositoryLane,
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// Sub-agents intentionally detach from the foreground turn. This makes the
|
|
511
|
+
// editor available immediately; completion messages later wake the main agent.
|
|
512
|
+
if (params.tasks && params.tasks.length > 0) {
|
|
513
|
+
if (params.tasks.length > config.maxConcurrency) {
|
|
514
|
+
return {
|
|
515
|
+
content: [
|
|
516
|
+
{
|
|
517
|
+
type: "text",
|
|
518
|
+
text: `Too many parallel tasks (${params.tasks.length}). Max is ${config.maxConcurrency} (configurable via /subagents-setup).`,
|
|
519
|
+
},
|
|
520
|
+
],
|
|
521
|
+
details: makeDetails("parallel", true)([]),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const results: SingleResult[] = [];
|
|
526
|
+
// Preserve caller order (and deterministic completion batching) while
|
|
527
|
+
// preparing each isolated filesystem before its queue entry can start.
|
|
528
|
+
for (const item of params.tasks) {
|
|
529
|
+
results.push(await startBackground(
|
|
530
|
+
item.agent,
|
|
531
|
+
item.task,
|
|
532
|
+
item.cwd,
|
|
533
|
+
defaultIsolationMode("parallel", item.agent, item.isolation as IsolationMode | undefined),
|
|
534
|
+
));
|
|
535
|
+
}
|
|
536
|
+
const startedRuns = results.filter((result) => result.exitCode === -1);
|
|
537
|
+
const started = startedRuns.length;
|
|
538
|
+
const startedRefs = startedRuns.map((result) =>
|
|
539
|
+
result.runId === undefined ? result.agent : `#${result.runId} ${result.agent}`,
|
|
540
|
+
);
|
|
541
|
+
const failureLines = results.flatMap((result, index) => {
|
|
542
|
+
if (result.exitCode === -1) return [];
|
|
543
|
+
const reason = getResultOutput(result).trim() || "unknown startup failure";
|
|
544
|
+
return [
|
|
545
|
+
`- tasks[${index}] (${params.tasks![index]!.agent}) failed to start: ${reason.replace(/\n/g, "\n ")}`,
|
|
546
|
+
];
|
|
547
|
+
});
|
|
548
|
+
if (started === 0) {
|
|
549
|
+
// Pi marks custom-tool failures only when execute throws; returning an
|
|
550
|
+
// `isError` property is still a successful AgentToolResult.
|
|
551
|
+
throw new Error(`No background subagents were started.\n${failureLines.join("\n")}`);
|
|
552
|
+
}
|
|
553
|
+
const text = [
|
|
554
|
+
`Started ${started} background subagent${started === 1 ? "" : "s"}: ${startedRefs.join(", ")}. Results will automatically resume the main agent when ready.`,
|
|
555
|
+
...(failureLines.length > 0
|
|
556
|
+
? [`${failureLines.length} task${failureLines.length === 1 ? "" : "s"} failed before launch:`, ...failureLines]
|
|
557
|
+
: []),
|
|
558
|
+
].join("\n");
|
|
559
|
+
return {
|
|
560
|
+
content: [{ type: "text", text }],
|
|
561
|
+
details: makeDetails("parallel", true)(results),
|
|
562
|
+
terminate: true,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const result = await startBackground(
|
|
567
|
+
params.agent as string,
|
|
568
|
+
params.task as string,
|
|
569
|
+
params.cwd,
|
|
570
|
+
defaultIsolationMode("single", params.agent as string, params.isolation as IsolationMode | undefined),
|
|
571
|
+
);
|
|
572
|
+
if (result.exitCode !== -1) {
|
|
573
|
+
throw new Error(getResultOutput(result));
|
|
574
|
+
}
|
|
575
|
+
const runRef = result.runId === undefined ? result.agent : `#${result.runId} ${result.agent}`;
|
|
576
|
+
return {
|
|
577
|
+
content: [{ type: "text", text: `Started ${runRef} in the background. Its result will automatically resume the main agent when ready.` }],
|
|
578
|
+
details: makeDetails("single", true)([result]),
|
|
579
|
+
terminate: true,
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
},
|
|
583
|
+
|
|
584
|
+
renderCall(args, theme) {
|
|
585
|
+
if (args.tasks && args.tasks.length > 0) {
|
|
586
|
+
let text = `${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", `parallel (${args.tasks.length})`)}`;
|
|
587
|
+
for (const t of args.tasks.slice(0, 4)) {
|
|
588
|
+
const preview = formatTaskSummary(t.task, 48);
|
|
589
|
+
const isolation = defaultIsolationMode("parallel", t.agent, t.isolation) === "worktree" ? " [worktree]" : "";
|
|
590
|
+
text += `\n ${theme.fg("accent", t.agent)}${theme.fg("dim", isolation)} ${theme.fg("dim", preview)}`;
|
|
591
|
+
}
|
|
592
|
+
if (args.tasks.length > 4) text += `\n ${theme.fg("dim", `… +${args.tasks.length - 4} more`)}`;
|
|
593
|
+
return new Text(text, 0, 0);
|
|
594
|
+
}
|
|
595
|
+
const task: string = args.task ?? "";
|
|
596
|
+
const preview = formatTaskSummary(task, 60);
|
|
597
|
+
const isolation = args.isolation === "worktree" ? " [worktree]" : "";
|
|
598
|
+
return new Text(
|
|
599
|
+
`${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", args.agent ?? "?")}${theme.fg("dim", isolation)} ${theme.fg("dim", preview)}`,
|
|
600
|
+
0,
|
|
601
|
+
0,
|
|
602
|
+
);
|
|
603
|
+
},
|
|
604
|
+
|
|
605
|
+
renderResult(result, _options, theme) {
|
|
606
|
+
const details = result.details as SubagentDetails | undefined;
|
|
607
|
+
if (!details || details.results.length === 0) return new Text(theme.fg("dim", "(no output)"), 0, 0);
|
|
608
|
+
|
|
609
|
+
if (details.mode === "single") {
|
|
610
|
+
const r = details.results[0];
|
|
611
|
+
const pending = r.exitCode === -1;
|
|
612
|
+
const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
|
|
613
|
+
const usage = formatUsage(r.usage);
|
|
614
|
+
const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (main after ${r.modelFallbackFrom} failed)` : ""}`;
|
|
615
|
+
const isolation = r.isolation === "worktree" ? ` · worktree ${r.integrationStatus ?? "active"}` : "";
|
|
616
|
+
const runId = r.runId === undefined ? "" : `${theme.fg("dim", `#${r.runId}`)} `;
|
|
617
|
+
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}` : ""}`)}`;
|
|
618
|
+
return new Text(line, 0, 0);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Parallel mode: header + one compact line per agent
|
|
622
|
+
const lines: string[] = [
|
|
623
|
+
`${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", `parallel (${details.results.length})`)}`,
|
|
624
|
+
];
|
|
625
|
+
for (const r of details.results) {
|
|
626
|
+
const pending = r.exitCode === -1;
|
|
627
|
+
const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
|
|
628
|
+
const usage = formatUsage(r.usage);
|
|
629
|
+
const model = `${r.model ?? "?"}${r.modelFallbackFrom ? ` (main after ${r.modelFallbackFrom} failed)` : ""}`;
|
|
630
|
+
const isolation = r.isolation === "worktree" ? ` · worktree ${r.integrationStatus ?? "active"}` : "";
|
|
631
|
+
const runId = r.runId === undefined ? "" : `${theme.fg("dim", `#${r.runId}`)} `;
|
|
632
|
+
lines.push(` ${icon} ${runId}${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${r.thinking ? ` · thinking ${r.thinking}` : ""}${isolation}${pending ? " · background" : ""}${usage ? ` · ${usage}` : ""}`)}`);
|
|
633
|
+
}
|
|
634
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
}
|