@bacnh85/pi-subagent 0.10.0 → 0.11.0
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/CHANGELOG.md +11 -0
- package/README.md +11 -9
- package/agent-format.md +4 -2
- package/agents/general-purpose.md +4 -2
- package/agents/planner.md +3 -1
- package/agents/reviewer.md +3 -1
- package/agents/scout.md +3 -3
- package/agents/tester.md +3 -3
- package/agents/worker.md +4 -2
- package/extensions/agents.ts +310 -310
- package/extensions/index.ts +1357 -1357
- package/extensions/model.ts +41 -41
- package/extensions/render.ts +205 -205
- package/extensions/runner.ts +200 -200
- package/extensions/security.ts +313 -313
- package/extensions/service.ts +115 -115
- package/extensions/thread-viewer.ts +246 -246
- package/extensions/threads.ts +99 -99
- package/package.json +9 -9
package/extensions/index.ts
CHANGED
|
@@ -16,45 +16,45 @@
|
|
|
16
16
|
import * as path from "node:path";
|
|
17
17
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
18
18
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
CONFIG_DIR_NAME,
|
|
20
|
+
DynamicBorder,
|
|
21
|
+
type ExtensionAPI,
|
|
22
|
+
type ExtensionContext,
|
|
23
|
+
getAgentDir,
|
|
24
|
+
getMarkdownTheme,
|
|
25
|
+
type ThemeColor,
|
|
26
26
|
} from "@earendil-works/pi-coding-agent";
|
|
27
27
|
import { Container, Markdown, SelectList, Spacer, Text } from "@earendil-works/pi-tui";
|
|
28
28
|
import { Type } from "typebox";
|
|
29
29
|
|
|
30
30
|
import { type AgentColor, type AgentConfig, type AgentScope, discoverAgents, formatAgentList, getModelCandidates, invalidateAgentCache } from "./agents.ts";
|
|
31
31
|
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
type SubAgentProgress,
|
|
33
|
+
type SubAgentResult,
|
|
34
|
+
getFinalOutput,
|
|
35
|
+
getResultOutput,
|
|
36
|
+
isFailedResult,
|
|
37
|
+
mapWithConcurrencyLimit,
|
|
38
|
+
runSubAgent,
|
|
39
|
+
startHeartbeat,
|
|
40
40
|
} from "./runner.ts";
|
|
41
41
|
import {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
42
|
+
isRateLimitError,
|
|
43
|
+
normalizeTimeout,
|
|
44
|
+
resolveSafeCwd,
|
|
45
|
+
validateAgentTools,
|
|
46
|
+
truncateParallelOutput,
|
|
47
|
+
validateExecutionRequest,
|
|
48
|
+
READ_ONLY_TOOLS,
|
|
49
|
+
MAX_CONCURRENCY,
|
|
50
|
+
MAX_PARALLEL_TASKS,
|
|
51
|
+
MAX_CHAIN_LENGTH,
|
|
52
|
+
MAX_INSTRUCTIONS_LENGTH,
|
|
53
53
|
} from "./security.ts";
|
|
54
54
|
import {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
aggregateUsage,
|
|
56
|
+
formatUsageStats,
|
|
57
|
+
renderSingleResult,
|
|
58
58
|
} from "./render.ts";
|
|
59
59
|
import { type SubagentThread, threadStore } from "./threads.ts";
|
|
60
60
|
import { SUBAGENT_REQUEST_EVENT, runNamedAgent, type SubagentRunRequest } from "./service.ts";
|
|
@@ -71,17 +71,17 @@ import { ThreadViewer, type ThreadViewerCallbacks } from "./thread-viewer.ts";
|
|
|
71
71
|
|
|
72
72
|
/** Namespace for trusted configuration loaded from pi settings, never from tool params. */
|
|
73
73
|
function getTrustedConfig(ctx: ExtensionContext): { allowUnconfirmedProjectAgents: boolean; allowExternalCwd: boolean } {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
74
|
+
// Use pi's settings infrastructure if available; fall back to env vars for testing.
|
|
75
|
+
// The model cannot influence these values.
|
|
76
|
+
const settings = (ctx as any).settings ?? {};
|
|
77
|
+
return {
|
|
78
|
+
allowUnconfirmedProjectAgents:
|
|
79
|
+
(settings as Record<string, unknown>).allowUnconfirmedProjectAgents === true ||
|
|
80
|
+
process.env.PI_SUBAGENT_ALLOW_UNCONFIRMED_PROJECT_AGENTS === "true",
|
|
81
|
+
allowExternalCwd:
|
|
82
|
+
(settings as Record<string, unknown>).allowExternalCwd === true ||
|
|
83
|
+
process.env.PI_SUBAGENT_ALLOW_EXTERNAL_CWD === "true",
|
|
84
|
+
};
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
|
|
@@ -90,44 +90,44 @@ function getTrustedConfig(ctx: ExtensionContext): { allowUnconfirmedProjectAgent
|
|
|
90
90
|
// ---------------------------------------------------------------------------
|
|
91
91
|
|
|
92
92
|
const TaskItem = Type.Object({
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
agent: Type.String({ description: "Name of the agent to invoke" }),
|
|
94
|
+
task: Type.String({ description: "Task to delegate to the agent" }),
|
|
95
|
+
cwd: Type.Optional(Type.String({ description: "Working directory for the agent" })),
|
|
96
|
+
timeout: Type.Optional(Type.Number({ description: "Inactivity timeout in milliseconds for this task; real child activity resets it (default 3 minutes, absolute cap 20 minutes)" })),
|
|
97
97
|
});
|
|
98
98
|
|
|
99
99
|
const ChainItem = Type.Object({
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
agent: Type.String({ description: "Name of the agent to invoke" }),
|
|
101
|
+
task: Type.String({ description: "Task with optional {previous} placeholder for prior output" }),
|
|
102
|
+
cwd: Type.Optional(Type.String({ description: "Working directory for the agent" })),
|
|
103
|
+
timeout: Type.Optional(Type.Number({ description: "Inactivity timeout in milliseconds for this step; real child activity resets it (default 3 minutes, absolute cap 20 minutes)" })),
|
|
104
104
|
});
|
|
105
105
|
|
|
106
106
|
const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
description:
|
|
108
|
+
'Which agent directories to use. Default: "user". Use "both" to include project-local agents.',
|
|
109
|
+
default: "user",
|
|
110
110
|
});
|
|
111
111
|
|
|
112
112
|
const SubagentParams = Type.Object({
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
113
|
+
agent: Type.Optional(Type.String({ description: "Name of the agent to invoke (single mode)" })),
|
|
114
|
+
task: Type.Optional(Type.String({ description: "Task to delegate (single mode)" })),
|
|
115
|
+
tasks: Type.Optional(
|
|
116
|
+
Type.Array(TaskItem, { description: "Array of {agent, task} for parallel execution" }),
|
|
117
|
+
),
|
|
118
|
+
chain: Type.Optional(
|
|
119
|
+
Type.Array(ChainItem, {
|
|
120
|
+
description: "Array of {agent, task} for sequential execution with {previous}",
|
|
121
|
+
}),
|
|
122
|
+
),
|
|
123
|
+
agentScope: Type.Optional(AgentScopeSchema),
|
|
124
|
+
// Security: confirmProjectAgents is NOT exposed as a model-controllable parameter.
|
|
125
|
+
// Project-agent confirmation is enforced via trusted configuration.
|
|
126
|
+
// See Security model section in README.
|
|
127
|
+
cwd: Type.Optional(Type.String({ description: "Working directory (single mode, must be inside workspace)" })),
|
|
128
|
+
timeout: Type.Optional(Type.Number({ description: "Global inactivity timeout in milliseconds (default 3 minutes; real activity resets it; fixed 20-minute absolute cap)" })),
|
|
129
|
+
instructions: Type.Optional(Type.String({ description: "Bounded repository/task instructions passed to each child (max 16 KB)" })),
|
|
130
|
+
abortOnFailure: Type.Optional(Type.Boolean({ description: "In parallel mode, cancel remaining tasks when one fails. Default: false.", default: false })),
|
|
131
131
|
});
|
|
132
132
|
|
|
133
133
|
// ---------------------------------------------------------------------------
|
|
@@ -135,10 +135,10 @@ const SubagentParams = Type.Object({
|
|
|
135
135
|
// ---------------------------------------------------------------------------
|
|
136
136
|
|
|
137
137
|
interface SubagentDetails {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
138
|
+
mode: "single" | "parallel" | "chain";
|
|
139
|
+
agentScope: AgentScope;
|
|
140
|
+
projectAgentsDir: string | null;
|
|
141
|
+
results: SubAgentResult[];
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
// ---------------------------------------------------------------------------
|
|
@@ -146,1288 +146,1288 @@ interface SubagentDetails {
|
|
|
146
146
|
// ---------------------------------------------------------------------------
|
|
147
147
|
|
|
148
148
|
export default function (pi: ExtensionAPI) {
|
|
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
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
149
|
+
let currentCtx: ExtensionContext | undefined;
|
|
150
|
+
|
|
151
|
+
// Invalidate agent cache + clear thread store on session replacement.
|
|
152
|
+
pi.on("session_start", (event, ctx) => {
|
|
153
|
+
currentCtx = ctx;
|
|
154
|
+
if (event.reason === "reload") invalidateAgentCache();
|
|
155
|
+
threadStore.clear();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Resolve bundled agents directory relative to this extension file
|
|
159
|
+
const bundledAgentsDir = path.resolve(__dirname, "../agents");
|
|
160
|
+
|
|
161
|
+
// Inject available agent catalog into system prompt for semantic auto-delegation
|
|
162
|
+
pi.on("before_agent_start", async (event) => {
|
|
163
|
+
const ctx = currentCtx;
|
|
164
|
+
const discovery = discoverAgents(ctx?.cwd ?? process.cwd(), "both", bundledAgentsDir);
|
|
165
|
+
const catalog = discovery.agents
|
|
166
|
+
.map((agent) => {
|
|
167
|
+
const candidates = getModelCandidates(agent);
|
|
168
|
+
const modelInfo = candidates.length > 0
|
|
169
|
+
? ` (models: ${candidates.join(" → ")} → parent fallback)`
|
|
170
|
+
: " (parent fallback)";
|
|
171
|
+
const thinkingInfo = agent.thinking ? `, thinking: ${agent.thinking}` : "";
|
|
172
|
+
const sandboxInfo = agent.sandbox ? `, sandbox: ${agent.sandbox}` : "";
|
|
173
|
+
return `- **${agent.name}**: ${agent.description}${modelInfo}${thinkingInfo}${sandboxInfo}`;
|
|
174
|
+
})
|
|
175
|
+
.join("\n");
|
|
176
|
+
return {
|
|
177
|
+
systemPrompt:
|
|
178
|
+
event.systemPrompt +
|
|
179
|
+
`\n\n## Available Subagents\n${catalog}\n\n` +
|
|
180
|
+
"The subagent tool can delegate tasks to these specialized agents with isolated context. " +
|
|
181
|
+
"Use for read-heavy exploration, parallel analysis, or work that would flood the main context.\n" +
|
|
182
|
+
"Prefer **scout** and **tester** for cheap routine work. " +
|
|
183
|
+
"Prefer **worker** or **general-purpose** for normal coding. " +
|
|
184
|
+
"Prefer **planner** and **reviewer** for consequential reasoning. " +
|
|
185
|
+
"Modes: single, parallel (max 8 tasks, 4 concurrent), chain.",
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Public one-request/one-response service used by pi-review.
|
|
190
|
+
pi.events.on(SUBAGENT_REQUEST_EVENT, (raw) => {
|
|
191
|
+
const request = raw as SubagentRunRequest;
|
|
192
|
+
const ctx = currentCtx;
|
|
193
|
+
if (!ctx || !request?.id || typeof request.respond !== "function") return;
|
|
194
|
+
if (request.accept && !request.accept()) return;
|
|
195
|
+
const agent = discoverAgents(ctx.cwd, "user", bundledAgentsDir).agents.find((item) => item.name === request.agent);
|
|
196
|
+
if (!agent) {
|
|
197
|
+
request.respond({ id: request.id, ok: false, error: `Unknown agent: ${request.agent}` });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const thread = threadStore.createThread({ agentName: agent.name, task: request.task, mode: "single", color: agent.color ? AGENT_TO_THEME_COLOR[agent.color as AgentColor] : undefined });
|
|
201
|
+
void runNamedAgent({
|
|
202
|
+
agent: request.readOnly ? { ...agent, tools: ["read", "grep", "find", "ls"] } : agent,
|
|
203
|
+
task: request.task,
|
|
204
|
+
cwd: request.cwd ?? ctx.cwd,
|
|
205
|
+
ctx,
|
|
206
|
+
timeout: request.timeout,
|
|
207
|
+
instructions: request.instructions,
|
|
208
|
+
signal: request.signal,
|
|
209
|
+
onMessage: (result) => threadStore.updateThread(thread.id, { result }),
|
|
210
|
+
onProgress: (progress) => { threadStore.updateProgress(thread.id, progress); request.onProgress?.(progress); },
|
|
211
|
+
}).then((result) => {
|
|
212
|
+
threadStore.updateThread(thread.id, {
|
|
213
|
+
status: isFailedResult(result) ? (result.stopReason === "aborted" ? "aborted" : "failed") : "completed",
|
|
214
|
+
result,
|
|
215
|
+
});
|
|
216
|
+
try {
|
|
217
|
+
if (isFailedResult(result)) request.respond({ id: request.id, ok: false, error: getResultOutput(result) });
|
|
218
|
+
else request.respond({ id: request.id, ok: true, result });
|
|
219
|
+
} catch { /* respond channel closed */ }
|
|
220
|
+
}, (error) => {
|
|
221
|
+
threadStore.updateThread(thread.id, { status: "failed" });
|
|
222
|
+
try {
|
|
223
|
+
request.respond({ id: request.id, ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
224
|
+
} catch { /* respond channel closed */ }
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// /subagent command — list available agents
|
|
229
|
+
pi.registerCommand("subagent", {
|
|
230
|
+
description: "List available sub-agents, reload agent definitions, or show agent details",
|
|
231
|
+
handler: async (args, ctx) => {
|
|
232
|
+
const cmd = args.trim().toLowerCase();
|
|
233
|
+
const discovery = discoverAgents(ctx.cwd, "both", bundledAgentsDir);
|
|
234
|
+
|
|
235
|
+
if (cmd === "reload" || cmd === "refresh") {
|
|
236
|
+
invalidateAgentCache();
|
|
237
|
+
const fresh = discoverAgents(ctx.cwd, "both", bundledAgentsDir);
|
|
238
|
+
const list = formatAgentList(fresh.agents, 20);
|
|
239
|
+
const extra = list.remaining > 0 ? `\n ... +${list.remaining} more` : "";
|
|
240
|
+
const dirs = fresh.projectAgentsDir ? `project: ${fresh.projectAgentsDir}` : "no project agents dir";
|
|
241
|
+
const diagText = fresh.diagnostics.length > 0
|
|
242
|
+
? "\n\nWarnings:\n" + fresh.diagnostics.map(d => ` - [${d.severity}] ${d.filePath}: ${d.issue}`).join("\n")
|
|
243
|
+
: "";
|
|
244
|
+
pi.sendMessage({
|
|
245
|
+
customType: "pi-subagent",
|
|
246
|
+
content: `Agent definitions reloaded.\n\nAvailable agents (${fresh.agents.length}):\n ${list.text}${extra}${diagText}\n\nDirectories searched:\n user: ${path.join(getAgentDir(), "agents")}\n ${dirs}\n bundled: ${bundledAgentsDir}`,
|
|
247
|
+
display: true,
|
|
248
|
+
});
|
|
249
|
+
ctx.ui.notify("Agent definitions reloaded", "info");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Handle listing keywords before agent lookup
|
|
254
|
+
if (cmd === "all" || cmd === "list" || cmd === "agents") {
|
|
255
|
+
const list = formatAgentList(discovery.agents, 20);
|
|
256
|
+
const extra = list.remaining > 0 ? `\n ... +${list.remaining} more` : "";
|
|
257
|
+
const dirs = discovery.projectAgentsDir ? `\n project: ${discovery.projectAgentsDir}` : "";
|
|
258
|
+
const diagText = discovery.diagnostics.length > 0
|
|
259
|
+
? "\n\nWarnings:\n" + discovery.diagnostics.map(d => ` - [${d.severity}] ${d.filePath}: ${d.issue}`).join("\n")
|
|
260
|
+
: "";
|
|
261
|
+
pi.sendMessage({
|
|
262
|
+
customType: "pi-subagent",
|
|
263
|
+
content: `Available agents (${discovery.agents.length}):\n ${list.text}${extra}${diagText}\n\nScopes searched:\n user: ${path.join(getAgentDir(), "agents")}${dirs}\n bundled: ${bundledAgentsDir}\n\nUse /subagent <name> for agent details, /subagent reload to refresh.`,
|
|
264
|
+
display: true,
|
|
265
|
+
});
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (cmd) {
|
|
270
|
+
// Show details for a specific agent
|
|
271
|
+
const agent = discovery.agents.find(
|
|
272
|
+
(a) => a.name.toLowerCase() === cmd,
|
|
273
|
+
);
|
|
274
|
+
if (!agent) {
|
|
275
|
+
ctx.ui.notify(`Unknown agent: "${args.trim()}". Use /subagent to list all.`, "error");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const candidates = getModelCandidates(agent);
|
|
279
|
+
pi.sendMessage({
|
|
280
|
+
customType: "pi-subagent",
|
|
281
|
+
content: [
|
|
282
|
+
`Agent: ${agent.name} (${agent.source})`,
|
|
283
|
+
`Description: ${agent.description}`,
|
|
284
|
+
`Models: ${candidates.length > 0 ? `${candidates.join(" → ")} → parent fallback` : "parent fallback"}`,
|
|
285
|
+
`Thinking: ${agent.thinking || "off"}`,
|
|
286
|
+
`Tools: ${agent.tools?.join(", ") || "all default"}`,
|
|
287
|
+
`Source file: ${agent.filePath}`,
|
|
288
|
+
"",
|
|
289
|
+
"--- System Prompt ---",
|
|
290
|
+
agent.systemPrompt,
|
|
291
|
+
].join("\n"),
|
|
292
|
+
display: true,
|
|
293
|
+
});
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// List all agents
|
|
298
|
+
const list = formatAgentList(discovery.agents, 20);
|
|
299
|
+
const extra = list.remaining > 0 ? `\n ... +${list.remaining} more` : "";
|
|
300
|
+
const dirs = discovery.projectAgentsDir ? `\n project: ${discovery.projectAgentsDir}` : "";
|
|
301
|
+
const diagText = discovery.diagnostics.length > 0
|
|
302
|
+
? "\n\nWarnings:\n" + discovery.diagnostics.map(d => ` - [${d.severity}] ${d.filePath}: ${d.issue}`).join("\n")
|
|
303
|
+
: "";
|
|
304
|
+
pi.sendMessage({
|
|
305
|
+
customType: "pi-subagent",
|
|
306
|
+
content: `Available agents (${discovery.agents.length}):\n ${list.text}${extra}${diagText}\n\nScopes searched:\n user: ${path.join(getAgentDir(), "agents")}${dirs}\n bundled: ${bundledAgentsDir}\n\nUse /subagent <name> for agent details, /subagent reload to refresh.`,
|
|
307
|
+
display: true,
|
|
308
|
+
});
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
/** Map AgentColor (from agent frontmatter) to ThemeColor (for pi TUI). */
|
|
313
|
+
const AGENT_TO_THEME_COLOR: Record<AgentColor, ThemeColor> = {
|
|
314
|
+
red: "error",
|
|
315
|
+
blue: "accent",
|
|
316
|
+
green: "success",
|
|
317
|
+
yellow: "warning",
|
|
318
|
+
purple: "syntaxType",
|
|
319
|
+
orange: "syntaxString",
|
|
320
|
+
pink: "customMessageLabel",
|
|
321
|
+
cyan: "syntaxVariable",
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
/** Resolve agent-defined color to a valid ThemeColor for thread creation. */
|
|
325
|
+
const agentToThemeColor = (agentName: string): ThemeColor | undefined => {
|
|
326
|
+
const ctx = currentCtx;
|
|
327
|
+
if (!ctx) return undefined;
|
|
328
|
+
const agent = discoverAgents(ctx.cwd, "both", bundledAgentsDir).agents.find(a => a.name === agentName);
|
|
329
|
+
return agent?.color ? AGENT_TO_THEME_COLOR[agent.color] : undefined;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
/** Look up agent color by name for TUI rendering. */
|
|
333
|
+
const resolveAgentColor = (name: string): ThemeColor => {
|
|
334
|
+
const ctx = currentCtx;
|
|
335
|
+
if (!ctx) return "accent";
|
|
336
|
+
const found = discoverAgents(ctx.cwd, "both", bundledAgentsDir).agents.find(a => a.name === name);
|
|
337
|
+
return found?.color ? AGENT_TO_THEME_COLOR[found.color] : "accent";
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
pi.registerTool({
|
|
341
|
+
name: "subagent",
|
|
342
|
+
label: "Subagent",
|
|
343
|
+
description: [
|
|
344
|
+
"Delegate tasks to specialized subagents with isolated context (SDK-based, minimal overhead).",
|
|
345
|
+
"Modes: single (agent + task), parallel (tasks array, max 8, 4 concurrent), chain (sequential with {previous}).",
|
|
346
|
+
`Default agent scope is "user" (from ${path.join(getAgentDir(), "agents")}).`,
|
|
347
|
+
`To enable project-local agents in ${CONFIG_DIR_NAME}/agents, set agentScope: "both" or "project".`,
|
|
348
|
+
].join(" "),
|
|
349
|
+
parameters: SubagentParams,
|
|
350
|
+
promptSnippet: "Delegate tasks to specialized sub-agents with automatic role-based model routing",
|
|
351
|
+
promptGuidelines: [
|
|
352
|
+
"Use subagent to delegate work that would flood the main context with search results or file contents.",
|
|
353
|
+
"Modes: single {agent, task}, parallel {tasks: [...]} (max 8, 4 concurrent), chain {chain: [...]} (sequential with {previous}).",
|
|
354
|
+
"Bundled agents: scout (fast recon), tester (verification), worker (implementation), general-purpose (fallback), planner (planning), reviewer (review).",
|
|
355
|
+
"Use /subagent to list all available agents or /subagent <name> for agent details.",
|
|
356
|
+
],
|
|
357
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
358
|
+
const agentScope: AgentScope = params.agentScope ?? "user";
|
|
359
|
+
const discovery = discoverAgents(ctx.cwd, agentScope, bundledAgentsDir);
|
|
360
|
+
const agents = discovery.agents;
|
|
361
|
+
|
|
362
|
+
// Trusted configuration — never from tool params.
|
|
363
|
+
const trusted = getTrustedConfig(ctx);
|
|
364
|
+
const confirmProjectAgents = !trusted.allowUnconfirmedProjectAgents;
|
|
365
|
+
const allowExternalCwd = trusted.allowExternalCwd;
|
|
366
|
+
|
|
367
|
+
// Resolve workspace root for cwd validation.
|
|
368
|
+
const workspaceRoot = ctx.cwd;
|
|
369
|
+
|
|
370
|
+
const hasChain = (params.chain?.length ?? 0) > 0;
|
|
371
|
+
const hasTasks = (params.tasks?.length ?? 0) > 0;
|
|
372
|
+
const hasSingle = Boolean(params.agent && params.task);
|
|
373
|
+
const modeCount = Number(hasChain) + Number(hasTasks) + Number(hasSingle);
|
|
374
|
+
|
|
375
|
+
const makeDetails =
|
|
376
|
+
(mode: "single" | "parallel" | "chain") =>
|
|
377
|
+
(results: SubAgentResult[]): SubagentDetails => ({
|
|
378
|
+
mode,
|
|
379
|
+
agentScope,
|
|
380
|
+
projectAgentsDir: discovery.projectAgentsDir,
|
|
381
|
+
results,
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Validate execution request before any processing.
|
|
385
|
+
const validationErrors = validateExecutionRequest({
|
|
386
|
+
agentName: params.agent,
|
|
387
|
+
task: params.task,
|
|
388
|
+
tasks: params.tasks,
|
|
389
|
+
chain: params.chain,
|
|
390
|
+
timeout: params.timeout,
|
|
391
|
+
});
|
|
392
|
+
if (validationErrors.length > 0) {
|
|
393
|
+
const errorMessages = validationErrors.map((e) => ` • ${e.field}: ${e.message}`).join("\n");
|
|
394
|
+
return {
|
|
395
|
+
content: [{ type: "text", text: `Invalid parameters:\n${errorMessages}` }],
|
|
396
|
+
details: makeDetails("single")([]),
|
|
397
|
+
isError: true,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Validate: exactly one mode
|
|
402
|
+
if (modeCount !== 1) {
|
|
403
|
+
const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
|
|
404
|
+
return {
|
|
405
|
+
content: [
|
|
406
|
+
{
|
|
407
|
+
type: "text",
|
|
408
|
+
text: [
|
|
409
|
+
"Invalid parameters. Provide exactly one mode:",
|
|
410
|
+
" single: { agent, task }",
|
|
411
|
+
" parallel: { tasks: [...] }",
|
|
412
|
+
" chain: { chain: [...] }",
|
|
413
|
+
`Available agents: ${available}`,
|
|
414
|
+
].join("\n"),
|
|
415
|
+
},
|
|
416
|
+
],
|
|
417
|
+
details: makeDetails("single")([]),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Handle project-local agent confirmation
|
|
422
|
+
// Security: confirmation policy comes from trusted config, never from tool params.
|
|
423
|
+
if (agentScope === "project" || agentScope === "both") {
|
|
424
|
+
const requestedAgentNames = new Set<string>();
|
|
425
|
+
if (params.chain) for (const s of params.chain) requestedAgentNames.add(s.agent);
|
|
426
|
+
if (params.tasks) for (const t of params.tasks) requestedAgentNames.add(t.agent);
|
|
427
|
+
if (params.agent) requestedAgentNames.add(params.agent);
|
|
428
|
+
|
|
429
|
+
const projectAgentsRequested = Array.from(requestedAgentNames)
|
|
430
|
+
.map((name) => agents.find((a) => a.name === name))
|
|
431
|
+
.filter((a): a is AgentConfig => a?.source === "project");
|
|
432
|
+
|
|
433
|
+
if (projectAgentsRequested.length > 0) {
|
|
434
|
+
if (confirmProjectAgents) {
|
|
435
|
+
if (ctx.hasUI) {
|
|
436
|
+
const names = projectAgentsRequested.map((a) => a.name).join(", ");
|
|
437
|
+
const dir = discovery.projectAgentsDir ?? "(unknown)";
|
|
438
|
+
const ok = await ctx.ui.confirm(
|
|
439
|
+
"Run project-local agents?",
|
|
440
|
+
`Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
|
|
441
|
+
);
|
|
442
|
+
if (!ok) {
|
|
443
|
+
return {
|
|
444
|
+
content: [{ type: "text", text: "Canceled: project-local agents not approved." }],
|
|
445
|
+
details: makeDetails(hasChain ? "chain" : hasTasks ? "parallel" : "single")([]),
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
} else {
|
|
449
|
+
// Fail closed in headless sessions.
|
|
450
|
+
return {
|
|
451
|
+
content: [{
|
|
452
|
+
type: "text",
|
|
453
|
+
text: "Project agents require explicit user approval. "
|
|
454
|
+
+ "Enable the trusted project-agent setting to use them in headless mode.",
|
|
455
|
+
}],
|
|
456
|
+
details: makeDetails(hasChain ? "chain" : hasTasks ? "parallel" : "single")([]),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// else: allowUnconfirmedProjectAgents is true — skip confirmation.
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const modelRegistry = ctx.modelRegistry;
|
|
465
|
+
const modelRuntime = (modelRegistry as any).runtime;
|
|
466
|
+
const authStorage = (modelRegistry as any).authStorage;
|
|
467
|
+
|
|
468
|
+
// Helper: resolve a safe child working directory.
|
|
469
|
+
function resolveChildCwd(childCwd: string | undefined): string {
|
|
470
|
+
const safe = resolveSafeCwd({ workspaceRoot, childCwd, allowExternalCwd });
|
|
471
|
+
if (safe.error) {
|
|
472
|
+
throw new Error(safe.error);
|
|
473
|
+
}
|
|
474
|
+
return safe.path;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Helper: validate and normalise tools for an agent.
|
|
478
|
+
function resolveChildTools(agentTools: string[] | undefined, sandbox?: string, readOnly?: boolean): string[] {
|
|
479
|
+
const defaultTools = ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
480
|
+
let rawTools = agentTools ?? defaultTools;
|
|
481
|
+
// sandbox overrides tools: silently strip mutation tools, not an error
|
|
482
|
+
if (sandbox === "read-only") {
|
|
483
|
+
rawTools = rawTools.filter(t => READ_ONLY_TOOLS.includes(t));
|
|
484
|
+
if (rawTools.length === 0) rawTools = [...READ_ONLY_TOOLS];
|
|
485
|
+
}
|
|
486
|
+
const effectiveReadOnly = readOnly || sandbox === "read-only";
|
|
487
|
+
const result = validateAgentTools({ tools: rawTools, readOnly: effectiveReadOnly });
|
|
488
|
+
if (result.errors.length > 0) {
|
|
489
|
+
throw new Error(`Tool validation errors: ${result.errors.join("; ")}`);
|
|
490
|
+
}
|
|
491
|
+
return result.tools;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Helper: normalise timeout.
|
|
495
|
+
function resolveChildTimeout(childTimeout: number | undefined, globalTimeout: number | undefined): number | undefined {
|
|
496
|
+
const effectiveTimeout = childTimeout ?? globalTimeout;
|
|
497
|
+
const result = normalizeTimeout({ requested: effectiveTimeout });
|
|
498
|
+
if (result.error) {
|
|
499
|
+
throw new Error(result.error);
|
|
500
|
+
}
|
|
501
|
+
return result.timeoutMs;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Helper: run a single agent via SDK with security validation
|
|
505
|
+
async function runOne(
|
|
506
|
+
agentName: string,
|
|
507
|
+
task: string,
|
|
508
|
+
cwd: string | undefined,
|
|
509
|
+
parentSignal?: AbortSignal,
|
|
510
|
+
timeoutMs?: number,
|
|
511
|
+
onProgress?: (partial: SubAgentResult) => void,
|
|
512
|
+
onActivity?: (progress: SubAgentProgress) => void,
|
|
513
|
+
heartbeatDetails?: () => SubagentDetails,
|
|
514
|
+
onHeartbeat?: () => void,
|
|
515
|
+
isReadOnly?: boolean,
|
|
516
|
+
): Promise<SubAgentResult> {
|
|
517
|
+
const agent = agents.find((a) => a.name === agentName);
|
|
518
|
+
|
|
519
|
+
if (!agent) {
|
|
520
|
+
const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
|
|
521
|
+
return {
|
|
522
|
+
agent: agentName,
|
|
523
|
+
task,
|
|
524
|
+
exitCode: 1,
|
|
525
|
+
status: "error",
|
|
526
|
+
stopReason: "error",
|
|
527
|
+
messages: [],
|
|
528
|
+
stderr: `Unknown agent: "${agentName}". Available: ${available}.`,
|
|
529
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
530
|
+
errorMessage: `Unknown agent: "${agentName}"`,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const resolved = await resolveModel(getModelCandidates(agent), ctx.model, ctx.modelRegistry);
|
|
535
|
+
if (!resolved.model) {
|
|
536
|
+
const tried = resolved.attempted.join(", ") || "none";
|
|
537
|
+
const parentInfo = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "none";
|
|
538
|
+
return {
|
|
539
|
+
agent: agentName,
|
|
540
|
+
task,
|
|
541
|
+
exitCode: 1,
|
|
542
|
+
status: "error",
|
|
543
|
+
stopReason: "error",
|
|
544
|
+
messages: [],
|
|
545
|
+
stderr: `Model not found for agent "${agentName}". Tried: ${tried}. Parent model: ${parentInfo}. Check agent definition and pi model configuration.`,
|
|
546
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
547
|
+
errorMessage: `No model resolved (tried: ${tried})`,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Security: validate tools, timeout, and cwd (wrapped in try/catch).
|
|
552
|
+
let tools: string[];
|
|
553
|
+
let effectiveTimeoutMs: number | undefined;
|
|
554
|
+
let safeCwd: string;
|
|
555
|
+
try {
|
|
556
|
+
tools = resolveChildTools(agent.tools, agent.sandbox, isReadOnly);
|
|
557
|
+
effectiveTimeoutMs = resolveChildTimeout(timeoutMs, params.timeout);
|
|
558
|
+
safeCwd = resolveChildCwd(cwd);
|
|
559
|
+
} catch (err: unknown) {
|
|
560
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
561
|
+
return {
|
|
562
|
+
agent: agentName,
|
|
563
|
+
task,
|
|
564
|
+
exitCode: 1,
|
|
565
|
+
status: "error",
|
|
566
|
+
stopReason: "error",
|
|
567
|
+
messages: [],
|
|
568
|
+
stderr: `Validation error: ${errorMsg}`,
|
|
569
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
570
|
+
errorMessage: errorMsg,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Retry loop: rate-limit model fallback
|
|
575
|
+
const candidates = getModelCandidates(agent);
|
|
576
|
+
const triedModels: string[] = [];
|
|
577
|
+
|
|
578
|
+
const stopHeartbeat = onUpdate ? startHeartbeat(() => {
|
|
579
|
+
onHeartbeat?.();
|
|
580
|
+
onUpdate({ content: [{ type: "text", text: `Subagent ${agentName} is still running…` }], details: heartbeatDetails?.() ?? makeDetails("single")([]) });
|
|
581
|
+
}) : undefined;
|
|
582
|
+
try {
|
|
583
|
+
const tryWithFallback = async (): Promise<SubAgentResult> => {
|
|
584
|
+
const remaining = candidates.filter(m => !triedModels.includes(m));
|
|
585
|
+
const isParentFallback = remaining.length === 0;
|
|
586
|
+
const fallbackResolved = await resolveModel(remaining, ctx.model, ctx.modelRegistry);
|
|
587
|
+
if (!fallbackResolved.model) {
|
|
588
|
+
return {
|
|
589
|
+
agent: agentName,
|
|
590
|
+
task,
|
|
591
|
+
exitCode: 1,
|
|
592
|
+
status: "error" as const,
|
|
593
|
+
stopReason: "error" as const,
|
|
594
|
+
messages: [],
|
|
595
|
+
stderr: [
|
|
596
|
+
`All models rate-limited or unavailable.`,
|
|
597
|
+
`Tried: ${triedModels.join(" → ") || "(none)"}.`,
|
|
598
|
+
`Remaining candidates: ${remaining.join(", ") || "none"}.`,
|
|
599
|
+
`Parent: ${ctx.model?.provider}/${ctx.model?.id}.`,
|
|
600
|
+
].join(" "),
|
|
601
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
602
|
+
errorMessage: `All models exhausted (tried: ${triedModels.join(" → ") || "none"})`,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
const triedName = `${fallbackResolved.model!.provider}/${fallbackResolved.model!.id}`;
|
|
606
|
+
if (triedModels.includes(triedName)) {
|
|
607
|
+
// Already tried this model (e.g., all candidates unavailable
|
|
608
|
+
// and parent fallback) — no further options.
|
|
609
|
+
return {
|
|
610
|
+
agent: agentName,
|
|
611
|
+
task,
|
|
612
|
+
exitCode: 1,
|
|
613
|
+
status: "error" as const,
|
|
614
|
+
stopReason: "error" as const,
|
|
615
|
+
messages: [],
|
|
616
|
+
stderr: [
|
|
617
|
+
`All available models exhausted.`,
|
|
618
|
+
`Tried: ${triedModels.join(" → ")}.`,
|
|
619
|
+
].join(" "),
|
|
620
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
621
|
+
errorMessage: `All available models exhausted (tried: ${triedModels.join(" → ")})`,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
triedModels.push(triedName);
|
|
625
|
+
// Also track the raw candidate name so candidates.filter() can
|
|
626
|
+
// exclude it even when the agent uses unqualified names.
|
|
627
|
+
// Avoid duplicating when candidate name is already qualified (matchedCandidate === triedName).
|
|
628
|
+
if (fallbackResolved.matchedCandidate && fallbackResolved.matchedCandidate !== triedName) {
|
|
629
|
+
triedModels.push(fallbackResolved.matchedCandidate);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const result = await runSubAgent({
|
|
633
|
+
cwd: safeCwd,
|
|
634
|
+
systemPrompt: params.instructions
|
|
635
|
+
? `${agent.systemPrompt}\n\n## Task Contract\n${params.instructions.slice(0, MAX_INSTRUCTIONS_LENGTH)}`
|
|
636
|
+
: agent.systemPrompt,
|
|
637
|
+
task,
|
|
638
|
+
tools,
|
|
639
|
+
model: fallbackResolved.model,
|
|
640
|
+
modelRuntime,
|
|
641
|
+
authStorage,
|
|
642
|
+
modelRegistry,
|
|
643
|
+
signal: parentSignal,
|
|
644
|
+
timeoutMs: effectiveTimeoutMs,
|
|
645
|
+
agentName,
|
|
646
|
+
thinkingLevel: agent.thinking,
|
|
647
|
+
onMessage: onProgress,
|
|
648
|
+
onProgress: onActivity,
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
if (result.errorMessage && isRateLimitError(result.errorMessage)) {
|
|
652
|
+
// If the model that just rate-limited was the parent fallback
|
|
653
|
+
// (no remaining candidates), stop — no further options.
|
|
654
|
+
if (isParentFallback) {
|
|
655
|
+
return {
|
|
656
|
+
agent: agentName,
|
|
657
|
+
task,
|
|
658
|
+
exitCode: 1,
|
|
659
|
+
status: "error" as const,
|
|
660
|
+
stopReason: "error" as const,
|
|
661
|
+
messages: [],
|
|
662
|
+
stderr: [
|
|
663
|
+
`All available models exhausted.`,
|
|
664
|
+
`Tried: ${triedModels.join(" → ")}.`,
|
|
665
|
+
].join(" "),
|
|
666
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
667
|
+
errorMessage: `All available models exhausted (tried: ${triedModels.join(" → ")})`,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
return tryWithFallback();
|
|
671
|
+
}
|
|
672
|
+
return result;
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
return tryWithFallback();
|
|
676
|
+
} finally {
|
|
677
|
+
stopHeartbeat?.();
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// --- Chain mode ---
|
|
682
|
+
if (params.chain && params.chain.length > 0) {
|
|
683
|
+
const results: SubAgentResult[] = [];
|
|
684
|
+
let previousOutput = "";
|
|
685
|
+
|
|
686
|
+
for (let i = 0; i < params.chain.length; i++) {
|
|
687
|
+
const step = params.chain[i];
|
|
688
|
+
const taskWithContext = step.task.replace(/\{previous\}/g, () => previousOutput);
|
|
689
|
+
|
|
690
|
+
const thread = threadStore.createThread({
|
|
691
|
+
agentName: step.agent,
|
|
692
|
+
task: taskWithContext,
|
|
693
|
+
mode: "chain-step",
|
|
694
|
+
toolCallId: _toolCallId,
|
|
695
|
+
color: agentToThemeColor(step.agent),
|
|
696
|
+
});
|
|
697
|
+
const result = await runOne(
|
|
698
|
+
step.agent, taskWithContext, step.cwd,
|
|
699
|
+
signal, step.timeout ?? params.timeout,
|
|
700
|
+
(partial) => threadStore.updateThread(thread.id, { result: partial }),
|
|
701
|
+
(progress) => threadStore.updateProgress(thread.id, progress),
|
|
702
|
+
() => makeDetails("chain")(results),
|
|
703
|
+
() => threadStore.refreshHeartbeat(thread.id),
|
|
704
|
+
);
|
|
705
|
+
threadStore.updateThread(thread.id, {
|
|
706
|
+
status: isFailedResult(result) ? (result.stopReason === "aborted" ? "aborted" : "failed") : "completed",
|
|
707
|
+
result,
|
|
708
|
+
});
|
|
709
|
+
results.push(result);
|
|
710
|
+
|
|
711
|
+
const isError = isFailedResult(result);
|
|
712
|
+
if (isError) {
|
|
713
|
+
const errorMsg = getResultOutput(result);
|
|
714
|
+
if (onUpdate) {
|
|
715
|
+
onUpdate({
|
|
716
|
+
content: [{ type: "text", text: errorMsg }],
|
|
717
|
+
details: makeDetails("chain")(results),
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
// Include successful previous step outputs in the error content
|
|
721
|
+
const prevCount = i;
|
|
722
|
+
let contentText = `Chain stopped at step ${i + 1} (${step.agent}): ${errorMsg}`;
|
|
723
|
+
if (prevCount > 0) {
|
|
724
|
+
const prevSummaries = results
|
|
725
|
+
.slice(0, prevCount)
|
|
726
|
+
.map((r, j) => {
|
|
727
|
+
const out = getResultOutput(r).slice(0, 500);
|
|
728
|
+
return `Step ${j + 1} (${r.agent}): ${out}`;
|
|
729
|
+
})
|
|
730
|
+
.join("\n");
|
|
731
|
+
contentText = `Chain stopped at step ${i + 1}/${params.chain.length}. ${prevCount} previous step(s) succeeded:\n\n${prevSummaries}\n\nError at step ${i + 1} (${step.agent}): ${errorMsg}`;
|
|
732
|
+
}
|
|
733
|
+
return {
|
|
734
|
+
content: [{ type: "text", text: contentText }],
|
|
735
|
+
details: makeDetails("chain")(results),
|
|
736
|
+
isError: true,
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
previousOutput = getFinalOutput(result.messages);
|
|
741
|
+
|
|
742
|
+
if (onUpdate) {
|
|
743
|
+
onUpdate({
|
|
744
|
+
content: [{ type: "text", text: getFinalOutput(result.messages) || "(no output)" }],
|
|
745
|
+
details: makeDetails("chain")(results),
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const last = results[results.length - 1];
|
|
751
|
+
return {
|
|
752
|
+
content: [
|
|
753
|
+
{ type: "text", text: getFinalOutput(last.messages) || "(no output)" },
|
|
754
|
+
],
|
|
755
|
+
details: makeDetails("chain")(results),
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// --- Parallel mode ---
|
|
760
|
+
if (params.tasks && params.tasks.length > 0) {
|
|
761
|
+
const abortOnFailure = params.abortOnFailure ?? false;
|
|
762
|
+
const parallelController = new AbortController();
|
|
763
|
+
let abortCause: "parent" | "sibling" | "timeout" | undefined;
|
|
764
|
+
let cleanupParentSignal: (() => void) | undefined;
|
|
765
|
+
|
|
766
|
+
// Link parent abort into parallelController so queued tasks see aborted state
|
|
767
|
+
if (signal) {
|
|
768
|
+
if (signal.aborted) {
|
|
769
|
+
abortCause = "parent";
|
|
770
|
+
parallelController.abort();
|
|
771
|
+
} else {
|
|
772
|
+
const onParentAbort = () => {
|
|
773
|
+
if (!abortCause) abortCause = "parent";
|
|
774
|
+
parallelController.abort();
|
|
775
|
+
};
|
|
776
|
+
signal.addEventListener("abort", onParentAbort, { once: true });
|
|
777
|
+
cleanupParentSignal = () => signal.removeEventListener("abort", onParentAbort);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Wrap all remaining setup + execution so cleanupParentSignal always runs.
|
|
782
|
+
try {
|
|
783
|
+
// Pre-create threads for all parallel tasks
|
|
784
|
+
const parallelThreads = params.tasks.map((t) =>
|
|
785
|
+
threadStore.createThread({
|
|
786
|
+
agentName: t.agent,
|
|
787
|
+
task: t.task,
|
|
788
|
+
mode: "parallel-task",
|
|
789
|
+
toolCallId: _toolCallId,
|
|
790
|
+
color: agentToThemeColor(t.agent),
|
|
791
|
+
}),
|
|
792
|
+
);
|
|
793
|
+
|
|
794
|
+
const allResults: SubAgentResult[] = new Array(params.tasks.length);
|
|
795
|
+
// Initialize placeholder results for streaming
|
|
796
|
+
for (let i = 0; i < params.tasks.length; i++) {
|
|
797
|
+
allResults[i] = {
|
|
798
|
+
agent: params.tasks[i].agent,
|
|
799
|
+
task: params.tasks[i].task,
|
|
800
|
+
exitCode: -1,
|
|
801
|
+
messages: [],
|
|
802
|
+
stderr: "",
|
|
803
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const emitParallelUpdate = () => {
|
|
808
|
+
if (onUpdate) {
|
|
809
|
+
const running = allResults.filter((r) => r.exitCode === -1).length;
|
|
810
|
+
const done = allResults.filter((r) => r.exitCode !== -1).length;
|
|
811
|
+
onUpdate({
|
|
812
|
+
content: [
|
|
813
|
+
{
|
|
814
|
+
type: "text",
|
|
815
|
+
text: `Parallel: ${done}/${allResults.length} done, ${running} running...`,
|
|
816
|
+
},
|
|
817
|
+
],
|
|
818
|
+
details: makeDetails("parallel")([...allResults]),
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
const results = await mapWithConcurrencyLimit(
|
|
824
|
+
params.tasks,
|
|
825
|
+
MAX_CONCURRENCY,
|
|
826
|
+
async (t, index) => {
|
|
827
|
+
// Skip if already aborted by sibling failure or parent abort
|
|
828
|
+
if (parallelController.signal.aborted) {
|
|
829
|
+
const skippedResult: SubAgentResult = {
|
|
830
|
+
agent: t.agent,
|
|
831
|
+
task: t.task,
|
|
832
|
+
exitCode: 1,
|
|
833
|
+
status: "error",
|
|
834
|
+
messages: [],
|
|
835
|
+
stderr: "",
|
|
836
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
837
|
+
stopReason: "aborted",
|
|
838
|
+
errorMessage:
|
|
839
|
+
abortCause === "sibling"
|
|
840
|
+
? "Cancelled: sibling task failed"
|
|
841
|
+
: abortCause === "timeout"
|
|
842
|
+
? "Cancelled: sibling task timed out"
|
|
843
|
+
: "Cancelled: parent operation aborted",
|
|
844
|
+
};
|
|
845
|
+
allResults[index] = skippedResult;
|
|
846
|
+
threadStore.updateThread(parallelThreads[index].id, {
|
|
847
|
+
status: "aborted",
|
|
848
|
+
result: skippedResult,
|
|
849
|
+
});
|
|
850
|
+
emitParallelUpdate();
|
|
851
|
+
return skippedResult;
|
|
852
|
+
}
|
|
853
|
+
const result = await runOne(
|
|
854
|
+
t.agent, t.task, t.cwd,
|
|
855
|
+
parallelController.signal, t.timeout ?? params.timeout,
|
|
856
|
+
(partial) => threadStore.updateThread(parallelThreads[index].id, { result: partial }),
|
|
857
|
+
(progress) => threadStore.updateProgress(parallelThreads[index].id, progress),
|
|
858
|
+
() => makeDetails("parallel")([...allResults]),
|
|
859
|
+
() => threadStore.refreshHeartbeat(parallelThreads[index].id),
|
|
860
|
+
);
|
|
861
|
+
allResults[index] = result;
|
|
862
|
+
threadStore.updateThread(parallelThreads[index].id, {
|
|
863
|
+
status: isFailedResult(result) ? (result.stopReason === "aborted" ? "aborted" : "failed") : "completed",
|
|
864
|
+
result,
|
|
865
|
+
});
|
|
866
|
+
// Early-abort: if this task failed and abortOnFailure is set
|
|
867
|
+
if (abortOnFailure && isFailedResult(result) && !abortCause) {
|
|
868
|
+
abortCause = result.stopReason === "timeout" ? "timeout" : "sibling";
|
|
869
|
+
parallelController.abort();
|
|
870
|
+
}
|
|
871
|
+
emitParallelUpdate();
|
|
872
|
+
return result;
|
|
873
|
+
},
|
|
874
|
+
);
|
|
875
|
+
|
|
876
|
+
const successCount = results.filter((r) => !isFailedResult(r)).length;
|
|
877
|
+
const cancelCount = results.filter((r) => r.stopReason === "aborted" && r.errorMessage?.includes("Cancelled")).length;
|
|
878
|
+
const summaries = results.map((r) => {
|
|
879
|
+
const output = truncateParallelOutput(getResultOutput(r));
|
|
880
|
+
const status = isFailedResult(r)
|
|
881
|
+
? `failed${r.stopReason ? ` (${r.stopReason})` : ""}`
|
|
882
|
+
: "completed";
|
|
883
|
+
return `### [${r.agent}] ${status}\n\n${output}`;
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
let headerText = `Parallel: ${successCount}/${results.length} succeeded`;
|
|
887
|
+
if (cancelCount > 0) headerText += ` (${cancelCount} cancelled)`;
|
|
888
|
+
return {
|
|
889
|
+
content: [
|
|
890
|
+
{
|
|
891
|
+
type: "text",
|
|
892
|
+
text: `${headerText}\n\n${summaries.join("\n\n---\n\n")}`,
|
|
893
|
+
},
|
|
894
|
+
],
|
|
895
|
+
details: makeDetails("parallel")(results),
|
|
896
|
+
};
|
|
897
|
+
} finally {
|
|
898
|
+
cleanupParentSignal?.();
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// --- Single mode ---
|
|
903
|
+
if (params.agent && params.task) {
|
|
904
|
+
const thread = threadStore.createThread({
|
|
905
|
+
agentName: params.agent,
|
|
906
|
+
task: params.task,
|
|
907
|
+
mode: "single",
|
|
908
|
+
toolCallId: _toolCallId,
|
|
909
|
+
color: agentToThemeColor(params.agent),
|
|
910
|
+
});
|
|
911
|
+
const result = await runOne(
|
|
912
|
+
params.agent, params.task, params.cwd,
|
|
913
|
+
signal, params.timeout,
|
|
914
|
+
(partial) => threadStore.updateThread(thread.id, { result: partial }),
|
|
915
|
+
(progress) => threadStore.updateProgress(thread.id, progress),
|
|
916
|
+
() => makeDetails("single")([]),
|
|
917
|
+
() => threadStore.refreshHeartbeat(thread.id),
|
|
918
|
+
);
|
|
919
|
+
threadStore.updateThread(thread.id, {
|
|
920
|
+
status: isFailedResult(result) ? (result.stopReason === "aborted" ? "aborted" : "failed") : "completed",
|
|
921
|
+
result,
|
|
922
|
+
});
|
|
923
|
+
const isError = isFailedResult(result);
|
|
924
|
+
|
|
925
|
+
if (onUpdate) {
|
|
926
|
+
onUpdate({
|
|
927
|
+
content: [
|
|
928
|
+
{ type: "text", text: getFinalOutput(result.messages) || "(running...)" },
|
|
929
|
+
],
|
|
930
|
+
details: makeDetails("single")([result]),
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
if (isError) {
|
|
935
|
+
const errorMsg = getResultOutput(result);
|
|
936
|
+
return {
|
|
937
|
+
content: [
|
|
938
|
+
{
|
|
939
|
+
type: "text",
|
|
940
|
+
text: `Agent ${result.stopReason || "failed"}: ${errorMsg}`,
|
|
941
|
+
},
|
|
942
|
+
],
|
|
943
|
+
details: makeDetails("single")([result]),
|
|
944
|
+
isError: true,
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
return {
|
|
949
|
+
content: [
|
|
950
|
+
{ type: "text", text: getFinalOutput(result.messages) || "(no output)" },
|
|
951
|
+
],
|
|
952
|
+
details: makeDetails("single")([result]),
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Exhaustiveness check: the modeCount === 1 validation above ensures
|
|
957
|
+
// at least one of the three branches is taken, but TS cannot prove it.
|
|
958
|
+
throw new Error("unreachable");
|
|
959
|
+
},
|
|
960
|
+
|
|
961
|
+
// ------------------------------------------------------------------
|
|
962
|
+
// TUI rendering
|
|
963
|
+
// ------------------------------------------------------------------
|
|
964
|
+
|
|
965
|
+
renderCall(args, theme, _context) {
|
|
966
|
+
const scope: AgentScope = args.agentScope ?? "user";
|
|
967
|
+
const fg = theme.fg.bind(theme);
|
|
968
|
+
|
|
969
|
+
// Chain
|
|
970
|
+
if (args.chain && args.chain.length > 0) {
|
|
971
|
+
let text =
|
|
972
|
+
fg("toolTitle", theme.bold("subagent ")) +
|
|
973
|
+
fg("accent", `chain (${args.chain.length} steps)`) +
|
|
974
|
+
fg("muted", ` [${scope}]`);
|
|
975
|
+
for (let i = 0; i < Math.min(args.chain.length, 3); i++) {
|
|
976
|
+
const step = args.chain[i];
|
|
977
|
+
const cleanTask = step.task.replace(/\{previous\}/g, "").trim();
|
|
978
|
+
const preview = cleanTask.length > 40 ? `${cleanTask.slice(0, 40)}...` : cleanTask;
|
|
979
|
+
text +=
|
|
980
|
+
"\n " +
|
|
981
|
+
fg("muted", `${i + 1}.`) +
|
|
982
|
+
" " +
|
|
983
|
+
fg(resolveAgentColor(step.agent), step.agent) +
|
|
984
|
+
fg("dim", ` ${preview}`);
|
|
985
|
+
}
|
|
986
|
+
if (args.chain.length > 3)
|
|
987
|
+
text += `\n ${fg("muted", `... +${args.chain.length - 3} more`)}`;
|
|
988
|
+
return new Text(text, 0, 0);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// Parallel
|
|
992
|
+
if (args.tasks && args.tasks.length > 0) {
|
|
993
|
+
let text =
|
|
994
|
+
fg("toolTitle", theme.bold("subagent ")) +
|
|
995
|
+
fg("accent", `parallel (${args.tasks.length} tasks)`) +
|
|
996
|
+
fg("muted", ` [${scope}]`);
|
|
997
|
+
for (const t of args.tasks.slice(0, 3)) {
|
|
998
|
+
const preview = t.task.length > 40 ? `${t.task.slice(0, 40)}...` : t.task;
|
|
999
|
+
text += `\n ${fg(resolveAgentColor(t.agent), t.agent)}${fg("dim", ` ${preview}`)}`;
|
|
1000
|
+
}
|
|
1001
|
+
if (args.tasks.length > 3)
|
|
1002
|
+
text += `\n ${fg("muted", `... +${args.tasks.length - 3} more`)}`;
|
|
1003
|
+
return new Text(text, 0, 0);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Single
|
|
1007
|
+
const agentName = args.agent || "...";
|
|
1008
|
+
const preview = args.task
|
|
1009
|
+
? args.task.length > 60
|
|
1010
|
+
? `${args.task.slice(0, 60)}...`
|
|
1011
|
+
: args.task
|
|
1012
|
+
: "...";
|
|
1013
|
+
let text =
|
|
1014
|
+
fg("toolTitle", theme.bold("subagent ")) +
|
|
1015
|
+
fg(resolveAgentColor(agentName), agentName) +
|
|
1016
|
+
fg("muted", ` [${scope}]`);
|
|
1017
|
+
text += `\n ${fg("dim", preview)}`;
|
|
1018
|
+
return new Text(text, 0, 0);
|
|
1019
|
+
},
|
|
1020
|
+
|
|
1021
|
+
renderResult(result, { expanded }, theme, _context) {
|
|
1022
|
+
const details = result.details as SubagentDetails | undefined;
|
|
1023
|
+
if (!details || details.results.length === 0) {
|
|
1024
|
+
const text = result.content[0];
|
|
1025
|
+
return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const fg = theme.fg.bind(theme);
|
|
1029
|
+
const mdTheme = getMarkdownTheme();
|
|
1030
|
+
|
|
1031
|
+
// --- Single ---
|
|
1032
|
+
if (details.mode === "single" && details.results.length === 1) {
|
|
1033
|
+
const r = details.results[0];
|
|
1034
|
+
return renderSingleResult(r, expanded, theme, resolveAgentColor(r.agent));
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// --- Chain ---
|
|
1038
|
+
if (details.mode === "chain") {
|
|
1039
|
+
const successCount = details.results.filter((r) => !isFailedResult(r)).length;
|
|
1040
|
+
const icon =
|
|
1041
|
+
successCount === details.results.length
|
|
1042
|
+
? fg("success", "✓")
|
|
1043
|
+
: fg("error", "✗");
|
|
1044
|
+
|
|
1045
|
+
if (expanded) {
|
|
1046
|
+
const container = new Container();
|
|
1047
|
+
container.addChild(
|
|
1048
|
+
new Text(
|
|
1049
|
+
icon +
|
|
1050
|
+
" " +
|
|
1051
|
+
fg("toolTitle", theme.bold("chain ")) +
|
|
1052
|
+
fg("accent", `${successCount}/${details.results.length} steps`),
|
|
1053
|
+
0,
|
|
1054
|
+
0,
|
|
1055
|
+
),
|
|
1056
|
+
);
|
|
1057
|
+
for (const r of details.results) {
|
|
1058
|
+
container.addChild(new Spacer(1));
|
|
1059
|
+
const stepIcon = isFailedResult(r) ? fg("error", "✗") : fg("success", "✓");
|
|
1060
|
+
container.addChild(
|
|
1061
|
+
new Text(
|
|
1062
|
+
fg("muted", `─── Step ${r.exitCode !== -1 ? "" : "?"}: `) +
|
|
1063
|
+
fg(resolveAgentColor(r.agent), r.agent) +
|
|
1064
|
+
` ${stepIcon}`,
|
|
1065
|
+
0,
|
|
1066
|
+
0,
|
|
1067
|
+
),
|
|
1068
|
+
);
|
|
1069
|
+
if (r.errorMessage) {
|
|
1070
|
+
container.addChild(
|
|
1071
|
+
new Text(fg("error", `Error: ${r.errorMessage}`), 0, 0),
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
const finalOutput = getResultOutput(r);
|
|
1075
|
+
if (finalOutput) {
|
|
1076
|
+
container.addChild(new Spacer(1));
|
|
1077
|
+
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
1078
|
+
}
|
|
1079
|
+
const usageStr = formatUsageStats(r.usage, r.model);
|
|
1080
|
+
if (usageStr)
|
|
1081
|
+
container.addChild(new Text(fg("dim", usageStr), 0, 0));
|
|
1082
|
+
}
|
|
1083
|
+
const totalUsage = formatUsageStats(aggregateUsage(details.results));
|
|
1084
|
+
if (totalUsage) {
|
|
1085
|
+
container.addChild(new Spacer(1));
|
|
1086
|
+
container.addChild(new Text(fg("dim", `Total: ${totalUsage}`), 0, 0));
|
|
1087
|
+
}
|
|
1088
|
+
return container;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
let text =
|
|
1092
|
+
icon +
|
|
1093
|
+
" " +
|
|
1094
|
+
fg("toolTitle", theme.bold("chain ")) +
|
|
1095
|
+
fg("accent", `${successCount}/${details.results.length} steps`);
|
|
1096
|
+
for (const r of details.results) {
|
|
1097
|
+
const stepIcon = isFailedResult(r) ? fg("error", "✗") : fg("success", "✓");
|
|
1098
|
+
const color = resolveAgentColor(r.agent);
|
|
1099
|
+
text += `\n ${stepIcon} ${fg(color, r.agent)}`;
|
|
1100
|
+
}
|
|
1101
|
+
const totalUsage = formatUsageStats(aggregateUsage(details.results));
|
|
1102
|
+
if (totalUsage) text += `\n${fg("dim", totalUsage)}`;
|
|
1103
|
+
text += `\n${fg("muted", "(Ctrl+O to expand)")}`;
|
|
1104
|
+
return new Text(text, 0, 0);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// --- Parallel ---
|
|
1108
|
+
if (details.mode === "parallel") {
|
|
1109
|
+
const running = details.results.filter((r) => r.exitCode === -1).length;
|
|
1110
|
+
const successCount = details.results.filter(
|
|
1111
|
+
(r) => r.exitCode !== -1 && !isFailedResult(r),
|
|
1112
|
+
).length;
|
|
1113
|
+
const failCount = details.results.filter(
|
|
1114
|
+
(r) => r.exitCode !== -1 && isFailedResult(r),
|
|
1115
|
+
).length;
|
|
1116
|
+
const isRunning = running > 0;
|
|
1117
|
+
const icon = isRunning
|
|
1118
|
+
? fg("warning", "⏳")
|
|
1119
|
+
: failCount > 0
|
|
1120
|
+
? fg("warning", "◐")
|
|
1121
|
+
: fg("success", "✓");
|
|
1122
|
+
const status = isRunning
|
|
1123
|
+
? `${successCount + failCount}/${details.results.length} done, ${running} running`
|
|
1124
|
+
: `${successCount}/${details.results.length} tasks`;
|
|
1125
|
+
|
|
1126
|
+
if (expanded && !isRunning) {
|
|
1127
|
+
const container = new Container();
|
|
1128
|
+
container.addChild(
|
|
1129
|
+
new Text(
|
|
1130
|
+
`${icon} ${fg("toolTitle", theme.bold("parallel "))}${fg("accent", status)}`,
|
|
1131
|
+
0,
|
|
1132
|
+
0,
|
|
1133
|
+
),
|
|
1134
|
+
);
|
|
1135
|
+
for (const r of details.results) {
|
|
1136
|
+
container.addChild(new Spacer(1));
|
|
1137
|
+
const taskIcon = isFailedResult(r)
|
|
1138
|
+
? fg("error", "✗")
|
|
1139
|
+
: fg("success", "✓");
|
|
1140
|
+
container.addChild(
|
|
1141
|
+
new Text(
|
|
1142
|
+
fg("muted", "─── ") + fg(resolveAgentColor(r.agent), r.agent) + ` ${taskIcon}`,
|
|
1143
|
+
0,
|
|
1144
|
+
0,
|
|
1145
|
+
),
|
|
1146
|
+
);
|
|
1147
|
+
container.addChild(
|
|
1148
|
+
new Text(fg("muted", "Task: ") + fg("dim", r.task), 0, 0),
|
|
1149
|
+
);
|
|
1150
|
+
if (r.errorMessage) {
|
|
1151
|
+
container.addChild(
|
|
1152
|
+
new Text(fg("error", `Error: ${r.errorMessage}`), 0, 0),
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
const finalOutput = getResultOutput(r);
|
|
1156
|
+
if (finalOutput) {
|
|
1157
|
+
container.addChild(new Spacer(1));
|
|
1158
|
+
container.addChild(
|
|
1159
|
+
new Markdown(finalOutput.trim(), 0, 0, mdTheme),
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
const taskUsage = formatUsageStats(r.usage, r.model);
|
|
1163
|
+
if (taskUsage)
|
|
1164
|
+
container.addChild(new Text(fg("dim", taskUsage), 0, 0));
|
|
1165
|
+
}
|
|
1166
|
+
const totalUsage = formatUsageStats(aggregateUsage(details.results));
|
|
1167
|
+
if (totalUsage) {
|
|
1168
|
+
container.addChild(new Spacer(1));
|
|
1169
|
+
container.addChild(new Text(fg("dim", `Total: ${totalUsage}`), 0, 0));
|
|
1170
|
+
}
|
|
1171
|
+
return container;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
let text = `${icon} ${fg("toolTitle", theme.bold("parallel "))}${fg("accent", status)}`;
|
|
1175
|
+
for (const r of details.results) {
|
|
1176
|
+
const taskIcon =
|
|
1177
|
+
r.exitCode === -1
|
|
1178
|
+
? fg("warning", "⏳")
|
|
1179
|
+
: isFailedResult(r)
|
|
1180
|
+
? fg("error", "✗")
|
|
1181
|
+
: fg("success", "✓");
|
|
1182
|
+
text += `\n ${taskIcon} ${fg(resolveAgentColor(r.agent), r.agent)}`;
|
|
1183
|
+
}
|
|
1184
|
+
if (!isRunning) {
|
|
1185
|
+
const totalUsage = formatUsageStats(aggregateUsage(details.results));
|
|
1186
|
+
if (totalUsage) text += `\n${fg("dim", totalUsage)}`;
|
|
1187
|
+
}
|
|
1188
|
+
if (!expanded) text += `\n${fg("muted", "(Ctrl+O to expand)")}`;
|
|
1189
|
+
return new Text(text, 0, 0);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
const fallback = result.content[0];
|
|
1193
|
+
return new Text(fallback?.type === "text" ? fallback.text : "(no output)", 0, 0);
|
|
1194
|
+
},
|
|
1195
|
+
});
|
|
1196
|
+
// /agent command — switch between subagent threads.
|
|
1197
|
+
// When a thread is selected, the viewer replaces the main TUI (not overlay).
|
|
1198
|
+
pi.registerCommand("agent", {
|
|
1199
|
+
description: "Switch to a subagent thread to view its work in isolation",
|
|
1200
|
+
handler: async (_args, ctx) => {
|
|
1201
|
+
// Show picker overlay
|
|
1202
|
+
const selectedId = await showAgentPicker(ctx, buildPickerItems(threadStore.getAllThreads()));
|
|
1203
|
+
if (!selectedId) return; // Cancelled — stay in current view
|
|
1204
|
+
|
|
1205
|
+
// Main selected — close viewer if active, return to conversation
|
|
1206
|
+
if (selectedId === "__main__") {
|
|
1207
|
+
if (activeViewerDone) {
|
|
1208
|
+
activeViewerDone();
|
|
1209
|
+
activeViewerDone = null;
|
|
1210
|
+
}
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// Close existing viewer (if any) before opening new one
|
|
1215
|
+
if (activeViewerDone) {
|
|
1216
|
+
activeViewerDone();
|
|
1217
|
+
activeViewerDone = null;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// Show thread viewer (re-resolve against current store)
|
|
1221
|
+
const freshThreads = threadStore.getAllThreads();
|
|
1222
|
+
const idx = freshThreads.findIndex((t) => t.id === selectedId);
|
|
1223
|
+
if (idx === -1) {
|
|
1224
|
+
ctx.ui.notify("Selected subagent thread no longer exists.", "warning");
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
await showThreadViewer(ctx, freshThreads, idx);
|
|
1229
|
+
},
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
// ---------------------------------------------------------------------------
|
|
1233
|
+
// Module-level viewer state (so /agent can close an active viewer)
|
|
1234
|
+
// ---------------------------------------------------------------------------
|
|
1235
|
+
let activeViewerDone: (() => void) | null = null;
|
|
1236
|
+
|
|
1237
|
+
// ---------------------------------------------------------------------------
|
|
1238
|
+
// Picker helpers (shared between /agent handler and Ctrl+P in viewer)
|
|
1239
|
+
// ---------------------------------------------------------------------------
|
|
1240
|
+
|
|
1241
|
+
interface PickerItem { value: string; label: string; description: string }
|
|
1242
|
+
|
|
1243
|
+
function buildPickerItems(threads: SubagentThread[]): PickerItem[] {
|
|
1244
|
+
const items: PickerItem[] = [
|
|
1245
|
+
{ value: "__main__", label: "Main [default]", description: "(current)" },
|
|
1246
|
+
];
|
|
1247
|
+
for (const t of threads) {
|
|
1248
|
+
let statusIcon: string;
|
|
1249
|
+
switch (t.status) {
|
|
1250
|
+
case "running": statusIcon = "⏳"; break;
|
|
1251
|
+
case "completed": statusIcon = "✓"; break;
|
|
1252
|
+
case "failed": statusIcon = "✗"; break;
|
|
1253
|
+
case "aborted": statusIcon = "✗"; break;
|
|
1254
|
+
}
|
|
1255
|
+
let modeTag = "";
|
|
1256
|
+
if (t.mode === "parallel-task") modeTag = " [parallel]";
|
|
1257
|
+
else if (t.mode === "chain-step") modeTag = " [chain]";
|
|
1258
|
+
const label = `${statusIcon} ${t.agentName}${modeTag}`;
|
|
1259
|
+
const desc = t.task.length > 60 ? `${t.task.slice(0, 57)}...` : t.task;
|
|
1260
|
+
items.push({ value: t.id, label, description: desc });
|
|
1261
|
+
}
|
|
1262
|
+
return items;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
async function showAgentPicker(
|
|
1266
|
+
ctx: { ui: { custom: <T>(factory: any, opts?: any) => Promise<T> } },
|
|
1267
|
+
items: PickerItem[],
|
|
1268
|
+
): Promise<string | null> {
|
|
1269
|
+
return ctx.ui.custom<string | null>((tui: any, theme: any, _kb: any, done: (value: string | null) => void) => {
|
|
1270
|
+
const container = new Container();
|
|
1271
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
1272
|
+
container.addChild(new Text(theme.fg("accent", theme.bold("Subagents")), 1, 0));
|
|
1273
|
+
container.addChild(new Text(theme.fg("dim", "⌥ + ← previous, ⌥ + → next."), 1, 0));
|
|
1274
|
+
|
|
1275
|
+
const selectList = new SelectList(
|
|
1276
|
+
items.map((it) => ({ value: it.value, label: it.label, description: it.description })),
|
|
1277
|
+
Math.min(items.length + 2, 15),
|
|
1278
|
+
{
|
|
1279
|
+
selectedPrefix: (t: string) => theme.fg("accent", t),
|
|
1280
|
+
selectedText: (t: string) => theme.fg("accent", t),
|
|
1281
|
+
description: (t: string) => theme.fg("muted", t),
|
|
1282
|
+
scrollInfo: (t: string) => theme.fg("dim", t),
|
|
1283
|
+
noMatch: (t: string) => theme.fg("warning", t),
|
|
1284
|
+
},
|
|
1285
|
+
);
|
|
1286
|
+
selectList.onSelect = (item) => done(item.value);
|
|
1287
|
+
selectList.onCancel = () => done(null);
|
|
1288
|
+
container.addChild(selectList);
|
|
1289
|
+
|
|
1290
|
+
container.addChild(new Text(
|
|
1291
|
+
`${theme.fg("dim", "↑↓ navigate · enter select · esc back")}`,
|
|
1292
|
+
1, 0,
|
|
1293
|
+
));
|
|
1294
|
+
|
|
1295
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
1296
|
+
|
|
1297
|
+
return {
|
|
1298
|
+
render: (w: number) => container.render(w),
|
|
1299
|
+
invalidate: () => container.invalidate(),
|
|
1300
|
+
handleInput: (data: string) => { selectList.handleInput(data); tui.requestRender(); },
|
|
1301
|
+
};
|
|
1302
|
+
}, { overlay: true });
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// Helper: show thread viewer as overlay so editor remains visible.
|
|
1306
|
+
// Uses dynamic thread list + store subscriptions for live progress.
|
|
1307
|
+
// Ctrl+P opens picker overlay to jump to any thread.
|
|
1308
|
+
async function showThreadViewer(
|
|
1309
|
+
ctx: { ui: { custom: <T>(factory: any, opts?: any) => Promise<T> } },
|
|
1310
|
+
_threads: SubagentThread[],
|
|
1311
|
+
startIndex: number,
|
|
1312
|
+
): Promise<void> {
|
|
1313
|
+
let currentIndex = startIndex;
|
|
1314
|
+
|
|
1315
|
+
// Resolve thread list dynamically
|
|
1316
|
+
const getThreads = () => threadStore.getAllThreads();
|
|
1317
|
+
|
|
1318
|
+
// Overlay mode: viewer appears above editor, Esc dismisses
|
|
1319
|
+
await ctx.ui.custom<void>((tui: any, theme: any, _kb: any, done: () => void) => {
|
|
1320
|
+
let unsubscribe: (() => void) | undefined;
|
|
1321
|
+
let closed = false;
|
|
1322
|
+
|
|
1323
|
+
const cleanup = () => {
|
|
1324
|
+
if (unsubscribe) {
|
|
1325
|
+
unsubscribe();
|
|
1326
|
+
unsubscribe = undefined;
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
|
|
1330
|
+
const close = () => {
|
|
1331
|
+
if (closed) return;
|
|
1332
|
+
closed = true;
|
|
1333
|
+
cleanup();
|
|
1334
|
+
activeViewerDone = null;
|
|
1335
|
+
done();
|
|
1336
|
+
};
|
|
1337
|
+
|
|
1338
|
+
// Track this viewer so /agent can close it before opening a new one
|
|
1339
|
+
activeViewerDone = close;
|
|
1340
|
+
|
|
1341
|
+
function makeCallbacks(): ThreadViewerCallbacks {
|
|
1342
|
+
const list = getThreads();
|
|
1343
|
+
return {
|
|
1344
|
+
onClose: close,
|
|
1345
|
+
onPrev: () => {
|
|
1346
|
+
const current = getThreads();
|
|
1347
|
+
if (currentIndex > 0) {
|
|
1348
|
+
currentIndex--;
|
|
1349
|
+
viewer.setThread(current[currentIndex], makeCallbacks());
|
|
1350
|
+
tui.requestRender();
|
|
1351
|
+
}
|
|
1352
|
+
},
|
|
1353
|
+
onNext: () => {
|
|
1354
|
+
const current = getThreads();
|
|
1355
|
+
if (currentIndex < current.length - 1) {
|
|
1356
|
+
currentIndex++;
|
|
1357
|
+
viewer.setThread(current[currentIndex], makeCallbacks());
|
|
1358
|
+
tui.requestRender();
|
|
1359
|
+
}
|
|
1360
|
+
},
|
|
1361
|
+
hasPrev: currentIndex > 0,
|
|
1362
|
+
hasNext: currentIndex < list.length - 1,
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
const list = getThreads();
|
|
1367
|
+
if (list.length === 0 || currentIndex < 0 || currentIndex >= list.length) {
|
|
1368
|
+
close();
|
|
1369
|
+
return {
|
|
1370
|
+
render: (_w: number) => [],
|
|
1371
|
+
invalidate: () => {},
|
|
1372
|
+
handleInput: (_data: string) => {},
|
|
1373
|
+
dispose: () => {
|
|
1374
|
+
cleanup();
|
|
1375
|
+
if (activeViewerDone === close) activeViewerDone = null;
|
|
1376
|
+
closed = true;
|
|
1377
|
+
},
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
const viewer = new ThreadViewer(list[currentIndex], makeCallbacks(), theme);
|
|
1382
|
+
let pickerOpen = false;
|
|
1383
|
+
|
|
1384
|
+
// Subscribe to thread store for live updates (after viewer is created)
|
|
1385
|
+
unsubscribe = threadStore.subscribe(() => {
|
|
1386
|
+
const current = getThreads();
|
|
1387
|
+
if (current.length === 0) {
|
|
1388
|
+
close();
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
currentIndex = Math.min(currentIndex, current.length - 1);
|
|
1392
|
+
viewer.setThread(current[currentIndex], makeCallbacks());
|
|
1393
|
+
tui.requestRender();
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
return {
|
|
1397
|
+
render: (w: number) => viewer.render(w),
|
|
1398
|
+
invalidate: () => viewer.invalidate(),
|
|
1399
|
+
handleInput: (data: string) => {
|
|
1400
|
+
// Ctrl+P opens the picker to jump between threads
|
|
1401
|
+
if (data === "\x10") {
|
|
1402
|
+
if (!pickerOpen) {
|
|
1403
|
+
pickerOpen = true;
|
|
1404
|
+
openThreadPicker().finally(() => { pickerOpen = false; });
|
|
1405
|
+
}
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
viewer.handleInput(data);
|
|
1409
|
+
tui.requestRender();
|
|
1410
|
+
},
|
|
1411
|
+
dispose: () => {
|
|
1412
|
+
cleanup();
|
|
1413
|
+
if (activeViewerDone === close) activeViewerDone = null;
|
|
1414
|
+
closed = true;
|
|
1415
|
+
},
|
|
1416
|
+
};
|
|
1417
|
+
|
|
1418
|
+
// Opens picker overlay on top of viewer to jump to any thread
|
|
1419
|
+
async function openThreadPicker() {
|
|
1420
|
+
const items = buildPickerItems(getThreads());
|
|
1421
|
+
const selectedId = await showAgentPicker(ctx, items);
|
|
1422
|
+
if (!selectedId) return;
|
|
1423
|
+
if (selectedId === "__main__") { close(); return; }
|
|
1424
|
+
const idx = getThreads().findIndex((t) => t.id === selectedId);
|
|
1425
|
+
if (idx >= 0) {
|
|
1426
|
+
currentIndex = idx;
|
|
1427
|
+
viewer.setThread(getThreads()[currentIndex], makeCallbacks());
|
|
1428
|
+
tui.requestRender();
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}, { overlay: true, overlayOptions: { maxHeight: "70%" } }); // Overlay: editor stays visible below
|
|
1432
|
+
}
|
|
1433
1433
|
}
|