@oai404iao/pi-subagent 0.2.0 → 0.4.0-alpha.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 +266 -75
- package/agents/worker.md +1 -1
- package/config.example.json +4 -1
- package/config.schema.json +29 -4
- package/package.json +9 -6
- package/src/agent-state.ts +125 -0
- package/src/agent-sync.ts +64 -53
- package/src/agents.ts +3 -22
- package/src/catalog.ts +59 -7
- package/src/completion-mailbox.ts +656 -0
- package/src/config.ts +57 -6
- package/src/coordinator.ts +2476 -205
- package/src/descriptor.ts +118 -12
- package/src/index.ts +177 -27
- package/src/mailbox.ts +451 -0
- package/src/providers.ts +223 -28
- package/src/render.ts +25 -8
- package/src/scheduler.ts +173 -0
- package/src/schemas.ts +114 -11
- package/src/task-path.ts +188 -0
- package/src/types.ts +74 -15
package/src/coordinator.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
1
|
import { join, relative, resolve } from "node:path";
|
|
3
2
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
4
|
-
import type
|
|
3
|
+
import { type Model, uuidv7 } from "@earendil-works/pi-ai";
|
|
5
4
|
import {
|
|
6
5
|
type AgentSessionEvent,
|
|
7
6
|
type AgentToolResult,
|
|
8
7
|
type AgentToolUpdateCallback,
|
|
9
8
|
type ExtensionAPI,
|
|
10
9
|
type ExtensionContext,
|
|
10
|
+
type InlineExtension,
|
|
11
11
|
type ModelRegistry,
|
|
12
12
|
type ToolDefinition,
|
|
13
13
|
type CreateAgentSessionRuntimeFactory,
|
|
@@ -20,23 +20,60 @@ import {
|
|
|
20
20
|
ModelRuntime,
|
|
21
21
|
SessionManager,
|
|
22
22
|
} from "@earendil-works/pi-coding-agent";
|
|
23
|
+
import { syncBundledAgents, type AgentSyncResult } from "./agent-sync.ts";
|
|
23
24
|
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
catalogStatus,
|
|
26
|
+
closeAgent,
|
|
27
|
+
createAgentControlState,
|
|
28
|
+
currentAgentTurnId,
|
|
29
|
+
delegationStatus,
|
|
30
|
+
finishAgentTurn,
|
|
31
|
+
interruptAgentTurn,
|
|
32
|
+
queueAgentTurn,
|
|
33
|
+
setAgentResidency,
|
|
34
|
+
startAgentTurn,
|
|
35
|
+
type AgentControlState,
|
|
36
|
+
} from "./agent-state.ts";
|
|
28
37
|
import {
|
|
29
38
|
discoverAgents,
|
|
30
39
|
formatAgentCatalog,
|
|
31
40
|
type AgentDiscoveryResult,
|
|
32
41
|
} from "./agents.ts";
|
|
33
42
|
import { readPersistedCatalog } from "./catalog.ts";
|
|
34
|
-
import { DESCRIPTOR_CUSTOM_TYPE, foldDescriptor } from "./descriptor.ts";
|
|
35
43
|
import {
|
|
44
|
+
MAX_COMPLETIONS_PER_DELIVERY,
|
|
45
|
+
appendCompletionUpdate,
|
|
46
|
+
appendUndeliveredCompletion,
|
|
47
|
+
foldCompletionMailbox,
|
|
48
|
+
readCompletionMailbox,
|
|
49
|
+
releaseCompletionDeliveries,
|
|
50
|
+
reserveCompletionDelivery,
|
|
51
|
+
unreadCompletionCounts,
|
|
52
|
+
type CompletionUpdate,
|
|
53
|
+
} from "./completion-mailbox.ts";
|
|
54
|
+
import {
|
|
55
|
+
DESCRIPTOR_CUSTOM_TYPE,
|
|
56
|
+
DESCRIPTOR_VERSION,
|
|
57
|
+
descriptorContext,
|
|
58
|
+
foldDescriptor,
|
|
59
|
+
} from "./descriptor.ts";
|
|
60
|
+
import {
|
|
61
|
+
claimMailboxMessages,
|
|
62
|
+
commitMailboxClaim,
|
|
63
|
+
enqueueMailboxMessage,
|
|
64
|
+
foldOwnedMailbox,
|
|
65
|
+
formatMailboxBatch,
|
|
66
|
+
readMailbox,
|
|
67
|
+
} from "./mailbox.ts";
|
|
68
|
+
import {
|
|
69
|
+
FollowupTaskParameters,
|
|
36
70
|
InterruptParameters,
|
|
37
71
|
ListAgentsParameters,
|
|
38
72
|
ReportParameters,
|
|
39
73
|
SendMessageParameters,
|
|
74
|
+
WaitAgentParameters,
|
|
75
|
+
DEFAULT_WAIT_AGENT_TIMEOUT_MS,
|
|
76
|
+
MAX_WAIT_AGENT_TIMEOUT_MS,
|
|
40
77
|
delegationParameters,
|
|
41
78
|
forkDelegationParameters,
|
|
42
79
|
} from "./schemas.ts";
|
|
@@ -55,13 +92,30 @@ import {
|
|
|
55
92
|
ProviderRegistry,
|
|
56
93
|
SpawnProvider,
|
|
57
94
|
} from "./providers.ts";
|
|
95
|
+
import {
|
|
96
|
+
AgentOperationQueue,
|
|
97
|
+
BackgroundRunLimiter,
|
|
98
|
+
type BackgroundRunPermit,
|
|
99
|
+
} from "./scheduler.ts";
|
|
58
100
|
import { buildToolCeiling, resolveToolPolicy } from "./tool-policy.ts";
|
|
101
|
+
import {
|
|
102
|
+
ROOT_TASK_PATH,
|
|
103
|
+
descriptorTaskPath,
|
|
104
|
+
isAgentId,
|
|
105
|
+
numberedTaskName,
|
|
106
|
+
resolveTaskPath,
|
|
107
|
+
slugTaskName,
|
|
108
|
+
taskPath,
|
|
109
|
+
validateTaskName,
|
|
110
|
+
} from "./task-path.ts";
|
|
59
111
|
import {
|
|
60
112
|
snapshotAgent,
|
|
61
113
|
type AgentDefinition,
|
|
62
114
|
type CatalogChild,
|
|
115
|
+
type CatalogDiagnostic,
|
|
63
116
|
type CatalogEntry,
|
|
64
117
|
type ControlDetails,
|
|
118
|
+
type ContextInheritance,
|
|
65
119
|
type DelegationDetails,
|
|
66
120
|
type ParentMessageDetails,
|
|
67
121
|
type SubagentDescriptor,
|
|
@@ -75,10 +129,17 @@ import {
|
|
|
75
129
|
|
|
76
130
|
const REPORT_CUSTOM_TYPE = "pi-subagent/report";
|
|
77
131
|
const SETTLED_CUSTOM_TYPE = "pi-subagent/settled";
|
|
132
|
+
const AGENT_CUSTOM_TYPE = "pi-subagent/agent";
|
|
133
|
+
const LINEAGE_CUSTOM_TYPE = "pi-subagent/lineage";
|
|
134
|
+
const AGENT_ID_PATTERN =
|
|
135
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
78
136
|
const MAX_TRACE_ITEMS = 100;
|
|
79
137
|
const MAX_TRACE_TEXT = 4000;
|
|
138
|
+
const MAX_WAIT_AGENT_RESULT_BYTES = 256 * 1024;
|
|
80
139
|
const BACKGROUND_CONTROL_TOOLS = new Set([
|
|
81
140
|
"send_message",
|
|
141
|
+
"followup_task",
|
|
142
|
+
"wait_agent",
|
|
82
143
|
"interrupt_agent",
|
|
83
144
|
"list_agents",
|
|
84
145
|
]);
|
|
@@ -97,17 +158,54 @@ const REPORT_PROMPT = [
|
|
|
97
158
|
|
|
98
159
|
export interface DelegationInput {
|
|
99
160
|
agent: string;
|
|
161
|
+
task_name?: string;
|
|
100
162
|
description: string;
|
|
101
163
|
prompt: string;
|
|
102
164
|
run_in_background?: boolean;
|
|
165
|
+
context?: {
|
|
166
|
+
mode: ContextInheritance["mode"];
|
|
167
|
+
completed_turns?: number;
|
|
168
|
+
};
|
|
103
169
|
}
|
|
104
170
|
|
|
105
171
|
export type DelegationOutcome =
|
|
106
172
|
| { kind: "continuable"; details: DelegationDetails }
|
|
107
173
|
| { kind: "foreground"; details: DelegationDetails; result: SubagentRunResult };
|
|
108
174
|
|
|
175
|
+
export type SendMessageOutcome =
|
|
176
|
+
| { kind: "legacy"; agentId: string; taskPath: string }
|
|
177
|
+
| {
|
|
178
|
+
kind: "mailbox-v2";
|
|
179
|
+
agentId: string;
|
|
180
|
+
taskPath: string;
|
|
181
|
+
messageId: string;
|
|
182
|
+
pendingMessages: number;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export interface FollowupTaskOutcome {
|
|
186
|
+
agentId: string;
|
|
187
|
+
taskPath: string;
|
|
188
|
+
turnId: string;
|
|
189
|
+
claimedMessages: number;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface InterruptOutcome {
|
|
193
|
+
agentId: string;
|
|
194
|
+
taskPath: string;
|
|
195
|
+
active: boolean;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface WaitAgentOutcome {
|
|
199
|
+
timedOut: boolean;
|
|
200
|
+
timeoutMs: number;
|
|
201
|
+
updates: CompletionUpdate[];
|
|
202
|
+
unreadUpdates: number;
|
|
203
|
+
taskPaths: Record<string, string>;
|
|
204
|
+
}
|
|
205
|
+
|
|
109
206
|
interface ParentRef {
|
|
110
|
-
|
|
207
|
+
agentId: string;
|
|
208
|
+
taskPath: string;
|
|
111
209
|
depth: number;
|
|
112
210
|
cwd: string;
|
|
113
211
|
sessionManager: SessionView;
|
|
@@ -125,28 +223,60 @@ interface ParentRef {
|
|
|
125
223
|
}
|
|
126
224
|
|
|
127
225
|
interface Activation {
|
|
128
|
-
|
|
129
|
-
|
|
226
|
+
agentId: string;
|
|
227
|
+
runId: string;
|
|
130
228
|
descriptor: SubagentDescriptor;
|
|
131
229
|
parent: ParentRef;
|
|
132
230
|
runtime: AgentSessionRuntime;
|
|
133
231
|
seedMessageCount: number;
|
|
134
232
|
epochMessageStart: number;
|
|
135
|
-
|
|
233
|
+
controlState: AgentControlState;
|
|
136
234
|
trace: TraceItem[];
|
|
137
235
|
streamedText: string;
|
|
138
236
|
usage: ReturnType<typeof emptyUsage>;
|
|
237
|
+
startedTurnIds: Set<string>;
|
|
238
|
+
silentSettlementTurnIds: Set<string>;
|
|
239
|
+
pendingMailboxClaims: Set<string>;
|
|
240
|
+
userMessageGates: Map<string, UserMessageGate>;
|
|
139
241
|
ownedChildren: Set<string>;
|
|
140
242
|
currentRun?: Promise<SubagentRunResult>;
|
|
243
|
+
currentMessageGate?: Promise<void>;
|
|
141
244
|
pendingSettlement?: SubagentRunResult;
|
|
142
245
|
unsubscribe?: () => void;
|
|
143
246
|
onUpdate?: (details: DelegationDetails) => void;
|
|
144
247
|
published: boolean;
|
|
248
|
+
everPublished: boolean;
|
|
145
249
|
suppressSettlement: boolean;
|
|
146
250
|
finalizing: boolean;
|
|
147
|
-
finalizePromise?: Promise<
|
|
251
|
+
finalizePromise?: Promise<boolean>;
|
|
252
|
+
holdsBackgroundSlot: boolean;
|
|
253
|
+
turnAbortController?: AbortController;
|
|
254
|
+
persistenceGate: PersistenceGate;
|
|
148
255
|
disposed: boolean;
|
|
149
256
|
lastError?: string;
|
|
257
|
+
ownerActivation?: Activation;
|
|
258
|
+
lastUsedSequence: number;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
interface UserMessageGate {
|
|
262
|
+
prepare(): void;
|
|
263
|
+
accept(): void;
|
|
264
|
+
reject(error: Error): void;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
interface PersistenceGate {
|
|
268
|
+
promise: Promise<void>;
|
|
269
|
+
resolve(): void;
|
|
270
|
+
reject(error: Error): void;
|
|
271
|
+
readonly settled: boolean;
|
|
272
|
+
readonly state: "pending" | "fulfilled" | "rejected";
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
interface CompletionWaiter {
|
|
276
|
+
promise: Promise<"activity" | "timeout">;
|
|
277
|
+
wake(): void;
|
|
278
|
+
reject(error: Error): void;
|
|
279
|
+
dispose(): void;
|
|
150
280
|
}
|
|
151
281
|
|
|
152
282
|
interface CreateActivationOptions {
|
|
@@ -157,16 +287,60 @@ interface CreateActivationOptions {
|
|
|
157
287
|
onUpdate?: (details: DelegationDetails) => void;
|
|
158
288
|
}
|
|
159
289
|
|
|
290
|
+
interface StartPromptOptions {
|
|
291
|
+
detachAtAcceptance: boolean;
|
|
292
|
+
waitForCapacity: boolean;
|
|
293
|
+
preparePrompt?: (turnId: string) => string;
|
|
294
|
+
onPromptAccepted?: (turnId: string) => void;
|
|
295
|
+
acceptAfterUserMessage?: boolean;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
interface PendingPromptStart {
|
|
299
|
+
activation: Activation;
|
|
300
|
+
accepted: Promise<void>;
|
|
301
|
+
coldPrepared?: PreparedChildSession;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
type SerializedSendMessageOutcome =
|
|
305
|
+
| {
|
|
306
|
+
kind: "legacy";
|
|
307
|
+
agentId: string;
|
|
308
|
+
taskPath: string;
|
|
309
|
+
pending?: PendingPromptStart;
|
|
310
|
+
}
|
|
311
|
+
| Extract<SendMessageOutcome, { kind: "mailbox-v2" }>;
|
|
312
|
+
|
|
313
|
+
interface PendingFollowupStart extends PendingPromptStart {
|
|
314
|
+
outcome: FollowupTaskOutcome;
|
|
315
|
+
}
|
|
316
|
+
|
|
160
317
|
interface CatalogRecord {
|
|
161
|
-
|
|
318
|
+
agentId: string;
|
|
319
|
+
piSessionId: string;
|
|
320
|
+
taskPath: string;
|
|
162
321
|
descriptor: SubagentDescriptor;
|
|
163
322
|
sessionFile?: string;
|
|
164
323
|
active?: Activation;
|
|
324
|
+
pendingMessages: number;
|
|
325
|
+
unreadUpdatesByChild: Map<string, number>;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
interface ResolvedTarget {
|
|
329
|
+
agentId: string;
|
|
330
|
+
taskPath: string;
|
|
331
|
+
record?: CatalogRecord;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
interface ReservedTask {
|
|
335
|
+
name: string;
|
|
336
|
+
path: string;
|
|
337
|
+
release(): void;
|
|
165
338
|
}
|
|
166
339
|
|
|
167
340
|
interface CoordinatorCatalog {
|
|
168
341
|
records: CatalogRecord[];
|
|
169
342
|
diagnostics: CatalogEntry[];
|
|
343
|
+
rootUnreadUpdatesByChild: Map<string, number>;
|
|
170
344
|
}
|
|
171
345
|
|
|
172
346
|
function runtimeFromRegistry(registry: ModelRegistry): ModelRuntime {
|
|
@@ -191,6 +365,114 @@ function errorText(error: unknown): string {
|
|
|
191
365
|
return error instanceof Error ? error.message : String(error);
|
|
192
366
|
}
|
|
193
367
|
|
|
368
|
+
function abortReason(signal: AbortSignal): Error {
|
|
369
|
+
return signal.reason instanceof Error
|
|
370
|
+
? signal.reason
|
|
371
|
+
: new Error(signal.reason ? String(signal.reason) : "operation aborted");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function waitForPromise<T>(
|
|
375
|
+
promise: Promise<T>,
|
|
376
|
+
signal?: AbortSignal,
|
|
377
|
+
): Promise<T> {
|
|
378
|
+
if (!signal) return promise;
|
|
379
|
+
if (signal.aborted) return Promise.reject(abortReason(signal));
|
|
380
|
+
return new Promise<T>((resolvePromise, rejectPromise) => {
|
|
381
|
+
const abort = () => {
|
|
382
|
+
rejectPromise(abortReason(signal));
|
|
383
|
+
};
|
|
384
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
385
|
+
promise.then(
|
|
386
|
+
(value) => {
|
|
387
|
+
signal.removeEventListener("abort", abort);
|
|
388
|
+
resolvePromise(value);
|
|
389
|
+
},
|
|
390
|
+
(error) => {
|
|
391
|
+
signal.removeEventListener("abort", abort);
|
|
392
|
+
rejectPromise(error);
|
|
393
|
+
},
|
|
394
|
+
);
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function createPersistenceGate(
|
|
399
|
+
session: Pick<SessionView, "getEntries">,
|
|
400
|
+
requireNewAssistant: boolean,
|
|
401
|
+
): PersistenceGate {
|
|
402
|
+
const alreadyDurable = !requireNewAssistant && session
|
|
403
|
+
.getEntries()
|
|
404
|
+
.some(
|
|
405
|
+
(entry) =>
|
|
406
|
+
entry.type === "message"
|
|
407
|
+
&& entry.message.role === "assistant",
|
|
408
|
+
);
|
|
409
|
+
let state: PersistenceGate["state"] =
|
|
410
|
+
alreadyDurable ? "fulfilled" : "pending";
|
|
411
|
+
let resolvePromise!: () => void;
|
|
412
|
+
let rejectPromise!: (error: Error) => void;
|
|
413
|
+
const promise = alreadyDurable
|
|
414
|
+
? Promise.resolve()
|
|
415
|
+
: new Promise<void>((resolve, reject) => {
|
|
416
|
+
resolvePromise = resolve;
|
|
417
|
+
rejectPromise = reject;
|
|
418
|
+
});
|
|
419
|
+
void promise.catch(() => {});
|
|
420
|
+
return {
|
|
421
|
+
promise,
|
|
422
|
+
resolve: () => {
|
|
423
|
+
if (state !== "pending") return;
|
|
424
|
+
state = "fulfilled";
|
|
425
|
+
resolvePromise();
|
|
426
|
+
},
|
|
427
|
+
reject: (error) => {
|
|
428
|
+
if (state !== "pending") return;
|
|
429
|
+
state = "rejected";
|
|
430
|
+
rejectPromise(error);
|
|
431
|
+
},
|
|
432
|
+
get settled() {
|
|
433
|
+
return state !== "pending";
|
|
434
|
+
},
|
|
435
|
+
get state() {
|
|
436
|
+
return state;
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Read the durable pi-subagent control id recorded for a session, if any.
|
|
443
|
+
*
|
|
444
|
+
* The latest entry wins because a copied/forked Pi session may contain older
|
|
445
|
+
* identity checkpoints.
|
|
446
|
+
*/
|
|
447
|
+
function readAgentId(session: Pick<SessionView, "getEntries">): string | undefined {
|
|
448
|
+
const entries = session.getEntries();
|
|
449
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
450
|
+
const entry = entries[index];
|
|
451
|
+
if (!entry) continue;
|
|
452
|
+
if (entry.type !== "custom" || entry.customType !== AGENT_CUSTOM_TYPE) continue;
|
|
453
|
+
const data = entry.data as { agentId?: unknown } | undefined;
|
|
454
|
+
if (
|
|
455
|
+
typeof data?.agentId === "string"
|
|
456
|
+
&& AGENT_ID_PATTERN.test(data.agentId)
|
|
457
|
+
) {
|
|
458
|
+
return data.agentId;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return undefined;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Resolve the provider-neutral control id for a session, creating it on first
|
|
466
|
+
* use. In-memory SessionManager instances retain custom entries too.
|
|
467
|
+
*/
|
|
468
|
+
function ensureAgentId(session: SessionView): string {
|
|
469
|
+
const existing = readAgentId(session);
|
|
470
|
+
if (existing) return existing;
|
|
471
|
+
const agentId = uuidv7();
|
|
472
|
+
session.appendCustomEntry(AGENT_CUSTOM_TYPE, { agentId });
|
|
473
|
+
return agentId;
|
|
474
|
+
}
|
|
475
|
+
|
|
194
476
|
function stopReasonHeadline(reason: SubagentStopReason): string {
|
|
195
477
|
switch (reason) {
|
|
196
478
|
case "completed":
|
|
@@ -207,26 +489,135 @@ function stopReasonHeadline(reason: SubagentStopReason): string {
|
|
|
207
489
|
function makeRuntimeSettings(descriptor: SubagentDescriptor): SubagentSettings {
|
|
208
490
|
return {
|
|
209
491
|
agentScope: descriptor.runtime.agentScope,
|
|
210
|
-
syncBundledAgents: descriptor.runtime.syncBundledAgents,
|
|
211
492
|
maxDepth: descriptor.runtime.maxDepth,
|
|
212
493
|
enableRunInBackground: descriptor.runtime.enableRunInBackground,
|
|
213
494
|
defaultBackground: descriptor.runtime.defaultBackground,
|
|
495
|
+
maxConcurrentBackgroundRuns: descriptor.runtime.maxConcurrentBackgroundRuns,
|
|
496
|
+
maxIdleRuntimes: descriptor.runtime.maxIdleRuntimes,
|
|
497
|
+
backgroundProtocol: descriptor.runtime.backgroundProtocol,
|
|
214
498
|
reportDelivery: descriptor.runtime.reportDelivery,
|
|
215
499
|
inheritExtensions: descriptor.runtime.inheritExtensions,
|
|
500
|
+
openAIIdentity: descriptor.runtime.openAIIdentity,
|
|
216
501
|
maxOutputBytes: descriptor.runtime.maxOutputBytes,
|
|
217
502
|
};
|
|
218
503
|
}
|
|
219
504
|
|
|
505
|
+
function delegationContext(
|
|
506
|
+
providerName: SubagentProviderName,
|
|
507
|
+
input: DelegationInput,
|
|
508
|
+
): ContextInheritance {
|
|
509
|
+
if (providerName === "fork") {
|
|
510
|
+
if (input.context !== undefined) {
|
|
511
|
+
throw new Error(
|
|
512
|
+
"subagent_fork always uses all_completed context; use subagent for another context policy",
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
return { mode: "all_completed" };
|
|
516
|
+
}
|
|
517
|
+
const context = input.context;
|
|
518
|
+
if (!context) return { mode: "fresh" };
|
|
519
|
+
if (context.mode === "last_n_completed") {
|
|
520
|
+
if (
|
|
521
|
+
!Number.isSafeInteger(context.completed_turns)
|
|
522
|
+
|| context.completed_turns === undefined
|
|
523
|
+
|| context.completed_turns < 1
|
|
524
|
+
|| context.completed_turns > 100
|
|
525
|
+
) {
|
|
526
|
+
throw new Error(
|
|
527
|
+
"context.completed_turns must be an integer between 1 and 100 for last_n_completed",
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
return {
|
|
531
|
+
mode: context.mode,
|
|
532
|
+
completedTurns: context.completed_turns,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
if (
|
|
536
|
+
context.mode !== "fresh"
|
|
537
|
+
&& context.mode !== "all_completed"
|
|
538
|
+
) {
|
|
539
|
+
throw new Error(`unsupported context mode: ${String(context.mode)}`);
|
|
540
|
+
}
|
|
541
|
+
if (context.completed_turns !== undefined) {
|
|
542
|
+
throw new Error(
|
|
543
|
+
"context.completed_turns is available only for last_n_completed",
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
return { mode: context.mode };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function mailboxOwner(descriptor: SubagentDescriptor): {
|
|
550
|
+
parentAgentId: string;
|
|
551
|
+
agentId: string;
|
|
552
|
+
} {
|
|
553
|
+
return {
|
|
554
|
+
parentAgentId: descriptor.parentAgentId,
|
|
555
|
+
agentId: descriptor.agentId,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function completionWaiterKey(parent: ParentRef): string {
|
|
560
|
+
return `${parent.agentId}:${parent.sessionManager.getSessionId()}`;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function waitTimeout(value: number | undefined): number {
|
|
564
|
+
const timeoutMs = value ?? DEFAULT_WAIT_AGENT_TIMEOUT_MS;
|
|
565
|
+
if (
|
|
566
|
+
!Number.isSafeInteger(timeoutMs)
|
|
567
|
+
|| timeoutMs < 0
|
|
568
|
+
|| timeoutMs > MAX_WAIT_AGENT_TIMEOUT_MS
|
|
569
|
+
) {
|
|
570
|
+
throw new Error(
|
|
571
|
+
`timeout_ms must be a safe integer between 0 and ${MAX_WAIT_AGENT_TIMEOUT_MS}`,
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
return timeoutMs;
|
|
575
|
+
}
|
|
576
|
+
|
|
220
577
|
function isPathInside(parent: string, child: string): boolean {
|
|
221
578
|
const rel = relative(resolve(parent), resolve(child));
|
|
222
579
|
return rel === "" || (!rel.startsWith("..") && !rel.startsWith("/"));
|
|
223
580
|
}
|
|
224
581
|
|
|
582
|
+
function isOpenAIResponsesModel(model: Model<any>): boolean {
|
|
583
|
+
return model.api === "openai-responses"
|
|
584
|
+
|| model.api === "openai-codex-responses";
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async function loadCodexIdentityInlineExtension(
|
|
588
|
+
parentSessionManager: SessionView,
|
|
589
|
+
): Promise<InlineExtension> {
|
|
590
|
+
try {
|
|
591
|
+
const integration = await import(
|
|
592
|
+
"@oai404iao/pi-codex-minimal-tools/subagent-inline"
|
|
593
|
+
);
|
|
594
|
+
return integration.createCodexSubagentInlineExtension({
|
|
595
|
+
parentSessionManager,
|
|
596
|
+
});
|
|
597
|
+
} catch (error) {
|
|
598
|
+
throw new Error(
|
|
599
|
+
"openAIIdentity requires @oai404iao/pi-codex-minimal-tools with its subagent-inline export",
|
|
600
|
+
{ cause: error },
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
225
605
|
export class SubagentCoordinator {
|
|
226
606
|
private readonly providers = new ProviderRegistry();
|
|
227
607
|
private readonly active = new Map<string, Activation>();
|
|
608
|
+
private readonly agentOperations = new AgentOperationQueue();
|
|
609
|
+
private readonly completionOperations = new AgentOperationQueue();
|
|
610
|
+
private readonly idleRuntimeOperations = new AgentOperationQueue();
|
|
611
|
+
private readonly backgroundRuns = new BackgroundRunLimiter();
|
|
612
|
+
private readonly admittedOperations = new Set<Promise<unknown>>();
|
|
613
|
+
private readonly completionWaiters = new Map<string, CompletionWaiter>();
|
|
614
|
+
private readonly reservedTaskPaths = new Set<string>();
|
|
615
|
+
private readonly runtimeId = uuidv7();
|
|
616
|
+
private idleRuntimeLimit = 0;
|
|
617
|
+
private activationSequence = 0;
|
|
228
618
|
private agentSyncResult: AgentSyncResult | undefined;
|
|
229
619
|
private draining = false;
|
|
620
|
+
private shutdownPromise: Promise<void> | undefined;
|
|
230
621
|
|
|
231
622
|
constructor(
|
|
232
623
|
private readonly pi: ExtensionAPI,
|
|
@@ -251,26 +642,39 @@ export class SubagentCoordinator {
|
|
|
251
642
|
return join(this.agentDir, "agents");
|
|
252
643
|
}
|
|
253
644
|
|
|
645
|
+
configureBackgroundRuns(limit: number): void {
|
|
646
|
+
this.backgroundRuns.configure(limit);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async configureIdleRuntimes(limit: number): Promise<void> {
|
|
650
|
+
if (!Number.isSafeInteger(limit) || limit < 0) {
|
|
651
|
+
throw new Error("maxIdleRuntimes must be a non-negative safe integer");
|
|
652
|
+
}
|
|
653
|
+
this.idleRuntimeLimit = limit;
|
|
654
|
+
await this.trimIdleRuntimes();
|
|
655
|
+
}
|
|
656
|
+
|
|
254
657
|
discoverAvailableAgents(
|
|
255
658
|
cwd: string,
|
|
256
659
|
settings: SubagentSettings,
|
|
257
660
|
projectTrusted: boolean,
|
|
258
661
|
): AgentDiscoveryResult {
|
|
259
|
-
if (
|
|
662
|
+
if (!this.agentSyncResult) {
|
|
260
663
|
this.synchronizeBundledAgents();
|
|
261
664
|
}
|
|
262
|
-
const
|
|
263
|
-
? undefined
|
|
264
|
-
: unmodifiedManagedAgentNames(this.agentDir);
|
|
265
|
-
return discoverAgents({
|
|
665
|
+
const discovery = discoverAgents({
|
|
266
666
|
cwd,
|
|
267
667
|
scope: settings.agentScope,
|
|
268
668
|
projectTrusted,
|
|
269
|
-
bundledDir: this.bundledAgentsDir,
|
|
270
669
|
agentDir: this.agentDir,
|
|
271
|
-
includeBundled: !settings.syncBundledAgents && settings.agentScope !== "project",
|
|
272
|
-
excludeUserAgentNames,
|
|
273
670
|
});
|
|
671
|
+
return {
|
|
672
|
+
...discovery,
|
|
673
|
+
diagnostics: [
|
|
674
|
+
...(this.agentSyncResult?.diagnostics ?? []),
|
|
675
|
+
...discovery.diagnostics,
|
|
676
|
+
],
|
|
677
|
+
};
|
|
274
678
|
}
|
|
275
679
|
|
|
276
680
|
async parentFromContext(ctx: ExtensionContext): Promise<ParentRef> {
|
|
@@ -278,10 +682,18 @@ export class SubagentCoordinator {
|
|
|
278
682
|
const descriptor = folded.kind === "valid" ? folded.descriptor : undefined;
|
|
279
683
|
const modelRuntime = runtimeFromRegistry(ctx.modelRegistry);
|
|
280
684
|
return {
|
|
281
|
-
|
|
685
|
+
agentId: readAgentId(ctx.sessionManager) ?? ensureAgentId(
|
|
686
|
+
ctx.sessionManager as unknown as SessionView,
|
|
687
|
+
),
|
|
688
|
+
taskPath: descriptor
|
|
689
|
+
? descriptorTaskPath(descriptor)
|
|
690
|
+
: ROOT_TASK_PATH,
|
|
282
691
|
depth: descriptor?.depth ?? 0,
|
|
283
692
|
cwd: ctx.cwd,
|
|
284
|
-
|
|
693
|
+
// ExtensionContext narrows the live SessionManager to a read-only
|
|
694
|
+
// view; appending the agent entry through the same instance keeps
|
|
695
|
+
// the in-memory tree and the session file consistent.
|
|
696
|
+
sessionManager: ctx.sessionManager as unknown as SessionView,
|
|
285
697
|
modelRuntime,
|
|
286
698
|
model: ctx.model,
|
|
287
699
|
thinkingLevel: ctx.thinkingLevel ?? "off",
|
|
@@ -306,11 +718,35 @@ export class SubagentCoordinator {
|
|
|
306
718
|
signal?: AbortSignal,
|
|
307
719
|
onUpdate?: (details: DelegationDetails) => void,
|
|
308
720
|
agentDiscovery?: AgentDiscoveryResult,
|
|
721
|
+
): Promise<DelegationOutcome> {
|
|
722
|
+
return this.runAdmittedOperation(() =>
|
|
723
|
+
this.delegateAdmitted(
|
|
724
|
+
parent,
|
|
725
|
+
providerName,
|
|
726
|
+
input,
|
|
727
|
+
settings,
|
|
728
|
+
signal,
|
|
729
|
+
onUpdate,
|
|
730
|
+
agentDiscovery,
|
|
731
|
+
),
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
private async delegateAdmitted(
|
|
736
|
+
parent: ParentRef,
|
|
737
|
+
providerName: SubagentProviderName,
|
|
738
|
+
input: DelegationInput,
|
|
739
|
+
settings: SubagentSettings,
|
|
740
|
+
signal?: AbortSignal,
|
|
741
|
+
onUpdate?: (details: DelegationDetails) => void,
|
|
742
|
+
agentDiscovery?: AgentDiscoveryResult,
|
|
309
743
|
): Promise<DelegationOutcome> {
|
|
310
744
|
if (this.draining) throw new Error("pi-subagent is shutting down; no new delegation was accepted");
|
|
311
|
-
const
|
|
745
|
+
const context = delegationContext(providerName, input);
|
|
746
|
+
const resolvedProviderName: SubagentProviderName =
|
|
747
|
+
context.mode === "fresh" ? "spawn" : "fork";
|
|
748
|
+
const provider = this.providers.get(resolvedProviderName);
|
|
312
749
|
if (
|
|
313
|
-
providerName === "spawn" &&
|
|
314
750
|
!settings.enableRunInBackground &&
|
|
315
751
|
input.run_in_background === true
|
|
316
752
|
) {
|
|
@@ -319,8 +755,10 @@ export class SubagentCoordinator {
|
|
|
319
755
|
);
|
|
320
756
|
}
|
|
321
757
|
const runInBackground =
|
|
322
|
-
|
|
323
|
-
?
|
|
758
|
+
settings.enableRunInBackground
|
|
759
|
+
? providerName === "fork"
|
|
760
|
+
? (input.run_in_background ?? false)
|
|
761
|
+
: (input.run_in_background ?? settings.defaultBackground)
|
|
324
762
|
: false;
|
|
325
763
|
const mode: SubagentMode = runInBackground ? "continuable" : "one-shot";
|
|
326
764
|
if (mode === "continuable" && !provider.supportsContinuable) {
|
|
@@ -361,36 +799,74 @@ export class SubagentCoordinator {
|
|
|
361
799
|
|
|
362
800
|
const model = this.resolveModel(parent, agent);
|
|
363
801
|
const thinkingLevel = agent.thinking ?? parent.thinkingLevel;
|
|
364
|
-
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
...(parent.sessionManager.getSessionFile()
|
|
372
|
-
? { parentSessionFile: parent.sessionManager.getSessionFile() }
|
|
373
|
-
: {}),
|
|
374
|
-
depth,
|
|
375
|
-
cwd: parent.cwd,
|
|
376
|
-
createdAt: new Date().toISOString(),
|
|
377
|
-
agent: snapshotAgent(agent),
|
|
378
|
-
model: { provider: model.provider, id: model.id },
|
|
379
|
-
thinkingLevel,
|
|
380
|
-
runtime: {
|
|
381
|
-
agentScope: settings.agentScope,
|
|
382
|
-
syncBundledAgents: settings.syncBundledAgents,
|
|
383
|
-
maxDepth: settings.maxDepth,
|
|
384
|
-
enableRunInBackground: settings.enableRunInBackground,
|
|
385
|
-
defaultBackground: settings.defaultBackground,
|
|
386
|
-
reportDelivery: settings.reportDelivery,
|
|
387
|
-
inheritExtensions: settings.inheritExtensions,
|
|
388
|
-
maxOutputBytes: settings.maxOutputBytes,
|
|
389
|
-
},
|
|
390
|
-
};
|
|
391
|
-
|
|
802
|
+
parent.agentId = ensureAgentId(parent.sessionManager);
|
|
803
|
+
const reservedTask = await this.reserveTask(
|
|
804
|
+
parent,
|
|
805
|
+
input.task_name,
|
|
806
|
+
input.description,
|
|
807
|
+
);
|
|
808
|
+
let prepared: PreparedChildSession | undefined;
|
|
392
809
|
let activation: Activation | undefined;
|
|
810
|
+
let keepTaskReservation = false;
|
|
393
811
|
try {
|
|
812
|
+
prepared = await provider.prepare(parent, mode, context);
|
|
813
|
+
if (this.draining) {
|
|
814
|
+
throw new Error("pi-subagent is shutting down; no new delegation was accepted");
|
|
815
|
+
}
|
|
816
|
+
const openAIIdentityEnabled =
|
|
817
|
+
settings.openAIIdentity && isOpenAIResponsesModel(model);
|
|
818
|
+
const descriptor: SubagentDescriptor = {
|
|
819
|
+
version: DESCRIPTOR_VERSION,
|
|
820
|
+
mode,
|
|
821
|
+
provider: resolvedProviderName,
|
|
822
|
+
label: input.description.trim(),
|
|
823
|
+
agentId: uuidv7(),
|
|
824
|
+
parentAgentId: parent.agentId,
|
|
825
|
+
parentPiSessionId: parent.sessionManager.getSessionId(),
|
|
826
|
+
...(parent.sessionManager.getSessionFile()
|
|
827
|
+
? { parentSessionFile: parent.sessionManager.getSessionFile() }
|
|
828
|
+
: {}),
|
|
829
|
+
depth,
|
|
830
|
+
cwd: parent.cwd,
|
|
831
|
+
createdAt: new Date().toISOString(),
|
|
832
|
+
agent: snapshotAgent(agent),
|
|
833
|
+
model: { provider: model.provider, id: model.id },
|
|
834
|
+
thinkingLevel,
|
|
835
|
+
task: {
|
|
836
|
+
name: reservedTask.name,
|
|
837
|
+
path: reservedTask.path,
|
|
838
|
+
},
|
|
839
|
+
context,
|
|
840
|
+
runtime: {
|
|
841
|
+
agentScope: settings.agentScope,
|
|
842
|
+
maxDepth: settings.maxDepth,
|
|
843
|
+
enableRunInBackground: settings.enableRunInBackground,
|
|
844
|
+
defaultBackground: settings.defaultBackground,
|
|
845
|
+
maxConcurrentBackgroundRuns: settings.maxConcurrentBackgroundRuns,
|
|
846
|
+
maxIdleRuntimes: settings.maxIdleRuntimes,
|
|
847
|
+
backgroundProtocol: settings.backgroundProtocol ?? "legacy",
|
|
848
|
+
reportDelivery: settings.reportDelivery,
|
|
849
|
+
inheritExtensions: settings.inheritExtensions,
|
|
850
|
+
openAIIdentity: openAIIdentityEnabled,
|
|
851
|
+
maxOutputBytes: settings.maxOutputBytes,
|
|
852
|
+
},
|
|
853
|
+
};
|
|
854
|
+
prepared.sessionManager.appendCustomEntry(AGENT_CUSTOM_TYPE, {
|
|
855
|
+
agentId: descriptor.agentId,
|
|
856
|
+
});
|
|
857
|
+
prepared.sessionManager.appendCustomEntry(LINEAGE_CUSTOM_TYPE, {
|
|
858
|
+
version: 1,
|
|
859
|
+
agentId: descriptor.agentId,
|
|
860
|
+
parentAgentId: descriptor.parentAgentId,
|
|
861
|
+
parentPiSessionId: descriptor.parentPiSessionId,
|
|
862
|
+
relation: descriptor.provider,
|
|
863
|
+
agentName: descriptor.agent.name,
|
|
864
|
+
taskPath: descriptor.task.path,
|
|
865
|
+
openAIIdentity: openAIIdentityEnabled,
|
|
866
|
+
...(descriptor.parentSessionFile
|
|
867
|
+
? { parentSessionFile: descriptor.parentSessionFile }
|
|
868
|
+
: {}),
|
|
869
|
+
});
|
|
394
870
|
activation = await this.createActivation({
|
|
395
871
|
parent,
|
|
396
872
|
descriptor,
|
|
@@ -398,11 +874,18 @@ export class SubagentCoordinator {
|
|
|
398
874
|
isNew: true,
|
|
399
875
|
onUpdate,
|
|
400
876
|
});
|
|
877
|
+
if (this.draining) {
|
|
878
|
+
throw new Error("pi-subagent is shutting down; no new delegation was accepted");
|
|
879
|
+
}
|
|
401
880
|
if (mode === "continuable" && parent.activation) {
|
|
402
|
-
|
|
881
|
+
this.acquireParentOwnership(activation, parent);
|
|
403
882
|
}
|
|
404
|
-
const started = this.startPrompt(activation, input.prompt, signal,
|
|
883
|
+
const started = this.startPrompt(activation, input.prompt, signal, {
|
|
884
|
+
detachAtAcceptance: mode === "continuable",
|
|
885
|
+
waitForCapacity: !parent.activation?.holdsBackgroundSlot,
|
|
886
|
+
});
|
|
405
887
|
await started.accepted;
|
|
888
|
+
keepTaskReservation = true;
|
|
406
889
|
if (mode === "continuable") {
|
|
407
890
|
activation.onUpdate = undefined;
|
|
408
891
|
return { kind: "continuable", details: this.detailsOf(activation) };
|
|
@@ -414,37 +897,144 @@ export class SubagentCoordinator {
|
|
|
414
897
|
await this.disposeActivation(activation);
|
|
415
898
|
return { kind: "foreground", details, result };
|
|
416
899
|
} catch (error) {
|
|
417
|
-
if (activation && !activation.published) {
|
|
900
|
+
if (activation && prepared && !activation.published) {
|
|
418
901
|
activation.suppressSettlement = true;
|
|
419
902
|
await this.rollbackActivation(activation, prepared);
|
|
420
|
-
} else if (!activation) {
|
|
903
|
+
} else if (!activation && prepared) {
|
|
421
904
|
await prepared.rollback();
|
|
422
905
|
}
|
|
423
906
|
throw error;
|
|
907
|
+
} finally {
|
|
908
|
+
if (!keepTaskReservation) reservedTask.release();
|
|
424
909
|
}
|
|
425
910
|
}
|
|
426
911
|
|
|
427
|
-
async sendMessage(
|
|
912
|
+
async sendMessage(
|
|
913
|
+
parent: ParentRef,
|
|
914
|
+
childId: string,
|
|
915
|
+
message: string,
|
|
916
|
+
signal?: AbortSignal,
|
|
917
|
+
): Promise<void> {
|
|
918
|
+
await this.sendMessageWithOutcome(parent, childId, message, signal);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
async sendMessageWithOutcome(
|
|
922
|
+
parent: ParentRef,
|
|
923
|
+
childId: string,
|
|
924
|
+
message: string,
|
|
925
|
+
signal?: AbortSignal,
|
|
926
|
+
): Promise<SendMessageOutcome> {
|
|
927
|
+
return this.runAdmittedOperation(() =>
|
|
928
|
+
this.sendMessageAdmitted(parent, childId, message, signal),
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
private async sendMessageAdmitted(
|
|
933
|
+
parent: ParentRef,
|
|
934
|
+
childId: string,
|
|
935
|
+
message: string,
|
|
936
|
+
signal?: AbortSignal,
|
|
937
|
+
): Promise<SendMessageOutcome> {
|
|
428
938
|
if (this.draining) throw new Error("pi-subagent is shutting down; message was not delivered");
|
|
939
|
+
const target = await this.resolveTarget(parent, childId);
|
|
940
|
+
const delivery = await this.agentOperations.run(target.agentId, () =>
|
|
941
|
+
this.sendMessageSerialized(parent, target.agentId, message, signal),
|
|
942
|
+
);
|
|
943
|
+
if (delivery.kind === "mailbox-v2") return delivery;
|
|
944
|
+
const pending = delivery.pending;
|
|
945
|
+
if (!pending) return delivery;
|
|
946
|
+
try {
|
|
947
|
+
await pending.accepted;
|
|
948
|
+
} catch (error) {
|
|
949
|
+
if (pending.coldPrepared && !pending.activation.published) {
|
|
950
|
+
await this.agentOperations.run(target.agentId, async () => {
|
|
951
|
+
if (this.active.get(target.agentId) !== pending.activation) return;
|
|
952
|
+
pending.activation.suppressSettlement = true;
|
|
953
|
+
await this.rollbackActivation(
|
|
954
|
+
pending.activation,
|
|
955
|
+
pending.coldPrepared!,
|
|
956
|
+
);
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
throw error;
|
|
960
|
+
}
|
|
961
|
+
return {
|
|
962
|
+
kind: "legacy",
|
|
963
|
+
agentId: delivery.agentId,
|
|
964
|
+
taskPath: delivery.taskPath,
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
private async sendMessageSerialized(
|
|
969
|
+
parent: ParentRef,
|
|
970
|
+
childId: string,
|
|
971
|
+
message: string,
|
|
972
|
+
signal?: AbortSignal,
|
|
973
|
+
): Promise<SerializedSendMessageOutcome> {
|
|
974
|
+
if (this.draining) {
|
|
975
|
+
throw new Error("pi-subagent is shutting down; message was not delivered");
|
|
976
|
+
}
|
|
429
977
|
let activation = this.active.get(childId);
|
|
430
978
|
let coldPrepared: PreparedChildSession | undefined;
|
|
431
|
-
if (activation?.
|
|
432
|
-
|
|
433
|
-
activation
|
|
979
|
+
if (activation?.disposed) activation = undefined;
|
|
980
|
+
if (activation) {
|
|
981
|
+
this.touchActivation(activation);
|
|
982
|
+
this.assertContinuableDirectChild(parent, activation.descriptor);
|
|
983
|
+
if (activation.descriptor.runtime.backgroundProtocol === "mailbox-v2") {
|
|
984
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
985
|
+
await waitForPromise(activation.persistenceGate.promise, signal);
|
|
986
|
+
if (this.draining || activation.disposed) {
|
|
987
|
+
throw new Error(
|
|
988
|
+
`subagent ${childId} became unavailable before its mailbox could be persisted`,
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
const enqueued = enqueueMailboxMessage(
|
|
992
|
+
activation.runtime.session.sessionManager,
|
|
993
|
+
{
|
|
994
|
+
senderAgentId: parent.agentId,
|
|
995
|
+
recipientAgentId: childId,
|
|
996
|
+
content: message,
|
|
997
|
+
},
|
|
998
|
+
);
|
|
999
|
+
return {
|
|
1000
|
+
kind: "mailbox-v2",
|
|
1001
|
+
agentId: childId,
|
|
1002
|
+
taskPath: descriptorTaskPath(activation.descriptor),
|
|
1003
|
+
messageId: enqueued.message.messageId,
|
|
1004
|
+
pendingMessages: enqueued.pendingMessages,
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
if (activation?.pendingSettlement && !activation.currentRun) {
|
|
1009
|
+
await this.finalizeContinuableLocked(activation);
|
|
1010
|
+
if (activation.disposed || this.active.get(childId) !== activation) {
|
|
1011
|
+
activation = undefined;
|
|
1012
|
+
}
|
|
434
1013
|
}
|
|
435
|
-
|
|
436
1014
|
if (!activation) {
|
|
437
1015
|
const located = await this.findPersistedChild(parent, childId);
|
|
438
1016
|
if (!located) throw new Error(`unknown subagent: ${childId}; message was not delivered`);
|
|
439
|
-
|
|
440
|
-
throw new Error(`subagent ${childId} is one-shot and cannot accept follow-up messages`);
|
|
441
|
-
}
|
|
442
|
-
this.assertDirectParent(parent, located.descriptor);
|
|
1017
|
+
this.assertContinuableDirectChild(parent, located.descriptor);
|
|
443
1018
|
const manager = SessionManager.open(
|
|
444
1019
|
located.sessionFile,
|
|
445
1020
|
parent.sessionManager.getSessionDir(),
|
|
446
1021
|
parent.cwd,
|
|
447
1022
|
);
|
|
1023
|
+
if (located.descriptor.runtime.backgroundProtocol === "mailbox-v2") {
|
|
1024
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
1025
|
+
const enqueued = enqueueMailboxMessage(manager, {
|
|
1026
|
+
senderAgentId: parent.agentId,
|
|
1027
|
+
recipientAgentId: childId,
|
|
1028
|
+
content: message,
|
|
1029
|
+
});
|
|
1030
|
+
return {
|
|
1031
|
+
kind: "mailbox-v2",
|
|
1032
|
+
agentId: childId,
|
|
1033
|
+
taskPath: descriptorTaskPath(located.descriptor),
|
|
1034
|
+
messageId: enqueued.message.messageId,
|
|
1035
|
+
pendingMessages: enqueued.pendingMessages,
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
448
1038
|
coldPrepared = {
|
|
449
1039
|
sessionManager: manager,
|
|
450
1040
|
seedMessageCount: manager.buildSessionContext().messages.length,
|
|
@@ -456,21 +1046,61 @@ export class SubagentCoordinator {
|
|
|
456
1046
|
prepared: coldPrepared,
|
|
457
1047
|
isNew: false,
|
|
458
1048
|
});
|
|
459
|
-
|
|
1049
|
+
this.acquireParentOwnership(activation, parent);
|
|
460
1050
|
} else {
|
|
461
|
-
this.
|
|
1051
|
+
this.assertContinuableDirectChild(parent, activation.descriptor);
|
|
462
1052
|
}
|
|
1053
|
+
const resolvedPath = descriptorTaskPath(activation.descriptor);
|
|
463
1054
|
|
|
464
1055
|
const session = activation.runtime.session;
|
|
465
1056
|
if (activation.currentRun || session.isStreaming) {
|
|
466
1057
|
if (signal?.aborted) throw signal.reason ?? new Error("message delivery aborted");
|
|
1058
|
+
if (activation.controlState.turn.state === "queued") {
|
|
1059
|
+
if (!activation.currentMessageGate) {
|
|
1060
|
+
throw new Error(
|
|
1061
|
+
`subagent ${activation.agentId} is queued but has no message gate`,
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
return {
|
|
1065
|
+
kind: "legacy",
|
|
1066
|
+
agentId: childId,
|
|
1067
|
+
taskPath: resolvedPath,
|
|
1068
|
+
pending: {
|
|
1069
|
+
activation,
|
|
1070
|
+
accepted: waitForPromise(
|
|
1071
|
+
activation.currentMessageGate,
|
|
1072
|
+
signal,
|
|
1073
|
+
).then(() => {
|
|
1074
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
1075
|
+
return session.followUp(message);
|
|
1076
|
+
}),
|
|
1077
|
+
},
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
467
1080
|
await session.followUp(message);
|
|
468
|
-
return
|
|
1081
|
+
return {
|
|
1082
|
+
kind: "legacy",
|
|
1083
|
+
agentId: childId,
|
|
1084
|
+
taskPath: resolvedPath,
|
|
1085
|
+
};
|
|
469
1086
|
}
|
|
470
1087
|
|
|
471
1088
|
try {
|
|
472
|
-
|
|
473
|
-
|
|
1089
|
+
this.acquireParentOwnership(activation, parent);
|
|
1090
|
+
const started = this.startPrompt(activation, message, signal, {
|
|
1091
|
+
detachAtAcceptance: true,
|
|
1092
|
+
waitForCapacity: !parent.activation?.holdsBackgroundSlot,
|
|
1093
|
+
});
|
|
1094
|
+
return {
|
|
1095
|
+
kind: "legacy",
|
|
1096
|
+
agentId: childId,
|
|
1097
|
+
taskPath: resolvedPath,
|
|
1098
|
+
pending: {
|
|
1099
|
+
activation,
|
|
1100
|
+
accepted: started.accepted,
|
|
1101
|
+
...(coldPrepared ? { coldPrepared } : {}),
|
|
1102
|
+
},
|
|
1103
|
+
};
|
|
474
1104
|
} catch (error) {
|
|
475
1105
|
if (coldPrepared && !activation.published) {
|
|
476
1106
|
activation.suppressSettlement = true;
|
|
@@ -480,44 +1110,589 @@ export class SubagentCoordinator {
|
|
|
480
1110
|
}
|
|
481
1111
|
}
|
|
482
1112
|
|
|
1113
|
+
async followupTask(
|
|
1114
|
+
parent: ParentRef,
|
|
1115
|
+
childId: string,
|
|
1116
|
+
signal?: AbortSignal,
|
|
1117
|
+
): Promise<FollowupTaskOutcome> {
|
|
1118
|
+
return this.runAdmittedOperation(() =>
|
|
1119
|
+
this.followupTaskAdmitted(parent, childId, signal),
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
private async followupTaskAdmitted(
|
|
1124
|
+
parent: ParentRef,
|
|
1125
|
+
childId: string,
|
|
1126
|
+
signal?: AbortSignal,
|
|
1127
|
+
): Promise<FollowupTaskOutcome> {
|
|
1128
|
+
if (this.draining) {
|
|
1129
|
+
throw new Error("pi-subagent is shutting down; follow-up task was not started");
|
|
1130
|
+
}
|
|
1131
|
+
const target = await this.resolveTarget(parent, childId);
|
|
1132
|
+
const pending = await this.agentOperations.run(target.agentId, () =>
|
|
1133
|
+
this.followupTaskSerialized(parent, target.agentId, signal),
|
|
1134
|
+
);
|
|
1135
|
+
try {
|
|
1136
|
+
await pending.accepted;
|
|
1137
|
+
} catch (error) {
|
|
1138
|
+
if (pending.coldPrepared && !pending.activation.published) {
|
|
1139
|
+
await this.agentOperations.run(target.agentId, async () => {
|
|
1140
|
+
if (this.active.get(target.agentId) !== pending.activation) return;
|
|
1141
|
+
pending.activation.suppressSettlement = true;
|
|
1142
|
+
await this.rollbackActivation(
|
|
1143
|
+
pending.activation,
|
|
1144
|
+
pending.coldPrepared!,
|
|
1145
|
+
);
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
throw error;
|
|
1149
|
+
}
|
|
1150
|
+
return pending.outcome;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
private async followupTaskSerialized(
|
|
1154
|
+
parent: ParentRef,
|
|
1155
|
+
childId: string,
|
|
1156
|
+
signal?: AbortSignal,
|
|
1157
|
+
): Promise<PendingFollowupStart> {
|
|
1158
|
+
if (this.draining) {
|
|
1159
|
+
throw new Error("pi-subagent is shutting down; follow-up task was not started");
|
|
1160
|
+
}
|
|
1161
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
1162
|
+
let activation = this.active.get(childId);
|
|
1163
|
+
if (activation?.disposed) activation = undefined;
|
|
1164
|
+
let descriptor: SubagentDescriptor;
|
|
1165
|
+
let manager: SessionManager;
|
|
1166
|
+
let coldPrepared: PreparedChildSession | undefined;
|
|
1167
|
+
|
|
1168
|
+
if (activation) {
|
|
1169
|
+
this.touchActivation(activation);
|
|
1170
|
+
descriptor = activation.descriptor;
|
|
1171
|
+
this.assertContinuableDirectChild(parent, descriptor);
|
|
1172
|
+
manager = activation.runtime.session.sessionManager;
|
|
1173
|
+
if (activation.currentRun || activation.runtime.session.isStreaming) {
|
|
1174
|
+
throw new Error(
|
|
1175
|
+
`subagent ${childId} already has a scheduled or running turn; mailbox messages were not consumed`,
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
} else {
|
|
1179
|
+
const located = await this.findPersistedChild(parent, childId);
|
|
1180
|
+
if (!located) {
|
|
1181
|
+
throw new Error(`unknown subagent: ${childId}; follow-up task was not started`);
|
|
1182
|
+
}
|
|
1183
|
+
descriptor = located.descriptor;
|
|
1184
|
+
this.assertContinuableDirectChild(parent, descriptor);
|
|
1185
|
+
manager = SessionManager.open(
|
|
1186
|
+
located.sessionFile,
|
|
1187
|
+
parent.sessionManager.getSessionDir(),
|
|
1188
|
+
parent.cwd,
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
if (descriptor.runtime.backgroundProtocol !== "mailbox-v2") {
|
|
1192
|
+
throw new Error(
|
|
1193
|
+
`subagent ${childId} uses the legacy background protocol; use send_message to start its next turn`,
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const owner = mailboxOwner(descriptor);
|
|
1198
|
+
const batch = readMailbox(manager.getEntries(), owner).pending;
|
|
1199
|
+
if (batch.length === 0) {
|
|
1200
|
+
throw new Error(`subagent ${childId} has no pending mailbox messages`);
|
|
1201
|
+
}
|
|
1202
|
+
const messageIds = batch.map((message) => message.messageId);
|
|
1203
|
+
|
|
1204
|
+
if (!activation) {
|
|
1205
|
+
coldPrepared = {
|
|
1206
|
+
sessionManager: manager,
|
|
1207
|
+
seedMessageCount: manager.buildSessionContext().messages.length,
|
|
1208
|
+
rollback: () => Promise.resolve(),
|
|
1209
|
+
};
|
|
1210
|
+
activation = await this.createActivation({
|
|
1211
|
+
parent,
|
|
1212
|
+
descriptor,
|
|
1213
|
+
prepared: coldPrepared,
|
|
1214
|
+
isNew: false,
|
|
1215
|
+
});
|
|
1216
|
+
this.acquireParentOwnership(activation, parent);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
try {
|
|
1220
|
+
this.acquireParentOwnership(activation, parent);
|
|
1221
|
+
const started = this.startPrompt(activation, "", signal, {
|
|
1222
|
+
detachAtAcceptance: true,
|
|
1223
|
+
waitForCapacity: !parent.activation?.holdsBackgroundSlot,
|
|
1224
|
+
preparePrompt: (turnId) =>
|
|
1225
|
+
formatMailboxBatch(batch, turnId),
|
|
1226
|
+
onPromptAccepted: (turnId) => {
|
|
1227
|
+
claimMailboxMessages(
|
|
1228
|
+
manager,
|
|
1229
|
+
messageIds,
|
|
1230
|
+
turnId,
|
|
1231
|
+
owner,
|
|
1232
|
+
);
|
|
1233
|
+
activation.pendingMailboxClaims.add(turnId);
|
|
1234
|
+
},
|
|
1235
|
+
acceptAfterUserMessage: true,
|
|
1236
|
+
});
|
|
1237
|
+
return {
|
|
1238
|
+
activation,
|
|
1239
|
+
accepted: started.accepted,
|
|
1240
|
+
...(coldPrepared ? { coldPrepared } : {}),
|
|
1241
|
+
outcome: {
|
|
1242
|
+
agentId: childId,
|
|
1243
|
+
taskPath: descriptorTaskPath(descriptor),
|
|
1244
|
+
turnId: started.turnId,
|
|
1245
|
+
claimedMessages: batch.length,
|
|
1246
|
+
},
|
|
1247
|
+
};
|
|
1248
|
+
} catch (error) {
|
|
1249
|
+
if (coldPrepared && !activation.published) {
|
|
1250
|
+
activation.suppressSettlement = true;
|
|
1251
|
+
await this.rollbackActivation(activation, coldPrepared);
|
|
1252
|
+
}
|
|
1253
|
+
throw error;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
async waitAgent(
|
|
1258
|
+
parent: ParentRef,
|
|
1259
|
+
toolCallId: string,
|
|
1260
|
+
timeoutMs?: number,
|
|
1261
|
+
signal?: AbortSignal,
|
|
1262
|
+
): Promise<WaitAgentOutcome> {
|
|
1263
|
+
return this.runAdmittedOperation(async () => {
|
|
1264
|
+
const outcome = await this.waitAgentAdmitted(
|
|
1265
|
+
parent,
|
|
1266
|
+
toolCallId,
|
|
1267
|
+
waitTimeout(timeoutMs),
|
|
1268
|
+
signal,
|
|
1269
|
+
);
|
|
1270
|
+
if (outcome.updates.length === 0) {
|
|
1271
|
+
return { ...outcome, taskPaths: {} };
|
|
1272
|
+
}
|
|
1273
|
+
let paths = new Map<string, string>();
|
|
1274
|
+
try {
|
|
1275
|
+
const catalog = await this.catalogRecords(parent);
|
|
1276
|
+
paths = new Map(
|
|
1277
|
+
catalog.records.map((record) => [
|
|
1278
|
+
record.agentId,
|
|
1279
|
+
record.taskPath,
|
|
1280
|
+
]),
|
|
1281
|
+
);
|
|
1282
|
+
} catch {
|
|
1283
|
+
// Completion delivery is already durably reserved. Readable
|
|
1284
|
+
// path enrichment is cosmetic and must not turn that delivery
|
|
1285
|
+
// into a failed tool call.
|
|
1286
|
+
}
|
|
1287
|
+
return {
|
|
1288
|
+
...outcome,
|
|
1289
|
+
taskPaths: Object.fromEntries(
|
|
1290
|
+
outcome.updates.map((update) => [
|
|
1291
|
+
update.childAgentId,
|
|
1292
|
+
paths.get(update.childAgentId)
|
|
1293
|
+
?? update.childAgentId,
|
|
1294
|
+
]),
|
|
1295
|
+
),
|
|
1296
|
+
};
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
private async waitAgentAdmitted(
|
|
1301
|
+
parent: ParentRef,
|
|
1302
|
+
toolCallId: string,
|
|
1303
|
+
timeoutMs: number,
|
|
1304
|
+
signal?: AbortSignal,
|
|
1305
|
+
): Promise<WaitAgentOutcome> {
|
|
1306
|
+
if (this.draining) {
|
|
1307
|
+
throw new Error("pi-subagent is shutting down; wait_agent was not accepted");
|
|
1308
|
+
}
|
|
1309
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
1310
|
+
if (!toolCallId.trim() || toolCallId.length > 512) {
|
|
1311
|
+
throw new Error("wait_agent requires a non-empty tool call id of at most 512 characters");
|
|
1312
|
+
}
|
|
1313
|
+
const key = completionWaiterKey(parent);
|
|
1314
|
+
const deadline = Date.now() + timeoutMs;
|
|
1315
|
+
while (true) {
|
|
1316
|
+
let waiter: CompletionWaiter | undefined;
|
|
1317
|
+
const immediate = await this.completionOperations.run(
|
|
1318
|
+
key,
|
|
1319
|
+
async (): Promise<WaitAgentOutcome | undefined> => {
|
|
1320
|
+
if (this.draining) {
|
|
1321
|
+
throw new Error("pi-subagent is shutting down; wait_agent was interrupted");
|
|
1322
|
+
}
|
|
1323
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
1324
|
+
if (this.completionWaiters.has(key)) {
|
|
1325
|
+
throw new Error(
|
|
1326
|
+
`wait_agent is already waiting for direct-child activity on ${parent.agentId}`,
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
const snapshot = readCompletionMailbox(
|
|
1330
|
+
parent.sessionManager.getEntries(),
|
|
1331
|
+
{
|
|
1332
|
+
parentAgentId: parent.agentId,
|
|
1333
|
+
activeRuntimeId: this.runtimeId,
|
|
1334
|
+
},
|
|
1335
|
+
);
|
|
1336
|
+
if (snapshot.currentRuntimeReservations.length > 0) {
|
|
1337
|
+
throw new Error(
|
|
1338
|
+
"a previous wait_agent delivery is awaiting its durable tool result",
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
if (snapshot.available.length > 0) {
|
|
1342
|
+
return this.reserveWaitAgentOutcome(
|
|
1343
|
+
parent,
|
|
1344
|
+
toolCallId,
|
|
1345
|
+
timeoutMs,
|
|
1346
|
+
snapshot,
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
const remaining = Math.max(0, deadline - Date.now());
|
|
1350
|
+
if (remaining === 0) {
|
|
1351
|
+
return {
|
|
1352
|
+
timedOut: true,
|
|
1353
|
+
timeoutMs,
|
|
1354
|
+
updates: [],
|
|
1355
|
+
unreadUpdates: snapshot.unread.length,
|
|
1356
|
+
taskPaths: {},
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
waiter = this.createCompletionWaiter(remaining, signal);
|
|
1360
|
+
this.completionWaiters.set(key, waiter);
|
|
1361
|
+
|
|
1362
|
+
// Append happens before notify, but fold once more after
|
|
1363
|
+
// subscription so no completion can land in the check/register gap.
|
|
1364
|
+
const rechecked = readCompletionMailbox(
|
|
1365
|
+
parent.sessionManager.getEntries(),
|
|
1366
|
+
{
|
|
1367
|
+
parentAgentId: parent.agentId,
|
|
1368
|
+
activeRuntimeId: this.runtimeId,
|
|
1369
|
+
},
|
|
1370
|
+
);
|
|
1371
|
+
if (rechecked.available.length > 0) {
|
|
1372
|
+
try {
|
|
1373
|
+
return this.reserveWaitAgentOutcome(
|
|
1374
|
+
parent,
|
|
1375
|
+
toolCallId,
|
|
1376
|
+
timeoutMs,
|
|
1377
|
+
rechecked,
|
|
1378
|
+
);
|
|
1379
|
+
} finally {
|
|
1380
|
+
this.removeCompletionWaiter(key, waiter);
|
|
1381
|
+
waiter = undefined;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
return undefined;
|
|
1385
|
+
},
|
|
1386
|
+
);
|
|
1387
|
+
if (immediate) return immediate;
|
|
1388
|
+
if (!waiter) {
|
|
1389
|
+
throw new Error("wait_agent failed to establish an activity subscription");
|
|
1390
|
+
}
|
|
1391
|
+
let activity: "activity" | "timeout";
|
|
1392
|
+
try {
|
|
1393
|
+
activity = await waiter.promise;
|
|
1394
|
+
} catch (error) {
|
|
1395
|
+
this.removeCompletionWaiter(key, waiter);
|
|
1396
|
+
throw error;
|
|
1397
|
+
}
|
|
1398
|
+
const afterWake = await this.completionOperations.run(
|
|
1399
|
+
key,
|
|
1400
|
+
async (): Promise<WaitAgentOutcome | undefined> => {
|
|
1401
|
+
try {
|
|
1402
|
+
if (this.draining) {
|
|
1403
|
+
throw new Error(
|
|
1404
|
+
"pi-subagent is shutting down; wait_agent was interrupted",
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
1408
|
+
if (this.completionWaiters.get(key) !== waiter) {
|
|
1409
|
+
throw new Error(
|
|
1410
|
+
"wait_agent lost ownership of its activity subscription",
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
const snapshot = readCompletionMailbox(
|
|
1414
|
+
parent.sessionManager.getEntries(),
|
|
1415
|
+
{
|
|
1416
|
+
parentAgentId: parent.agentId,
|
|
1417
|
+
activeRuntimeId: this.runtimeId,
|
|
1418
|
+
},
|
|
1419
|
+
);
|
|
1420
|
+
if (snapshot.currentRuntimeReservations.length > 0) {
|
|
1421
|
+
throw new Error(
|
|
1422
|
+
"a previous wait_agent delivery is awaiting its durable tool result",
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
if (snapshot.available.length > 0) {
|
|
1426
|
+
return this.reserveWaitAgentOutcome(
|
|
1427
|
+
parent,
|
|
1428
|
+
toolCallId,
|
|
1429
|
+
timeoutMs,
|
|
1430
|
+
snapshot,
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
if (activity === "timeout" || Date.now() >= deadline) {
|
|
1434
|
+
return {
|
|
1435
|
+
timedOut: true,
|
|
1436
|
+
timeoutMs,
|
|
1437
|
+
updates: [],
|
|
1438
|
+
unreadUpdates: snapshot.unread.length,
|
|
1439
|
+
taskPaths: {},
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
return undefined;
|
|
1443
|
+
} finally {
|
|
1444
|
+
this.removeCompletionWaiter(key, waiter!);
|
|
1445
|
+
}
|
|
1446
|
+
},
|
|
1447
|
+
);
|
|
1448
|
+
if (afterWake) return afterWake;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
private reserveWaitAgentOutcome(
|
|
1453
|
+
parent: ParentRef,
|
|
1454
|
+
toolCallId: string,
|
|
1455
|
+
timeoutMs: number,
|
|
1456
|
+
snapshot: ReturnType<typeof readCompletionMailbox>,
|
|
1457
|
+
): WaitAgentOutcome {
|
|
1458
|
+
const updates = this.boundedWaitAgentUpdates(snapshot.available);
|
|
1459
|
+
reserveCompletionDelivery(parent.sessionManager, {
|
|
1460
|
+
parentAgentId: parent.agentId,
|
|
1461
|
+
runtimeId: this.runtimeId,
|
|
1462
|
+
toolCallId,
|
|
1463
|
+
completionIds: updates.map((update) => update.completionId),
|
|
1464
|
+
});
|
|
1465
|
+
return {
|
|
1466
|
+
timedOut: false,
|
|
1467
|
+
timeoutMs,
|
|
1468
|
+
updates,
|
|
1469
|
+
unreadUpdates: Math.max(
|
|
1470
|
+
0,
|
|
1471
|
+
snapshot.unread.length - updates.length,
|
|
1472
|
+
),
|
|
1473
|
+
taskPaths: {},
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
private boundedWaitAgentUpdates(
|
|
1478
|
+
available: readonly CompletionUpdate[],
|
|
1479
|
+
): CompletionUpdate[] {
|
|
1480
|
+
const updates: CompletionUpdate[] = [];
|
|
1481
|
+
let remainingBytes = MAX_WAIT_AGENT_RESULT_BYTES;
|
|
1482
|
+
for (const update of available) {
|
|
1483
|
+
if (updates.length >= MAX_COMPLETIONS_PER_DELIVERY) break;
|
|
1484
|
+
const metadataBytes = Buffer.byteLength(
|
|
1485
|
+
`completion ${update.completionId}\nchild=${update.childAgentId} turn=${update.turnId} stop=${update.stopReason}\n`,
|
|
1486
|
+
"utf8",
|
|
1487
|
+
) + 256;
|
|
1488
|
+
const fullBytes =
|
|
1489
|
+
metadataBytes + Buffer.byteLength(update.output, "utf8");
|
|
1490
|
+
if (updates.length > 0 && fullBytes > remainingBytes) break;
|
|
1491
|
+
const outputBudget = Math.max(0, remainingBytes - metadataBytes);
|
|
1492
|
+
const truncated = truncateUtf8(update.output, outputBudget);
|
|
1493
|
+
updates.push({
|
|
1494
|
+
...update,
|
|
1495
|
+
output: truncated.text,
|
|
1496
|
+
...(truncated.truncated || update.outputTruncated
|
|
1497
|
+
? {
|
|
1498
|
+
outputTruncated: true,
|
|
1499
|
+
omittedBytes:
|
|
1500
|
+
(update.omittedBytes ?? 0)
|
|
1501
|
+
+ truncated.omittedBytes,
|
|
1502
|
+
}
|
|
1503
|
+
: {}),
|
|
1504
|
+
});
|
|
1505
|
+
remainingBytes = Math.max(
|
|
1506
|
+
0,
|
|
1507
|
+
remainingBytes
|
|
1508
|
+
- metadataBytes
|
|
1509
|
+
- Buffer.byteLength(truncated.text, "utf8"),
|
|
1510
|
+
);
|
|
1511
|
+
if (truncated.truncated) break;
|
|
1512
|
+
}
|
|
1513
|
+
return updates;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
async releaseWaitAgentDeliveries(
|
|
1517
|
+
parent: ParentRef,
|
|
1518
|
+
reason: string,
|
|
1519
|
+
): Promise<number> {
|
|
1520
|
+
const key = completionWaiterKey(parent);
|
|
1521
|
+
return this.completionOperations.run(key, async () =>
|
|
1522
|
+
releaseCompletionDeliveries(parent.sessionManager, {
|
|
1523
|
+
parentAgentId: parent.agentId,
|
|
1524
|
+
runtimeId: this.runtimeId,
|
|
1525
|
+
reason,
|
|
1526
|
+
}),
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
private createCompletionWaiter(
|
|
1531
|
+
timeoutMs: number,
|
|
1532
|
+
signal?: AbortSignal,
|
|
1533
|
+
): CompletionWaiter {
|
|
1534
|
+
let settled = false;
|
|
1535
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1536
|
+
let resolvePromise!: (activity: "activity" | "timeout") => void;
|
|
1537
|
+
let rejectPromise!: (error: Error) => void;
|
|
1538
|
+
const promise = new Promise<"activity" | "timeout">(
|
|
1539
|
+
(resolve, reject) => {
|
|
1540
|
+
resolvePromise = resolve;
|
|
1541
|
+
rejectPromise = reject;
|
|
1542
|
+
},
|
|
1543
|
+
);
|
|
1544
|
+
const onAbort = () => {
|
|
1545
|
+
if (settled) return;
|
|
1546
|
+
settled = true;
|
|
1547
|
+
if (timer) clearTimeout(timer);
|
|
1548
|
+
rejectPromise(abortReason(signal!));
|
|
1549
|
+
};
|
|
1550
|
+
if (signal) signal.addEventListener("abort", onAbort, { once: true });
|
|
1551
|
+
timer = setTimeout(() => {
|
|
1552
|
+
if (settled) return;
|
|
1553
|
+
settled = true;
|
|
1554
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
1555
|
+
resolvePromise("timeout");
|
|
1556
|
+
}, timeoutMs);
|
|
1557
|
+
// An awaited tool deadline must keep headless SDK processes alive.
|
|
1558
|
+
// Wake, abort and shutdown clear it rather than leaving a background timer.
|
|
1559
|
+
const dispose = () => {
|
|
1560
|
+
if (timer) clearTimeout(timer);
|
|
1561
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
1562
|
+
};
|
|
1563
|
+
return {
|
|
1564
|
+
promise,
|
|
1565
|
+
wake: () => {
|
|
1566
|
+
if (settled) return;
|
|
1567
|
+
settled = true;
|
|
1568
|
+
dispose();
|
|
1569
|
+
resolvePromise("activity");
|
|
1570
|
+
},
|
|
1571
|
+
reject: (error) => {
|
|
1572
|
+
if (settled) return;
|
|
1573
|
+
settled = true;
|
|
1574
|
+
dispose();
|
|
1575
|
+
rejectPromise(error);
|
|
1576
|
+
},
|
|
1577
|
+
dispose,
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
private removeCompletionWaiter(
|
|
1582
|
+
key: string,
|
|
1583
|
+
waiter: CompletionWaiter,
|
|
1584
|
+
): void {
|
|
1585
|
+
if (this.completionWaiters.get(key) === waiter) {
|
|
1586
|
+
this.completionWaiters.delete(key);
|
|
1587
|
+
}
|
|
1588
|
+
waiter.dispose();
|
|
1589
|
+
}
|
|
1590
|
+
|
|
483
1591
|
async interrupt(parent: ParentRef, targetId: string): Promise<void> {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
1592
|
+
await this.interruptWithOutcome(parent, targetId);
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
async interruptWithOutcome(
|
|
1596
|
+
parent: ParentRef,
|
|
1597
|
+
targetId: string,
|
|
1598
|
+
): Promise<InterruptOutcome> {
|
|
1599
|
+
return this.runAdmittedOperation(() =>
|
|
1600
|
+
this.interruptAdmitted(parent, targetId),
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
private async interruptAdmitted(
|
|
1605
|
+
parent: ParentRef,
|
|
1606
|
+
targetId: string,
|
|
1607
|
+
): Promise<InterruptOutcome> {
|
|
1608
|
+
const resolved = await this.resolveTarget(parent, targetId, {
|
|
1609
|
+
allowUnknownId: true,
|
|
1610
|
+
});
|
|
1611
|
+
return this.agentOperations.run(resolved.agentId, async () => {
|
|
1612
|
+
if (
|
|
1613
|
+
resolved.record
|
|
1614
|
+
&& !(await this.isDescendantOf(
|
|
1615
|
+
parent,
|
|
1616
|
+
resolved.record.descriptor,
|
|
1617
|
+
))
|
|
1618
|
+
) {
|
|
1619
|
+
throw new Error(
|
|
1620
|
+
`subagent ${resolved.taskPath} is not a descendant of ${parent.taskPath}`,
|
|
1621
|
+
);
|
|
1622
|
+
}
|
|
1623
|
+
const target = this.active.get(resolved.agentId);
|
|
1624
|
+
if (!target) {
|
|
1625
|
+
return {
|
|
1626
|
+
agentId: resolved.agentId,
|
|
1627
|
+
taskPath: resolved.taskPath,
|
|
1628
|
+
active: false,
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
if (!(await this.isDescendantOf(parent, target.descriptor))) {
|
|
1632
|
+
throw new Error(
|
|
1633
|
+
`subagent ${resolved.taskPath} is not a live descendant of ${parent.taskPath}`,
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
this.touchActivation(target);
|
|
1637
|
+
interruptAgentTurn(target.controlState);
|
|
1638
|
+
this.emitUpdate(target);
|
|
1639
|
+
target.turnAbortController?.abort(
|
|
1640
|
+
new Error(`subagent ${resolved.taskPath} was interrupted`),
|
|
1641
|
+
);
|
|
1642
|
+
void target.runtime.session.abort().catch((error) => {
|
|
1643
|
+
target.lastError = errorText(error);
|
|
1644
|
+
});
|
|
1645
|
+
return {
|
|
1646
|
+
agentId: resolved.agentId,
|
|
1647
|
+
taskPath: descriptorTaskPath(target.descriptor),
|
|
1648
|
+
active: true,
|
|
1649
|
+
};
|
|
492
1650
|
});
|
|
493
1651
|
}
|
|
494
1652
|
|
|
495
1653
|
async list(parent: ParentRef, scope: "children" | "descendants"): Promise<CatalogEntry[]> {
|
|
1654
|
+
return this.runAdmittedOperation(() => this.listAdmitted(parent, scope));
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
private async listAdmitted(
|
|
1658
|
+
parent: ParentRef,
|
|
1659
|
+
scope: "children" | "descendants",
|
|
1660
|
+
): Promise<CatalogEntry[]> {
|
|
496
1661
|
const catalog = await this.catalogRecords(parent);
|
|
497
1662
|
const records = catalog.records;
|
|
498
|
-
const byId = new Map(records.map((record) => [record.
|
|
1663
|
+
const byId = new Map(records.map((record) => [record.agentId, record]));
|
|
499
1664
|
const children: CatalogChild[] = [];
|
|
500
1665
|
for (const record of records) {
|
|
501
1666
|
if (record.descriptor.mode !== "continuable") continue;
|
|
502
|
-
const distance = this.distanceFrom(parent.
|
|
1667
|
+
const distance = this.distanceFrom(parent.agentId, record.descriptor, byId);
|
|
503
1668
|
if (distance === undefined || (scope === "children" && distance !== 1)) continue;
|
|
504
1669
|
children.push({
|
|
505
1670
|
kind: "child",
|
|
506
|
-
|
|
507
|
-
|
|
1671
|
+
agentId: record.agentId,
|
|
1672
|
+
parentAgentId: record.descriptor.parentAgentId,
|
|
1673
|
+
taskPath: record.taskPath,
|
|
1674
|
+
parentTaskPath:
|
|
1675
|
+
record.descriptor.parentAgentId === parent.agentId
|
|
1676
|
+
? parent.taskPath
|
|
1677
|
+
: (byId.get(record.descriptor.parentAgentId)?.taskPath
|
|
1678
|
+
?? record.descriptor.parentAgentId),
|
|
508
1679
|
depth: distance,
|
|
509
1680
|
descriptor: record.descriptor,
|
|
510
1681
|
...(record.sessionFile ? { sessionFile: record.sessionFile } : {}),
|
|
511
|
-
status: record.active
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
1682
|
+
status: record.active ? catalogStatus(record.active.controlState) : "ready",
|
|
1683
|
+
pendingMessages: record.pendingMessages,
|
|
1684
|
+
unreadUpdates:
|
|
1685
|
+
record.descriptor.parentAgentId === parent.agentId
|
|
1686
|
+
? (catalog.rootUnreadUpdatesByChild.get(record.agentId) ?? 0)
|
|
1687
|
+
: (byId
|
|
1688
|
+
.get(record.descriptor.parentAgentId)
|
|
1689
|
+
?.unreadUpdatesByChild.get(record.agentId) ?? 0),
|
|
516
1690
|
});
|
|
517
1691
|
}
|
|
518
1692
|
children.sort(
|
|
519
1693
|
(left, right) =>
|
|
520
|
-
left.descriptor.createdAt.localeCompare(right.descriptor.createdAt) ||
|
|
1694
|
+
left.descriptor.createdAt.localeCompare(right.descriptor.createdAt) ||
|
|
1695
|
+
left.agentId.localeCompare(right.agentId),
|
|
521
1696
|
);
|
|
522
1697
|
const parentFile = parent.sessionManager.getSessionFile();
|
|
523
1698
|
const diagnostics = parentFile
|
|
@@ -529,11 +1704,16 @@ export class SubagentCoordinator {
|
|
|
529
1704
|
}
|
|
530
1705
|
|
|
531
1706
|
async report(child: Activation, output: string): Promise<void> {
|
|
1707
|
+
return this.runAdmittedOperation(() => this.reportAdmitted(child, output));
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
private async reportAdmitted(child: Activation, output: string): Promise<void> {
|
|
532
1711
|
if (child.descriptor.mode !== "continuable") {
|
|
533
1712
|
throw new Error("report is available only to continuable subagents");
|
|
534
1713
|
}
|
|
535
1714
|
const truncated = truncateUtf8(output, child.descriptor.runtime.maxOutputBytes);
|
|
536
|
-
const
|
|
1715
|
+
const readablePath = descriptorTaskPath(child.descriptor);
|
|
1716
|
+
const content = `Background subagent ${readablePath} (${child.agentId}) reported:\n\n${truncated.text}${
|
|
537
1717
|
truncated.truncated ? `\n\n[Report truncated; ${truncated.omittedBytes} bytes omitted.]` : ""
|
|
538
1718
|
}`;
|
|
539
1719
|
await child.parent.deliver(
|
|
@@ -541,7 +1721,8 @@ export class SubagentCoordinator {
|
|
|
541
1721
|
content,
|
|
542
1722
|
{
|
|
543
1723
|
kind: "report",
|
|
544
|
-
|
|
1724
|
+
childAgentId: child.agentId,
|
|
1725
|
+
taskPath: readablePath,
|
|
545
1726
|
label: child.descriptor.label,
|
|
546
1727
|
...(truncated.truncated ? { truncated: true } : {}),
|
|
547
1728
|
},
|
|
@@ -549,19 +1730,84 @@ export class SubagentCoordinator {
|
|
|
549
1730
|
);
|
|
550
1731
|
}
|
|
551
1732
|
|
|
1733
|
+
private runAdmittedOperation<T>(operation: () => Promise<T>): Promise<T> {
|
|
1734
|
+
if (this.draining) {
|
|
1735
|
+
return Promise.reject(new Error("pi-subagent is shutting down"));
|
|
1736
|
+
}
|
|
1737
|
+
const promise = operation();
|
|
1738
|
+
this.admittedOperations.add(promise);
|
|
1739
|
+
const remove = () => {
|
|
1740
|
+
this.admittedOperations.delete(promise);
|
|
1741
|
+
};
|
|
1742
|
+
void promise.then(remove, remove);
|
|
1743
|
+
return promise;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
private async waitForAdmittedOperations(): Promise<void> {
|
|
1747
|
+
while (this.admittedOperations.size > 0) {
|
|
1748
|
+
await Promise.allSettled([...this.admittedOperations]);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
|
|
552
1752
|
async shutdown(): Promise<void> {
|
|
553
|
-
if (this.
|
|
1753
|
+
if (this.shutdownPromise) return this.shutdownPromise;
|
|
554
1754
|
this.draining = true;
|
|
1755
|
+
this.rejectCompletionWaiters(
|
|
1756
|
+
new Error("pi-subagent is shutting down"),
|
|
1757
|
+
);
|
|
1758
|
+
this.shutdownPromise = this.performShutdown();
|
|
1759
|
+
return this.shutdownPromise;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
private async performShutdown(): Promise<void> {
|
|
1763
|
+
this.backgroundRuns.close();
|
|
1764
|
+
await this.abortActiveRuns();
|
|
1765
|
+
await this.agentOperations.waitForIdle();
|
|
1766
|
+
await this.idleRuntimeOperations.waitForIdle();
|
|
1767
|
+
await this.completionOperations.waitForIdle();
|
|
1768
|
+
await this.abortActiveRuns();
|
|
1769
|
+
await this.waitForAdmittedOperations();
|
|
1770
|
+
await this.agentOperations.waitForIdle();
|
|
1771
|
+
await this.abortActiveRuns();
|
|
555
1772
|
const activations = [...this.active.values()];
|
|
556
|
-
for (const activation of activations) activation.suppressSettlement = true;
|
|
557
1773
|
await Promise.allSettled(
|
|
558
|
-
activations
|
|
559
|
-
|
|
560
|
-
|
|
1774
|
+
activations
|
|
1775
|
+
.map((activation) => activation.currentRun)
|
|
1776
|
+
.filter(
|
|
1777
|
+
(run): run is Promise<SubagentRunResult> => run !== undefined,
|
|
1778
|
+
),
|
|
561
1779
|
);
|
|
562
1780
|
for (const activation of activations.sort((left, right) => right.descriptor.depth - left.descriptor.depth)) {
|
|
563
1781
|
await this.disposeActivation(activation).catch(() => {});
|
|
564
1782
|
}
|
|
1783
|
+
await this.agentOperations.waitForIdle();
|
|
1784
|
+
await this.idleRuntimeOperations.waitForIdle();
|
|
1785
|
+
await this.completionOperations.waitForIdle();
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
private rejectCompletionWaiters(error: Error): void {
|
|
1789
|
+
for (const [key, waiter] of this.completionWaiters) {
|
|
1790
|
+
this.completionWaiters.delete(key);
|
|
1791
|
+
waiter.reject(error);
|
|
1792
|
+
waiter.dispose();
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
private async abortActiveRuns(): Promise<void> {
|
|
1797
|
+
const activations = [...this.active.values()];
|
|
1798
|
+
for (const activation of activations) {
|
|
1799
|
+
activation.suppressSettlement = true;
|
|
1800
|
+
activation.turnAbortController?.abort(
|
|
1801
|
+
new Error("pi-subagent is shutting down"),
|
|
1802
|
+
);
|
|
1803
|
+
}
|
|
1804
|
+
await Promise.allSettled(
|
|
1805
|
+
activations.map(async (activation) => {
|
|
1806
|
+
if (!activation.runtime.session.isIdle) {
|
|
1807
|
+
await activation.runtime.session.abort();
|
|
1808
|
+
}
|
|
1809
|
+
}),
|
|
1810
|
+
);
|
|
565
1811
|
}
|
|
566
1812
|
|
|
567
1813
|
createChildToolDefinitions(
|
|
@@ -569,6 +1815,7 @@ export class SubagentCoordinator {
|
|
|
569
1815
|
enableRunInBackground = true,
|
|
570
1816
|
defaultBackground = true,
|
|
571
1817
|
agentDiscovery?: AgentDiscoveryResult,
|
|
1818
|
+
backgroundProtocol: NonNullable<SubagentSettings["backgroundProtocol"]> = "legacy",
|
|
572
1819
|
): ToolDefinition[] {
|
|
573
1820
|
const agentNames = agentDiscovery?.agents.map((agent) => agent.name);
|
|
574
1821
|
const assertBackgroundControlEnabled = (toolName: string): void => {
|
|
@@ -588,12 +1835,12 @@ export class SubagentCoordinator {
|
|
|
588
1835
|
name: "subagent",
|
|
589
1836
|
label: "Subagent",
|
|
590
1837
|
description:
|
|
591
|
-
"Delegate a standalone task to a
|
|
1838
|
+
"Delegate a standalone task to a named child path, with optional completed-turn context inheritance. " +
|
|
592
1839
|
(!enableRunInBackground
|
|
593
1840
|
? "This foreground-only instance always waits for the result."
|
|
594
1841
|
: defaultBackground
|
|
595
|
-
? "It runs in the background by default and returns a durable id."
|
|
596
|
-
: "It waits for the result by default; background mode returns a durable id."),
|
|
1842
|
+
? "It runs in the background by default and returns a readable path plus durable id."
|
|
1843
|
+
: "It waits for the result by default; background mode returns a readable path plus durable id."),
|
|
597
1844
|
parameters: delegationParameters(enableRunInBackground, agentNames),
|
|
598
1845
|
execute: async (_id, params, signal, onUpdate) => {
|
|
599
1846
|
const activation = getActivation();
|
|
@@ -614,8 +1861,11 @@ export class SubagentCoordinator {
|
|
|
614
1861
|
name: "subagent_fork",
|
|
615
1862
|
label: "Subagent Fork",
|
|
616
1863
|
description:
|
|
617
|
-
"Delegate a
|
|
618
|
-
parameters: forkDelegationParameters(
|
|
1864
|
+
"Delegate a task to a child seeded with all completed turns from this conversation. It is foreground by default and can be explicitly continuable.",
|
|
1865
|
+
parameters: forkDelegationParameters(
|
|
1866
|
+
agentNames,
|
|
1867
|
+
enableRunInBackground,
|
|
1868
|
+
),
|
|
619
1869
|
execute: async (_id, params, signal, onUpdate) => {
|
|
620
1870
|
const activation = getActivation();
|
|
621
1871
|
const outcome = await this.delegate(
|
|
@@ -635,12 +1885,14 @@ export class SubagentCoordinator {
|
|
|
635
1885
|
name: "send_message",
|
|
636
1886
|
label: "Send Message",
|
|
637
1887
|
description:
|
|
638
|
-
|
|
1888
|
+
backgroundProtocol === "mailbox-v2"
|
|
1889
|
+
? "Durably append a message to a direct mailbox-v2 child's FIFO mailbox without starting or resuming it."
|
|
1890
|
+
: "Queue a message as a direct continuable child's next FIFO turn. This returns acceptance, not the child's answer.",
|
|
639
1891
|
parameters: SendMessageParameters,
|
|
640
1892
|
execute: async (_id, params, signal) => {
|
|
641
1893
|
assertBackgroundControlEnabled("send_message");
|
|
642
1894
|
const activation = getActivation();
|
|
643
|
-
await this.
|
|
1895
|
+
const delivery = await this.sendMessageWithOutcome(
|
|
644
1896
|
this.parentForActivation(activation),
|
|
645
1897
|
params.subagent_id,
|
|
646
1898
|
params.message,
|
|
@@ -650,10 +1902,92 @@ export class SubagentCoordinator {
|
|
|
650
1902
|
content: [
|
|
651
1903
|
{
|
|
652
1904
|
type: "text",
|
|
653
|
-
text:
|
|
1905
|
+
text:
|
|
1906
|
+
delivery.kind === "mailbox-v2"
|
|
1907
|
+
? `message ${delivery.messageId} durably enqueued for ${delivery.taskPath}; ${delivery.pendingMessages} pending`
|
|
1908
|
+
: `message queued as the next turn for ${delivery.taskPath}`,
|
|
1909
|
+
},
|
|
1910
|
+
],
|
|
1911
|
+
details: {
|
|
1912
|
+
kind: "control",
|
|
1913
|
+
action: "send",
|
|
1914
|
+
agentId: delivery.agentId,
|
|
1915
|
+
taskPath: delivery.taskPath,
|
|
1916
|
+
...(delivery.kind === "mailbox-v2"
|
|
1917
|
+
? {
|
|
1918
|
+
messageId: delivery.messageId,
|
|
1919
|
+
pendingMessages: delivery.pendingMessages,
|
|
1920
|
+
}
|
|
1921
|
+
: {}),
|
|
1922
|
+
} satisfies ControlDetails,
|
|
1923
|
+
};
|
|
1924
|
+
},
|
|
1925
|
+
});
|
|
1926
|
+
|
|
1927
|
+
const followup = defineTool({
|
|
1928
|
+
name: "followup_task",
|
|
1929
|
+
label: "Follow-up Task",
|
|
1930
|
+
description:
|
|
1931
|
+
"Start exactly one scheduled turn for a direct mailbox-v2 child, atomically claiming its current pending FIFO mailbox batch.",
|
|
1932
|
+
parameters: FollowupTaskParameters,
|
|
1933
|
+
execute: async (_id, params, signal) => {
|
|
1934
|
+
assertBackgroundControlEnabled("followup_task");
|
|
1935
|
+
const activation = getActivation();
|
|
1936
|
+
const outcome = await this.followupTask(
|
|
1937
|
+
this.parentForActivation(activation),
|
|
1938
|
+
params.subagent_id,
|
|
1939
|
+
signal,
|
|
1940
|
+
);
|
|
1941
|
+
return {
|
|
1942
|
+
content: [
|
|
1943
|
+
{
|
|
1944
|
+
type: "text",
|
|
1945
|
+
text: `started turn ${outcome.turnId} for ${outcome.taskPath}, claiming ${outcome.claimedMessages} mailbox message${outcome.claimedMessages === 1 ? "" : "s"}`,
|
|
654
1946
|
},
|
|
655
1947
|
],
|
|
656
|
-
details: {
|
|
1948
|
+
details: {
|
|
1949
|
+
kind: "control",
|
|
1950
|
+
action: "followup",
|
|
1951
|
+
agentId: outcome.agentId,
|
|
1952
|
+
taskPath: outcome.taskPath,
|
|
1953
|
+
turnId: outcome.turnId,
|
|
1954
|
+
claimedMessages: outcome.claimedMessages,
|
|
1955
|
+
} satisfies ControlDetails,
|
|
1956
|
+
};
|
|
1957
|
+
},
|
|
1958
|
+
});
|
|
1959
|
+
|
|
1960
|
+
const wait = defineTool({
|
|
1961
|
+
name: "wait_agent",
|
|
1962
|
+
label: "Wait Agent",
|
|
1963
|
+
description:
|
|
1964
|
+
"Wait event-driven for unread completion updates from direct mailbox-v2 children. This does not start a child or occupy a background scheduler slot.",
|
|
1965
|
+
parameters: WaitAgentParameters,
|
|
1966
|
+
execute: async (id, params, signal) => {
|
|
1967
|
+
assertBackgroundControlEnabled("wait_agent");
|
|
1968
|
+
const activation = getActivation();
|
|
1969
|
+
const outcome = await this.waitAgent(
|
|
1970
|
+
this.parentForActivation(activation),
|
|
1971
|
+
id,
|
|
1972
|
+
params.timeout_ms,
|
|
1973
|
+
signal,
|
|
1974
|
+
);
|
|
1975
|
+
return {
|
|
1976
|
+
content: [
|
|
1977
|
+
{
|
|
1978
|
+
type: "text",
|
|
1979
|
+
text: this.formatWaitAgentOutcome(outcome),
|
|
1980
|
+
},
|
|
1981
|
+
],
|
|
1982
|
+
details: {
|
|
1983
|
+
kind: "control",
|
|
1984
|
+
action: "wait",
|
|
1985
|
+
timedOut: outcome.timedOut,
|
|
1986
|
+
completionIds: outcome.updates.map(
|
|
1987
|
+
(update) => update.completionId,
|
|
1988
|
+
),
|
|
1989
|
+
unreadUpdates: outcome.unreadUpdates,
|
|
1990
|
+
} satisfies ControlDetails,
|
|
657
1991
|
};
|
|
658
1992
|
},
|
|
659
1993
|
});
|
|
@@ -667,10 +2001,18 @@ export class SubagentCoordinator {
|
|
|
667
2001
|
execute: async (_id, params) => {
|
|
668
2002
|
assertBackgroundControlEnabled("interrupt_agent");
|
|
669
2003
|
const activation = getActivation();
|
|
670
|
-
await this.
|
|
2004
|
+
const outcome = await this.interruptWithOutcome(
|
|
2005
|
+
this.parentForActivation(activation),
|
|
2006
|
+
params.agent_id,
|
|
2007
|
+
);
|
|
671
2008
|
return {
|
|
672
|
-
content: [{ type: "text", text: `interrupt requested for
|
|
673
|
-
details: {
|
|
2009
|
+
content: [{ type: "text", text: `interrupt requested for ${outcome.taskPath}` }],
|
|
2010
|
+
details: {
|
|
2011
|
+
kind: "control",
|
|
2012
|
+
action: "interrupt",
|
|
2013
|
+
agentId: outcome.agentId,
|
|
2014
|
+
taskPath: outcome.taskPath,
|
|
2015
|
+
} satisfies ControlDetails,
|
|
674
2016
|
};
|
|
675
2017
|
},
|
|
676
2018
|
});
|
|
@@ -679,7 +2021,7 @@ export class SubagentCoordinator {
|
|
|
679
2021
|
name: "list_agents",
|
|
680
2022
|
label: "List Agents",
|
|
681
2023
|
description:
|
|
682
|
-
"List direct continuable children or all descendants as running, idle, or ready
|
|
2024
|
+
"List direct continuable children or all descendants as running, idle, or ready, with separate mailbox task and completion counts.",
|
|
683
2025
|
parameters: ListAgentsParameters,
|
|
684
2026
|
execute: async (_id, params) => {
|
|
685
2027
|
assertBackgroundControlEnabled("list_agents");
|
|
@@ -706,18 +2048,28 @@ export class SubagentCoordinator {
|
|
|
706
2048
|
await this.report(activation, params.output);
|
|
707
2049
|
return {
|
|
708
2050
|
content: [{ type: "text", text: `report accepted by the agent that started you` }],
|
|
709
|
-
|
|
2051
|
+
details: {
|
|
2052
|
+
kind: "control",
|
|
2053
|
+
action: "report",
|
|
2054
|
+
agentId: activation.agentId,
|
|
2055
|
+
taskPath: descriptorTaskPath(activation.descriptor),
|
|
2056
|
+
} satisfies ControlDetails,
|
|
710
2057
|
};
|
|
711
2058
|
},
|
|
712
2059
|
});
|
|
713
2060
|
|
|
714
|
-
return [spawn, fork, send, interrupt, list, report];
|
|
2061
|
+
return [spawn, fork, send, followup, wait, interrupt, list, report];
|
|
715
2062
|
}
|
|
716
2063
|
|
|
717
2064
|
outcomeToolResult(outcome: DelegationOutcome): AgentToolResult<DelegationDetails> {
|
|
718
2065
|
if (outcome.kind === "continuable") {
|
|
719
2066
|
return {
|
|
720
|
-
content: [
|
|
2067
|
+
content: [
|
|
2068
|
+
{
|
|
2069
|
+
type: "text",
|
|
2070
|
+
text: `started subagent ${outcome.details.taskPath} (${outcome.details.agentId})`,
|
|
2071
|
+
},
|
|
2072
|
+
],
|
|
721
2073
|
details: outcome.details,
|
|
722
2074
|
};
|
|
723
2075
|
}
|
|
@@ -740,14 +2092,56 @@ export class SubagentCoordinator {
|
|
|
740
2092
|
if (entries.length === 0) return "(no subagents)";
|
|
741
2093
|
return entries
|
|
742
2094
|
.map((entry) => {
|
|
743
|
-
if (entry.kind === "diagnostic")
|
|
2095
|
+
if (entry.kind === "diagnostic") {
|
|
2096
|
+
return `${entry.piSessionId} [diagnostic: ${entry.reason}]`;
|
|
2097
|
+
}
|
|
744
2098
|
const location =
|
|
745
|
-
scope === "descendants"
|
|
746
|
-
|
|
2099
|
+
scope === "descendants"
|
|
2100
|
+
? ` parent=${entry.parentTaskPath} depth=${entry.depth}`
|
|
2101
|
+
: "";
|
|
2102
|
+
const mailbox =
|
|
2103
|
+
entry.descriptor.runtime.backgroundProtocol === "mailbox-v2"
|
|
2104
|
+
? ` pending=${entry.pendingMessages} updates=${entry.unreadUpdates}`
|
|
2105
|
+
: "";
|
|
2106
|
+
return `${entry.taskPath} [${entry.status}]${mailbox}${location} — ${entry.descriptor.label} (${entry.descriptor.agent.name}) id=${entry.agentId}`;
|
|
747
2107
|
})
|
|
748
2108
|
.join("\n");
|
|
749
2109
|
}
|
|
750
2110
|
|
|
2111
|
+
formatWaitAgentOutcome(outcome: WaitAgentOutcome): string {
|
|
2112
|
+
if (outcome.timedOut) {
|
|
2113
|
+
return `wait_agent timed out after ${outcome.timeoutMs}ms with no completion updates`;
|
|
2114
|
+
}
|
|
2115
|
+
const updates = outcome.updates.map((update) => {
|
|
2116
|
+
const output = update.output.trim() || "(no output)";
|
|
2117
|
+
const fullPath =
|
|
2118
|
+
outcome.taskPaths[update.childAgentId]
|
|
2119
|
+
?? update.childAgentId;
|
|
2120
|
+
const readablePath =
|
|
2121
|
+
fullPath.length > 200
|
|
2122
|
+
? `${fullPath.slice(0, 199)}…`
|
|
2123
|
+
: fullPath;
|
|
2124
|
+
const truncation = update.outputTruncated
|
|
2125
|
+
? `\n[Completion output truncated${
|
|
2126
|
+
update.omittedBytes !== undefined
|
|
2127
|
+
? `; ${update.omittedBytes} bytes omitted`
|
|
2128
|
+
: ""
|
|
2129
|
+
}.]`
|
|
2130
|
+
: "";
|
|
2131
|
+
return [
|
|
2132
|
+
`completion ${update.completionId}`,
|
|
2133
|
+
`child=${readablePath} id=${update.childAgentId} turn=${update.turnId} stop=${update.stopReason}`,
|
|
2134
|
+
`${output}${truncation}`,
|
|
2135
|
+
].join("\n");
|
|
2136
|
+
});
|
|
2137
|
+
if (outcome.unreadUpdates > 0) {
|
|
2138
|
+
updates.push(
|
|
2139
|
+
`${outcome.unreadUpdates} additional completion update${outcome.unreadUpdates === 1 ? "" : "s"} remain unread`,
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
2142
|
+
return updates.join("\n\n");
|
|
2143
|
+
}
|
|
2144
|
+
|
|
751
2145
|
private resolveModel(parent: ParentRef, agent: AgentDefinition): Model<any> {
|
|
752
2146
|
if (!agent.model) {
|
|
753
2147
|
if (!parent.model) throw new Error("no parent model is selected for the subagent");
|
|
@@ -789,6 +2183,7 @@ export class SubagentCoordinator {
|
|
|
789
2183
|
descriptor.runtime.enableRunInBackground,
|
|
790
2184
|
descriptor.runtime.defaultBackground,
|
|
791
2185
|
agentDiscovery,
|
|
2186
|
+
descriptor.runtime.backgroundProtocol,
|
|
792
2187
|
);
|
|
793
2188
|
const model =
|
|
794
2189
|
options.parent.modelRuntime.getModel(descriptor.model.provider, descriptor.model.id) ??
|
|
@@ -801,6 +2196,14 @@ export class SubagentCoordinator {
|
|
|
801
2196
|
`cannot materialize subagent: model ${descriptor.model.provider}/${descriptor.model.id} is unavailable`,
|
|
802
2197
|
);
|
|
803
2198
|
}
|
|
2199
|
+
const extensionFactories: InlineExtension[] =
|
|
2200
|
+
descriptor.runtime.openAIIdentity && isOpenAIResponsesModel(model)
|
|
2201
|
+
? [
|
|
2202
|
+
await loadCodexIdentityInlineExtension(
|
|
2203
|
+
options.parent.sessionManager,
|
|
2204
|
+
),
|
|
2205
|
+
]
|
|
2206
|
+
: [];
|
|
804
2207
|
|
|
805
2208
|
const appendSystemPrompt = [
|
|
806
2209
|
descriptor.agent.systemPrompt,
|
|
@@ -836,10 +2239,13 @@ export class SubagentCoordinator {
|
|
|
836
2239
|
noExtensions: !descriptor.runtime.inheritExtensions,
|
|
837
2240
|
noThemes: true,
|
|
838
2241
|
appendSystemPrompt,
|
|
2242
|
+
extensionFactories,
|
|
839
2243
|
extensionsOverride: (base) => ({
|
|
840
2244
|
...base,
|
|
841
2245
|
extensions: base.extensions.filter(
|
|
842
|
-
(extension) =>
|
|
2246
|
+
(extension) =>
|
|
2247
|
+
extension.resolvedPath.startsWith("<inline:")
|
|
2248
|
+
|| !isPathInside(this.packageRoot, extension.resolvedPath),
|
|
843
2249
|
),
|
|
844
2250
|
}),
|
|
845
2251
|
},
|
|
@@ -898,6 +2304,15 @@ export class SubagentCoordinator {
|
|
|
898
2304
|
) {
|
|
899
2305
|
return false;
|
|
900
2306
|
}
|
|
2307
|
+
if (
|
|
2308
|
+
descriptor.runtime.backgroundProtocol !== "mailbox-v2"
|
|
2309
|
+
&& (
|
|
2310
|
+
tool === "followup_task"
|
|
2311
|
+
|| tool === "wait_agent"
|
|
2312
|
+
)
|
|
2313
|
+
) {
|
|
2314
|
+
return false;
|
|
2315
|
+
}
|
|
901
2316
|
return true;
|
|
902
2317
|
});
|
|
903
2318
|
runtime.session.setActiveToolsByName(activeTools);
|
|
@@ -935,26 +2350,41 @@ export class SubagentCoordinator {
|
|
|
935
2350
|
}
|
|
936
2351
|
|
|
937
2352
|
activation = {
|
|
938
|
-
|
|
939
|
-
|
|
2353
|
+
agentId: descriptor.agentId,
|
|
2354
|
+
runId: uuidv7(),
|
|
940
2355
|
descriptor,
|
|
941
2356
|
parent: options.parent,
|
|
942
2357
|
runtime,
|
|
943
2358
|
seedMessageCount: options.prepared.seedMessageCount,
|
|
944
2359
|
epochMessageStart: runtime.session.messages.length,
|
|
945
|
-
|
|
2360
|
+
controlState: createAgentControlState(),
|
|
946
2361
|
trace: [],
|
|
947
2362
|
streamedText: "",
|
|
948
2363
|
usage: emptyUsage(),
|
|
2364
|
+
startedTurnIds: new Set(),
|
|
2365
|
+
silentSettlementTurnIds: new Set(),
|
|
2366
|
+
pendingMailboxClaims: new Set(),
|
|
2367
|
+
userMessageGates: new Map(),
|
|
949
2368
|
ownedChildren: new Set(),
|
|
950
2369
|
onUpdate: options.onUpdate,
|
|
951
2370
|
published: false,
|
|
2371
|
+
everPublished: false,
|
|
952
2372
|
suppressSettlement: false,
|
|
953
2373
|
finalizing: false,
|
|
2374
|
+
holdsBackgroundSlot: false,
|
|
2375
|
+
persistenceGate: createPersistenceGate(
|
|
2376
|
+
runtime.session.sessionManager,
|
|
2377
|
+
options.isNew,
|
|
2378
|
+
),
|
|
2379
|
+
lastUsedSequence: ++this.activationSequence,
|
|
954
2380
|
disposed: false,
|
|
955
2381
|
};
|
|
2382
|
+
const existing = this.active.get(activation.agentId);
|
|
2383
|
+
if (existing && !existing.disposed) {
|
|
2384
|
+
throw new Error(`subagent ${activation.agentId} already has a resident runtime`);
|
|
2385
|
+
}
|
|
956
2386
|
activation.unsubscribe = runtime.session.subscribe((event) => this.observe(activation!, event));
|
|
957
|
-
this.active.set(activation.
|
|
2387
|
+
this.active.set(activation.agentId, activation);
|
|
958
2388
|
return activation;
|
|
959
2389
|
} catch (error) {
|
|
960
2390
|
await runtime.dispose().catch(() => {});
|
|
@@ -962,16 +2392,33 @@ export class SubagentCoordinator {
|
|
|
962
2392
|
}
|
|
963
2393
|
}
|
|
964
2394
|
|
|
2395
|
+
private async acquireBackgroundRun(
|
|
2396
|
+
activation: Activation,
|
|
2397
|
+
signal: AbortSignal | undefined,
|
|
2398
|
+
waitForCapacity: boolean,
|
|
2399
|
+
): Promise<BackgroundRunPermit | undefined> {
|
|
2400
|
+
if (activation.descriptor.mode !== "continuable") return undefined;
|
|
2401
|
+
const permit = await this.backgroundRuns.acquire({
|
|
2402
|
+
...(signal ? { signal } : {}),
|
|
2403
|
+
waitForCapacity,
|
|
2404
|
+
});
|
|
2405
|
+
activation.holdsBackgroundSlot = true;
|
|
2406
|
+
return permit;
|
|
2407
|
+
}
|
|
2408
|
+
|
|
965
2409
|
private startPrompt(
|
|
966
2410
|
activation: Activation,
|
|
967
2411
|
prompt: string,
|
|
968
2412
|
signal: AbortSignal | undefined,
|
|
969
|
-
|
|
970
|
-
): { accepted: Promise<void>; result: Promise<SubagentRunResult> } {
|
|
971
|
-
if (activation.currentRun) throw new Error(`subagent ${activation.
|
|
2413
|
+
options: StartPromptOptions,
|
|
2414
|
+
): { turnId: string; accepted: Promise<void>; result: Promise<SubagentRunResult> } {
|
|
2415
|
+
if (activation.currentRun) throw new Error(`subagent ${activation.agentId} is already running`);
|
|
972
2416
|
if (signal?.aborted) throw signal.reason ?? new Error("subagent start aborted");
|
|
2417
|
+
this.touchActivation(activation);
|
|
973
2418
|
activation.pendingSettlement = undefined;
|
|
974
|
-
activation
|
|
2419
|
+
this.resetTurnCapture(activation);
|
|
2420
|
+
const turnId = uuidv7();
|
|
2421
|
+
queueAgentTurn(activation.controlState, turnId);
|
|
975
2422
|
this.emitUpdate(activation);
|
|
976
2423
|
|
|
977
2424
|
let resolveAccepted!: () => void;
|
|
@@ -981,47 +2428,134 @@ export class SubagentCoordinator {
|
|
|
981
2428
|
resolveAccepted = resolvePromise;
|
|
982
2429
|
rejectAccepted = rejectPromise;
|
|
983
2430
|
});
|
|
2431
|
+
activation.currentMessageGate = accepted;
|
|
2432
|
+
const clearCurrentMessageGate = () => {
|
|
2433
|
+
if (activation.currentMessageGate === accepted) {
|
|
2434
|
+
activation.currentMessageGate = undefined;
|
|
2435
|
+
}
|
|
2436
|
+
};
|
|
2437
|
+
void accepted.then(clearCurrentMessageGate, clearCurrentMessageGate);
|
|
2438
|
+
const turnAbortController = new AbortController();
|
|
2439
|
+
activation.turnAbortController = turnAbortController;
|
|
984
2440
|
const abort = () => {
|
|
2441
|
+
turnAbortController.abort(
|
|
2442
|
+
signal?.reason ?? new Error(`subagent turn ${turnId} was aborted`),
|
|
2443
|
+
);
|
|
985
2444
|
void activation.runtime.session.abort().catch(() => {});
|
|
986
2445
|
};
|
|
987
2446
|
if (signal) signal.addEventListener("abort", abort, { once: true });
|
|
2447
|
+
let preflightSucceeded = false;
|
|
2448
|
+
const acceptPrompt = () => {
|
|
2449
|
+
if (acceptedSettled) return;
|
|
2450
|
+
if (turnAbortController.signal.aborted) {
|
|
2451
|
+
throw abortReason(turnAbortController.signal);
|
|
2452
|
+
}
|
|
2453
|
+
acceptedSettled = true;
|
|
2454
|
+
activation.userMessageGates.delete(turnId);
|
|
2455
|
+
this.publish(activation);
|
|
2456
|
+
if (options.detachAtAcceptance && signal) {
|
|
2457
|
+
signal.removeEventListener("abort", abort);
|
|
2458
|
+
}
|
|
2459
|
+
resolveAccepted();
|
|
2460
|
+
};
|
|
2461
|
+
const rejectPrompt = (error: Error) => {
|
|
2462
|
+
if (acceptedSettled) return;
|
|
2463
|
+
acceptedSettled = true;
|
|
2464
|
+
activation.userMessageGates.delete(turnId);
|
|
2465
|
+
activation.silentSettlementTurnIds.add(turnId);
|
|
2466
|
+
if (!activation.published && !activation.everPublished) {
|
|
2467
|
+
activation.suppressSettlement = true;
|
|
2468
|
+
}
|
|
2469
|
+
rejectAccepted(error);
|
|
2470
|
+
};
|
|
2471
|
+
if (options.acceptAfterUserMessage) {
|
|
2472
|
+
activation.userMessageGates.set(turnId, {
|
|
2473
|
+
prepare: () => {
|
|
2474
|
+
if (turnAbortController.signal.aborted) {
|
|
2475
|
+
throw abortReason(turnAbortController.signal);
|
|
2476
|
+
}
|
|
2477
|
+
options.onPromptAccepted?.(turnId);
|
|
2478
|
+
},
|
|
2479
|
+
accept: acceptPrompt,
|
|
2480
|
+
reject: rejectPrompt,
|
|
2481
|
+
});
|
|
2482
|
+
}
|
|
988
2483
|
|
|
989
2484
|
const core = (async (): Promise<SubagentRunResult> => {
|
|
2485
|
+
let permit: BackgroundRunPermit | undefined;
|
|
990
2486
|
try {
|
|
991
|
-
await
|
|
2487
|
+
permit = await this.acquireBackgroundRun(
|
|
2488
|
+
activation,
|
|
2489
|
+
turnAbortController.signal,
|
|
2490
|
+
options.waitForCapacity,
|
|
2491
|
+
);
|
|
2492
|
+
if (turnAbortController.signal.aborted) {
|
|
2493
|
+
throw abortReason(turnAbortController.signal);
|
|
2494
|
+
}
|
|
2495
|
+
const preparedPrompt = options.preparePrompt
|
|
2496
|
+
? options.preparePrompt(turnId)
|
|
2497
|
+
: prompt;
|
|
2498
|
+
startAgentTurn(activation.controlState, turnId);
|
|
2499
|
+
activation.startedTurnIds.add(turnId);
|
|
2500
|
+
this.emitTurnStart(activation, turnId);
|
|
2501
|
+
this.emitUpdate(activation);
|
|
2502
|
+
await activation.runtime.session.prompt(preparedPrompt, {
|
|
2503
|
+
...(options.preparePrompt
|
|
2504
|
+
? {
|
|
2505
|
+
expandPromptTemplates: false,
|
|
2506
|
+
source: "extension" as const,
|
|
2507
|
+
}
|
|
2508
|
+
: {}),
|
|
992
2509
|
preflightResult: (success) => {
|
|
993
2510
|
if (acceptedSettled) return;
|
|
994
|
-
acceptedSettled = true;
|
|
995
2511
|
if (success) {
|
|
996
|
-
|
|
997
|
-
if (
|
|
998
|
-
|
|
2512
|
+
preflightSucceeded = true;
|
|
2513
|
+
if (options.acceptAfterUserMessage) {
|
|
2514
|
+
return;
|
|
2515
|
+
}
|
|
2516
|
+
options.onPromptAccepted?.(turnId);
|
|
2517
|
+
acceptPrompt();
|
|
999
2518
|
} else {
|
|
1000
|
-
|
|
1001
|
-
|
|
2519
|
+
rejectPrompt(
|
|
2520
|
+
new Error("subagent prompt was rejected before acceptance"),
|
|
2521
|
+
);
|
|
1002
2522
|
}
|
|
1003
2523
|
},
|
|
1004
2524
|
});
|
|
1005
2525
|
if (!acceptedSettled) {
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
2526
|
+
if (options.acceptAfterUserMessage && preflightSucceeded) {
|
|
2527
|
+
throw new Error(
|
|
2528
|
+
"subagent mailbox prompt was handled before a user turn started",
|
|
2529
|
+
);
|
|
2530
|
+
}
|
|
2531
|
+
if (turnAbortController.signal.aborted) {
|
|
2532
|
+
throw abortReason(turnAbortController.signal);
|
|
2533
|
+
}
|
|
2534
|
+
options.onPromptAccepted?.(turnId);
|
|
2535
|
+
acceptPrompt();
|
|
1010
2536
|
}
|
|
1011
|
-
return this.collectResult(activation, "completed");
|
|
2537
|
+
return this.collectResult(activation, turnId, "completed");
|
|
1012
2538
|
} catch (error) {
|
|
1013
2539
|
activation.lastError = errorText(error);
|
|
1014
2540
|
if (!acceptedSettled) {
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
2541
|
+
rejectPrompt(
|
|
2542
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
2543
|
+
);
|
|
1018
2544
|
}
|
|
1019
|
-
const fallback = signal
|
|
1020
|
-
const result = this.collectResult(activation, fallback);
|
|
2545
|
+
const fallback = turnAbortController.signal.aborted ? "aborted" : "error";
|
|
2546
|
+
const result = this.collectResult(activation, turnId, fallback);
|
|
1021
2547
|
if (!result.output) result.output = activation.lastError;
|
|
1022
2548
|
return result;
|
|
1023
2549
|
} finally {
|
|
2550
|
+
activation.userMessageGates.delete(turnId);
|
|
1024
2551
|
if (signal) signal.removeEventListener("abort", abort);
|
|
2552
|
+
if (activation.turnAbortController === turnAbortController) {
|
|
2553
|
+
activation.turnAbortController = undefined;
|
|
2554
|
+
}
|
|
2555
|
+
if (permit) {
|
|
2556
|
+
activation.holdsBackgroundSlot = false;
|
|
2557
|
+
permit.release();
|
|
2558
|
+
}
|
|
1025
2559
|
}
|
|
1026
2560
|
})();
|
|
1027
2561
|
|
|
@@ -1033,7 +2567,7 @@ export class SubagentCoordinator {
|
|
|
1033
2567
|
});
|
|
1034
2568
|
activation.currentRun = lifecycle;
|
|
1035
2569
|
void lifecycle.catch(() => {});
|
|
1036
|
-
return { accepted, result: lifecycle };
|
|
2570
|
+
return { turnId, accepted, result: lifecycle };
|
|
1037
2571
|
}
|
|
1038
2572
|
|
|
1039
2573
|
private startInternalMessage(
|
|
@@ -1044,23 +2578,68 @@ export class SubagentCoordinator {
|
|
|
1044
2578
|
): void {
|
|
1045
2579
|
if (activation.currentRun || activation.disposed) return;
|
|
1046
2580
|
activation.pendingSettlement = undefined;
|
|
1047
|
-
activation
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
2581
|
+
this.resetTurnCapture(activation);
|
|
2582
|
+
const turnId = uuidv7();
|
|
2583
|
+
queueAgentTurn(activation.controlState, turnId);
|
|
2584
|
+
this.emitUpdate(activation);
|
|
2585
|
+
const turnAbortController = new AbortController();
|
|
2586
|
+
activation.turnAbortController = turnAbortController;
|
|
2587
|
+
let resolveMessageGate!: () => void;
|
|
2588
|
+
let rejectMessageGate!: (error: Error) => void;
|
|
2589
|
+
let messageGateSettled = false;
|
|
2590
|
+
const messageGate = new Promise<void>((resolvePromise, rejectPromise) => {
|
|
2591
|
+
resolveMessageGate = resolvePromise;
|
|
2592
|
+
rejectMessageGate = rejectPromise;
|
|
2593
|
+
});
|
|
2594
|
+
activation.currentMessageGate = messageGate;
|
|
2595
|
+
void messageGate.catch(() => {});
|
|
2596
|
+
const core = (async (): Promise<SubagentRunResult> => {
|
|
2597
|
+
let permit: BackgroundRunPermit | undefined;
|
|
2598
|
+
try {
|
|
2599
|
+
permit = await this.acquireBackgroundRun(
|
|
2600
|
+
activation,
|
|
2601
|
+
turnAbortController.signal,
|
|
2602
|
+
true,
|
|
2603
|
+
);
|
|
2604
|
+
startAgentTurn(activation.controlState, turnId);
|
|
2605
|
+
activation.startedTurnIds.add(turnId);
|
|
2606
|
+
this.emitTurnStart(activation, turnId);
|
|
2607
|
+
this.emitUpdate(activation);
|
|
2608
|
+
messageGateSettled = true;
|
|
2609
|
+
resolveMessageGate();
|
|
2610
|
+
await activation.runtime.session.sendCustomMessage(
|
|
1051
2611
|
{ customType, content, display: true, details },
|
|
1052
2612
|
{ triggerTurn: true, deliverAs: "followUp" },
|
|
1053
|
-
)
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
(
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
}
|
|
1063
|
-
|
|
2613
|
+
);
|
|
2614
|
+
return this.collectResult(activation, turnId, "completed");
|
|
2615
|
+
} catch (error) {
|
|
2616
|
+
activation.lastError = errorText(error);
|
|
2617
|
+
if (!messageGateSettled) {
|
|
2618
|
+
messageGateSettled = true;
|
|
2619
|
+
rejectMessageGate(
|
|
2620
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
2621
|
+
);
|
|
2622
|
+
}
|
|
2623
|
+
const result = this.collectResult(
|
|
2624
|
+
activation,
|
|
2625
|
+
turnId,
|
|
2626
|
+
turnAbortController.signal.aborted ? "aborted" : "error",
|
|
2627
|
+
);
|
|
2628
|
+
if (!result.output) result.output = activation.lastError;
|
|
2629
|
+
return result;
|
|
2630
|
+
} finally {
|
|
2631
|
+
if (activation.turnAbortController === turnAbortController) {
|
|
2632
|
+
activation.turnAbortController = undefined;
|
|
2633
|
+
}
|
|
2634
|
+
if (activation.currentMessageGate === messageGate) {
|
|
2635
|
+
activation.currentMessageGate = undefined;
|
|
2636
|
+
}
|
|
2637
|
+
if (permit) {
|
|
2638
|
+
activation.holdsBackgroundSlot = false;
|
|
2639
|
+
permit.release();
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
})();
|
|
1064
2643
|
let lifecycle!: Promise<SubagentRunResult>;
|
|
1065
2644
|
lifecycle = core.then(async (result) => {
|
|
1066
2645
|
if (activation.currentRun === lifecycle) activation.currentRun = undefined;
|
|
@@ -1072,8 +2651,66 @@ export class SubagentCoordinator {
|
|
|
1072
2651
|
}
|
|
1073
2652
|
|
|
1074
2653
|
private async runFinished(activation: Activation, result: SubagentRunResult): Promise<void> {
|
|
1075
|
-
activation.
|
|
2654
|
+
activation.pendingMailboxClaims.delete(result.turnId);
|
|
2655
|
+
activation.persistenceGate.reject(
|
|
2656
|
+
new Error(
|
|
2657
|
+
`subagent ${activation.agentId} ended before its session became durable`,
|
|
2658
|
+
),
|
|
2659
|
+
);
|
|
2660
|
+
finishAgentTurn(activation.controlState, result.turnId, result.stopReason);
|
|
2661
|
+
if (result.stopReason !== "completed") {
|
|
2662
|
+
activation.runtime.session.clearQueue();
|
|
2663
|
+
}
|
|
2664
|
+
const turnStarted = activation.startedTurnIds.delete(result.turnId);
|
|
2665
|
+
if (turnStarted) {
|
|
2666
|
+
this.emitTurnEnd(activation, result);
|
|
2667
|
+
}
|
|
1076
2668
|
activation.pendingSettlement = result;
|
|
2669
|
+
if (
|
|
2670
|
+
turnStarted
|
|
2671
|
+
&& !activation.silentSettlementTurnIds.has(result.turnId)
|
|
2672
|
+
&& activation.published
|
|
2673
|
+
&& activation.descriptor.mode === "continuable"
|
|
2674
|
+
&& activation.descriptor.runtime.backgroundProtocol === "mailbox-v2"
|
|
2675
|
+
) {
|
|
2676
|
+
try {
|
|
2677
|
+
appendCompletionUpdate(activation.parent.sessionManager, {
|
|
2678
|
+
parentAgentId: activation.descriptor.parentAgentId,
|
|
2679
|
+
childAgentId: activation.agentId,
|
|
2680
|
+
result,
|
|
2681
|
+
});
|
|
2682
|
+
} catch (error) {
|
|
2683
|
+
activation.lastError = errorText(error);
|
|
2684
|
+
let durableFallback = false;
|
|
2685
|
+
try {
|
|
2686
|
+
appendUndeliveredCompletion(
|
|
2687
|
+
activation.runtime.session.sessionManager,
|
|
2688
|
+
{
|
|
2689
|
+
parentAgentId:
|
|
2690
|
+
activation.descriptor.parentAgentId,
|
|
2691
|
+
childAgentId: activation.agentId,
|
|
2692
|
+
result,
|
|
2693
|
+
error: activation.lastError,
|
|
2694
|
+
},
|
|
2695
|
+
);
|
|
2696
|
+
durableFallback = true;
|
|
2697
|
+
} catch {
|
|
2698
|
+
// The explicit event below remains the final observable path
|
|
2699
|
+
// when both parent delivery and child fallback persistence fail.
|
|
2700
|
+
}
|
|
2701
|
+
this.pi.events.emit("pi-subagent:completion-error", {
|
|
2702
|
+
runId: activation.runId,
|
|
2703
|
+
turnId: result.turnId,
|
|
2704
|
+
agentId: activation.agentId,
|
|
2705
|
+
parentAgentId: activation.descriptor.parentAgentId,
|
|
2706
|
+
error: activation.lastError,
|
|
2707
|
+
durableFallback,
|
|
2708
|
+
});
|
|
2709
|
+
}
|
|
2710
|
+
this.completionWaiters
|
|
2711
|
+
.get(completionWaiterKey(activation.parent))
|
|
2712
|
+
?.wake();
|
|
2713
|
+
}
|
|
1077
2714
|
this.emitUpdate(activation, result);
|
|
1078
2715
|
if (activation.descriptor.mode === "one-shot") {
|
|
1079
2716
|
this.emitEnd(activation, result);
|
|
@@ -1081,7 +2718,6 @@ export class SubagentCoordinator {
|
|
|
1081
2718
|
}
|
|
1082
2719
|
if (activation.suppressSettlement || this.draining) return;
|
|
1083
2720
|
if (activation.ownedChildren.size > 0) {
|
|
1084
|
-
activation.status = "waiting";
|
|
1085
2721
|
this.emitUpdate(activation, result);
|
|
1086
2722
|
return;
|
|
1087
2723
|
}
|
|
@@ -1089,29 +2725,74 @@ export class SubagentCoordinator {
|
|
|
1089
2725
|
}
|
|
1090
2726
|
|
|
1091
2727
|
private async finalizeContinuable(activation: Activation): Promise<void> {
|
|
1092
|
-
|
|
2728
|
+
const retained = await this.agentOperations.run(activation.agentId, () =>
|
|
2729
|
+
this.finalizeContinuableLocked(activation),
|
|
2730
|
+
);
|
|
2731
|
+
if (retained) {
|
|
2732
|
+
await this.trimIdleRuntimes(activation.agentId);
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
private async finalizeContinuableLocked(activation: Activation): Promise<boolean> {
|
|
2737
|
+
if (activation.finalizing || activation.disposed || activation.currentRun) {
|
|
2738
|
+
return false;
|
|
2739
|
+
}
|
|
1093
2740
|
const result = activation.pendingSettlement;
|
|
1094
|
-
if (!result || activation.ownedChildren.size > 0) return;
|
|
2741
|
+
if (!result || activation.ownedChildren.size > 0) return false;
|
|
1095
2742
|
activation.finalizing = true;
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
2743
|
+
const finalizePromise = (async (): Promise<boolean> => {
|
|
2744
|
+
let retained = false;
|
|
2745
|
+
try {
|
|
2746
|
+
const silentSettlement =
|
|
2747
|
+
activation.silentSettlementTurnIds.has(result.turnId);
|
|
2748
|
+
if (
|
|
2749
|
+
activation.descriptor.runtime.backgroundProtocol !== "mailbox-v2"
|
|
2750
|
+
&& !silentSettlement
|
|
2751
|
+
&& !activation.suppressSettlement
|
|
2752
|
+
&& !this.draining
|
|
2753
|
+
) {
|
|
2754
|
+
await this.deliverSettlement(activation, result).catch((error) => {
|
|
2755
|
+
activation.lastError = errorText(error);
|
|
2756
|
+
});
|
|
2757
|
+
}
|
|
2758
|
+
this.emitEnd(activation, result);
|
|
2759
|
+
activation.silentSettlementTurnIds.delete(result.turnId);
|
|
2760
|
+
activation.pendingSettlement = undefined;
|
|
2761
|
+
if (this.shouldRetainIdleRuntime(activation)) {
|
|
2762
|
+
activation.published = false;
|
|
2763
|
+
activation.runId = uuidv7();
|
|
2764
|
+
await this.releaseParentOwnership(activation);
|
|
2765
|
+
this.touchActivation(activation);
|
|
2766
|
+
retained = true;
|
|
2767
|
+
} else {
|
|
2768
|
+
await this.disposeActivation(activation);
|
|
2769
|
+
await this.releaseParentOwnership(activation);
|
|
2770
|
+
}
|
|
2771
|
+
} finally {
|
|
2772
|
+
if (!activation.disposed) {
|
|
2773
|
+
activation.finalizing = false;
|
|
2774
|
+
}
|
|
1101
2775
|
}
|
|
1102
|
-
|
|
1103
|
-
await this.disposeActivation(activation);
|
|
1104
|
-
await this.releaseParentOwnership(activation);
|
|
2776
|
+
return retained;
|
|
1105
2777
|
})();
|
|
1106
|
-
|
|
2778
|
+
activation.finalizePromise = finalizePromise;
|
|
2779
|
+
try {
|
|
2780
|
+
return await finalizePromise;
|
|
2781
|
+
} finally {
|
|
2782
|
+
if (activation.finalizePromise === finalizePromise) {
|
|
2783
|
+
activation.finalizePromise = undefined;
|
|
2784
|
+
}
|
|
2785
|
+
if (!activation.disposed) activation.finalizing = false;
|
|
2786
|
+
}
|
|
1107
2787
|
}
|
|
1108
2788
|
|
|
1109
2789
|
private async deliverSettlement(activation: Activation, result: SubagentRunResult): Promise<void> {
|
|
2790
|
+
const readablePath = descriptorTaskPath(activation.descriptor);
|
|
1110
2791
|
const truncated = truncateUtf8(result.output, activation.descriptor.runtime.maxOutputBytes);
|
|
1111
2792
|
const closing = truncated.text.trim()
|
|
1112
2793
|
? `Its closing message:\n\n${truncated.text}`
|
|
1113
2794
|
: "It left no closing message.";
|
|
1114
|
-
const content = `Background subagent ${activation.
|
|
2795
|
+
const content = `Background subagent ${readablePath} (${activation.agentId}) ${stopReasonHeadline(result.stopReason)} and will do no further work unless you send it more.\n\n${closing}${
|
|
1115
2796
|
truncated.truncated ? `\n\n[Closing message truncated; ${truncated.omittedBytes} bytes omitted.]` : ""
|
|
1116
2797
|
}`;
|
|
1117
2798
|
await activation.parent.deliver(
|
|
@@ -1119,7 +2800,8 @@ export class SubagentCoordinator {
|
|
|
1119
2800
|
content,
|
|
1120
2801
|
{
|
|
1121
2802
|
kind: "settled",
|
|
1122
|
-
|
|
2803
|
+
childAgentId: activation.agentId,
|
|
2804
|
+
taskPath: readablePath,
|
|
1123
2805
|
label: activation.descriptor.label,
|
|
1124
2806
|
stopReason: result.stopReason,
|
|
1125
2807
|
...(truncated.truncated ? { truncated: true } : {}),
|
|
@@ -1130,7 +2812,8 @@ export class SubagentCoordinator {
|
|
|
1130
2812
|
|
|
1131
2813
|
private parentForActivation(activation: Activation): ParentRef {
|
|
1132
2814
|
return {
|
|
1133
|
-
|
|
2815
|
+
agentId: activation.agentId,
|
|
2816
|
+
taskPath: descriptorTaskPath(activation.descriptor),
|
|
1134
2817
|
depth: activation.descriptor.depth,
|
|
1135
2818
|
cwd: activation.descriptor.cwd,
|
|
1136
2819
|
sessionManager: activation.runtime.session.sessionManager,
|
|
@@ -1140,7 +2823,7 @@ export class SubagentCoordinator {
|
|
|
1140
2823
|
projectTrusted: activation.parent.projectTrusted,
|
|
1141
2824
|
activation,
|
|
1142
2825
|
deliver: async (customType, content, details, delivery) => {
|
|
1143
|
-
if (activation.disposed) throw new Error(`parent subagent ${activation.
|
|
2826
|
+
if (activation.disposed) throw new Error(`parent subagent ${activation.agentId} is no longer resident`);
|
|
1144
2827
|
const session = activation.runtime.session;
|
|
1145
2828
|
if (delivery === "quiet") {
|
|
1146
2829
|
await session.sendCustomMessage(
|
|
@@ -1163,20 +2846,38 @@ export class SubagentCoordinator {
|
|
|
1163
2846
|
};
|
|
1164
2847
|
}
|
|
1165
2848
|
|
|
1166
|
-
private
|
|
2849
|
+
private resetTurnCapture(activation: Activation): void {
|
|
2850
|
+
activation.epochMessageStart = activation.runtime.session.messages.length;
|
|
2851
|
+
activation.streamedText = "";
|
|
2852
|
+
activation.usage = emptyUsage();
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
private collectResult(
|
|
2856
|
+
activation: Activation,
|
|
2857
|
+
turnId: string,
|
|
2858
|
+
fallback: SubagentStopReason,
|
|
2859
|
+
): SubagentRunResult {
|
|
1167
2860
|
const messages = activation.runtime.session.messages;
|
|
1168
2861
|
const output = finalAssistantText(messages, activation.epochMessageStart, activation.streamedText);
|
|
1169
2862
|
const stopReason = finalStopReason(messages, activation.epochMessageStart, fallback);
|
|
1170
2863
|
const truncated = truncateUtf8(output, activation.descriptor.runtime.maxOutputBytes);
|
|
1171
2864
|
const sessionFile = activation.runtime.session.sessionFile;
|
|
1172
2865
|
return {
|
|
1173
|
-
|
|
2866
|
+
agentId: activation.agentId,
|
|
2867
|
+
turnId,
|
|
2868
|
+
piSessionId: activation.runtime.session.sessionId,
|
|
1174
2869
|
...(sessionFile ? { sessionFile } : {}),
|
|
1175
2870
|
output: truncated.truncated
|
|
1176
2871
|
? `${truncated.text}\n\n[Output truncated; ${truncated.omittedBytes} bytes omitted.${
|
|
1177
2872
|
sessionFile ? ` Full output: ${sessionFile}` : " Full output remains in the active child session."
|
|
1178
2873
|
}]`
|
|
1179
2874
|
: truncated.text,
|
|
2875
|
+
...(truncated.truncated
|
|
2876
|
+
? {
|
|
2877
|
+
outputTruncated: true,
|
|
2878
|
+
omittedBytes: truncated.omittedBytes,
|
|
2879
|
+
}
|
|
2880
|
+
: {}),
|
|
1180
2881
|
stopReason,
|
|
1181
2882
|
usage: structuredClone(activation.usage),
|
|
1182
2883
|
};
|
|
@@ -1197,12 +2898,47 @@ export class SubagentCoordinator {
|
|
|
1197
2898
|
this.emitUpdate(activation);
|
|
1198
2899
|
return;
|
|
1199
2900
|
}
|
|
2901
|
+
if (event.type === "agent_end") {
|
|
2902
|
+
void this.releaseWaitAgentDeliveries(
|
|
2903
|
+
this.parentForActivation(activation),
|
|
2904
|
+
"parent agent turn ended without a durable wait_agent result",
|
|
2905
|
+
).catch((error) => {
|
|
2906
|
+
activation.lastError = errorText(error);
|
|
2907
|
+
});
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
1200
2910
|
if (event.type !== "message_end") return;
|
|
2911
|
+
if (event.message.role === "user") {
|
|
2912
|
+
const turnId = currentAgentTurnId(activation.controlState);
|
|
2913
|
+
const gate = turnId
|
|
2914
|
+
? activation.userMessageGates.get(turnId)
|
|
2915
|
+
: undefined;
|
|
2916
|
+
if (turnId && gate) {
|
|
2917
|
+
try {
|
|
2918
|
+
gate.prepare();
|
|
2919
|
+
commitMailboxClaim(
|
|
2920
|
+
activation.runtime.session.sessionManager,
|
|
2921
|
+
turnId,
|
|
2922
|
+
event.message,
|
|
2923
|
+
);
|
|
2924
|
+
activation.pendingMailboxClaims.delete(turnId);
|
|
2925
|
+
gate.accept();
|
|
2926
|
+
} catch (error) {
|
|
2927
|
+
const failure =
|
|
2928
|
+
error instanceof Error ? error : new Error(String(error));
|
|
2929
|
+
activation.lastError = failure.message;
|
|
2930
|
+
gate.reject(failure);
|
|
2931
|
+
void activation.runtime.session.abort().catch(() => {});
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
return;
|
|
2935
|
+
}
|
|
1201
2936
|
if (event.message.role === "toolResult") {
|
|
1202
2937
|
if (event.message.usage) addUsage(activation.usage, event.message.usage, false);
|
|
1203
2938
|
return;
|
|
1204
2939
|
}
|
|
1205
2940
|
if (event.message.role !== "assistant") return;
|
|
2941
|
+
activation.persistenceGate.resolve();
|
|
1206
2942
|
addUsage(activation.usage, event.message.usage);
|
|
1207
2943
|
const text = event.message.content
|
|
1208
2944
|
.filter((part): part is Extract<(typeof event.message.content)[number], { type: "text" }> => part.type === "text")
|
|
@@ -1223,23 +2959,57 @@ export class SubagentCoordinator {
|
|
|
1223
2959
|
private publish(activation: Activation): void {
|
|
1224
2960
|
if (activation.published) return;
|
|
1225
2961
|
activation.published = true;
|
|
2962
|
+
activation.everPublished = true;
|
|
1226
2963
|
this.pi.events.emit("pi-subagent:start", {
|
|
1227
|
-
runId: activation.
|
|
1228
|
-
|
|
2964
|
+
runId: activation.runId,
|
|
2965
|
+
agentId: activation.agentId,
|
|
2966
|
+
taskPath: descriptorTaskPath(activation.descriptor),
|
|
2967
|
+
piSessionId: activation.runtime.session.sessionId,
|
|
2968
|
+
parentAgentId: activation.descriptor.parentAgentId,
|
|
1229
2969
|
provider: activation.descriptor.provider,
|
|
1230
2970
|
mode: activation.descriptor.mode,
|
|
1231
|
-
|
|
2971
|
+
context: descriptorContext(activation.descriptor),
|
|
1232
2972
|
});
|
|
1233
2973
|
}
|
|
1234
2974
|
|
|
1235
2975
|
private emitEnd(activation: Activation, result: SubagentRunResult): void {
|
|
1236
2976
|
if (!activation.published) return;
|
|
1237
2977
|
this.pi.events.emit("pi-subagent:end", {
|
|
1238
|
-
runId: activation.
|
|
1239
|
-
|
|
2978
|
+
runId: activation.runId,
|
|
2979
|
+
agentId: activation.agentId,
|
|
2980
|
+
taskPath: descriptorTaskPath(activation.descriptor),
|
|
2981
|
+
piSessionId: activation.runtime.session.sessionId,
|
|
2982
|
+
parentAgentId: activation.descriptor.parentAgentId,
|
|
2983
|
+
provider: activation.descriptor.provider,
|
|
2984
|
+
mode: activation.descriptor.mode,
|
|
2985
|
+
stopReason: result.stopReason,
|
|
2986
|
+
output: result.output,
|
|
2987
|
+
});
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
private emitTurnStart(activation: Activation, turnId: string): void {
|
|
2991
|
+
this.pi.events.emit("pi-subagent:turn-start", {
|
|
2992
|
+
runId: activation.runId,
|
|
2993
|
+
turnId,
|
|
2994
|
+
agentId: activation.agentId,
|
|
2995
|
+
taskPath: descriptorTaskPath(activation.descriptor),
|
|
2996
|
+
piSessionId: activation.runtime.session.sessionId,
|
|
2997
|
+
parentAgentId: activation.descriptor.parentAgentId,
|
|
2998
|
+
provider: activation.descriptor.provider,
|
|
2999
|
+
mode: activation.descriptor.mode,
|
|
3000
|
+
});
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
private emitTurnEnd(activation: Activation, result: SubagentRunResult): void {
|
|
3004
|
+
this.pi.events.emit("pi-subagent:turn-end", {
|
|
3005
|
+
runId: activation.runId,
|
|
3006
|
+
turnId: result.turnId,
|
|
3007
|
+
agentId: activation.agentId,
|
|
3008
|
+
taskPath: descriptorTaskPath(activation.descriptor),
|
|
3009
|
+
piSessionId: activation.runtime.session.sessionId,
|
|
3010
|
+
parentAgentId: activation.descriptor.parentAgentId,
|
|
1240
3011
|
provider: activation.descriptor.provider,
|
|
1241
3012
|
mode: activation.descriptor.mode,
|
|
1242
|
-
parentId: activation.descriptor.parentSessionId,
|
|
1243
3013
|
stopReason: result.stopReason,
|
|
1244
3014
|
output: result.output,
|
|
1245
3015
|
});
|
|
@@ -1248,13 +3018,22 @@ export class SubagentCoordinator {
|
|
|
1248
3018
|
private detailsOf(activation: Activation, result?: SubagentRunResult): DelegationDetails {
|
|
1249
3019
|
return {
|
|
1250
3020
|
kind: "delegation",
|
|
1251
|
-
|
|
3021
|
+
agentId: activation.agentId,
|
|
3022
|
+
taskPath: descriptorTaskPath(activation.descriptor),
|
|
3023
|
+
...(currentAgentTurnId(activation.controlState)
|
|
3024
|
+
? { turnId: currentAgentTurnId(activation.controlState) }
|
|
3025
|
+
: {}),
|
|
3026
|
+
piSessionId: activation.runtime.session.sessionId,
|
|
1252
3027
|
provider: activation.descriptor.provider,
|
|
1253
3028
|
mode: activation.descriptor.mode,
|
|
3029
|
+
context: descriptorContext(activation.descriptor),
|
|
1254
3030
|
agent: activation.descriptor.agent.name,
|
|
1255
3031
|
label: activation.descriptor.label,
|
|
1256
3032
|
depth: activation.descriptor.depth,
|
|
1257
|
-
status:
|
|
3033
|
+
status: delegationStatus(
|
|
3034
|
+
activation.controlState,
|
|
3035
|
+
activation.ownedChildren.size > 0,
|
|
3036
|
+
),
|
|
1258
3037
|
...(activation.runtime.session.sessionFile
|
|
1259
3038
|
? { sessionFile: activation.runtime.session.sessionFile }
|
|
1260
3039
|
: {}),
|
|
@@ -1296,36 +3075,435 @@ export class SubagentCoordinator {
|
|
|
1296
3075
|
private async disposeActivation(activation: Activation): Promise<void> {
|
|
1297
3076
|
if (activation.disposed) return;
|
|
1298
3077
|
activation.disposed = true;
|
|
3078
|
+
const waiterKey = completionWaiterKey(
|
|
3079
|
+
this.parentForActivation(activation),
|
|
3080
|
+
);
|
|
3081
|
+
const waiter = this.completionWaiters.get(waiterKey);
|
|
3082
|
+
if (waiter) {
|
|
3083
|
+
this.removeCompletionWaiter(waiterKey, waiter);
|
|
3084
|
+
waiter.reject(
|
|
3085
|
+
new Error(
|
|
3086
|
+
`parent subagent ${activation.agentId} was disposed while wait_agent was pending`,
|
|
3087
|
+
),
|
|
3088
|
+
);
|
|
3089
|
+
}
|
|
3090
|
+
activation.persistenceGate.reject(
|
|
3091
|
+
new Error(
|
|
3092
|
+
`subagent ${activation.agentId} ended before its session became durable`,
|
|
3093
|
+
),
|
|
3094
|
+
);
|
|
3095
|
+
setAgentResidency(activation.controlState, "unloaded");
|
|
3096
|
+
if (activation.descriptor.mode === "one-shot") closeAgent(activation.controlState);
|
|
1299
3097
|
activation.unsubscribe?.();
|
|
1300
3098
|
activation.unsubscribe = undefined;
|
|
1301
3099
|
if (!activation.runtime.session.isIdle) await activation.runtime.session.abort().catch(() => {});
|
|
1302
3100
|
try {
|
|
1303
3101
|
await activation.runtime.dispose();
|
|
1304
3102
|
} finally {
|
|
1305
|
-
if (this.active.get(activation.
|
|
3103
|
+
if (this.active.get(activation.agentId) === activation) this.active.delete(activation.agentId);
|
|
1306
3104
|
}
|
|
1307
3105
|
}
|
|
1308
3106
|
|
|
1309
3107
|
private async releaseParentOwnership(activation: Activation): Promise<void> {
|
|
1310
|
-
const owner = activation.
|
|
3108
|
+
const owner = activation.ownerActivation;
|
|
1311
3109
|
if (!owner) return;
|
|
1312
|
-
|
|
3110
|
+
activation.ownerActivation = undefined;
|
|
3111
|
+
owner.ownedChildren.delete(activation.agentId);
|
|
1313
3112
|
if (
|
|
1314
3113
|
!owner.currentRun &&
|
|
1315
3114
|
owner.ownedChildren.size === 0 &&
|
|
1316
3115
|
owner.pendingSettlement &&
|
|
1317
3116
|
owner.descriptor.mode === "continuable"
|
|
1318
3117
|
) {
|
|
1319
|
-
|
|
3118
|
+
void this.finalizeContinuable(owner).catch((error) => {
|
|
3119
|
+
owner.lastError = errorText(error);
|
|
3120
|
+
});
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
private acquireParentOwnership(
|
|
3125
|
+
activation: Activation,
|
|
3126
|
+
parent: ParentRef,
|
|
3127
|
+
): void {
|
|
3128
|
+
if (
|
|
3129
|
+
activation.ownerActivation
|
|
3130
|
+
&& activation.ownerActivation !== parent.activation
|
|
3131
|
+
) {
|
|
3132
|
+
throw new Error(
|
|
3133
|
+
`subagent ${descriptorTaskPath(activation.descriptor)} is still owned by another resident parent`,
|
|
3134
|
+
);
|
|
3135
|
+
}
|
|
3136
|
+
activation.parent = parent;
|
|
3137
|
+
if (!parent.activation) return;
|
|
3138
|
+
parent.activation.ownedChildren.add(activation.agentId);
|
|
3139
|
+
activation.ownerActivation = parent.activation;
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
private touchActivation(activation: Activation): void {
|
|
3143
|
+
activation.lastUsedSequence = ++this.activationSequence;
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
private shouldRetainIdleRuntime(activation: Activation): boolean {
|
|
3147
|
+
return (
|
|
3148
|
+
this.idleRuntimeLimit > 0
|
|
3149
|
+
&& !this.draining
|
|
3150
|
+
&& activation.descriptor.mode === "continuable"
|
|
3151
|
+
&& !activation.disposed
|
|
3152
|
+
&& !activation.currentRun
|
|
3153
|
+
&& activation.runtime.session.isIdle
|
|
3154
|
+
&& activation.ownedChildren.size === 0
|
|
3155
|
+
&& activation.pendingMailboxClaims.size === 0
|
|
3156
|
+
&& activation.persistenceGate.state !== "rejected"
|
|
3157
|
+
);
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
private isEvictableIdleRuntime(activation: Activation): boolean {
|
|
3161
|
+
return (
|
|
3162
|
+
!activation.disposed
|
|
3163
|
+
&& !activation.finalizing
|
|
3164
|
+
&& activation.descriptor.mode === "continuable"
|
|
3165
|
+
&& !activation.currentRun
|
|
3166
|
+
&& !activation.pendingSettlement
|
|
3167
|
+
&& activation.runtime.session.isIdle
|
|
3168
|
+
&& activation.ownedChildren.size === 0
|
|
3169
|
+
&& activation.pendingMailboxClaims.size === 0
|
|
3170
|
+
&& !activation.ownerActivation
|
|
3171
|
+
);
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
private async trimIdleRuntimes(protectedAgentId?: string): Promise<void> {
|
|
3175
|
+
await this.idleRuntimeOperations.run("idle-runtime-lru", async () =>
|
|
3176
|
+
this.trimIdleRuntimesLocked(protectedAgentId),
|
|
3177
|
+
);
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
private async trimIdleRuntimesLocked(
|
|
3181
|
+
protectedAgentId?: string,
|
|
3182
|
+
): Promise<void> {
|
|
3183
|
+
while (true) {
|
|
3184
|
+
const retained = [...this.active.values()].filter(
|
|
3185
|
+
(activation) =>
|
|
3186
|
+
activation.agentId === protectedAgentId
|
|
3187
|
+
? this.shouldRetainIdleRuntime(activation)
|
|
3188
|
+
: this.isEvictableIdleRuntime(activation),
|
|
3189
|
+
);
|
|
3190
|
+
if (retained.length <= this.idleRuntimeLimit) return;
|
|
3191
|
+
const candidate = retained
|
|
3192
|
+
.filter(
|
|
3193
|
+
(activation) =>
|
|
3194
|
+
activation.agentId !== protectedAgentId
|
|
3195
|
+
&& this.isEvictableIdleRuntime(activation),
|
|
3196
|
+
)
|
|
3197
|
+
.sort(
|
|
3198
|
+
(left, right) =>
|
|
3199
|
+
left.lastUsedSequence - right.lastUsedSequence
|
|
3200
|
+
|| left.agentId.localeCompare(right.agentId),
|
|
3201
|
+
)[0];
|
|
3202
|
+
if (!candidate) return;
|
|
3203
|
+
await this.agentOperations.run(candidate.agentId, async () => {
|
|
3204
|
+
if (
|
|
3205
|
+
this.active.get(candidate.agentId) === candidate
|
|
3206
|
+
&& this.isEvictableIdleRuntime(candidate)
|
|
3207
|
+
) {
|
|
3208
|
+
await this.disposeActivation(candidate);
|
|
3209
|
+
}
|
|
3210
|
+
});
|
|
1320
3211
|
}
|
|
1321
3212
|
}
|
|
1322
3213
|
|
|
1323
3214
|
private assertDirectParent(parent: ParentRef, descriptor: SubagentDescriptor): void {
|
|
1324
|
-
if (descriptor.
|
|
3215
|
+
if (descriptor.parentAgentId !== parent.agentId) {
|
|
3216
|
+
throw new Error(
|
|
3217
|
+
`subagent ${descriptor.label} is not a direct child of ${parent.agentId}`,
|
|
3218
|
+
);
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
private assertContinuableDirectChild(
|
|
3223
|
+
parent: ParentRef,
|
|
3224
|
+
descriptor: SubagentDescriptor,
|
|
3225
|
+
): void {
|
|
3226
|
+
if (descriptor.mode !== "continuable") {
|
|
3227
|
+
throw new Error(
|
|
3228
|
+
`subagent ${descriptor.agentId} is one-shot and cannot accept follow-up work`,
|
|
3229
|
+
);
|
|
3230
|
+
}
|
|
3231
|
+
this.assertDirectParent(parent, descriptor);
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
private treeRootAgentId(
|
|
3235
|
+
agentId: string,
|
|
3236
|
+
byId: ReadonlyMap<string, CatalogRecord>,
|
|
3237
|
+
): string {
|
|
3238
|
+
let current = agentId;
|
|
3239
|
+
const visited = new Set<string>();
|
|
3240
|
+
while (true) {
|
|
3241
|
+
if (visited.has(current)) {
|
|
3242
|
+
throw new Error("subagent descriptor lineage contains a cycle");
|
|
3243
|
+
}
|
|
3244
|
+
visited.add(current);
|
|
3245
|
+
const record = byId.get(current);
|
|
3246
|
+
if (!record) return current;
|
|
3247
|
+
current = record.descriptor.parentAgentId;
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
private async reserveTask(
|
|
3252
|
+
parent: ParentRef,
|
|
3253
|
+
requestedName: string | undefined,
|
|
3254
|
+
label: string,
|
|
3255
|
+
): Promise<ReservedTask> {
|
|
3256
|
+
const catalog = await this.catalogRecords(parent);
|
|
3257
|
+
const usedPaths = new Set(
|
|
3258
|
+
catalog.records
|
|
3259
|
+
.filter(
|
|
3260
|
+
(record) =>
|
|
3261
|
+
record.descriptor.parentAgentId === parent.agentId,
|
|
3262
|
+
)
|
|
3263
|
+
.map((record) => record.taskPath),
|
|
3264
|
+
);
|
|
3265
|
+
const reserve = (name: string): ReservedTask | undefined => {
|
|
3266
|
+
const path = taskPath(parent.taskPath, name);
|
|
3267
|
+
const reservationKey = `${parent.agentId}:${path}`;
|
|
3268
|
+
if (
|
|
3269
|
+
usedPaths.has(path)
|
|
3270
|
+
|| this.reservedTaskPaths.has(reservationKey)
|
|
3271
|
+
) {
|
|
3272
|
+
return undefined;
|
|
3273
|
+
}
|
|
3274
|
+
this.reservedTaskPaths.add(reservationKey);
|
|
3275
|
+
let released = false;
|
|
3276
|
+
return {
|
|
3277
|
+
name,
|
|
3278
|
+
path,
|
|
3279
|
+
release: () => {
|
|
3280
|
+
if (released) return;
|
|
3281
|
+
released = true;
|
|
3282
|
+
this.reservedTaskPaths.delete(reservationKey);
|
|
3283
|
+
},
|
|
3284
|
+
};
|
|
3285
|
+
};
|
|
3286
|
+
|
|
3287
|
+
if (requestedName !== undefined) {
|
|
3288
|
+
const name = validateTaskName(requestedName);
|
|
3289
|
+
const reserved = reserve(name);
|
|
3290
|
+
if (!reserved) {
|
|
3291
|
+
throw new Error(
|
|
3292
|
+
`task path ${taskPath(parent.taskPath, name)} is already in use`,
|
|
3293
|
+
);
|
|
3294
|
+
}
|
|
3295
|
+
return reserved;
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
const base = slugTaskName(label);
|
|
3299
|
+
const initial = reserve(base);
|
|
3300
|
+
if (initial) return initial;
|
|
3301
|
+
for (let ordinal = 2; ordinal < Number.MAX_SAFE_INTEGER; ordinal++) {
|
|
3302
|
+
const candidate = reserve(numberedTaskName(base, ordinal));
|
|
3303
|
+
if (candidate) return candidate;
|
|
3304
|
+
}
|
|
3305
|
+
throw new Error(`could not allocate a readable child path under ${parent.taskPath}`);
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
private async resolveTarget(
|
|
3309
|
+
parent: ParentRef,
|
|
3310
|
+
target: string,
|
|
3311
|
+
options: { allowUnknownId?: boolean } = {},
|
|
3312
|
+
): Promise<ResolvedTarget> {
|
|
3313
|
+
const catalog = await this.catalogRecords(parent);
|
|
3314
|
+
const byId = new Map(
|
|
3315
|
+
catalog.records.map((record) => [record.agentId, record]),
|
|
3316
|
+
);
|
|
3317
|
+
if (isAgentId(target)) {
|
|
3318
|
+
const record = byId.get(target);
|
|
3319
|
+
if (record) {
|
|
3320
|
+
return {
|
|
3321
|
+
agentId: record.agentId,
|
|
3322
|
+
taskPath: record.taskPath,
|
|
3323
|
+
record,
|
|
3324
|
+
};
|
|
3325
|
+
}
|
|
3326
|
+
if (options.allowUnknownId) {
|
|
3327
|
+
return {
|
|
3328
|
+
agentId: target,
|
|
3329
|
+
taskPath: target,
|
|
3330
|
+
};
|
|
3331
|
+
}
|
|
3332
|
+
return {
|
|
3333
|
+
agentId: target,
|
|
3334
|
+
taskPath: target,
|
|
3335
|
+
};
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
const path = resolveTaskPath(parent.taskPath, target);
|
|
3339
|
+
const rootAgentId = this.treeRootAgentId(parent.agentId, byId);
|
|
3340
|
+
const matches = this.reachableTreeRecords(rootAgentId, byId).filter(
|
|
3341
|
+
(record) => record.taskPath === path,
|
|
3342
|
+
);
|
|
3343
|
+
if (matches.length === 0) {
|
|
3344
|
+
throw new Error(`unknown subagent task path: ${path}`);
|
|
3345
|
+
}
|
|
3346
|
+
if (matches.length > 1) {
|
|
1325
3347
|
throw new Error(
|
|
1326
|
-
`subagent ${
|
|
3348
|
+
`ambiguous subagent task path ${path}; use a durable agent id`,
|
|
1327
3349
|
);
|
|
1328
3350
|
}
|
|
3351
|
+
const record = matches[0]!;
|
|
3352
|
+
return {
|
|
3353
|
+
agentId: record.agentId,
|
|
3354
|
+
taskPath: record.taskPath,
|
|
3355
|
+
record,
|
|
3356
|
+
};
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
private reachableTreeRecords(
|
|
3360
|
+
rootAgentId: string,
|
|
3361
|
+
byId: ReadonlyMap<string, CatalogRecord>,
|
|
3362
|
+
): CatalogRecord[] {
|
|
3363
|
+
const paths = new Map<string, string>([
|
|
3364
|
+
[rootAgentId, ROOT_TASK_PATH],
|
|
3365
|
+
]);
|
|
3366
|
+
const depths = new Map<string, number>([[rootAgentId, 0]]);
|
|
3367
|
+
const reachable: CatalogRecord[] = [];
|
|
3368
|
+
const pending = new Set(byId.values());
|
|
3369
|
+
let progressed = true;
|
|
3370
|
+
while (progressed) {
|
|
3371
|
+
progressed = false;
|
|
3372
|
+
for (const record of [...pending]) {
|
|
3373
|
+
const parentPath = paths.get(
|
|
3374
|
+
record.descriptor.parentAgentId,
|
|
3375
|
+
);
|
|
3376
|
+
const parentDepth = depths.get(
|
|
3377
|
+
record.descriptor.parentAgentId,
|
|
3378
|
+
);
|
|
3379
|
+
if (parentPath === undefined || parentDepth === undefined) {
|
|
3380
|
+
continue;
|
|
3381
|
+
}
|
|
3382
|
+
if (record.descriptor.version === 3) {
|
|
3383
|
+
let expectedPath: string;
|
|
3384
|
+
try {
|
|
3385
|
+
expectedPath = taskPath(
|
|
3386
|
+
parentPath,
|
|
3387
|
+
record.descriptor.task.name,
|
|
3388
|
+
);
|
|
3389
|
+
} catch {
|
|
3390
|
+
pending.delete(record);
|
|
3391
|
+
progressed = true;
|
|
3392
|
+
continue;
|
|
3393
|
+
}
|
|
3394
|
+
if (
|
|
3395
|
+
record.taskPath !== expectedPath
|
|
3396
|
+
|| record.descriptor.depth !== parentDepth + 1
|
|
3397
|
+
) {
|
|
3398
|
+
pending.delete(record);
|
|
3399
|
+
progressed = true;
|
|
3400
|
+
continue;
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
pending.delete(record);
|
|
3404
|
+
reachable.push(record);
|
|
3405
|
+
paths.set(record.agentId, record.taskPath);
|
|
3406
|
+
depths.set(record.agentId, record.descriptor.depth);
|
|
3407
|
+
progressed = true;
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
return reachable;
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3413
|
+
private catalogTopologyDiagnostics(
|
|
3414
|
+
records: readonly CatalogRecord[],
|
|
3415
|
+
): CatalogDiagnostic[] {
|
|
3416
|
+
const byId = new Map(
|
|
3417
|
+
records.map((record) => [record.agentId, record]),
|
|
3418
|
+
);
|
|
3419
|
+
const problems = new Map<string, string>();
|
|
3420
|
+
for (const record of records) {
|
|
3421
|
+
if (record.descriptor.version !== 3) continue;
|
|
3422
|
+
const parent = byId.get(record.descriptor.parentAgentId);
|
|
3423
|
+
if (parent) {
|
|
3424
|
+
let expectedPath: string | undefined;
|
|
3425
|
+
try {
|
|
3426
|
+
expectedPath = taskPath(
|
|
3427
|
+
parent.taskPath,
|
|
3428
|
+
record.descriptor.task.name,
|
|
3429
|
+
);
|
|
3430
|
+
} catch {
|
|
3431
|
+
problems.set(
|
|
3432
|
+
record.agentId,
|
|
3433
|
+
"descriptor task path cannot be joined to its parent",
|
|
3434
|
+
);
|
|
3435
|
+
}
|
|
3436
|
+
if (
|
|
3437
|
+
expectedPath !== undefined
|
|
3438
|
+
&& (
|
|
3439
|
+
record.taskPath !== expectedPath
|
|
3440
|
+
|| record.descriptor.depth
|
|
3441
|
+
!== parent.descriptor.depth + 1
|
|
3442
|
+
)
|
|
3443
|
+
) {
|
|
3444
|
+
problems.set(
|
|
3445
|
+
record.agentId,
|
|
3446
|
+
"descriptor task path or depth does not match its parent",
|
|
3447
|
+
);
|
|
3448
|
+
}
|
|
3449
|
+
} else if (
|
|
3450
|
+
record.descriptor.depth === 1
|
|
3451
|
+
&& record.taskPath
|
|
3452
|
+
!== taskPath(
|
|
3453
|
+
ROOT_TASK_PATH,
|
|
3454
|
+
record.descriptor.task.name,
|
|
3455
|
+
)
|
|
3456
|
+
) {
|
|
3457
|
+
problems.set(
|
|
3458
|
+
record.agentId,
|
|
3459
|
+
"root child task path does not match its task name",
|
|
3460
|
+
);
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
|
|
3464
|
+
for (const start of records) {
|
|
3465
|
+
const chain: string[] = [];
|
|
3466
|
+
const indexes = new Map<string, number>();
|
|
3467
|
+
let current: CatalogRecord | undefined = start;
|
|
3468
|
+
while (current) {
|
|
3469
|
+
const existing = indexes.get(current.agentId);
|
|
3470
|
+
if (existing !== undefined) {
|
|
3471
|
+
for (const agentId of chain.slice(existing)) {
|
|
3472
|
+
problems.set(
|
|
3473
|
+
agentId,
|
|
3474
|
+
"descriptor lineage contains a cycle",
|
|
3475
|
+
);
|
|
3476
|
+
}
|
|
3477
|
+
break;
|
|
3478
|
+
}
|
|
3479
|
+
indexes.set(current.agentId, chain.length);
|
|
3480
|
+
chain.push(current.agentId);
|
|
3481
|
+
current = byId.get(current.descriptor.parentAgentId);
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
return [...problems]
|
|
3486
|
+
.map(([agentId, message]) => {
|
|
3487
|
+
const record = byId.get(agentId)!;
|
|
3488
|
+
return {
|
|
3489
|
+
kind: "diagnostic" as const,
|
|
3490
|
+
piSessionId: record.piSessionId,
|
|
3491
|
+
reason: "corrupt" as const,
|
|
3492
|
+
...(record.sessionFile
|
|
3493
|
+
? { sessionFile: record.sessionFile }
|
|
3494
|
+
: {}),
|
|
3495
|
+
...(record.descriptor.parentSessionFile
|
|
3496
|
+
? {
|
|
3497
|
+
parentSessionFile:
|
|
3498
|
+
record.descriptor.parentSessionFile,
|
|
3499
|
+
}
|
|
3500
|
+
: {}),
|
|
3501
|
+
message,
|
|
3502
|
+
};
|
|
3503
|
+
})
|
|
3504
|
+
.sort((left, right) =>
|
|
3505
|
+
left.piSessionId.localeCompare(right.piSessionId),
|
|
3506
|
+
);
|
|
1329
3507
|
}
|
|
1330
3508
|
|
|
1331
3509
|
private async findPersistedChild(
|
|
@@ -1333,7 +3511,7 @@ export class SubagentCoordinator {
|
|
|
1333
3511
|
childId: string,
|
|
1334
3512
|
): Promise<{ descriptor: SubagentDescriptor; sessionFile: string } | undefined> {
|
|
1335
3513
|
const catalog = await readPersistedCatalog(parent.sessionManager);
|
|
1336
|
-
const entry = catalog.descriptors.find((candidate) => candidate.
|
|
3514
|
+
const entry = catalog.descriptors.find((candidate) => candidate.agentId === childId);
|
|
1337
3515
|
return entry ? { descriptor: entry.descriptor, sessionFile: entry.sessionFile } : undefined;
|
|
1338
3516
|
}
|
|
1339
3517
|
|
|
@@ -1341,26 +3519,114 @@ export class SubagentCoordinator {
|
|
|
1341
3519
|
const persisted = await readPersistedCatalog(parent.sessionManager);
|
|
1342
3520
|
const records = new Map<string, CatalogRecord>();
|
|
1343
3521
|
for (const item of persisted.descriptors) {
|
|
1344
|
-
records.set(item.
|
|
1345
|
-
|
|
3522
|
+
records.set(item.agentId, {
|
|
3523
|
+
agentId: item.agentId,
|
|
3524
|
+
piSessionId: item.piSessionId,
|
|
3525
|
+
taskPath: descriptorTaskPath(item.descriptor),
|
|
1346
3526
|
descriptor: item.descriptor,
|
|
1347
3527
|
sessionFile: item.sessionFile,
|
|
3528
|
+
pendingMessages: item.pendingMessages,
|
|
3529
|
+
unreadUpdatesByChild: new Map(item.unreadUpdatesByChild),
|
|
3530
|
+
});
|
|
3531
|
+
}
|
|
3532
|
+
const activeSessionIds = new Set(
|
|
3533
|
+
[...this.active.values()].map((activation) => activation.runtime.session.sessionId),
|
|
3534
|
+
);
|
|
3535
|
+
const activeDiagnostics: CatalogEntry[] = [];
|
|
3536
|
+
const rootCompletions = foldCompletionMailbox(
|
|
3537
|
+
parent.sessionManager.getEntries(),
|
|
3538
|
+
{ parentAgentId: parent.agentId },
|
|
3539
|
+
);
|
|
3540
|
+
const rootUnreadUpdatesByChild =
|
|
3541
|
+
rootCompletions.kind === "valid"
|
|
3542
|
+
? unreadCompletionCounts(rootCompletions.snapshot)
|
|
3543
|
+
: new Map<string, number>();
|
|
3544
|
+
if (rootCompletions.kind === "corrupt") {
|
|
3545
|
+
const parentFile = parent.sessionManager.getSessionFile();
|
|
3546
|
+
activeDiagnostics.push({
|
|
3547
|
+
kind: "diagnostic",
|
|
3548
|
+
piSessionId: parent.sessionManager.getSessionId(),
|
|
3549
|
+
reason: "corrupt",
|
|
3550
|
+
...(parentFile
|
|
3551
|
+
? {
|
|
3552
|
+
sessionFile: parentFile,
|
|
3553
|
+
parentSessionFile: parentFile,
|
|
3554
|
+
}
|
|
3555
|
+
: {}),
|
|
3556
|
+
message: `corrupt completion mailbox: ${rootCompletions.message}`,
|
|
1348
3557
|
});
|
|
1349
3558
|
}
|
|
1350
3559
|
for (const activation of this.active.values()) {
|
|
1351
3560
|
if (activation.descriptor.cwd !== parent.cwd) continue;
|
|
1352
|
-
|
|
1353
|
-
|
|
3561
|
+
let pendingMessages = 0;
|
|
3562
|
+
if (activation.descriptor.runtime.backgroundProtocol === "mailbox-v2") {
|
|
3563
|
+
const mailbox = foldOwnedMailbox(
|
|
3564
|
+
activation.runtime.session.sessionManager.getEntries(),
|
|
3565
|
+
mailboxOwner(activation.descriptor),
|
|
3566
|
+
);
|
|
3567
|
+
if (mailbox.kind === "valid") {
|
|
3568
|
+
pendingMessages = mailbox.snapshot.pending.length;
|
|
3569
|
+
} else {
|
|
3570
|
+
activeDiagnostics.push({
|
|
3571
|
+
kind: "diagnostic",
|
|
3572
|
+
piSessionId: activation.runtime.session.sessionId,
|
|
3573
|
+
reason: "corrupt",
|
|
3574
|
+
...(activation.runtime.session.sessionFile
|
|
3575
|
+
? { sessionFile: activation.runtime.session.sessionFile }
|
|
3576
|
+
: {}),
|
|
3577
|
+
...(activation.descriptor.parentSessionFile
|
|
3578
|
+
? { parentSessionFile: activation.descriptor.parentSessionFile }
|
|
3579
|
+
: {}),
|
|
3580
|
+
message: `corrupt subagent mailbox: ${mailbox.message}`,
|
|
3581
|
+
});
|
|
3582
|
+
continue;
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
const completions = foldCompletionMailbox(
|
|
3586
|
+
activation.runtime.session.sessionManager.getEntries(),
|
|
3587
|
+
{ parentAgentId: activation.agentId },
|
|
3588
|
+
);
|
|
3589
|
+
if (completions.kind === "corrupt") {
|
|
3590
|
+
activeDiagnostics.push({
|
|
3591
|
+
kind: "diagnostic",
|
|
3592
|
+
piSessionId: activation.runtime.session.sessionId,
|
|
3593
|
+
reason: "corrupt",
|
|
3594
|
+
...(activation.runtime.session.sessionFile
|
|
3595
|
+
? { sessionFile: activation.runtime.session.sessionFile }
|
|
3596
|
+
: {}),
|
|
3597
|
+
...(activation.descriptor.parentSessionFile
|
|
3598
|
+
? { parentSessionFile: activation.descriptor.parentSessionFile }
|
|
3599
|
+
: {}),
|
|
3600
|
+
message: `corrupt completion mailbox: ${completions.message}`,
|
|
3601
|
+
});
|
|
3602
|
+
continue;
|
|
3603
|
+
}
|
|
3604
|
+
records.set(activation.agentId, {
|
|
3605
|
+
agentId: activation.agentId,
|
|
3606
|
+
piSessionId: activation.runtime.session.sessionId,
|
|
3607
|
+
taskPath: descriptorTaskPath(activation.descriptor),
|
|
1354
3608
|
descriptor: activation.descriptor,
|
|
1355
3609
|
...(activation.runtime.session.sessionFile
|
|
1356
3610
|
? { sessionFile: activation.runtime.session.sessionFile }
|
|
1357
3611
|
: {}),
|
|
1358
3612
|
active: activation,
|
|
3613
|
+
pendingMessages,
|
|
3614
|
+
unreadUpdatesByChild: unreadCompletionCounts(
|
|
3615
|
+
completions.snapshot,
|
|
3616
|
+
),
|
|
1359
3617
|
});
|
|
1360
3618
|
}
|
|
3619
|
+
const catalogRecords = [...records.values()];
|
|
1361
3620
|
return {
|
|
1362
|
-
records:
|
|
1363
|
-
diagnostics:
|
|
3621
|
+
records: catalogRecords,
|
|
3622
|
+
diagnostics: [
|
|
3623
|
+
...persisted.diagnostics.filter(
|
|
3624
|
+
(diagnostic) => !activeSessionIds.has(diagnostic.piSessionId),
|
|
3625
|
+
),
|
|
3626
|
+
...activeDiagnostics,
|
|
3627
|
+
...this.catalogTopologyDiagnostics(catalogRecords),
|
|
3628
|
+
],
|
|
3629
|
+
rootUnreadUpdatesByChild,
|
|
1364
3630
|
};
|
|
1365
3631
|
}
|
|
1366
3632
|
|
|
@@ -1369,7 +3635,7 @@ export class SubagentCoordinator {
|
|
|
1369
3635
|
descriptor: SubagentDescriptor,
|
|
1370
3636
|
byId: Map<string, CatalogRecord>,
|
|
1371
3637
|
): number | undefined {
|
|
1372
|
-
let parentId = descriptor.
|
|
3638
|
+
let parentId = descriptor.parentAgentId;
|
|
1373
3639
|
let distance = 1;
|
|
1374
3640
|
const visited = new Set<string>();
|
|
1375
3641
|
while (true) {
|
|
@@ -1378,16 +3644,21 @@ export class SubagentCoordinator {
|
|
|
1378
3644
|
visited.add(parentId);
|
|
1379
3645
|
const parent = byId.get(parentId);
|
|
1380
3646
|
if (!parent) return undefined;
|
|
1381
|
-
parentId = parent.descriptor.
|
|
3647
|
+
parentId = parent.descriptor.parentAgentId;
|
|
1382
3648
|
distance++;
|
|
1383
3649
|
}
|
|
1384
3650
|
}
|
|
1385
3651
|
|
|
1386
3652
|
private async isDescendantOf(parent: ParentRef, descriptor: SubagentDescriptor): Promise<boolean> {
|
|
1387
3653
|
const { records } = await this.catalogRecords(parent);
|
|
1388
|
-
const byId = new Map(records.map((record) => [record.
|
|
1389
|
-
return this.distanceFrom(parent.
|
|
3654
|
+
const byId = new Map(records.map((record) => [record.agentId, record]));
|
|
3655
|
+
return this.distanceFrom(parent.agentId, descriptor, byId) !== undefined;
|
|
1390
3656
|
}
|
|
1391
3657
|
}
|
|
1392
3658
|
|
|
1393
|
-
export {
|
|
3659
|
+
export {
|
|
3660
|
+
REPORT_CUSTOM_TYPE,
|
|
3661
|
+
SETTLED_CUSTOM_TYPE,
|
|
3662
|
+
AGENT_CUSTOM_TYPE,
|
|
3663
|
+
LINEAGE_CUSTOM_TYPE,
|
|
3664
|
+
};
|