@ferris1225/pi-subagents 0.5.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/README-zh.md +10 -5
- package/README.md +13 -6
- package/package.json +1 -1
- package/src/background.ts +67 -0
- package/src/index.ts +133 -109
- package/src/prompt.ts +6 -5
- package/src/spawn.ts +5 -3
package/README-zh.md
CHANGED
|
@@ -70,7 +70,7 @@ pi 从不把每个 agent 的描述展示给主模型——它只看到 `subagent
|
|
|
70
70
|
3. **`Use PROACTIVELY when …` 描述** —— 在 Claude Code agent 生态被验证过的触发措辞。
|
|
71
71
|
|
|
72
72
|
这段指令会引导出一条干净的流程:**`explore` → `worker` → `reviewer`**,独立任务并行扇出,
|
|
73
|
-
|
|
73
|
+
以及「信任但需验证」的交接。由于子代理在后台运行,依赖前序结果的步骤必须等结果送达后再启动。
|
|
74
74
|
|
|
75
75
|
## 配置
|
|
76
76
|
|
|
@@ -121,20 +121,25 @@ pi 从不把每个 agent 的描述展示给主模型——它只看到 `subagent
|
|
|
121
121
|
{ "tasks": [ { "agent": "explore", "task": "..." }, { "agent": "explore", "task": "..." } ] }
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
+
所有子代理默认在后台启动。工具会立即结束当前主代理回合,编辑器可直接继续输入,
|
|
125
|
+
无需按 Escape;完成输出会显示出来,并在后续用户请求前加入上下文。启动完成后,
|
|
126
|
+
Escape 只中止前台工作;切换会话、`/reload` 或退出才会清理仍在运行的后台进程。
|
|
127
|
+
|
|
124
128
|
## 实时状态与通知
|
|
125
129
|
|
|
126
130
|
子代理运行期间,编辑器上方的挂件为每个运行显示一行状态(图标、agent、模型、
|
|
127
|
-
token 用量、耗时),其下缩进一行显示它正在做什么:`thinking`、`
|
|
131
|
+
token 用量、耗时),其下缩进一行显示它正在做什么:`thinking`、`responding`、
|
|
128
132
|
`read src/index.ts`、`bash npm test`……(不会是一坨 JSON 参数)。
|
|
133
|
+
`responding` 表示模型正在流式输出普通文本,**不**表示向文件系统写入。
|
|
129
134
|
|
|
130
135
|
运行结束(成功**或**失败)时,该行立即从挂件消失,主窗口收到一条通知,
|
|
131
|
-
给出最终摘要(`✓ worker · openai/gpt-5 · ↑12.4k ↓3.1k · 47s
|
|
132
|
-
|
|
136
|
+
给出最终摘要(`✓ worker · openai/gpt-5 · ↑12.4k ↓3.1k · 47s`)。完成结果消息
|
|
137
|
+
才是对话中持久保留、并会供后续请求使用的记录。
|
|
133
138
|
|
|
134
139
|
子代理使用配置的思考强度(默认 `--thinking max`);pi 会按目标模型实际支持
|
|
135
140
|
的级别自适应降级(`max → xhigh → high → … → off`),弱模型也能平稳运行。
|
|
136
141
|
任务内容通过 stdin 传递,只有 agent system prompt 使用短生命周期临时文件。
|
|
137
|
-
|
|
142
|
+
子进程输出在内存中流式处理;默认不设运行时限,只有显式中止时才清理整个进程树。
|
|
138
143
|
|
|
139
144
|
## 开发
|
|
140
145
|
|
package/README.md
CHANGED
|
@@ -79,7 +79,8 @@ tool. Three levers fix that:
|
|
|
79
79
|
Claude Code agent ecosystem.
|
|
80
80
|
|
|
81
81
|
The directive encourages a clean flow: **`explore` → `worker` → `reviewer`**, parallel
|
|
82
|
-
fan-out for independent tasks, and trust-but-verify handoffs.
|
|
82
|
+
fan-out for independent tasks, and trust-but-verify handoffs. Because runs are backgrounded,
|
|
83
|
+
start dependent steps only after the preceding result is delivered.
|
|
83
84
|
|
|
84
85
|
## Configuration
|
|
85
86
|
|
|
@@ -130,24 +131,30 @@ Tool shape:
|
|
|
130
131
|
{ "tasks": [ { "agent": "explore", "task": "..." }, { "agent": "explore", "task": "..." } ] }
|
|
131
132
|
```
|
|
132
133
|
|
|
134
|
+
Every run starts in the background. The tool immediately ends the current main-agent turn,
|
|
135
|
+
so the editor is ready for another request without pressing Escape. The completed output is
|
|
136
|
+
shown and added to the context before a later user prompt. Escape only interrupts foreground
|
|
137
|
+
work after launch; session switch, reload, or exit cancels remaining background processes.
|
|
138
|
+
|
|
133
139
|
## Live status & notifications
|
|
134
140
|
|
|
135
141
|
While sub-agents run, a widget above the editor shows one line per run — status
|
|
136
142
|
icon, agent, model, token usage, elapsed time — plus a second, indented line
|
|
137
|
-
with what the agent is doing right now: `thinking`, `
|
|
143
|
+
with what the agent is doing right now: `thinking`, `responding`,
|
|
138
144
|
`read src/index.ts`, `bash npm test`, … (never a raw JSON args blob).
|
|
145
|
+
`responding` means the model is streaming normal text, **not** writing to the filesystem.
|
|
139
146
|
|
|
140
147
|
When a run finishes (done **or** failed), its row disappears from the widget and
|
|
141
148
|
the main window gets a notification with the final summary
|
|
142
|
-
(`✓ worker · openai/gpt-5 · ↑12.4k ↓3.1k · 47s`).
|
|
143
|
-
|
|
149
|
+
(`✓ worker · openai/gpt-5 · ↑12.4k ↓3.1k · 47s`). Its completed result message
|
|
150
|
+
is the durable record in the conversation and context for a later request.
|
|
144
151
|
|
|
145
152
|
Sub-agents use the configured thinking level (default `--thinking max`);
|
|
146
153
|
pi clamps it adaptively to what the resolved model supports
|
|
147
154
|
(`max → xhigh → high → … → off`), so weaker models degrade gracefully.
|
|
148
155
|
The task is sent through stdin; only the agent system prompt uses a short-lived
|
|
149
|
-
file. Child output is streamed in memory
|
|
150
|
-
|
|
156
|
+
file. Child output is streamed in memory. Runs have no default time limit;
|
|
157
|
+
explicit cancellation cleans up the process tree.
|
|
151
158
|
|
|
152
159
|
## Development
|
|
153
160
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ferris1225/pi-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Focused sub-agent delegation for pi: explore / plan / worker / reviewer agents in isolated context, with proactive dispatch injection and per-agent model selection.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded background task scheduler.
|
|
3
|
+
*
|
|
4
|
+
* Tasks get their own AbortSignal rather than inheriting the foreground agent
|
|
5
|
+
* turn's signal. The owning extension cancels all work only on session teardown.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type BackgroundTask = (signal: AbortSignal) => Promise<void>;
|
|
9
|
+
|
|
10
|
+
interface PendingTask {
|
|
11
|
+
task: BackgroundTask;
|
|
12
|
+
controller: AbortController;
|
|
13
|
+
onCancelled?: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class BackgroundTaskQueue {
|
|
17
|
+
private readonly concurrency: number;
|
|
18
|
+
private readonly pending: PendingTask[] = [];
|
|
19
|
+
private readonly active = new Set<AbortController>();
|
|
20
|
+
private stopped = false;
|
|
21
|
+
|
|
22
|
+
constructor(concurrency: number) {
|
|
23
|
+
this.concurrency = Math.max(1, concurrency);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
enqueue(task: BackgroundTask, onCancelled?: () => void): AbortController {
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
if (this.stopped) {
|
|
29
|
+
controller.abort();
|
|
30
|
+
onCancelled?.();
|
|
31
|
+
return controller;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.pending.push({ task, controller, onCancelled });
|
|
35
|
+
this.drain();
|
|
36
|
+
return controller;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Stop queued work and request cancellation for running work. */
|
|
40
|
+
cancelAll(): void {
|
|
41
|
+
if (this.stopped) return;
|
|
42
|
+
this.stopped = true;
|
|
43
|
+
|
|
44
|
+
for (const entry of this.pending.splice(0)) {
|
|
45
|
+
entry.controller.abort();
|
|
46
|
+
entry.onCancelled?.();
|
|
47
|
+
}
|
|
48
|
+
for (const controller of this.active) controller.abort();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private drain(): void {
|
|
52
|
+
while (!this.stopped && this.active.size < this.concurrency) {
|
|
53
|
+
const entry = this.pending.shift();
|
|
54
|
+
if (!entry) return;
|
|
55
|
+
if (entry.controller.signal.aborted) {
|
|
56
|
+
entry.onCancelled?.();
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.active.add(entry.controller);
|
|
61
|
+
void entry.task(entry.controller.signal).catch(() => undefined).finally(() => {
|
|
62
|
+
this.active.delete(entry.controller);
|
|
63
|
+
this.drain();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
16
|
-
import { StringEnum } from "@earendil-works/pi-ai";
|
|
17
16
|
import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
17
|
import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
19
18
|
import { Type } from "typebox";
|
|
20
19
|
import { discoverAgents, type AgentConfig } from "./agents.ts";
|
|
20
|
+
import { BackgroundTaskQueue } from "./background.ts";
|
|
21
21
|
import { getConfigPath, loadConfig, saveConfig } from "./config.ts";
|
|
22
22
|
import { repairUnavailableModelOverrides } from "./models.ts";
|
|
23
23
|
import { buildDelegationDirective } from "./prompt.ts";
|
|
@@ -30,9 +30,7 @@ import {
|
|
|
30
30
|
getFinalOutput,
|
|
31
31
|
getResultOutput,
|
|
32
32
|
isFailedResult,
|
|
33
|
-
mapWithConcurrencyLimit,
|
|
34
33
|
runSingleAgent,
|
|
35
|
-
type OnUpdateCallback,
|
|
36
34
|
type SingleResult,
|
|
37
35
|
type SubagentDetails,
|
|
38
36
|
type SubagentLiveEvent,
|
|
@@ -57,6 +55,32 @@ function emptyUsage(): UsageStats {
|
|
|
57
55
|
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
|
|
58
56
|
}
|
|
59
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
|
+
|
|
60
84
|
function aggregateUsage(results: SingleResult[]): UsageStats {
|
|
61
85
|
const total = emptyUsage();
|
|
62
86
|
for (const r of results) {
|
|
@@ -88,6 +112,8 @@ function formatUsage(usage: UsageStats): string {
|
|
|
88
112
|
|
|
89
113
|
export default function (pi: ExtensionAPI): void {
|
|
90
114
|
const configPath = getConfigPath(getAgentDir());
|
|
115
|
+
const backgroundQueue = new BackgroundTaskQueue(MAX_CONCURRENCY);
|
|
116
|
+
let sessionActive = true;
|
|
91
117
|
|
|
92
118
|
// Recursion guard: child sub-agents are leaf processes and cannot delegate again.
|
|
93
119
|
if (currentSubagentDepth() >= MAX_SUBAGENT_DEPTH) {
|
|
@@ -100,6 +126,19 @@ export default function (pi: ExtensionAPI): void {
|
|
|
100
126
|
return;
|
|
101
127
|
}
|
|
102
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
|
+
|
|
103
142
|
pi.registerTool({
|
|
104
143
|
name: "subagent",
|
|
105
144
|
label: "Subagent",
|
|
@@ -107,17 +146,18 @@ export default function (pi: ExtensionAPI): void {
|
|
|
107
146
|
"Delegate a discrete, self-contained task to a specialized sub-agent running in an ISOLATED context window.",
|
|
108
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).",
|
|
109
148
|
"Modes: single ({agent, task}) or parallel ({tasks: [{agent, task}, ...]}).",
|
|
110
|
-
"
|
|
111
|
-
"Each agent has no memory of this conversation — brief it fully (goal, exact paths, constraints, expected output)."
|
|
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)."
|
|
112
151
|
].join(" "),
|
|
113
152
|
promptSnippet:
|
|
114
|
-
"
|
|
153
|
+
"Start background subagents: explore (read-only search), worker (implement), reviewer (adversarial review); completed results arrive in a later prompt.",
|
|
115
154
|
promptGuidelines: [
|
|
116
155
|
"Use subagent to delegate discrete, self-contained tasks so the main context stays clean; do orchestration and verification yourself.",
|
|
117
156
|
"Use subagent with agent 'explore' for broad or open-ended code search before large changes.",
|
|
118
157
|
"Use subagent with agent 'worker' to implement a well-scoped task; it plans internally.",
|
|
119
158
|
"Use subagent with agent 'reviewer' for a fresh read-only review before reporting work done or committing.",
|
|
120
|
-
"
|
|
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.",
|
|
121
161
|
],
|
|
122
162
|
parameters: SubagentParams,
|
|
123
163
|
|
|
@@ -143,12 +183,13 @@ export default function (pi: ExtensionAPI): void {
|
|
|
143
183
|
}
|
|
144
184
|
}
|
|
145
185
|
|
|
146
|
-
// Finished runs leave the widget immediately
|
|
147
|
-
//
|
|
186
|
+
// Finished runs leave the widget immediately. Their final findings arrive
|
|
187
|
+
// as a custom message before the next foreground prompt.
|
|
148
188
|
const finishRun = (runId: number, status: "done" | "failed"): void => {
|
|
149
189
|
monitor.setStatus(runId, status); // stamps endedAt for the elapsed time
|
|
150
190
|
const run = monitor.removeRun(runId);
|
|
151
191
|
if (!run) return; // already finished — stay idempotent
|
|
192
|
+
if (!sessionActive) return;
|
|
152
193
|
const icon = status === "done" ? "✓" : "✗";
|
|
153
194
|
ctx.ui.notify(`${icon} ${monitor.summarize(run)}`, status === "done" ? "info" : "error");
|
|
154
195
|
};
|
|
@@ -174,7 +215,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
174
215
|
monitor.setActivity(runId, "thinking");
|
|
175
216
|
break;
|
|
176
217
|
case "text":
|
|
177
|
-
|
|
218
|
+
// A text delta is model output, not a filesystem write.
|
|
219
|
+
monitor.setActivity(runId, "responding");
|
|
178
220
|
break;
|
|
179
221
|
}
|
|
180
222
|
};
|
|
@@ -194,8 +236,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
194
236
|
const hasSingle = Boolean(params.agent && params.task);
|
|
195
237
|
|
|
196
238
|
const makeDetails =
|
|
197
|
-
(mode: "single" | "parallel") =>
|
|
198
|
-
(results: SingleResult[]): SubagentDetails => ({ mode, results });
|
|
239
|
+
(mode: "single" | "parallel", background = false) =>
|
|
240
|
+
(results: SingleResult[]): SubagentDetails => ({ mode, results, background });
|
|
199
241
|
|
|
200
242
|
const catalog = agents.map((a) => a.name).join(", ") || "none";
|
|
201
243
|
|
|
@@ -211,124 +253,104 @@ export default function (pi: ExtensionAPI): void {
|
|
|
211
253
|
};
|
|
212
254
|
}
|
|
213
255
|
|
|
214
|
-
|
|
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.
|
|
215
311
|
if (params.tasks && params.tasks.length > 0) {
|
|
216
312
|
if (params.tasks.length > MAX_PARALLEL_TASKS) {
|
|
217
313
|
return {
|
|
218
314
|
content: [
|
|
219
315
|
{ type: "text", text: `Too many parallel tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.` },
|
|
220
316
|
],
|
|
221
|
-
details: makeDetails("parallel")([]),
|
|
317
|
+
details: makeDetails("parallel", true)([]),
|
|
222
318
|
};
|
|
223
319
|
}
|
|
224
320
|
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
});
|
|
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);
|
|
287
324
|
return {
|
|
288
325
|
content: [
|
|
289
326
|
{
|
|
290
327
|
type: "text",
|
|
291
|
-
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"),
|
|
292
332
|
},
|
|
293
333
|
],
|
|
294
|
-
details: makeDetails("parallel")(results),
|
|
334
|
+
details: makeDetails("parallel", true)(results),
|
|
335
|
+
isError: failures.length > 0,
|
|
336
|
+
terminate: true,
|
|
295
337
|
};
|
|
296
338
|
}
|
|
297
339
|
|
|
298
|
-
|
|
299
|
-
|
|
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)) {
|
|
340
|
+
const result = startBackground(params.agent as string, params.task as string, params.cwd);
|
|
341
|
+
if (result.exitCode !== -1) {
|
|
322
342
|
return {
|
|
323
|
-
content: [{ type: "text", text:
|
|
343
|
+
content: [{ type: "text", text: getResultOutput(result) }],
|
|
324
344
|
details: makeDetails("single")([result]),
|
|
325
345
|
isError: true,
|
|
326
346
|
};
|
|
327
347
|
}
|
|
328
348
|
return {
|
|
329
|
-
content: [{ type: "text", text:
|
|
330
|
-
details: makeDetails("single")([result]),
|
|
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,
|
|
331
352
|
};
|
|
353
|
+
|
|
332
354
|
},
|
|
333
355
|
|
|
334
356
|
renderCall(args, theme) {
|
|
@@ -356,10 +378,11 @@ export default function (pi: ExtensionAPI): void {
|
|
|
356
378
|
|
|
357
379
|
if (details.mode === "single") {
|
|
358
380
|
const r = details.results[0];
|
|
359
|
-
const
|
|
381
|
+
const pending = r.exitCode === -1;
|
|
382
|
+
const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
|
|
360
383
|
const usage = formatUsage(r.usage);
|
|
361
384
|
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}` : ""}`)}`;
|
|
385
|
+
const line = `${theme.fg("toolTitle", theme.bold("subagent "))}${icon} ${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${pending ? " · background" : ""}${usage ? ` · ${usage}` : ""}`)}`;
|
|
363
386
|
return new Text(line, 0, 0);
|
|
364
387
|
}
|
|
365
388
|
|
|
@@ -368,10 +391,11 @@ export default function (pi: ExtensionAPI): void {
|
|
|
368
391
|
`${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", `parallel (${details.results.length})`)}`,
|
|
369
392
|
];
|
|
370
393
|
for (const r of details.results) {
|
|
371
|
-
const
|
|
394
|
+
const pending = r.exitCode === -1;
|
|
395
|
+
const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
|
|
372
396
|
const usage = formatUsage(r.usage);
|
|
373
397
|
const model = r.model ?? "?";
|
|
374
|
-
lines.push(` ${icon} ${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${usage ? ` · ${usage}` : ""}`)}`);
|
|
398
|
+
lines.push(` ${icon} ${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${pending ? " · background" : ""}${usage ? ` · ${usage}` : ""}`)}`);
|
|
375
399
|
}
|
|
376
400
|
return new Text(lines.join("\n"), 0, 0);
|
|
377
401
|
},
|
package/src/prompt.ts
CHANGED
|
@@ -37,18 +37,19 @@ export function buildDelegationDirective(agents: AgentConfig[]): string {
|
|
|
37
37
|
return `
|
|
38
38
|
## Sub-agent delegation (pi-subagents)
|
|
39
39
|
|
|
40
|
-
You have a \`subagent\` tool that
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
You have a \`subagent\` tool that starts specialized agents in ISOLATED background processes.
|
|
41
|
+
It immediately ends the current main-agent turn so the user can keep working. Completed
|
|
42
|
+
findings are added before a later user prompt; do not assume a launched result is available
|
|
43
|
+
in the same turn.
|
|
43
44
|
|
|
44
45
|
Available agents:
|
|
45
46
|
${catalog}
|
|
46
47
|
|
|
47
48
|
${routing ? `Routing:\n${routing}\n` : ""}Dispatch discipline:
|
|
48
|
-
- Default to delegating every discrete task to a sub-agent;
|
|
49
|
+
- Default to delegating every discrete task to a sub-agent; use completed findings in a later user turn for orchestration and verification.
|
|
49
50
|
- Only handle inline: pure Q&A, a single trivial edit/lookup, or when the user explicitly says to do it directly. When in doubt, delegate.
|
|
50
51
|
- For an already-known or trivial target, use a direct search/read tool (e.g. grep/find/read) — do not over-delegate a one-line lookup.
|
|
51
|
-
${hasMultiple ? "- Run INDEPENDENT tasks in parallel: one subagent call with a `tasks` array, and track them with your todo list.
|
|
52
|
+
${hasMultiple ? "- Run INDEPENDENT tasks in parallel: one subagent call with a `tasks` array, and track them with your todo list. Launch dependent work only after its prerequisite result arrives (e.g. explore, then worker, then reviewer).\n" : ""}- Brief each sub-agent as self-contained: goal, exact paths, constraints, expected output. It has NO memory of this conversation.
|
|
52
53
|
- Treat delegated agents as leaf workers: do not ask a sub-agent to dispatch another sub-agent; child processes do not have this tool.
|
|
53
54
|
- Trust but verify: a sub-agent's summary describes intent, not outcome. Check the actual changes/results before reporting work done.
|
|
54
55
|
|
package/src/spawn.ts
CHANGED
|
@@ -27,8 +27,8 @@ export const SUBAGENT_THINKING_LEVEL: ThinkingLevel = DEFAULT_THINKING_LEVEL;
|
|
|
27
27
|
/** Child processes are leaf agents: they never receive the subagent tool. */
|
|
28
28
|
export const MAX_SUBAGENT_DEPTH = 1;
|
|
29
29
|
export const DEPTH_ENV_VAR = "PI_SUBAGENT_DEPTH";
|
|
30
|
-
/**
|
|
31
|
-
export const SUBAGENT_TIMEOUT_MS =
|
|
30
|
+
/** No default deadline: sub-agents may run until completion or explicit cancellation. */
|
|
31
|
+
export const SUBAGENT_TIMEOUT_MS = 0;
|
|
32
32
|
export const SUBAGENT_KILL_GRACE_MS = 5_000;
|
|
33
33
|
|
|
34
34
|
export interface UsageStats {
|
|
@@ -57,6 +57,8 @@ export interface SingleResult {
|
|
|
57
57
|
export interface SubagentDetails {
|
|
58
58
|
mode: "single" | "parallel";
|
|
59
59
|
results: SingleResult[];
|
|
60
|
+
/** The tool returned immediately while the child process continues in the background. */
|
|
61
|
+
background?: boolean;
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
export type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
|
|
@@ -179,7 +181,7 @@ export interface RunSingleOptions {
|
|
|
179
181
|
cwd?: string;
|
|
180
182
|
/** Thinking level passed to the child pi process. */
|
|
181
183
|
thinkingLevel?: ThinkingLevel;
|
|
182
|
-
/**
|
|
184
|
+
/** Optional timeout; zero (the default) disables it. Intended for tests and controlled callers. */
|
|
183
185
|
timeoutMs?: number;
|
|
184
186
|
signal?: AbortSignal;
|
|
185
187
|
onUpdate?: OnUpdateCallback;
|