@narumitw/pi-subagents 0.20.0 → 0.25.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.md +11 -7
- package/package.json +1 -1
- package/src/agents.ts +5 -4
- package/src/stateful.ts +31 -182
- package/src/subagents.ts +5 -4
- package/src/orchestration.ts +0 -164
package/README.md
CHANGED
|
@@ -81,9 +81,11 @@ Count-selection guidance:
|
|
|
81
81
|
- A single `subagent` call is blocking. Use one only when context isolation, high-volume output
|
|
82
82
|
isolation, or independent review is worth waiting for; otherwise the main agent should do the work.
|
|
83
83
|
- Do not delegate ordinary critical-path work merely to wait for it. Use detached `subagent_spawn`
|
|
84
|
-
only
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
only for a concrete bounded subtask that can run independently alongside useful main-agent work
|
|
85
|
+
and when parallel work, bounded context/output, independent review, a distinct model/tool profile,
|
|
86
|
+
or workspace isolation provides concrete value. After spawning, do useful non-overlapping work
|
|
87
|
+
immediately. Call `subagent_wait` sparingly, only when the immediate next critical-path step needs
|
|
88
|
+
the result and is blocked until it arrives; do not wait repeatedly by reflex.
|
|
87
89
|
- Prefer **2–4 parallel read-only subagents** when a broad task naturally splits into independent
|
|
88
90
|
branches that can each return a concise summary.
|
|
89
91
|
- Exceed 4 tasks only when the branches are clearly distinct and worth the extra cost, while staying
|
|
@@ -202,9 +204,9 @@ Run a chain where each step receives the previous output:
|
|
|
202
204
|
|
|
203
205
|
Stateful lifecycle tools are available by default. `subagent_spawn` is detached: it schedules work, returns immediately with an opaque `agentId`, and later injects one bounded `pi-subagent-completion` message per settled turn. The message uses `deliverAs: "steer"` with `triggerTurn: false`, so an active root turn can consume it naturally.
|
|
204
206
|
|
|
205
|
-
Detached work
|
|
207
|
+
Detached work follows a critical-path policy: spawn only a concrete bounded subtask that can run independently alongside useful main-agent work, then do that non-overlapping work immediately. Use `subagent_wait` sparingly and only when the immediate next critical-path step requires the result and is blocked until it arrives. The extension does not start an autonomous recovery turn merely because delegated work remains live or completes. If the root turn has already ended, completion stays queued for the next turn instead of waking the root.
|
|
206
208
|
|
|
207
|
-
A single detached agent
|
|
209
|
+
A single detached agent additionally needs a concrete isolation or specialization benefit such as independent review, bounded context/output, a distinct model/tool profile, or workspace isolation. Simple work that the main agent can perform directly should not be delegated.
|
|
208
210
|
|
|
209
211
|
When `subagent_wait` starts while a turn is running, that wait consumes the turn's completion: the terminal output is returned by the tool and is not also injected as a duplicate asynchronous completion. Multiple active waiters may all receive the terminal result. A timed-out or aborted wait does not consume it, so later settlement still delivers the normal asynchronous completion. If a turn has already settled and queued its completion before a wait starts, the queued message cannot be retracted safely.
|
|
210
212
|
|
|
@@ -237,7 +239,7 @@ Set `"enabled": false` to remove all stateful lifecycle tools. Otherwise, the ex
|
|
|
237
239
|
| `subagent_send` | Send follow-up work to a reusable agent; shared-workspace write conflicts are guarded unless explicitly overridden. |
|
|
238
240
|
| `subagent_message` | Queue a bounded mailbox message without starting a turn; sender IDs must be `root` or an agent in the same tree. |
|
|
239
241
|
| `subagent_messages` | Read and optionally acknowledge unread mailbox messages. |
|
|
240
|
-
| `subagent_wait` | Wait when
|
|
242
|
+
| `subagent_wait` | Wait sparingly when the immediate next critical-path step is blocked on a result; timeout does not terminate the agent, and parent abort/user steering cancels only the wait. Already-terminal agents return immediately. |
|
|
241
243
|
| `subagent_list` | List retained agents and lifecycle states. |
|
|
242
244
|
| `subagent_interrupt` | Abort the current turn while retaining its identity and history. |
|
|
243
245
|
| `subagent_close` | Abort if necessary, close the agent, and remove it from retained persistence. |
|
|
@@ -292,10 +294,12 @@ Built-in agents are available without setup and can be overridden by user or pro
|
|
|
292
294
|
| --- | --- | --- |
|
|
293
295
|
| `scout` | Read-only codebase reconnaissance. | `read`, `grep`, `find`, `ls`, `bash` |
|
|
294
296
|
| `planner` | Grounded implementation plans. | `read`, `grep`, `find`, `ls` |
|
|
295
|
-
| `reviewer` | Independent review and verification. | `read`, `grep`, `find`, `ls`, `bash` |
|
|
297
|
+
| `reviewer` | Independent review of code and existing verification evidence. | `read`, `grep`, `find`, `ls`, `bash` |
|
|
296
298
|
| `worker` | General-purpose implementation. | Pi default tools |
|
|
297
299
|
| `general`, `general-purpose` | Aliases for `worker`. | Pi default tools |
|
|
298
300
|
|
|
301
|
+
The built-in `reviewer` does not run tests, builds, benchmarks, or formatters. It recommends additional verification commands for the main agent to run instead. Custom agents can override this behavior.
|
|
302
|
+
|
|
299
303
|
Built-in agents inherit the active/default Pi model instead of forcing a provider-specific model alias, which keeps subprocesses usable across different Pi setups.
|
|
300
304
|
|
|
301
305
|
## ⚙️ Configure agent tools
|
package/package.json
CHANGED
package/src/agents.ts
CHANGED
|
@@ -93,14 +93,15 @@ const BUILT_IN_AGENTS: AgentConfig[] = [
|
|
|
93
93
|
},
|
|
94
94
|
{
|
|
95
95
|
name: "reviewer",
|
|
96
|
-
description: "Independent code review
|
|
96
|
+
description: "Independent code review agent that inspects existing verification evidence.",
|
|
97
97
|
tools: ["read", "grep", "find", "ls", "bash"],
|
|
98
98
|
source: "built-in",
|
|
99
99
|
filePath: "built-in:reviewer",
|
|
100
100
|
systemPrompt: [
|
|
101
|
-
"You are a reviewer subagent. Review changes adversarially and
|
|
102
|
-
"Do not edit files
|
|
103
|
-
"
|
|
101
|
+
"You are a reviewer subagent. Review changes adversarially and assess claims against the code and existing evidence.",
|
|
102
|
+
"Do not edit files or run tests, builds, benchmarks, formatters, or other long-running verification commands.",
|
|
103
|
+
"Inspect code, diffs, test definitions, and existing verification evidence. Recommend any additional commands for the main agent to run.",
|
|
104
|
+
"Report PASS, FAIL, or PARTIAL with evidence, commands inspected, and specific follow-ups.",
|
|
104
105
|
].join("\n"),
|
|
105
106
|
},
|
|
106
107
|
{
|
package/src/stateful.ts
CHANGED
|
@@ -7,17 +7,8 @@ import { discoverAgents, type AgentScope, isThinkingLevel } from "./agents.js";
|
|
|
7
7
|
import { buildContextSnapshot, type ContextMode, redactPrivateText } from "./context.js";
|
|
8
8
|
import { assertSubagentDepthAllowed } from "./execution.js";
|
|
9
9
|
import { DEFAULT_MAX_CONTEXT_BYTES, truncateUtf8 } from "./limits.js";
|
|
10
|
-
import {
|
|
11
|
-
ORCHESTRATION_MARKER_PREFIX,
|
|
12
|
-
RootOrchestrationState,
|
|
13
|
-
type OrchestrationRecoveryTicket,
|
|
14
|
-
} from "./orchestration.js";
|
|
15
10
|
import { AgentPersistence } from "./persistence.js";
|
|
16
|
-
import {
|
|
17
|
-
AgentRegistry,
|
|
18
|
-
type AgentTurnCompletion,
|
|
19
|
-
type ManagedAgent,
|
|
20
|
-
} from "./registry.js";
|
|
11
|
+
import { AgentRegistry, type AgentTurnCompletion, type ManagedAgent } from "./registry.js";
|
|
21
12
|
import { readSubagentSettings } from "./settings.js";
|
|
22
13
|
import { SubprocessTransport } from "./subprocess-transport.js";
|
|
23
14
|
import {
|
|
@@ -58,8 +49,6 @@ export function registerStatefulSubagents(
|
|
|
58
49
|
const isolatedAgents = new Map<string, string>();
|
|
59
50
|
const seenMessageIds = new Set<string>();
|
|
60
51
|
const parentRuntime: ParentRuntimeSnapshot = { model: undefined, thinkingLevel: "off" };
|
|
61
|
-
const orchestration = new RootOrchestrationState();
|
|
62
|
-
const cancelledRecoveryNonces = new Set<string>();
|
|
63
52
|
const completionWaiters = new Map<string, number>();
|
|
64
53
|
|
|
65
54
|
const requireRegistry = () => {
|
|
@@ -69,11 +58,12 @@ export function registerStatefulSubagents(
|
|
|
69
58
|
|
|
70
59
|
pi.on("session_start", async (_event, ctx) => {
|
|
71
60
|
const generation = ++runtimeGeneration;
|
|
72
|
-
rememberCancelledRecovery(orchestration.supersedePending(), cancelledRecoveryNonces);
|
|
73
|
-
orchestration.reset();
|
|
74
61
|
parentRuntime.model = ctx.model;
|
|
75
62
|
parentRuntime.thinkingLevel = normalizeRuntimeThinkingLevel(pi.getThinkingLevel());
|
|
76
|
-
const owner =
|
|
63
|
+
const owner =
|
|
64
|
+
ctx.sessionManager.getSessionId?.() ??
|
|
65
|
+
ctx.sessionManager.getSessionFile?.() ??
|
|
66
|
+
`ephemeral:${ctx.cwd}`;
|
|
77
67
|
const sessionPersistence = new AgentPersistence(owner, {
|
|
78
68
|
retentionDays: settings.retentionDays,
|
|
79
69
|
maxStoredAgents: settings.maxStoredAgents,
|
|
@@ -112,7 +102,6 @@ export function registerStatefulSubagents(
|
|
|
112
102
|
},
|
|
113
103
|
onTurnComplete: (completion) => {
|
|
114
104
|
if (generation !== runtimeGeneration) return;
|
|
115
|
-
orchestration.complete(completion.agent.id);
|
|
116
105
|
if (!completionWaiters.has(completion.agent.id)) {
|
|
117
106
|
sendDetachedCompletion(pi, completion);
|
|
118
107
|
}
|
|
@@ -122,8 +111,7 @@ export function registerStatefulSubagents(
|
|
|
122
111
|
.load()
|
|
123
112
|
.filter(
|
|
124
113
|
(agent) =>
|
|
125
|
-
(agent.agentScope !== "project" && agent.agentScope !== "both") ||
|
|
126
|
-
ctx.isProjectTrusted(),
|
|
114
|
+
(agent.agentScope !== "project" && agent.agentScope !== "both") || ctx.isProjectTrusted(),
|
|
127
115
|
);
|
|
128
116
|
for (const agent of restored) {
|
|
129
117
|
for (const message of agent.mailbox) seenMessageIds.add(message.id);
|
|
@@ -140,38 +128,6 @@ export function registerStatefulSubagents(
|
|
|
140
128
|
sweepTimer.unref();
|
|
141
129
|
});
|
|
142
130
|
|
|
143
|
-
pi.on("input", (event) => {
|
|
144
|
-
if (event.source === "extension") {
|
|
145
|
-
const nonce = extractOrchestrationNonce(event.text);
|
|
146
|
-
if (nonce && cancelledRecoveryNonces.delete(nonce)) return { action: "handled" as const };
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
rememberCancelledRecovery(orchestration.supersedePending(), cancelledRecoveryNonces);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
pi.on("before_agent_start", () => {
|
|
153
|
-
orchestration.beginTurn();
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
pi.on("before_provider_request", () => {
|
|
157
|
-
orchestration.observeAvailable();
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
pi.on("agent_end", (_event, ctx) => {
|
|
161
|
-
const ticket = orchestration.endTurn();
|
|
162
|
-
if (ticket && !hasPendingRootMessages(ctx)) queueOrchestrationFollowUp(pi, ctx, ticket);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
const settledEvents = pi as unknown as {
|
|
166
|
-
on(
|
|
167
|
-
event: "agent_settled",
|
|
168
|
-
handler: (event: unknown, ctx: ExtensionContext) => void,
|
|
169
|
-
): void;
|
|
170
|
-
};
|
|
171
|
-
settledEvents.on("agent_settled", (_event, ctx) => {
|
|
172
|
-
dispatchOrchestrationRecovery(pi, ctx, orchestration);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
131
|
pi.on("model_select", (event) => {
|
|
176
132
|
parentRuntime.model = event.model;
|
|
177
133
|
});
|
|
@@ -182,8 +138,6 @@ export function registerStatefulSubagents(
|
|
|
182
138
|
|
|
183
139
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
184
140
|
runtimeGeneration++;
|
|
185
|
-
rememberCancelledRecovery(orchestration.supersedePending(), cancelledRecoveryNonces);
|
|
186
|
-
orchestration.reset();
|
|
187
141
|
if (sweepTimer) clearInterval(sweepTimer);
|
|
188
142
|
sweepTimer = undefined;
|
|
189
143
|
for (const agentId of isolatedAgents.keys()) {
|
|
@@ -203,25 +157,23 @@ export function registerStatefulSubagents(
|
|
|
203
157
|
persistence = undefined;
|
|
204
158
|
if (cleanupError && ctx.hasUI) {
|
|
205
159
|
const reason = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
|
|
206
|
-
ctx.ui.notify(
|
|
207
|
-
`Some isolated subagent workspaces could not be removed: ${reason}`,
|
|
208
|
-
"warning",
|
|
209
|
-
);
|
|
160
|
+
ctx.ui.notify(`Some isolated subagent workspaces could not be removed: ${reason}`, "warning");
|
|
210
161
|
}
|
|
211
162
|
});
|
|
212
163
|
|
|
213
164
|
pi.registerTool({
|
|
214
165
|
name: "subagent_spawn",
|
|
215
166
|
label: "Spawn Subagent",
|
|
216
|
-
description:
|
|
167
|
+
description:
|
|
168
|
+
"Start an addressable background subagent, return immediately with an agentId, and receive its completion asynchronously.",
|
|
217
169
|
promptSnippet: "Start a reusable detached subagent; completion is delivered asynchronously",
|
|
218
170
|
promptGuidelines: [
|
|
219
|
-
"Do not
|
|
220
|
-
"
|
|
221
|
-
"Use one blocking subagent parallel call for multiple independent one-shot tasks; do not use repeated
|
|
222
|
-
"After
|
|
223
|
-
"Consume
|
|
224
|
-
"
|
|
171
|
+
"Do not use subagent_spawn for simple or critical-path work that the main agent can perform directly.",
|
|
172
|
+
"Use a single subagent_spawn only for a concrete bounded subtask that can run independently alongside useful main-agent work and has an isolation or specialization benefit such as independent review, bounded context/output, a distinct model/tool profile, or workspace isolation.",
|
|
173
|
+
"Use one blocking subagent parallel call for multiple independent one-shot tasks; do not use repeated subagent_spawn calls when no reuse or overlap is needed.",
|
|
174
|
+
"After subagent_spawn returns, do useful non-overlapping local work immediately. Call subagent_wait sparingly, only when the immediate next critical-path step requires the result and is blocked until it arrives; do not wait repeatedly by reflex.",
|
|
175
|
+
"Consume and synthesize available subagent_spawn completion messages; interrupt or close agents that are no longer needed.",
|
|
176
|
+
"subagent_spawn completion is delivered automatically. Do not poll, wait forever, or spawn additional agents without a distinct need.",
|
|
225
177
|
],
|
|
226
178
|
parameters: Type.Object({
|
|
227
179
|
agent: Type.String({ minLength: 1 }),
|
|
@@ -247,13 +199,7 @@ export function registerStatefulSubagents(
|
|
|
247
199
|
const scope = (params.agentScope ?? "user") as AgentScope;
|
|
248
200
|
assertSubagentDepthAllowed();
|
|
249
201
|
const cwd = params.cwd ?? ctx.cwd;
|
|
250
|
-
await confirmProjectAgent(
|
|
251
|
-
params.agent,
|
|
252
|
-
scope,
|
|
253
|
-
params.confirmProjectAgents ?? true,
|
|
254
|
-
ctx,
|
|
255
|
-
cwd,
|
|
256
|
-
);
|
|
202
|
+
await confirmProjectAgent(params.agent, scope, params.confirmProjectAgents ?? true, ctx, cwd);
|
|
257
203
|
const resolvedAgent = discoverAgents(cwd, scope, readSubagentSettings()).agents.find(
|
|
258
204
|
(agent) => agent.name === params.agent,
|
|
259
205
|
);
|
|
@@ -269,12 +215,7 @@ export function registerStatefulSubagents(
|
|
|
269
215
|
);
|
|
270
216
|
const requestedCwd = cwd;
|
|
271
217
|
if ((params.workspaceMode ?? "shared") === "shared" && !params.allowConcurrentWrites) {
|
|
272
|
-
assertNoSharedWriteConflict(
|
|
273
|
-
requireRegistry(),
|
|
274
|
-
params.agent,
|
|
275
|
-
requestedCwd,
|
|
276
|
-
scope,
|
|
277
|
-
);
|
|
218
|
+
assertNoSharedWriteConflict(requireRegistry(), params.agent, requestedCwd, scope);
|
|
278
219
|
}
|
|
279
220
|
const workspaceOwner = `pending-${randomUUID()}`;
|
|
280
221
|
const workspace =
|
|
@@ -298,10 +239,9 @@ export function registerStatefulSubagents(
|
|
|
298
239
|
throw error;
|
|
299
240
|
}
|
|
300
241
|
if (workspace) isolatedAgents.set(agent.id, workspaceOwner);
|
|
301
|
-
trackSpawnedAgent(orchestration, agent);
|
|
302
242
|
return result(
|
|
303
243
|
agent,
|
|
304
|
-
`Spawned ${agent.agent} as ${agent.id}.
|
|
244
|
+
`Spawned ${agent.agent} as ${agent.id}. Do useful non-overlapping work immediately. Completion will arrive asynchronously; call subagent_wait only if the immediate next critical-path step is blocked on this result.`,
|
|
305
245
|
);
|
|
306
246
|
},
|
|
307
247
|
});
|
|
@@ -334,7 +274,6 @@ export function registerStatefulSubagents(
|
|
|
334
274
|
isolatedAgents.has(existing.id),
|
|
335
275
|
);
|
|
336
276
|
const agent = await requireRegistry().followUp(params.agentId, params.task);
|
|
337
|
-
trackSpawnedAgent(orchestration, agent);
|
|
338
277
|
return result(agent, `Started follow-up for ${agent.id}.`);
|
|
339
278
|
},
|
|
340
279
|
});
|
|
@@ -384,9 +323,7 @@ export function registerStatefulSubagents(
|
|
|
384
323
|
}));
|
|
385
324
|
const text = summaries.length
|
|
386
325
|
? summaries
|
|
387
|
-
.map(
|
|
388
|
-
(message) => `${message.id} from ${message.senderId}: ${message.content}`,
|
|
389
|
-
)
|
|
326
|
+
.map((message) => `${message.id} from ${message.senderId}: ${message.content}`)
|
|
390
327
|
.join("\n")
|
|
391
328
|
: "No unread messages.";
|
|
392
329
|
return {
|
|
@@ -399,7 +336,8 @@ export function registerStatefulSubagents(
|
|
|
399
336
|
pi.registerTool({
|
|
400
337
|
name: "subagent_wait",
|
|
401
338
|
label: "Wait for Subagent",
|
|
402
|
-
description:
|
|
339
|
+
description:
|
|
340
|
+
"Wait for a stateful subagent turn without terminating it when the wait times out. Use sparingly, only when the immediate next critical-path step is blocked on this result.",
|
|
403
341
|
parameters: Type.Object({
|
|
404
342
|
agentId: Type.String(),
|
|
405
343
|
timeoutMs: Type.Optional(Type.Number({ minimum: 1, maximum: 3_600_000, default: 30_000 })),
|
|
@@ -412,7 +350,6 @@ export function registerStatefulSubagents(
|
|
|
412
350
|
}
|
|
413
351
|
try {
|
|
414
352
|
const waited = await requireRegistry().wait(params.agentId, params.timeoutMs, signal);
|
|
415
|
-
if (!waited.timedOut) orchestration.observe(waited.agent.id);
|
|
416
353
|
return result(
|
|
417
354
|
waited.agent,
|
|
418
355
|
waited.timedOut
|
|
@@ -436,9 +373,7 @@ export function registerStatefulSubagents(
|
|
|
436
373
|
content: [
|
|
437
374
|
{
|
|
438
375
|
type: "text",
|
|
439
|
-
text: agents.length
|
|
440
|
-
? agents.map(formatLine).join("\n")
|
|
441
|
-
: "No stateful subagents.",
|
|
376
|
+
text: agents.length ? agents.map(formatLine).join("\n") : "No stateful subagents.",
|
|
442
377
|
},
|
|
443
378
|
],
|
|
444
379
|
details: { agents: agents.map(summarizeAgent) },
|
|
@@ -481,7 +416,6 @@ export function registerStatefulSubagents(
|
|
|
481
416
|
async execute(_id, params) {
|
|
482
417
|
const existing = requireRegistry().get(params.agentId);
|
|
483
418
|
if (existing?.state === "closed" && !params.subtree) {
|
|
484
|
-
orchestration.resolve(existing.id);
|
|
485
419
|
const pendingOwner = isolatedAgents.get(existing.id);
|
|
486
420
|
if (pendingOwner) await workspaceManager.cleanup(pendingOwner);
|
|
487
421
|
isolatedAgents.delete(existing.id);
|
|
@@ -494,7 +428,6 @@ export function registerStatefulSubagents(
|
|
|
494
428
|
} finally {
|
|
495
429
|
await cleanupClosedWorkspaces(requireRegistry(), isolatedAgents, workspaceManager);
|
|
496
430
|
}
|
|
497
|
-
for (const closed of agents) orchestration.resolve(closed.id);
|
|
498
431
|
return {
|
|
499
432
|
content: [{ type: "text", text: `Closed ${agents.length} agent(s).` }],
|
|
500
433
|
details: {
|
|
@@ -509,7 +442,6 @@ export function registerStatefulSubagents(
|
|
|
509
442
|
} finally {
|
|
510
443
|
await cleanupClosedWorkspaces(requireRegistry(), isolatedAgents, workspaceManager);
|
|
511
444
|
}
|
|
512
|
-
orchestration.resolve(agent.id);
|
|
513
445
|
return result(agent, `Closed ${agent.id}.`);
|
|
514
446
|
},
|
|
515
447
|
});
|
|
@@ -530,7 +462,6 @@ export function registerStatefulSubagents(
|
|
|
530
462
|
isolatedAgents.clear();
|
|
531
463
|
}
|
|
532
464
|
seenMessageIds.clear();
|
|
533
|
-
orchestration.reset();
|
|
534
465
|
await persistence?.delete();
|
|
535
466
|
ctx.ui.notify("Cleared stateful subagents.", "info");
|
|
536
467
|
return;
|
|
@@ -577,12 +508,7 @@ export function assertFollowUpWriteAllowed(
|
|
|
577
508
|
isolatedWorkspace: boolean,
|
|
578
509
|
): void {
|
|
579
510
|
if (allowConcurrentWrites || isolatedWorkspace) return;
|
|
580
|
-
assertNoSharedWriteConflict(
|
|
581
|
-
registry,
|
|
582
|
-
agent.agent,
|
|
583
|
-
agent.cwd,
|
|
584
|
-
agent.agentScope ?? "user",
|
|
585
|
-
);
|
|
511
|
+
assertNoSharedWriteConflict(registry, agent.agent, agent.cwd, agent.agentScope ?? "user");
|
|
586
512
|
}
|
|
587
513
|
|
|
588
514
|
export function isWriteCapable(tools: string[] | undefined): boolean {
|
|
@@ -614,7 +540,10 @@ async function confirmProjectAgent(
|
|
|
614
540
|
throw new Error("Project-local subagent definitions require a trusted project");
|
|
615
541
|
}
|
|
616
542
|
if (confirm && ctx.hasUI) {
|
|
617
|
-
const approved = await ctx.ui.confirm(
|
|
543
|
+
const approved = await ctx.ui.confirm(
|
|
544
|
+
"Run project-local agent?",
|
|
545
|
+
`Agent: ${name}\nSource: ${agent.filePath}`,
|
|
546
|
+
);
|
|
618
547
|
if (!approved) throw new Error("Project-local subagent was not approved");
|
|
619
548
|
}
|
|
620
549
|
}
|
|
@@ -623,9 +552,7 @@ function isSameCwd(left: string, right: string): boolean {
|
|
|
623
552
|
return path.resolve(left) === path.resolve(right);
|
|
624
553
|
}
|
|
625
554
|
|
|
626
|
-
function normalizeContextMode(
|
|
627
|
-
value: "none" | "all" | "summary" | number | undefined,
|
|
628
|
-
): ContextMode {
|
|
555
|
+
function normalizeContextMode(value: "none" | "all" | "summary" | number | undefined): ContextMode {
|
|
629
556
|
if (value === undefined) return "none";
|
|
630
557
|
if (value === "none" || value === "all" || value === "summary") return value;
|
|
631
558
|
return Math.max(1, Math.floor(value));
|
|
@@ -680,85 +607,7 @@ function summarizeAgent(agent: ManagedAgent) {
|
|
|
680
607
|
};
|
|
681
608
|
}
|
|
682
609
|
|
|
683
|
-
function
|
|
684
|
-
orchestration: RootOrchestrationState,
|
|
685
|
-
agent: ManagedAgent,
|
|
686
|
-
): void {
|
|
687
|
-
orchestration.spawn(agent.id);
|
|
688
|
-
if (agent.state === "closed") orchestration.resolve(agent.id);
|
|
689
|
-
else if (agent.state !== "starting" && agent.state !== "running") {
|
|
690
|
-
orchestration.complete(agent.id);
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
function rememberCancelledRecovery(
|
|
695
|
-
ticket: OrchestrationRecoveryTicket | undefined,
|
|
696
|
-
cancelledNonces: Set<string>,
|
|
697
|
-
): void {
|
|
698
|
-
if (!ticket) return;
|
|
699
|
-
cancelledNonces.add(ticket.nonce);
|
|
700
|
-
if (cancelledNonces.size <= 64) return;
|
|
701
|
-
const oldest = cancelledNonces.values().next().value;
|
|
702
|
-
if (oldest) cancelledNonces.delete(oldest);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
function extractOrchestrationNonce(text: string): string | undefined {
|
|
706
|
-
const marker = `<!-- ${ORCHESTRATION_MARKER_PREFIX}`;
|
|
707
|
-
const start = text.lastIndexOf(marker);
|
|
708
|
-
if (start < 0) return undefined;
|
|
709
|
-
const valueStart = start + marker.length;
|
|
710
|
-
const end = text.indexOf(" -->", valueStart);
|
|
711
|
-
return end < 0 ? undefined : text.slice(valueStart, end);
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
function queueOrchestrationFollowUp(
|
|
715
|
-
pi: ExtensionAPI,
|
|
716
|
-
ctx: ExtensionContext,
|
|
717
|
-
ticket: OrchestrationRecoveryTicket,
|
|
718
|
-
): void {
|
|
719
|
-
try {
|
|
720
|
-
pi.sendUserMessage(ticket.prompt, { deliverAs: "followUp" });
|
|
721
|
-
} catch (error) {
|
|
722
|
-
if (ctx.hasUI) {
|
|
723
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
724
|
-
ctx.ui.notify(`Subagent coordination follow-up failed: ${reason}`, "warning");
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
function dispatchOrchestrationRecovery(
|
|
730
|
-
pi: ExtensionAPI,
|
|
731
|
-
ctx: ExtensionContext,
|
|
732
|
-
orchestration: RootOrchestrationState,
|
|
733
|
-
): boolean {
|
|
734
|
-
const ticket = orchestration.pendingTicket();
|
|
735
|
-
if (!ticket || !orchestration.isCurrent(ticket)) return false;
|
|
736
|
-
if (hasPendingRootMessages(ctx)) return false;
|
|
737
|
-
try {
|
|
738
|
-
pi.sendUserMessage(ticket.prompt);
|
|
739
|
-
orchestration.markDelivered(ticket);
|
|
740
|
-
return true;
|
|
741
|
-
} catch (error) {
|
|
742
|
-
if (ctx.hasUI) {
|
|
743
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
744
|
-
ctx.ui.notify(`Subagent coordination prompt failed: ${reason}`, "warning");
|
|
745
|
-
}
|
|
746
|
-
return false;
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
function hasPendingRootMessages(ctx: ExtensionContext): boolean {
|
|
751
|
-
try {
|
|
752
|
-
return ctx.hasPendingMessages();
|
|
753
|
-
} catch {
|
|
754
|
-
return true;
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
function sendDetachedCompletion(
|
|
759
|
-
pi: ExtensionAPI,
|
|
760
|
-
completion: AgentTurnCompletion,
|
|
761
|
-
): void {
|
|
610
|
+
function sendDetachedCompletion(pi: ExtensionAPI, completion: AgentTurnCompletion): void {
|
|
762
611
|
const content = buildDetachedCompletionMessage(completion);
|
|
763
612
|
pi.sendMessage(
|
|
764
613
|
{
|
|
@@ -798,8 +647,8 @@ export function buildDetachedCompletionMessage(completion: AgentTurnCompletion):
|
|
|
798
647
|
}
|
|
799
648
|
|
|
800
649
|
function sanitizeCompletionLine(value: string, maxBytes: number): string {
|
|
801
|
-
return truncateUtf8(redactPrivateText(value), maxBytes)
|
|
802
|
-
.replace(/[\u0000-\u001f\u007f]+/g, " ")
|
|
650
|
+
return truncateUtf8(redactPrivateText(value), maxBytes)
|
|
651
|
+
.text.replace(/[\u0000-\u001f\u007f]+/g, " ")
|
|
803
652
|
.replace(/\s+/g, " ")
|
|
804
653
|
.trim();
|
|
805
654
|
}
|
package/src/subagents.ts
CHANGED
|
@@ -39,9 +39,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
39
39
|
"Use no subagent for simple answers, quick targeted edits, latency-sensitive one-step work, tasks requiring frequent user back-and-forth, or critical-path work needed for the main agent's next action.",
|
|
40
40
|
"A single blocking subagent call should be used only when independent context, high-volume output isolation, or an external review is worth waiting for; otherwise do the task in the main agent.",
|
|
41
41
|
"For one-shot parallel work, use a single subagent call with tasks instead of repeated subagent_spawn calls, even when the user explicitly requests multiple agents.",
|
|
42
|
-
"When subagent_spawn is available, use it only
|
|
43
|
-
"After
|
|
44
|
-
"Consume
|
|
42
|
+
"When subagent_spawn is available, use it only for a concrete bounded subtask that can run independently alongside useful main-agent work and has a parallel, isolation, or specialization benefit; otherwise keep the work in the main agent.",
|
|
43
|
+
"After subagent_spawn returns, do useful non-overlapping work immediately. Call subagent_wait sparingly, only when the immediate next critical-path step requires the result and is blocked until it arrives; do not wait repeatedly by reflex.",
|
|
44
|
+
"Consume and synthesize available subagent_spawn completion messages; interrupt or close agents that are no longer needed.",
|
|
45
45
|
"Use subagent parallel mode with 2-4 parallel read-only subagents when work has broad independent branches; prefer scout or reviewer for fan-out and add an aggregator when synthesis helps.",
|
|
46
46
|
"Use more than 4 subagent tasks only when clearly justified by distinct independent branches, and stay within the existing hard max 8 parallel tasks.",
|
|
47
47
|
"Do not use subagent parallel mode for write-heavy implementation touching the same files or shared state; serialize those changes in the main agent or one worker.",
|
|
@@ -65,7 +65,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
65
65
|
|
|
66
66
|
pi.on("tool_result", (event) => {
|
|
67
67
|
if (event.toolName !== "subagent") return;
|
|
68
|
-
if ((event.details as (SubagentDetails & { isError?: boolean }) | undefined)?.isError)
|
|
68
|
+
if ((event.details as (SubagentDetails & { isError?: boolean }) | undefined)?.isError)
|
|
69
|
+
return { isError: true };
|
|
69
70
|
});
|
|
70
71
|
|
|
71
72
|
pi.on("session_start", (_event, ctx) => {
|
package/src/orchestration.ts
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
|
|
3
|
-
export type OrchestrationRecoveryTicket = {
|
|
4
|
-
generation: number;
|
|
5
|
-
revision: number;
|
|
6
|
-
nonce: string;
|
|
7
|
-
prompt: string;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
type DelegatedAgent = {
|
|
11
|
-
live: boolean;
|
|
12
|
-
resultAvailable: boolean;
|
|
13
|
-
observedInTurn: boolean;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export const ORCHESTRATION_MARKER_PREFIX = "pi-subagent-orchestration:";
|
|
17
|
-
|
|
18
|
-
const RECOVERY_PROMPT = [
|
|
19
|
-
"Delegated subagents still need root coordination.",
|
|
20
|
-
"Do not yield permanently while current delegated work is unresolved.",
|
|
21
|
-
"Continue useful non-overlapping local work, or call subagent_wait when no useful local continuation remains.",
|
|
22
|
-
"Consume available subagent results and synthesize them before finishing.",
|
|
23
|
-
"Interrupt or close agents that are no longer needed; do not wait forever or spawn more agents unnecessarily.",
|
|
24
|
-
].join(" ");
|
|
25
|
-
|
|
26
|
-
/** Ephemeral root-turn coordination state. Never persisted across sessions. */
|
|
27
|
-
export class RootOrchestrationState {
|
|
28
|
-
private generation = 0;
|
|
29
|
-
private revision = 0;
|
|
30
|
-
private turnOpen = false;
|
|
31
|
-
private agents = new Map<string, DelegatedAgent>();
|
|
32
|
-
private pending: OrchestrationRecoveryTicket | undefined;
|
|
33
|
-
private deliveredRevision: number | undefined;
|
|
34
|
-
|
|
35
|
-
reset(): void {
|
|
36
|
-
this.generation += 1;
|
|
37
|
-
this.revision = 0;
|
|
38
|
-
this.turnOpen = false;
|
|
39
|
-
this.agents.clear();
|
|
40
|
-
this.pending = undefined;
|
|
41
|
-
this.deliveredRevision = undefined;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
beginTurn(): void {
|
|
45
|
-
this.turnOpen = true;
|
|
46
|
-
if (this.pending) this.deliveredRevision = this.pending.revision;
|
|
47
|
-
this.observeAvailable();
|
|
48
|
-
// New root work supersedes an intent that has not been delivered yet. The
|
|
49
|
-
// turn gets a chance to coordinate before agent_end evaluates it again.
|
|
50
|
-
this.pending = undefined;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
spawn(agentId: string): void {
|
|
54
|
-
this.agents.set(agentId, {
|
|
55
|
-
live: true,
|
|
56
|
-
resultAvailable: false,
|
|
57
|
-
observedInTurn: false,
|
|
58
|
-
});
|
|
59
|
-
this.bumpRevision();
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
complete(agentId: string): void {
|
|
63
|
-
const existing = this.agents.get(agentId);
|
|
64
|
-
if (!existing) return;
|
|
65
|
-
this.agents.set(agentId, {
|
|
66
|
-
live: false,
|
|
67
|
-
resultAvailable: true,
|
|
68
|
-
observedInTurn: false,
|
|
69
|
-
});
|
|
70
|
-
this.bumpRevision();
|
|
71
|
-
if (!this.turnOpen) this.ensureRecovery();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
resolve(agentId: string): void {
|
|
75
|
-
if (!this.agents.delete(agentId)) return;
|
|
76
|
-
this.bumpRevision();
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
observe(agentId: string): void {
|
|
80
|
-
const agent = this.agents.get(agentId);
|
|
81
|
-
if (agent && !agent.live && agent.resultAvailable) agent.observedInTurn = true;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
observeAvailable(): void {
|
|
85
|
-
for (const agent of this.agents.values()) {
|
|
86
|
-
if (!agent.live && agent.resultAvailable) agent.observedInTurn = true;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
endTurn(): OrchestrationRecoveryTicket | undefined {
|
|
91
|
-
if (this.turnOpen) {
|
|
92
|
-
for (const [id, agent] of this.agents) {
|
|
93
|
-
if (!agent.live && agent.resultAvailable && agent.observedInTurn) {
|
|
94
|
-
this.agents.delete(id);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
this.turnOpen = false;
|
|
98
|
-
}
|
|
99
|
-
return this.ensureRecovery();
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
pendingTicket(): OrchestrationRecoveryTicket | undefined {
|
|
103
|
-
return this.pending;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
isCurrent(ticket: OrchestrationRecoveryTicket): boolean {
|
|
107
|
-
return (
|
|
108
|
-
ticket.generation === this.generation &&
|
|
109
|
-
ticket.revision === this.revision &&
|
|
110
|
-
ticket.nonce === this.pending?.nonce &&
|
|
111
|
-
this.hasUnresolved()
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
markDelivered(ticket: OrchestrationRecoveryTicket): void {
|
|
116
|
-
if (!this.isCurrent(ticket)) return;
|
|
117
|
-
this.pending = undefined;
|
|
118
|
-
this.deliveredRevision = ticket.revision;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
supersedePending(): OrchestrationRecoveryTicket | undefined {
|
|
122
|
-
const ticket = this.pending;
|
|
123
|
-
if (!ticket) return undefined;
|
|
124
|
-
this.pending = undefined;
|
|
125
|
-
this.deliveredRevision = ticket.revision;
|
|
126
|
-
return ticket;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
hasUnresolved(): boolean {
|
|
130
|
-
return this.agents.size > 0;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
liveAgentIds(): string[] {
|
|
134
|
-
return [...this.agents]
|
|
135
|
-
.filter(([, agent]) => agent.live)
|
|
136
|
-
.map(([id]) => id)
|
|
137
|
-
.sort();
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
private ensureRecovery(): OrchestrationRecoveryTicket | undefined {
|
|
141
|
-
if (!this.hasUnresolved()) {
|
|
142
|
-
this.pending = undefined;
|
|
143
|
-
return undefined;
|
|
144
|
-
}
|
|
145
|
-
if (this.deliveredRevision === this.revision) return undefined;
|
|
146
|
-
if (this.pending?.revision === this.revision && this.pending.generation === this.generation) {
|
|
147
|
-
return this.pending;
|
|
148
|
-
}
|
|
149
|
-
const nonce = randomUUID();
|
|
150
|
-
this.pending = {
|
|
151
|
-
generation: this.generation,
|
|
152
|
-
revision: this.revision,
|
|
153
|
-
nonce,
|
|
154
|
-
prompt: `${RECOVERY_PROMPT}\n<!-- ${ORCHESTRATION_MARKER_PREFIX}${nonce} -->`,
|
|
155
|
-
};
|
|
156
|
-
return this.pending;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
private bumpRevision(): void {
|
|
160
|
-
this.revision += 1;
|
|
161
|
-
this.pending = undefined;
|
|
162
|
-
this.deliveredRevision = undefined;
|
|
163
|
-
}
|
|
164
|
-
}
|