@ferris1225/pi-subagents 0.3.0 → 0.5.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/LICENSE +21 -21
- package/README-zh.md +153 -134
- package/README.md +167 -143
- package/agents/explore.md +42 -42
- package/agents/plan.md +41 -41
- package/agents/reviewer.md +45 -45
- package/agents/worker.md +44 -44
- package/package.json +54 -54
- package/src/agents.ts +157 -157
- package/src/config.ts +168 -155
- package/src/index.ts +437 -417
- package/src/models.ts +69 -0
- package/src/monitor.ts +275 -238
- package/src/prompt.ts +58 -57
- package/src/setup.ts +264 -222
- package/src/spawn.ts +473 -361
- package/src/ui.ts +231 -231
package/src/index.ts
CHANGED
|
@@ -1,417 +1,437 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pi-subagents — focused sub-agent delegation for pi.
|
|
3
|
-
*
|
|
4
|
-
* Registers:
|
|
5
|
-
* - a `subagent` tool that runs explore/plan/worker/reviewer agents as isolated
|
|
6
|
-
* `pi` child processes (single or parallel),
|
|
7
|
-
* - a `/subagents-setup` command for selection-only configuration,
|
|
8
|
-
* - a `before_agent_start` hook that injects a delegation directive into the
|
|
9
|
-
* parent system prompt so the main model uses the tool proactively.
|
|
10
|
-
*
|
|
11
|
-
* The tool is
|
|
12
|
-
*
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import { buildDelegationDirective } from "./prompt.ts";
|
|
24
|
-
import { runSetup } from "./setup.ts";
|
|
25
|
-
import {
|
|
26
|
-
MAX_CONCURRENCY,
|
|
27
|
-
MAX_PARALLEL_TASKS,
|
|
28
|
-
MAX_SUBAGENT_DEPTH,
|
|
29
|
-
currentSubagentDepth,
|
|
30
|
-
getFinalOutput,
|
|
31
|
-
getResultOutput,
|
|
32
|
-
isFailedResult,
|
|
33
|
-
mapWithConcurrencyLimit,
|
|
34
|
-
runSingleAgent,
|
|
35
|
-
type OnUpdateCallback,
|
|
36
|
-
type SingleResult,
|
|
37
|
-
type SubagentDetails,
|
|
38
|
-
type SubagentLiveEvent,
|
|
39
|
-
type UsageStats,
|
|
40
|
-
} from "./spawn.ts";
|
|
41
|
-
import { monitor, statusColor, statusIcon, statusLabel } from "./monitor.ts";
|
|
42
|
-
|
|
43
|
-
const TaskItem = Type.Object({
|
|
44
|
-
agent: Type.String({ description: "Name of the agent to invoke" }),
|
|
45
|
-
task: Type.String({ description: "Self-contained task to delegate (the agent has no memory of this conversation)" }),
|
|
46
|
-
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
const SubagentParams = Type.Object({
|
|
50
|
-
agent: Type.Optional(Type.String({ description: "Name of the agent to invoke (single mode)" })),
|
|
51
|
-
task: Type.Optional(Type.String({ description: "Self-contained task to delegate (single mode)" })),
|
|
52
|
-
tasks: Type.Optional(Type.Array(TaskItem, { description: "Array of {agent, task} for parallel execution" })),
|
|
53
|
-
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process (single mode)" })),
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
function emptyUsage(): UsageStats {
|
|
57
|
-
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function aggregateUsage(results: SingleResult[]): UsageStats {
|
|
61
|
-
const total = emptyUsage();
|
|
62
|
-
for (const r of results) {
|
|
63
|
-
total.input += r.usage.input;
|
|
64
|
-
total.output += r.usage.output;
|
|
65
|
-
total.cacheRead += r.usage.cacheRead;
|
|
66
|
-
total.cacheWrite += r.usage.cacheWrite;
|
|
67
|
-
total.cost += r.usage.cost;
|
|
68
|
-
total.turns += r.usage.turns;
|
|
69
|
-
}
|
|
70
|
-
return total;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function formatTokens(count: number): string {
|
|
74
|
-
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
|
75
|
-
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k`;
|
|
76
|
-
return String(count);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function formatUsage(usage: UsageStats): string {
|
|
80
|
-
const parts: string[] = [];
|
|
81
|
-
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
82
|
-
if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
|
|
83
|
-
if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
|
|
84
|
-
if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
|
|
85
|
-
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
86
|
-
return parts.join(" ");
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export default function (pi: ExtensionAPI): void {
|
|
90
|
-
const configPath = getConfigPath(getAgentDir());
|
|
91
|
-
|
|
92
|
-
// Recursion guard:
|
|
93
|
-
if (currentSubagentDepth() >= MAX_SUBAGENT_DEPTH) {
|
|
94
|
-
pi.registerCommand("subagents-setup", {
|
|
95
|
-
description: "Configure pi-subagents (disabled in nested sub-agent processes)",
|
|
96
|
-
handler: async (_args, ctx) => {
|
|
97
|
-
ctx.ui.notify("pi-subagents setup is unavailable inside a nested sub-agent.", "warning");
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
pi.registerTool({
|
|
104
|
-
name: "subagent",
|
|
105
|
-
label: "Subagent",
|
|
106
|
-
description: [
|
|
107
|
-
"Delegate a discrete, self-contained task to a specialized sub-agent running in an ISOLATED context window.",
|
|
108
|
-
"Agents: explore (read-only codebase recon), plan (implementation plan, opt-in), worker (implement/fix/refactor/test, full tools), reviewer (adversarial pre-commit review, read-only).",
|
|
109
|
-
"Modes: single ({agent, task}) or parallel ({tasks: [{agent, task}, ...]}).",
|
|
110
|
-
"Use it to keep the main conversation clean: delegate the work, then orchestrate and verify the results yourself.",
|
|
111
|
-
"Each agent has no memory of this conversation — brief it fully (goal, exact paths, constraints, expected output).",
|
|
112
|
-
].join(" "),
|
|
113
|
-
promptSnippet:
|
|
114
|
-
"Delegate discrete tasks to isolated sub-agents: explore (read-only search), worker (implement), reviewer (adversarial pre-commit review); plan is opt-in.",
|
|
115
|
-
promptGuidelines: [
|
|
116
|
-
"Use subagent to delegate discrete, self-contained tasks so the main context stays clean; do orchestration and verification yourself.",
|
|
117
|
-
"Use subagent with agent 'explore' for broad or open-ended code search before large changes.",
|
|
118
|
-
"Use subagent with agent 'worker' to implement a well-scoped task; it plans internally.",
|
|
119
|
-
"Use subagent with agent 'reviewer' for a fresh read-only review before reporting work done or committing.",
|
|
120
|
-
"Run independent tasks in parallel by passing a tasks array to subagent; keep dependent work sequential.",
|
|
121
|
-
],
|
|
122
|
-
parameters: SubagentParams,
|
|
123
|
-
|
|
124
|
-
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
125
|
-
monitor.beginTurn();
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
const
|
|
246
|
-
const
|
|
247
|
-
const
|
|
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
|
-
return
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (
|
|
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
|
-
const
|
|
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
|
-
|
|
1
|
+
/**
|
|
2
|
+
* pi-subagents — focused sub-agent delegation for pi.
|
|
3
|
+
*
|
|
4
|
+
* Registers:
|
|
5
|
+
* - a `subagent` tool that runs explore/plan/worker/reviewer agents as isolated
|
|
6
|
+
* `pi` child processes (single or parallel),
|
|
7
|
+
* - a `/subagents-setup` command for selection-only configuration,
|
|
8
|
+
* - a `before_agent_start` hook that injects a delegation directive into the
|
|
9
|
+
* parent system prompt so the main model uses the tool proactively.
|
|
10
|
+
*
|
|
11
|
+
* The tool is not registered inside child sub-agent processes, which prevents
|
|
12
|
+
* runaway recursion and keeps child context windows clean.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
16
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
17
|
+
import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
19
|
+
import { Type } from "typebox";
|
|
20
|
+
import { discoverAgents, type AgentConfig } from "./agents.ts";
|
|
21
|
+
import { getConfigPath, loadConfig, saveConfig } from "./config.ts";
|
|
22
|
+
import { repairUnavailableModelOverrides } from "./models.ts";
|
|
23
|
+
import { buildDelegationDirective } from "./prompt.ts";
|
|
24
|
+
import { runSetup } from "./setup.ts";
|
|
25
|
+
import {
|
|
26
|
+
MAX_CONCURRENCY,
|
|
27
|
+
MAX_PARALLEL_TASKS,
|
|
28
|
+
MAX_SUBAGENT_DEPTH,
|
|
29
|
+
currentSubagentDepth,
|
|
30
|
+
getFinalOutput,
|
|
31
|
+
getResultOutput,
|
|
32
|
+
isFailedResult,
|
|
33
|
+
mapWithConcurrencyLimit,
|
|
34
|
+
runSingleAgent,
|
|
35
|
+
type OnUpdateCallback,
|
|
36
|
+
type SingleResult,
|
|
37
|
+
type SubagentDetails,
|
|
38
|
+
type SubagentLiveEvent,
|
|
39
|
+
type UsageStats,
|
|
40
|
+
} from "./spawn.ts";
|
|
41
|
+
import { formatToolActivity, monitor, statusColor, statusIcon, statusLabel } from "./monitor.ts";
|
|
42
|
+
|
|
43
|
+
const TaskItem = Type.Object({
|
|
44
|
+
agent: Type.String({ description: "Name of the agent to invoke" }),
|
|
45
|
+
task: Type.String({ description: "Self-contained task to delegate (the agent has no memory of this conversation)" }),
|
|
46
|
+
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const SubagentParams = Type.Object({
|
|
50
|
+
agent: Type.Optional(Type.String({ description: "Name of the agent to invoke (single mode)" })),
|
|
51
|
+
task: Type.Optional(Type.String({ description: "Self-contained task to delegate (single mode)" })),
|
|
52
|
+
tasks: Type.Optional(Type.Array(TaskItem, { description: "Array of {agent, task} for parallel execution" })),
|
|
53
|
+
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process (single mode)" })),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
function emptyUsage(): UsageStats {
|
|
57
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function aggregateUsage(results: SingleResult[]): UsageStats {
|
|
61
|
+
const total = emptyUsage();
|
|
62
|
+
for (const r of results) {
|
|
63
|
+
total.input += r.usage.input;
|
|
64
|
+
total.output += r.usage.output;
|
|
65
|
+
total.cacheRead += r.usage.cacheRead;
|
|
66
|
+
total.cacheWrite += r.usage.cacheWrite;
|
|
67
|
+
total.cost += r.usage.cost;
|
|
68
|
+
total.turns += r.usage.turns;
|
|
69
|
+
}
|
|
70
|
+
return total;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function formatTokens(count: number): string {
|
|
74
|
+
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
|
75
|
+
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k`;
|
|
76
|
+
return String(count);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatUsage(usage: UsageStats): string {
|
|
80
|
+
const parts: string[] = [];
|
|
81
|
+
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
82
|
+
if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
|
|
83
|
+
if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
|
|
84
|
+
if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
|
|
85
|
+
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
86
|
+
return parts.join(" ");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export default function (pi: ExtensionAPI): void {
|
|
90
|
+
const configPath = getConfigPath(getAgentDir());
|
|
91
|
+
|
|
92
|
+
// Recursion guard: child sub-agents are leaf processes and cannot delegate again.
|
|
93
|
+
if (currentSubagentDepth() >= MAX_SUBAGENT_DEPTH) {
|
|
94
|
+
pi.registerCommand("subagents-setup", {
|
|
95
|
+
description: "Configure pi-subagents (disabled in nested sub-agent processes)",
|
|
96
|
+
handler: async (_args, ctx) => {
|
|
97
|
+
ctx.ui.notify("pi-subagents setup is unavailable inside a nested sub-agent.", "warning");
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
pi.registerTool({
|
|
104
|
+
name: "subagent",
|
|
105
|
+
label: "Subagent",
|
|
106
|
+
description: [
|
|
107
|
+
"Delegate a discrete, self-contained task to a specialized sub-agent running in an ISOLATED context window.",
|
|
108
|
+
"Agents: explore (read-only codebase recon), plan (implementation plan, opt-in), worker (implement/fix/refactor/test, full tools), reviewer (adversarial pre-commit review, read-only).",
|
|
109
|
+
"Modes: single ({agent, task}) or parallel ({tasks: [{agent, task}, ...]}).",
|
|
110
|
+
"Use it to keep the main conversation clean: delegate the work, then orchestrate and verify the results yourself.",
|
|
111
|
+
"Each agent has no memory of this conversation — brief it fully (goal, exact paths, constraints, expected output).",
|
|
112
|
+
].join(" "),
|
|
113
|
+
promptSnippet:
|
|
114
|
+
"Delegate discrete tasks to isolated sub-agents: explore (read-only search), worker (implement), reviewer (adversarial pre-commit review); plan is opt-in.",
|
|
115
|
+
promptGuidelines: [
|
|
116
|
+
"Use subagent to delegate discrete, self-contained tasks so the main context stays clean; do orchestration and verification yourself.",
|
|
117
|
+
"Use subagent with agent 'explore' for broad or open-ended code search before large changes.",
|
|
118
|
+
"Use subagent with agent 'worker' to implement a well-scoped task; it plans internally.",
|
|
119
|
+
"Use subagent with agent 'reviewer' for a fresh read-only review before reporting work done or committing.",
|
|
120
|
+
"Run independent tasks in parallel by passing a tasks array to subagent; keep dependent work sequential.",
|
|
121
|
+
],
|
|
122
|
+
parameters: SubagentParams,
|
|
123
|
+
|
|
124
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
125
|
+
monitor.beginTurn();
|
|
126
|
+
let config = await loadConfig(configPath);
|
|
127
|
+
const repairedModels = repairUnavailableModelOverrides(ctx, config.agentModels);
|
|
128
|
+
if (repairedModels.changed) {
|
|
129
|
+
config = { ...config, agentModels: repairedModels.agentModels };
|
|
130
|
+
try {
|
|
131
|
+
await saveConfig(config, configPath);
|
|
132
|
+
ctx.ui.notify(
|
|
133
|
+
repairedModels.fallbackRef
|
|
134
|
+
? `Unavailable sub-agent models switched to ${repairedModels.fallbackRef} and saved to config.`
|
|
135
|
+
: "Unavailable sub-agent model overrides removed; no main-window model is available.",
|
|
136
|
+
"warning",
|
|
137
|
+
);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
ctx.ui.notify(
|
|
140
|
+
`Could not persist repaired sub-agent model config: ${error instanceof Error ? error.message : String(error)}`,
|
|
141
|
+
"warning",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Finished runs leave the widget immediately; the main window gets a
|
|
147
|
+
// notification instead (the tool result remains the durable record).
|
|
148
|
+
const finishRun = (runId: number, status: "done" | "failed"): void => {
|
|
149
|
+
monitor.setStatus(runId, status); // stamps endedAt for the elapsed time
|
|
150
|
+
const run = monitor.removeRun(runId);
|
|
151
|
+
if (!run) return; // already finished — stay idempotent
|
|
152
|
+
const icon = status === "done" ? "✓" : "✗";
|
|
153
|
+
ctx.ui.notify(`${icon} ${monitor.summarize(run)}`, status === "done" ? "info" : "error");
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Live sub-agent activity → concise one-line status ("thinking",
|
|
157
|
+
// "read src/index.ts", ...), never a raw args blob.
|
|
158
|
+
const makeLiveHandler = (runId: number) => (e: SubagentLiveEvent): void => {
|
|
159
|
+
switch (e.kind) {
|
|
160
|
+
case "status":
|
|
161
|
+
if (e.status === "done" || e.status === "failed") finishRun(runId, e.status);
|
|
162
|
+
else monitor.setStatus(runId, e.status);
|
|
163
|
+
break;
|
|
164
|
+
case "usage":
|
|
165
|
+
monitor.setUsage(runId, e.usage, e.model);
|
|
166
|
+
break;
|
|
167
|
+
case "tool_start":
|
|
168
|
+
monitor.setActivity(runId, formatToolActivity(e.toolName, e.args));
|
|
169
|
+
break;
|
|
170
|
+
case "tool_end":
|
|
171
|
+
if (e.isError) monitor.setActivity(runId, `✗ ${e.toolName} failed`);
|
|
172
|
+
break;
|
|
173
|
+
case "thinking":
|
|
174
|
+
monitor.setActivity(runId, "thinking");
|
|
175
|
+
break;
|
|
176
|
+
case "text":
|
|
177
|
+
monitor.setActivity(runId, "writing");
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const discovery = discoverAgents(ctx.cwd, {
|
|
182
|
+
scope: config.agentScope,
|
|
183
|
+
enabledNames: config.enabledAgents,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Effective model precedence: setup override > current session model > frontmatter default.
|
|
187
|
+
const sessionRef = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined;
|
|
188
|
+
const agents: AgentConfig[] = discovery.agents.map((agent) => ({
|
|
189
|
+
...agent,
|
|
190
|
+
model: config.agentModels[agent.name] ?? sessionRef ?? agent.model,
|
|
191
|
+
}));
|
|
192
|
+
|
|
193
|
+
const hasTasks = (params.tasks?.length ?? 0) > 0;
|
|
194
|
+
const hasSingle = Boolean(params.agent && params.task);
|
|
195
|
+
|
|
196
|
+
const makeDetails =
|
|
197
|
+
(mode: "single" | "parallel") =>
|
|
198
|
+
(results: SingleResult[]): SubagentDetails => ({ mode, results });
|
|
199
|
+
|
|
200
|
+
const catalog = agents.map((a) => a.name).join(", ") || "none";
|
|
201
|
+
|
|
202
|
+
if (Number(hasTasks) + Number(hasSingle) !== 1) {
|
|
203
|
+
return {
|
|
204
|
+
content: [
|
|
205
|
+
{
|
|
206
|
+
type: "text",
|
|
207
|
+
text: `Invalid parameters. Provide exactly one mode: single {agent, task} or parallel {tasks: [...]}. Enabled agents: ${catalog}.`,
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
details: makeDetails("single")([]),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---- Parallel mode ----
|
|
215
|
+
if (params.tasks && params.tasks.length > 0) {
|
|
216
|
+
if (params.tasks.length > MAX_PARALLEL_TASKS) {
|
|
217
|
+
return {
|
|
218
|
+
content: [
|
|
219
|
+
{ type: "text", text: `Too many parallel tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.` },
|
|
220
|
+
],
|
|
221
|
+
details: makeDetails("parallel")([]),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const allResults: SingleResult[] = params.tasks.map((t) => ({
|
|
226
|
+
agent: t.agent,
|
|
227
|
+
agentSource: "unknown",
|
|
228
|
+
task: t.task,
|
|
229
|
+
exitCode: -1,
|
|
230
|
+
messages: [],
|
|
231
|
+
stderr: "",
|
|
232
|
+
usage: emptyUsage(),
|
|
233
|
+
}));
|
|
234
|
+
|
|
235
|
+
const emitParallelUpdate = (): void => {
|
|
236
|
+
if (!onUpdate) return;
|
|
237
|
+
const done = allResults.filter((r) => r.exitCode !== -1).length;
|
|
238
|
+
onUpdate({
|
|
239
|
+
content: [{ type: "text", text: `Parallel: ${done}/${allResults.length} done...` }],
|
|
240
|
+
details: makeDetails("parallel")([...allResults]),
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (t, index) => {
|
|
245
|
+
const resolvedModel = agents.find((a) => a.name === t.agent)?.model;
|
|
246
|
+
const runId = monitor.addRun(t.agent, resolvedModel);
|
|
247
|
+
const onLive = makeLiveHandler(runId);
|
|
248
|
+
const perTaskUpdate: OnUpdateCallback | undefined = onUpdate
|
|
249
|
+
? (partial) => {
|
|
250
|
+
const current = partial.details?.results[0];
|
|
251
|
+
if (current) {
|
|
252
|
+
allResults[index] = current;
|
|
253
|
+
emitParallelUpdate();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
: undefined;
|
|
257
|
+
let result: SingleResult;
|
|
258
|
+
try {
|
|
259
|
+
result = await runSingleAgent({
|
|
260
|
+
defaultCwd: ctx.cwd,
|
|
261
|
+
agent: agents.find((a) => a.name === t.agent),
|
|
262
|
+
agentName: t.agent,
|
|
263
|
+
task: t.task,
|
|
264
|
+
cwd: t.cwd,
|
|
265
|
+
thinkingLevel: config.thinkingLevel,
|
|
266
|
+
signal,
|
|
267
|
+
onUpdate: perTaskUpdate,
|
|
268
|
+
onLive,
|
|
269
|
+
makeDetails: makeDetails("parallel"),
|
|
270
|
+
});
|
|
271
|
+
} catch (err) {
|
|
272
|
+
finishRun(runId, "failed");
|
|
273
|
+
throw err;
|
|
274
|
+
}
|
|
275
|
+
allResults[index] = result;
|
|
276
|
+
emitParallelUpdate();
|
|
277
|
+
return result;
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const successCount = results.filter((r) => !isFailedResult(r)).length;
|
|
281
|
+
const summaries = results.map((r) => {
|
|
282
|
+
const output = getResultOutput(r);
|
|
283
|
+
const status = isFailedResult(r) ? "failed" : "completed";
|
|
284
|
+
const usage = formatUsage(r.usage);
|
|
285
|
+
return `### [${r.agent}] ${status}${usage ? ` (${usage})` : ""}\n\n${output}`;
|
|
286
|
+
});
|
|
287
|
+
return {
|
|
288
|
+
content: [
|
|
289
|
+
{
|
|
290
|
+
type: "text",
|
|
291
|
+
text: `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n---\n\n")}`,
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
details: makeDetails("parallel")(results),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ---- Single mode ----
|
|
299
|
+
const resolvedModel = agents.find((a) => a.name === params.agent)?.model;
|
|
300
|
+
const runId = monitor.addRun(params.agent as string, resolvedModel);
|
|
301
|
+
const onLive = makeLiveHandler(runId);
|
|
302
|
+
let result: SingleResult;
|
|
303
|
+
try {
|
|
304
|
+
result = await runSingleAgent({
|
|
305
|
+
defaultCwd: ctx.cwd,
|
|
306
|
+
agent: agents.find((a) => a.name === params.agent),
|
|
307
|
+
agentName: params.agent as string,
|
|
308
|
+
task: params.task as string,
|
|
309
|
+
cwd: params.cwd,
|
|
310
|
+
thinkingLevel: config.thinkingLevel,
|
|
311
|
+
signal,
|
|
312
|
+
onUpdate,
|
|
313
|
+
onLive,
|
|
314
|
+
makeDetails: makeDetails("single"),
|
|
315
|
+
});
|
|
316
|
+
} catch (err) {
|
|
317
|
+
finishRun(runId, "failed");
|
|
318
|
+
throw err;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (isFailedResult(result)) {
|
|
322
|
+
return {
|
|
323
|
+
content: [{ type: "text", text: `Agent ${result.agent} ${result.stopReason || "failed"}: ${getResultOutput(result)}` }],
|
|
324
|
+
details: makeDetails("single")([result]),
|
|
325
|
+
isError: true,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
content: [{ type: "text", text: getFinalOutput(result.messages) || "(no output)" }],
|
|
330
|
+
details: makeDetails("single")([result]),
|
|
331
|
+
};
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
renderCall(args, theme) {
|
|
335
|
+
if (args.tasks && args.tasks.length > 0) {
|
|
336
|
+
let text = `${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", `parallel (${args.tasks.length})`)}`;
|
|
337
|
+
for (const t of args.tasks.slice(0, 4)) {
|
|
338
|
+
const preview = t.task.length > 48 ? `${t.task.slice(0, 48)}…` : t.task;
|
|
339
|
+
text += `\n ${theme.fg("accent", t.agent)} ${theme.fg("dim", preview)}`;
|
|
340
|
+
}
|
|
341
|
+
if (args.tasks.length > 4) text += `\n ${theme.fg("dim", `… +${args.tasks.length - 4} more`)}`;
|
|
342
|
+
return new Text(text, 0, 0);
|
|
343
|
+
}
|
|
344
|
+
const task: string = args.task ?? "";
|
|
345
|
+
const preview = task.length > 60 ? `${task.slice(0, 60)}…` : task;
|
|
346
|
+
return new Text(
|
|
347
|
+
`${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", args.agent ?? "?")} ${theme.fg("dim", preview)}`,
|
|
348
|
+
0,
|
|
349
|
+
0,
|
|
350
|
+
);
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
renderResult(result, _options, theme) {
|
|
354
|
+
const details = result.details as SubagentDetails | undefined;
|
|
355
|
+
if (!details || details.results.length === 0) return new Text(theme.fg("dim", "(no output)"), 0, 0);
|
|
356
|
+
|
|
357
|
+
if (details.mode === "single") {
|
|
358
|
+
const r = details.results[0];
|
|
359
|
+
const icon = statusIcon(isFailedResult(r) ? "failed" : "done", theme);
|
|
360
|
+
const usage = formatUsage(r.usage);
|
|
361
|
+
const model = r.model ?? "?";
|
|
362
|
+
const line = `${theme.fg("toolTitle", theme.bold("subagent "))}${icon} ${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${usage ? ` · ${usage}` : ""}`)}`;
|
|
363
|
+
return new Text(line, 0, 0);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Parallel mode: header + one compact line per agent
|
|
367
|
+
const lines: string[] = [
|
|
368
|
+
`${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", `parallel (${details.results.length})`)}`,
|
|
369
|
+
];
|
|
370
|
+
for (const r of details.results) {
|
|
371
|
+
const icon = statusIcon(isFailedResult(r) ? "failed" : "done", theme);
|
|
372
|
+
const usage = formatUsage(r.usage);
|
|
373
|
+
const model = r.model ?? "?";
|
|
374
|
+
lines.push(` ${icon} ${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${usage ? ` · ${usage}` : ""}`)}`);
|
|
375
|
+
}
|
|
376
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
pi.registerCommand("subagents-setup", {
|
|
381
|
+
description: "Configure pi-subagents: enable agents, pick per-agent models, toggle proactive injection",
|
|
382
|
+
handler: async (_args, ctx) => {
|
|
383
|
+
await runSetup(ctx, configPath);
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Persistent widget above the editor showing live sub-agent status.
|
|
388
|
+
pi.on("session_start", (_e, ctx) => {
|
|
389
|
+
if (ctx.mode !== "tui") return;
|
|
390
|
+
ctx.ui.setWidget(
|
|
391
|
+
"pi-subagents",
|
|
392
|
+
(tui, theme) => {
|
|
393
|
+
const unsub = monitor.subscribe(() => tui.requestRender());
|
|
394
|
+
// Tick once a second so elapsed time stays live while runs are active.
|
|
395
|
+
const timer = setInterval(() => {
|
|
396
|
+
if (monitor.getRuns().some((r) => r.status === "queued" || r.status === "running")) {
|
|
397
|
+
tui.requestRender();
|
|
398
|
+
}
|
|
399
|
+
}, 1000);
|
|
400
|
+
return {
|
|
401
|
+
render(width: number): string[] {
|
|
402
|
+
const runs = monitor.getRuns();
|
|
403
|
+
if (runs.length === 0) return [];
|
|
404
|
+
const lines: string[] = [];
|
|
405
|
+
for (const r of runs) {
|
|
406
|
+
const icon = statusIcon(r.status, theme);
|
|
407
|
+
const label = theme.fg(statusColor(r.status), statusLabel(r.status));
|
|
408
|
+
lines.push(truncateToWidth(` ${icon} ${monitor.summarize(r)} · ${label}`, width, ""));
|
|
409
|
+
// Activity sits one indent level below the agent name.
|
|
410
|
+
if (r.activity) lines.push(truncateToWidth(theme.fg("dim", ` ${r.activity}`), width, ""));
|
|
411
|
+
}
|
|
412
|
+
return lines;
|
|
413
|
+
},
|
|
414
|
+
invalidate() {},
|
|
415
|
+
dispose() {
|
|
416
|
+
unsub();
|
|
417
|
+
clearInterval(timer);
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
},
|
|
421
|
+
{ placement: "aboveEditor" },
|
|
422
|
+
);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Proactive dispatch: inject the delegation directive into the parent system prompt.
|
|
426
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
427
|
+
const config = await loadConfig(configPath);
|
|
428
|
+
if (!config.proactiveInjection) return undefined;
|
|
429
|
+
const { agents } = discoverAgents(ctx.cwd, {
|
|
430
|
+
scope: config.agentScope,
|
|
431
|
+
enabledNames: config.enabledAgents,
|
|
432
|
+
});
|
|
433
|
+
const directive = buildDelegationDirective(agents);
|
|
434
|
+
if (!directive) return undefined;
|
|
435
|
+
return { systemPrompt: `${event.systemPrompt}\n${directive}` };
|
|
436
|
+
});
|
|
437
|
+
}
|