@narumitw/pi-subagents 0.20.0 → 0.26.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 +94 -50
- package/package.json +1 -1
- package/src/agents.ts +8 -4
- 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.ts +398 -254
- package/src/subagents.ts +20 -19
- package/src/subprocess-transport.ts +10 -9
- package/src/orchestration.ts +0 -164
package/src/stateful.ts
CHANGED
|
@@ -1,30 +1,31 @@
|
|
|
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 {
|
|
11
|
-
ORCHESTRATION_MARKER_PREFIX,
|
|
12
|
-
RootOrchestrationState,
|
|
13
|
-
type OrchestrationRecoveryTicket,
|
|
14
|
-
} from "./orchestration.js";
|
|
15
|
-
import { AgentPersistence } from "./persistence.js";
|
|
16
|
-
import {
|
|
17
|
-
AgentRegistry,
|
|
18
|
-
type AgentTurnCompletion,
|
|
19
|
-
type ManagedAgent,
|
|
20
|
-
} from "./registry.js";
|
|
21
|
-
import { readSubagentSettings } from "./settings.js";
|
|
22
|
-
import { SubprocessTransport } from "./subprocess-transport.js";
|
|
23
19
|
import {
|
|
24
20
|
type ChildSessionFactory,
|
|
25
21
|
InProcessTransport,
|
|
26
22
|
type ParentRuntimeSnapshot,
|
|
27
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 { SubprocessTransport } from "./subprocess-transport.js";
|
|
28
29
|
import { WorkspaceManager } from "./workspace.js";
|
|
29
30
|
|
|
30
31
|
const ContextModeSchema = Type.Union([
|
|
@@ -36,31 +37,121 @@ const ScopeSchema = StringEnum(["user", "project", "both"] as const, {
|
|
|
36
37
|
'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.',
|
|
37
38
|
default: "user",
|
|
38
39
|
});
|
|
40
|
+
const StatefulThinkingLevelSchema = StringEnum(THINKING_LEVELS, {
|
|
41
|
+
description:
|
|
42
|
+
"Optional requested Pi thinking level selected for this task difficulty; retained for every turn of the spawned agent.",
|
|
43
|
+
});
|
|
39
44
|
const MAX_TOOL_MESSAGE_BYTES = 2 * 1024;
|
|
40
45
|
const MAX_COMPLETION_ERROR_BYTES = 512;
|
|
46
|
+
const MAX_COMPLETIONS_PER_MESSAGE = 16;
|
|
47
|
+
const COMPLETION_BATCH_DELAY_MS = 10;
|
|
48
|
+
|
|
49
|
+
function createSpawnPromptGuidelines(completionDelivery: CompletionDelivery): string[] {
|
|
50
|
+
const deliveryGuidance =
|
|
51
|
+
completionDelivery === "auto-resume"
|
|
52
|
+
? "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."
|
|
53
|
+
: "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.";
|
|
54
|
+
const noLocalWorkGuidance =
|
|
55
|
+
completionDelivery === "auto-resume"
|
|
56
|
+
? "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."
|
|
57
|
+
: "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.";
|
|
58
|
+
return [
|
|
59
|
+
"Do not use subagent_spawn for simple or critical-path work that the main agent can perform directly.",
|
|
60
|
+
"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.",
|
|
61
|
+
deliveryGuidance,
|
|
62
|
+
"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.",
|
|
63
|
+
"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.",
|
|
64
|
+
"When subagent_spawn fits the completion-delivery policy, do not choose a blocking parallel subagent merely to keep delegation in the same turn.",
|
|
65
|
+
"Add another subagent_spawn only for truly independent work with safe workspace concurrency.",
|
|
66
|
+
noLocalWorkGuidance,
|
|
67
|
+
"Consume and synthesize available subagent_spawn completion messages; interrupt or close agents that are no longer needed.",
|
|
68
|
+
"subagent_spawn completion is delivered automatically. Do not poll with subagent_list or subagent_messages, repeatedly check progress, or duplicate the delegated work.",
|
|
69
|
+
];
|
|
70
|
+
}
|
|
41
71
|
|
|
42
72
|
export interface StatefulSubagentDependencies {
|
|
43
73
|
createInProcessSession?: ChildSessionFactory;
|
|
74
|
+
workspaceManager?: WorkspaceManager;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface StatefulSubagentRuntimeStatus {
|
|
78
|
+
enabled: boolean;
|
|
79
|
+
initialized: boolean;
|
|
80
|
+
transport: "subprocess" | "in-process";
|
|
81
|
+
completionDelivery: CompletionDelivery;
|
|
82
|
+
activeAgents: number;
|
|
83
|
+
retainedAgents: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface StatefulSubagentController {
|
|
87
|
+
getCompletionDelivery(): CompletionDelivery;
|
|
88
|
+
setCompletionDelivery(value: CompletionDelivery): void;
|
|
89
|
+
getRuntimeStatus(): StatefulSubagentRuntimeStatus;
|
|
90
|
+
listAgents(includeClosed?: boolean): ManagedAgent[];
|
|
91
|
+
clearAgents(): Promise<number>;
|
|
44
92
|
}
|
|
45
93
|
|
|
46
94
|
export function registerStatefulSubagents(
|
|
47
95
|
pi: ExtensionAPI,
|
|
48
96
|
dependencies: StatefulSubagentDependencies = {},
|
|
49
|
-
):
|
|
97
|
+
): StatefulSubagentController {
|
|
50
98
|
const settings = readSubagentSettings()?.stateful ?? {};
|
|
51
|
-
|
|
52
|
-
|
|
99
|
+
const enabled = settings.enabled !== false;
|
|
100
|
+
const transportKind = resolveStatefulTransportKind(settings.transport);
|
|
101
|
+
let completionDelivery = resolveCompletionDelivery(settings.completionDelivery);
|
|
102
|
+
let completionBroker: CompletionDeliveryBroker | undefined;
|
|
103
|
+
let refreshSpawnToolRegistration: (() => void) | undefined;
|
|
53
104
|
let registry: AgentRegistry | undefined;
|
|
54
105
|
let persistence: AgentPersistence | undefined;
|
|
55
106
|
let sweepTimer: NodeJS.Timeout | undefined;
|
|
56
107
|
let runtimeGeneration = 0;
|
|
57
|
-
const workspaceManager = new WorkspaceManager();
|
|
108
|
+
const workspaceManager = dependencies.workspaceManager ?? new WorkspaceManager();
|
|
58
109
|
const isolatedAgents = new Map<string, string>();
|
|
59
110
|
const seenMessageIds = new Set<string>();
|
|
60
111
|
const parentRuntime: ParentRuntimeSnapshot = { model: undefined, thinkingLevel: "off" };
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
112
|
+
|
|
113
|
+
const clearAgents = async (): Promise<number> => {
|
|
114
|
+
const currentRegistry = registry;
|
|
115
|
+
if (!currentRegistry) return 0;
|
|
116
|
+
const count = currentRegistry.list().length;
|
|
117
|
+
try {
|
|
118
|
+
await currentRegistry.closeAll();
|
|
119
|
+
} finally {
|
|
120
|
+
await workspaceManager.cleanupAll();
|
|
121
|
+
isolatedAgents.clear();
|
|
122
|
+
}
|
|
123
|
+
seenMessageIds.clear();
|
|
124
|
+
await persistence?.delete();
|
|
125
|
+
return count;
|
|
126
|
+
};
|
|
127
|
+
const controller: StatefulSubagentController = {
|
|
128
|
+
getCompletionDelivery() {
|
|
129
|
+
return completionDelivery;
|
|
130
|
+
},
|
|
131
|
+
setCompletionDelivery(value) {
|
|
132
|
+
completionDelivery = value;
|
|
133
|
+
completionBroker?.setDelivery(value);
|
|
134
|
+
refreshSpawnToolRegistration?.();
|
|
135
|
+
},
|
|
136
|
+
getRuntimeStatus() {
|
|
137
|
+
const agents = registry?.list(true) ?? [];
|
|
138
|
+
return {
|
|
139
|
+
enabled,
|
|
140
|
+
initialized: registry !== undefined,
|
|
141
|
+
transport: transportKind,
|
|
142
|
+
completionDelivery,
|
|
143
|
+
activeAgents: agents.filter(
|
|
144
|
+
(agent) => agent.state === "starting" || agent.state === "running",
|
|
145
|
+
).length,
|
|
146
|
+
retainedAgents: agents.filter((agent) => agent.state !== "closed").length,
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
listAgents(includeClosed = false) {
|
|
150
|
+
return registry?.list(includeClosed) ?? [];
|
|
151
|
+
},
|
|
152
|
+
clearAgents,
|
|
153
|
+
};
|
|
154
|
+
if (!enabled) return controller;
|
|
64
155
|
|
|
65
156
|
const requireRegistry = () => {
|
|
66
157
|
if (!registry) throw new Error("Stateful subagents are not initialized for this session");
|
|
@@ -69,24 +160,34 @@ export function registerStatefulSubagents(
|
|
|
69
160
|
|
|
70
161
|
pi.on("session_start", async (_event, ctx) => {
|
|
71
162
|
const generation = ++runtimeGeneration;
|
|
72
|
-
|
|
73
|
-
|
|
163
|
+
completionBroker?.close();
|
|
164
|
+
completionBroker = undefined;
|
|
74
165
|
parentRuntime.model = ctx.model;
|
|
75
166
|
parentRuntime.thinkingLevel = normalizeRuntimeThinkingLevel(pi.getThinkingLevel());
|
|
76
|
-
const owner =
|
|
167
|
+
const owner =
|
|
168
|
+
ctx.sessionManager.getSessionId?.() ??
|
|
169
|
+
ctx.sessionManager.getSessionFile?.() ??
|
|
170
|
+
`ephemeral:${ctx.cwd}`;
|
|
77
171
|
const sessionPersistence = new AgentPersistence(owner, {
|
|
78
172
|
retentionDays: settings.retentionDays,
|
|
79
173
|
maxStoredAgents: settings.maxStoredAgents,
|
|
80
174
|
});
|
|
81
175
|
persistence = sessionPersistence;
|
|
176
|
+
completionBroker = new CompletionDeliveryBroker(pi, ctx, completionDelivery, {
|
|
177
|
+
onDeliveryError: (error) => {
|
|
178
|
+
if (!ctx.hasUI) return;
|
|
179
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
180
|
+
ctx.ui.notify(`Subagent completion delivery failed: ${reason}`, "warning");
|
|
181
|
+
},
|
|
182
|
+
});
|
|
82
183
|
const transport =
|
|
83
|
-
|
|
184
|
+
transportKind === "in-process"
|
|
84
185
|
? new InProcessTransport({
|
|
85
186
|
modelRegistry: ctx.modelRegistry,
|
|
86
187
|
getParentRuntime: () => ({ ...parentRuntime }),
|
|
87
188
|
createSession: dependencies.createInProcessSession,
|
|
88
189
|
})
|
|
89
|
-
: new SubprocessTransport(
|
|
190
|
+
: new SubprocessTransport();
|
|
90
191
|
registry = new AgentRegistry(transport, {
|
|
91
192
|
maxAgents: settings.maxAgents,
|
|
92
193
|
maxActiveTurns: settings.maxActiveTurns,
|
|
@@ -112,18 +213,14 @@ export function registerStatefulSubagents(
|
|
|
112
213
|
},
|
|
113
214
|
onTurnComplete: (completion) => {
|
|
114
215
|
if (generation !== runtimeGeneration) return;
|
|
115
|
-
|
|
116
|
-
if (!completionWaiters.has(completion.agent.id)) {
|
|
117
|
-
sendDetachedCompletion(pi, completion);
|
|
118
|
-
}
|
|
216
|
+
completionBroker?.enqueue(completion);
|
|
119
217
|
},
|
|
120
218
|
});
|
|
121
219
|
const restored = sessionPersistence
|
|
122
220
|
.load()
|
|
123
221
|
.filter(
|
|
124
222
|
(agent) =>
|
|
125
|
-
(agent.agentScope !== "project" && agent.agentScope !== "both") ||
|
|
126
|
-
ctx.isProjectTrusted(),
|
|
223
|
+
(agent.agentScope !== "project" && agent.agentScope !== "both") || ctx.isProjectTrusted(),
|
|
127
224
|
);
|
|
128
225
|
for (const agent of restored) {
|
|
129
226
|
for (const message of agent.mailbox) seenMessageIds.add(message.id);
|
|
@@ -140,36 +237,12 @@ export function registerStatefulSubagents(
|
|
|
140
237
|
sweepTimer.unref();
|
|
141
238
|
});
|
|
142
239
|
|
|
143
|
-
pi.on("
|
|
144
|
-
|
|
145
|
-
const nonce = extractOrchestrationNonce(event.text);
|
|
146
|
-
if (nonce && cancelledRecoveryNonces.delete(nonce)) return { action: "handled" as const };
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
rememberCancelledRecovery(orchestration.supersedePending(), cancelledRecoveryNonces);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
pi.on("before_agent_start", () => {
|
|
153
|
-
orchestration.beginTurn();
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
pi.on("before_provider_request", () => {
|
|
157
|
-
orchestration.observeAvailable();
|
|
240
|
+
pi.on("agent_start", () => {
|
|
241
|
+
completionBroker?.onParentTurnStart();
|
|
158
242
|
});
|
|
159
243
|
|
|
160
|
-
pi.on("
|
|
161
|
-
|
|
162
|
-
if (ticket && !hasPendingRootMessages(ctx)) queueOrchestrationFollowUp(pi, ctx, ticket);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
const settledEvents = pi as unknown as {
|
|
166
|
-
on(
|
|
167
|
-
event: "agent_settled",
|
|
168
|
-
handler: (event: unknown, ctx: ExtensionContext) => void,
|
|
169
|
-
): void;
|
|
170
|
-
};
|
|
171
|
-
settledEvents.on("agent_settled", (_event, ctx) => {
|
|
172
|
-
dispatchOrchestrationRecovery(pi, ctx, orchestration);
|
|
244
|
+
pi.on("agent_settled", () => {
|
|
245
|
+
completionBroker?.onParentSettled();
|
|
173
246
|
});
|
|
174
247
|
|
|
175
248
|
pi.on("model_select", (event) => {
|
|
@@ -182,8 +255,8 @@ export function registerStatefulSubagents(
|
|
|
182
255
|
|
|
183
256
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
184
257
|
runtimeGeneration++;
|
|
185
|
-
|
|
186
|
-
|
|
258
|
+
completionBroker?.close();
|
|
259
|
+
completionBroker = undefined;
|
|
187
260
|
if (sweepTimer) clearInterval(sweepTimer);
|
|
188
261
|
sweepTimer = undefined;
|
|
189
262
|
for (const agentId of isolatedAgents.keys()) {
|
|
@@ -191,41 +264,35 @@ export function registerStatefulSubagents(
|
|
|
191
264
|
}
|
|
192
265
|
isolatedAgents.clear();
|
|
193
266
|
seenMessageIds.clear();
|
|
194
|
-
completionWaiters.clear();
|
|
195
267
|
let cleanupError: unknown;
|
|
196
268
|
try {
|
|
197
269
|
await workspaceManager.cleanupAll();
|
|
198
270
|
} catch (error) {
|
|
199
271
|
cleanupError = error;
|
|
200
272
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
273
|
+
try {
|
|
274
|
+
await registry?.shutdown();
|
|
275
|
+
} finally {
|
|
276
|
+
registry = undefined;
|
|
277
|
+
persistence = undefined;
|
|
278
|
+
}
|
|
204
279
|
if (cleanupError && ctx.hasUI) {
|
|
205
280
|
const reason = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
|
|
206
|
-
ctx.ui.notify(
|
|
207
|
-
`Some isolated subagent workspaces could not be removed: ${reason}`,
|
|
208
|
-
"warning",
|
|
209
|
-
);
|
|
281
|
+
ctx.ui.notify(`Some isolated subagent workspaces could not be removed: ${reason}`, "warning");
|
|
210
282
|
}
|
|
211
283
|
});
|
|
212
284
|
|
|
213
|
-
|
|
285
|
+
const spawnTool = defineTool({
|
|
214
286
|
name: "subagent_spawn",
|
|
215
287
|
label: "Spawn Subagent",
|
|
216
|
-
description:
|
|
288
|
+
description:
|
|
289
|
+
"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.",
|
|
217
290
|
promptSnippet: "Start a reusable detached subagent; completion is delivered asynchronously",
|
|
218
|
-
promptGuidelines:
|
|
219
|
-
"Do not delegate simple or critical-path work that the main agent can perform directly.",
|
|
220
|
-
"A single detached subagent is appropriate only for a concrete isolation or specialization benefit such as independent review, bounded context/output, a distinct model/tool profile, or workspace isolation.",
|
|
221
|
-
"Use one blocking subagent parallel call for multiple independent one-shot tasks; do not use repeated detached spawns when no reuse or overlap is needed.",
|
|
222
|
-
"After spawning, continue useful non-overlapping local work when available; otherwise call subagent_wait rather than yielding while delegated work remains unresolved.",
|
|
223
|
-
"Consume available completion messages and synthesize their results before finishing; interrupt or close agents that are no longer needed.",
|
|
224
|
-
"Detached completion is delivered automatically. Do not poll, wait forever, or spawn additional agents without a distinct need.",
|
|
225
|
-
],
|
|
291
|
+
promptGuidelines: createSpawnPromptGuidelines(completionDelivery),
|
|
226
292
|
parameters: Type.Object({
|
|
227
293
|
agent: Type.String({ minLength: 1 }),
|
|
228
294
|
task: Type.String({ minLength: 1, maxLength: DEFAULT_MAX_CONTEXT_BYTES }),
|
|
295
|
+
thinkingLevel: Type.Optional(StatefulThinkingLevelSchema),
|
|
229
296
|
cwd: Type.Optional(Type.String()),
|
|
230
297
|
agentScope: Type.Optional(ScopeSchema),
|
|
231
298
|
confirmProjectAgents: Type.Optional(Type.Boolean({ default: true })),
|
|
@@ -247,13 +314,7 @@ export function registerStatefulSubagents(
|
|
|
247
314
|
const scope = (params.agentScope ?? "user") as AgentScope;
|
|
248
315
|
assertSubagentDepthAllowed();
|
|
249
316
|
const cwd = params.cwd ?? ctx.cwd;
|
|
250
|
-
await confirmProjectAgent(
|
|
251
|
-
params.agent,
|
|
252
|
-
scope,
|
|
253
|
-
params.confirmProjectAgents ?? true,
|
|
254
|
-
ctx,
|
|
255
|
-
cwd,
|
|
256
|
-
);
|
|
317
|
+
await confirmProjectAgent(params.agent, scope, params.confirmProjectAgents ?? true, ctx, cwd);
|
|
257
318
|
const resolvedAgent = discoverAgents(cwd, scope, readSubagentSettings()).agents.find(
|
|
258
319
|
(agent) => agent.name === params.agent,
|
|
259
320
|
);
|
|
@@ -269,12 +330,7 @@ export function registerStatefulSubagents(
|
|
|
269
330
|
);
|
|
270
331
|
const requestedCwd = cwd;
|
|
271
332
|
if ((params.workspaceMode ?? "shared") === "shared" && !params.allowConcurrentWrites) {
|
|
272
|
-
assertNoSharedWriteConflict(
|
|
273
|
-
requireRegistry(),
|
|
274
|
-
params.agent,
|
|
275
|
-
requestedCwd,
|
|
276
|
-
scope,
|
|
277
|
-
);
|
|
333
|
+
assertNoSharedWriteConflict(requireRegistry(), params.agent, requestedCwd, scope);
|
|
278
334
|
}
|
|
279
335
|
const workspaceOwner = `pending-${randomUUID()}`;
|
|
280
336
|
const workspace =
|
|
@@ -288,6 +344,7 @@ export function registerStatefulSubagents(
|
|
|
288
344
|
task: params.task,
|
|
289
345
|
cwd: workspace?.path ?? requestedCwd,
|
|
290
346
|
agentScope: scope,
|
|
347
|
+
thinkingLevel: params.thinkingLevel,
|
|
291
348
|
parentId: params.parentId,
|
|
292
349
|
context: snapshot.text || undefined,
|
|
293
350
|
contextSourceIds: snapshot.sourceIds,
|
|
@@ -298,13 +355,21 @@ export function registerStatefulSubagents(
|
|
|
298
355
|
throw error;
|
|
299
356
|
}
|
|
300
357
|
if (workspace) isolatedAgents.set(agent.id, workspaceOwner);
|
|
301
|
-
|
|
358
|
+
const deliveryNote =
|
|
359
|
+
completionDelivery === "auto-resume"
|
|
360
|
+
? "If no useful local work remains, briefly tell the user what was launched and end the response; auto-resume will request synthesis after completion."
|
|
361
|
+
: "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.";
|
|
302
362
|
return result(
|
|
303
363
|
agent,
|
|
304
|
-
`Spawned ${agent.agent} as ${agent.id}.
|
|
364
|
+
`Spawned ${agent.agent} as ${agent.id}. Do useful non-overlapping work immediately. ${deliveryNote} Do not poll for progress.`,
|
|
305
365
|
);
|
|
306
366
|
},
|
|
307
367
|
});
|
|
368
|
+
refreshSpawnToolRegistration = () => {
|
|
369
|
+
spawnTool.promptGuidelines = createSpawnPromptGuidelines(completionDelivery);
|
|
370
|
+
pi.registerTool(spawnTool);
|
|
371
|
+
};
|
|
372
|
+
refreshSpawnToolRegistration();
|
|
308
373
|
|
|
309
374
|
pi.registerTool({
|
|
310
375
|
name: "subagent_send",
|
|
@@ -334,7 +399,6 @@ export function registerStatefulSubagents(
|
|
|
334
399
|
isolatedAgents.has(existing.id),
|
|
335
400
|
);
|
|
336
401
|
const agent = await requireRegistry().followUp(params.agentId, params.task);
|
|
337
|
-
trackSpawnedAgent(orchestration, agent);
|
|
338
402
|
return result(agent, `Started follow-up for ${agent.id}.`);
|
|
339
403
|
},
|
|
340
404
|
});
|
|
@@ -384,9 +448,7 @@ export function registerStatefulSubagents(
|
|
|
384
448
|
}));
|
|
385
449
|
const text = summaries.length
|
|
386
450
|
? summaries
|
|
387
|
-
.map(
|
|
388
|
-
(message) => `${message.id} from ${message.senderId}: ${message.content}`,
|
|
389
|
-
)
|
|
451
|
+
.map((message) => `${message.id} from ${message.senderId}: ${message.content}`)
|
|
390
452
|
.join("\n")
|
|
391
453
|
: "No unread messages.";
|
|
392
454
|
return {
|
|
@@ -396,35 +458,6 @@ export function registerStatefulSubagents(
|
|
|
396
458
|
},
|
|
397
459
|
});
|
|
398
460
|
|
|
399
|
-
pi.registerTool({
|
|
400
|
-
name: "subagent_wait",
|
|
401
|
-
label: "Wait for Subagent",
|
|
402
|
-
description: "Wait for a stateful subagent turn without terminating it when the wait times out.",
|
|
403
|
-
parameters: Type.Object({
|
|
404
|
-
agentId: Type.String(),
|
|
405
|
-
timeoutMs: Type.Optional(Type.Number({ minimum: 1, maximum: 3_600_000, default: 30_000 })),
|
|
406
|
-
}),
|
|
407
|
-
async execute(_id, params, signal) {
|
|
408
|
-
const existing = requireRegistry().get(params.agentId);
|
|
409
|
-
const registered = existing?.state === "starting" || existing?.state === "running";
|
|
410
|
-
if (registered) {
|
|
411
|
-
completionWaiters.set(params.agentId, (completionWaiters.get(params.agentId) ?? 0) + 1);
|
|
412
|
-
}
|
|
413
|
-
try {
|
|
414
|
-
const waited = await requireRegistry().wait(params.agentId, params.timeoutMs, signal);
|
|
415
|
-
if (!waited.timedOut) orchestration.observe(waited.agent.id);
|
|
416
|
-
return result(
|
|
417
|
-
waited.agent,
|
|
418
|
-
waited.timedOut
|
|
419
|
-
? `Wait timed out; ${waited.agent.id} is ${waited.agent.state}.`
|
|
420
|
-
: formatFinal(waited.agent),
|
|
421
|
-
);
|
|
422
|
-
} finally {
|
|
423
|
-
if (registered) decrementWaiter(completionWaiters, params.agentId);
|
|
424
|
-
}
|
|
425
|
-
},
|
|
426
|
-
});
|
|
427
|
-
|
|
428
461
|
pi.registerTool({
|
|
429
462
|
name: "subagent_list",
|
|
430
463
|
label: "List Subagents",
|
|
@@ -436,9 +469,7 @@ export function registerStatefulSubagents(
|
|
|
436
469
|
content: [
|
|
437
470
|
{
|
|
438
471
|
type: "text",
|
|
439
|
-
text: agents.length
|
|
440
|
-
? agents.map(formatLine).join("\n")
|
|
441
|
-
: "No stateful subagents.",
|
|
472
|
+
text: agents.length ? agents.map(formatLine).join("\n") : "No stateful subagents.",
|
|
442
473
|
},
|
|
443
474
|
],
|
|
444
475
|
details: { agents: agents.map(summarizeAgent) },
|
|
@@ -457,10 +488,12 @@ export function registerStatefulSubagents(
|
|
|
457
488
|
async execute(_id, params) {
|
|
458
489
|
if (params.subtree) {
|
|
459
490
|
const agents = await requireRegistry().interruptTree(params.agentId);
|
|
491
|
+
const root = requireRegistry().get(params.agentId);
|
|
492
|
+
if (!root) throw new Error(`Unknown subagent: ${params.agentId}`);
|
|
460
493
|
return {
|
|
461
494
|
content: [{ type: "text", text: `Interrupted ${agents.length} active agent(s).` }],
|
|
462
495
|
details: {
|
|
463
|
-
agent: summarizeAgent(
|
|
496
|
+
agent: summarizeAgent(root),
|
|
464
497
|
agents: agents.map(summarizeAgent),
|
|
465
498
|
},
|
|
466
499
|
};
|
|
@@ -481,7 +514,6 @@ export function registerStatefulSubagents(
|
|
|
481
514
|
async execute(_id, params) {
|
|
482
515
|
const existing = requireRegistry().get(params.agentId);
|
|
483
516
|
if (existing?.state === "closed" && !params.subtree) {
|
|
484
|
-
orchestration.resolve(existing.id);
|
|
485
517
|
const pendingOwner = isolatedAgents.get(existing.id);
|
|
486
518
|
if (pendingOwner) await workspaceManager.cleanup(pendingOwner);
|
|
487
519
|
isolatedAgents.delete(existing.id);
|
|
@@ -494,11 +526,12 @@ export function registerStatefulSubagents(
|
|
|
494
526
|
} finally {
|
|
495
527
|
await cleanupClosedWorkspaces(requireRegistry(), isolatedAgents, workspaceManager);
|
|
496
528
|
}
|
|
497
|
-
|
|
529
|
+
const root = requireRegistry().get(params.agentId);
|
|
530
|
+
if (!root) throw new Error(`Unknown subagent: ${params.agentId}`);
|
|
498
531
|
return {
|
|
499
532
|
content: [{ type: "text", text: `Closed ${agents.length} agent(s).` }],
|
|
500
533
|
details: {
|
|
501
|
-
agent: summarizeAgent(
|
|
534
|
+
agent: summarizeAgent(root),
|
|
502
535
|
agents: agents.map(summarizeAgent),
|
|
503
536
|
},
|
|
504
537
|
};
|
|
@@ -509,39 +542,44 @@ export function registerStatefulSubagents(
|
|
|
509
542
|
} finally {
|
|
510
543
|
await cleanupClosedWorkspaces(requireRegistry(), isolatedAgents, workspaceManager);
|
|
511
544
|
}
|
|
512
|
-
orchestration.resolve(agent.id);
|
|
513
545
|
return result(agent, `Closed ${agent.id}.`);
|
|
514
546
|
},
|
|
515
547
|
});
|
|
516
548
|
|
|
517
549
|
pi.registerCommand("subagents:agents", {
|
|
518
|
-
description: "Inspect or clear
|
|
550
|
+
description: "Inspect or clear current-session subagents",
|
|
519
551
|
getArgumentCompletions(prefix: string) {
|
|
520
552
|
return ["list", "clear"]
|
|
521
553
|
.filter((value) => value.startsWith(prefix))
|
|
522
554
|
.map((value) => ({ value, label: value }));
|
|
523
555
|
},
|
|
524
556
|
async handler(args, ctx) {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
await persistence?.delete();
|
|
535
|
-
ctx.ui.notify("Cleared stateful subagents.", "info");
|
|
557
|
+
const subcommand = args.trim().toLowerCase() || "list";
|
|
558
|
+
if (subcommand === "clear") {
|
|
559
|
+
const count = await controller.clearAgents();
|
|
560
|
+
ctx.ui.notify(
|
|
561
|
+
count > 0
|
|
562
|
+
? `Cleared ${count} current-session subagent${count === 1 ? "" : "s"}.`
|
|
563
|
+
: statefulEmptyMessage(controller.getRuntimeStatus()),
|
|
564
|
+
"info",
|
|
565
|
+
);
|
|
536
566
|
return;
|
|
537
567
|
}
|
|
538
|
-
|
|
568
|
+
if (subcommand !== "list") {
|
|
569
|
+
ctx.ui.notify(`Unknown /subagents:agents subcommand: ${subcommand}`, "warning");
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
const agents = controller.listAgents(true);
|
|
539
573
|
ctx.ui.notify(
|
|
540
|
-
agents.length
|
|
574
|
+
agents.length
|
|
575
|
+
? agents.map(formatLine).join("\n")
|
|
576
|
+
: statefulEmptyMessage(controller.getRuntimeStatus()),
|
|
541
577
|
"info",
|
|
542
578
|
);
|
|
543
579
|
},
|
|
544
580
|
});
|
|
581
|
+
|
|
582
|
+
return controller;
|
|
545
583
|
}
|
|
546
584
|
|
|
547
585
|
export function assertNoSharedWriteConflict(
|
|
@@ -564,7 +602,7 @@ export function assertNoSharedWriteConflict(
|
|
|
564
602
|
if (isWriteCapable(activeConfig?.tools)) {
|
|
565
603
|
throw new Error(
|
|
566
604
|
`Write-capable subagent ${active.id} is already active in shared workspace ${cwd}. ` +
|
|
567
|
-
"
|
|
605
|
+
"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.",
|
|
568
606
|
);
|
|
569
607
|
}
|
|
570
608
|
}
|
|
@@ -577,12 +615,7 @@ export function assertFollowUpWriteAllowed(
|
|
|
577
615
|
isolatedWorkspace: boolean,
|
|
578
616
|
): void {
|
|
579
617
|
if (allowConcurrentWrites || isolatedWorkspace) return;
|
|
580
|
-
assertNoSharedWriteConflict(
|
|
581
|
-
registry,
|
|
582
|
-
agent.agent,
|
|
583
|
-
agent.cwd,
|
|
584
|
-
agent.agentScope ?? "user",
|
|
585
|
-
);
|
|
618
|
+
assertNoSharedWriteConflict(registry, agent.agent, agent.cwd, agent.agentScope ?? "user");
|
|
586
619
|
}
|
|
587
620
|
|
|
588
621
|
export function isWriteCapable(tools: string[] | undefined): boolean {
|
|
@@ -590,12 +623,6 @@ export function isWriteCapable(tools: string[] | undefined): boolean {
|
|
|
590
623
|
return tools.some((tool) => ["bash", "write", "edit"].includes(tool));
|
|
591
624
|
}
|
|
592
625
|
|
|
593
|
-
function decrementWaiter(waiters: Map<string, number>, agentId: string): void {
|
|
594
|
-
const count = waiters.get(agentId);
|
|
595
|
-
if (count === undefined || count <= 1) waiters.delete(agentId);
|
|
596
|
-
else waiters.set(agentId, count - 1);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
626
|
async function confirmProjectAgent(
|
|
600
627
|
name: string,
|
|
601
628
|
scope: AgentScope,
|
|
@@ -614,7 +641,10 @@ async function confirmProjectAgent(
|
|
|
614
641
|
throw new Error("Project-local subagent definitions require a trusted project");
|
|
615
642
|
}
|
|
616
643
|
if (confirm && ctx.hasUI) {
|
|
617
|
-
const approved = await ctx.ui.confirm(
|
|
644
|
+
const approved = await ctx.ui.confirm(
|
|
645
|
+
"Run project-local agent?",
|
|
646
|
+
`Agent: ${name}\nSource: ${agent.filePath}`,
|
|
647
|
+
);
|
|
618
648
|
if (!approved) throw new Error("Project-local subagent was not approved");
|
|
619
649
|
}
|
|
620
650
|
}
|
|
@@ -623,9 +653,7 @@ function isSameCwd(left: string, right: string): boolean {
|
|
|
623
653
|
return path.resolve(left) === path.resolve(right);
|
|
624
654
|
}
|
|
625
655
|
|
|
626
|
-
function normalizeContextMode(
|
|
627
|
-
value: "none" | "all" | "summary" | number | undefined,
|
|
628
|
-
): ContextMode {
|
|
656
|
+
function normalizeContextMode(value: "none" | "all" | "summary" | number | undefined): ContextMode {
|
|
629
657
|
if (value === undefined) return "none";
|
|
630
658
|
if (value === "none" || value === "all" || value === "summary") return value;
|
|
631
659
|
return Math.max(1, Math.floor(value));
|
|
@@ -639,23 +667,40 @@ export function resolveSpawnContextMode(
|
|
|
639
667
|
return normalizeContextMode(value);
|
|
640
668
|
}
|
|
641
669
|
|
|
642
|
-
function
|
|
670
|
+
function statefulEmptyMessage(status: StatefulSubagentRuntimeStatus): string {
|
|
671
|
+
if (!status.enabled) return "Stateful subagents are disabled in user settings.";
|
|
672
|
+
if (!status.initialized) return "Stateful subagents are not initialized for this session.";
|
|
673
|
+
return "No current-session subagents.";
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
export function formatStatefulAgentLine(agent: ManagedAgent): string {
|
|
643
677
|
const elapsedSeconds = Math.max(0, Math.floor((Date.now() - agent.updatedAt) / 1000));
|
|
644
678
|
const actions =
|
|
645
679
|
agent.state === "running" || agent.state === "starting"
|
|
646
|
-
? "
|
|
680
|
+
? "interrupt, close"
|
|
647
681
|
: agent.state === "closed"
|
|
648
682
|
? "inspect"
|
|
649
683
|
: "send, close";
|
|
650
|
-
const task = agent.currentTask ? ` — ${agent.currentTask
|
|
684
|
+
const task = agent.currentTask ? ` — ${sanitizeStatusLine(agent.currentTask, 80)}` : "";
|
|
651
685
|
const unread = agent.mailbox.filter((message) => !message.readAt).length;
|
|
652
686
|
const indent = " ".repeat(agent.depth);
|
|
653
|
-
|
|
687
|
+
const thinking = agent.thinkingLevel ? ` thinking:${agent.thinkingLevel}` : "";
|
|
688
|
+
return `${indent}${sanitizeStatusLine(agent.id, 128)} ${sanitizeStatusLine(agent.agent, 128)} ${agent.state} ${elapsedSeconds}s${thinking} unread:${unread} [${actions}]${task}`;
|
|
654
689
|
}
|
|
655
690
|
|
|
656
|
-
function
|
|
657
|
-
|
|
658
|
-
|
|
691
|
+
function sanitizeStatusLine(value: string, maxLength: number): string {
|
|
692
|
+
return (
|
|
693
|
+
value
|
|
694
|
+
.slice(0, maxLength)
|
|
695
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: Escape untrusted terminal controls.
|
|
696
|
+
.replace(/[\u0000-\u001f\u007f-\u009f]/gu, " ")
|
|
697
|
+
.replace(/\s+/gu, " ")
|
|
698
|
+
.trim()
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function formatLine(agent: ManagedAgent): string {
|
|
703
|
+
return formatStatefulAgentLine(agent);
|
|
659
704
|
}
|
|
660
705
|
|
|
661
706
|
function summarizeAgent(agent: ManagedAgent) {
|
|
@@ -670,6 +715,7 @@ function summarizeAgent(agent: ManagedAgent) {
|
|
|
670
715
|
createdAt: agent.createdAt,
|
|
671
716
|
updatedAt: agent.updatedAt,
|
|
672
717
|
cwd: agent.cwd,
|
|
718
|
+
thinkingLevel: agent.thinkingLevel,
|
|
673
719
|
currentTask: agent.currentTask
|
|
674
720
|
? truncateUtf8(agent.currentTask, MAX_TOOL_MESSAGE_BYTES).text
|
|
675
721
|
: undefined,
|
|
@@ -680,99 +726,188 @@ function summarizeAgent(agent: ManagedAgent) {
|
|
|
680
726
|
};
|
|
681
727
|
}
|
|
682
728
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
agent:
|
|
686
|
-
|
|
687
|
-
orchestration.spawn(agent.id);
|
|
688
|
-
if (agent.state === "closed") orchestration.resolve(agent.id);
|
|
689
|
-
else if (agent.state !== "starting" && agent.state !== "running") {
|
|
690
|
-
orchestration.complete(agent.id);
|
|
691
|
-
}
|
|
729
|
+
interface CompletionMetadata {
|
|
730
|
+
agentId: string;
|
|
731
|
+
agent: string;
|
|
732
|
+
state: string;
|
|
692
733
|
}
|
|
693
734
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
735
|
+
interface CompletionMessage {
|
|
736
|
+
customType: "pi-subagent-completion";
|
|
737
|
+
content: string;
|
|
738
|
+
display: true;
|
|
739
|
+
details:
|
|
740
|
+
| CompletionMetadata
|
|
741
|
+
| {
|
|
742
|
+
completionCount: number;
|
|
743
|
+
completions: CompletionMetadata[];
|
|
744
|
+
};
|
|
703
745
|
}
|
|
704
746
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
const end = text.indexOf(" -->", valueStart);
|
|
711
|
-
return end < 0 ? undefined : text.slice(valueStart, end);
|
|
747
|
+
type CompletionContext = Pick<ExtensionContext, "hasPendingMessages" | "isIdle">;
|
|
748
|
+
type CompletionPi = Pick<ExtensionAPI, "sendMessage">;
|
|
749
|
+
|
|
750
|
+
export interface CompletionDeliveryBrokerOptions {
|
|
751
|
+
onDeliveryError?: (error: unknown) => void;
|
|
712
752
|
}
|
|
713
753
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
754
|
+
/**
|
|
755
|
+
* Coalesces detached completions so one bounded notification batch starts at
|
|
756
|
+
* most one root synthesis turn. The broker belongs to one parent session and
|
|
757
|
+
* must be closed when that session is replaced or shut down.
|
|
758
|
+
*/
|
|
759
|
+
export class CompletionDeliveryBroker {
|
|
760
|
+
private pending: AgentTurnCompletion[] = [];
|
|
761
|
+
private flushTimer?: NodeJS.Timeout;
|
|
762
|
+
private wakeInFlight = false;
|
|
763
|
+
private closed = false;
|
|
764
|
+
|
|
765
|
+
constructor(
|
|
766
|
+
private readonly pi: CompletionPi,
|
|
767
|
+
private readonly ctx: CompletionContext,
|
|
768
|
+
private delivery: CompletionDelivery,
|
|
769
|
+
private readonly options: CompletionDeliveryBrokerOptions = {},
|
|
770
|
+
) {}
|
|
771
|
+
|
|
772
|
+
enqueue(completion: AgentTurnCompletion): void {
|
|
773
|
+
if (this.closed) return;
|
|
774
|
+
this.pending.push(completion);
|
|
775
|
+
this.scheduleFlush();
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
setDelivery(value: CompletionDelivery): void {
|
|
779
|
+
this.delivery = value;
|
|
780
|
+
this.scheduleFlush();
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
onParentTurnStart(): void {
|
|
784
|
+
this.wakeInFlight = false;
|
|
785
|
+
this.scheduleFlush();
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
onParentSettled(): void {
|
|
789
|
+
this.wakeInFlight = false;
|
|
790
|
+
this.scheduleFlush();
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
flush(): void {
|
|
794
|
+
if (this.closed || this.pending.length === 0) return;
|
|
795
|
+
if (this.flushTimer) clearTimeout(this.flushTimer);
|
|
796
|
+
this.flushTimer = undefined;
|
|
797
|
+
if (this.delivery === "auto-resume" && !this.isRootIdle()) return;
|
|
798
|
+
|
|
799
|
+
const completions = this.pending.splice(0);
|
|
800
|
+
const batches = chunkCompletions(completions);
|
|
801
|
+
let canWake = this.shouldWakeRoot();
|
|
802
|
+
for (let index = 0; index < batches.length; index++) {
|
|
803
|
+
const triggerTurn = canWake && index === batches.length - 1;
|
|
804
|
+
const message = buildCompletionMessage(batches[index]);
|
|
805
|
+
if (triggerTurn) this.wakeInFlight = true;
|
|
806
|
+
try {
|
|
807
|
+
this.pi.sendMessage(message, { deliverAs: "steer", triggerTurn });
|
|
808
|
+
} catch (primaryError) {
|
|
809
|
+
if (triggerTurn) this.wakeInFlight = false;
|
|
810
|
+
canWake = false;
|
|
811
|
+
try {
|
|
812
|
+
this.pi.sendMessage(message, { deliverAs: "nextTurn", triggerTurn: false });
|
|
813
|
+
} catch (fallbackError) {
|
|
814
|
+
this.pending = [...batches.slice(index).flat(), ...this.pending];
|
|
815
|
+
try {
|
|
816
|
+
this.options.onDeliveryError?.(
|
|
817
|
+
new AggregateError(
|
|
818
|
+
[primaryError, fallbackError],
|
|
819
|
+
"Detached subagent completion delivery failed",
|
|
820
|
+
),
|
|
821
|
+
);
|
|
822
|
+
} catch {
|
|
823
|
+
// Delivery retention must survive a failing observer.
|
|
824
|
+
}
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
725
828
|
}
|
|
726
829
|
}
|
|
727
|
-
}
|
|
728
830
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
831
|
+
close(): void {
|
|
832
|
+
this.closed = true;
|
|
833
|
+
if (this.flushTimer) clearTimeout(this.flushTimer);
|
|
834
|
+
this.flushTimer = undefined;
|
|
835
|
+
this.pending = [];
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
private scheduleFlush(): void {
|
|
839
|
+
if (this.closed || this.pending.length === 0 || this.flushTimer) return;
|
|
840
|
+
this.flushTimer = setTimeout(() => {
|
|
841
|
+
this.flushTimer = undefined;
|
|
842
|
+
this.flush();
|
|
843
|
+
}, COMPLETION_BATCH_DELAY_MS);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
private isRootIdle(): boolean {
|
|
847
|
+
try {
|
|
848
|
+
return this.ctx.isIdle();
|
|
849
|
+
} catch {
|
|
850
|
+
return false;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
private shouldWakeRoot(): boolean {
|
|
855
|
+
if (this.delivery !== "auto-resume" || this.wakeInFlight) return false;
|
|
856
|
+
try {
|
|
857
|
+
return !this.ctx.hasPendingMessages();
|
|
858
|
+
} catch {
|
|
859
|
+
return false;
|
|
745
860
|
}
|
|
746
|
-
return false;
|
|
747
861
|
}
|
|
748
862
|
}
|
|
749
863
|
|
|
750
|
-
function
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
return true;
|
|
864
|
+
function chunkCompletions(completions: AgentTurnCompletion[]): AgentTurnCompletion[][] {
|
|
865
|
+
const batches: AgentTurnCompletion[][] = [];
|
|
866
|
+
for (let index = 0; index < completions.length; index += MAX_COMPLETIONS_PER_MESSAGE) {
|
|
867
|
+
batches.push(completions.slice(index, index + MAX_COMPLETIONS_PER_MESSAGE));
|
|
755
868
|
}
|
|
869
|
+
return batches;
|
|
756
870
|
}
|
|
757
871
|
|
|
758
|
-
function
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
const content = buildDetachedCompletionMessage(completion);
|
|
763
|
-
pi.sendMessage(
|
|
764
|
-
{
|
|
872
|
+
function buildCompletionMessage(completions: AgentTurnCompletion[]): CompletionMessage {
|
|
873
|
+
if (completions.length === 1) {
|
|
874
|
+
const completion = completions[0];
|
|
875
|
+
return {
|
|
765
876
|
customType: "pi-subagent-completion",
|
|
766
|
-
content,
|
|
877
|
+
content: buildDetachedCompletionMessage(completion),
|
|
767
878
|
display: true,
|
|
768
|
-
details:
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
879
|
+
details: completionMetadata(completion),
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
const content = truncateUtf8(
|
|
883
|
+
[
|
|
884
|
+
"Message Type: SUBAGENT_COMPLETION_BATCH",
|
|
885
|
+
`Completion Count: ${completions.length}`,
|
|
886
|
+
...completions.flatMap((completion, index) => [
|
|
887
|
+
"",
|
|
888
|
+
`--- Completion ${index + 1} of ${completions.length} ---`,
|
|
889
|
+
buildDetachedCompletionMessage(completion),
|
|
890
|
+
]),
|
|
891
|
+
].join("\n"),
|
|
892
|
+
DEFAULT_MAX_CONTEXT_BYTES,
|
|
893
|
+
).text;
|
|
894
|
+
return {
|
|
895
|
+
customType: "pi-subagent-completion",
|
|
896
|
+
content,
|
|
897
|
+
display: true,
|
|
898
|
+
details: {
|
|
899
|
+
completionCount: completions.length,
|
|
900
|
+
completions: completions.map(completionMetadata),
|
|
773
901
|
},
|
|
774
|
-
|
|
775
|
-
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function completionMetadata(completion: AgentTurnCompletion): CompletionMetadata {
|
|
906
|
+
return {
|
|
907
|
+
agentId: completion.agent.id,
|
|
908
|
+
agent: completion.agent.agent,
|
|
909
|
+
state: completion.agent.state,
|
|
910
|
+
};
|
|
776
911
|
}
|
|
777
912
|
|
|
778
913
|
export function buildDetachedCompletionMessage(completion: AgentTurnCompletion): string {
|
|
@@ -798,10 +933,13 @@ export function buildDetachedCompletionMessage(completion: AgentTurnCompletion):
|
|
|
798
933
|
}
|
|
799
934
|
|
|
800
935
|
function sanitizeCompletionLine(value: string, maxBytes: number): string {
|
|
801
|
-
return
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
936
|
+
return (
|
|
937
|
+
truncateUtf8(redactPrivateText(value), maxBytes)
|
|
938
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: Strip untrusted terminal controls.
|
|
939
|
+
.text.replace(/[\u0000-\u001f\u007f]+/g, " ")
|
|
940
|
+
.replace(/\s+/g, " ")
|
|
941
|
+
.trim()
|
|
942
|
+
);
|
|
805
943
|
}
|
|
806
944
|
|
|
807
945
|
async function cleanupClosedWorkspaces(
|
|
@@ -829,6 +967,12 @@ export function resolveStatefulTransportKind(
|
|
|
829
967
|
return value ?? "subprocess";
|
|
830
968
|
}
|
|
831
969
|
|
|
970
|
+
export function resolveCompletionDelivery(
|
|
971
|
+
value: CompletionDelivery | undefined,
|
|
972
|
+
): CompletionDelivery {
|
|
973
|
+
return value ?? "next-turn";
|
|
974
|
+
}
|
|
975
|
+
|
|
832
976
|
function normalizeRuntimeThinkingLevel(value: string): ParentRuntimeSnapshot["thinkingLevel"] {
|
|
833
977
|
return isThinkingLevel(value) ? value : "off";
|
|
834
978
|
}
|