@oai404iao/pi-subagent 0.3.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 +250 -75
- package/agents/worker.md +1 -1
- package/config.example.json +3 -1
- package/config.schema.json +24 -4
- package/package.json +7 -7
- package/src/agent-state.ts +125 -0
- package/src/agent-sync.ts +64 -53
- package/src/agents.ts +3 -22
- package/src/catalog.ts +49 -0
- package/src/completion-mailbox.ts +656 -0
- package/src/config.ts +51 -6
- package/src/coordinator.ts +2385 -231
- package/src/descriptor.ts +104 -11
- package/src/index.ts +175 -26
- package/src/mailbox.ts +451 -0
- package/src/providers.ts +221 -28
- package/src/render.ts +25 -8
- package/src/scheduler.ts +173 -0
- package/src/schemas.ts +111 -11
- package/src/task-path.ts +188 -0
- package/src/types.ts +56 -7
package/src/coordinator.ts
CHANGED
|
@@ -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,
|
|
@@ -81,8 +135,11 @@ const AGENT_ID_PATTERN =
|
|
|
81
135
|
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
82
136
|
const MAX_TRACE_ITEMS = 100;
|
|
83
137
|
const MAX_TRACE_TEXT = 4000;
|
|
138
|
+
const MAX_WAIT_AGENT_RESULT_BYTES = 256 * 1024;
|
|
84
139
|
const BACKGROUND_CONTROL_TOOLS = new Set([
|
|
85
140
|
"send_message",
|
|
141
|
+
"followup_task",
|
|
142
|
+
"wait_agent",
|
|
86
143
|
"interrupt_agent",
|
|
87
144
|
"list_agents",
|
|
88
145
|
]);
|
|
@@ -101,17 +158,54 @@ const REPORT_PROMPT = [
|
|
|
101
158
|
|
|
102
159
|
export interface DelegationInput {
|
|
103
160
|
agent: string;
|
|
161
|
+
task_name?: string;
|
|
104
162
|
description: string;
|
|
105
163
|
prompt: string;
|
|
106
164
|
run_in_background?: boolean;
|
|
165
|
+
context?: {
|
|
166
|
+
mode: ContextInheritance["mode"];
|
|
167
|
+
completed_turns?: number;
|
|
168
|
+
};
|
|
107
169
|
}
|
|
108
170
|
|
|
109
171
|
export type DelegationOutcome =
|
|
110
172
|
| { kind: "continuable"; details: DelegationDetails }
|
|
111
173
|
| { kind: "foreground"; details: DelegationDetails; result: SubagentRunResult };
|
|
112
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
|
+
|
|
113
206
|
interface ParentRef {
|
|
114
207
|
agentId: string;
|
|
208
|
+
taskPath: string;
|
|
115
209
|
depth: number;
|
|
116
210
|
cwd: string;
|
|
117
211
|
sessionManager: SessionView;
|
|
@@ -136,21 +230,53 @@ interface Activation {
|
|
|
136
230
|
runtime: AgentSessionRuntime;
|
|
137
231
|
seedMessageCount: number;
|
|
138
232
|
epochMessageStart: number;
|
|
139
|
-
|
|
233
|
+
controlState: AgentControlState;
|
|
140
234
|
trace: TraceItem[];
|
|
141
235
|
streamedText: string;
|
|
142
236
|
usage: ReturnType<typeof emptyUsage>;
|
|
237
|
+
startedTurnIds: Set<string>;
|
|
238
|
+
silentSettlementTurnIds: Set<string>;
|
|
239
|
+
pendingMailboxClaims: Set<string>;
|
|
240
|
+
userMessageGates: Map<string, UserMessageGate>;
|
|
143
241
|
ownedChildren: Set<string>;
|
|
144
242
|
currentRun?: Promise<SubagentRunResult>;
|
|
243
|
+
currentMessageGate?: Promise<void>;
|
|
145
244
|
pendingSettlement?: SubagentRunResult;
|
|
146
245
|
unsubscribe?: () => void;
|
|
147
246
|
onUpdate?: (details: DelegationDetails) => void;
|
|
148
247
|
published: boolean;
|
|
248
|
+
everPublished: boolean;
|
|
149
249
|
suppressSettlement: boolean;
|
|
150
250
|
finalizing: boolean;
|
|
151
|
-
finalizePromise?: Promise<
|
|
251
|
+
finalizePromise?: Promise<boolean>;
|
|
252
|
+
holdsBackgroundSlot: boolean;
|
|
253
|
+
turnAbortController?: AbortController;
|
|
254
|
+
persistenceGate: PersistenceGate;
|
|
152
255
|
disposed: boolean;
|
|
153
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;
|
|
154
280
|
}
|
|
155
281
|
|
|
156
282
|
interface CreateActivationOptions {
|
|
@@ -161,16 +287,60 @@ interface CreateActivationOptions {
|
|
|
161
287
|
onUpdate?: (details: DelegationDetails) => void;
|
|
162
288
|
}
|
|
163
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
|
+
|
|
164
317
|
interface CatalogRecord {
|
|
165
318
|
agentId: string;
|
|
319
|
+
piSessionId: string;
|
|
320
|
+
taskPath: string;
|
|
166
321
|
descriptor: SubagentDescriptor;
|
|
167
322
|
sessionFile?: string;
|
|
168
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;
|
|
169
338
|
}
|
|
170
339
|
|
|
171
340
|
interface CoordinatorCatalog {
|
|
172
341
|
records: CatalogRecord[];
|
|
173
342
|
diagnostics: CatalogEntry[];
|
|
343
|
+
rootUnreadUpdatesByChild: Map<string, number>;
|
|
174
344
|
}
|
|
175
345
|
|
|
176
346
|
function runtimeFromRegistry(registry: ModelRegistry): ModelRuntime {
|
|
@@ -195,6 +365,79 @@ function errorText(error: unknown): string {
|
|
|
195
365
|
return error instanceof Error ? error.message : String(error);
|
|
196
366
|
}
|
|
197
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
|
+
|
|
198
441
|
/**
|
|
199
442
|
* Read the durable pi-subagent control id recorded for a session, if any.
|
|
200
443
|
*
|
|
@@ -246,10 +489,12 @@ function stopReasonHeadline(reason: SubagentStopReason): string {
|
|
|
246
489
|
function makeRuntimeSettings(descriptor: SubagentDescriptor): SubagentSettings {
|
|
247
490
|
return {
|
|
248
491
|
agentScope: descriptor.runtime.agentScope,
|
|
249
|
-
syncBundledAgents: descriptor.runtime.syncBundledAgents,
|
|
250
492
|
maxDepth: descriptor.runtime.maxDepth,
|
|
251
493
|
enableRunInBackground: descriptor.runtime.enableRunInBackground,
|
|
252
494
|
defaultBackground: descriptor.runtime.defaultBackground,
|
|
495
|
+
maxConcurrentBackgroundRuns: descriptor.runtime.maxConcurrentBackgroundRuns,
|
|
496
|
+
maxIdleRuntimes: descriptor.runtime.maxIdleRuntimes,
|
|
497
|
+
backgroundProtocol: descriptor.runtime.backgroundProtocol,
|
|
253
498
|
reportDelivery: descriptor.runtime.reportDelivery,
|
|
254
499
|
inheritExtensions: descriptor.runtime.inheritExtensions,
|
|
255
500
|
openAIIdentity: descriptor.runtime.openAIIdentity,
|
|
@@ -257,6 +502,78 @@ function makeRuntimeSettings(descriptor: SubagentDescriptor): SubagentSettings {
|
|
|
257
502
|
};
|
|
258
503
|
}
|
|
259
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
|
+
|
|
260
577
|
function isPathInside(parent: string, child: string): boolean {
|
|
261
578
|
const rel = relative(resolve(parent), resolve(child));
|
|
262
579
|
return rel === "" || (!rel.startsWith("..") && !rel.startsWith("/"));
|
|
@@ -288,8 +605,19 @@ async function loadCodexIdentityInlineExtension(
|
|
|
288
605
|
export class SubagentCoordinator {
|
|
289
606
|
private readonly providers = new ProviderRegistry();
|
|
290
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;
|
|
291
618
|
private agentSyncResult: AgentSyncResult | undefined;
|
|
292
619
|
private draining = false;
|
|
620
|
+
private shutdownPromise: Promise<void> | undefined;
|
|
293
621
|
|
|
294
622
|
constructor(
|
|
295
623
|
private readonly pi: ExtensionAPI,
|
|
@@ -314,26 +642,39 @@ export class SubagentCoordinator {
|
|
|
314
642
|
return join(this.agentDir, "agents");
|
|
315
643
|
}
|
|
316
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
|
+
|
|
317
657
|
discoverAvailableAgents(
|
|
318
658
|
cwd: string,
|
|
319
659
|
settings: SubagentSettings,
|
|
320
660
|
projectTrusted: boolean,
|
|
321
661
|
): AgentDiscoveryResult {
|
|
322
|
-
if (
|
|
662
|
+
if (!this.agentSyncResult) {
|
|
323
663
|
this.synchronizeBundledAgents();
|
|
324
664
|
}
|
|
325
|
-
const
|
|
326
|
-
? undefined
|
|
327
|
-
: unmodifiedManagedAgentNames(this.agentDir);
|
|
328
|
-
return discoverAgents({
|
|
665
|
+
const discovery = discoverAgents({
|
|
329
666
|
cwd,
|
|
330
667
|
scope: settings.agentScope,
|
|
331
668
|
projectTrusted,
|
|
332
|
-
bundledDir: this.bundledAgentsDir,
|
|
333
669
|
agentDir: this.agentDir,
|
|
334
|
-
includeBundled: !settings.syncBundledAgents && settings.agentScope !== "project",
|
|
335
|
-
excludeUserAgentNames,
|
|
336
670
|
});
|
|
671
|
+
return {
|
|
672
|
+
...discovery,
|
|
673
|
+
diagnostics: [
|
|
674
|
+
...(this.agentSyncResult?.diagnostics ?? []),
|
|
675
|
+
...discovery.diagnostics,
|
|
676
|
+
],
|
|
677
|
+
};
|
|
337
678
|
}
|
|
338
679
|
|
|
339
680
|
async parentFromContext(ctx: ExtensionContext): Promise<ParentRef> {
|
|
@@ -344,6 +685,9 @@ export class SubagentCoordinator {
|
|
|
344
685
|
agentId: readAgentId(ctx.sessionManager) ?? ensureAgentId(
|
|
345
686
|
ctx.sessionManager as unknown as SessionView,
|
|
346
687
|
),
|
|
688
|
+
taskPath: descriptor
|
|
689
|
+
? descriptorTaskPath(descriptor)
|
|
690
|
+
: ROOT_TASK_PATH,
|
|
347
691
|
depth: descriptor?.depth ?? 0,
|
|
348
692
|
cwd: ctx.cwd,
|
|
349
693
|
// ExtensionContext narrows the live SessionManager to a read-only
|
|
@@ -374,11 +718,35 @@ export class SubagentCoordinator {
|
|
|
374
718
|
signal?: AbortSignal,
|
|
375
719
|
onUpdate?: (details: DelegationDetails) => void,
|
|
376
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,
|
|
377
743
|
): Promise<DelegationOutcome> {
|
|
378
744
|
if (this.draining) throw new Error("pi-subagent is shutting down; no new delegation was accepted");
|
|
379
|
-
const
|
|
745
|
+
const context = delegationContext(providerName, input);
|
|
746
|
+
const resolvedProviderName: SubagentProviderName =
|
|
747
|
+
context.mode === "fresh" ? "spawn" : "fork";
|
|
748
|
+
const provider = this.providers.get(resolvedProviderName);
|
|
380
749
|
if (
|
|
381
|
-
providerName === "spawn" &&
|
|
382
750
|
!settings.enableRunInBackground &&
|
|
383
751
|
input.run_in_background === true
|
|
384
752
|
) {
|
|
@@ -387,8 +755,10 @@ export class SubagentCoordinator {
|
|
|
387
755
|
);
|
|
388
756
|
}
|
|
389
757
|
const runInBackground =
|
|
390
|
-
|
|
391
|
-
?
|
|
758
|
+
settings.enableRunInBackground
|
|
759
|
+
? providerName === "fork"
|
|
760
|
+
? (input.run_in_background ?? false)
|
|
761
|
+
: (input.run_in_background ?? settings.defaultBackground)
|
|
392
762
|
: false;
|
|
393
763
|
const mode: SubagentMode = runInBackground ? "continuable" : "one-shot";
|
|
394
764
|
if (mode === "continuable" && !provider.supportsContinuable) {
|
|
@@ -429,57 +799,74 @@ export class SubagentCoordinator {
|
|
|
429
799
|
|
|
430
800
|
const model = this.resolveModel(parent, agent);
|
|
431
801
|
const thinkingLevel = agent.thinking ?? parent.thinkingLevel;
|
|
432
|
-
const prepared = await provider.prepare(parent, mode);
|
|
433
802
|
parent.agentId = ensureAgentId(parent.sessionManager);
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
label: input.description.trim(),
|
|
441
|
-
agentId: uuidv7(),
|
|
442
|
-
parentAgentId: parent.agentId,
|
|
443
|
-
parentPiSessionId: parent.sessionManager.getSessionId(),
|
|
444
|
-
...(parent.sessionManager.getSessionFile()
|
|
445
|
-
? { parentSessionFile: parent.sessionManager.getSessionFile() }
|
|
446
|
-
: {}),
|
|
447
|
-
depth,
|
|
448
|
-
cwd: parent.cwd,
|
|
449
|
-
createdAt: new Date().toISOString(),
|
|
450
|
-
agent: snapshotAgent(agent),
|
|
451
|
-
model: { provider: model.provider, id: model.id },
|
|
452
|
-
thinkingLevel,
|
|
453
|
-
runtime: {
|
|
454
|
-
agentScope: settings.agentScope,
|
|
455
|
-
syncBundledAgents: settings.syncBundledAgents,
|
|
456
|
-
maxDepth: settings.maxDepth,
|
|
457
|
-
enableRunInBackground: settings.enableRunInBackground,
|
|
458
|
-
defaultBackground: settings.defaultBackground,
|
|
459
|
-
reportDelivery: settings.reportDelivery,
|
|
460
|
-
inheritExtensions: settings.inheritExtensions,
|
|
461
|
-
openAIIdentity: openAIIdentityEnabled,
|
|
462
|
-
maxOutputBytes: settings.maxOutputBytes,
|
|
463
|
-
},
|
|
464
|
-
};
|
|
465
|
-
prepared.sessionManager.appendCustomEntry(AGENT_CUSTOM_TYPE, {
|
|
466
|
-
agentId: descriptor.agentId,
|
|
467
|
-
});
|
|
468
|
-
prepared.sessionManager.appendCustomEntry(LINEAGE_CUSTOM_TYPE, {
|
|
469
|
-
version: 1,
|
|
470
|
-
agentId: descriptor.agentId,
|
|
471
|
-
parentAgentId: descriptor.parentAgentId,
|
|
472
|
-
parentPiSessionId: descriptor.parentPiSessionId,
|
|
473
|
-
relation: descriptor.provider,
|
|
474
|
-
agentName: descriptor.agent.name,
|
|
475
|
-
openAIIdentity: openAIIdentityEnabled,
|
|
476
|
-
...(descriptor.parentSessionFile
|
|
477
|
-
? { parentSessionFile: descriptor.parentSessionFile }
|
|
478
|
-
: {}),
|
|
479
|
-
});
|
|
480
|
-
|
|
803
|
+
const reservedTask = await this.reserveTask(
|
|
804
|
+
parent,
|
|
805
|
+
input.task_name,
|
|
806
|
+
input.description,
|
|
807
|
+
);
|
|
808
|
+
let prepared: PreparedChildSession | undefined;
|
|
481
809
|
let activation: Activation | undefined;
|
|
810
|
+
let keepTaskReservation = false;
|
|
482
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
|
+
});
|
|
483
870
|
activation = await this.createActivation({
|
|
484
871
|
parent,
|
|
485
872
|
descriptor,
|
|
@@ -487,11 +874,18 @@ export class SubagentCoordinator {
|
|
|
487
874
|
isNew: true,
|
|
488
875
|
onUpdate,
|
|
489
876
|
});
|
|
877
|
+
if (this.draining) {
|
|
878
|
+
throw new Error("pi-subagent is shutting down; no new delegation was accepted");
|
|
879
|
+
}
|
|
490
880
|
if (mode === "continuable" && parent.activation) {
|
|
491
|
-
|
|
881
|
+
this.acquireParentOwnership(activation, parent);
|
|
492
882
|
}
|
|
493
|
-
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
|
+
});
|
|
494
887
|
await started.accepted;
|
|
888
|
+
keepTaskReservation = true;
|
|
495
889
|
if (mode === "continuable") {
|
|
496
890
|
activation.onUpdate = undefined;
|
|
497
891
|
return { kind: "continuable", details: this.detailsOf(activation) };
|
|
@@ -503,37 +897,144 @@ export class SubagentCoordinator {
|
|
|
503
897
|
await this.disposeActivation(activation);
|
|
504
898
|
return { kind: "foreground", details, result };
|
|
505
899
|
} catch (error) {
|
|
506
|
-
if (activation && !activation.published) {
|
|
900
|
+
if (activation && prepared && !activation.published) {
|
|
507
901
|
activation.suppressSettlement = true;
|
|
508
902
|
await this.rollbackActivation(activation, prepared);
|
|
509
|
-
} else if (!activation) {
|
|
903
|
+
} else if (!activation && prepared) {
|
|
510
904
|
await prepared.rollback();
|
|
511
905
|
}
|
|
512
906
|
throw error;
|
|
907
|
+
} finally {
|
|
908
|
+
if (!keepTaskReservation) reservedTask.release();
|
|
513
909
|
}
|
|
514
910
|
}
|
|
515
911
|
|
|
516
|
-
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> {
|
|
517
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
|
+
}
|
|
518
977
|
let activation = this.active.get(childId);
|
|
519
978
|
let coldPrepared: PreparedChildSession | undefined;
|
|
520
|
-
if (activation?.
|
|
521
|
-
|
|
522
|
-
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
|
+
}
|
|
523
1013
|
}
|
|
524
|
-
|
|
525
1014
|
if (!activation) {
|
|
526
1015
|
const located = await this.findPersistedChild(parent, childId);
|
|
527
1016
|
if (!located) throw new Error(`unknown subagent: ${childId}; message was not delivered`);
|
|
528
|
-
|
|
529
|
-
throw new Error(`subagent ${childId} is one-shot and cannot accept follow-up messages`);
|
|
530
|
-
}
|
|
531
|
-
this.assertDirectParent(parent, located.descriptor);
|
|
1017
|
+
this.assertContinuableDirectChild(parent, located.descriptor);
|
|
532
1018
|
const manager = SessionManager.open(
|
|
533
1019
|
located.sessionFile,
|
|
534
1020
|
parent.sessionManager.getSessionDir(),
|
|
535
1021
|
parent.cwd,
|
|
536
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
|
+
}
|
|
537
1038
|
coldPrepared = {
|
|
538
1039
|
sessionManager: manager,
|
|
539
1040
|
seedMessageCount: manager.buildSessionContext().messages.length,
|
|
@@ -545,21 +1046,61 @@ export class SubagentCoordinator {
|
|
|
545
1046
|
prepared: coldPrepared,
|
|
546
1047
|
isNew: false,
|
|
547
1048
|
});
|
|
548
|
-
|
|
1049
|
+
this.acquireParentOwnership(activation, parent);
|
|
549
1050
|
} else {
|
|
550
|
-
this.
|
|
1051
|
+
this.assertContinuableDirectChild(parent, activation.descriptor);
|
|
551
1052
|
}
|
|
1053
|
+
const resolvedPath = descriptorTaskPath(activation.descriptor);
|
|
552
1054
|
|
|
553
1055
|
const session = activation.runtime.session;
|
|
554
1056
|
if (activation.currentRun || session.isStreaming) {
|
|
555
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
|
+
}
|
|
556
1080
|
await session.followUp(message);
|
|
557
|
-
return
|
|
1081
|
+
return {
|
|
1082
|
+
kind: "legacy",
|
|
1083
|
+
agentId: childId,
|
|
1084
|
+
taskPath: resolvedPath,
|
|
1085
|
+
};
|
|
558
1086
|
}
|
|
559
1087
|
|
|
560
1088
|
try {
|
|
561
|
-
|
|
562
|
-
|
|
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
|
+
};
|
|
563
1104
|
} catch (error) {
|
|
564
1105
|
if (coldPrepared && !activation.published) {
|
|
565
1106
|
activation.suppressSettlement = true;
|
|
@@ -569,89 +1110,704 @@ export class SubagentCoordinator {
|
|
|
569
1110
|
}
|
|
570
1111
|
}
|
|
571
1112
|
|
|
572
|
-
async
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
target.lastError = errorText(error);
|
|
581
|
-
});
|
|
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
|
+
);
|
|
582
1121
|
}
|
|
583
1122
|
|
|
584
|
-
async
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
const distance = this.distanceFrom(parent.agentId, record.descriptor, byId);
|
|
592
|
-
if (distance === undefined || (scope === "children" && distance !== 1)) continue;
|
|
593
|
-
children.push({
|
|
594
|
-
kind: "child",
|
|
595
|
-
agentId: record.agentId,
|
|
596
|
-
parentAgentId: record.descriptor.parentAgentId,
|
|
597
|
-
depth: distance,
|
|
598
|
-
descriptor: record.descriptor,
|
|
599
|
-
...(record.sessionFile ? { sessionFile: record.sessionFile } : {}),
|
|
600
|
-
status: record.active
|
|
601
|
-
? record.active.currentRun || record.active.runtime.session.isStreaming
|
|
602
|
-
? "running"
|
|
603
|
-
: "idle"
|
|
604
|
-
: "ready",
|
|
605
|
-
});
|
|
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");
|
|
606
1130
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
left.agentId.localeCompare(right.agentId),
|
|
1131
|
+
const target = await this.resolveTarget(parent, childId);
|
|
1132
|
+
const pending = await this.agentOperations.run(target.agentId, () =>
|
|
1133
|
+
this.followupTaskSerialized(parent, target.agentId, signal),
|
|
611
1134
|
);
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
)
|
|
617
|
-
|
|
618
|
-
|
|
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;
|
|
619
1151
|
}
|
|
620
1152
|
|
|
621
|
-
async
|
|
622
|
-
|
|
623
|
-
|
|
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");
|
|
624
1160
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
+
|
|
1591
|
+
async interrupt(parent: ParentRef, targetId: string): Promise<void> {
|
|
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
|
+
};
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
|
|
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[]> {
|
|
1661
|
+
const catalog = await this.catalogRecords(parent);
|
|
1662
|
+
const records = catalog.records;
|
|
1663
|
+
const byId = new Map(records.map((record) => [record.agentId, record]));
|
|
1664
|
+
const children: CatalogChild[] = [];
|
|
1665
|
+
for (const record of records) {
|
|
1666
|
+
if (record.descriptor.mode !== "continuable") continue;
|
|
1667
|
+
const distance = this.distanceFrom(parent.agentId, record.descriptor, byId);
|
|
1668
|
+
if (distance === undefined || (scope === "children" && distance !== 1)) continue;
|
|
1669
|
+
children.push({
|
|
1670
|
+
kind: "child",
|
|
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),
|
|
1679
|
+
depth: distance,
|
|
1680
|
+
descriptor: record.descriptor,
|
|
1681
|
+
...(record.sessionFile ? { sessionFile: record.sessionFile } : {}),
|
|
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),
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
children.sort(
|
|
1693
|
+
(left, right) =>
|
|
1694
|
+
left.descriptor.createdAt.localeCompare(right.descriptor.createdAt) ||
|
|
1695
|
+
left.agentId.localeCompare(right.agentId),
|
|
1696
|
+
);
|
|
1697
|
+
const parentFile = parent.sessionManager.getSessionFile();
|
|
1698
|
+
const diagnostics = parentFile
|
|
1699
|
+
? catalog.diagnostics.filter(
|
|
1700
|
+
(entry) => entry.kind === "diagnostic" && entry.parentSessionFile === parentFile,
|
|
1701
|
+
)
|
|
1702
|
+
: [];
|
|
1703
|
+
return [...children, ...diagnostics];
|
|
1704
|
+
}
|
|
1705
|
+
|
|
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> {
|
|
1711
|
+
if (child.descriptor.mode !== "continuable") {
|
|
1712
|
+
throw new Error("report is available only to continuable subagents");
|
|
1713
|
+
}
|
|
1714
|
+
const truncated = truncateUtf8(output, child.descriptor.runtime.maxOutputBytes);
|
|
1715
|
+
const readablePath = descriptorTaskPath(child.descriptor);
|
|
1716
|
+
const content = `Background subagent ${readablePath} (${child.agentId}) reported:\n\n${truncated.text}${
|
|
1717
|
+
truncated.truncated ? `\n\n[Report truncated; ${truncated.omittedBytes} bytes omitted.]` : ""
|
|
1718
|
+
}`;
|
|
1719
|
+
await child.parent.deliver(
|
|
1720
|
+
REPORT_CUSTOM_TYPE,
|
|
1721
|
+
content,
|
|
1722
|
+
{
|
|
1723
|
+
kind: "report",
|
|
1724
|
+
childAgentId: child.agentId,
|
|
1725
|
+
taskPath: readablePath,
|
|
1726
|
+
label: child.descriptor.label,
|
|
1727
|
+
...(truncated.truncated ? { truncated: true } : {}),
|
|
1728
|
+
},
|
|
1729
|
+
child.descriptor.runtime.reportDelivery,
|
|
1730
|
+
);
|
|
1731
|
+
}
|
|
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
|
+
}
|
|
641
1751
|
|
|
642
1752
|
async shutdown(): Promise<void> {
|
|
643
|
-
if (this.
|
|
1753
|
+
if (this.shutdownPromise) return this.shutdownPromise;
|
|
644
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();
|
|
645
1772
|
const activations = [...this.active.values()];
|
|
646
|
-
for (const activation of activations) activation.suppressSettlement = true;
|
|
647
1773
|
await Promise.allSettled(
|
|
648
|
-
activations
|
|
649
|
-
|
|
650
|
-
|
|
1774
|
+
activations
|
|
1775
|
+
.map((activation) => activation.currentRun)
|
|
1776
|
+
.filter(
|
|
1777
|
+
(run): run is Promise<SubagentRunResult> => run !== undefined,
|
|
1778
|
+
),
|
|
651
1779
|
);
|
|
652
1780
|
for (const activation of activations.sort((left, right) => right.descriptor.depth - left.descriptor.depth)) {
|
|
653
1781
|
await this.disposeActivation(activation).catch(() => {});
|
|
654
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
|
+
);
|
|
655
1811
|
}
|
|
656
1812
|
|
|
657
1813
|
createChildToolDefinitions(
|
|
@@ -659,6 +1815,7 @@ export class SubagentCoordinator {
|
|
|
659
1815
|
enableRunInBackground = true,
|
|
660
1816
|
defaultBackground = true,
|
|
661
1817
|
agentDiscovery?: AgentDiscoveryResult,
|
|
1818
|
+
backgroundProtocol: NonNullable<SubagentSettings["backgroundProtocol"]> = "legacy",
|
|
662
1819
|
): ToolDefinition[] {
|
|
663
1820
|
const agentNames = agentDiscovery?.agents.map((agent) => agent.name);
|
|
664
1821
|
const assertBackgroundControlEnabled = (toolName: string): void => {
|
|
@@ -678,12 +1835,12 @@ export class SubagentCoordinator {
|
|
|
678
1835
|
name: "subagent",
|
|
679
1836
|
label: "Subagent",
|
|
680
1837
|
description:
|
|
681
|
-
"Delegate a standalone task to a
|
|
1838
|
+
"Delegate a standalone task to a named child path, with optional completed-turn context inheritance. " +
|
|
682
1839
|
(!enableRunInBackground
|
|
683
1840
|
? "This foreground-only instance always waits for the result."
|
|
684
1841
|
: defaultBackground
|
|
685
|
-
? "It runs in the background by default and returns a durable id."
|
|
686
|
-
: "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."),
|
|
687
1844
|
parameters: delegationParameters(enableRunInBackground, agentNames),
|
|
688
1845
|
execute: async (_id, params, signal, onUpdate) => {
|
|
689
1846
|
const activation = getActivation();
|
|
@@ -704,8 +1861,11 @@ export class SubagentCoordinator {
|
|
|
704
1861
|
name: "subagent_fork",
|
|
705
1862
|
label: "Subagent Fork",
|
|
706
1863
|
description:
|
|
707
|
-
"Delegate a
|
|
708
|
-
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
|
+
),
|
|
709
1869
|
execute: async (_id, params, signal, onUpdate) => {
|
|
710
1870
|
const activation = getActivation();
|
|
711
1871
|
const outcome = await this.delegate(
|
|
@@ -725,12 +1885,14 @@ export class SubagentCoordinator {
|
|
|
725
1885
|
name: "send_message",
|
|
726
1886
|
label: "Send Message",
|
|
727
1887
|
description:
|
|
728
|
-
|
|
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.",
|
|
729
1891
|
parameters: SendMessageParameters,
|
|
730
1892
|
execute: async (_id, params, signal) => {
|
|
731
1893
|
assertBackgroundControlEnabled("send_message");
|
|
732
1894
|
const activation = getActivation();
|
|
733
|
-
await this.
|
|
1895
|
+
const delivery = await this.sendMessageWithOutcome(
|
|
734
1896
|
this.parentForActivation(activation),
|
|
735
1897
|
params.subagent_id,
|
|
736
1898
|
params.message,
|
|
@@ -740,10 +1902,92 @@ export class SubagentCoordinator {
|
|
|
740
1902
|
content: [
|
|
741
1903
|
{
|
|
742
1904
|
type: "text",
|
|
743
|
-
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"}`,
|
|
744
1946
|
},
|
|
745
1947
|
],
|
|
746
|
-
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,
|
|
747
1991
|
};
|
|
748
1992
|
},
|
|
749
1993
|
});
|
|
@@ -757,10 +2001,18 @@ export class SubagentCoordinator {
|
|
|
757
2001
|
execute: async (_id, params) => {
|
|
758
2002
|
assertBackgroundControlEnabled("interrupt_agent");
|
|
759
2003
|
const activation = getActivation();
|
|
760
|
-
await this.
|
|
2004
|
+
const outcome = await this.interruptWithOutcome(
|
|
2005
|
+
this.parentForActivation(activation),
|
|
2006
|
+
params.agent_id,
|
|
2007
|
+
);
|
|
761
2008
|
return {
|
|
762
|
-
content: [{ type: "text", text: `interrupt requested for
|
|
763
|
-
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,
|
|
764
2016
|
};
|
|
765
2017
|
},
|
|
766
2018
|
});
|
|
@@ -769,7 +2021,7 @@ export class SubagentCoordinator {
|
|
|
769
2021
|
name: "list_agents",
|
|
770
2022
|
label: "List Agents",
|
|
771
2023
|
description:
|
|
772
|
-
"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.",
|
|
773
2025
|
parameters: ListAgentsParameters,
|
|
774
2026
|
execute: async (_id, params) => {
|
|
775
2027
|
assertBackgroundControlEnabled("list_agents");
|
|
@@ -796,18 +2048,28 @@ export class SubagentCoordinator {
|
|
|
796
2048
|
await this.report(activation, params.output);
|
|
797
2049
|
return {
|
|
798
2050
|
content: [{ type: "text", text: `report accepted by the agent that started you` }],
|
|
799
|
-
|
|
2051
|
+
details: {
|
|
2052
|
+
kind: "control",
|
|
2053
|
+
action: "report",
|
|
2054
|
+
agentId: activation.agentId,
|
|
2055
|
+
taskPath: descriptorTaskPath(activation.descriptor),
|
|
2056
|
+
} satisfies ControlDetails,
|
|
800
2057
|
};
|
|
801
2058
|
},
|
|
802
2059
|
});
|
|
803
2060
|
|
|
804
|
-
return [spawn, fork, send, interrupt, list, report];
|
|
2061
|
+
return [spawn, fork, send, followup, wait, interrupt, list, report];
|
|
805
2062
|
}
|
|
806
2063
|
|
|
807
2064
|
outcomeToolResult(outcome: DelegationOutcome): AgentToolResult<DelegationDetails> {
|
|
808
2065
|
if (outcome.kind === "continuable") {
|
|
809
2066
|
return {
|
|
810
|
-
content: [
|
|
2067
|
+
content: [
|
|
2068
|
+
{
|
|
2069
|
+
type: "text",
|
|
2070
|
+
text: `started subagent ${outcome.details.taskPath} (${outcome.details.agentId})`,
|
|
2071
|
+
},
|
|
2072
|
+
],
|
|
811
2073
|
details: outcome.details,
|
|
812
2074
|
};
|
|
813
2075
|
}
|
|
@@ -834,12 +2096,52 @@ export class SubagentCoordinator {
|
|
|
834
2096
|
return `${entry.piSessionId} [diagnostic: ${entry.reason}]`;
|
|
835
2097
|
}
|
|
836
2098
|
const location =
|
|
837
|
-
scope === "descendants"
|
|
838
|
-
|
|
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}`;
|
|
839
2107
|
})
|
|
840
2108
|
.join("\n");
|
|
841
2109
|
}
|
|
842
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
|
+
|
|
843
2145
|
private resolveModel(parent: ParentRef, agent: AgentDefinition): Model<any> {
|
|
844
2146
|
if (!agent.model) {
|
|
845
2147
|
if (!parent.model) throw new Error("no parent model is selected for the subagent");
|
|
@@ -881,6 +2183,7 @@ export class SubagentCoordinator {
|
|
|
881
2183
|
descriptor.runtime.enableRunInBackground,
|
|
882
2184
|
descriptor.runtime.defaultBackground,
|
|
883
2185
|
agentDiscovery,
|
|
2186
|
+
descriptor.runtime.backgroundProtocol,
|
|
884
2187
|
);
|
|
885
2188
|
const model =
|
|
886
2189
|
options.parent.modelRuntime.getModel(descriptor.model.provider, descriptor.model.id) ??
|
|
@@ -1001,6 +2304,15 @@ export class SubagentCoordinator {
|
|
|
1001
2304
|
) {
|
|
1002
2305
|
return false;
|
|
1003
2306
|
}
|
|
2307
|
+
if (
|
|
2308
|
+
descriptor.runtime.backgroundProtocol !== "mailbox-v2"
|
|
2309
|
+
&& (
|
|
2310
|
+
tool === "followup_task"
|
|
2311
|
+
|| tool === "wait_agent"
|
|
2312
|
+
)
|
|
2313
|
+
) {
|
|
2314
|
+
return false;
|
|
2315
|
+
}
|
|
1004
2316
|
return true;
|
|
1005
2317
|
});
|
|
1006
2318
|
runtime.session.setActiveToolsByName(activeTools);
|
|
@@ -1045,17 +2357,32 @@ export class SubagentCoordinator {
|
|
|
1045
2357
|
runtime,
|
|
1046
2358
|
seedMessageCount: options.prepared.seedMessageCount,
|
|
1047
2359
|
epochMessageStart: runtime.session.messages.length,
|
|
1048
|
-
|
|
2360
|
+
controlState: createAgentControlState(),
|
|
1049
2361
|
trace: [],
|
|
1050
2362
|
streamedText: "",
|
|
1051
2363
|
usage: emptyUsage(),
|
|
2364
|
+
startedTurnIds: new Set(),
|
|
2365
|
+
silentSettlementTurnIds: new Set(),
|
|
2366
|
+
pendingMailboxClaims: new Set(),
|
|
2367
|
+
userMessageGates: new Map(),
|
|
1052
2368
|
ownedChildren: new Set(),
|
|
1053
2369
|
onUpdate: options.onUpdate,
|
|
1054
2370
|
published: false,
|
|
2371
|
+
everPublished: false,
|
|
1055
2372
|
suppressSettlement: false,
|
|
1056
2373
|
finalizing: false,
|
|
2374
|
+
holdsBackgroundSlot: false,
|
|
2375
|
+
persistenceGate: createPersistenceGate(
|
|
2376
|
+
runtime.session.sessionManager,
|
|
2377
|
+
options.isNew,
|
|
2378
|
+
),
|
|
2379
|
+
lastUsedSequence: ++this.activationSequence,
|
|
1057
2380
|
disposed: false,
|
|
1058
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
|
+
}
|
|
1059
2386
|
activation.unsubscribe = runtime.session.subscribe((event) => this.observe(activation!, event));
|
|
1060
2387
|
this.active.set(activation.agentId, activation);
|
|
1061
2388
|
return activation;
|
|
@@ -1065,16 +2392,33 @@ export class SubagentCoordinator {
|
|
|
1065
2392
|
}
|
|
1066
2393
|
}
|
|
1067
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
|
+
|
|
1068
2409
|
private startPrompt(
|
|
1069
2410
|
activation: Activation,
|
|
1070
2411
|
prompt: string,
|
|
1071
2412
|
signal: AbortSignal | undefined,
|
|
1072
|
-
|
|
1073
|
-
): { accepted: Promise<void>; result: Promise<SubagentRunResult> } {
|
|
2413
|
+
options: StartPromptOptions,
|
|
2414
|
+
): { turnId: string; accepted: Promise<void>; result: Promise<SubagentRunResult> } {
|
|
1074
2415
|
if (activation.currentRun) throw new Error(`subagent ${activation.agentId} is already running`);
|
|
1075
2416
|
if (signal?.aborted) throw signal.reason ?? new Error("subagent start aborted");
|
|
2417
|
+
this.touchActivation(activation);
|
|
1076
2418
|
activation.pendingSettlement = undefined;
|
|
1077
|
-
activation
|
|
2419
|
+
this.resetTurnCapture(activation);
|
|
2420
|
+
const turnId = uuidv7();
|
|
2421
|
+
queueAgentTurn(activation.controlState, turnId);
|
|
1078
2422
|
this.emitUpdate(activation);
|
|
1079
2423
|
|
|
1080
2424
|
let resolveAccepted!: () => void;
|
|
@@ -1084,47 +2428,134 @@ export class SubagentCoordinator {
|
|
|
1084
2428
|
resolveAccepted = resolvePromise;
|
|
1085
2429
|
rejectAccepted = rejectPromise;
|
|
1086
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;
|
|
1087
2440
|
const abort = () => {
|
|
2441
|
+
turnAbortController.abort(
|
|
2442
|
+
signal?.reason ?? new Error(`subagent turn ${turnId} was aborted`),
|
|
2443
|
+
);
|
|
1088
2444
|
void activation.runtime.session.abort().catch(() => {});
|
|
1089
2445
|
};
|
|
1090
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
|
+
}
|
|
1091
2483
|
|
|
1092
2484
|
const core = (async (): Promise<SubagentRunResult> => {
|
|
2485
|
+
let permit: BackgroundRunPermit | undefined;
|
|
1093
2486
|
try {
|
|
1094
|
-
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
|
+
: {}),
|
|
1095
2509
|
preflightResult: (success) => {
|
|
1096
2510
|
if (acceptedSettled) return;
|
|
1097
|
-
acceptedSettled = true;
|
|
1098
2511
|
if (success) {
|
|
1099
|
-
|
|
1100
|
-
if (
|
|
1101
|
-
|
|
2512
|
+
preflightSucceeded = true;
|
|
2513
|
+
if (options.acceptAfterUserMessage) {
|
|
2514
|
+
return;
|
|
2515
|
+
}
|
|
2516
|
+
options.onPromptAccepted?.(turnId);
|
|
2517
|
+
acceptPrompt();
|
|
1102
2518
|
} else {
|
|
1103
|
-
|
|
1104
|
-
|
|
2519
|
+
rejectPrompt(
|
|
2520
|
+
new Error("subagent prompt was rejected before acceptance"),
|
|
2521
|
+
);
|
|
1105
2522
|
}
|
|
1106
2523
|
},
|
|
1107
2524
|
});
|
|
1108
2525
|
if (!acceptedSettled) {
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
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();
|
|
1113
2536
|
}
|
|
1114
|
-
return this.collectResult(activation, "completed");
|
|
2537
|
+
return this.collectResult(activation, turnId, "completed");
|
|
1115
2538
|
} catch (error) {
|
|
1116
2539
|
activation.lastError = errorText(error);
|
|
1117
2540
|
if (!acceptedSettled) {
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
2541
|
+
rejectPrompt(
|
|
2542
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
2543
|
+
);
|
|
1121
2544
|
}
|
|
1122
|
-
const fallback = signal
|
|
1123
|
-
const result = this.collectResult(activation, fallback);
|
|
2545
|
+
const fallback = turnAbortController.signal.aborted ? "aborted" : "error";
|
|
2546
|
+
const result = this.collectResult(activation, turnId, fallback);
|
|
1124
2547
|
if (!result.output) result.output = activation.lastError;
|
|
1125
2548
|
return result;
|
|
1126
2549
|
} finally {
|
|
2550
|
+
activation.userMessageGates.delete(turnId);
|
|
1127
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
|
+
}
|
|
1128
2559
|
}
|
|
1129
2560
|
})();
|
|
1130
2561
|
|
|
@@ -1136,7 +2567,7 @@ export class SubagentCoordinator {
|
|
|
1136
2567
|
});
|
|
1137
2568
|
activation.currentRun = lifecycle;
|
|
1138
2569
|
void lifecycle.catch(() => {});
|
|
1139
|
-
return { accepted, result: lifecycle };
|
|
2570
|
+
return { turnId, accepted, result: lifecycle };
|
|
1140
2571
|
}
|
|
1141
2572
|
|
|
1142
2573
|
private startInternalMessage(
|
|
@@ -1147,23 +2578,68 @@ export class SubagentCoordinator {
|
|
|
1147
2578
|
): void {
|
|
1148
2579
|
if (activation.currentRun || activation.disposed) return;
|
|
1149
2580
|
activation.pendingSettlement = undefined;
|
|
1150
|
-
activation
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1153
|
-
|
|
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(
|
|
1154
2611
|
{ customType, content, display: true, details },
|
|
1155
2612
|
{ triggerTurn: true, deliverAs: "followUp" },
|
|
1156
|
-
)
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
(
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
}
|
|
1166
|
-
|
|
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
|
+
})();
|
|
1167
2643
|
let lifecycle!: Promise<SubagentRunResult>;
|
|
1168
2644
|
lifecycle = core.then(async (result) => {
|
|
1169
2645
|
if (activation.currentRun === lifecycle) activation.currentRun = undefined;
|
|
@@ -1175,8 +2651,66 @@ export class SubagentCoordinator {
|
|
|
1175
2651
|
}
|
|
1176
2652
|
|
|
1177
2653
|
private async runFinished(activation: Activation, result: SubagentRunResult): Promise<void> {
|
|
1178
|
-
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
|
+
}
|
|
1179
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
|
+
}
|
|
1180
2714
|
this.emitUpdate(activation, result);
|
|
1181
2715
|
if (activation.descriptor.mode === "one-shot") {
|
|
1182
2716
|
this.emitEnd(activation, result);
|
|
@@ -1184,7 +2718,6 @@ export class SubagentCoordinator {
|
|
|
1184
2718
|
}
|
|
1185
2719
|
if (activation.suppressSettlement || this.draining) return;
|
|
1186
2720
|
if (activation.ownedChildren.size > 0) {
|
|
1187
|
-
activation.status = "waiting";
|
|
1188
2721
|
this.emitUpdate(activation, result);
|
|
1189
2722
|
return;
|
|
1190
2723
|
}
|
|
@@ -1192,29 +2725,74 @@ export class SubagentCoordinator {
|
|
|
1192
2725
|
}
|
|
1193
2726
|
|
|
1194
2727
|
private async finalizeContinuable(activation: Activation): Promise<void> {
|
|
1195
|
-
|
|
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
|
+
}
|
|
1196
2740
|
const result = activation.pendingSettlement;
|
|
1197
|
-
if (!result || activation.ownedChildren.size > 0) return;
|
|
2741
|
+
if (!result || activation.ownedChildren.size > 0) return false;
|
|
1198
2742
|
activation.finalizing = true;
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
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
|
+
}
|
|
1204
2775
|
}
|
|
1205
|
-
|
|
1206
|
-
await this.disposeActivation(activation);
|
|
1207
|
-
await this.releaseParentOwnership(activation);
|
|
2776
|
+
return retained;
|
|
1208
2777
|
})();
|
|
1209
|
-
|
|
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
|
+
}
|
|
1210
2787
|
}
|
|
1211
2788
|
|
|
1212
2789
|
private async deliverSettlement(activation: Activation, result: SubagentRunResult): Promise<void> {
|
|
2790
|
+
const readablePath = descriptorTaskPath(activation.descriptor);
|
|
1213
2791
|
const truncated = truncateUtf8(result.output, activation.descriptor.runtime.maxOutputBytes);
|
|
1214
2792
|
const closing = truncated.text.trim()
|
|
1215
2793
|
? `Its closing message:\n\n${truncated.text}`
|
|
1216
2794
|
: "It left no closing message.";
|
|
1217
|
-
const content = `Background subagent ${activation.agentId} ${stopReasonHeadline(result.stopReason)} and will do no further work unless you send it more.\n\n${closing}${
|
|
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}${
|
|
1218
2796
|
truncated.truncated ? `\n\n[Closing message truncated; ${truncated.omittedBytes} bytes omitted.]` : ""
|
|
1219
2797
|
}`;
|
|
1220
2798
|
await activation.parent.deliver(
|
|
@@ -1223,6 +2801,7 @@ export class SubagentCoordinator {
|
|
|
1223
2801
|
{
|
|
1224
2802
|
kind: "settled",
|
|
1225
2803
|
childAgentId: activation.agentId,
|
|
2804
|
+
taskPath: readablePath,
|
|
1226
2805
|
label: activation.descriptor.label,
|
|
1227
2806
|
stopReason: result.stopReason,
|
|
1228
2807
|
...(truncated.truncated ? { truncated: true } : {}),
|
|
@@ -1234,6 +2813,7 @@ export class SubagentCoordinator {
|
|
|
1234
2813
|
private parentForActivation(activation: Activation): ParentRef {
|
|
1235
2814
|
return {
|
|
1236
2815
|
agentId: activation.agentId,
|
|
2816
|
+
taskPath: descriptorTaskPath(activation.descriptor),
|
|
1237
2817
|
depth: activation.descriptor.depth,
|
|
1238
2818
|
cwd: activation.descriptor.cwd,
|
|
1239
2819
|
sessionManager: activation.runtime.session.sessionManager,
|
|
@@ -1266,7 +2846,17 @@ export class SubagentCoordinator {
|
|
|
1266
2846
|
};
|
|
1267
2847
|
}
|
|
1268
2848
|
|
|
1269
|
-
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 {
|
|
1270
2860
|
const messages = activation.runtime.session.messages;
|
|
1271
2861
|
const output = finalAssistantText(messages, activation.epochMessageStart, activation.streamedText);
|
|
1272
2862
|
const stopReason = finalStopReason(messages, activation.epochMessageStart, fallback);
|
|
@@ -1274,6 +2864,7 @@ export class SubagentCoordinator {
|
|
|
1274
2864
|
const sessionFile = activation.runtime.session.sessionFile;
|
|
1275
2865
|
return {
|
|
1276
2866
|
agentId: activation.agentId,
|
|
2867
|
+
turnId,
|
|
1277
2868
|
piSessionId: activation.runtime.session.sessionId,
|
|
1278
2869
|
...(sessionFile ? { sessionFile } : {}),
|
|
1279
2870
|
output: truncated.truncated
|
|
@@ -1281,6 +2872,12 @@ export class SubagentCoordinator {
|
|
|
1281
2872
|
sessionFile ? ` Full output: ${sessionFile}` : " Full output remains in the active child session."
|
|
1282
2873
|
}]`
|
|
1283
2874
|
: truncated.text,
|
|
2875
|
+
...(truncated.truncated
|
|
2876
|
+
? {
|
|
2877
|
+
outputTruncated: true,
|
|
2878
|
+
omittedBytes: truncated.omittedBytes,
|
|
2879
|
+
}
|
|
2880
|
+
: {}),
|
|
1284
2881
|
stopReason,
|
|
1285
2882
|
usage: structuredClone(activation.usage),
|
|
1286
2883
|
};
|
|
@@ -1301,12 +2898,47 @@ export class SubagentCoordinator {
|
|
|
1301
2898
|
this.emitUpdate(activation);
|
|
1302
2899
|
return;
|
|
1303
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
|
+
}
|
|
1304
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
|
+
}
|
|
1305
2936
|
if (event.message.role === "toolResult") {
|
|
1306
2937
|
if (event.message.usage) addUsage(activation.usage, event.message.usage, false);
|
|
1307
2938
|
return;
|
|
1308
2939
|
}
|
|
1309
2940
|
if (event.message.role !== "assistant") return;
|
|
2941
|
+
activation.persistenceGate.resolve();
|
|
1310
2942
|
addUsage(activation.usage, event.message.usage);
|
|
1311
2943
|
const text = event.message.content
|
|
1312
2944
|
.filter((part): part is Extract<(typeof event.message.content)[number], { type: "text" }> => part.type === "text")
|
|
@@ -1327,13 +2959,16 @@ export class SubagentCoordinator {
|
|
|
1327
2959
|
private publish(activation: Activation): void {
|
|
1328
2960
|
if (activation.published) return;
|
|
1329
2961
|
activation.published = true;
|
|
2962
|
+
activation.everPublished = true;
|
|
1330
2963
|
this.pi.events.emit("pi-subagent:start", {
|
|
1331
2964
|
runId: activation.runId,
|
|
1332
2965
|
agentId: activation.agentId,
|
|
2966
|
+
taskPath: descriptorTaskPath(activation.descriptor),
|
|
1333
2967
|
piSessionId: activation.runtime.session.sessionId,
|
|
1334
2968
|
parentAgentId: activation.descriptor.parentAgentId,
|
|
1335
2969
|
provider: activation.descriptor.provider,
|
|
1336
2970
|
mode: activation.descriptor.mode,
|
|
2971
|
+
context: descriptorContext(activation.descriptor),
|
|
1337
2972
|
});
|
|
1338
2973
|
}
|
|
1339
2974
|
|
|
@@ -1342,6 +2977,35 @@ export class SubagentCoordinator {
|
|
|
1342
2977
|
this.pi.events.emit("pi-subagent:end", {
|
|
1343
2978
|
runId: activation.runId,
|
|
1344
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),
|
|
1345
3009
|
piSessionId: activation.runtime.session.sessionId,
|
|
1346
3010
|
parentAgentId: activation.descriptor.parentAgentId,
|
|
1347
3011
|
provider: activation.descriptor.provider,
|
|
@@ -1355,13 +3019,21 @@ export class SubagentCoordinator {
|
|
|
1355
3019
|
return {
|
|
1356
3020
|
kind: "delegation",
|
|
1357
3021
|
agentId: activation.agentId,
|
|
3022
|
+
taskPath: descriptorTaskPath(activation.descriptor),
|
|
3023
|
+
...(currentAgentTurnId(activation.controlState)
|
|
3024
|
+
? { turnId: currentAgentTurnId(activation.controlState) }
|
|
3025
|
+
: {}),
|
|
1358
3026
|
piSessionId: activation.runtime.session.sessionId,
|
|
1359
3027
|
provider: activation.descriptor.provider,
|
|
1360
3028
|
mode: activation.descriptor.mode,
|
|
3029
|
+
context: descriptorContext(activation.descriptor),
|
|
1361
3030
|
agent: activation.descriptor.agent.name,
|
|
1362
3031
|
label: activation.descriptor.label,
|
|
1363
3032
|
depth: activation.descriptor.depth,
|
|
1364
|
-
status:
|
|
3033
|
+
status: delegationStatus(
|
|
3034
|
+
activation.controlState,
|
|
3035
|
+
activation.ownedChildren.size > 0,
|
|
3036
|
+
),
|
|
1365
3037
|
...(activation.runtime.session.sessionFile
|
|
1366
3038
|
? { sessionFile: activation.runtime.session.sessionFile }
|
|
1367
3039
|
: {}),
|
|
@@ -1403,6 +3075,25 @@ export class SubagentCoordinator {
|
|
|
1403
3075
|
private async disposeActivation(activation: Activation): Promise<void> {
|
|
1404
3076
|
if (activation.disposed) return;
|
|
1405
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);
|
|
1406
3097
|
activation.unsubscribe?.();
|
|
1407
3098
|
activation.unsubscribe = undefined;
|
|
1408
3099
|
if (!activation.runtime.session.isIdle) await activation.runtime.session.abort().catch(() => {});
|
|
@@ -1414,8 +3105,9 @@ export class SubagentCoordinator {
|
|
|
1414
3105
|
}
|
|
1415
3106
|
|
|
1416
3107
|
private async releaseParentOwnership(activation: Activation): Promise<void> {
|
|
1417
|
-
const owner = activation.
|
|
3108
|
+
const owner = activation.ownerActivation;
|
|
1418
3109
|
if (!owner) return;
|
|
3110
|
+
activation.ownerActivation = undefined;
|
|
1419
3111
|
owner.ownedChildren.delete(activation.agentId);
|
|
1420
3112
|
if (
|
|
1421
3113
|
!owner.currentRun &&
|
|
@@ -1423,16 +3115,395 @@ export class SubagentCoordinator {
|
|
|
1423
3115
|
owner.pendingSettlement &&
|
|
1424
3116
|
owner.descriptor.mode === "continuable"
|
|
1425
3117
|
) {
|
|
1426
|
-
|
|
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
|
+
});
|
|
1427
3211
|
}
|
|
1428
3212
|
}
|
|
1429
3213
|
|
|
1430
3214
|
private assertDirectParent(parent: ParentRef, descriptor: SubagentDescriptor): void {
|
|
1431
3215
|
if (descriptor.parentAgentId !== parent.agentId) {
|
|
1432
3216
|
throw new Error(
|
|
1433
|
-
`subagent ${descriptor.label} is not a direct child of ${parent.agentId}
|
|
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) {
|
|
3347
|
+
throw new Error(
|
|
3348
|
+
`ambiguous subagent task path ${path}; use a durable agent id`,
|
|
1434
3349
|
);
|
|
1435
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
|
+
);
|
|
1436
3507
|
}
|
|
1437
3508
|
|
|
1438
3509
|
private async findPersistedChild(
|
|
@@ -1450,29 +3521,112 @@ export class SubagentCoordinator {
|
|
|
1450
3521
|
for (const item of persisted.descriptors) {
|
|
1451
3522
|
records.set(item.agentId, {
|
|
1452
3523
|
agentId: item.agentId,
|
|
3524
|
+
piSessionId: item.piSessionId,
|
|
3525
|
+
taskPath: descriptorTaskPath(item.descriptor),
|
|
1453
3526
|
descriptor: item.descriptor,
|
|
1454
3527
|
sessionFile: item.sessionFile,
|
|
3528
|
+
pendingMessages: item.pendingMessages,
|
|
3529
|
+
unreadUpdatesByChild: new Map(item.unreadUpdatesByChild),
|
|
1455
3530
|
});
|
|
1456
3531
|
}
|
|
1457
3532
|
const activeSessionIds = new Set(
|
|
1458
3533
|
[...this.active.values()].map((activation) => activation.runtime.session.sessionId),
|
|
1459
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}`,
|
|
3557
|
+
});
|
|
3558
|
+
}
|
|
1460
3559
|
for (const activation of this.active.values()) {
|
|
1461
3560
|
if (activation.descriptor.cwd !== parent.cwd) continue;
|
|
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
|
+
}
|
|
1462
3604
|
records.set(activation.agentId, {
|
|
1463
3605
|
agentId: activation.agentId,
|
|
3606
|
+
piSessionId: activation.runtime.session.sessionId,
|
|
3607
|
+
taskPath: descriptorTaskPath(activation.descriptor),
|
|
1464
3608
|
descriptor: activation.descriptor,
|
|
1465
3609
|
...(activation.runtime.session.sessionFile
|
|
1466
3610
|
? { sessionFile: activation.runtime.session.sessionFile }
|
|
1467
3611
|
: {}),
|
|
1468
3612
|
active: activation,
|
|
3613
|
+
pendingMessages,
|
|
3614
|
+
unreadUpdatesByChild: unreadCompletionCounts(
|
|
3615
|
+
completions.snapshot,
|
|
3616
|
+
),
|
|
1469
3617
|
});
|
|
1470
3618
|
}
|
|
3619
|
+
const catalogRecords = [...records.values()];
|
|
1471
3620
|
return {
|
|
1472
|
-
records:
|
|
1473
|
-
diagnostics:
|
|
1474
|
-
|
|
1475
|
-
|
|
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,
|
|
1476
3630
|
};
|
|
1477
3631
|
}
|
|
1478
3632
|
|