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