@narumitw/pi-subagents 0.43.0 → 0.43.1
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 +53 -13
- package/package.json +1 -1
- package/src/agents.ts +16 -0
- package/src/config-ui.ts +151 -20
- package/src/consult-render.ts +194 -0
- package/src/consult.ts +164 -37
- package/src/cwd-policy.ts +183 -0
- package/src/execution.ts +127 -64
- package/src/in-process-transport.ts +3 -3
- package/src/inspect-render.ts +234 -0
- package/src/inspect.ts +51 -3
- package/src/persistence.ts +29 -0
- package/src/registry.ts +12 -0
- package/src/render-common.ts +252 -0
- package/src/render.ts +134 -99
- package/src/runner.ts +2 -0
- package/src/settings.ts +111 -11
- package/src/stateful-guidance.ts +35 -0
- package/src/stateful-lifecycle.ts +31 -0
- package/src/stateful-render.ts +249 -0
- package/src/stateful-safety.ts +91 -0
- package/src/stateful.ts +235 -218
- package/src/subagents.ts +60 -18
- package/src/subprocess-transport.ts +19 -2
package/src/stateful.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import * as path from "node:path";
|
|
3
2
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
4
3
|
import {
|
|
5
4
|
defineTool,
|
|
@@ -13,9 +12,15 @@ import {
|
|
|
13
12
|
discoverAgents,
|
|
14
13
|
isThinkingLevel,
|
|
15
14
|
type SubagentRuntimeSettings,
|
|
15
|
+
type SubagentSettings,
|
|
16
16
|
THINKING_LEVELS,
|
|
17
17
|
} from "./agents.js";
|
|
18
18
|
import { buildContextSnapshot, type ContextMode, redactPrivateText } from "./context.js";
|
|
19
|
+
import {
|
|
20
|
+
assertDelegationTargetAllowed,
|
|
21
|
+
resolveSubagentTarget,
|
|
22
|
+
targetPolicyAudit,
|
|
23
|
+
} from "./cwd-policy.js";
|
|
19
24
|
import { assertSubagentDepthAllowed } from "./execution.js";
|
|
20
25
|
import {
|
|
21
26
|
type ChildSessionFactory,
|
|
@@ -31,8 +36,22 @@ import {
|
|
|
31
36
|
type AgentTurnCompletion,
|
|
32
37
|
type ManagedAgent,
|
|
33
38
|
} from "./registry.js";
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
39
|
+
import { DEFAULT_DELEGATION_CWD_POLICY, readSubagentSettings } from "./settings.js";
|
|
40
|
+
import { createSpawnPromptGuidelines } from "./stateful-guidance.js";
|
|
41
|
+
import { assertCurrentSpawn, disposeStatefulRuntime } from "./stateful-lifecycle.js";
|
|
42
|
+
import { createStatefulToolRenderer } from "./stateful-render.js";
|
|
43
|
+
import {
|
|
44
|
+
assertFollowUpWriteAllowed,
|
|
45
|
+
assertNoSharedWriteConflict,
|
|
46
|
+
confirmProjectAgent,
|
|
47
|
+
} from "./stateful-safety.js";
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
assertFollowUpWriteAllowed,
|
|
51
|
+
assertNoSharedWriteConflict,
|
|
52
|
+
isWriteCapable,
|
|
53
|
+
} from "./stateful-safety.js";
|
|
54
|
+
|
|
36
55
|
import {
|
|
37
56
|
MailboxParamsSchema,
|
|
38
57
|
ManageParamsSchema,
|
|
@@ -60,45 +79,12 @@ const MAX_COMPLETION_ERROR_BYTES = 512;
|
|
|
60
79
|
const MAX_COMPLETIONS_PER_MESSAGE = 16;
|
|
61
80
|
const COMPLETION_BATCH_DELAY_MS = 10;
|
|
62
81
|
|
|
63
|
-
function createSpawnPromptGuidelines(
|
|
64
|
-
completionDelivery: CompletionDelivery,
|
|
65
|
-
blockingEnabled = true,
|
|
66
|
-
): string[] {
|
|
67
|
-
const deliveryGuidance =
|
|
68
|
-
completionDelivery === "auto-resume"
|
|
69
|
-
? blockingEnabled
|
|
70
|
-
? "With subagent_spawn completion delivery set to auto-resume, prefer one subagent_spawn for broad asynchronous research or review that covers related branches even when the final answer depends on its result; do not choose blocking parallel fan-out merely to keep delegation in the same turn."
|
|
71
|
-
: "With subagent_spawn completion delivery set to auto-resume, prefer one subagent_spawn for broad asynchronous research or review that covers related branches even when the final answer depends on its result."
|
|
72
|
-
: blockingEnabled
|
|
73
|
-
? "With subagent_spawn completion delivery set to next-turn (the default), prefer one subagent_spawn for broad asynchronous research or review only when the current response does not depend on its result; use the blocking subagent when the final answer depends on the detached result."
|
|
74
|
-
: "With subagent_spawn completion delivery set to next-turn (the default), use subagent_spawn only when the current response does not depend on its result; complete final-answer-dependent work directly because an idle root is not awakened.";
|
|
75
|
-
const noLocalWorkGuidance =
|
|
76
|
-
completionDelivery === "auto-resume"
|
|
77
|
-
? "After subagent_spawn returns, do useful non-overlapping local work immediately. If none remains, briefly tell the user what subagent_spawn launched and end the response; auto-resume will request a synthesis turn after completion."
|
|
78
|
-
: "After subagent_spawn returns, do useful non-overlapping local work immediately. If none remains, briefly tell the user what subagent_spawn launched and end the response only when the current response does not depend on its result; next-turn delivery will not wake an idle root.";
|
|
79
|
-
return [
|
|
80
|
-
"Do not use subagent_spawn for simple or critical-path work that the main agent can perform directly.",
|
|
81
|
-
"Set subagent_spawn thinkingLevel to the lowest sufficient thinking level for the delegated task: use off or minimal for extraction, formatting, or mechanical work; low for straightforward bounded work; medium for ordinary multi-step research or implementation; high for complex debugging, design, review, or cross-file analysis; xhigh for highly ambiguous, cross-system, or high-risk analysis; and max only for the hardest tasks when quality clearly outweighs latency and cost. Omit subagent_spawn thinkingLevel only to preserve the agent or child default.",
|
|
82
|
-
deliveryGuidance,
|
|
83
|
-
"Use a single subagent_spawn only for a concrete bounded subtask that can run independently and has an isolation or specialization benefit such as independent review, bounded context/output, a distinct model/tool profile, or workspace isolation.",
|
|
84
|
-
...(blockingEnabled
|
|
85
|
-
? [
|
|
86
|
-
"Use the blocking subagent instead of subagent_spawn when synchronous output is required before the main agent can continue and waiting is intentional; queued steering cannot be processed until that blocking call returns.",
|
|
87
|
-
"When subagent_spawn fits the completion-delivery policy, do not choose a blocking parallel subagent merely to keep delegation in the same turn.",
|
|
88
|
-
]
|
|
89
|
-
: []),
|
|
90
|
-
"Add another subagent_spawn only for truly independent work with safe workspace concurrency.",
|
|
91
|
-
noLocalWorkGuidance,
|
|
92
|
-
'Consume and synthesize available subagent_spawn completion messages; use subagent_manage with action "interrupt" or "close" for agents that are no longer needed.',
|
|
93
|
-
'Completion from subagent_spawn is delivered automatically. Do not poll with subagent_manage action "list" or subagent_mailbox action "read", repeatedly check progress, or duplicate the delegated work.',
|
|
94
|
-
];
|
|
95
|
-
}
|
|
96
|
-
|
|
97
82
|
export interface StatefulSubagentDependencies {
|
|
98
83
|
blockingEnabled?: boolean;
|
|
99
84
|
createInProcessSession?: ChildSessionFactory;
|
|
100
85
|
workspaceManager?: WorkspaceManager;
|
|
101
86
|
settings?: SubagentRuntimeSettings;
|
|
87
|
+
getSettings?: () => SubagentSettings | undefined;
|
|
102
88
|
}
|
|
103
89
|
|
|
104
90
|
export interface StatefulSubagentRuntimeStatus {
|
|
@@ -114,6 +100,7 @@ export interface StatefulSubagentController {
|
|
|
114
100
|
getCompletionDelivery(): CompletionDelivery;
|
|
115
101
|
setCompletionDelivery(value: CompletionDelivery): void;
|
|
116
102
|
setAgentCatalog(value: string): void;
|
|
103
|
+
refreshSettingsGuidance(): void;
|
|
117
104
|
getRuntimeStatus(): StatefulSubagentRuntimeStatus;
|
|
118
105
|
listAgents(includeClosed?: boolean): ManagedAgent[];
|
|
119
106
|
listRunInspection(includeClosed?: boolean): AgentRunInspectionSummary[];
|
|
@@ -144,23 +131,31 @@ export function registerStatefulSubagents(
|
|
|
144
131
|
let persistence: AgentPersistence | undefined;
|
|
145
132
|
let sweepTimer: NodeJS.Timeout | undefined;
|
|
146
133
|
let runtimeGeneration = 0;
|
|
134
|
+
let runtimeTransition: Promise<void> = Promise.resolve();
|
|
147
135
|
const workspaceManager = dependencies.workspaceManager ?? new WorkspaceManager();
|
|
148
136
|
const isolatedAgents = new Map<string, string>();
|
|
149
137
|
const seenMessageIds = new Set<string>();
|
|
150
138
|
const parentRuntime: ParentRuntimeSnapshot = { model: undefined, thinkingLevel: "off" };
|
|
139
|
+
const getCurrentSettings = () =>
|
|
140
|
+
dependencies.getSettings ? dependencies.getSettings() : readSubagentSettings();
|
|
151
141
|
|
|
152
142
|
const clearAgents = async (): Promise<number> => {
|
|
143
|
+
const generation = runtimeGeneration;
|
|
153
144
|
const currentRegistry = registry;
|
|
145
|
+
const currentPersistence = persistence;
|
|
154
146
|
if (!currentRegistry) return 0;
|
|
155
147
|
const count = currentRegistry.list().length;
|
|
156
|
-
|
|
148
|
+
const clear = async () => {
|
|
157
149
|
await currentRegistry.closeAll();
|
|
158
|
-
|
|
150
|
+
if (generation !== runtimeGeneration) return;
|
|
159
151
|
await workspaceManager.cleanupAll();
|
|
160
152
|
isolatedAgents.clear();
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
153
|
+
seenMessageIds.clear();
|
|
154
|
+
await currentPersistence?.delete();
|
|
155
|
+
};
|
|
156
|
+
const transition = runtimeTransition.then(clear, clear);
|
|
157
|
+
runtimeTransition = transition.catch(() => undefined);
|
|
158
|
+
await transition;
|
|
164
159
|
return count;
|
|
165
160
|
};
|
|
166
161
|
const controller: StatefulSubagentController = {
|
|
@@ -176,6 +171,9 @@ export function registerStatefulSubagents(
|
|
|
176
171
|
agentCatalog = value;
|
|
177
172
|
refreshSpawnToolRegistration?.();
|
|
178
173
|
},
|
|
174
|
+
refreshSettingsGuidance() {
|
|
175
|
+
refreshSpawnToolRegistration?.();
|
|
176
|
+
},
|
|
179
177
|
getRuntimeStatus() {
|
|
180
178
|
const counts = registry?.inspectionCounts() ?? { activeAgents: 0, retainedAgents: 0 };
|
|
181
179
|
return {
|
|
@@ -213,79 +211,125 @@ export function registerStatefulSubagents(
|
|
|
213
211
|
const generation = ++runtimeGeneration;
|
|
214
212
|
completionBroker?.close();
|
|
215
213
|
completionBroker = undefined;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
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
|
-
})
|
|
214
|
+
if (sweepTimer) clearInterval(sweepTimer);
|
|
215
|
+
sweepTimer = undefined;
|
|
216
|
+
const previousRegistry = registry;
|
|
217
|
+
registry = undefined;
|
|
218
|
+
persistence = undefined;
|
|
219
|
+
isolatedAgents.clear();
|
|
220
|
+
seenMessageIds.clear();
|
|
221
|
+
const initialize = async () => {
|
|
222
|
+
const cleanupErrors = await disposeStatefulRuntime(previousRegistry, workspaceManager);
|
|
223
|
+
if (generation !== runtimeGeneration) return;
|
|
224
|
+
if (cleanupErrors.length > 0 && ctx.hasUI) {
|
|
225
|
+
ctx.ui.notify(
|
|
226
|
+
`Previous subagent runtime cleanup reported ${cleanupErrors.length} error(s).`,
|
|
227
|
+
"warning",
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
parentRuntime.model = ctx.model;
|
|
231
|
+
parentRuntime.thinkingLevel = normalizeRuntimeThinkingLevel(pi.getThinkingLevel());
|
|
232
|
+
const owner =
|
|
233
|
+
ctx.sessionManager.getSessionId?.() ??
|
|
234
|
+
ctx.sessionManager.getSessionFile?.() ??
|
|
235
|
+
`ephemeral:${ctx.cwd}`;
|
|
236
|
+
const sessionPersistence = new AgentPersistence(owner, {
|
|
237
|
+
retentionDays: settings.retentionDays,
|
|
238
|
+
maxStoredAgents: settings.maxStoredAgents,
|
|
239
|
+
});
|
|
240
|
+
const sessionBroker = new CompletionDeliveryBroker(pi, ctx, completionDelivery, {
|
|
241
|
+
onDeliveryError: (error) => {
|
|
242
|
+
if (!ctx.hasUI) return;
|
|
243
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
244
|
+
ctx.ui.notify(`Subagent completion delivery failed: ${reason}`, "warning");
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
const transport =
|
|
248
|
+
transportKind === "in-process"
|
|
249
|
+
? new InProcessTransport({
|
|
250
|
+
modelRegistry: ctx.modelRegistry,
|
|
251
|
+
getParentRuntime: () => ({ ...parentRuntime }),
|
|
252
|
+
createSession: dependencies.createInProcessSession,
|
|
253
|
+
discoverAgent: (agent) =>
|
|
254
|
+
discoverAgents(
|
|
255
|
+
agent.cwd,
|
|
256
|
+
agent.agentScope ?? "user",
|
|
257
|
+
getCurrentSettings(),
|
|
258
|
+
).agents.find((candidate) => candidate.name === agent.agent),
|
|
259
|
+
})
|
|
260
|
+
: new SubprocessTransport({ getSettings: getCurrentSettings });
|
|
261
|
+
const nextRegistry = new AgentRegistry(transport, {
|
|
262
|
+
maxAgents: settings.maxAgents,
|
|
263
|
+
maxActiveTurns: settings.maxActiveTurns,
|
|
264
|
+
maxDepth: settings.maxDepth,
|
|
265
|
+
maxChildrenPerAgent: settings.maxChildrenPerAgent,
|
|
266
|
+
maxMailboxMessages: settings.maxMailboxMessages,
|
|
267
|
+
maxMailboxMessageBytes: settings.maxMailboxMessageBytes,
|
|
268
|
+
idleTtlMs: settings.idleTtlMs,
|
|
269
|
+
onChange: async (agents) => {
|
|
270
|
+
await sessionPersistence.save(agents);
|
|
271
|
+
if (generation !== runtimeGeneration) return;
|
|
272
|
+
for (const agent of agents) {
|
|
273
|
+
for (const message of agent.mailbox) {
|
|
274
|
+
if (seenMessageIds.has(message.id)) continue;
|
|
275
|
+
seenMessageIds.add(message.id);
|
|
276
|
+
pi.appendEntry("pi-subagent-message", {
|
|
277
|
+
senderId: message.senderId,
|
|
278
|
+
recipientId: message.recipientId,
|
|
279
|
+
content: redactPrivateText(message.content).slice(0, 160),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
262
282
|
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
completionBroker?.enqueue(completion);
|
|
268
|
-
},
|
|
269
|
-
});
|
|
270
|
-
const restored = sessionPersistence
|
|
271
|
-
.load()
|
|
272
|
-
.filter(
|
|
273
|
-
(agent) =>
|
|
274
|
-
(agent.agentScope !== "project" && agent.agentScope !== "both") || ctx.isProjectTrusted(),
|
|
275
|
-
);
|
|
276
|
-
for (const agent of restored) {
|
|
277
|
-
for (const message of agent.mailbox) seenMessageIds.add(message.id);
|
|
278
|
-
}
|
|
279
|
-
registry.restore(restored);
|
|
280
|
-
const sweepEveryMs = Math.max(1_000, Math.min(settings.idleTtlMs ?? 60 * 60 * 1000, 60_000));
|
|
281
|
-
sweepTimer = setInterval(() => {
|
|
282
|
-
void registry?.sweepExpired().catch((error: unknown) => {
|
|
283
|
-
if (!ctx.hasUI) return;
|
|
284
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
285
|
-
ctx.ui.notify(`Subagent expiry cleanup failed: ${reason}`, "warning");
|
|
283
|
+
},
|
|
284
|
+
onTurnComplete: (completion) => {
|
|
285
|
+
if (generation === runtimeGeneration) sessionBroker.enqueue(completion);
|
|
286
|
+
},
|
|
286
287
|
});
|
|
287
|
-
|
|
288
|
-
|
|
288
|
+
const restored = sessionPersistence
|
|
289
|
+
.load()
|
|
290
|
+
.filter(
|
|
291
|
+
(agent) =>
|
|
292
|
+
agent.workspaceMode !== "worktree" &&
|
|
293
|
+
((agent.agentScope !== "project" && agent.agentScope !== "both") ||
|
|
294
|
+
ctx.isProjectTrusted()),
|
|
295
|
+
)
|
|
296
|
+
.flatMap((agent) => {
|
|
297
|
+
try {
|
|
298
|
+
const target = resolveSubagentTarget({
|
|
299
|
+
workspace: ctx.cwd,
|
|
300
|
+
requestedCwd: agent.cwd,
|
|
301
|
+
currentProjectTrusted: ctx.isProjectTrusted(),
|
|
302
|
+
});
|
|
303
|
+
return [{ ...agent, cwd: target.cwd, target: targetPolicyAudit(target) }];
|
|
304
|
+
} catch {
|
|
305
|
+
return [];
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
for (const agent of restored) {
|
|
309
|
+
for (const message of agent.mailbox) seenMessageIds.add(message.id);
|
|
310
|
+
}
|
|
311
|
+
nextRegistry.restore(restored);
|
|
312
|
+
if (generation !== runtimeGeneration) {
|
|
313
|
+
sessionBroker.close();
|
|
314
|
+
await disposeStatefulRuntime(nextRegistry, workspaceManager);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
registry = nextRegistry;
|
|
318
|
+
persistence = sessionPersistence;
|
|
319
|
+
completionBroker = sessionBroker;
|
|
320
|
+
const sweepEveryMs = Math.max(1_000, Math.min(settings.idleTtlMs ?? 60 * 60 * 1000, 60_000));
|
|
321
|
+
sweepTimer = setInterval(() => {
|
|
322
|
+
void nextRegistry.sweepExpired().catch((error: unknown) => {
|
|
323
|
+
if (!ctx.hasUI || generation !== runtimeGeneration) return;
|
|
324
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
325
|
+
ctx.ui.notify(`Subagent expiry cleanup failed: ${reason}`, "warning");
|
|
326
|
+
});
|
|
327
|
+
}, sweepEveryMs);
|
|
328
|
+
sweepTimer.unref();
|
|
329
|
+
};
|
|
330
|
+
const transition = runtimeTransition.then(initialize, initialize);
|
|
331
|
+
runtimeTransition = transition.catch(() => undefined);
|
|
332
|
+
await transition;
|
|
289
333
|
});
|
|
290
334
|
|
|
291
335
|
pi.on("agent_start", () => {
|
|
@@ -310,35 +354,28 @@ export function registerStatefulSubagents(
|
|
|
310
354
|
completionBroker = undefined;
|
|
311
355
|
if (sweepTimer) clearInterval(sweepTimer);
|
|
312
356
|
sweepTimer = undefined;
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
357
|
+
const previousRegistry = registry;
|
|
358
|
+
registry = undefined;
|
|
359
|
+
persistence = undefined;
|
|
316
360
|
isolatedAgents.clear();
|
|
317
361
|
seenMessageIds.clear();
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
registry = undefined;
|
|
328
|
-
persistence = undefined;
|
|
329
|
-
}
|
|
330
|
-
if (cleanupError && ctx.hasUI) {
|
|
331
|
-
const reason = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
|
|
332
|
-
ctx.ui.notify(`Some isolated subagent workspaces could not be removed: ${reason}`, "warning");
|
|
333
|
-
}
|
|
362
|
+
const shutdown = async () => {
|
|
363
|
+
const errors = await disposeStatefulRuntime(previousRegistry, workspaceManager);
|
|
364
|
+
if (errors.length > 0 && ctx.hasUI) {
|
|
365
|
+
ctx.ui.notify(`Subagent shutdown cleanup reported ${errors.length} error(s).`, "warning");
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
const transition = runtimeTransition.then(shutdown, shutdown);
|
|
369
|
+
runtimeTransition = transition.catch(() => undefined);
|
|
370
|
+
await transition;
|
|
334
371
|
});
|
|
335
372
|
|
|
336
|
-
const baseSpawnDescription =
|
|
337
|
-
|
|
373
|
+
const baseSpawnDescription = () =>
|
|
374
|
+
`Start an addressable background subagent with an optional thinking level chosen for the task difficulty, return immediately with an agentId, and receive its completion asynchronously. Working-directory target policy: ${dependencies.getSettings?.()?.cwdPolicy?.delegation ?? DEFAULT_DELEGATION_CWD_POLICY}. This controls launch targets and protected project resources, not filesystem access or sandboxing.`;
|
|
338
375
|
const spawnTool = defineTool({
|
|
339
376
|
name: "subagent_spawn",
|
|
340
377
|
label: "Spawn Subagent",
|
|
341
|
-
description: appendAgentCatalog(baseSpawnDescription, agentCatalog),
|
|
378
|
+
description: appendAgentCatalog(baseSpawnDescription(), agentCatalog),
|
|
342
379
|
promptSnippet: "Start a reusable detached subagent; completion is delivered asynchronously",
|
|
343
380
|
promptGuidelines: createSpawnPromptGuidelines(completionDelivery, blockingEnabled),
|
|
344
381
|
parameters: Type.Object({
|
|
@@ -362,12 +399,33 @@ export function registerStatefulSubagents(
|
|
|
362
399
|
}),
|
|
363
400
|
),
|
|
364
401
|
}),
|
|
365
|
-
|
|
402
|
+
...createStatefulToolRenderer("spawn"),
|
|
403
|
+
async execute(_id, params, signal, _update, ctx) {
|
|
366
404
|
const scope = (params.agentScope ?? "user") as AgentScope;
|
|
367
405
|
assertSubagentDepthAllowed();
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
const
|
|
406
|
+
const generation = runtimeGeneration;
|
|
407
|
+
const currentSettings = getCurrentSettings();
|
|
408
|
+
const target = resolveSubagentTarget({
|
|
409
|
+
workspace: ctx.cwd,
|
|
410
|
+
requestedCwd: params.cwd,
|
|
411
|
+
currentProjectTrusted: ctx.isProjectTrusted(),
|
|
412
|
+
});
|
|
413
|
+
assertDelegationTargetAllowed(
|
|
414
|
+
target,
|
|
415
|
+
currentSettings?.cwdPolicy?.delegation ?? DEFAULT_DELEGATION_CWD_POLICY,
|
|
416
|
+
);
|
|
417
|
+
const cwd = target.cwd;
|
|
418
|
+
await confirmProjectAgent(
|
|
419
|
+
params.agent,
|
|
420
|
+
scope,
|
|
421
|
+
params.confirmProjectAgents ?? true,
|
|
422
|
+
ctx,
|
|
423
|
+
cwd,
|
|
424
|
+
currentSettings,
|
|
425
|
+
);
|
|
426
|
+
assertCurrentSpawn(signal, generation, runtimeGeneration);
|
|
427
|
+
const ownedRegistry = requireRegistry();
|
|
428
|
+
const resolvedAgent = discoverAgents(cwd, scope, currentSettings).agents.find(
|
|
371
429
|
(agent) => agent.name === params.agent,
|
|
372
430
|
);
|
|
373
431
|
if (params.workspaceMode === "worktree" && resolvedAgent?.source === "project") {
|
|
@@ -382,16 +440,29 @@ export function registerStatefulSubagents(
|
|
|
382
440
|
);
|
|
383
441
|
const requestedCwd = cwd;
|
|
384
442
|
if ((params.workspaceMode ?? "shared") === "shared" && !params.allowConcurrentWrites) {
|
|
385
|
-
assertNoSharedWriteConflict(
|
|
443
|
+
assertNoSharedWriteConflict(
|
|
444
|
+
ownedRegistry,
|
|
445
|
+
params.agent,
|
|
446
|
+
requestedCwd,
|
|
447
|
+
scope,
|
|
448
|
+
currentSettings,
|
|
449
|
+
);
|
|
386
450
|
}
|
|
387
451
|
const workspaceOwner = `pending-${randomUUID()}`;
|
|
388
452
|
const workspace =
|
|
389
453
|
params.workspaceMode === "worktree"
|
|
390
454
|
? await workspaceManager.create(workspaceOwner, requestedCwd)
|
|
391
455
|
: undefined;
|
|
392
|
-
let agent: ManagedAgent;
|
|
393
456
|
try {
|
|
394
|
-
|
|
457
|
+
assertCurrentSpawn(signal, generation, runtimeGeneration);
|
|
458
|
+
} catch (error) {
|
|
459
|
+
if (workspace) await workspaceManager.cleanup(workspaceOwner);
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
const targetSnapshot = targetPolicyAudit(target);
|
|
463
|
+
let agent: ManagedAgent | undefined;
|
|
464
|
+
try {
|
|
465
|
+
agent = await ownedRegistry.spawn({
|
|
395
466
|
agent: params.agent,
|
|
396
467
|
task: params.task,
|
|
397
468
|
cwd: workspace?.path ?? requestedCwd,
|
|
@@ -401,11 +472,16 @@ export function registerStatefulSubagents(
|
|
|
401
472
|
context: snapshot.text || undefined,
|
|
402
473
|
contextSourceIds: snapshot.sourceIds,
|
|
403
474
|
contextTruncated: snapshot.truncated,
|
|
475
|
+
workspaceMode: workspace ? "worktree" : undefined,
|
|
476
|
+
target: targetSnapshot,
|
|
404
477
|
});
|
|
478
|
+
assertCurrentSpawn(signal, generation, runtimeGeneration);
|
|
405
479
|
} catch (error) {
|
|
480
|
+
if (agent) await ownedRegistry.closeTree(agent.id).catch(() => undefined);
|
|
406
481
|
if (workspace) await workspaceManager.cleanup(workspaceOwner);
|
|
407
482
|
throw error;
|
|
408
483
|
}
|
|
484
|
+
if (!agent) throw new Error("Subagent spawn completed without a retained agent");
|
|
409
485
|
if (workspace) isolatedAgents.set(agent.id, workspaceOwner);
|
|
410
486
|
const deliveryNote =
|
|
411
487
|
completionDelivery === "auto-resume"
|
|
@@ -418,7 +494,7 @@ export function registerStatefulSubagents(
|
|
|
418
494
|
},
|
|
419
495
|
});
|
|
420
496
|
refreshSpawnToolRegistration = () => {
|
|
421
|
-
spawnTool.description = appendAgentCatalog(baseSpawnDescription, agentCatalog);
|
|
497
|
+
spawnTool.description = appendAgentCatalog(baseSpawnDescription(), agentCatalog);
|
|
422
498
|
spawnTool.promptGuidelines = createSpawnPromptGuidelines(completionDelivery, blockingEnabled);
|
|
423
499
|
pi.registerTool(spawnTool);
|
|
424
500
|
};
|
|
@@ -437,8 +513,12 @@ export function registerStatefulSubagents(
|
|
|
437
513
|
Type.Boolean({ description: "Override the shared-workspace write conflict guard." }),
|
|
438
514
|
),
|
|
439
515
|
}),
|
|
440
|
-
|
|
441
|
-
|
|
516
|
+
...createStatefulToolRenderer("send"),
|
|
517
|
+
async execute(_id, params, signal, _update, ctx) {
|
|
518
|
+
const generation = runtimeGeneration;
|
|
519
|
+
const ownedRegistry = requireRegistry();
|
|
520
|
+
const currentSettings = getCurrentSettings();
|
|
521
|
+
const existing = ownedRegistry.get(params.agentId);
|
|
442
522
|
if (!existing) throw new Error(`Unknown subagent: ${params.agentId}`);
|
|
443
523
|
await confirmProjectAgent(
|
|
444
524
|
existing.agent,
|
|
@@ -446,14 +526,18 @@ export function registerStatefulSubagents(
|
|
|
446
526
|
false,
|
|
447
527
|
ctx,
|
|
448
528
|
existing.cwd,
|
|
529
|
+
currentSettings,
|
|
449
530
|
);
|
|
531
|
+
assertCurrentSpawn(signal, generation, runtimeGeneration);
|
|
450
532
|
assertFollowUpWriteAllowed(
|
|
451
|
-
|
|
533
|
+
ownedRegistry,
|
|
452
534
|
existing,
|
|
453
535
|
params.allowConcurrentWrites ?? false,
|
|
454
536
|
isolatedAgents.has(existing.id),
|
|
537
|
+
currentSettings,
|
|
455
538
|
);
|
|
456
|
-
const agent = await
|
|
539
|
+
const agent = await ownedRegistry.followUp(params.agentId, params.task);
|
|
540
|
+
assertCurrentSpawn(signal, generation, runtimeGeneration);
|
|
457
541
|
return result(agent, `Started follow-up for ${agent.id}.`);
|
|
458
542
|
},
|
|
459
543
|
});
|
|
@@ -465,6 +549,7 @@ export function registerStatefulSubagents(
|
|
|
465
549
|
"List retained subagents through the compatibility route, interrupt active work while keeping an agent reusable, or close agents and release their resources. Prefer subagent_inspect when the whole activated capability must be read-only.",
|
|
466
550
|
promptSnippet: "List or control retained detached subagents",
|
|
467
551
|
parameters: ManageParamsSchema,
|
|
552
|
+
...createStatefulToolRenderer("manage"),
|
|
468
553
|
async execute(_id, params): Promise<StatefulActionToolResult> {
|
|
469
554
|
const operation = validateManageParams(params);
|
|
470
555
|
if (operation.action === "list") {
|
|
@@ -533,6 +618,7 @@ export function registerStatefulSubagents(
|
|
|
533
618
|
"Queue a bounded message without starting a turn, or read unread mailbox messages. Read acknowledges returned messages by default; use subagent_inspect for metadata-only unread counts.",
|
|
534
619
|
promptSnippet: "Send or read queue-only detached-subagent mailbox messages",
|
|
535
620
|
parameters: MailboxParamsSchema,
|
|
621
|
+
...createStatefulToolRenderer("mailbox"),
|
|
536
622
|
async execute(_id, params): Promise<StatefulActionToolResult> {
|
|
537
623
|
const operation = validateMailboxParams(params);
|
|
538
624
|
if (operation.action === "send") {
|
|
@@ -571,77 +657,6 @@ export function registerStatefulSubagents(
|
|
|
571
657
|
return controller;
|
|
572
658
|
}
|
|
573
659
|
|
|
574
|
-
export function assertNoSharedWriteConflict(
|
|
575
|
-
registry: AgentRegistry,
|
|
576
|
-
agentName: string,
|
|
577
|
-
cwd: string,
|
|
578
|
-
scope: AgentScope,
|
|
579
|
-
): void {
|
|
580
|
-
const agents = discoverAgents(cwd, scope, readSubagentSettings()).agents;
|
|
581
|
-
const requested = agents.find((agent) => agent.name === agentName);
|
|
582
|
-
if (!isWriteCapable(requested?.tools)) return;
|
|
583
|
-
for (const active of registry.list()) {
|
|
584
|
-
if (
|
|
585
|
-
!isSameCwd(active.cwd, cwd) ||
|
|
586
|
-
(active.state !== "running" && active.state !== "starting")
|
|
587
|
-
) {
|
|
588
|
-
continue;
|
|
589
|
-
}
|
|
590
|
-
const activeConfig = agents.find((agent) => agent.name === active.agent);
|
|
591
|
-
if (isWriteCapable(activeConfig?.tools)) {
|
|
592
|
-
throw new Error(
|
|
593
|
-
`Write-capable subagent ${active.id} is already active in shared workspace ${cwd}. ` +
|
|
594
|
-
"Prefer one subagent_spawn covering combined asynchronous work. Use the blocking subagent parallel mode only when concurrent synchronous outputs justify making the main agent unavailable. Otherwise let the active agent finish or close it; set allowConcurrentWrites only when overlapping writes are knowingly safe, or use workspaceMode worktree when repository isolation is needed.",
|
|
595
|
-
);
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
export function assertFollowUpWriteAllowed(
|
|
601
|
-
registry: AgentRegistry,
|
|
602
|
-
agent: ManagedAgent,
|
|
603
|
-
allowConcurrentWrites: boolean,
|
|
604
|
-
isolatedWorkspace: boolean,
|
|
605
|
-
): void {
|
|
606
|
-
if (allowConcurrentWrites || isolatedWorkspace) return;
|
|
607
|
-
assertNoSharedWriteConflict(registry, agent.agent, agent.cwd, agent.agentScope ?? "user");
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
export function isWriteCapable(tools: string[] | undefined): boolean {
|
|
611
|
-
if (!tools) return true;
|
|
612
|
-
return tools.some((tool) => ["bash", "write", "edit"].includes(tool));
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
async function confirmProjectAgent(
|
|
616
|
-
name: string,
|
|
617
|
-
scope: AgentScope,
|
|
618
|
-
confirm: boolean,
|
|
619
|
-
ctx: ExtensionContext,
|
|
620
|
-
cwd: string,
|
|
621
|
-
): Promise<void> {
|
|
622
|
-
if (scope !== "project" && scope !== "both") return;
|
|
623
|
-
if (!isSameCwd(cwd, ctx.cwd)) {
|
|
624
|
-
throw new Error("Project-local subagent definitions cannot run with an overridden cwd");
|
|
625
|
-
}
|
|
626
|
-
if (!ctx.isProjectTrusted()) {
|
|
627
|
-
throw new Error("Project-local subagent definitions require a trusted project");
|
|
628
|
-
}
|
|
629
|
-
const discovery = discoverAgents(cwd, scope, readSubagentSettings());
|
|
630
|
-
const agent = discovery.agents.find((candidate) => candidate.name === name);
|
|
631
|
-
if (agent?.source !== "project") return;
|
|
632
|
-
if (confirm && ctx.hasUI) {
|
|
633
|
-
const approved = await ctx.ui.confirm(
|
|
634
|
-
"Run project-local agent?",
|
|
635
|
-
`Agent: ${safeTerminalLine(name, 256)}\nSource: ${safeTerminalLine(agent.filePath)}`,
|
|
636
|
-
);
|
|
637
|
-
if (!approved) throw new Error("Project-local subagent was not approved");
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
function isSameCwd(left: string, right: string): boolean {
|
|
642
|
-
return path.resolve(left) === path.resolve(right);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
660
|
function normalizeContextMode(value: "none" | "all" | "summary" | number | undefined): ContextMode {
|
|
646
661
|
if (value === undefined) return "none";
|
|
647
662
|
if (value === "none" || value === "all" || value === "summary") return value;
|
|
@@ -698,6 +713,7 @@ function summarizeAgent(agent: ManagedAgent) {
|
|
|
698
713
|
createdAt: agent.createdAt,
|
|
699
714
|
updatedAt: agent.updatedAt,
|
|
700
715
|
cwd: agent.cwd,
|
|
716
|
+
workspaceMode: agent.workspaceMode ?? "shared",
|
|
701
717
|
thinkingLevel: agent.thinkingLevel,
|
|
702
718
|
currentTask: agent.currentTask
|
|
703
719
|
? truncateUtf8(agent.currentTask, MAX_TOOL_MESSAGE_BYTES).text
|
|
@@ -705,6 +721,7 @@ function summarizeAgent(agent: ManagedAgent) {
|
|
|
705
721
|
historyCount: agent.history.length,
|
|
706
722
|
unreadMessages: agent.mailbox.filter((message) => !message.readAt).length,
|
|
707
723
|
error: agent.error ? truncateUtf8(agent.error, MAX_TOOL_MESSAGE_BYTES).text : undefined,
|
|
724
|
+
target: agent.target,
|
|
708
725
|
policy: agent.policy,
|
|
709
726
|
};
|
|
710
727
|
}
|