@narumitw/pi-subagents 0.42.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 +142 -32
- package/package.json +1 -1
- package/src/agents.ts +49 -8
- package/src/config-ui.ts +223 -28
- package/src/consult-policy.ts +15 -0
- package/src/consult-render.ts +194 -0
- package/src/consult.ts +815 -0
- package/src/cwd-policy.ts +183 -0
- package/src/execution.ts +135 -66
- package/src/in-process-transport.ts +3 -3
- package/src/inspect-render.ts +234 -0
- package/src/inspect.ts +453 -0
- package/src/limits.ts +1 -0
- package/src/params.ts +2 -0
- package/src/persistence.ts +29 -0
- package/src/registry.ts +87 -0
- package/src/render-common.ts +252 -0
- package/src/render.ts +134 -99
- package/src/runner.ts +162 -22
- package/src/safe-text.ts +67 -0
- package/src/settings.ts +199 -12
- 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 +254 -225
- package/src/subagents.ts +100 -19
- 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,
|
|
@@ -24,8 +29,29 @@ import {
|
|
|
24
29
|
} from "./in-process-transport.js";
|
|
25
30
|
import { DEFAULT_MAX_CONTEXT_BYTES, truncateUtf8 } from "./limits.js";
|
|
26
31
|
import { AgentPersistence } from "./persistence.js";
|
|
27
|
-
import {
|
|
28
|
-
|
|
32
|
+
import {
|
|
33
|
+
AgentRegistry,
|
|
34
|
+
type AgentRunInspectionDetail,
|
|
35
|
+
type AgentRunInspectionSummary,
|
|
36
|
+
type AgentTurnCompletion,
|
|
37
|
+
type ManagedAgent,
|
|
38
|
+
} from "./registry.js";
|
|
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
|
+
|
|
29
55
|
import {
|
|
30
56
|
MailboxParamsSchema,
|
|
31
57
|
ManageParamsSchema,
|
|
@@ -53,45 +79,12 @@ const MAX_COMPLETION_ERROR_BYTES = 512;
|
|
|
53
79
|
const MAX_COMPLETIONS_PER_MESSAGE = 16;
|
|
54
80
|
const COMPLETION_BATCH_DELAY_MS = 10;
|
|
55
81
|
|
|
56
|
-
function createSpawnPromptGuidelines(
|
|
57
|
-
completionDelivery: CompletionDelivery,
|
|
58
|
-
blockingEnabled = true,
|
|
59
|
-
): string[] {
|
|
60
|
-
const deliveryGuidance =
|
|
61
|
-
completionDelivery === "auto-resume"
|
|
62
|
-
? blockingEnabled
|
|
63
|
-
? "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."
|
|
64
|
-
: "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."
|
|
65
|
-
: blockingEnabled
|
|
66
|
-
? "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."
|
|
67
|
-
: "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.";
|
|
68
|
-
const noLocalWorkGuidance =
|
|
69
|
-
completionDelivery === "auto-resume"
|
|
70
|
-
? "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."
|
|
71
|
-
: "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.";
|
|
72
|
-
return [
|
|
73
|
-
"Do not use subagent_spawn for simple or critical-path work that the main agent can perform directly.",
|
|
74
|
-
"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.",
|
|
75
|
-
deliveryGuidance,
|
|
76
|
-
"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.",
|
|
77
|
-
...(blockingEnabled
|
|
78
|
-
? [
|
|
79
|
-
"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.",
|
|
80
|
-
"When subagent_spawn fits the completion-delivery policy, do not choose a blocking parallel subagent merely to keep delegation in the same turn.",
|
|
81
|
-
]
|
|
82
|
-
: []),
|
|
83
|
-
"Add another subagent_spawn only for truly independent work with safe workspace concurrency.",
|
|
84
|
-
noLocalWorkGuidance,
|
|
85
|
-
'Consume and synthesize available subagent_spawn completion messages; use subagent_manage with action "interrupt" or "close" for agents that are no longer needed.',
|
|
86
|
-
'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.',
|
|
87
|
-
];
|
|
88
|
-
}
|
|
89
|
-
|
|
90
82
|
export interface StatefulSubagentDependencies {
|
|
91
83
|
blockingEnabled?: boolean;
|
|
92
84
|
createInProcessSession?: ChildSessionFactory;
|
|
93
85
|
workspaceManager?: WorkspaceManager;
|
|
94
86
|
settings?: SubagentRuntimeSettings;
|
|
87
|
+
getSettings?: () => SubagentSettings | undefined;
|
|
95
88
|
}
|
|
96
89
|
|
|
97
90
|
export interface StatefulSubagentRuntimeStatus {
|
|
@@ -107,8 +100,11 @@ export interface StatefulSubagentController {
|
|
|
107
100
|
getCompletionDelivery(): CompletionDelivery;
|
|
108
101
|
setCompletionDelivery(value: CompletionDelivery): void;
|
|
109
102
|
setAgentCatalog(value: string): void;
|
|
103
|
+
refreshSettingsGuidance(): void;
|
|
110
104
|
getRuntimeStatus(): StatefulSubagentRuntimeStatus;
|
|
111
105
|
listAgents(includeClosed?: boolean): ManagedAgent[];
|
|
106
|
+
listRunInspection(includeClosed?: boolean): AgentRunInspectionSummary[];
|
|
107
|
+
getRunInspection(agentId: string): AgentRunInspectionDetail | undefined;
|
|
112
108
|
clearAgents(): Promise<number>;
|
|
113
109
|
}
|
|
114
110
|
|
|
@@ -135,23 +131,31 @@ export function registerStatefulSubagents(
|
|
|
135
131
|
let persistence: AgentPersistence | undefined;
|
|
136
132
|
let sweepTimer: NodeJS.Timeout | undefined;
|
|
137
133
|
let runtimeGeneration = 0;
|
|
134
|
+
let runtimeTransition: Promise<void> = Promise.resolve();
|
|
138
135
|
const workspaceManager = dependencies.workspaceManager ?? new WorkspaceManager();
|
|
139
136
|
const isolatedAgents = new Map<string, string>();
|
|
140
137
|
const seenMessageIds = new Set<string>();
|
|
141
138
|
const parentRuntime: ParentRuntimeSnapshot = { model: undefined, thinkingLevel: "off" };
|
|
139
|
+
const getCurrentSettings = () =>
|
|
140
|
+
dependencies.getSettings ? dependencies.getSettings() : readSubagentSettings();
|
|
142
141
|
|
|
143
142
|
const clearAgents = async (): Promise<number> => {
|
|
143
|
+
const generation = runtimeGeneration;
|
|
144
144
|
const currentRegistry = registry;
|
|
145
|
+
const currentPersistence = persistence;
|
|
145
146
|
if (!currentRegistry) return 0;
|
|
146
147
|
const count = currentRegistry.list().length;
|
|
147
|
-
|
|
148
|
+
const clear = async () => {
|
|
148
149
|
await currentRegistry.closeAll();
|
|
149
|
-
|
|
150
|
+
if (generation !== runtimeGeneration) return;
|
|
150
151
|
await workspaceManager.cleanupAll();
|
|
151
152
|
isolatedAgents.clear();
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
seenMessageIds.clear();
|
|
154
|
+
await currentPersistence?.delete();
|
|
155
|
+
};
|
|
156
|
+
const transition = runtimeTransition.then(clear, clear);
|
|
157
|
+
runtimeTransition = transition.catch(() => undefined);
|
|
158
|
+
await transition;
|
|
155
159
|
return count;
|
|
156
160
|
};
|
|
157
161
|
const controller: StatefulSubagentController = {
|
|
@@ -167,22 +171,28 @@ export function registerStatefulSubagents(
|
|
|
167
171
|
agentCatalog = value;
|
|
168
172
|
refreshSpawnToolRegistration?.();
|
|
169
173
|
},
|
|
174
|
+
refreshSettingsGuidance() {
|
|
175
|
+
refreshSpawnToolRegistration?.();
|
|
176
|
+
},
|
|
170
177
|
getRuntimeStatus() {
|
|
171
|
-
const
|
|
178
|
+
const counts = registry?.inspectionCounts() ?? { activeAgents: 0, retainedAgents: 0 };
|
|
172
179
|
return {
|
|
173
180
|
enabled,
|
|
174
181
|
initialized: registry !== undefined,
|
|
175
182
|
transport: transportKind,
|
|
176
183
|
completionDelivery,
|
|
177
|
-
|
|
178
|
-
(agent) => agent.state === "starting" || agent.state === "running",
|
|
179
|
-
).length,
|
|
180
|
-
retainedAgents: agents.filter((agent) => agent.state !== "closed").length,
|
|
184
|
+
...counts,
|
|
181
185
|
};
|
|
182
186
|
},
|
|
183
187
|
listAgents(includeClosed = false) {
|
|
184
188
|
return registry?.list(includeClosed) ?? [];
|
|
185
189
|
},
|
|
190
|
+
listRunInspection(includeClosed = false) {
|
|
191
|
+
return registry?.listInspection(includeClosed) ?? [];
|
|
192
|
+
},
|
|
193
|
+
getRunInspection(agentId) {
|
|
194
|
+
return registry?.getInspection(agentId);
|
|
195
|
+
},
|
|
186
196
|
clearAgents,
|
|
187
197
|
};
|
|
188
198
|
if (!enabled) return controller;
|
|
@@ -201,79 +211,125 @@ export function registerStatefulSubagents(
|
|
|
201
211
|
const generation = ++runtimeGeneration;
|
|
202
212
|
completionBroker?.close();
|
|
203
213
|
completionBroker = undefined;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
})
|
|
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
|
+
}
|
|
250
282
|
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
completionBroker?.enqueue(completion);
|
|
256
|
-
},
|
|
257
|
-
});
|
|
258
|
-
const restored = sessionPersistence
|
|
259
|
-
.load()
|
|
260
|
-
.filter(
|
|
261
|
-
(agent) =>
|
|
262
|
-
(agent.agentScope !== "project" && agent.agentScope !== "both") || ctx.isProjectTrusted(),
|
|
263
|
-
);
|
|
264
|
-
for (const agent of restored) {
|
|
265
|
-
for (const message of agent.mailbox) seenMessageIds.add(message.id);
|
|
266
|
-
}
|
|
267
|
-
registry.restore(restored);
|
|
268
|
-
const sweepEveryMs = Math.max(1_000, Math.min(settings.idleTtlMs ?? 60 * 60 * 1000, 60_000));
|
|
269
|
-
sweepTimer = setInterval(() => {
|
|
270
|
-
void registry?.sweepExpired().catch((error: unknown) => {
|
|
271
|
-
if (!ctx.hasUI) return;
|
|
272
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
273
|
-
ctx.ui.notify(`Subagent expiry cleanup failed: ${reason}`, "warning");
|
|
283
|
+
},
|
|
284
|
+
onTurnComplete: (completion) => {
|
|
285
|
+
if (generation === runtimeGeneration) sessionBroker.enqueue(completion);
|
|
286
|
+
},
|
|
274
287
|
});
|
|
275
|
-
|
|
276
|
-
|
|
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;
|
|
277
333
|
});
|
|
278
334
|
|
|
279
335
|
pi.on("agent_start", () => {
|
|
@@ -298,35 +354,28 @@ export function registerStatefulSubagents(
|
|
|
298
354
|
completionBroker = undefined;
|
|
299
355
|
if (sweepTimer) clearInterval(sweepTimer);
|
|
300
356
|
sweepTimer = undefined;
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
357
|
+
const previousRegistry = registry;
|
|
358
|
+
registry = undefined;
|
|
359
|
+
persistence = undefined;
|
|
304
360
|
isolatedAgents.clear();
|
|
305
361
|
seenMessageIds.clear();
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
registry = undefined;
|
|
316
|
-
persistence = undefined;
|
|
317
|
-
}
|
|
318
|
-
if (cleanupError && ctx.hasUI) {
|
|
319
|
-
const reason = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
|
|
320
|
-
ctx.ui.notify(`Some isolated subagent workspaces could not be removed: ${reason}`, "warning");
|
|
321
|
-
}
|
|
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;
|
|
322
371
|
});
|
|
323
372
|
|
|
324
|
-
const baseSpawnDescription =
|
|
325
|
-
|
|
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.`;
|
|
326
375
|
const spawnTool = defineTool({
|
|
327
376
|
name: "subagent_spawn",
|
|
328
377
|
label: "Spawn Subagent",
|
|
329
|
-
description: appendAgentCatalog(baseSpawnDescription, agentCatalog),
|
|
378
|
+
description: appendAgentCatalog(baseSpawnDescription(), agentCatalog),
|
|
330
379
|
promptSnippet: "Start a reusable detached subagent; completion is delivered asynchronously",
|
|
331
380
|
promptGuidelines: createSpawnPromptGuidelines(completionDelivery, blockingEnabled),
|
|
332
381
|
parameters: Type.Object({
|
|
@@ -350,12 +399,33 @@ export function registerStatefulSubagents(
|
|
|
350
399
|
}),
|
|
351
400
|
),
|
|
352
401
|
}),
|
|
353
|
-
|
|
402
|
+
...createStatefulToolRenderer("spawn"),
|
|
403
|
+
async execute(_id, params, signal, _update, ctx) {
|
|
354
404
|
const scope = (params.agentScope ?? "user") as AgentScope;
|
|
355
405
|
assertSubagentDepthAllowed();
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
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(
|
|
359
429
|
(agent) => agent.name === params.agent,
|
|
360
430
|
);
|
|
361
431
|
if (params.workspaceMode === "worktree" && resolvedAgent?.source === "project") {
|
|
@@ -370,16 +440,29 @@ export function registerStatefulSubagents(
|
|
|
370
440
|
);
|
|
371
441
|
const requestedCwd = cwd;
|
|
372
442
|
if ((params.workspaceMode ?? "shared") === "shared" && !params.allowConcurrentWrites) {
|
|
373
|
-
assertNoSharedWriteConflict(
|
|
443
|
+
assertNoSharedWriteConflict(
|
|
444
|
+
ownedRegistry,
|
|
445
|
+
params.agent,
|
|
446
|
+
requestedCwd,
|
|
447
|
+
scope,
|
|
448
|
+
currentSettings,
|
|
449
|
+
);
|
|
374
450
|
}
|
|
375
451
|
const workspaceOwner = `pending-${randomUUID()}`;
|
|
376
452
|
const workspace =
|
|
377
453
|
params.workspaceMode === "worktree"
|
|
378
454
|
? await workspaceManager.create(workspaceOwner, requestedCwd)
|
|
379
455
|
: undefined;
|
|
380
|
-
let agent: ManagedAgent;
|
|
381
456
|
try {
|
|
382
|
-
|
|
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({
|
|
383
466
|
agent: params.agent,
|
|
384
467
|
task: params.task,
|
|
385
468
|
cwd: workspace?.path ?? requestedCwd,
|
|
@@ -389,11 +472,16 @@ export function registerStatefulSubagents(
|
|
|
389
472
|
context: snapshot.text || undefined,
|
|
390
473
|
contextSourceIds: snapshot.sourceIds,
|
|
391
474
|
contextTruncated: snapshot.truncated,
|
|
475
|
+
workspaceMode: workspace ? "worktree" : undefined,
|
|
476
|
+
target: targetSnapshot,
|
|
392
477
|
});
|
|
478
|
+
assertCurrentSpawn(signal, generation, runtimeGeneration);
|
|
393
479
|
} catch (error) {
|
|
480
|
+
if (agent) await ownedRegistry.closeTree(agent.id).catch(() => undefined);
|
|
394
481
|
if (workspace) await workspaceManager.cleanup(workspaceOwner);
|
|
395
482
|
throw error;
|
|
396
483
|
}
|
|
484
|
+
if (!agent) throw new Error("Subagent spawn completed without a retained agent");
|
|
397
485
|
if (workspace) isolatedAgents.set(agent.id, workspaceOwner);
|
|
398
486
|
const deliveryNote =
|
|
399
487
|
completionDelivery === "auto-resume"
|
|
@@ -406,7 +494,7 @@ export function registerStatefulSubagents(
|
|
|
406
494
|
},
|
|
407
495
|
});
|
|
408
496
|
refreshSpawnToolRegistration = () => {
|
|
409
|
-
spawnTool.description = appendAgentCatalog(baseSpawnDescription, agentCatalog);
|
|
497
|
+
spawnTool.description = appendAgentCatalog(baseSpawnDescription(), agentCatalog);
|
|
410
498
|
spawnTool.promptGuidelines = createSpawnPromptGuidelines(completionDelivery, blockingEnabled);
|
|
411
499
|
pi.registerTool(spawnTool);
|
|
412
500
|
};
|
|
@@ -425,8 +513,12 @@ export function registerStatefulSubagents(
|
|
|
425
513
|
Type.Boolean({ description: "Override the shared-workspace write conflict guard." }),
|
|
426
514
|
),
|
|
427
515
|
}),
|
|
428
|
-
|
|
429
|
-
|
|
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);
|
|
430
522
|
if (!existing) throw new Error(`Unknown subagent: ${params.agentId}`);
|
|
431
523
|
await confirmProjectAgent(
|
|
432
524
|
existing.agent,
|
|
@@ -434,14 +526,18 @@ export function registerStatefulSubagents(
|
|
|
434
526
|
false,
|
|
435
527
|
ctx,
|
|
436
528
|
existing.cwd,
|
|
529
|
+
currentSettings,
|
|
437
530
|
);
|
|
531
|
+
assertCurrentSpawn(signal, generation, runtimeGeneration);
|
|
438
532
|
assertFollowUpWriteAllowed(
|
|
439
|
-
|
|
533
|
+
ownedRegistry,
|
|
440
534
|
existing,
|
|
441
535
|
params.allowConcurrentWrites ?? false,
|
|
442
536
|
isolatedAgents.has(existing.id),
|
|
537
|
+
currentSettings,
|
|
443
538
|
);
|
|
444
|
-
const agent = await
|
|
539
|
+
const agent = await ownedRegistry.followUp(params.agentId, params.task);
|
|
540
|
+
assertCurrentSpawn(signal, generation, runtimeGeneration);
|
|
445
541
|
return result(agent, `Started follow-up for ${agent.id}.`);
|
|
446
542
|
},
|
|
447
543
|
});
|
|
@@ -450,9 +546,10 @@ export function registerStatefulSubagents(
|
|
|
450
546
|
name: "subagent_manage",
|
|
451
547
|
label: "Manage Subagents",
|
|
452
548
|
description:
|
|
453
|
-
"List retained subagents, interrupt active work while keeping an agent reusable, or close agents and release their resources.",
|
|
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.",
|
|
454
550
|
promptSnippet: "List or control retained detached subagents",
|
|
455
551
|
parameters: ManageParamsSchema,
|
|
552
|
+
...createStatefulToolRenderer("manage"),
|
|
456
553
|
async execute(_id, params): Promise<StatefulActionToolResult> {
|
|
457
554
|
const operation = validateManageParams(params);
|
|
458
555
|
if (operation.action === "list") {
|
|
@@ -518,9 +615,10 @@ export function registerStatefulSubagents(
|
|
|
518
615
|
name: "subagent_mailbox",
|
|
519
616
|
label: "Subagent Mailbox",
|
|
520
617
|
description:
|
|
521
|
-
"Queue a bounded message without starting a turn, or read unread mailbox messages
|
|
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.",
|
|
522
619
|
promptSnippet: "Send or read queue-only detached-subagent mailbox messages",
|
|
523
620
|
parameters: MailboxParamsSchema,
|
|
621
|
+
...createStatefulToolRenderer("mailbox"),
|
|
524
622
|
async execute(_id, params): Promise<StatefulActionToolResult> {
|
|
525
623
|
const operation = validateMailboxParams(params);
|
|
526
624
|
if (operation.action === "send") {
|
|
@@ -559,77 +657,6 @@ export function registerStatefulSubagents(
|
|
|
559
657
|
return controller;
|
|
560
658
|
}
|
|
561
659
|
|
|
562
|
-
export function assertNoSharedWriteConflict(
|
|
563
|
-
registry: AgentRegistry,
|
|
564
|
-
agentName: string,
|
|
565
|
-
cwd: string,
|
|
566
|
-
scope: AgentScope,
|
|
567
|
-
): void {
|
|
568
|
-
const agents = discoverAgents(cwd, scope, readSubagentSettings()).agents;
|
|
569
|
-
const requested = agents.find((agent) => agent.name === agentName);
|
|
570
|
-
if (!isWriteCapable(requested?.tools)) return;
|
|
571
|
-
for (const active of registry.list()) {
|
|
572
|
-
if (
|
|
573
|
-
!isSameCwd(active.cwd, cwd) ||
|
|
574
|
-
(active.state !== "running" && active.state !== "starting")
|
|
575
|
-
) {
|
|
576
|
-
continue;
|
|
577
|
-
}
|
|
578
|
-
const activeConfig = agents.find((agent) => agent.name === active.agent);
|
|
579
|
-
if (isWriteCapable(activeConfig?.tools)) {
|
|
580
|
-
throw new Error(
|
|
581
|
-
`Write-capable subagent ${active.id} is already active in shared workspace ${cwd}. ` +
|
|
582
|
-
"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.",
|
|
583
|
-
);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
export function assertFollowUpWriteAllowed(
|
|
589
|
-
registry: AgentRegistry,
|
|
590
|
-
agent: ManagedAgent,
|
|
591
|
-
allowConcurrentWrites: boolean,
|
|
592
|
-
isolatedWorkspace: boolean,
|
|
593
|
-
): void {
|
|
594
|
-
if (allowConcurrentWrites || isolatedWorkspace) return;
|
|
595
|
-
assertNoSharedWriteConflict(registry, agent.agent, agent.cwd, agent.agentScope ?? "user");
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
export function isWriteCapable(tools: string[] | undefined): boolean {
|
|
599
|
-
if (!tools) return true;
|
|
600
|
-
return tools.some((tool) => ["bash", "write", "edit"].includes(tool));
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
async function confirmProjectAgent(
|
|
604
|
-
name: string,
|
|
605
|
-
scope: AgentScope,
|
|
606
|
-
confirm: boolean,
|
|
607
|
-
ctx: ExtensionContext,
|
|
608
|
-
cwd: string,
|
|
609
|
-
): Promise<void> {
|
|
610
|
-
if (scope !== "project" && scope !== "both") return;
|
|
611
|
-
const discovery = discoverAgents(cwd, scope, readSubagentSettings());
|
|
612
|
-
const agent = discovery.agents.find((candidate) => candidate.name === name);
|
|
613
|
-
if (agent?.source !== "project") return;
|
|
614
|
-
if (!isSameCwd(cwd, ctx.cwd)) {
|
|
615
|
-
throw new Error("Project-local subagent definitions cannot run with an overridden cwd");
|
|
616
|
-
}
|
|
617
|
-
if (!ctx.isProjectTrusted()) {
|
|
618
|
-
throw new Error("Project-local subagent definitions require a trusted project");
|
|
619
|
-
}
|
|
620
|
-
if (confirm && ctx.hasUI) {
|
|
621
|
-
const approved = await ctx.ui.confirm(
|
|
622
|
-
"Run project-local agent?",
|
|
623
|
-
`Agent: ${name}\nSource: ${agent.filePath}`,
|
|
624
|
-
);
|
|
625
|
-
if (!approved) throw new Error("Project-local subagent was not approved");
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
function isSameCwd(left: string, right: string): boolean {
|
|
630
|
-
return path.resolve(left) === path.resolve(right);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
660
|
function normalizeContextMode(value: "none" | "all" | "summary" | number | undefined): ContextMode {
|
|
634
661
|
if (value === undefined) return "none";
|
|
635
662
|
if (value === "none" || value === "all" || value === "summary") return value;
|
|
@@ -686,6 +713,7 @@ function summarizeAgent(agent: ManagedAgent) {
|
|
|
686
713
|
createdAt: agent.createdAt,
|
|
687
714
|
updatedAt: agent.updatedAt,
|
|
688
715
|
cwd: agent.cwd,
|
|
716
|
+
workspaceMode: agent.workspaceMode ?? "shared",
|
|
689
717
|
thinkingLevel: agent.thinkingLevel,
|
|
690
718
|
currentTask: agent.currentTask
|
|
691
719
|
? truncateUtf8(agent.currentTask, MAX_TOOL_MESSAGE_BYTES).text
|
|
@@ -693,6 +721,7 @@ function summarizeAgent(agent: ManagedAgent) {
|
|
|
693
721
|
historyCount: agent.history.length,
|
|
694
722
|
unreadMessages: agent.mailbox.filter((message) => !message.readAt).length,
|
|
695
723
|
error: agent.error ? truncateUtf8(agent.error, MAX_TOOL_MESSAGE_BYTES).text : undefined,
|
|
724
|
+
target: agent.target,
|
|
696
725
|
policy: agent.policy,
|
|
697
726
|
};
|
|
698
727
|
}
|