@narumitw/pi-subagents 0.25.0 → 0.27.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 +131 -60
- package/package.json +1 -1
- package/src/agents.ts +3 -0
- package/src/config-ui.ts +566 -205
- package/src/in-process-transport.ts +15 -10
- package/src/persistence.ts +4 -1
- package/src/registry.ts +33 -15
- package/src/settings.ts +113 -0
- package/src/stateful-tool-params.ts +202 -0
- package/src/stateful.ts +493 -213
- package/src/subagents.ts +18 -18
- package/src/subprocess-transport.ts +10 -9
package/src/stateful.ts
CHANGED
|
@@ -1,21 +1,37 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
3
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
4
|
+
import {
|
|
5
|
+
defineTool,
|
|
6
|
+
type ExtensionAPI,
|
|
7
|
+
type ExtensionContext,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
9
|
import { Type } from "typebox";
|
|
6
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
type AgentScope,
|
|
12
|
+
type CompletionDelivery,
|
|
13
|
+
discoverAgents,
|
|
14
|
+
isThinkingLevel,
|
|
15
|
+
THINKING_LEVELS,
|
|
16
|
+
} from "./agents.js";
|
|
7
17
|
import { buildContextSnapshot, type ContextMode, redactPrivateText } from "./context.js";
|
|
8
18
|
import { assertSubagentDepthAllowed } from "./execution.js";
|
|
9
|
-
import { DEFAULT_MAX_CONTEXT_BYTES, truncateUtf8 } from "./limits.js";
|
|
10
|
-
import { AgentPersistence } from "./persistence.js";
|
|
11
|
-
import { AgentRegistry, type AgentTurnCompletion, type ManagedAgent } from "./registry.js";
|
|
12
|
-
import { readSubagentSettings } from "./settings.js";
|
|
13
|
-
import { SubprocessTransport } from "./subprocess-transport.js";
|
|
14
19
|
import {
|
|
15
20
|
type ChildSessionFactory,
|
|
16
21
|
InProcessTransport,
|
|
17
22
|
type ParentRuntimeSnapshot,
|
|
18
23
|
} from "./in-process-transport.js";
|
|
24
|
+
import { DEFAULT_MAX_CONTEXT_BYTES, truncateUtf8 } from "./limits.js";
|
|
25
|
+
import { AgentPersistence } from "./persistence.js";
|
|
26
|
+
import { AgentRegistry, type AgentTurnCompletion, type ManagedAgent } from "./registry.js";
|
|
27
|
+
import { readSubagentSettings } from "./settings.js";
|
|
28
|
+
import {
|
|
29
|
+
MailboxParamsSchema,
|
|
30
|
+
ManageParamsSchema,
|
|
31
|
+
validateMailboxParams,
|
|
32
|
+
validateManageParams,
|
|
33
|
+
} from "./stateful-tool-params.js";
|
|
34
|
+
import { SubprocessTransport } from "./subprocess-transport.js";
|
|
19
35
|
import { WorkspaceManager } from "./workspace.js";
|
|
20
36
|
|
|
21
37
|
const ContextModeSchema = Type.Union([
|
|
@@ -27,37 +43,141 @@ const ScopeSchema = StringEnum(["user", "project", "both"] as const, {
|
|
|
27
43
|
'Per-invocation custom agent scope for this spawn. Default: "user". Use "project" for project-local agents or "both" for user and project agents; the selected scope is retained for follow-ups.',
|
|
28
44
|
default: "user",
|
|
29
45
|
});
|
|
46
|
+
const StatefulThinkingLevelSchema = StringEnum(THINKING_LEVELS, {
|
|
47
|
+
description:
|
|
48
|
+
"Optional requested Pi thinking level selected for this task difficulty; retained for every turn of the spawned agent.",
|
|
49
|
+
});
|
|
30
50
|
const MAX_TOOL_MESSAGE_BYTES = 2 * 1024;
|
|
31
51
|
const MAX_COMPLETION_ERROR_BYTES = 512;
|
|
52
|
+
const MAX_COMPLETIONS_PER_MESSAGE = 16;
|
|
53
|
+
const COMPLETION_BATCH_DELAY_MS = 10;
|
|
54
|
+
|
|
55
|
+
function createSpawnPromptGuidelines(completionDelivery: CompletionDelivery): string[] {
|
|
56
|
+
const deliveryGuidance =
|
|
57
|
+
completionDelivery === "auto-resume"
|
|
58
|
+
? "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."
|
|
59
|
+
: "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.";
|
|
60
|
+
const noLocalWorkGuidance =
|
|
61
|
+
completionDelivery === "auto-resume"
|
|
62
|
+
? "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."
|
|
63
|
+
: "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.";
|
|
64
|
+
return [
|
|
65
|
+
"Do not use subagent_spawn for simple or critical-path work that the main agent can perform directly.",
|
|
66
|
+
"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.",
|
|
67
|
+
deliveryGuidance,
|
|
68
|
+
"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.",
|
|
69
|
+
"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.",
|
|
70
|
+
"When subagent_spawn fits the completion-delivery policy, do not choose a blocking parallel subagent merely to keep delegation in the same turn.",
|
|
71
|
+
"Add another subagent_spawn only for truly independent work with safe workspace concurrency.",
|
|
72
|
+
noLocalWorkGuidance,
|
|
73
|
+
'Consume and synthesize available subagent_spawn completion messages; use subagent_manage with action "interrupt" or "close" for agents that are no longer needed.',
|
|
74
|
+
'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.',
|
|
75
|
+
];
|
|
76
|
+
}
|
|
32
77
|
|
|
33
78
|
export interface StatefulSubagentDependencies {
|
|
34
79
|
createInProcessSession?: ChildSessionFactory;
|
|
80
|
+
workspaceManager?: WorkspaceManager;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface StatefulSubagentRuntimeStatus {
|
|
84
|
+
enabled: boolean;
|
|
85
|
+
initialized: boolean;
|
|
86
|
+
transport: "subprocess" | "in-process";
|
|
87
|
+
completionDelivery: CompletionDelivery;
|
|
88
|
+
activeAgents: number;
|
|
89
|
+
retainedAgents: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface StatefulSubagentController {
|
|
93
|
+
getCompletionDelivery(): CompletionDelivery;
|
|
94
|
+
setCompletionDelivery(value: CompletionDelivery): void;
|
|
95
|
+
getRuntimeStatus(): StatefulSubagentRuntimeStatus;
|
|
96
|
+
listAgents(includeClosed?: boolean): ManagedAgent[];
|
|
97
|
+
clearAgents(): Promise<number>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface StatefulActionToolResult {
|
|
101
|
+
content: Array<{ type: "text"; text: string }>;
|
|
102
|
+
details: Record<string, unknown>;
|
|
35
103
|
}
|
|
36
104
|
|
|
37
105
|
export function registerStatefulSubagents(
|
|
38
106
|
pi: ExtensionAPI,
|
|
39
107
|
dependencies: StatefulSubagentDependencies = {},
|
|
40
|
-
):
|
|
108
|
+
): StatefulSubagentController {
|
|
41
109
|
const settings = readSubagentSettings()?.stateful ?? {};
|
|
42
|
-
|
|
43
|
-
|
|
110
|
+
const enabled = settings.enabled !== false;
|
|
111
|
+
const transportKind = resolveStatefulTransportKind(settings.transport);
|
|
112
|
+
let completionDelivery = resolveCompletionDelivery(settings.completionDelivery);
|
|
113
|
+
let completionBroker: CompletionDeliveryBroker | undefined;
|
|
114
|
+
let refreshSpawnToolRegistration: (() => void) | undefined;
|
|
44
115
|
let registry: AgentRegistry | undefined;
|
|
45
116
|
let persistence: AgentPersistence | undefined;
|
|
46
117
|
let sweepTimer: NodeJS.Timeout | undefined;
|
|
47
118
|
let runtimeGeneration = 0;
|
|
48
|
-
const workspaceManager = new WorkspaceManager();
|
|
119
|
+
const workspaceManager = dependencies.workspaceManager ?? new WorkspaceManager();
|
|
49
120
|
const isolatedAgents = new Map<string, string>();
|
|
50
121
|
const seenMessageIds = new Set<string>();
|
|
51
122
|
const parentRuntime: ParentRuntimeSnapshot = { model: undefined, thinkingLevel: "off" };
|
|
52
|
-
|
|
123
|
+
|
|
124
|
+
const clearAgents = async (): Promise<number> => {
|
|
125
|
+
const currentRegistry = registry;
|
|
126
|
+
if (!currentRegistry) return 0;
|
|
127
|
+
const count = currentRegistry.list().length;
|
|
128
|
+
try {
|
|
129
|
+
await currentRegistry.closeAll();
|
|
130
|
+
} finally {
|
|
131
|
+
await workspaceManager.cleanupAll();
|
|
132
|
+
isolatedAgents.clear();
|
|
133
|
+
}
|
|
134
|
+
seenMessageIds.clear();
|
|
135
|
+
await persistence?.delete();
|
|
136
|
+
return count;
|
|
137
|
+
};
|
|
138
|
+
const controller: StatefulSubagentController = {
|
|
139
|
+
getCompletionDelivery() {
|
|
140
|
+
return completionDelivery;
|
|
141
|
+
},
|
|
142
|
+
setCompletionDelivery(value) {
|
|
143
|
+
completionDelivery = value;
|
|
144
|
+
completionBroker?.setDelivery(value);
|
|
145
|
+
refreshSpawnToolRegistration?.();
|
|
146
|
+
},
|
|
147
|
+
getRuntimeStatus() {
|
|
148
|
+
const agents = registry?.list(true) ?? [];
|
|
149
|
+
return {
|
|
150
|
+
enabled,
|
|
151
|
+
initialized: registry !== undefined,
|
|
152
|
+
transport: transportKind,
|
|
153
|
+
completionDelivery,
|
|
154
|
+
activeAgents: agents.filter(
|
|
155
|
+
(agent) => agent.state === "starting" || agent.state === "running",
|
|
156
|
+
).length,
|
|
157
|
+
retainedAgents: agents.filter((agent) => agent.state !== "closed").length,
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
listAgents(includeClosed = false) {
|
|
161
|
+
return registry?.list(includeClosed) ?? [];
|
|
162
|
+
},
|
|
163
|
+
clearAgents,
|
|
164
|
+
};
|
|
165
|
+
if (!enabled) return controller;
|
|
53
166
|
|
|
54
167
|
const requireRegistry = () => {
|
|
55
168
|
if (!registry) throw new Error("Stateful subagents are not initialized for this session");
|
|
56
169
|
return registry;
|
|
57
170
|
};
|
|
171
|
+
const requireAgent = (agentId: string) => {
|
|
172
|
+
const agent = requireRegistry().get(agentId);
|
|
173
|
+
if (!agent) throw new Error(`Unknown subagent: ${agentId}`);
|
|
174
|
+
return agent;
|
|
175
|
+
};
|
|
58
176
|
|
|
59
177
|
pi.on("session_start", async (_event, ctx) => {
|
|
60
178
|
const generation = ++runtimeGeneration;
|
|
179
|
+
completionBroker?.close();
|
|
180
|
+
completionBroker = undefined;
|
|
61
181
|
parentRuntime.model = ctx.model;
|
|
62
182
|
parentRuntime.thinkingLevel = normalizeRuntimeThinkingLevel(pi.getThinkingLevel());
|
|
63
183
|
const owner =
|
|
@@ -69,14 +189,21 @@ export function registerStatefulSubagents(
|
|
|
69
189
|
maxStoredAgents: settings.maxStoredAgents,
|
|
70
190
|
});
|
|
71
191
|
persistence = sessionPersistence;
|
|
192
|
+
completionBroker = new CompletionDeliveryBroker(pi, ctx, completionDelivery, {
|
|
193
|
+
onDeliveryError: (error) => {
|
|
194
|
+
if (!ctx.hasUI) return;
|
|
195
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
196
|
+
ctx.ui.notify(`Subagent completion delivery failed: ${reason}`, "warning");
|
|
197
|
+
},
|
|
198
|
+
});
|
|
72
199
|
const transport =
|
|
73
|
-
|
|
200
|
+
transportKind === "in-process"
|
|
74
201
|
? new InProcessTransport({
|
|
75
202
|
modelRegistry: ctx.modelRegistry,
|
|
76
203
|
getParentRuntime: () => ({ ...parentRuntime }),
|
|
77
204
|
createSession: dependencies.createInProcessSession,
|
|
78
205
|
})
|
|
79
|
-
: new SubprocessTransport(
|
|
206
|
+
: new SubprocessTransport();
|
|
80
207
|
registry = new AgentRegistry(transport, {
|
|
81
208
|
maxAgents: settings.maxAgents,
|
|
82
209
|
maxActiveTurns: settings.maxActiveTurns,
|
|
@@ -102,9 +229,7 @@ export function registerStatefulSubagents(
|
|
|
102
229
|
},
|
|
103
230
|
onTurnComplete: (completion) => {
|
|
104
231
|
if (generation !== runtimeGeneration) return;
|
|
105
|
-
|
|
106
|
-
sendDetachedCompletion(pi, completion);
|
|
107
|
-
}
|
|
232
|
+
completionBroker?.enqueue(completion);
|
|
108
233
|
},
|
|
109
234
|
});
|
|
110
235
|
const restored = sessionPersistence
|
|
@@ -128,6 +253,14 @@ export function registerStatefulSubagents(
|
|
|
128
253
|
sweepTimer.unref();
|
|
129
254
|
});
|
|
130
255
|
|
|
256
|
+
pi.on("agent_start", () => {
|
|
257
|
+
completionBroker?.onParentTurnStart();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
pi.on("agent_settled", () => {
|
|
261
|
+
completionBroker?.onParentSettled();
|
|
262
|
+
});
|
|
263
|
+
|
|
131
264
|
pi.on("model_select", (event) => {
|
|
132
265
|
parentRuntime.model = event.model;
|
|
133
266
|
});
|
|
@@ -138,6 +271,8 @@ export function registerStatefulSubagents(
|
|
|
138
271
|
|
|
139
272
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
140
273
|
runtimeGeneration++;
|
|
274
|
+
completionBroker?.close();
|
|
275
|
+
completionBroker = undefined;
|
|
141
276
|
if (sweepTimer) clearInterval(sweepTimer);
|
|
142
277
|
sweepTimer = undefined;
|
|
143
278
|
for (const agentId of isolatedAgents.keys()) {
|
|
@@ -145,39 +280,35 @@ export function registerStatefulSubagents(
|
|
|
145
280
|
}
|
|
146
281
|
isolatedAgents.clear();
|
|
147
282
|
seenMessageIds.clear();
|
|
148
|
-
completionWaiters.clear();
|
|
149
283
|
let cleanupError: unknown;
|
|
150
284
|
try {
|
|
151
285
|
await workspaceManager.cleanupAll();
|
|
152
286
|
} catch (error) {
|
|
153
287
|
cleanupError = error;
|
|
154
288
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
289
|
+
try {
|
|
290
|
+
await registry?.shutdown();
|
|
291
|
+
} finally {
|
|
292
|
+
registry = undefined;
|
|
293
|
+
persistence = undefined;
|
|
294
|
+
}
|
|
158
295
|
if (cleanupError && ctx.hasUI) {
|
|
159
296
|
const reason = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
|
|
160
297
|
ctx.ui.notify(`Some isolated subagent workspaces could not be removed: ${reason}`, "warning");
|
|
161
298
|
}
|
|
162
299
|
});
|
|
163
300
|
|
|
164
|
-
|
|
301
|
+
const spawnTool = defineTool({
|
|
165
302
|
name: "subagent_spawn",
|
|
166
303
|
label: "Spawn Subagent",
|
|
167
304
|
description:
|
|
168
|
-
"Start an addressable background subagent, return immediately with an agentId, and receive its completion asynchronously.",
|
|
305
|
+
"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.",
|
|
169
306
|
promptSnippet: "Start a reusable detached subagent; completion is delivered asynchronously",
|
|
170
|
-
promptGuidelines:
|
|
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.",
|
|
177
|
-
],
|
|
307
|
+
promptGuidelines: createSpawnPromptGuidelines(completionDelivery),
|
|
178
308
|
parameters: Type.Object({
|
|
179
309
|
agent: Type.String({ minLength: 1 }),
|
|
180
310
|
task: Type.String({ minLength: 1, maxLength: DEFAULT_MAX_CONTEXT_BYTES }),
|
|
311
|
+
thinkingLevel: Type.Optional(StatefulThinkingLevelSchema),
|
|
181
312
|
cwd: Type.Optional(Type.String()),
|
|
182
313
|
agentScope: Type.Optional(ScopeSchema),
|
|
183
314
|
confirmProjectAgents: Type.Optional(Type.Boolean({ default: true })),
|
|
@@ -229,6 +360,7 @@ export function registerStatefulSubagents(
|
|
|
229
360
|
task: params.task,
|
|
230
361
|
cwd: workspace?.path ?? requestedCwd,
|
|
231
362
|
agentScope: scope,
|
|
363
|
+
thinkingLevel: params.thinkingLevel,
|
|
232
364
|
parentId: params.parentId,
|
|
233
365
|
context: snapshot.text || undefined,
|
|
234
366
|
contextSourceIds: snapshot.sourceIds,
|
|
@@ -239,17 +371,28 @@ export function registerStatefulSubagents(
|
|
|
239
371
|
throw error;
|
|
240
372
|
}
|
|
241
373
|
if (workspace) isolatedAgents.set(agent.id, workspaceOwner);
|
|
374
|
+
const deliveryNote =
|
|
375
|
+
completionDelivery === "auto-resume"
|
|
376
|
+
? "If no useful local work remains, briefly tell the user what was launched and end the response; auto-resume will request synthesis after completion."
|
|
377
|
+
: "End the response without the result only when the current response does not depend on it; next-turn delivery will not wake an idle root.";
|
|
242
378
|
return result(
|
|
243
379
|
agent,
|
|
244
|
-
`Spawned ${agent.agent} as ${agent.id}. Do useful non-overlapping work immediately.
|
|
380
|
+
`Spawned ${agent.agent} as ${agent.id}. Do useful non-overlapping work immediately. ${deliveryNote} Do not poll for progress.`,
|
|
245
381
|
);
|
|
246
382
|
},
|
|
247
383
|
});
|
|
384
|
+
refreshSpawnToolRegistration = () => {
|
|
385
|
+
spawnTool.promptGuidelines = createSpawnPromptGuidelines(completionDelivery);
|
|
386
|
+
pi.registerTool(spawnTool);
|
|
387
|
+
};
|
|
388
|
+
refreshSpawnToolRegistration();
|
|
248
389
|
|
|
249
390
|
pi.registerTool({
|
|
250
391
|
name: "subagent_send",
|
|
251
392
|
label: "Send Subagent Follow-up",
|
|
252
|
-
description:
|
|
393
|
+
description:
|
|
394
|
+
"Send follow-up work to an idle, completed, interrupted, or failed subagent and start a new turn. Use subagent_mailbox for queue-only messages.",
|
|
395
|
+
promptSnippet: "Start a new detached follow-up turn on a retained subagent",
|
|
253
396
|
parameters: Type.Object({
|
|
254
397
|
agentId: Type.String(),
|
|
255
398
|
task: Type.String({ minLength: 1, maxLength: DEFAULT_MAX_CONTEXT_BYTES }),
|
|
@@ -279,166 +422,66 @@ export function registerStatefulSubagents(
|
|
|
279
422
|
});
|
|
280
423
|
|
|
281
424
|
pi.registerTool({
|
|
282
|
-
name: "
|
|
283
|
-
label: "
|
|
284
|
-
description: "Queue a bounded mailbox message without starting a turn.",
|
|
285
|
-
parameters: Type.Object({
|
|
286
|
-
agentId: Type.String(),
|
|
287
|
-
message: Type.String({ minLength: 1, maxLength: 16 * 1024 }),
|
|
288
|
-
senderId: Type.Optional(Type.String()),
|
|
289
|
-
deduplicationKey: Type.Optional(Type.String({ maxLength: 256 })),
|
|
290
|
-
}),
|
|
291
|
-
async execute(_id, params) {
|
|
292
|
-
const message = await requireRegistry().sendMessage(
|
|
293
|
-
params.agentId,
|
|
294
|
-
params.message,
|
|
295
|
-
params.senderId,
|
|
296
|
-
params.deduplicationKey,
|
|
297
|
-
);
|
|
298
|
-
return {
|
|
299
|
-
content: [{ type: "text", text: `Queued ${message.id} for ${message.recipientId}.` }],
|
|
300
|
-
details: { message },
|
|
301
|
-
};
|
|
302
|
-
},
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
pi.registerTool({
|
|
306
|
-
name: "subagent_messages",
|
|
307
|
-
label: "Read Subagent Messages",
|
|
308
|
-
description: "Read unread mailbox messages and optionally acknowledge them.",
|
|
309
|
-
parameters: Type.Object({
|
|
310
|
-
agentId: Type.String(),
|
|
311
|
-
acknowledge: Type.Optional(Type.Boolean({ default: true })),
|
|
312
|
-
limit: Type.Optional(Type.Number({ minimum: 1, maximum: 20, default: 20 })),
|
|
313
|
-
}),
|
|
314
|
-
async execute(_id, params) {
|
|
315
|
-
const messages = await requireRegistry().readMessages(
|
|
316
|
-
params.agentId,
|
|
317
|
-
params.acknowledge,
|
|
318
|
-
params.limit,
|
|
319
|
-
);
|
|
320
|
-
const summaries = messages.map((message) => ({
|
|
321
|
-
...message,
|
|
322
|
-
content: truncateUtf8(message.content, MAX_TOOL_MESSAGE_BYTES).text,
|
|
323
|
-
}));
|
|
324
|
-
const text = summaries.length
|
|
325
|
-
? summaries
|
|
326
|
-
.map((message) => `${message.id} from ${message.senderId}: ${message.content}`)
|
|
327
|
-
.join("\n")
|
|
328
|
-
: "No unread messages.";
|
|
329
|
-
return {
|
|
330
|
-
content: [{ type: "text", text: truncateUtf8(text, DEFAULT_MAX_CONTEXT_BYTES).text }],
|
|
331
|
-
details: { messages: summaries },
|
|
332
|
-
};
|
|
333
|
-
},
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
pi.registerTool({
|
|
337
|
-
name: "subagent_wait",
|
|
338
|
-
label: "Wait for Subagent",
|
|
425
|
+
name: "subagent_manage",
|
|
426
|
+
label: "Manage Subagents",
|
|
339
427
|
description:
|
|
340
|
-
"
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const registered = existing?.state === "starting" || existing?.state === "running";
|
|
348
|
-
if (registered) {
|
|
349
|
-
completionWaiters.set(params.agentId, (completionWaiters.get(params.agentId) ?? 0) + 1);
|
|
350
|
-
}
|
|
351
|
-
try {
|
|
352
|
-
const waited = await requireRegistry().wait(params.agentId, params.timeoutMs, signal);
|
|
353
|
-
return result(
|
|
354
|
-
waited.agent,
|
|
355
|
-
waited.timedOut
|
|
356
|
-
? `Wait timed out; ${waited.agent.id} is ${waited.agent.state}.`
|
|
357
|
-
: formatFinal(waited.agent),
|
|
358
|
-
);
|
|
359
|
-
} finally {
|
|
360
|
-
if (registered) decrementWaiter(completionWaiters, params.agentId);
|
|
361
|
-
}
|
|
362
|
-
},
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
pi.registerTool({
|
|
366
|
-
name: "subagent_list",
|
|
367
|
-
label: "List Subagents",
|
|
368
|
-
description: "List stateful subagents and lifecycle states.",
|
|
369
|
-
parameters: Type.Object({ includeClosed: Type.Optional(Type.Boolean({ default: false })) }),
|
|
370
|
-
async execute(_id, params) {
|
|
371
|
-
const agents = requireRegistry().list(params.includeClosed);
|
|
372
|
-
return {
|
|
373
|
-
content: [
|
|
374
|
-
{
|
|
375
|
-
type: "text",
|
|
376
|
-
text: agents.length ? agents.map(formatLine).join("\n") : "No stateful subagents.",
|
|
377
|
-
},
|
|
378
|
-
],
|
|
379
|
-
details: { agents: agents.map(summarizeAgent) },
|
|
380
|
-
};
|
|
381
|
-
},
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
pi.registerTool({
|
|
385
|
-
name: "subagent_interrupt",
|
|
386
|
-
label: "Interrupt Subagent",
|
|
387
|
-
description: "Interrupt the current turn while retaining the subagent for follow-up work.",
|
|
388
|
-
parameters: Type.Object({
|
|
389
|
-
agentId: Type.String(),
|
|
390
|
-
subtree: Type.Optional(Type.Boolean({ default: false })),
|
|
391
|
-
}),
|
|
392
|
-
async execute(_id, params) {
|
|
393
|
-
if (params.subtree) {
|
|
394
|
-
const agents = await requireRegistry().interruptTree(params.agentId);
|
|
428
|
+
"List retained subagents, interrupt active work while keeping an agent reusable, or close agents and release their resources.",
|
|
429
|
+
promptSnippet: "List or control retained detached subagents",
|
|
430
|
+
parameters: ManageParamsSchema,
|
|
431
|
+
async execute(_id, params): Promise<StatefulActionToolResult> {
|
|
432
|
+
const operation = validateManageParams(params);
|
|
433
|
+
if (operation.action === "list") {
|
|
434
|
+
const agents = requireRegistry().list(operation.includeClosed);
|
|
395
435
|
return {
|
|
396
|
-
content: [
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
436
|
+
content: [
|
|
437
|
+
{
|
|
438
|
+
type: "text",
|
|
439
|
+
text: agents.length ? agents.map(formatLine).join("\n") : "No stateful subagents.",
|
|
440
|
+
},
|
|
441
|
+
],
|
|
442
|
+
details: { agents: agents.map(summarizeAgent) },
|
|
401
443
|
};
|
|
402
444
|
}
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
445
|
+
const agentId = operation.agentId;
|
|
446
|
+
if (operation.action === "interrupt") {
|
|
447
|
+
if (operation.subtree) {
|
|
448
|
+
const agents = await requireRegistry().interruptTree(agentId);
|
|
449
|
+
return {
|
|
450
|
+
content: [{ type: "text", text: `Interrupted ${agents.length} active agent(s).` }],
|
|
451
|
+
details: {
|
|
452
|
+
agent: summarizeAgent(requireAgent(agentId)),
|
|
453
|
+
agents: agents.map(summarizeAgent),
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
const agent = await requireRegistry().interrupt(agentId);
|
|
458
|
+
return result(agent, `Interrupted ${agent.id}; it remains reusable.`);
|
|
459
|
+
}
|
|
460
|
+
const existing = requireRegistry().get(agentId);
|
|
461
|
+
if (existing?.state === "closed" && !operation.subtree) {
|
|
419
462
|
const pendingOwner = isolatedAgents.get(existing.id);
|
|
420
463
|
if (pendingOwner) await workspaceManager.cleanup(pendingOwner);
|
|
421
464
|
isolatedAgents.delete(existing.id);
|
|
422
465
|
return result(existing, `Closed ${existing.id}.`);
|
|
423
466
|
}
|
|
424
|
-
if (
|
|
467
|
+
if (operation.subtree) {
|
|
425
468
|
let agents: ManagedAgent[];
|
|
426
469
|
try {
|
|
427
|
-
agents = await requireRegistry().closeTree(
|
|
470
|
+
agents = await requireRegistry().closeTree(agentId);
|
|
428
471
|
} finally {
|
|
429
472
|
await cleanupClosedWorkspaces(requireRegistry(), isolatedAgents, workspaceManager);
|
|
430
473
|
}
|
|
431
474
|
return {
|
|
432
475
|
content: [{ type: "text", text: `Closed ${agents.length} agent(s).` }],
|
|
433
476
|
details: {
|
|
434
|
-
agent: summarizeAgent(
|
|
477
|
+
agent: summarizeAgent(requireAgent(agentId)),
|
|
435
478
|
agents: agents.map(summarizeAgent),
|
|
436
479
|
},
|
|
437
480
|
};
|
|
438
481
|
}
|
|
439
482
|
let agent: ManagedAgent;
|
|
440
483
|
try {
|
|
441
|
-
agent = await requireRegistry().close(
|
|
484
|
+
agent = await requireRegistry().close(agentId);
|
|
442
485
|
} finally {
|
|
443
486
|
await cleanupClosedWorkspaces(requireRegistry(), isolatedAgents, workspaceManager);
|
|
444
487
|
}
|
|
@@ -446,33 +489,82 @@ export function registerStatefulSubagents(
|
|
|
446
489
|
},
|
|
447
490
|
});
|
|
448
491
|
|
|
492
|
+
pi.registerTool({
|
|
493
|
+
name: "subagent_mailbox",
|
|
494
|
+
label: "Subagent Mailbox",
|
|
495
|
+
description:
|
|
496
|
+
"Queue a bounded message without starting a turn, or read unread mailbox messages and optionally acknowledge them.",
|
|
497
|
+
promptSnippet: "Send or read queue-only detached-subagent mailbox messages",
|
|
498
|
+
parameters: MailboxParamsSchema,
|
|
499
|
+
async execute(_id, params): Promise<StatefulActionToolResult> {
|
|
500
|
+
const operation = validateMailboxParams(params);
|
|
501
|
+
if (operation.action === "send") {
|
|
502
|
+
const message = await requireRegistry().sendMessage(
|
|
503
|
+
operation.agentId,
|
|
504
|
+
operation.message,
|
|
505
|
+
operation.senderId,
|
|
506
|
+
operation.deduplicationKey,
|
|
507
|
+
);
|
|
508
|
+
return {
|
|
509
|
+
content: [{ type: "text", text: `Queued ${message.id} for ${message.recipientId}.` }],
|
|
510
|
+
details: { message },
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
const messages = await requireRegistry().readMessages(
|
|
514
|
+
operation.agentId,
|
|
515
|
+
operation.acknowledge,
|
|
516
|
+
operation.limit,
|
|
517
|
+
);
|
|
518
|
+
const summaries = messages.map((message) => ({
|
|
519
|
+
...message,
|
|
520
|
+
content: truncateUtf8(message.content, MAX_TOOL_MESSAGE_BYTES).text,
|
|
521
|
+
}));
|
|
522
|
+
const text = summaries.length
|
|
523
|
+
? summaries
|
|
524
|
+
.map((message) => `${message.id} from ${message.senderId}: ${message.content}`)
|
|
525
|
+
.join("\n")
|
|
526
|
+
: "No unread messages.";
|
|
527
|
+
return {
|
|
528
|
+
content: [{ type: "text", text: truncateUtf8(text, DEFAULT_MAX_CONTEXT_BYTES).text }],
|
|
529
|
+
details: { messages: summaries },
|
|
530
|
+
};
|
|
531
|
+
},
|
|
532
|
+
});
|
|
533
|
+
|
|
449
534
|
pi.registerCommand("subagents:agents", {
|
|
450
|
-
description: "Inspect or clear
|
|
535
|
+
description: "Inspect or clear current-session subagents",
|
|
451
536
|
getArgumentCompletions(prefix: string) {
|
|
452
537
|
return ["list", "clear"]
|
|
453
538
|
.filter((value) => value.startsWith(prefix))
|
|
454
539
|
.map((value) => ({ value, label: value }));
|
|
455
540
|
},
|
|
456
541
|
async handler(args, ctx) {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
ctx.ui.notify("Cleared stateful subagents.", "info");
|
|
542
|
+
const subcommand = args.trim().toLowerCase() || "list";
|
|
543
|
+
if (subcommand === "clear") {
|
|
544
|
+
const count = await controller.clearAgents();
|
|
545
|
+
ctx.ui.notify(
|
|
546
|
+
count > 0
|
|
547
|
+
? `Cleared ${count} current-session subagent${count === 1 ? "" : "s"}.`
|
|
548
|
+
: statefulEmptyMessage(controller.getRuntimeStatus()),
|
|
549
|
+
"info",
|
|
550
|
+
);
|
|
467
551
|
return;
|
|
468
552
|
}
|
|
469
|
-
|
|
553
|
+
if (subcommand !== "list") {
|
|
554
|
+
ctx.ui.notify(`Unknown /subagents:agents subcommand: ${subcommand}`, "warning");
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const agents = controller.listAgents(true);
|
|
470
558
|
ctx.ui.notify(
|
|
471
|
-
agents.length
|
|
559
|
+
agents.length
|
|
560
|
+
? agents.map(formatLine).join("\n")
|
|
561
|
+
: statefulEmptyMessage(controller.getRuntimeStatus()),
|
|
472
562
|
"info",
|
|
473
563
|
);
|
|
474
564
|
},
|
|
475
565
|
});
|
|
566
|
+
|
|
567
|
+
return controller;
|
|
476
568
|
}
|
|
477
569
|
|
|
478
570
|
export function assertNoSharedWriteConflict(
|
|
@@ -495,7 +587,7 @@ export function assertNoSharedWriteConflict(
|
|
|
495
587
|
if (isWriteCapable(activeConfig?.tools)) {
|
|
496
588
|
throw new Error(
|
|
497
589
|
`Write-capable subagent ${active.id} is already active in shared workspace ${cwd}. ` +
|
|
498
|
-
"
|
|
590
|
+
"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.",
|
|
499
591
|
);
|
|
500
592
|
}
|
|
501
593
|
}
|
|
@@ -516,12 +608,6 @@ export function isWriteCapable(tools: string[] | undefined): boolean {
|
|
|
516
608
|
return tools.some((tool) => ["bash", "write", "edit"].includes(tool));
|
|
517
609
|
}
|
|
518
610
|
|
|
519
|
-
function decrementWaiter(waiters: Map<string, number>, agentId: string): void {
|
|
520
|
-
const count = waiters.get(agentId);
|
|
521
|
-
if (count === undefined || count <= 1) waiters.delete(agentId);
|
|
522
|
-
else waiters.set(agentId, count - 1);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
611
|
async function confirmProjectAgent(
|
|
526
612
|
name: string,
|
|
527
613
|
scope: AgentScope,
|
|
@@ -566,23 +652,40 @@ export function resolveSpawnContextMode(
|
|
|
566
652
|
return normalizeContextMode(value);
|
|
567
653
|
}
|
|
568
654
|
|
|
569
|
-
function
|
|
655
|
+
function statefulEmptyMessage(status: StatefulSubagentRuntimeStatus): string {
|
|
656
|
+
if (!status.enabled) return "Stateful subagents are disabled in user settings.";
|
|
657
|
+
if (!status.initialized) return "Stateful subagents are not initialized for this session.";
|
|
658
|
+
return "No current-session subagents.";
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
export function formatStatefulAgentLine(agent: ManagedAgent): string {
|
|
570
662
|
const elapsedSeconds = Math.max(0, Math.floor((Date.now() - agent.updatedAt) / 1000));
|
|
571
663
|
const actions =
|
|
572
664
|
agent.state === "running" || agent.state === "starting"
|
|
573
|
-
? "
|
|
665
|
+
? "interrupt, close"
|
|
574
666
|
: agent.state === "closed"
|
|
575
667
|
? "inspect"
|
|
576
668
|
: "send, close";
|
|
577
|
-
const task = agent.currentTask ? ` — ${agent.currentTask
|
|
669
|
+
const task = agent.currentTask ? ` — ${sanitizeStatusLine(agent.currentTask, 80)}` : "";
|
|
578
670
|
const unread = agent.mailbox.filter((message) => !message.readAt).length;
|
|
579
671
|
const indent = " ".repeat(agent.depth);
|
|
580
|
-
|
|
672
|
+
const thinking = agent.thinkingLevel ? ` thinking:${agent.thinkingLevel}` : "";
|
|
673
|
+
return `${indent}${sanitizeStatusLine(agent.id, 128)} ${sanitizeStatusLine(agent.agent, 128)} ${agent.state} ${elapsedSeconds}s${thinking} unread:${unread} [${actions}]${task}`;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function sanitizeStatusLine(value: string, maxLength: number): string {
|
|
677
|
+
return (
|
|
678
|
+
value
|
|
679
|
+
.slice(0, maxLength)
|
|
680
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: Escape untrusted terminal controls.
|
|
681
|
+
.replace(/[\u0000-\u001f\u007f-\u009f]/gu, " ")
|
|
682
|
+
.replace(/\s+/gu, " ")
|
|
683
|
+
.trim()
|
|
684
|
+
);
|
|
581
685
|
}
|
|
582
686
|
|
|
583
|
-
function
|
|
584
|
-
|
|
585
|
-
return last?.output || agent.error || `${agent.id} is ${agent.state}.`;
|
|
687
|
+
function formatLine(agent: ManagedAgent): string {
|
|
688
|
+
return formatStatefulAgentLine(agent);
|
|
586
689
|
}
|
|
587
690
|
|
|
588
691
|
function summarizeAgent(agent: ManagedAgent) {
|
|
@@ -597,6 +700,7 @@ function summarizeAgent(agent: ManagedAgent) {
|
|
|
597
700
|
createdAt: agent.createdAt,
|
|
598
701
|
updatedAt: agent.updatedAt,
|
|
599
702
|
cwd: agent.cwd,
|
|
703
|
+
thinkingLevel: agent.thinkingLevel,
|
|
600
704
|
currentTask: agent.currentTask
|
|
601
705
|
? truncateUtf8(agent.currentTask, MAX_TOOL_MESSAGE_BYTES).text
|
|
602
706
|
: undefined,
|
|
@@ -607,21 +711,188 @@ function summarizeAgent(agent: ManagedAgent) {
|
|
|
607
711
|
};
|
|
608
712
|
}
|
|
609
713
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
714
|
+
interface CompletionMetadata {
|
|
715
|
+
agentId: string;
|
|
716
|
+
agent: string;
|
|
717
|
+
state: string;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
interface CompletionMessage {
|
|
721
|
+
customType: "pi-subagent-completion";
|
|
722
|
+
content: string;
|
|
723
|
+
display: true;
|
|
724
|
+
details:
|
|
725
|
+
| CompletionMetadata
|
|
726
|
+
| {
|
|
727
|
+
completionCount: number;
|
|
728
|
+
completions: CompletionMetadata[];
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
type CompletionContext = Pick<ExtensionContext, "hasPendingMessages" | "isIdle">;
|
|
733
|
+
type CompletionPi = Pick<ExtensionAPI, "sendMessage">;
|
|
734
|
+
|
|
735
|
+
export interface CompletionDeliveryBrokerOptions {
|
|
736
|
+
onDeliveryError?: (error: unknown) => void;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Coalesces detached completions so one bounded notification batch starts at
|
|
741
|
+
* most one root synthesis turn. The broker belongs to one parent session and
|
|
742
|
+
* must be closed when that session is replaced or shut down.
|
|
743
|
+
*/
|
|
744
|
+
export class CompletionDeliveryBroker {
|
|
745
|
+
private pending: AgentTurnCompletion[] = [];
|
|
746
|
+
private flushTimer?: NodeJS.Timeout;
|
|
747
|
+
private wakeInFlight = false;
|
|
748
|
+
private closed = false;
|
|
749
|
+
|
|
750
|
+
constructor(
|
|
751
|
+
private readonly pi: CompletionPi,
|
|
752
|
+
private readonly ctx: CompletionContext,
|
|
753
|
+
private delivery: CompletionDelivery,
|
|
754
|
+
private readonly options: CompletionDeliveryBrokerOptions = {},
|
|
755
|
+
) {}
|
|
756
|
+
|
|
757
|
+
enqueue(completion: AgentTurnCompletion): void {
|
|
758
|
+
if (this.closed) return;
|
|
759
|
+
this.pending.push(completion);
|
|
760
|
+
this.scheduleFlush();
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
setDelivery(value: CompletionDelivery): void {
|
|
764
|
+
this.delivery = value;
|
|
765
|
+
this.scheduleFlush();
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
onParentTurnStart(): void {
|
|
769
|
+
this.wakeInFlight = false;
|
|
770
|
+
this.scheduleFlush();
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
onParentSettled(): void {
|
|
774
|
+
this.wakeInFlight = false;
|
|
775
|
+
this.scheduleFlush();
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
flush(): void {
|
|
779
|
+
if (this.closed || this.pending.length === 0) return;
|
|
780
|
+
if (this.flushTimer) clearTimeout(this.flushTimer);
|
|
781
|
+
this.flushTimer = undefined;
|
|
782
|
+
if (this.delivery === "auto-resume" && !this.isRootIdle()) return;
|
|
783
|
+
|
|
784
|
+
const completions = this.pending.splice(0);
|
|
785
|
+
const batches = chunkCompletions(completions);
|
|
786
|
+
let canWake = this.shouldWakeRoot();
|
|
787
|
+
for (let index = 0; index < batches.length; index++) {
|
|
788
|
+
const triggerTurn = canWake && index === batches.length - 1;
|
|
789
|
+
const message = buildCompletionMessage(batches[index]);
|
|
790
|
+
if (triggerTurn) this.wakeInFlight = true;
|
|
791
|
+
try {
|
|
792
|
+
this.pi.sendMessage(message, { deliverAs: "steer", triggerTurn });
|
|
793
|
+
} catch (primaryError) {
|
|
794
|
+
if (triggerTurn) this.wakeInFlight = false;
|
|
795
|
+
canWake = false;
|
|
796
|
+
try {
|
|
797
|
+
this.pi.sendMessage(message, { deliverAs: "nextTurn", triggerTurn: false });
|
|
798
|
+
} catch (fallbackError) {
|
|
799
|
+
this.pending = [...batches.slice(index).flat(), ...this.pending];
|
|
800
|
+
try {
|
|
801
|
+
this.options.onDeliveryError?.(
|
|
802
|
+
new AggregateError(
|
|
803
|
+
[primaryError, fallbackError],
|
|
804
|
+
"Detached subagent completion delivery failed",
|
|
805
|
+
),
|
|
806
|
+
);
|
|
807
|
+
} catch {
|
|
808
|
+
// Delivery retention must survive a failing observer.
|
|
809
|
+
}
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
close(): void {
|
|
817
|
+
this.closed = true;
|
|
818
|
+
if (this.flushTimer) clearTimeout(this.flushTimer);
|
|
819
|
+
this.flushTimer = undefined;
|
|
820
|
+
this.pending = [];
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
private scheduleFlush(): void {
|
|
824
|
+
if (this.closed || this.pending.length === 0 || this.flushTimer) return;
|
|
825
|
+
this.flushTimer = setTimeout(() => {
|
|
826
|
+
this.flushTimer = undefined;
|
|
827
|
+
this.flush();
|
|
828
|
+
}, COMPLETION_BATCH_DELAY_MS);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
private isRootIdle(): boolean {
|
|
832
|
+
try {
|
|
833
|
+
return this.ctx.isIdle();
|
|
834
|
+
} catch {
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
private shouldWakeRoot(): boolean {
|
|
840
|
+
if (this.delivery !== "auto-resume" || this.wakeInFlight) return false;
|
|
841
|
+
try {
|
|
842
|
+
return !this.ctx.hasPendingMessages();
|
|
843
|
+
} catch {
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function chunkCompletions(completions: AgentTurnCompletion[]): AgentTurnCompletion[][] {
|
|
850
|
+
const batches: AgentTurnCompletion[][] = [];
|
|
851
|
+
for (let index = 0; index < completions.length; index += MAX_COMPLETIONS_PER_MESSAGE) {
|
|
852
|
+
batches.push(completions.slice(index, index + MAX_COMPLETIONS_PER_MESSAGE));
|
|
853
|
+
}
|
|
854
|
+
return batches;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function buildCompletionMessage(completions: AgentTurnCompletion[]): CompletionMessage {
|
|
858
|
+
if (completions.length === 1) {
|
|
859
|
+
const completion = completions[0];
|
|
860
|
+
return {
|
|
614
861
|
customType: "pi-subagent-completion",
|
|
615
|
-
content,
|
|
862
|
+
content: buildDetachedCompletionMessage(completion),
|
|
616
863
|
display: true,
|
|
617
|
-
details:
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
864
|
+
details: completionMetadata(completion),
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
const content = truncateUtf8(
|
|
868
|
+
[
|
|
869
|
+
"Message Type: SUBAGENT_COMPLETION_BATCH",
|
|
870
|
+
`Completion Count: ${completions.length}`,
|
|
871
|
+
...completions.flatMap((completion, index) => [
|
|
872
|
+
"",
|
|
873
|
+
`--- Completion ${index + 1} of ${completions.length} ---`,
|
|
874
|
+
buildDetachedCompletionMessage(completion),
|
|
875
|
+
]),
|
|
876
|
+
].join("\n"),
|
|
877
|
+
DEFAULT_MAX_CONTEXT_BYTES,
|
|
878
|
+
).text;
|
|
879
|
+
return {
|
|
880
|
+
customType: "pi-subagent-completion",
|
|
881
|
+
content,
|
|
882
|
+
display: true,
|
|
883
|
+
details: {
|
|
884
|
+
completionCount: completions.length,
|
|
885
|
+
completions: completions.map(completionMetadata),
|
|
622
886
|
},
|
|
623
|
-
|
|
624
|
-
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function completionMetadata(completion: AgentTurnCompletion): CompletionMetadata {
|
|
891
|
+
return {
|
|
892
|
+
agentId: completion.agent.id,
|
|
893
|
+
agent: completion.agent.agent,
|
|
894
|
+
state: completion.agent.state,
|
|
895
|
+
};
|
|
625
896
|
}
|
|
626
897
|
|
|
627
898
|
export function buildDetachedCompletionMessage(completion: AgentTurnCompletion): string {
|
|
@@ -647,10 +918,13 @@ export function buildDetachedCompletionMessage(completion: AgentTurnCompletion):
|
|
|
647
918
|
}
|
|
648
919
|
|
|
649
920
|
function sanitizeCompletionLine(value: string, maxBytes: number): string {
|
|
650
|
-
return
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
921
|
+
return (
|
|
922
|
+
truncateUtf8(redactPrivateText(value), maxBytes)
|
|
923
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: Strip untrusted terminal controls.
|
|
924
|
+
.text.replace(/[\u0000-\u001f\u007f]+/g, " ")
|
|
925
|
+
.replace(/\s+/g, " ")
|
|
926
|
+
.trim()
|
|
927
|
+
);
|
|
654
928
|
}
|
|
655
929
|
|
|
656
930
|
async function cleanupClosedWorkspaces(
|
|
@@ -678,6 +952,12 @@ export function resolveStatefulTransportKind(
|
|
|
678
952
|
return value ?? "subprocess";
|
|
679
953
|
}
|
|
680
954
|
|
|
955
|
+
export function resolveCompletionDelivery(
|
|
956
|
+
value: CompletionDelivery | undefined,
|
|
957
|
+
): CompletionDelivery {
|
|
958
|
+
return value ?? "next-turn";
|
|
959
|
+
}
|
|
960
|
+
|
|
681
961
|
function normalizeRuntimeThinkingLevel(value: string): ParentRuntimeSnapshot["thinkingLevel"] {
|
|
682
962
|
return isThinkingLevel(value) ? value : "off";
|
|
683
963
|
}
|