@oai404iao/pi-subagent 0.3.0 → 0.4.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 +294 -89
- package/agents/worker.md +1 -1
- package/config.example.json +3 -4
- package/config.schema.json +26 -20
- package/index.ts +2 -0
- package/package.json +13 -12
- package/src/agent-state.ts +125 -0
- package/src/agent-sync.ts +171 -88
- package/src/agents.ts +3 -22
- package/src/catalog.ts +47 -0
- package/src/completion-mailbox.ts +656 -0
- package/src/config.ts +43 -35
- package/src/coordinator.ts +2172 -326
- package/src/descriptor.ts +96 -33
- package/src/index.ts +176 -58
- package/src/mailbox.ts +451 -0
- package/src/providers.ts +221 -28
- package/src/render.ts +23 -16
- package/src/scheduler.ts +173 -0
- package/src/schemas.ts +76 -38
- package/src/task-path.ts +146 -0
- package/src/types.ts +53 -15
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,
|
|
@@ -69,20 +123,23 @@ import {
|
|
|
69
123
|
type SubagentProviderName,
|
|
70
124
|
type SubagentRunResult,
|
|
71
125
|
type SubagentSettings,
|
|
126
|
+
type RuntimeMode,
|
|
72
127
|
type SubagentStopReason,
|
|
73
128
|
type TraceItem,
|
|
74
129
|
} from "./types.ts";
|
|
75
130
|
|
|
76
131
|
const REPORT_CUSTOM_TYPE = "pi-subagent/report";
|
|
77
|
-
const SETTLED_CUSTOM_TYPE = "pi-subagent/settled";
|
|
78
132
|
const AGENT_CUSTOM_TYPE = "pi-subagent/agent";
|
|
79
133
|
const LINEAGE_CUSTOM_TYPE = "pi-subagent/lineage";
|
|
80
134
|
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
|
]);
|
|
@@ -96,22 +153,57 @@ const DELEGATION_SCOPE_PROMPT = [
|
|
|
96
153
|
const REPORT_PROMPT = [
|
|
97
154
|
"You have a `report` tool that sends a selected update to the agent that started you.",
|
|
98
155
|
"Call it with a self-contained answer before finishing, and earlier when a finding changes what the parent should do.",
|
|
99
|
-
"
|
|
156
|
+
"A report is recorded for the parent without waking it; it does not end this turn and does not prevent later follow-up messages.",
|
|
157
|
+
"Your final answer is delivered separately as a completion update.",
|
|
100
158
|
].join(" ");
|
|
101
159
|
|
|
102
160
|
export interface DelegationInput {
|
|
103
161
|
agent: string;
|
|
162
|
+
task_name?: string;
|
|
104
163
|
description: string;
|
|
105
164
|
prompt: string;
|
|
106
|
-
|
|
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 interface SendMessageOutcome {
|
|
176
|
+
kind: "mailbox";
|
|
177
|
+
agentId: string;
|
|
178
|
+
taskPath: string;
|
|
179
|
+
messageId: string;
|
|
180
|
+
pendingMessages: number;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface FollowupTaskOutcome {
|
|
184
|
+
agentId: string;
|
|
185
|
+
taskPath: string;
|
|
186
|
+
turnId: string;
|
|
187
|
+
claimedMessages: number;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface InterruptOutcome {
|
|
191
|
+
agentId: string;
|
|
192
|
+
taskPath: string;
|
|
193
|
+
active: boolean;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface WaitAgentOutcome {
|
|
197
|
+
timedOut: boolean;
|
|
198
|
+
timeoutMs: number;
|
|
199
|
+
updates: CompletionUpdate[];
|
|
200
|
+
unreadUpdates: number;
|
|
201
|
+
taskPaths: Record<string, string>;
|
|
202
|
+
}
|
|
203
|
+
|
|
113
204
|
interface ParentRef {
|
|
114
205
|
agentId: string;
|
|
206
|
+
taskPath: string;
|
|
115
207
|
depth: number;
|
|
116
208
|
cwd: string;
|
|
117
209
|
sessionManager: SessionView;
|
|
@@ -124,7 +216,6 @@ interface ParentRef {
|
|
|
124
216
|
customType: string,
|
|
125
217
|
content: string,
|
|
126
218
|
details: ParentMessageDetails,
|
|
127
|
-
delivery: "wakeup" | "quiet",
|
|
128
219
|
): Promise<void>;
|
|
129
220
|
}
|
|
130
221
|
|
|
@@ -136,21 +227,52 @@ interface Activation {
|
|
|
136
227
|
runtime: AgentSessionRuntime;
|
|
137
228
|
seedMessageCount: number;
|
|
138
229
|
epochMessageStart: number;
|
|
139
|
-
|
|
230
|
+
controlState: AgentControlState;
|
|
140
231
|
trace: TraceItem[];
|
|
141
232
|
streamedText: string;
|
|
142
233
|
usage: ReturnType<typeof emptyUsage>;
|
|
234
|
+
startedTurnIds: Set<string>;
|
|
235
|
+
silentSettlementTurnIds: Set<string>;
|
|
236
|
+
pendingMailboxClaims: Set<string>;
|
|
237
|
+
userMessageGates: Map<string, UserMessageGate>;
|
|
143
238
|
ownedChildren: Set<string>;
|
|
144
239
|
currentRun?: Promise<SubagentRunResult>;
|
|
145
240
|
pendingSettlement?: SubagentRunResult;
|
|
146
241
|
unsubscribe?: () => void;
|
|
147
242
|
onUpdate?: (details: DelegationDetails) => void;
|
|
148
243
|
published: boolean;
|
|
244
|
+
everPublished: boolean;
|
|
149
245
|
suppressSettlement: boolean;
|
|
150
246
|
finalizing: boolean;
|
|
151
|
-
finalizePromise?: Promise<
|
|
247
|
+
finalizePromise?: Promise<boolean>;
|
|
248
|
+
holdsBackgroundSlot: boolean;
|
|
249
|
+
turnAbortController?: AbortController;
|
|
250
|
+
persistenceGate: PersistenceGate;
|
|
152
251
|
disposed: boolean;
|
|
153
252
|
lastError?: string;
|
|
253
|
+
ownerActivation?: Activation;
|
|
254
|
+
lastUsedSequence: number;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
interface UserMessageGate {
|
|
258
|
+
prepare(): void;
|
|
259
|
+
accept(): void;
|
|
260
|
+
reject(error: Error): void;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
interface PersistenceGate {
|
|
264
|
+
promise: Promise<void>;
|
|
265
|
+
resolve(): void;
|
|
266
|
+
reject(error: Error): void;
|
|
267
|
+
readonly settled: boolean;
|
|
268
|
+
readonly state: "pending" | "fulfilled" | "rejected";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
interface CompletionWaiter {
|
|
272
|
+
promise: Promise<"activity" | "timeout">;
|
|
273
|
+
wake(): void;
|
|
274
|
+
reject(error: Error): void;
|
|
275
|
+
dispose(): void;
|
|
154
276
|
}
|
|
155
277
|
|
|
156
278
|
interface CreateActivationOptions {
|
|
@@ -161,16 +283,53 @@ interface CreateActivationOptions {
|
|
|
161
283
|
onUpdate?: (details: DelegationDetails) => void;
|
|
162
284
|
}
|
|
163
285
|
|
|
286
|
+
interface StartPromptOptions {
|
|
287
|
+
detachAtAcceptance: boolean;
|
|
288
|
+
waitForCapacity: boolean;
|
|
289
|
+
preparePrompt?: (turnId: string) => string;
|
|
290
|
+
onPromptAccepted?: (turnId: string) => void;
|
|
291
|
+
acceptAfterUserMessage?: boolean;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
interface PendingPromptStart {
|
|
295
|
+
activation: Activation;
|
|
296
|
+
accepted: Promise<void>;
|
|
297
|
+
coldPrepared?: PreparedChildSession;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
type SerializedSendMessageOutcome = SendMessageOutcome;
|
|
301
|
+
|
|
302
|
+
interface PendingFollowupStart extends PendingPromptStart {
|
|
303
|
+
outcome: FollowupTaskOutcome;
|
|
304
|
+
}
|
|
305
|
+
|
|
164
306
|
interface CatalogRecord {
|
|
165
307
|
agentId: string;
|
|
308
|
+
piSessionId: string;
|
|
309
|
+
taskPath: string;
|
|
166
310
|
descriptor: SubagentDescriptor;
|
|
167
311
|
sessionFile?: string;
|
|
168
312
|
active?: Activation;
|
|
313
|
+
pendingMessages: number;
|
|
314
|
+
unreadUpdatesByChild: Map<string, number>;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
interface ResolvedTarget {
|
|
318
|
+
agentId: string;
|
|
319
|
+
taskPath: string;
|
|
320
|
+
record?: CatalogRecord;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
interface ReservedTask {
|
|
324
|
+
name: string;
|
|
325
|
+
path: string;
|
|
326
|
+
release(): void;
|
|
169
327
|
}
|
|
170
328
|
|
|
171
329
|
interface CoordinatorCatalog {
|
|
172
330
|
records: CatalogRecord[];
|
|
173
331
|
diagnostics: CatalogEntry[];
|
|
332
|
+
rootUnreadUpdatesByChild: Map<string, number>;
|
|
174
333
|
}
|
|
175
334
|
|
|
176
335
|
function runtimeFromRegistry(registry: ModelRegistry): ModelRuntime {
|
|
@@ -195,6 +354,79 @@ function errorText(error: unknown): string {
|
|
|
195
354
|
return error instanceof Error ? error.message : String(error);
|
|
196
355
|
}
|
|
197
356
|
|
|
357
|
+
function abortReason(signal: AbortSignal): Error {
|
|
358
|
+
return signal.reason instanceof Error
|
|
359
|
+
? signal.reason
|
|
360
|
+
: new Error(signal.reason ? String(signal.reason) : "operation aborted");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function waitForPromise<T>(
|
|
364
|
+
promise: Promise<T>,
|
|
365
|
+
signal?: AbortSignal,
|
|
366
|
+
): Promise<T> {
|
|
367
|
+
if (!signal) return promise;
|
|
368
|
+
if (signal.aborted) return Promise.reject(abortReason(signal));
|
|
369
|
+
return new Promise<T>((resolvePromise, rejectPromise) => {
|
|
370
|
+
const abort = () => {
|
|
371
|
+
rejectPromise(abortReason(signal));
|
|
372
|
+
};
|
|
373
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
374
|
+
promise.then(
|
|
375
|
+
(value) => {
|
|
376
|
+
signal.removeEventListener("abort", abort);
|
|
377
|
+
resolvePromise(value);
|
|
378
|
+
},
|
|
379
|
+
(error) => {
|
|
380
|
+
signal.removeEventListener("abort", abort);
|
|
381
|
+
rejectPromise(error);
|
|
382
|
+
},
|
|
383
|
+
);
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function createPersistenceGate(
|
|
388
|
+
session: Pick<SessionView, "getEntries">,
|
|
389
|
+
requireNewAssistant: boolean,
|
|
390
|
+
): PersistenceGate {
|
|
391
|
+
const alreadyDurable = !requireNewAssistant && session
|
|
392
|
+
.getEntries()
|
|
393
|
+
.some(
|
|
394
|
+
(entry) =>
|
|
395
|
+
entry.type === "message"
|
|
396
|
+
&& entry.message.role === "assistant",
|
|
397
|
+
);
|
|
398
|
+
let state: PersistenceGate["state"] =
|
|
399
|
+
alreadyDurable ? "fulfilled" : "pending";
|
|
400
|
+
let resolvePromise!: () => void;
|
|
401
|
+
let rejectPromise!: (error: Error) => void;
|
|
402
|
+
const promise = alreadyDurable
|
|
403
|
+
? Promise.resolve()
|
|
404
|
+
: new Promise<void>((resolve, reject) => {
|
|
405
|
+
resolvePromise = resolve;
|
|
406
|
+
rejectPromise = reject;
|
|
407
|
+
});
|
|
408
|
+
void promise.catch(() => {});
|
|
409
|
+
return {
|
|
410
|
+
promise,
|
|
411
|
+
resolve: () => {
|
|
412
|
+
if (state !== "pending") return;
|
|
413
|
+
state = "fulfilled";
|
|
414
|
+
resolvePromise();
|
|
415
|
+
},
|
|
416
|
+
reject: (error) => {
|
|
417
|
+
if (state !== "pending") return;
|
|
418
|
+
state = "rejected";
|
|
419
|
+
rejectPromise(error);
|
|
420
|
+
},
|
|
421
|
+
get settled() {
|
|
422
|
+
return state !== "pending";
|
|
423
|
+
},
|
|
424
|
+
get state() {
|
|
425
|
+
return state;
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
198
430
|
/**
|
|
199
431
|
* Read the durable pi-subagent control id recorded for a session, if any.
|
|
200
432
|
*
|
|
@@ -246,17 +478,88 @@ function stopReasonHeadline(reason: SubagentStopReason): string {
|
|
|
246
478
|
function makeRuntimeSettings(descriptor: SubagentDescriptor): SubagentSettings {
|
|
247
479
|
return {
|
|
248
480
|
agentScope: descriptor.runtime.agentScope,
|
|
249
|
-
syncBundledAgents: descriptor.runtime.syncBundledAgents,
|
|
250
481
|
maxDepth: descriptor.runtime.maxDepth,
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
482
|
+
runtimeMode: descriptor.runtime.runtimeMode,
|
|
483
|
+
maxConcurrentBackgroundRuns: descriptor.runtime.maxConcurrentBackgroundRuns,
|
|
484
|
+
maxIdleRuntimes: descriptor.runtime.maxIdleRuntimes,
|
|
254
485
|
inheritExtensions: descriptor.runtime.inheritExtensions,
|
|
255
486
|
openAIIdentity: descriptor.runtime.openAIIdentity,
|
|
256
487
|
maxOutputBytes: descriptor.runtime.maxOutputBytes,
|
|
257
488
|
};
|
|
258
489
|
}
|
|
259
490
|
|
|
491
|
+
function delegationContext(
|
|
492
|
+
providerName: SubagentProviderName,
|
|
493
|
+
input: DelegationInput,
|
|
494
|
+
): ContextInheritance {
|
|
495
|
+
if (providerName === "fork") {
|
|
496
|
+
if (input.context !== undefined) {
|
|
497
|
+
throw new Error(
|
|
498
|
+
"subagent_fork always uses all_completed context; use subagent for another context policy",
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
return { mode: "all_completed" };
|
|
502
|
+
}
|
|
503
|
+
const context = input.context;
|
|
504
|
+
if (!context) return { mode: "fresh" };
|
|
505
|
+
if (context.mode === "last_n_completed") {
|
|
506
|
+
if (
|
|
507
|
+
!Number.isSafeInteger(context.completed_turns)
|
|
508
|
+
|| context.completed_turns === undefined
|
|
509
|
+
|| context.completed_turns < 1
|
|
510
|
+
|| context.completed_turns > 100
|
|
511
|
+
) {
|
|
512
|
+
throw new Error(
|
|
513
|
+
"context.completed_turns must be an integer between 1 and 100 for last_n_completed",
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
mode: context.mode,
|
|
518
|
+
completedTurns: context.completed_turns,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
if (
|
|
522
|
+
context.mode !== "fresh"
|
|
523
|
+
&& context.mode !== "all_completed"
|
|
524
|
+
) {
|
|
525
|
+
throw new Error(`unsupported context mode: ${String(context.mode)}`);
|
|
526
|
+
}
|
|
527
|
+
if (context.completed_turns !== undefined) {
|
|
528
|
+
throw new Error(
|
|
529
|
+
"context.completed_turns is available only for last_n_completed",
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
return { mode: context.mode };
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function mailboxOwner(descriptor: SubagentDescriptor): {
|
|
536
|
+
parentAgentId: string;
|
|
537
|
+
agentId: string;
|
|
538
|
+
} {
|
|
539
|
+
return {
|
|
540
|
+
parentAgentId: descriptor.parentAgentId,
|
|
541
|
+
agentId: descriptor.agentId,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function completionWaiterKey(parent: ParentRef): string {
|
|
546
|
+
return `${parent.agentId}:${parent.sessionManager.getSessionId()}`;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function waitTimeout(value: number | undefined): number {
|
|
550
|
+
const timeoutMs = value ?? DEFAULT_WAIT_AGENT_TIMEOUT_MS;
|
|
551
|
+
if (
|
|
552
|
+
!Number.isSafeInteger(timeoutMs)
|
|
553
|
+
|| timeoutMs < 0
|
|
554
|
+
|| timeoutMs > MAX_WAIT_AGENT_TIMEOUT_MS
|
|
555
|
+
) {
|
|
556
|
+
throw new Error(
|
|
557
|
+
`timeout_ms must be a safe integer between 0 and ${MAX_WAIT_AGENT_TIMEOUT_MS}`,
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
return timeoutMs;
|
|
561
|
+
}
|
|
562
|
+
|
|
260
563
|
function isPathInside(parent: string, child: string): boolean {
|
|
261
564
|
const rel = relative(resolve(parent), resolve(child));
|
|
262
565
|
return rel === "" || (!rel.startsWith("..") && !rel.startsWith("/"));
|
|
@@ -288,8 +591,19 @@ async function loadCodexIdentityInlineExtension(
|
|
|
288
591
|
export class SubagentCoordinator {
|
|
289
592
|
private readonly providers = new ProviderRegistry();
|
|
290
593
|
private readonly active = new Map<string, Activation>();
|
|
594
|
+
private readonly agentOperations = new AgentOperationQueue();
|
|
595
|
+
private readonly completionOperations = new AgentOperationQueue();
|
|
596
|
+
private readonly idleRuntimeOperations = new AgentOperationQueue();
|
|
597
|
+
private readonly backgroundRuns = new BackgroundRunLimiter();
|
|
598
|
+
private readonly admittedOperations = new Set<Promise<unknown>>();
|
|
599
|
+
private readonly completionWaiters = new Map<string, CompletionWaiter>();
|
|
600
|
+
private readonly reservedTaskPaths = new Set<string>();
|
|
601
|
+
private readonly runtimeId = uuidv7();
|
|
602
|
+
private idleRuntimeLimit = 0;
|
|
603
|
+
private activationSequence = 0;
|
|
291
604
|
private agentSyncResult: AgentSyncResult | undefined;
|
|
292
605
|
private draining = false;
|
|
606
|
+
private shutdownPromise: Promise<void> | undefined;
|
|
293
607
|
|
|
294
608
|
constructor(
|
|
295
609
|
private readonly pi: ExtensionAPI,
|
|
@@ -314,26 +628,39 @@ export class SubagentCoordinator {
|
|
|
314
628
|
return join(this.agentDir, "agents");
|
|
315
629
|
}
|
|
316
630
|
|
|
631
|
+
configureBackgroundRuns(limit: number): void {
|
|
632
|
+
this.backgroundRuns.configure(limit);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
async configureIdleRuntimes(limit: number): Promise<void> {
|
|
636
|
+
if (!Number.isSafeInteger(limit) || limit < 0) {
|
|
637
|
+
throw new Error("maxIdleRuntimes must be a non-negative safe integer");
|
|
638
|
+
}
|
|
639
|
+
this.idleRuntimeLimit = limit;
|
|
640
|
+
await this.trimIdleRuntimes();
|
|
641
|
+
}
|
|
642
|
+
|
|
317
643
|
discoverAvailableAgents(
|
|
318
644
|
cwd: string,
|
|
319
645
|
settings: SubagentSettings,
|
|
320
646
|
projectTrusted: boolean,
|
|
321
647
|
): AgentDiscoveryResult {
|
|
322
|
-
if (
|
|
648
|
+
if (!this.agentSyncResult) {
|
|
323
649
|
this.synchronizeBundledAgents();
|
|
324
650
|
}
|
|
325
|
-
const
|
|
326
|
-
? undefined
|
|
327
|
-
: unmodifiedManagedAgentNames(this.agentDir);
|
|
328
|
-
return discoverAgents({
|
|
651
|
+
const discovery = discoverAgents({
|
|
329
652
|
cwd,
|
|
330
653
|
scope: settings.agentScope,
|
|
331
654
|
projectTrusted,
|
|
332
|
-
bundledDir: this.bundledAgentsDir,
|
|
333
655
|
agentDir: this.agentDir,
|
|
334
|
-
includeBundled: !settings.syncBundledAgents && settings.agentScope !== "project",
|
|
335
|
-
excludeUserAgentNames,
|
|
336
656
|
});
|
|
657
|
+
return {
|
|
658
|
+
...discovery,
|
|
659
|
+
diagnostics: [
|
|
660
|
+
...(this.agentSyncResult?.diagnostics ?? []),
|
|
661
|
+
...discovery.diagnostics,
|
|
662
|
+
],
|
|
663
|
+
};
|
|
337
664
|
}
|
|
338
665
|
|
|
339
666
|
async parentFromContext(ctx: ExtensionContext): Promise<ParentRef> {
|
|
@@ -344,6 +671,9 @@ export class SubagentCoordinator {
|
|
|
344
671
|
agentId: readAgentId(ctx.sessionManager) ?? ensureAgentId(
|
|
345
672
|
ctx.sessionManager as unknown as SessionView,
|
|
346
673
|
),
|
|
674
|
+
taskPath: descriptor
|
|
675
|
+
? descriptorTaskPath(descriptor)
|
|
676
|
+
: ROOT_TASK_PATH,
|
|
347
677
|
depth: descriptor?.depth ?? 0,
|
|
348
678
|
cwd: ctx.cwd,
|
|
349
679
|
// ExtensionContext narrows the live SessionManager to a read-only
|
|
@@ -354,13 +684,13 @@ export class SubagentCoordinator {
|
|
|
354
684
|
model: ctx.model,
|
|
355
685
|
thinkingLevel: ctx.thinkingLevel ?? "off",
|
|
356
686
|
projectTrusted: ctx.isProjectTrusted(),
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
687
|
+
// Reports never start a parent turn. The entry becomes part of the
|
|
688
|
+
// parent session (and therefore its next model context) while the
|
|
689
|
+
// parent keeps working; durable completions are read with wait_agent.
|
|
690
|
+
deliver: async (customType, content, details) => {
|
|
691
|
+
const options = ctx.isIdle()
|
|
692
|
+
? { triggerTurn: false }
|
|
693
|
+
: { triggerTurn: false, deliverAs: "nextTurn" as const };
|
|
364
694
|
this.pi.sendMessage({ customType, content, display: true, details }, options);
|
|
365
695
|
},
|
|
366
696
|
};
|
|
@@ -374,34 +704,47 @@ export class SubagentCoordinator {
|
|
|
374
704
|
signal?: AbortSignal,
|
|
375
705
|
onUpdate?: (details: DelegationDetails) => void,
|
|
376
706
|
agentDiscovery?: AgentDiscoveryResult,
|
|
707
|
+
): Promise<DelegationOutcome> {
|
|
708
|
+
return this.runAdmittedOperation(() =>
|
|
709
|
+
this.delegateAdmitted(
|
|
710
|
+
parent,
|
|
711
|
+
providerName,
|
|
712
|
+
input,
|
|
713
|
+
settings,
|
|
714
|
+
signal,
|
|
715
|
+
onUpdate,
|
|
716
|
+
agentDiscovery,
|
|
717
|
+
),
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
private async delegateAdmitted(
|
|
722
|
+
parent: ParentRef,
|
|
723
|
+
providerName: SubagentProviderName,
|
|
724
|
+
input: DelegationInput,
|
|
725
|
+
settings: SubagentSettings,
|
|
726
|
+
signal?: AbortSignal,
|
|
727
|
+
onUpdate?: (details: DelegationDetails) => void,
|
|
728
|
+
agentDiscovery?: AgentDiscoveryResult,
|
|
377
729
|
): Promise<DelegationOutcome> {
|
|
378
730
|
if (this.draining) throw new Error("pi-subagent is shutting down; no new delegation was accepted");
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
throw new Error(
|
|
386
|
-
"run_in_background is disabled by pi-subagent foreground-only mode (enableRunInBackground: false)",
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
const runInBackground =
|
|
390
|
-
providerName === "spawn" && settings.enableRunInBackground
|
|
391
|
-
? (input.run_in_background ?? settings.defaultBackground)
|
|
392
|
-
: false;
|
|
393
|
-
const mode: SubagentMode = runInBackground ? "continuable" : "one-shot";
|
|
731
|
+
const context = delegationContext(providerName, input);
|
|
732
|
+
const resolvedProviderName: SubagentProviderName =
|
|
733
|
+
context.mode === "fresh" ? "spawn" : "fork";
|
|
734
|
+
const provider = this.providers.get(resolvedProviderName);
|
|
735
|
+
const mode: SubagentMode =
|
|
736
|
+
settings.runtimeMode === "background" ? "continuable" : "one-shot";
|
|
394
737
|
if (mode === "continuable" && !provider.supportsContinuable) {
|
|
395
738
|
throw new Error(`subagent provider "${provider.name}" does not support continuable children`);
|
|
396
739
|
}
|
|
397
740
|
if (mode === "continuable" && !parent.sessionManager.getSessionFile()) {
|
|
398
741
|
throw new Error(
|
|
399
|
-
|
|
742
|
+
'continuable subagents require a persisted parent session; set runtimeMode to "foreground" to delegate from an ephemeral session',
|
|
400
743
|
);
|
|
401
744
|
}
|
|
402
745
|
if (mode === "continuable" && parent.activation?.descriptor.mode === "one-shot") {
|
|
403
746
|
throw new Error(
|
|
404
|
-
"a
|
|
747
|
+
"a foreground child cannot leave a continuable descendant behind; its runtime mode is fixed at creation",
|
|
405
748
|
);
|
|
406
749
|
}
|
|
407
750
|
|
|
@@ -429,57 +772,71 @@ export class SubagentCoordinator {
|
|
|
429
772
|
|
|
430
773
|
const model = this.resolveModel(parent, agent);
|
|
431
774
|
const thinkingLevel = agent.thinking ?? parent.thinkingLevel;
|
|
432
|
-
const prepared = await provider.prepare(parent, mode);
|
|
433
775
|
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
|
-
|
|
776
|
+
const reservedTask = await this.reserveTask(
|
|
777
|
+
parent,
|
|
778
|
+
input.task_name,
|
|
779
|
+
input.description,
|
|
780
|
+
);
|
|
781
|
+
let prepared: PreparedChildSession | undefined;
|
|
481
782
|
let activation: Activation | undefined;
|
|
783
|
+
let keepTaskReservation = false;
|
|
482
784
|
try {
|
|
785
|
+
prepared = await provider.prepare(parent, mode, context);
|
|
786
|
+
if (this.draining) {
|
|
787
|
+
throw new Error("pi-subagent is shutting down; no new delegation was accepted");
|
|
788
|
+
}
|
|
789
|
+
const openAIIdentityEnabled =
|
|
790
|
+
settings.openAIIdentity && isOpenAIResponsesModel(model);
|
|
791
|
+
const descriptor: SubagentDescriptor = {
|
|
792
|
+
version: DESCRIPTOR_VERSION,
|
|
793
|
+
mode,
|
|
794
|
+
provider: resolvedProviderName,
|
|
795
|
+
label: input.description.trim(),
|
|
796
|
+
agentId: uuidv7(),
|
|
797
|
+
parentAgentId: parent.agentId,
|
|
798
|
+
parentPiSessionId: parent.sessionManager.getSessionId(),
|
|
799
|
+
...(parent.sessionManager.getSessionFile()
|
|
800
|
+
? { parentSessionFile: parent.sessionManager.getSessionFile() }
|
|
801
|
+
: {}),
|
|
802
|
+
depth,
|
|
803
|
+
cwd: parent.cwd,
|
|
804
|
+
createdAt: new Date().toISOString(),
|
|
805
|
+
agent: snapshotAgent(agent),
|
|
806
|
+
model: { provider: model.provider, id: model.id },
|
|
807
|
+
thinkingLevel,
|
|
808
|
+
task: {
|
|
809
|
+
name: reservedTask.name,
|
|
810
|
+
path: reservedTask.path,
|
|
811
|
+
},
|
|
812
|
+
context,
|
|
813
|
+
runtime: {
|
|
814
|
+
agentScope: settings.agentScope,
|
|
815
|
+
maxDepth: settings.maxDepth,
|
|
816
|
+
runtimeMode: settings.runtimeMode,
|
|
817
|
+
maxConcurrentBackgroundRuns: settings.maxConcurrentBackgroundRuns,
|
|
818
|
+
maxIdleRuntimes: settings.maxIdleRuntimes,
|
|
819
|
+
inheritExtensions: settings.inheritExtensions,
|
|
820
|
+
openAIIdentity: openAIIdentityEnabled,
|
|
821
|
+
maxOutputBytes: settings.maxOutputBytes,
|
|
822
|
+
},
|
|
823
|
+
};
|
|
824
|
+
prepared.sessionManager.appendCustomEntry(AGENT_CUSTOM_TYPE, {
|
|
825
|
+
agentId: descriptor.agentId,
|
|
826
|
+
});
|
|
827
|
+
prepared.sessionManager.appendCustomEntry(LINEAGE_CUSTOM_TYPE, {
|
|
828
|
+
version: 1,
|
|
829
|
+
agentId: descriptor.agentId,
|
|
830
|
+
parentAgentId: descriptor.parentAgentId,
|
|
831
|
+
parentPiSessionId: descriptor.parentPiSessionId,
|
|
832
|
+
relation: descriptor.provider,
|
|
833
|
+
agentName: descriptor.agent.name,
|
|
834
|
+
taskPath: descriptor.task.path,
|
|
835
|
+
openAIIdentity: openAIIdentityEnabled,
|
|
836
|
+
...(descriptor.parentSessionFile
|
|
837
|
+
? { parentSessionFile: descriptor.parentSessionFile }
|
|
838
|
+
: {}),
|
|
839
|
+
});
|
|
483
840
|
activation = await this.createActivation({
|
|
484
841
|
parent,
|
|
485
842
|
descriptor,
|
|
@@ -487,11 +844,18 @@ export class SubagentCoordinator {
|
|
|
487
844
|
isNew: true,
|
|
488
845
|
onUpdate,
|
|
489
846
|
});
|
|
847
|
+
if (this.draining) {
|
|
848
|
+
throw new Error("pi-subagent is shutting down; no new delegation was accepted");
|
|
849
|
+
}
|
|
490
850
|
if (mode === "continuable" && parent.activation) {
|
|
491
|
-
|
|
851
|
+
this.acquireParentOwnership(activation, parent);
|
|
492
852
|
}
|
|
493
|
-
const started = this.startPrompt(activation, input.prompt, signal,
|
|
853
|
+
const started = this.startPrompt(activation, input.prompt, signal, {
|
|
854
|
+
detachAtAcceptance: mode === "continuable",
|
|
855
|
+
waitForCapacity: !parent.activation?.holdsBackgroundSlot,
|
|
856
|
+
});
|
|
494
857
|
await started.accepted;
|
|
858
|
+
keepTaskReservation = true;
|
|
495
859
|
if (mode === "continuable") {
|
|
496
860
|
activation.onUpdate = undefined;
|
|
497
861
|
return { kind: "continuable", details: this.detailsOf(activation) };
|
|
@@ -503,37 +867,197 @@ export class SubagentCoordinator {
|
|
|
503
867
|
await this.disposeActivation(activation);
|
|
504
868
|
return { kind: "foreground", details, result };
|
|
505
869
|
} catch (error) {
|
|
506
|
-
if (activation && !activation.published) {
|
|
870
|
+
if (activation && prepared && !activation.published) {
|
|
507
871
|
activation.suppressSettlement = true;
|
|
508
872
|
await this.rollbackActivation(activation, prepared);
|
|
509
|
-
} else if (!activation) {
|
|
873
|
+
} else if (!activation && prepared) {
|
|
510
874
|
await prepared.rollback();
|
|
511
875
|
}
|
|
512
876
|
throw error;
|
|
877
|
+
} finally {
|
|
878
|
+
if (!keepTaskReservation) reservedTask.release();
|
|
513
879
|
}
|
|
514
880
|
}
|
|
515
881
|
|
|
516
|
-
async sendMessage(
|
|
882
|
+
async sendMessage(
|
|
883
|
+
parent: ParentRef,
|
|
884
|
+
childId: string,
|
|
885
|
+
message: string,
|
|
886
|
+
signal?: AbortSignal,
|
|
887
|
+
): Promise<void> {
|
|
888
|
+
await this.sendMessageWithOutcome(parent, childId, message, signal);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
async sendMessageWithOutcome(
|
|
892
|
+
parent: ParentRef,
|
|
893
|
+
childId: string,
|
|
894
|
+
message: string,
|
|
895
|
+
signal?: AbortSignal,
|
|
896
|
+
): Promise<SendMessageOutcome> {
|
|
897
|
+
return this.runAdmittedOperation(() =>
|
|
898
|
+
this.sendMessageAdmitted(parent, childId, message, signal),
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
private async sendMessageAdmitted(
|
|
903
|
+
parent: ParentRef,
|
|
904
|
+
childId: string,
|
|
905
|
+
message: string,
|
|
906
|
+
signal?: AbortSignal,
|
|
907
|
+
): Promise<SendMessageOutcome> {
|
|
517
908
|
if (this.draining) throw new Error("pi-subagent is shutting down; message was not delivered");
|
|
909
|
+
const target = await this.resolveTarget(parent, childId);
|
|
910
|
+
return this.agentOperations.run(target.agentId, () =>
|
|
911
|
+
this.sendMessageSerialized(parent, target.agentId, message, signal),
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
private async sendMessageSerialized(
|
|
916
|
+
parent: ParentRef,
|
|
917
|
+
childId: string,
|
|
918
|
+
message: string,
|
|
919
|
+
signal?: AbortSignal,
|
|
920
|
+
): Promise<SerializedSendMessageOutcome> {
|
|
921
|
+
if (this.draining) {
|
|
922
|
+
throw new Error("pi-subagent is shutting down; message was not delivered");
|
|
923
|
+
}
|
|
518
924
|
let activation = this.active.get(childId);
|
|
519
|
-
|
|
520
|
-
if (activation
|
|
521
|
-
|
|
522
|
-
activation
|
|
925
|
+
if (activation?.disposed) activation = undefined;
|
|
926
|
+
if (activation) {
|
|
927
|
+
this.touchActivation(activation);
|
|
928
|
+
this.assertContinuableDirectChild(parent, activation.descriptor);
|
|
929
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
930
|
+
await waitForPromise(activation.persistenceGate.promise, signal);
|
|
931
|
+
if (this.draining || activation.disposed) {
|
|
932
|
+
throw new Error(
|
|
933
|
+
`subagent ${childId} became unavailable before its mailbox could be persisted`,
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
const enqueued = enqueueMailboxMessage(
|
|
937
|
+
activation.runtime.session.sessionManager,
|
|
938
|
+
{
|
|
939
|
+
senderAgentId: parent.agentId,
|
|
940
|
+
recipientAgentId: childId,
|
|
941
|
+
content: message,
|
|
942
|
+
},
|
|
943
|
+
);
|
|
944
|
+
return {
|
|
945
|
+
kind: "mailbox",
|
|
946
|
+
agentId: childId,
|
|
947
|
+
taskPath: descriptorTaskPath(activation.descriptor),
|
|
948
|
+
messageId: enqueued.message.messageId,
|
|
949
|
+
pendingMessages: enqueued.pendingMessages,
|
|
950
|
+
};
|
|
523
951
|
}
|
|
952
|
+
const located = await this.findPersistedChild(parent, childId);
|
|
953
|
+
if (!located) throw new Error(`unknown subagent: ${childId}; message was not delivered`);
|
|
954
|
+
this.assertContinuableDirectChild(parent, located.descriptor);
|
|
955
|
+
const manager = SessionManager.open(
|
|
956
|
+
located.sessionFile,
|
|
957
|
+
parent.sessionManager.getSessionDir(),
|
|
958
|
+
parent.cwd,
|
|
959
|
+
);
|
|
960
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
961
|
+
const enqueued = enqueueMailboxMessage(manager, {
|
|
962
|
+
senderAgentId: parent.agentId,
|
|
963
|
+
recipientAgentId: childId,
|
|
964
|
+
content: message,
|
|
965
|
+
});
|
|
966
|
+
return {
|
|
967
|
+
kind: "mailbox",
|
|
968
|
+
agentId: childId,
|
|
969
|
+
taskPath: descriptorTaskPath(located.descriptor),
|
|
970
|
+
messageId: enqueued.message.messageId,
|
|
971
|
+
pendingMessages: enqueued.pendingMessages,
|
|
972
|
+
};
|
|
973
|
+
}
|
|
524
974
|
|
|
525
|
-
|
|
975
|
+
async followupTask(
|
|
976
|
+
parent: ParentRef,
|
|
977
|
+
childId: string,
|
|
978
|
+
signal?: AbortSignal,
|
|
979
|
+
): Promise<FollowupTaskOutcome> {
|
|
980
|
+
return this.runAdmittedOperation(() =>
|
|
981
|
+
this.followupTaskAdmitted(parent, childId, signal),
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
private async followupTaskAdmitted(
|
|
986
|
+
parent: ParentRef,
|
|
987
|
+
childId: string,
|
|
988
|
+
signal?: AbortSignal,
|
|
989
|
+
): Promise<FollowupTaskOutcome> {
|
|
990
|
+
if (this.draining) {
|
|
991
|
+
throw new Error("pi-subagent is shutting down; follow-up task was not started");
|
|
992
|
+
}
|
|
993
|
+
const target = await this.resolveTarget(parent, childId);
|
|
994
|
+
const pending = await this.agentOperations.run(target.agentId, () =>
|
|
995
|
+
this.followupTaskSerialized(parent, target.agentId, signal),
|
|
996
|
+
);
|
|
997
|
+
try {
|
|
998
|
+
await pending.accepted;
|
|
999
|
+
} catch (error) {
|
|
1000
|
+
if (pending.coldPrepared && !pending.activation.published) {
|
|
1001
|
+
await this.agentOperations.run(target.agentId, async () => {
|
|
1002
|
+
if (this.active.get(target.agentId) !== pending.activation) return;
|
|
1003
|
+
pending.activation.suppressSettlement = true;
|
|
1004
|
+
await this.rollbackActivation(
|
|
1005
|
+
pending.activation,
|
|
1006
|
+
pending.coldPrepared!,
|
|
1007
|
+
);
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
throw error;
|
|
1011
|
+
}
|
|
1012
|
+
return pending.outcome;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
private async followupTaskSerialized(
|
|
1016
|
+
parent: ParentRef,
|
|
1017
|
+
childId: string,
|
|
1018
|
+
signal?: AbortSignal,
|
|
1019
|
+
): Promise<PendingFollowupStart> {
|
|
1020
|
+
if (this.draining) {
|
|
1021
|
+
throw new Error("pi-subagent is shutting down; follow-up task was not started");
|
|
1022
|
+
}
|
|
1023
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
1024
|
+
let activation = this.active.get(childId);
|
|
1025
|
+
if (activation?.disposed) activation = undefined;
|
|
1026
|
+
let descriptor: SubagentDescriptor;
|
|
1027
|
+
let manager: SessionManager;
|
|
1028
|
+
let coldPrepared: PreparedChildSession | undefined;
|
|
1029
|
+
|
|
1030
|
+
if (activation) {
|
|
1031
|
+
this.touchActivation(activation);
|
|
1032
|
+
descriptor = activation.descriptor;
|
|
1033
|
+
this.assertContinuableDirectChild(parent, descriptor);
|
|
1034
|
+
manager = activation.runtime.session.sessionManager;
|
|
1035
|
+
if (activation.currentRun || activation.runtime.session.isStreaming) {
|
|
1036
|
+
throw new Error(
|
|
1037
|
+
`subagent ${childId} already has a scheduled or running turn; mailbox messages were not consumed`,
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
} else {
|
|
526
1041
|
const located = await this.findPersistedChild(parent, childId);
|
|
527
|
-
if (!located)
|
|
528
|
-
|
|
529
|
-
throw new Error(`subagent ${childId} is one-shot and cannot accept follow-up messages`);
|
|
1042
|
+
if (!located) {
|
|
1043
|
+
throw new Error(`unknown subagent: ${childId}; follow-up task was not started`);
|
|
530
1044
|
}
|
|
531
|
-
|
|
532
|
-
|
|
1045
|
+
descriptor = located.descriptor;
|
|
1046
|
+
this.assertContinuableDirectChild(parent, descriptor);
|
|
1047
|
+
manager = SessionManager.open(
|
|
533
1048
|
located.sessionFile,
|
|
534
1049
|
parent.sessionManager.getSessionDir(),
|
|
535
1050
|
parent.cwd,
|
|
536
1051
|
);
|
|
1052
|
+
}
|
|
1053
|
+
const owner = mailboxOwner(descriptor);
|
|
1054
|
+
const batch = readMailbox(manager.getEntries(), owner).pending;
|
|
1055
|
+
if (batch.length === 0) {
|
|
1056
|
+
throw new Error(`subagent ${childId} has no pending mailbox messages`);
|
|
1057
|
+
}
|
|
1058
|
+
const messageIds = batch.map((message) => message.messageId);
|
|
1059
|
+
|
|
1060
|
+
if (!activation) {
|
|
537
1061
|
coldPrepared = {
|
|
538
1062
|
sessionManager: manager,
|
|
539
1063
|
seedMessageCount: manager.buildSessionContext().messages.length,
|
|
@@ -541,25 +1065,42 @@ export class SubagentCoordinator {
|
|
|
541
1065
|
};
|
|
542
1066
|
activation = await this.createActivation({
|
|
543
1067
|
parent,
|
|
544
|
-
descriptor
|
|
1068
|
+
descriptor,
|
|
545
1069
|
prepared: coldPrepared,
|
|
546
1070
|
isNew: false,
|
|
547
1071
|
});
|
|
548
|
-
|
|
549
|
-
} else {
|
|
550
|
-
this.assertDirectParent(parent, activation.descriptor);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
const session = activation.runtime.session;
|
|
554
|
-
if (activation.currentRun || session.isStreaming) {
|
|
555
|
-
if (signal?.aborted) throw signal.reason ?? new Error("message delivery aborted");
|
|
556
|
-
await session.followUp(message);
|
|
557
|
-
return;
|
|
1072
|
+
this.acquireParentOwnership(activation, parent);
|
|
558
1073
|
}
|
|
559
1074
|
|
|
560
1075
|
try {
|
|
561
|
-
|
|
562
|
-
|
|
1076
|
+
this.acquireParentOwnership(activation, parent);
|
|
1077
|
+
const started = this.startPrompt(activation, "", signal, {
|
|
1078
|
+
detachAtAcceptance: true,
|
|
1079
|
+
waitForCapacity: !parent.activation?.holdsBackgroundSlot,
|
|
1080
|
+
preparePrompt: (turnId) =>
|
|
1081
|
+
formatMailboxBatch(batch, turnId),
|
|
1082
|
+
onPromptAccepted: (turnId) => {
|
|
1083
|
+
claimMailboxMessages(
|
|
1084
|
+
manager,
|
|
1085
|
+
messageIds,
|
|
1086
|
+
turnId,
|
|
1087
|
+
owner,
|
|
1088
|
+
);
|
|
1089
|
+
activation.pendingMailboxClaims.add(turnId);
|
|
1090
|
+
},
|
|
1091
|
+
acceptAfterUserMessage: true,
|
|
1092
|
+
});
|
|
1093
|
+
return {
|
|
1094
|
+
activation,
|
|
1095
|
+
accepted: started.accepted,
|
|
1096
|
+
...(coldPrepared ? { coldPrepared } : {}),
|
|
1097
|
+
outcome: {
|
|
1098
|
+
agentId: childId,
|
|
1099
|
+
taskPath: descriptorTaskPath(descriptor),
|
|
1100
|
+
turnId: started.turnId,
|
|
1101
|
+
claimedMessages: batch.length,
|
|
1102
|
+
},
|
|
1103
|
+
};
|
|
563
1104
|
} catch (error) {
|
|
564
1105
|
if (coldPrepared && !activation.published) {
|
|
565
1106
|
activation.suppressSettlement = true;
|
|
@@ -569,49 +1110,449 @@ export class SubagentCoordinator {
|
|
|
569
1110
|
}
|
|
570
1111
|
}
|
|
571
1112
|
|
|
572
|
-
async
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
1113
|
+
async waitAgent(
|
|
1114
|
+
parent: ParentRef,
|
|
1115
|
+
toolCallId: string,
|
|
1116
|
+
timeoutMs?: number,
|
|
1117
|
+
signal?: AbortSignal,
|
|
1118
|
+
): Promise<WaitAgentOutcome> {
|
|
1119
|
+
return this.runAdmittedOperation(async () => {
|
|
1120
|
+
const outcome = await this.waitAgentAdmitted(
|
|
1121
|
+
parent,
|
|
1122
|
+
toolCallId,
|
|
1123
|
+
waitTimeout(timeoutMs),
|
|
1124
|
+
signal,
|
|
1125
|
+
);
|
|
1126
|
+
if (outcome.updates.length === 0) {
|
|
1127
|
+
return { ...outcome, taskPaths: {} };
|
|
1128
|
+
}
|
|
1129
|
+
let paths = new Map<string, string>();
|
|
1130
|
+
try {
|
|
1131
|
+
const catalog = await this.catalogRecords(parent);
|
|
1132
|
+
paths = new Map(
|
|
1133
|
+
catalog.records.map((record) => [
|
|
1134
|
+
record.agentId,
|
|
1135
|
+
record.taskPath,
|
|
1136
|
+
]),
|
|
1137
|
+
);
|
|
1138
|
+
} catch {
|
|
1139
|
+
// Completion delivery is already durably reserved. Readable
|
|
1140
|
+
// path enrichment is cosmetic and must not turn that delivery
|
|
1141
|
+
// into a failed tool call.
|
|
1142
|
+
}
|
|
1143
|
+
return {
|
|
1144
|
+
...outcome,
|
|
1145
|
+
taskPaths: Object.fromEntries(
|
|
1146
|
+
outcome.updates.map((update) => [
|
|
1147
|
+
update.childAgentId,
|
|
1148
|
+
paths.get(update.childAgentId)
|
|
1149
|
+
?? update.childAgentId,
|
|
1150
|
+
]),
|
|
1151
|
+
),
|
|
1152
|
+
};
|
|
581
1153
|
});
|
|
582
1154
|
}
|
|
583
1155
|
|
|
584
|
-
async
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
-
});
|
|
1156
|
+
private async waitAgentAdmitted(
|
|
1157
|
+
parent: ParentRef,
|
|
1158
|
+
toolCallId: string,
|
|
1159
|
+
timeoutMs: number,
|
|
1160
|
+
signal?: AbortSignal,
|
|
1161
|
+
): Promise<WaitAgentOutcome> {
|
|
1162
|
+
if (this.draining) {
|
|
1163
|
+
throw new Error("pi-subagent is shutting down; wait_agent was not accepted");
|
|
606
1164
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
);
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
|
|
1165
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
1166
|
+
if (!toolCallId.trim() || toolCallId.length > 512) {
|
|
1167
|
+
throw new Error("wait_agent requires a non-empty tool call id of at most 512 characters");
|
|
1168
|
+
}
|
|
1169
|
+
const key = completionWaiterKey(parent);
|
|
1170
|
+
const deadline = Date.now() + timeoutMs;
|
|
1171
|
+
while (true) {
|
|
1172
|
+
let waiter: CompletionWaiter | undefined;
|
|
1173
|
+
const immediate = await this.completionOperations.run(
|
|
1174
|
+
key,
|
|
1175
|
+
async (): Promise<WaitAgentOutcome | undefined> => {
|
|
1176
|
+
if (this.draining) {
|
|
1177
|
+
throw new Error("pi-subagent is shutting down; wait_agent was interrupted");
|
|
1178
|
+
}
|
|
1179
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
1180
|
+
if (this.completionWaiters.has(key)) {
|
|
1181
|
+
throw new Error(
|
|
1182
|
+
`wait_agent is already waiting for direct-child activity on ${parent.agentId}`,
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
const snapshot = readCompletionMailbox(
|
|
1186
|
+
parent.sessionManager.getEntries(),
|
|
1187
|
+
{
|
|
1188
|
+
parentAgentId: parent.agentId,
|
|
1189
|
+
activeRuntimeId: this.runtimeId,
|
|
1190
|
+
},
|
|
1191
|
+
);
|
|
1192
|
+
if (snapshot.currentRuntimeReservations.length > 0) {
|
|
1193
|
+
throw new Error(
|
|
1194
|
+
"a previous wait_agent delivery is awaiting its durable tool result",
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
if (snapshot.available.length > 0) {
|
|
1198
|
+
return this.reserveWaitAgentOutcome(
|
|
1199
|
+
parent,
|
|
1200
|
+
toolCallId,
|
|
1201
|
+
timeoutMs,
|
|
1202
|
+
snapshot,
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
const remaining = Math.max(0, deadline - Date.now());
|
|
1206
|
+
if (remaining === 0) {
|
|
1207
|
+
return {
|
|
1208
|
+
timedOut: true,
|
|
1209
|
+
timeoutMs,
|
|
1210
|
+
updates: [],
|
|
1211
|
+
unreadUpdates: snapshot.unread.length,
|
|
1212
|
+
taskPaths: {},
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
waiter = this.createCompletionWaiter(remaining, signal);
|
|
1216
|
+
this.completionWaiters.set(key, waiter);
|
|
1217
|
+
|
|
1218
|
+
// Append happens before notify, but fold once more after
|
|
1219
|
+
// subscription so no completion can land in the check/register gap.
|
|
1220
|
+
const rechecked = readCompletionMailbox(
|
|
1221
|
+
parent.sessionManager.getEntries(),
|
|
1222
|
+
{
|
|
1223
|
+
parentAgentId: parent.agentId,
|
|
1224
|
+
activeRuntimeId: this.runtimeId,
|
|
1225
|
+
},
|
|
1226
|
+
);
|
|
1227
|
+
if (rechecked.available.length > 0) {
|
|
1228
|
+
try {
|
|
1229
|
+
return this.reserveWaitAgentOutcome(
|
|
1230
|
+
parent,
|
|
1231
|
+
toolCallId,
|
|
1232
|
+
timeoutMs,
|
|
1233
|
+
rechecked,
|
|
1234
|
+
);
|
|
1235
|
+
} finally {
|
|
1236
|
+
this.removeCompletionWaiter(key, waiter);
|
|
1237
|
+
waiter = undefined;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
return undefined;
|
|
1241
|
+
},
|
|
1242
|
+
);
|
|
1243
|
+
if (immediate) return immediate;
|
|
1244
|
+
if (!waiter) {
|
|
1245
|
+
throw new Error("wait_agent failed to establish an activity subscription");
|
|
1246
|
+
}
|
|
1247
|
+
let activity: "activity" | "timeout";
|
|
1248
|
+
try {
|
|
1249
|
+
activity = await waiter.promise;
|
|
1250
|
+
} catch (error) {
|
|
1251
|
+
this.removeCompletionWaiter(key, waiter);
|
|
1252
|
+
throw error;
|
|
1253
|
+
}
|
|
1254
|
+
const afterWake = await this.completionOperations.run(
|
|
1255
|
+
key,
|
|
1256
|
+
async (): Promise<WaitAgentOutcome | undefined> => {
|
|
1257
|
+
try {
|
|
1258
|
+
if (this.draining) {
|
|
1259
|
+
throw new Error(
|
|
1260
|
+
"pi-subagent is shutting down; wait_agent was interrupted",
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
1264
|
+
if (this.completionWaiters.get(key) !== waiter) {
|
|
1265
|
+
throw new Error(
|
|
1266
|
+
"wait_agent lost ownership of its activity subscription",
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
const snapshot = readCompletionMailbox(
|
|
1270
|
+
parent.sessionManager.getEntries(),
|
|
1271
|
+
{
|
|
1272
|
+
parentAgentId: parent.agentId,
|
|
1273
|
+
activeRuntimeId: this.runtimeId,
|
|
1274
|
+
},
|
|
1275
|
+
);
|
|
1276
|
+
if (snapshot.currentRuntimeReservations.length > 0) {
|
|
1277
|
+
throw new Error(
|
|
1278
|
+
"a previous wait_agent delivery is awaiting its durable tool result",
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
if (snapshot.available.length > 0) {
|
|
1282
|
+
return this.reserveWaitAgentOutcome(
|
|
1283
|
+
parent,
|
|
1284
|
+
toolCallId,
|
|
1285
|
+
timeoutMs,
|
|
1286
|
+
snapshot,
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
if (activity === "timeout" || Date.now() >= deadline) {
|
|
1290
|
+
return {
|
|
1291
|
+
timedOut: true,
|
|
1292
|
+
timeoutMs,
|
|
1293
|
+
updates: [],
|
|
1294
|
+
unreadUpdates: snapshot.unread.length,
|
|
1295
|
+
taskPaths: {},
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
return undefined;
|
|
1299
|
+
} finally {
|
|
1300
|
+
this.removeCompletionWaiter(key, waiter!);
|
|
1301
|
+
}
|
|
1302
|
+
},
|
|
1303
|
+
);
|
|
1304
|
+
if (afterWake) return afterWake;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
private reserveWaitAgentOutcome(
|
|
1309
|
+
parent: ParentRef,
|
|
1310
|
+
toolCallId: string,
|
|
1311
|
+
timeoutMs: number,
|
|
1312
|
+
snapshot: ReturnType<typeof readCompletionMailbox>,
|
|
1313
|
+
): WaitAgentOutcome {
|
|
1314
|
+
const updates = this.boundedWaitAgentUpdates(snapshot.available);
|
|
1315
|
+
reserveCompletionDelivery(parent.sessionManager, {
|
|
1316
|
+
parentAgentId: parent.agentId,
|
|
1317
|
+
runtimeId: this.runtimeId,
|
|
1318
|
+
toolCallId,
|
|
1319
|
+
completionIds: updates.map((update) => update.completionId),
|
|
1320
|
+
});
|
|
1321
|
+
return {
|
|
1322
|
+
timedOut: false,
|
|
1323
|
+
timeoutMs,
|
|
1324
|
+
updates,
|
|
1325
|
+
unreadUpdates: Math.max(
|
|
1326
|
+
0,
|
|
1327
|
+
snapshot.unread.length - updates.length,
|
|
1328
|
+
),
|
|
1329
|
+
taskPaths: {},
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
private boundedWaitAgentUpdates(
|
|
1334
|
+
available: readonly CompletionUpdate[],
|
|
1335
|
+
): CompletionUpdate[] {
|
|
1336
|
+
const updates: CompletionUpdate[] = [];
|
|
1337
|
+
let remainingBytes = MAX_WAIT_AGENT_RESULT_BYTES;
|
|
1338
|
+
for (const update of available) {
|
|
1339
|
+
if (updates.length >= MAX_COMPLETIONS_PER_DELIVERY) break;
|
|
1340
|
+
const metadataBytes = Buffer.byteLength(
|
|
1341
|
+
`completion ${update.completionId}\nchild=${update.childAgentId} turn=${update.turnId} stop=${update.stopReason}\n`,
|
|
1342
|
+
"utf8",
|
|
1343
|
+
) + 256;
|
|
1344
|
+
const fullBytes =
|
|
1345
|
+
metadataBytes + Buffer.byteLength(update.output, "utf8");
|
|
1346
|
+
if (updates.length > 0 && fullBytes > remainingBytes) break;
|
|
1347
|
+
const outputBudget = Math.max(0, remainingBytes - metadataBytes);
|
|
1348
|
+
const truncated = truncateUtf8(update.output, outputBudget);
|
|
1349
|
+
updates.push({
|
|
1350
|
+
...update,
|
|
1351
|
+
output: truncated.text,
|
|
1352
|
+
...(truncated.truncated || update.outputTruncated
|
|
1353
|
+
? {
|
|
1354
|
+
outputTruncated: true,
|
|
1355
|
+
omittedBytes:
|
|
1356
|
+
(update.omittedBytes ?? 0)
|
|
1357
|
+
+ truncated.omittedBytes,
|
|
1358
|
+
}
|
|
1359
|
+
: {}),
|
|
1360
|
+
});
|
|
1361
|
+
remainingBytes = Math.max(
|
|
1362
|
+
0,
|
|
1363
|
+
remainingBytes
|
|
1364
|
+
- metadataBytes
|
|
1365
|
+
- Buffer.byteLength(truncated.text, "utf8"),
|
|
1366
|
+
);
|
|
1367
|
+
if (truncated.truncated) break;
|
|
1368
|
+
}
|
|
1369
|
+
return updates;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
async releaseWaitAgentDeliveries(
|
|
1373
|
+
parent: ParentRef,
|
|
1374
|
+
reason: string,
|
|
1375
|
+
): Promise<number> {
|
|
1376
|
+
const key = completionWaiterKey(parent);
|
|
1377
|
+
return this.completionOperations.run(key, async () =>
|
|
1378
|
+
releaseCompletionDeliveries(parent.sessionManager, {
|
|
1379
|
+
parentAgentId: parent.agentId,
|
|
1380
|
+
runtimeId: this.runtimeId,
|
|
1381
|
+
reason,
|
|
1382
|
+
}),
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
private createCompletionWaiter(
|
|
1387
|
+
timeoutMs: number,
|
|
1388
|
+
signal?: AbortSignal,
|
|
1389
|
+
): CompletionWaiter {
|
|
1390
|
+
let settled = false;
|
|
1391
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1392
|
+
let resolvePromise!: (activity: "activity" | "timeout") => void;
|
|
1393
|
+
let rejectPromise!: (error: Error) => void;
|
|
1394
|
+
const promise = new Promise<"activity" | "timeout">(
|
|
1395
|
+
(resolve, reject) => {
|
|
1396
|
+
resolvePromise = resolve;
|
|
1397
|
+
rejectPromise = reject;
|
|
1398
|
+
},
|
|
1399
|
+
);
|
|
1400
|
+
const onAbort = () => {
|
|
1401
|
+
if (settled) return;
|
|
1402
|
+
settled = true;
|
|
1403
|
+
if (timer) clearTimeout(timer);
|
|
1404
|
+
rejectPromise(abortReason(signal!));
|
|
1405
|
+
};
|
|
1406
|
+
if (signal) signal.addEventListener("abort", onAbort, { once: true });
|
|
1407
|
+
timer = setTimeout(() => {
|
|
1408
|
+
if (settled) return;
|
|
1409
|
+
settled = true;
|
|
1410
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
1411
|
+
resolvePromise("timeout");
|
|
1412
|
+
}, timeoutMs);
|
|
1413
|
+
// An awaited tool deadline must keep headless SDK processes alive.
|
|
1414
|
+
// Wake, abort and shutdown clear it rather than leaving a background timer.
|
|
1415
|
+
const dispose = () => {
|
|
1416
|
+
if (timer) clearTimeout(timer);
|
|
1417
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
1418
|
+
};
|
|
1419
|
+
return {
|
|
1420
|
+
promise,
|
|
1421
|
+
wake: () => {
|
|
1422
|
+
if (settled) return;
|
|
1423
|
+
settled = true;
|
|
1424
|
+
dispose();
|
|
1425
|
+
resolvePromise("activity");
|
|
1426
|
+
},
|
|
1427
|
+
reject: (error) => {
|
|
1428
|
+
if (settled) return;
|
|
1429
|
+
settled = true;
|
|
1430
|
+
dispose();
|
|
1431
|
+
rejectPromise(error);
|
|
1432
|
+
},
|
|
1433
|
+
dispose,
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
private removeCompletionWaiter(
|
|
1438
|
+
key: string,
|
|
1439
|
+
waiter: CompletionWaiter,
|
|
1440
|
+
): void {
|
|
1441
|
+
if (this.completionWaiters.get(key) === waiter) {
|
|
1442
|
+
this.completionWaiters.delete(key);
|
|
1443
|
+
}
|
|
1444
|
+
waiter.dispose();
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
async interrupt(parent: ParentRef, targetId: string): Promise<void> {
|
|
1448
|
+
await this.interruptWithOutcome(parent, targetId);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
async interruptWithOutcome(
|
|
1452
|
+
parent: ParentRef,
|
|
1453
|
+
targetId: string,
|
|
1454
|
+
): Promise<InterruptOutcome> {
|
|
1455
|
+
return this.runAdmittedOperation(() =>
|
|
1456
|
+
this.interruptAdmitted(parent, targetId),
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
private async interruptAdmitted(
|
|
1461
|
+
parent: ParentRef,
|
|
1462
|
+
targetId: string,
|
|
1463
|
+
): Promise<InterruptOutcome> {
|
|
1464
|
+
const resolved = await this.resolveTarget(parent, targetId, {
|
|
1465
|
+
allowUnknownId: true,
|
|
1466
|
+
});
|
|
1467
|
+
return this.agentOperations.run(resolved.agentId, async () => {
|
|
1468
|
+
if (
|
|
1469
|
+
resolved.record
|
|
1470
|
+
&& !(await this.isDescendantOf(
|
|
1471
|
+
parent,
|
|
1472
|
+
resolved.record.descriptor,
|
|
1473
|
+
))
|
|
1474
|
+
) {
|
|
1475
|
+
throw new Error(
|
|
1476
|
+
`subagent ${resolved.taskPath} is not a descendant of ${parent.taskPath}`,
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
const target = this.active.get(resolved.agentId);
|
|
1480
|
+
if (!target) {
|
|
1481
|
+
return {
|
|
1482
|
+
agentId: resolved.agentId,
|
|
1483
|
+
taskPath: resolved.taskPath,
|
|
1484
|
+
active: false,
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
if (!(await this.isDescendantOf(parent, target.descriptor))) {
|
|
1488
|
+
throw new Error(
|
|
1489
|
+
`subagent ${resolved.taskPath} is not a live descendant of ${parent.taskPath}`,
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
this.touchActivation(target);
|
|
1493
|
+
interruptAgentTurn(target.controlState);
|
|
1494
|
+
this.emitUpdate(target);
|
|
1495
|
+
target.turnAbortController?.abort(
|
|
1496
|
+
new Error(`subagent ${resolved.taskPath} was interrupted`),
|
|
1497
|
+
);
|
|
1498
|
+
void target.runtime.session.abort().catch((error) => {
|
|
1499
|
+
target.lastError = errorText(error);
|
|
1500
|
+
});
|
|
1501
|
+
return {
|
|
1502
|
+
agentId: resolved.agentId,
|
|
1503
|
+
taskPath: descriptorTaskPath(target.descriptor),
|
|
1504
|
+
active: true,
|
|
1505
|
+
};
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
async list(parent: ParentRef, scope: "children" | "descendants"): Promise<CatalogEntry[]> {
|
|
1510
|
+
return this.runAdmittedOperation(() => this.listAdmitted(parent, scope));
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
private async listAdmitted(
|
|
1514
|
+
parent: ParentRef,
|
|
1515
|
+
scope: "children" | "descendants",
|
|
1516
|
+
): Promise<CatalogEntry[]> {
|
|
1517
|
+
const catalog = await this.catalogRecords(parent);
|
|
1518
|
+
const records = catalog.records;
|
|
1519
|
+
const byId = new Map(records.map((record) => [record.agentId, record]));
|
|
1520
|
+
const children: CatalogChild[] = [];
|
|
1521
|
+
for (const record of records) {
|
|
1522
|
+
if (record.descriptor.mode !== "continuable") continue;
|
|
1523
|
+
const distance = this.distanceFrom(parent.agentId, record.descriptor, byId);
|
|
1524
|
+
if (distance === undefined || (scope === "children" && distance !== 1)) continue;
|
|
1525
|
+
children.push({
|
|
1526
|
+
kind: "child",
|
|
1527
|
+
agentId: record.agentId,
|
|
1528
|
+
parentAgentId: record.descriptor.parentAgentId,
|
|
1529
|
+
taskPath: record.taskPath,
|
|
1530
|
+
parentTaskPath:
|
|
1531
|
+
record.descriptor.parentAgentId === parent.agentId
|
|
1532
|
+
? parent.taskPath
|
|
1533
|
+
: (byId.get(record.descriptor.parentAgentId)?.taskPath
|
|
1534
|
+
?? record.descriptor.parentAgentId),
|
|
1535
|
+
depth: distance,
|
|
1536
|
+
descriptor: record.descriptor,
|
|
1537
|
+
...(record.sessionFile ? { sessionFile: record.sessionFile } : {}),
|
|
1538
|
+
status: record.active ? catalogStatus(record.active.controlState) : "ready",
|
|
1539
|
+
pendingMessages: record.pendingMessages,
|
|
1540
|
+
unreadUpdates:
|
|
1541
|
+
record.descriptor.parentAgentId === parent.agentId
|
|
1542
|
+
? (catalog.rootUnreadUpdatesByChild.get(record.agentId) ?? 0)
|
|
1543
|
+
: (byId
|
|
1544
|
+
.get(record.descriptor.parentAgentId)
|
|
1545
|
+
?.unreadUpdatesByChild.get(record.agentId) ?? 0),
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
children.sort(
|
|
1549
|
+
(left, right) =>
|
|
1550
|
+
left.descriptor.createdAt.localeCompare(right.descriptor.createdAt) ||
|
|
1551
|
+
left.agentId.localeCompare(right.agentId),
|
|
1552
|
+
);
|
|
1553
|
+
const parentFile = parent.sessionManager.getSessionFile();
|
|
1554
|
+
const diagnostics = parentFile
|
|
1555
|
+
? catalog.diagnostics.filter(
|
|
615
1556
|
(entry) => entry.kind === "diagnostic" && entry.parentSessionFile === parentFile,
|
|
616
1557
|
)
|
|
617
1558
|
: [];
|
|
@@ -619,50 +1560,115 @@ export class SubagentCoordinator {
|
|
|
619
1560
|
}
|
|
620
1561
|
|
|
621
1562
|
async report(child: Activation, output: string): Promise<void> {
|
|
1563
|
+
return this.runAdmittedOperation(() => this.reportAdmitted(child, output));
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
private async reportAdmitted(child: Activation, output: string): Promise<void> {
|
|
622
1567
|
if (child.descriptor.mode !== "continuable") {
|
|
623
1568
|
throw new Error("report is available only to continuable subagents");
|
|
624
1569
|
}
|
|
625
1570
|
const truncated = truncateUtf8(output, child.descriptor.runtime.maxOutputBytes);
|
|
626
|
-
const
|
|
1571
|
+
const readablePath = descriptorTaskPath(child.descriptor);
|
|
1572
|
+
const content = `Background subagent ${readablePath} (${child.agentId}) reported:\n\n${truncated.text}${
|
|
627
1573
|
truncated.truncated ? `\n\n[Report truncated; ${truncated.omittedBytes} bytes omitted.]` : ""
|
|
628
1574
|
}`;
|
|
629
|
-
await child.parent.deliver(
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
)
|
|
1575
|
+
await child.parent.deliver(REPORT_CUSTOM_TYPE, content, {
|
|
1576
|
+
kind: "report",
|
|
1577
|
+
childAgentId: child.agentId,
|
|
1578
|
+
taskPath: readablePath,
|
|
1579
|
+
label: child.descriptor.label,
|
|
1580
|
+
...(truncated.truncated ? { truncated: true } : {}),
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
private runAdmittedOperation<T>(operation: () => Promise<T>): Promise<T> {
|
|
1585
|
+
if (this.draining) {
|
|
1586
|
+
return Promise.reject(new Error("pi-subagent is shutting down"));
|
|
1587
|
+
}
|
|
1588
|
+
const promise = operation();
|
|
1589
|
+
this.admittedOperations.add(promise);
|
|
1590
|
+
const remove = () => {
|
|
1591
|
+
this.admittedOperations.delete(promise);
|
|
1592
|
+
};
|
|
1593
|
+
void promise.then(remove, remove);
|
|
1594
|
+
return promise;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
private async waitForAdmittedOperations(): Promise<void> {
|
|
1598
|
+
while (this.admittedOperations.size > 0) {
|
|
1599
|
+
await Promise.allSettled([...this.admittedOperations]);
|
|
1600
|
+
}
|
|
640
1601
|
}
|
|
641
1602
|
|
|
642
1603
|
async shutdown(): Promise<void> {
|
|
643
|
-
if (this.
|
|
1604
|
+
if (this.shutdownPromise) return this.shutdownPromise;
|
|
644
1605
|
this.draining = true;
|
|
1606
|
+
this.rejectCompletionWaiters(
|
|
1607
|
+
new Error("pi-subagent is shutting down"),
|
|
1608
|
+
);
|
|
1609
|
+
this.shutdownPromise = this.performShutdown();
|
|
1610
|
+
return this.shutdownPromise;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
private async performShutdown(): Promise<void> {
|
|
1614
|
+
this.backgroundRuns.close();
|
|
1615
|
+
await this.abortActiveRuns();
|
|
1616
|
+
await this.agentOperations.waitForIdle();
|
|
1617
|
+
await this.idleRuntimeOperations.waitForIdle();
|
|
1618
|
+
await this.completionOperations.waitForIdle();
|
|
1619
|
+
await this.abortActiveRuns();
|
|
1620
|
+
await this.waitForAdmittedOperations();
|
|
1621
|
+
await this.agentOperations.waitForIdle();
|
|
1622
|
+
await this.abortActiveRuns();
|
|
645
1623
|
const activations = [...this.active.values()];
|
|
646
|
-
for (const activation of activations) activation.suppressSettlement = true;
|
|
647
1624
|
await Promise.allSettled(
|
|
648
|
-
activations
|
|
649
|
-
|
|
650
|
-
|
|
1625
|
+
activations
|
|
1626
|
+
.map((activation) => activation.currentRun)
|
|
1627
|
+
.filter(
|
|
1628
|
+
(run): run is Promise<SubagentRunResult> => run !== undefined,
|
|
1629
|
+
),
|
|
651
1630
|
);
|
|
652
1631
|
for (const activation of activations.sort((left, right) => right.descriptor.depth - left.descriptor.depth)) {
|
|
653
1632
|
await this.disposeActivation(activation).catch(() => {});
|
|
654
1633
|
}
|
|
1634
|
+
await this.agentOperations.waitForIdle();
|
|
1635
|
+
await this.idleRuntimeOperations.waitForIdle();
|
|
1636
|
+
await this.completionOperations.waitForIdle();
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
private rejectCompletionWaiters(error: Error): void {
|
|
1640
|
+
for (const [key, waiter] of this.completionWaiters) {
|
|
1641
|
+
this.completionWaiters.delete(key);
|
|
1642
|
+
waiter.reject(error);
|
|
1643
|
+
waiter.dispose();
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
private async abortActiveRuns(): Promise<void> {
|
|
1648
|
+
const activations = [...this.active.values()];
|
|
1649
|
+
for (const activation of activations) {
|
|
1650
|
+
activation.suppressSettlement = true;
|
|
1651
|
+
activation.turnAbortController?.abort(
|
|
1652
|
+
new Error("pi-subagent is shutting down"),
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
await Promise.allSettled(
|
|
1656
|
+
activations.map(async (activation) => {
|
|
1657
|
+
if (!activation.runtime.session.isIdle) {
|
|
1658
|
+
await activation.runtime.session.abort();
|
|
1659
|
+
}
|
|
1660
|
+
}),
|
|
1661
|
+
);
|
|
655
1662
|
}
|
|
656
1663
|
|
|
657
1664
|
createChildToolDefinitions(
|
|
658
1665
|
getActivation: () => Activation,
|
|
659
|
-
|
|
660
|
-
defaultBackground = true,
|
|
1666
|
+
runtimeMode: RuntimeMode = "background",
|
|
661
1667
|
agentDiscovery?: AgentDiscoveryResult,
|
|
662
1668
|
): ToolDefinition[] {
|
|
663
1669
|
const agentNames = agentDiscovery?.agents.map((agent) => agent.name);
|
|
664
1670
|
const assertBackgroundControlEnabled = (toolName: string): void => {
|
|
665
|
-
if (
|
|
1671
|
+
if (runtimeMode === "foreground") {
|
|
666
1672
|
throw new Error(`tool "${toolName}" is unavailable in foreground-only mode`);
|
|
667
1673
|
}
|
|
668
1674
|
};
|
|
@@ -678,13 +1684,11 @@ export class SubagentCoordinator {
|
|
|
678
1684
|
name: "subagent",
|
|
679
1685
|
label: "Subagent",
|
|
680
1686
|
description:
|
|
681
|
-
"Delegate a standalone task to a
|
|
682
|
-
(
|
|
1687
|
+
"Delegate a standalone task to a named child path, with optional completed-turn context inheritance. " +
|
|
1688
|
+
(runtimeMode === "foreground"
|
|
683
1689
|
? "This foreground-only instance always waits for the result."
|
|
684
|
-
:
|
|
685
|
-
|
|
686
|
-
: "It waits for the result by default; background mode returns a durable id."),
|
|
687
|
-
parameters: delegationParameters(enableRunInBackground, agentNames),
|
|
1690
|
+
: "It returns a readable path plus durable id; use send_message, followup_task, and wait_agent to continue it."),
|
|
1691
|
+
parameters: delegationParameters(agentNames),
|
|
688
1692
|
execute: async (_id, params, signal, onUpdate) => {
|
|
689
1693
|
const activation = getActivation();
|
|
690
1694
|
const outcome = await this.delegate(
|
|
@@ -704,7 +1708,10 @@ export class SubagentCoordinator {
|
|
|
704
1708
|
name: "subagent_fork",
|
|
705
1709
|
label: "Subagent Fork",
|
|
706
1710
|
description:
|
|
707
|
-
"Delegate a
|
|
1711
|
+
"Delegate a task to a child seeded with all completed turns from this conversation. " +
|
|
1712
|
+
(runtimeMode === "foreground"
|
|
1713
|
+
? "This foreground-only instance always waits for the result."
|
|
1714
|
+
: "It returns a readable path plus durable id and can be continued through its mailbox."),
|
|
708
1715
|
parameters: forkDelegationParameters(agentNames),
|
|
709
1716
|
execute: async (_id, params, signal, onUpdate) => {
|
|
710
1717
|
const activation = getActivation();
|
|
@@ -725,12 +1732,12 @@ export class SubagentCoordinator {
|
|
|
725
1732
|
name: "send_message",
|
|
726
1733
|
label: "Send Message",
|
|
727
1734
|
description:
|
|
728
|
-
"
|
|
1735
|
+
"Durably append a message to a direct continuable child's FIFO mailbox without starting or resuming it. Use followup_task to start the queued batch. This returns acceptance, not the child's answer.",
|
|
729
1736
|
parameters: SendMessageParameters,
|
|
730
1737
|
execute: async (_id, params, signal) => {
|
|
731
1738
|
assertBackgroundControlEnabled("send_message");
|
|
732
1739
|
const activation = getActivation();
|
|
733
|
-
await this.
|
|
1740
|
+
const delivery = await this.sendMessageWithOutcome(
|
|
734
1741
|
this.parentForActivation(activation),
|
|
735
1742
|
params.subagent_id,
|
|
736
1743
|
params.message,
|
|
@@ -740,10 +1747,92 @@ export class SubagentCoordinator {
|
|
|
740
1747
|
content: [
|
|
741
1748
|
{
|
|
742
1749
|
type: "text",
|
|
743
|
-
text:
|
|
1750
|
+
text:
|
|
1751
|
+
delivery.kind === "mailbox"
|
|
1752
|
+
? `message ${delivery.messageId} durably enqueued for ${delivery.taskPath}; ${delivery.pendingMessages} pending`
|
|
1753
|
+
: `message queued as the next turn for ${delivery.taskPath}`,
|
|
744
1754
|
},
|
|
745
1755
|
],
|
|
746
|
-
details: {
|
|
1756
|
+
details: {
|
|
1757
|
+
kind: "control",
|
|
1758
|
+
action: "send",
|
|
1759
|
+
agentId: delivery.agentId,
|
|
1760
|
+
taskPath: delivery.taskPath,
|
|
1761
|
+
...(delivery.kind === "mailbox"
|
|
1762
|
+
? {
|
|
1763
|
+
messageId: delivery.messageId,
|
|
1764
|
+
pendingMessages: delivery.pendingMessages,
|
|
1765
|
+
}
|
|
1766
|
+
: {}),
|
|
1767
|
+
} satisfies ControlDetails,
|
|
1768
|
+
};
|
|
1769
|
+
},
|
|
1770
|
+
});
|
|
1771
|
+
|
|
1772
|
+
const followup = defineTool({
|
|
1773
|
+
name: "followup_task",
|
|
1774
|
+
label: "Follow-up Task",
|
|
1775
|
+
description:
|
|
1776
|
+
"Start exactly one scheduled turn for a direct child, atomically claiming its current pending FIFO mailbox batch.",
|
|
1777
|
+
parameters: FollowupTaskParameters,
|
|
1778
|
+
execute: async (_id, params, signal) => {
|
|
1779
|
+
assertBackgroundControlEnabled("followup_task");
|
|
1780
|
+
const activation = getActivation();
|
|
1781
|
+
const outcome = await this.followupTask(
|
|
1782
|
+
this.parentForActivation(activation),
|
|
1783
|
+
params.subagent_id,
|
|
1784
|
+
signal,
|
|
1785
|
+
);
|
|
1786
|
+
return {
|
|
1787
|
+
content: [
|
|
1788
|
+
{
|
|
1789
|
+
type: "text",
|
|
1790
|
+
text: `started turn ${outcome.turnId} for ${outcome.taskPath}, claiming ${outcome.claimedMessages} mailbox message${outcome.claimedMessages === 1 ? "" : "s"}`,
|
|
1791
|
+
},
|
|
1792
|
+
],
|
|
1793
|
+
details: {
|
|
1794
|
+
kind: "control",
|
|
1795
|
+
action: "followup",
|
|
1796
|
+
agentId: outcome.agentId,
|
|
1797
|
+
taskPath: outcome.taskPath,
|
|
1798
|
+
turnId: outcome.turnId,
|
|
1799
|
+
claimedMessages: outcome.claimedMessages,
|
|
1800
|
+
} satisfies ControlDetails,
|
|
1801
|
+
};
|
|
1802
|
+
},
|
|
1803
|
+
});
|
|
1804
|
+
|
|
1805
|
+
const wait = defineTool({
|
|
1806
|
+
name: "wait_agent",
|
|
1807
|
+
label: "Wait Agent",
|
|
1808
|
+
description:
|
|
1809
|
+
"Wait event-driven for unread completion updates from direct children. This does not start a child or occupy a background scheduler slot.",
|
|
1810
|
+
parameters: WaitAgentParameters,
|
|
1811
|
+
execute: async (id, params, signal) => {
|
|
1812
|
+
assertBackgroundControlEnabled("wait_agent");
|
|
1813
|
+
const activation = getActivation();
|
|
1814
|
+
const outcome = await this.waitAgent(
|
|
1815
|
+
this.parentForActivation(activation),
|
|
1816
|
+
id,
|
|
1817
|
+
params.timeout_ms,
|
|
1818
|
+
signal,
|
|
1819
|
+
);
|
|
1820
|
+
return {
|
|
1821
|
+
content: [
|
|
1822
|
+
{
|
|
1823
|
+
type: "text",
|
|
1824
|
+
text: this.formatWaitAgentOutcome(outcome),
|
|
1825
|
+
},
|
|
1826
|
+
],
|
|
1827
|
+
details: {
|
|
1828
|
+
kind: "control",
|
|
1829
|
+
action: "wait",
|
|
1830
|
+
timedOut: outcome.timedOut,
|
|
1831
|
+
completionIds: outcome.updates.map(
|
|
1832
|
+
(update) => update.completionId,
|
|
1833
|
+
),
|
|
1834
|
+
unreadUpdates: outcome.unreadUpdates,
|
|
1835
|
+
} satisfies ControlDetails,
|
|
747
1836
|
};
|
|
748
1837
|
},
|
|
749
1838
|
});
|
|
@@ -757,10 +1846,18 @@ export class SubagentCoordinator {
|
|
|
757
1846
|
execute: async (_id, params) => {
|
|
758
1847
|
assertBackgroundControlEnabled("interrupt_agent");
|
|
759
1848
|
const activation = getActivation();
|
|
760
|
-
await this.
|
|
1849
|
+
const outcome = await this.interruptWithOutcome(
|
|
1850
|
+
this.parentForActivation(activation),
|
|
1851
|
+
params.agent_id,
|
|
1852
|
+
);
|
|
761
1853
|
return {
|
|
762
|
-
content: [{ type: "text", text: `interrupt requested for
|
|
763
|
-
details: {
|
|
1854
|
+
content: [{ type: "text", text: `interrupt requested for ${outcome.taskPath}` }],
|
|
1855
|
+
details: {
|
|
1856
|
+
kind: "control",
|
|
1857
|
+
action: "interrupt",
|
|
1858
|
+
agentId: outcome.agentId,
|
|
1859
|
+
taskPath: outcome.taskPath,
|
|
1860
|
+
} satisfies ControlDetails,
|
|
764
1861
|
};
|
|
765
1862
|
},
|
|
766
1863
|
});
|
|
@@ -769,7 +1866,7 @@ export class SubagentCoordinator {
|
|
|
769
1866
|
name: "list_agents",
|
|
770
1867
|
label: "List Agents",
|
|
771
1868
|
description:
|
|
772
|
-
"List direct continuable children or all descendants as running, idle, or ready
|
|
1869
|
+
"List direct continuable children or all descendants as running, idle, or ready, with separate mailbox task and completion counts.",
|
|
773
1870
|
parameters: ListAgentsParameters,
|
|
774
1871
|
execute: async (_id, params) => {
|
|
775
1872
|
assertBackgroundControlEnabled("list_agents");
|
|
@@ -796,18 +1893,28 @@ export class SubagentCoordinator {
|
|
|
796
1893
|
await this.report(activation, params.output);
|
|
797
1894
|
return {
|
|
798
1895
|
content: [{ type: "text", text: `report accepted by the agent that started you` }],
|
|
799
|
-
|
|
1896
|
+
details: {
|
|
1897
|
+
kind: "control",
|
|
1898
|
+
action: "report",
|
|
1899
|
+
agentId: activation.agentId,
|
|
1900
|
+
taskPath: descriptorTaskPath(activation.descriptor),
|
|
1901
|
+
} satisfies ControlDetails,
|
|
800
1902
|
};
|
|
801
1903
|
},
|
|
802
1904
|
});
|
|
803
1905
|
|
|
804
|
-
return [spawn, fork, send, interrupt, list, report];
|
|
1906
|
+
return [spawn, fork, send, followup, wait, interrupt, list, report];
|
|
805
1907
|
}
|
|
806
1908
|
|
|
807
1909
|
outcomeToolResult(outcome: DelegationOutcome): AgentToolResult<DelegationDetails> {
|
|
808
1910
|
if (outcome.kind === "continuable") {
|
|
809
1911
|
return {
|
|
810
|
-
content: [
|
|
1912
|
+
content: [
|
|
1913
|
+
{
|
|
1914
|
+
type: "text",
|
|
1915
|
+
text: `started subagent ${outcome.details.taskPath} (${outcome.details.agentId})`,
|
|
1916
|
+
},
|
|
1917
|
+
],
|
|
811
1918
|
details: outcome.details,
|
|
812
1919
|
};
|
|
813
1920
|
}
|
|
@@ -834,12 +1941,49 @@ export class SubagentCoordinator {
|
|
|
834
1941
|
return `${entry.piSessionId} [diagnostic: ${entry.reason}]`;
|
|
835
1942
|
}
|
|
836
1943
|
const location =
|
|
837
|
-
scope === "descendants"
|
|
838
|
-
|
|
1944
|
+
scope === "descendants"
|
|
1945
|
+
? ` parent=${entry.parentTaskPath} depth=${entry.depth}`
|
|
1946
|
+
: "";
|
|
1947
|
+
const mailbox = ` pending=${entry.pendingMessages} updates=${entry.unreadUpdates}`;
|
|
1948
|
+
return `${entry.taskPath} [${entry.status}]${mailbox}${location} — ${entry.descriptor.label} (${entry.descriptor.agent.name}) id=${entry.agentId}`;
|
|
839
1949
|
})
|
|
840
1950
|
.join("\n");
|
|
841
1951
|
}
|
|
842
1952
|
|
|
1953
|
+
formatWaitAgentOutcome(outcome: WaitAgentOutcome): string {
|
|
1954
|
+
if (outcome.timedOut) {
|
|
1955
|
+
return `wait_agent timed out after ${outcome.timeoutMs}ms with no completion updates`;
|
|
1956
|
+
}
|
|
1957
|
+
const updates = outcome.updates.map((update) => {
|
|
1958
|
+
const output = update.output.trim() || "(no output)";
|
|
1959
|
+
const fullPath =
|
|
1960
|
+
outcome.taskPaths[update.childAgentId]
|
|
1961
|
+
?? update.childAgentId;
|
|
1962
|
+
const readablePath =
|
|
1963
|
+
fullPath.length > 200
|
|
1964
|
+
? `${fullPath.slice(0, 199)}…`
|
|
1965
|
+
: fullPath;
|
|
1966
|
+
const truncation = update.outputTruncated
|
|
1967
|
+
? `\n[Completion output truncated${
|
|
1968
|
+
update.omittedBytes !== undefined
|
|
1969
|
+
? `; ${update.omittedBytes} bytes omitted`
|
|
1970
|
+
: ""
|
|
1971
|
+
}.]`
|
|
1972
|
+
: "";
|
|
1973
|
+
return [
|
|
1974
|
+
`completion ${update.completionId}`,
|
|
1975
|
+
`child=${readablePath} id=${update.childAgentId} turn=${update.turnId} stop=${update.stopReason}`,
|
|
1976
|
+
`${output}${truncation}`,
|
|
1977
|
+
].join("\n");
|
|
1978
|
+
});
|
|
1979
|
+
if (outcome.unreadUpdates > 0) {
|
|
1980
|
+
updates.push(
|
|
1981
|
+
`${outcome.unreadUpdates} additional completion update${outcome.unreadUpdates === 1 ? "" : "s"} remain unread`,
|
|
1982
|
+
);
|
|
1983
|
+
}
|
|
1984
|
+
return updates.join("\n\n");
|
|
1985
|
+
}
|
|
1986
|
+
|
|
843
1987
|
private resolveModel(parent: ParentRef, agent: AgentDefinition): Model<any> {
|
|
844
1988
|
if (!agent.model) {
|
|
845
1989
|
if (!parent.model) throw new Error("no parent model is selected for the subagent");
|
|
@@ -878,8 +2022,7 @@ export class SubagentCoordinator {
|
|
|
878
2022
|
if (!activation) throw new Error("subagent activation is not published yet");
|
|
879
2023
|
return activation;
|
|
880
2024
|
},
|
|
881
|
-
descriptor.runtime.
|
|
882
|
-
descriptor.runtime.defaultBackground,
|
|
2025
|
+
descriptor.runtime.runtimeMode,
|
|
883
2026
|
agentDiscovery,
|
|
884
2027
|
);
|
|
885
2028
|
const model =
|
|
@@ -996,8 +2139,8 @@ export class SubagentCoordinator {
|
|
|
996
2139
|
return false;
|
|
997
2140
|
}
|
|
998
2141
|
if (
|
|
999
|
-
|
|
1000
|
-
BACKGROUND_CONTROL_TOOLS.has(tool)
|
|
2142
|
+
descriptor.runtime.runtimeMode === "foreground"
|
|
2143
|
+
&& BACKGROUND_CONTROL_TOOLS.has(tool)
|
|
1001
2144
|
) {
|
|
1002
2145
|
return false;
|
|
1003
2146
|
}
|
|
@@ -1045,17 +2188,32 @@ export class SubagentCoordinator {
|
|
|
1045
2188
|
runtime,
|
|
1046
2189
|
seedMessageCount: options.prepared.seedMessageCount,
|
|
1047
2190
|
epochMessageStart: runtime.session.messages.length,
|
|
1048
|
-
|
|
2191
|
+
controlState: createAgentControlState(),
|
|
1049
2192
|
trace: [],
|
|
1050
2193
|
streamedText: "",
|
|
1051
2194
|
usage: emptyUsage(),
|
|
2195
|
+
startedTurnIds: new Set(),
|
|
2196
|
+
silentSettlementTurnIds: new Set(),
|
|
2197
|
+
pendingMailboxClaims: new Set(),
|
|
2198
|
+
userMessageGates: new Map(),
|
|
1052
2199
|
ownedChildren: new Set(),
|
|
1053
2200
|
onUpdate: options.onUpdate,
|
|
1054
2201
|
published: false,
|
|
2202
|
+
everPublished: false,
|
|
1055
2203
|
suppressSettlement: false,
|
|
1056
2204
|
finalizing: false,
|
|
2205
|
+
holdsBackgroundSlot: false,
|
|
2206
|
+
persistenceGate: createPersistenceGate(
|
|
2207
|
+
runtime.session.sessionManager,
|
|
2208
|
+
options.isNew,
|
|
2209
|
+
),
|
|
2210
|
+
lastUsedSequence: ++this.activationSequence,
|
|
1057
2211
|
disposed: false,
|
|
1058
2212
|
};
|
|
2213
|
+
const existing = this.active.get(activation.agentId);
|
|
2214
|
+
if (existing && !existing.disposed) {
|
|
2215
|
+
throw new Error(`subagent ${activation.agentId} already has a resident runtime`);
|
|
2216
|
+
}
|
|
1059
2217
|
activation.unsubscribe = runtime.session.subscribe((event) => this.observe(activation!, event));
|
|
1060
2218
|
this.active.set(activation.agentId, activation);
|
|
1061
2219
|
return activation;
|
|
@@ -1065,16 +2223,33 @@ export class SubagentCoordinator {
|
|
|
1065
2223
|
}
|
|
1066
2224
|
}
|
|
1067
2225
|
|
|
2226
|
+
private async acquireBackgroundRun(
|
|
2227
|
+
activation: Activation,
|
|
2228
|
+
signal: AbortSignal | undefined,
|
|
2229
|
+
waitForCapacity: boolean,
|
|
2230
|
+
): Promise<BackgroundRunPermit | undefined> {
|
|
2231
|
+
if (activation.descriptor.mode !== "continuable") return undefined;
|
|
2232
|
+
const permit = await this.backgroundRuns.acquire({
|
|
2233
|
+
...(signal ? { signal } : {}),
|
|
2234
|
+
waitForCapacity,
|
|
2235
|
+
});
|
|
2236
|
+
activation.holdsBackgroundSlot = true;
|
|
2237
|
+
return permit;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
1068
2240
|
private startPrompt(
|
|
1069
2241
|
activation: Activation,
|
|
1070
2242
|
prompt: string,
|
|
1071
2243
|
signal: AbortSignal | undefined,
|
|
1072
|
-
|
|
1073
|
-
): { accepted: Promise<void>; result: Promise<SubagentRunResult> } {
|
|
2244
|
+
options: StartPromptOptions,
|
|
2245
|
+
): { turnId: string; accepted: Promise<void>; result: Promise<SubagentRunResult> } {
|
|
1074
2246
|
if (activation.currentRun) throw new Error(`subagent ${activation.agentId} is already running`);
|
|
1075
2247
|
if (signal?.aborted) throw signal.reason ?? new Error("subagent start aborted");
|
|
2248
|
+
this.touchActivation(activation);
|
|
1076
2249
|
activation.pendingSettlement = undefined;
|
|
1077
|
-
activation
|
|
2250
|
+
this.resetTurnCapture(activation);
|
|
2251
|
+
const turnId = uuidv7();
|
|
2252
|
+
queueAgentTurn(activation.controlState, turnId);
|
|
1078
2253
|
this.emitUpdate(activation);
|
|
1079
2254
|
|
|
1080
2255
|
let resolveAccepted!: () => void;
|
|
@@ -1084,47 +2259,127 @@ export class SubagentCoordinator {
|
|
|
1084
2259
|
resolveAccepted = resolvePromise;
|
|
1085
2260
|
rejectAccepted = rejectPromise;
|
|
1086
2261
|
});
|
|
2262
|
+
const turnAbortController = new AbortController();
|
|
2263
|
+
activation.turnAbortController = turnAbortController;
|
|
1087
2264
|
const abort = () => {
|
|
2265
|
+
turnAbortController.abort(
|
|
2266
|
+
signal?.reason ?? new Error(`subagent turn ${turnId} was aborted`),
|
|
2267
|
+
);
|
|
1088
2268
|
void activation.runtime.session.abort().catch(() => {});
|
|
1089
2269
|
};
|
|
1090
2270
|
if (signal) signal.addEventListener("abort", abort, { once: true });
|
|
2271
|
+
let preflightSucceeded = false;
|
|
2272
|
+
const acceptPrompt = () => {
|
|
2273
|
+
if (acceptedSettled) return;
|
|
2274
|
+
if (turnAbortController.signal.aborted) {
|
|
2275
|
+
throw abortReason(turnAbortController.signal);
|
|
2276
|
+
}
|
|
2277
|
+
acceptedSettled = true;
|
|
2278
|
+
activation.userMessageGates.delete(turnId);
|
|
2279
|
+
this.publish(activation);
|
|
2280
|
+
if (options.detachAtAcceptance && signal) {
|
|
2281
|
+
signal.removeEventListener("abort", abort);
|
|
2282
|
+
}
|
|
2283
|
+
resolveAccepted();
|
|
2284
|
+
};
|
|
2285
|
+
const rejectPrompt = (error: Error) => {
|
|
2286
|
+
if (acceptedSettled) return;
|
|
2287
|
+
acceptedSettled = true;
|
|
2288
|
+
activation.userMessageGates.delete(turnId);
|
|
2289
|
+
activation.silentSettlementTurnIds.add(turnId);
|
|
2290
|
+
if (!activation.published && !activation.everPublished) {
|
|
2291
|
+
activation.suppressSettlement = true;
|
|
2292
|
+
}
|
|
2293
|
+
rejectAccepted(error);
|
|
2294
|
+
};
|
|
2295
|
+
if (options.acceptAfterUserMessage) {
|
|
2296
|
+
activation.userMessageGates.set(turnId, {
|
|
2297
|
+
prepare: () => {
|
|
2298
|
+
if (turnAbortController.signal.aborted) {
|
|
2299
|
+
throw abortReason(turnAbortController.signal);
|
|
2300
|
+
}
|
|
2301
|
+
options.onPromptAccepted?.(turnId);
|
|
2302
|
+
},
|
|
2303
|
+
accept: acceptPrompt,
|
|
2304
|
+
reject: rejectPrompt,
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
1091
2307
|
|
|
1092
2308
|
const core = (async (): Promise<SubagentRunResult> => {
|
|
2309
|
+
let permit: BackgroundRunPermit | undefined;
|
|
1093
2310
|
try {
|
|
1094
|
-
await
|
|
2311
|
+
permit = await this.acquireBackgroundRun(
|
|
2312
|
+
activation,
|
|
2313
|
+
turnAbortController.signal,
|
|
2314
|
+
options.waitForCapacity,
|
|
2315
|
+
);
|
|
2316
|
+
if (turnAbortController.signal.aborted) {
|
|
2317
|
+
throw abortReason(turnAbortController.signal);
|
|
2318
|
+
}
|
|
2319
|
+
const preparedPrompt = options.preparePrompt
|
|
2320
|
+
? options.preparePrompt(turnId)
|
|
2321
|
+
: prompt;
|
|
2322
|
+
startAgentTurn(activation.controlState, turnId);
|
|
2323
|
+
activation.startedTurnIds.add(turnId);
|
|
2324
|
+
this.emitTurnStart(activation, turnId);
|
|
2325
|
+
this.emitUpdate(activation);
|
|
2326
|
+
await activation.runtime.session.prompt(preparedPrompt, {
|
|
2327
|
+
...(options.preparePrompt
|
|
2328
|
+
? {
|
|
2329
|
+
expandPromptTemplates: false,
|
|
2330
|
+
source: "extension" as const,
|
|
2331
|
+
}
|
|
2332
|
+
: {}),
|
|
1095
2333
|
preflightResult: (success) => {
|
|
1096
2334
|
if (acceptedSettled) return;
|
|
1097
|
-
acceptedSettled = true;
|
|
1098
2335
|
if (success) {
|
|
1099
|
-
|
|
1100
|
-
if (
|
|
1101
|
-
|
|
2336
|
+
preflightSucceeded = true;
|
|
2337
|
+
if (options.acceptAfterUserMessage) {
|
|
2338
|
+
return;
|
|
2339
|
+
}
|
|
2340
|
+
options.onPromptAccepted?.(turnId);
|
|
2341
|
+
acceptPrompt();
|
|
1102
2342
|
} else {
|
|
1103
|
-
|
|
1104
|
-
|
|
2343
|
+
rejectPrompt(
|
|
2344
|
+
new Error("subagent prompt was rejected before acceptance"),
|
|
2345
|
+
);
|
|
1105
2346
|
}
|
|
1106
2347
|
},
|
|
1107
2348
|
});
|
|
1108
2349
|
if (!acceptedSettled) {
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
2350
|
+
if (options.acceptAfterUserMessage && preflightSucceeded) {
|
|
2351
|
+
throw new Error(
|
|
2352
|
+
"subagent mailbox prompt was handled before a user turn started",
|
|
2353
|
+
);
|
|
2354
|
+
}
|
|
2355
|
+
if (turnAbortController.signal.aborted) {
|
|
2356
|
+
throw abortReason(turnAbortController.signal);
|
|
2357
|
+
}
|
|
2358
|
+
options.onPromptAccepted?.(turnId);
|
|
2359
|
+
acceptPrompt();
|
|
1113
2360
|
}
|
|
1114
|
-
return this.collectResult(activation, "completed");
|
|
2361
|
+
return this.collectResult(activation, turnId, "completed");
|
|
1115
2362
|
} catch (error) {
|
|
1116
2363
|
activation.lastError = errorText(error);
|
|
1117
2364
|
if (!acceptedSettled) {
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
2365
|
+
rejectPrompt(
|
|
2366
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
2367
|
+
);
|
|
1121
2368
|
}
|
|
1122
|
-
const fallback = signal
|
|
1123
|
-
const result = this.collectResult(activation, fallback);
|
|
2369
|
+
const fallback = turnAbortController.signal.aborted ? "aborted" : "error";
|
|
2370
|
+
const result = this.collectResult(activation, turnId, fallback);
|
|
1124
2371
|
if (!result.output) result.output = activation.lastError;
|
|
1125
2372
|
return result;
|
|
1126
2373
|
} finally {
|
|
2374
|
+
activation.userMessageGates.delete(turnId);
|
|
1127
2375
|
if (signal) signal.removeEventListener("abort", abort);
|
|
2376
|
+
if (activation.turnAbortController === turnAbortController) {
|
|
2377
|
+
activation.turnAbortController = undefined;
|
|
2378
|
+
}
|
|
2379
|
+
if (permit) {
|
|
2380
|
+
activation.holdsBackgroundSlot = false;
|
|
2381
|
+
permit.release();
|
|
2382
|
+
}
|
|
1128
2383
|
}
|
|
1129
2384
|
})();
|
|
1130
2385
|
|
|
@@ -1136,47 +2391,69 @@ export class SubagentCoordinator {
|
|
|
1136
2391
|
});
|
|
1137
2392
|
activation.currentRun = lifecycle;
|
|
1138
2393
|
void lifecycle.catch(() => {});
|
|
1139
|
-
return { accepted, result: lifecycle };
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
private startInternalMessage(
|
|
1143
|
-
activation: Activation,
|
|
1144
|
-
customType: string,
|
|
1145
|
-
content: string,
|
|
1146
|
-
details: ParentMessageDetails,
|
|
1147
|
-
): void {
|
|
1148
|
-
if (activation.currentRun || activation.disposed) return;
|
|
1149
|
-
activation.pendingSettlement = undefined;
|
|
1150
|
-
activation.status = "running";
|
|
1151
|
-
const core = Promise.resolve()
|
|
1152
|
-
.then(() =>
|
|
1153
|
-
activation.runtime.session.sendCustomMessage(
|
|
1154
|
-
{ customType, content, display: true, details },
|
|
1155
|
-
{ triggerTurn: true, deliverAs: "followUp" },
|
|
1156
|
-
),
|
|
1157
|
-
)
|
|
1158
|
-
.then(
|
|
1159
|
-
() => this.collectResult(activation, "completed"),
|
|
1160
|
-
(error) => {
|
|
1161
|
-
activation.lastError = errorText(error);
|
|
1162
|
-
const result = this.collectResult(activation, "error");
|
|
1163
|
-
if (!result.output) result.output = activation.lastError ?? "";
|
|
1164
|
-
return result;
|
|
1165
|
-
},
|
|
1166
|
-
);
|
|
1167
|
-
let lifecycle!: Promise<SubagentRunResult>;
|
|
1168
|
-
lifecycle = core.then(async (result) => {
|
|
1169
|
-
if (activation.currentRun === lifecycle) activation.currentRun = undefined;
|
|
1170
|
-
await this.runFinished(activation, result);
|
|
1171
|
-
return result;
|
|
1172
|
-
});
|
|
1173
|
-
activation.currentRun = lifecycle;
|
|
1174
|
-
void lifecycle.catch(() => {});
|
|
2394
|
+
return { turnId, accepted, result: lifecycle };
|
|
1175
2395
|
}
|
|
1176
2396
|
|
|
1177
2397
|
private async runFinished(activation: Activation, result: SubagentRunResult): Promise<void> {
|
|
1178
|
-
activation.
|
|
2398
|
+
activation.pendingMailboxClaims.delete(result.turnId);
|
|
2399
|
+
activation.persistenceGate.reject(
|
|
2400
|
+
new Error(
|
|
2401
|
+
`subagent ${activation.agentId} ended before its session became durable`,
|
|
2402
|
+
),
|
|
2403
|
+
);
|
|
2404
|
+
finishAgentTurn(activation.controlState, result.turnId, result.stopReason);
|
|
2405
|
+
if (result.stopReason !== "completed") {
|
|
2406
|
+
activation.runtime.session.clearQueue();
|
|
2407
|
+
}
|
|
2408
|
+
const turnStarted = activation.startedTurnIds.delete(result.turnId);
|
|
2409
|
+
if (turnStarted) {
|
|
2410
|
+
this.emitTurnEnd(activation, result);
|
|
2411
|
+
}
|
|
1179
2412
|
activation.pendingSettlement = result;
|
|
2413
|
+
if (
|
|
2414
|
+
turnStarted
|
|
2415
|
+
&& !activation.silentSettlementTurnIds.has(result.turnId)
|
|
2416
|
+
&& activation.published
|
|
2417
|
+
&& activation.descriptor.mode === "continuable"
|
|
2418
|
+
) {
|
|
2419
|
+
try {
|
|
2420
|
+
appendCompletionUpdate(activation.parent.sessionManager, {
|
|
2421
|
+
parentAgentId: activation.descriptor.parentAgentId,
|
|
2422
|
+
childAgentId: activation.agentId,
|
|
2423
|
+
result,
|
|
2424
|
+
});
|
|
2425
|
+
} catch (error) {
|
|
2426
|
+
activation.lastError = errorText(error);
|
|
2427
|
+
let durableFallback = false;
|
|
2428
|
+
try {
|
|
2429
|
+
appendUndeliveredCompletion(
|
|
2430
|
+
activation.runtime.session.sessionManager,
|
|
2431
|
+
{
|
|
2432
|
+
parentAgentId:
|
|
2433
|
+
activation.descriptor.parentAgentId,
|
|
2434
|
+
childAgentId: activation.agentId,
|
|
2435
|
+
result,
|
|
2436
|
+
error: activation.lastError,
|
|
2437
|
+
},
|
|
2438
|
+
);
|
|
2439
|
+
durableFallback = true;
|
|
2440
|
+
} catch {
|
|
2441
|
+
// The explicit event below remains the final observable path
|
|
2442
|
+
// when both parent delivery and child fallback persistence fail.
|
|
2443
|
+
}
|
|
2444
|
+
this.pi.events.emit("pi-subagent:completion-error", {
|
|
2445
|
+
runId: activation.runId,
|
|
2446
|
+
turnId: result.turnId,
|
|
2447
|
+
agentId: activation.agentId,
|
|
2448
|
+
parentAgentId: activation.descriptor.parentAgentId,
|
|
2449
|
+
error: activation.lastError,
|
|
2450
|
+
durableFallback,
|
|
2451
|
+
});
|
|
2452
|
+
}
|
|
2453
|
+
this.completionWaiters
|
|
2454
|
+
.get(completionWaiterKey(activation.parent))
|
|
2455
|
+
?.wake();
|
|
2456
|
+
}
|
|
1180
2457
|
this.emitUpdate(activation, result);
|
|
1181
2458
|
if (activation.descriptor.mode === "one-shot") {
|
|
1182
2459
|
this.emitEnd(activation, result);
|
|
@@ -1184,7 +2461,6 @@ export class SubagentCoordinator {
|
|
|
1184
2461
|
}
|
|
1185
2462
|
if (activation.suppressSettlement || this.draining) return;
|
|
1186
2463
|
if (activation.ownedChildren.size > 0) {
|
|
1187
|
-
activation.status = "waiting";
|
|
1188
2464
|
this.emitUpdate(activation, result);
|
|
1189
2465
|
return;
|
|
1190
2466
|
}
|
|
@@ -1192,48 +2468,59 @@ export class SubagentCoordinator {
|
|
|
1192
2468
|
}
|
|
1193
2469
|
|
|
1194
2470
|
private async finalizeContinuable(activation: Activation): Promise<void> {
|
|
1195
|
-
|
|
2471
|
+
const retained = await this.agentOperations.run(activation.agentId, () =>
|
|
2472
|
+
this.finalizeContinuableLocked(activation),
|
|
2473
|
+
);
|
|
2474
|
+
if (retained) {
|
|
2475
|
+
await this.trimIdleRuntimes(activation.agentId);
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
private async finalizeContinuableLocked(activation: Activation): Promise<boolean> {
|
|
2480
|
+
if (activation.finalizing || activation.disposed || activation.currentRun) {
|
|
2481
|
+
return false;
|
|
2482
|
+
}
|
|
1196
2483
|
const result = activation.pendingSettlement;
|
|
1197
|
-
if (!result || activation.ownedChildren.size > 0) return;
|
|
2484
|
+
if (!result || activation.ownedChildren.size > 0) return false;
|
|
1198
2485
|
activation.finalizing = true;
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
2486
|
+
const finalizePromise = (async (): Promise<boolean> => {
|
|
2487
|
+
let retained = false;
|
|
2488
|
+
try {
|
|
2489
|
+
this.emitEnd(activation, result);
|
|
2490
|
+
activation.silentSettlementTurnIds.delete(result.turnId);
|
|
2491
|
+
activation.pendingSettlement = undefined;
|
|
2492
|
+
if (this.shouldRetainIdleRuntime(activation)) {
|
|
2493
|
+
activation.published = false;
|
|
2494
|
+
activation.runId = uuidv7();
|
|
2495
|
+
await this.releaseParentOwnership(activation);
|
|
2496
|
+
this.touchActivation(activation);
|
|
2497
|
+
retained = true;
|
|
2498
|
+
} else {
|
|
2499
|
+
await this.disposeActivation(activation);
|
|
2500
|
+
await this.releaseParentOwnership(activation);
|
|
2501
|
+
}
|
|
2502
|
+
} finally {
|
|
2503
|
+
if (!activation.disposed) {
|
|
2504
|
+
activation.finalizing = false;
|
|
2505
|
+
}
|
|
1204
2506
|
}
|
|
1205
|
-
|
|
1206
|
-
await this.disposeActivation(activation);
|
|
1207
|
-
await this.releaseParentOwnership(activation);
|
|
2507
|
+
return retained;
|
|
1208
2508
|
})();
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
truncated.truncated ? `\n\n[Closing message truncated; ${truncated.omittedBytes} bytes omitted.]` : ""
|
|
1219
|
-
}`;
|
|
1220
|
-
await activation.parent.deliver(
|
|
1221
|
-
SETTLED_CUSTOM_TYPE,
|
|
1222
|
-
content,
|
|
1223
|
-
{
|
|
1224
|
-
kind: "settled",
|
|
1225
|
-
childAgentId: activation.agentId,
|
|
1226
|
-
label: activation.descriptor.label,
|
|
1227
|
-
stopReason: result.stopReason,
|
|
1228
|
-
...(truncated.truncated ? { truncated: true } : {}),
|
|
1229
|
-
},
|
|
1230
|
-
"wakeup",
|
|
1231
|
-
);
|
|
2509
|
+
activation.finalizePromise = finalizePromise;
|
|
2510
|
+
try {
|
|
2511
|
+
return await finalizePromise;
|
|
2512
|
+
} finally {
|
|
2513
|
+
if (activation.finalizePromise === finalizePromise) {
|
|
2514
|
+
activation.finalizePromise = undefined;
|
|
2515
|
+
}
|
|
2516
|
+
if (!activation.disposed) activation.finalizing = false;
|
|
2517
|
+
}
|
|
1232
2518
|
}
|
|
1233
2519
|
|
|
1234
2520
|
private parentForActivation(activation: Activation): ParentRef {
|
|
1235
2521
|
return {
|
|
1236
2522
|
agentId: activation.agentId,
|
|
2523
|
+
taskPath: descriptorTaskPath(activation.descriptor),
|
|
1237
2524
|
depth: activation.descriptor.depth,
|
|
1238
2525
|
cwd: activation.descriptor.cwd,
|
|
1239
2526
|
sessionManager: activation.runtime.session.sessionManager,
|
|
@@ -1242,31 +2529,30 @@ export class SubagentCoordinator {
|
|
|
1242
2529
|
thinkingLevel: activation.runtime.session.thinkingLevel,
|
|
1243
2530
|
projectTrusted: activation.parent.projectTrusted,
|
|
1244
2531
|
activation,
|
|
1245
|
-
deliver: async (customType, content, details
|
|
2532
|
+
deliver: async (customType, content, details) => {
|
|
1246
2533
|
if (activation.disposed) throw new Error(`parent subagent ${activation.agentId} is no longer resident`);
|
|
1247
2534
|
const session = activation.runtime.session;
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
);
|
|
1255
|
-
return;
|
|
1256
|
-
}
|
|
1257
|
-
if (activation.currentRun || session.isStreaming) {
|
|
1258
|
-
await session.sendCustomMessage(
|
|
1259
|
-
{ customType, content, display: true, details },
|
|
1260
|
-
{ triggerTurn: true, deliverAs: "followUp" },
|
|
1261
|
-
);
|
|
1262
|
-
return;
|
|
1263
|
-
}
|
|
1264
|
-
this.startInternalMessage(activation, customType, content, details);
|
|
2535
|
+
await session.sendCustomMessage(
|
|
2536
|
+
{ customType, content, display: true, details },
|
|
2537
|
+
session.isStreaming
|
|
2538
|
+
? { triggerTurn: false, deliverAs: "nextTurn" }
|
|
2539
|
+
: { triggerTurn: false },
|
|
2540
|
+
);
|
|
1265
2541
|
},
|
|
1266
2542
|
};
|
|
1267
2543
|
}
|
|
1268
2544
|
|
|
1269
|
-
private
|
|
2545
|
+
private resetTurnCapture(activation: Activation): void {
|
|
2546
|
+
activation.epochMessageStart = activation.runtime.session.messages.length;
|
|
2547
|
+
activation.streamedText = "";
|
|
2548
|
+
activation.usage = emptyUsage();
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
private collectResult(
|
|
2552
|
+
activation: Activation,
|
|
2553
|
+
turnId: string,
|
|
2554
|
+
fallback: SubagentStopReason,
|
|
2555
|
+
): SubagentRunResult {
|
|
1270
2556
|
const messages = activation.runtime.session.messages;
|
|
1271
2557
|
const output = finalAssistantText(messages, activation.epochMessageStart, activation.streamedText);
|
|
1272
2558
|
const stopReason = finalStopReason(messages, activation.epochMessageStart, fallback);
|
|
@@ -1274,6 +2560,7 @@ export class SubagentCoordinator {
|
|
|
1274
2560
|
const sessionFile = activation.runtime.session.sessionFile;
|
|
1275
2561
|
return {
|
|
1276
2562
|
agentId: activation.agentId,
|
|
2563
|
+
turnId,
|
|
1277
2564
|
piSessionId: activation.runtime.session.sessionId,
|
|
1278
2565
|
...(sessionFile ? { sessionFile } : {}),
|
|
1279
2566
|
output: truncated.truncated
|
|
@@ -1281,6 +2568,12 @@ export class SubagentCoordinator {
|
|
|
1281
2568
|
sessionFile ? ` Full output: ${sessionFile}` : " Full output remains in the active child session."
|
|
1282
2569
|
}]`
|
|
1283
2570
|
: truncated.text,
|
|
2571
|
+
...(truncated.truncated
|
|
2572
|
+
? {
|
|
2573
|
+
outputTruncated: true,
|
|
2574
|
+
omittedBytes: truncated.omittedBytes,
|
|
2575
|
+
}
|
|
2576
|
+
: {}),
|
|
1284
2577
|
stopReason,
|
|
1285
2578
|
usage: structuredClone(activation.usage),
|
|
1286
2579
|
};
|
|
@@ -1301,12 +2594,47 @@ export class SubagentCoordinator {
|
|
|
1301
2594
|
this.emitUpdate(activation);
|
|
1302
2595
|
return;
|
|
1303
2596
|
}
|
|
2597
|
+
if (event.type === "agent_end") {
|
|
2598
|
+
void this.releaseWaitAgentDeliveries(
|
|
2599
|
+
this.parentForActivation(activation),
|
|
2600
|
+
"parent agent turn ended without a durable wait_agent result",
|
|
2601
|
+
).catch((error) => {
|
|
2602
|
+
activation.lastError = errorText(error);
|
|
2603
|
+
});
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
1304
2606
|
if (event.type !== "message_end") return;
|
|
2607
|
+
if (event.message.role === "user") {
|
|
2608
|
+
const turnId = currentAgentTurnId(activation.controlState);
|
|
2609
|
+
const gate = turnId
|
|
2610
|
+
? activation.userMessageGates.get(turnId)
|
|
2611
|
+
: undefined;
|
|
2612
|
+
if (turnId && gate) {
|
|
2613
|
+
try {
|
|
2614
|
+
gate.prepare();
|
|
2615
|
+
commitMailboxClaim(
|
|
2616
|
+
activation.runtime.session.sessionManager,
|
|
2617
|
+
turnId,
|
|
2618
|
+
event.message,
|
|
2619
|
+
);
|
|
2620
|
+
activation.pendingMailboxClaims.delete(turnId);
|
|
2621
|
+
gate.accept();
|
|
2622
|
+
} catch (error) {
|
|
2623
|
+
const failure =
|
|
2624
|
+
error instanceof Error ? error : new Error(String(error));
|
|
2625
|
+
activation.lastError = failure.message;
|
|
2626
|
+
gate.reject(failure);
|
|
2627
|
+
void activation.runtime.session.abort().catch(() => {});
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
1305
2632
|
if (event.message.role === "toolResult") {
|
|
1306
2633
|
if (event.message.usage) addUsage(activation.usage, event.message.usage, false);
|
|
1307
2634
|
return;
|
|
1308
2635
|
}
|
|
1309
2636
|
if (event.message.role !== "assistant") return;
|
|
2637
|
+
activation.persistenceGate.resolve();
|
|
1310
2638
|
addUsage(activation.usage, event.message.usage);
|
|
1311
2639
|
const text = event.message.content
|
|
1312
2640
|
.filter((part): part is Extract<(typeof event.message.content)[number], { type: "text" }> => part.type === "text")
|
|
@@ -1327,13 +2655,16 @@ export class SubagentCoordinator {
|
|
|
1327
2655
|
private publish(activation: Activation): void {
|
|
1328
2656
|
if (activation.published) return;
|
|
1329
2657
|
activation.published = true;
|
|
2658
|
+
activation.everPublished = true;
|
|
1330
2659
|
this.pi.events.emit("pi-subagent:start", {
|
|
1331
2660
|
runId: activation.runId,
|
|
1332
2661
|
agentId: activation.agentId,
|
|
2662
|
+
taskPath: descriptorTaskPath(activation.descriptor),
|
|
1333
2663
|
piSessionId: activation.runtime.session.sessionId,
|
|
1334
2664
|
parentAgentId: activation.descriptor.parentAgentId,
|
|
1335
2665
|
provider: activation.descriptor.provider,
|
|
1336
2666
|
mode: activation.descriptor.mode,
|
|
2667
|
+
context: descriptorContext(activation.descriptor),
|
|
1337
2668
|
});
|
|
1338
2669
|
}
|
|
1339
2670
|
|
|
@@ -1342,6 +2673,35 @@ export class SubagentCoordinator {
|
|
|
1342
2673
|
this.pi.events.emit("pi-subagent:end", {
|
|
1343
2674
|
runId: activation.runId,
|
|
1344
2675
|
agentId: activation.agentId,
|
|
2676
|
+
taskPath: descriptorTaskPath(activation.descriptor),
|
|
2677
|
+
piSessionId: activation.runtime.session.sessionId,
|
|
2678
|
+
parentAgentId: activation.descriptor.parentAgentId,
|
|
2679
|
+
provider: activation.descriptor.provider,
|
|
2680
|
+
mode: activation.descriptor.mode,
|
|
2681
|
+
stopReason: result.stopReason,
|
|
2682
|
+
output: result.output,
|
|
2683
|
+
});
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
private emitTurnStart(activation: Activation, turnId: string): void {
|
|
2687
|
+
this.pi.events.emit("pi-subagent:turn-start", {
|
|
2688
|
+
runId: activation.runId,
|
|
2689
|
+
turnId,
|
|
2690
|
+
agentId: activation.agentId,
|
|
2691
|
+
taskPath: descriptorTaskPath(activation.descriptor),
|
|
2692
|
+
piSessionId: activation.runtime.session.sessionId,
|
|
2693
|
+
parentAgentId: activation.descriptor.parentAgentId,
|
|
2694
|
+
provider: activation.descriptor.provider,
|
|
2695
|
+
mode: activation.descriptor.mode,
|
|
2696
|
+
});
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
private emitTurnEnd(activation: Activation, result: SubagentRunResult): void {
|
|
2700
|
+
this.pi.events.emit("pi-subagent:turn-end", {
|
|
2701
|
+
runId: activation.runId,
|
|
2702
|
+
turnId: result.turnId,
|
|
2703
|
+
agentId: activation.agentId,
|
|
2704
|
+
taskPath: descriptorTaskPath(activation.descriptor),
|
|
1345
2705
|
piSessionId: activation.runtime.session.sessionId,
|
|
1346
2706
|
parentAgentId: activation.descriptor.parentAgentId,
|
|
1347
2707
|
provider: activation.descriptor.provider,
|
|
@@ -1355,13 +2715,21 @@ export class SubagentCoordinator {
|
|
|
1355
2715
|
return {
|
|
1356
2716
|
kind: "delegation",
|
|
1357
2717
|
agentId: activation.agentId,
|
|
2718
|
+
taskPath: descriptorTaskPath(activation.descriptor),
|
|
2719
|
+
...(currentAgentTurnId(activation.controlState)
|
|
2720
|
+
? { turnId: currentAgentTurnId(activation.controlState) }
|
|
2721
|
+
: {}),
|
|
1358
2722
|
piSessionId: activation.runtime.session.sessionId,
|
|
1359
2723
|
provider: activation.descriptor.provider,
|
|
1360
2724
|
mode: activation.descriptor.mode,
|
|
2725
|
+
context: descriptorContext(activation.descriptor),
|
|
1361
2726
|
agent: activation.descriptor.agent.name,
|
|
1362
2727
|
label: activation.descriptor.label,
|
|
1363
2728
|
depth: activation.descriptor.depth,
|
|
1364
|
-
status:
|
|
2729
|
+
status: delegationStatus(
|
|
2730
|
+
activation.controlState,
|
|
2731
|
+
activation.ownedChildren.size > 0,
|
|
2732
|
+
),
|
|
1365
2733
|
...(activation.runtime.session.sessionFile
|
|
1366
2734
|
? { sessionFile: activation.runtime.session.sessionFile }
|
|
1367
2735
|
: {}),
|
|
@@ -1403,6 +2771,25 @@ export class SubagentCoordinator {
|
|
|
1403
2771
|
private async disposeActivation(activation: Activation): Promise<void> {
|
|
1404
2772
|
if (activation.disposed) return;
|
|
1405
2773
|
activation.disposed = true;
|
|
2774
|
+
const waiterKey = completionWaiterKey(
|
|
2775
|
+
this.parentForActivation(activation),
|
|
2776
|
+
);
|
|
2777
|
+
const waiter = this.completionWaiters.get(waiterKey);
|
|
2778
|
+
if (waiter) {
|
|
2779
|
+
this.removeCompletionWaiter(waiterKey, waiter);
|
|
2780
|
+
waiter.reject(
|
|
2781
|
+
new Error(
|
|
2782
|
+
`parent subagent ${activation.agentId} was disposed while wait_agent was pending`,
|
|
2783
|
+
),
|
|
2784
|
+
);
|
|
2785
|
+
}
|
|
2786
|
+
activation.persistenceGate.reject(
|
|
2787
|
+
new Error(
|
|
2788
|
+
`subagent ${activation.agentId} ended before its session became durable`,
|
|
2789
|
+
),
|
|
2790
|
+
);
|
|
2791
|
+
setAgentResidency(activation.controlState, "unloaded");
|
|
2792
|
+
if (activation.descriptor.mode === "one-shot") closeAgent(activation.controlState);
|
|
1406
2793
|
activation.unsubscribe?.();
|
|
1407
2794
|
activation.unsubscribe = undefined;
|
|
1408
2795
|
if (!activation.runtime.session.isIdle) await activation.runtime.session.abort().catch(() => {});
|
|
@@ -1414,8 +2801,9 @@ export class SubagentCoordinator {
|
|
|
1414
2801
|
}
|
|
1415
2802
|
|
|
1416
2803
|
private async releaseParentOwnership(activation: Activation): Promise<void> {
|
|
1417
|
-
const owner = activation.
|
|
2804
|
+
const owner = activation.ownerActivation;
|
|
1418
2805
|
if (!owner) return;
|
|
2806
|
+
activation.ownerActivation = undefined;
|
|
1419
2807
|
owner.ownedChildren.delete(activation.agentId);
|
|
1420
2808
|
if (
|
|
1421
2809
|
!owner.currentRun &&
|
|
@@ -1423,16 +2811,394 @@ export class SubagentCoordinator {
|
|
|
1423
2811
|
owner.pendingSettlement &&
|
|
1424
2812
|
owner.descriptor.mode === "continuable"
|
|
1425
2813
|
) {
|
|
1426
|
-
|
|
2814
|
+
void this.finalizeContinuable(owner).catch((error) => {
|
|
2815
|
+
owner.lastError = errorText(error);
|
|
2816
|
+
});
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
private acquireParentOwnership(
|
|
2821
|
+
activation: Activation,
|
|
2822
|
+
parent: ParentRef,
|
|
2823
|
+
): void {
|
|
2824
|
+
if (
|
|
2825
|
+
activation.ownerActivation
|
|
2826
|
+
&& activation.ownerActivation !== parent.activation
|
|
2827
|
+
) {
|
|
2828
|
+
throw new Error(
|
|
2829
|
+
`subagent ${descriptorTaskPath(activation.descriptor)} is still owned by another resident parent`,
|
|
2830
|
+
);
|
|
2831
|
+
}
|
|
2832
|
+
activation.parent = parent;
|
|
2833
|
+
if (!parent.activation) return;
|
|
2834
|
+
parent.activation.ownedChildren.add(activation.agentId);
|
|
2835
|
+
activation.ownerActivation = parent.activation;
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
private touchActivation(activation: Activation): void {
|
|
2839
|
+
activation.lastUsedSequence = ++this.activationSequence;
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
private shouldRetainIdleRuntime(activation: Activation): boolean {
|
|
2843
|
+
return (
|
|
2844
|
+
this.idleRuntimeLimit > 0
|
|
2845
|
+
&& !this.draining
|
|
2846
|
+
&& activation.descriptor.mode === "continuable"
|
|
2847
|
+
&& !activation.disposed
|
|
2848
|
+
&& !activation.currentRun
|
|
2849
|
+
&& activation.runtime.session.isIdle
|
|
2850
|
+
&& activation.ownedChildren.size === 0
|
|
2851
|
+
&& activation.pendingMailboxClaims.size === 0
|
|
2852
|
+
&& activation.persistenceGate.state !== "rejected"
|
|
2853
|
+
);
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
private isEvictableIdleRuntime(activation: Activation): boolean {
|
|
2857
|
+
return (
|
|
2858
|
+
!activation.disposed
|
|
2859
|
+
&& !activation.finalizing
|
|
2860
|
+
&& activation.descriptor.mode === "continuable"
|
|
2861
|
+
&& !activation.currentRun
|
|
2862
|
+
&& !activation.pendingSettlement
|
|
2863
|
+
&& activation.runtime.session.isIdle
|
|
2864
|
+
&& activation.ownedChildren.size === 0
|
|
2865
|
+
&& activation.pendingMailboxClaims.size === 0
|
|
2866
|
+
&& !activation.ownerActivation
|
|
2867
|
+
);
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
private async trimIdleRuntimes(protectedAgentId?: string): Promise<void> {
|
|
2871
|
+
await this.idleRuntimeOperations.run("idle-runtime-lru", async () =>
|
|
2872
|
+
this.trimIdleRuntimesLocked(protectedAgentId),
|
|
2873
|
+
);
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
private async trimIdleRuntimesLocked(
|
|
2877
|
+
protectedAgentId?: string,
|
|
2878
|
+
): Promise<void> {
|
|
2879
|
+
while (true) {
|
|
2880
|
+
const retained = [...this.active.values()].filter(
|
|
2881
|
+
(activation) =>
|
|
2882
|
+
activation.agentId === protectedAgentId
|
|
2883
|
+
? this.shouldRetainIdleRuntime(activation)
|
|
2884
|
+
: this.isEvictableIdleRuntime(activation),
|
|
2885
|
+
);
|
|
2886
|
+
if (retained.length <= this.idleRuntimeLimit) return;
|
|
2887
|
+
const candidate = retained
|
|
2888
|
+
.filter(
|
|
2889
|
+
(activation) =>
|
|
2890
|
+
activation.agentId !== protectedAgentId
|
|
2891
|
+
&& this.isEvictableIdleRuntime(activation),
|
|
2892
|
+
)
|
|
2893
|
+
.sort(
|
|
2894
|
+
(left, right) =>
|
|
2895
|
+
left.lastUsedSequence - right.lastUsedSequence
|
|
2896
|
+
|| left.agentId.localeCompare(right.agentId),
|
|
2897
|
+
)[0];
|
|
2898
|
+
if (!candidate) return;
|
|
2899
|
+
await this.agentOperations.run(candidate.agentId, async () => {
|
|
2900
|
+
if (
|
|
2901
|
+
this.active.get(candidate.agentId) === candidate
|
|
2902
|
+
&& this.isEvictableIdleRuntime(candidate)
|
|
2903
|
+
) {
|
|
2904
|
+
await this.disposeActivation(candidate);
|
|
2905
|
+
}
|
|
2906
|
+
});
|
|
1427
2907
|
}
|
|
1428
2908
|
}
|
|
1429
2909
|
|
|
1430
2910
|
private assertDirectParent(parent: ParentRef, descriptor: SubagentDescriptor): void {
|
|
1431
2911
|
if (descriptor.parentAgentId !== parent.agentId) {
|
|
1432
2912
|
throw new Error(
|
|
1433
|
-
`subagent ${descriptor.label} is not a direct child of ${parent.agentId}
|
|
2913
|
+
`subagent ${descriptor.label} is not a direct child of ${parent.agentId}`,
|
|
2914
|
+
);
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
private assertContinuableDirectChild(
|
|
2919
|
+
parent: ParentRef,
|
|
2920
|
+
descriptor: SubagentDescriptor,
|
|
2921
|
+
): void {
|
|
2922
|
+
if (descriptor.mode !== "continuable") {
|
|
2923
|
+
throw new Error(
|
|
2924
|
+
`subagent ${descriptor.agentId} is one-shot and cannot accept follow-up work`,
|
|
1434
2925
|
);
|
|
1435
2926
|
}
|
|
2927
|
+
this.assertDirectParent(parent, descriptor);
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
private treeRootAgentId(
|
|
2931
|
+
agentId: string,
|
|
2932
|
+
byId: ReadonlyMap<string, CatalogRecord>,
|
|
2933
|
+
): string {
|
|
2934
|
+
let current = agentId;
|
|
2935
|
+
const visited = new Set<string>();
|
|
2936
|
+
while (true) {
|
|
2937
|
+
if (visited.has(current)) {
|
|
2938
|
+
throw new Error("subagent descriptor lineage contains a cycle");
|
|
2939
|
+
}
|
|
2940
|
+
visited.add(current);
|
|
2941
|
+
const record = byId.get(current);
|
|
2942
|
+
if (!record) return current;
|
|
2943
|
+
current = record.descriptor.parentAgentId;
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
private async reserveTask(
|
|
2948
|
+
parent: ParentRef,
|
|
2949
|
+
requestedName: string | undefined,
|
|
2950
|
+
label: string,
|
|
2951
|
+
): Promise<ReservedTask> {
|
|
2952
|
+
const catalog = await this.catalogRecords(parent);
|
|
2953
|
+
const usedPaths = new Set(
|
|
2954
|
+
catalog.records
|
|
2955
|
+
.filter(
|
|
2956
|
+
(record) =>
|
|
2957
|
+
record.descriptor.parentAgentId === parent.agentId,
|
|
2958
|
+
)
|
|
2959
|
+
.map((record) => record.taskPath),
|
|
2960
|
+
);
|
|
2961
|
+
const reserve = (name: string): ReservedTask | undefined => {
|
|
2962
|
+
const path = taskPath(parent.taskPath, name);
|
|
2963
|
+
const reservationKey = `${parent.agentId}:${path}`;
|
|
2964
|
+
if (
|
|
2965
|
+
usedPaths.has(path)
|
|
2966
|
+
|| this.reservedTaskPaths.has(reservationKey)
|
|
2967
|
+
) {
|
|
2968
|
+
return undefined;
|
|
2969
|
+
}
|
|
2970
|
+
this.reservedTaskPaths.add(reservationKey);
|
|
2971
|
+
let released = false;
|
|
2972
|
+
return {
|
|
2973
|
+
name,
|
|
2974
|
+
path,
|
|
2975
|
+
release: () => {
|
|
2976
|
+
if (released) return;
|
|
2977
|
+
released = true;
|
|
2978
|
+
this.reservedTaskPaths.delete(reservationKey);
|
|
2979
|
+
},
|
|
2980
|
+
};
|
|
2981
|
+
};
|
|
2982
|
+
|
|
2983
|
+
if (requestedName !== undefined) {
|
|
2984
|
+
const name = validateTaskName(requestedName);
|
|
2985
|
+
const reserved = reserve(name);
|
|
2986
|
+
if (!reserved) {
|
|
2987
|
+
throw new Error(
|
|
2988
|
+
`task path ${taskPath(parent.taskPath, name)} is already in use`,
|
|
2989
|
+
);
|
|
2990
|
+
}
|
|
2991
|
+
return reserved;
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
const base = slugTaskName(label);
|
|
2995
|
+
const initial = reserve(base);
|
|
2996
|
+
if (initial) return initial;
|
|
2997
|
+
for (let ordinal = 2; ordinal < Number.MAX_SAFE_INTEGER; ordinal++) {
|
|
2998
|
+
const candidate = reserve(numberedTaskName(base, ordinal));
|
|
2999
|
+
if (candidate) return candidate;
|
|
3000
|
+
}
|
|
3001
|
+
throw new Error(`could not allocate a readable child path under ${parent.taskPath}`);
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
private async resolveTarget(
|
|
3005
|
+
parent: ParentRef,
|
|
3006
|
+
target: string,
|
|
3007
|
+
options: { allowUnknownId?: boolean } = {},
|
|
3008
|
+
): Promise<ResolvedTarget> {
|
|
3009
|
+
const catalog = await this.catalogRecords(parent);
|
|
3010
|
+
const byId = new Map(
|
|
3011
|
+
catalog.records.map((record) => [record.agentId, record]),
|
|
3012
|
+
);
|
|
3013
|
+
if (isAgentId(target)) {
|
|
3014
|
+
const record = byId.get(target);
|
|
3015
|
+
if (record) {
|
|
3016
|
+
return {
|
|
3017
|
+
agentId: record.agentId,
|
|
3018
|
+
taskPath: record.taskPath,
|
|
3019
|
+
record,
|
|
3020
|
+
};
|
|
3021
|
+
}
|
|
3022
|
+
if (options.allowUnknownId) {
|
|
3023
|
+
return {
|
|
3024
|
+
agentId: target,
|
|
3025
|
+
taskPath: target,
|
|
3026
|
+
};
|
|
3027
|
+
}
|
|
3028
|
+
return {
|
|
3029
|
+
agentId: target,
|
|
3030
|
+
taskPath: target,
|
|
3031
|
+
};
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
const path = resolveTaskPath(parent.taskPath, target);
|
|
3035
|
+
const rootAgentId = this.treeRootAgentId(parent.agentId, byId);
|
|
3036
|
+
const matches = this.reachableTreeRecords(rootAgentId, byId).filter(
|
|
3037
|
+
(record) => record.taskPath === path,
|
|
3038
|
+
);
|
|
3039
|
+
if (matches.length === 0) {
|
|
3040
|
+
throw new Error(`unknown subagent task path: ${path}`);
|
|
3041
|
+
}
|
|
3042
|
+
if (matches.length > 1) {
|
|
3043
|
+
throw new Error(
|
|
3044
|
+
`ambiguous subagent task path ${path}; use a durable agent id`,
|
|
3045
|
+
);
|
|
3046
|
+
}
|
|
3047
|
+
const record = matches[0]!;
|
|
3048
|
+
return {
|
|
3049
|
+
agentId: record.agentId,
|
|
3050
|
+
taskPath: record.taskPath,
|
|
3051
|
+
record,
|
|
3052
|
+
};
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
private reachableTreeRecords(
|
|
3056
|
+
rootAgentId: string,
|
|
3057
|
+
byId: ReadonlyMap<string, CatalogRecord>,
|
|
3058
|
+
): CatalogRecord[] {
|
|
3059
|
+
const paths = new Map<string, string>([
|
|
3060
|
+
[rootAgentId, ROOT_TASK_PATH],
|
|
3061
|
+
]);
|
|
3062
|
+
const depths = new Map<string, number>([[rootAgentId, 0]]);
|
|
3063
|
+
const reachable: CatalogRecord[] = [];
|
|
3064
|
+
const pending = new Set(byId.values());
|
|
3065
|
+
let progressed = true;
|
|
3066
|
+
while (progressed) {
|
|
3067
|
+
progressed = false;
|
|
3068
|
+
for (const record of [...pending]) {
|
|
3069
|
+
const parentPath = paths.get(
|
|
3070
|
+
record.descriptor.parentAgentId,
|
|
3071
|
+
);
|
|
3072
|
+
const parentDepth = depths.get(
|
|
3073
|
+
record.descriptor.parentAgentId,
|
|
3074
|
+
);
|
|
3075
|
+
if (parentPath === undefined || parentDepth === undefined) {
|
|
3076
|
+
continue;
|
|
3077
|
+
}
|
|
3078
|
+
{
|
|
3079
|
+
let expectedPath: string;
|
|
3080
|
+
try {
|
|
3081
|
+
expectedPath = taskPath(
|
|
3082
|
+
parentPath,
|
|
3083
|
+
record.descriptor.task.name,
|
|
3084
|
+
);
|
|
3085
|
+
} catch {
|
|
3086
|
+
pending.delete(record);
|
|
3087
|
+
progressed = true;
|
|
3088
|
+
continue;
|
|
3089
|
+
}
|
|
3090
|
+
if (
|
|
3091
|
+
record.taskPath !== expectedPath
|
|
3092
|
+
|| record.descriptor.depth !== parentDepth + 1
|
|
3093
|
+
) {
|
|
3094
|
+
pending.delete(record);
|
|
3095
|
+
progressed = true;
|
|
3096
|
+
continue;
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
pending.delete(record);
|
|
3100
|
+
reachable.push(record);
|
|
3101
|
+
paths.set(record.agentId, record.taskPath);
|
|
3102
|
+
depths.set(record.agentId, record.descriptor.depth);
|
|
3103
|
+
progressed = true;
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
return reachable;
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
private catalogTopologyDiagnostics(
|
|
3110
|
+
records: readonly CatalogRecord[],
|
|
3111
|
+
): CatalogDiagnostic[] {
|
|
3112
|
+
const byId = new Map(
|
|
3113
|
+
records.map((record) => [record.agentId, record]),
|
|
3114
|
+
);
|
|
3115
|
+
const problems = new Map<string, string>();
|
|
3116
|
+
for (const record of records) {
|
|
3117
|
+
const parent = byId.get(record.descriptor.parentAgentId);
|
|
3118
|
+
if (parent) {
|
|
3119
|
+
let expectedPath: string | undefined;
|
|
3120
|
+
try {
|
|
3121
|
+
expectedPath = taskPath(
|
|
3122
|
+
parent.taskPath,
|
|
3123
|
+
record.descriptor.task.name,
|
|
3124
|
+
);
|
|
3125
|
+
} catch {
|
|
3126
|
+
problems.set(
|
|
3127
|
+
record.agentId,
|
|
3128
|
+
"descriptor task path cannot be joined to its parent",
|
|
3129
|
+
);
|
|
3130
|
+
}
|
|
3131
|
+
if (
|
|
3132
|
+
expectedPath !== undefined
|
|
3133
|
+
&& (
|
|
3134
|
+
record.taskPath !== expectedPath
|
|
3135
|
+
|| record.descriptor.depth
|
|
3136
|
+
!== parent.descriptor.depth + 1
|
|
3137
|
+
)
|
|
3138
|
+
) {
|
|
3139
|
+
problems.set(
|
|
3140
|
+
record.agentId,
|
|
3141
|
+
"descriptor task path or depth does not match its parent",
|
|
3142
|
+
);
|
|
3143
|
+
}
|
|
3144
|
+
} else if (
|
|
3145
|
+
record.descriptor.depth === 1
|
|
3146
|
+
&& record.taskPath
|
|
3147
|
+
!== taskPath(
|
|
3148
|
+
ROOT_TASK_PATH,
|
|
3149
|
+
record.descriptor.task.name,
|
|
3150
|
+
)
|
|
3151
|
+
) {
|
|
3152
|
+
problems.set(
|
|
3153
|
+
record.agentId,
|
|
3154
|
+
"root child task path does not match its task name",
|
|
3155
|
+
);
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
for (const start of records) {
|
|
3160
|
+
const chain: string[] = [];
|
|
3161
|
+
const indexes = new Map<string, number>();
|
|
3162
|
+
let current: CatalogRecord | undefined = start;
|
|
3163
|
+
while (current) {
|
|
3164
|
+
const existing = indexes.get(current.agentId);
|
|
3165
|
+
if (existing !== undefined) {
|
|
3166
|
+
for (const agentId of chain.slice(existing)) {
|
|
3167
|
+
problems.set(
|
|
3168
|
+
agentId,
|
|
3169
|
+
"descriptor lineage contains a cycle",
|
|
3170
|
+
);
|
|
3171
|
+
}
|
|
3172
|
+
break;
|
|
3173
|
+
}
|
|
3174
|
+
indexes.set(current.agentId, chain.length);
|
|
3175
|
+
chain.push(current.agentId);
|
|
3176
|
+
current = byId.get(current.descriptor.parentAgentId);
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
return [...problems]
|
|
3181
|
+
.map(([agentId, message]) => {
|
|
3182
|
+
const record = byId.get(agentId)!;
|
|
3183
|
+
return {
|
|
3184
|
+
kind: "diagnostic" as const,
|
|
3185
|
+
piSessionId: record.piSessionId,
|
|
3186
|
+
reason: "corrupt" as const,
|
|
3187
|
+
...(record.sessionFile
|
|
3188
|
+
? { sessionFile: record.sessionFile }
|
|
3189
|
+
: {}),
|
|
3190
|
+
...(record.descriptor.parentSessionFile
|
|
3191
|
+
? {
|
|
3192
|
+
parentSessionFile:
|
|
3193
|
+
record.descriptor.parentSessionFile,
|
|
3194
|
+
}
|
|
3195
|
+
: {}),
|
|
3196
|
+
message,
|
|
3197
|
+
};
|
|
3198
|
+
})
|
|
3199
|
+
.sort((left, right) =>
|
|
3200
|
+
left.piSessionId.localeCompare(right.piSessionId),
|
|
3201
|
+
);
|
|
1436
3202
|
}
|
|
1437
3203
|
|
|
1438
3204
|
private async findPersistedChild(
|
|
@@ -1450,29 +3216,110 @@ export class SubagentCoordinator {
|
|
|
1450
3216
|
for (const item of persisted.descriptors) {
|
|
1451
3217
|
records.set(item.agentId, {
|
|
1452
3218
|
agentId: item.agentId,
|
|
3219
|
+
piSessionId: item.piSessionId,
|
|
3220
|
+
taskPath: descriptorTaskPath(item.descriptor),
|
|
1453
3221
|
descriptor: item.descriptor,
|
|
1454
3222
|
sessionFile: item.sessionFile,
|
|
3223
|
+
pendingMessages: item.pendingMessages,
|
|
3224
|
+
unreadUpdatesByChild: new Map(item.unreadUpdatesByChild),
|
|
1455
3225
|
});
|
|
1456
3226
|
}
|
|
1457
3227
|
const activeSessionIds = new Set(
|
|
1458
3228
|
[...this.active.values()].map((activation) => activation.runtime.session.sessionId),
|
|
1459
3229
|
);
|
|
3230
|
+
const activeDiagnostics: CatalogEntry[] = [];
|
|
3231
|
+
const rootCompletions = foldCompletionMailbox(
|
|
3232
|
+
parent.sessionManager.getEntries(),
|
|
3233
|
+
{ parentAgentId: parent.agentId },
|
|
3234
|
+
);
|
|
3235
|
+
const rootUnreadUpdatesByChild =
|
|
3236
|
+
rootCompletions.kind === "valid"
|
|
3237
|
+
? unreadCompletionCounts(rootCompletions.snapshot)
|
|
3238
|
+
: new Map<string, number>();
|
|
3239
|
+
if (rootCompletions.kind === "corrupt") {
|
|
3240
|
+
const parentFile = parent.sessionManager.getSessionFile();
|
|
3241
|
+
activeDiagnostics.push({
|
|
3242
|
+
kind: "diagnostic",
|
|
3243
|
+
piSessionId: parent.sessionManager.getSessionId(),
|
|
3244
|
+
reason: "corrupt",
|
|
3245
|
+
...(parentFile
|
|
3246
|
+
? {
|
|
3247
|
+
sessionFile: parentFile,
|
|
3248
|
+
parentSessionFile: parentFile,
|
|
3249
|
+
}
|
|
3250
|
+
: {}),
|
|
3251
|
+
message: `corrupt completion mailbox: ${rootCompletions.message}`,
|
|
3252
|
+
});
|
|
3253
|
+
}
|
|
1460
3254
|
for (const activation of this.active.values()) {
|
|
1461
3255
|
if (activation.descriptor.cwd !== parent.cwd) continue;
|
|
3256
|
+
let pendingMessages = 0;
|
|
3257
|
+
const mailbox = foldOwnedMailbox(
|
|
3258
|
+
activation.runtime.session.sessionManager.getEntries(),
|
|
3259
|
+
mailboxOwner(activation.descriptor),
|
|
3260
|
+
);
|
|
3261
|
+
if (mailbox.kind === "valid") {
|
|
3262
|
+
pendingMessages = mailbox.snapshot.pending.length;
|
|
3263
|
+
} else {
|
|
3264
|
+
activeDiagnostics.push({
|
|
3265
|
+
kind: "diagnostic",
|
|
3266
|
+
piSessionId: activation.runtime.session.sessionId,
|
|
3267
|
+
reason: "corrupt",
|
|
3268
|
+
...(activation.runtime.session.sessionFile
|
|
3269
|
+
? { sessionFile: activation.runtime.session.sessionFile }
|
|
3270
|
+
: {}),
|
|
3271
|
+
...(activation.descriptor.parentSessionFile
|
|
3272
|
+
? { parentSessionFile: activation.descriptor.parentSessionFile }
|
|
3273
|
+
: {}),
|
|
3274
|
+
message: `corrupt subagent mailbox: ${mailbox.message}`,
|
|
3275
|
+
});
|
|
3276
|
+
continue;
|
|
3277
|
+
}
|
|
3278
|
+
const completions = foldCompletionMailbox(
|
|
3279
|
+
activation.runtime.session.sessionManager.getEntries(),
|
|
3280
|
+
{ parentAgentId: activation.agentId },
|
|
3281
|
+
);
|
|
3282
|
+
if (completions.kind === "corrupt") {
|
|
3283
|
+
activeDiagnostics.push({
|
|
3284
|
+
kind: "diagnostic",
|
|
3285
|
+
piSessionId: activation.runtime.session.sessionId,
|
|
3286
|
+
reason: "corrupt",
|
|
3287
|
+
...(activation.runtime.session.sessionFile
|
|
3288
|
+
? { sessionFile: activation.runtime.session.sessionFile }
|
|
3289
|
+
: {}),
|
|
3290
|
+
...(activation.descriptor.parentSessionFile
|
|
3291
|
+
? { parentSessionFile: activation.descriptor.parentSessionFile }
|
|
3292
|
+
: {}),
|
|
3293
|
+
message: `corrupt completion mailbox: ${completions.message}`,
|
|
3294
|
+
});
|
|
3295
|
+
continue;
|
|
3296
|
+
}
|
|
1462
3297
|
records.set(activation.agentId, {
|
|
1463
3298
|
agentId: activation.agentId,
|
|
3299
|
+
piSessionId: activation.runtime.session.sessionId,
|
|
3300
|
+
taskPath: descriptorTaskPath(activation.descriptor),
|
|
1464
3301
|
descriptor: activation.descriptor,
|
|
1465
3302
|
...(activation.runtime.session.sessionFile
|
|
1466
3303
|
? { sessionFile: activation.runtime.session.sessionFile }
|
|
1467
3304
|
: {}),
|
|
1468
3305
|
active: activation,
|
|
3306
|
+
pendingMessages,
|
|
3307
|
+
unreadUpdatesByChild: unreadCompletionCounts(
|
|
3308
|
+
completions.snapshot,
|
|
3309
|
+
),
|
|
1469
3310
|
});
|
|
1470
3311
|
}
|
|
3312
|
+
const catalogRecords = [...records.values()];
|
|
1471
3313
|
return {
|
|
1472
|
-
records:
|
|
1473
|
-
diagnostics:
|
|
1474
|
-
|
|
1475
|
-
|
|
3314
|
+
records: catalogRecords,
|
|
3315
|
+
diagnostics: [
|
|
3316
|
+
...persisted.diagnostics.filter(
|
|
3317
|
+
(diagnostic) => !activeSessionIds.has(diagnostic.piSessionId),
|
|
3318
|
+
),
|
|
3319
|
+
...activeDiagnostics,
|
|
3320
|
+
...this.catalogTopologyDiagnostics(catalogRecords),
|
|
3321
|
+
],
|
|
3322
|
+
rootUnreadUpdatesByChild,
|
|
1476
3323
|
};
|
|
1477
3324
|
}
|
|
1478
3325
|
|
|
@@ -1504,7 +3351,6 @@ export class SubagentCoordinator {
|
|
|
1504
3351
|
|
|
1505
3352
|
export {
|
|
1506
3353
|
REPORT_CUSTOM_TYPE,
|
|
1507
|
-
SETTLED_CUSTOM_TYPE,
|
|
1508
3354
|
AGENT_CUSTOM_TYPE,
|
|
1509
3355
|
LINEAGE_CUSTOM_TYPE,
|
|
1510
3356
|
};
|