@howarewoo/pi-subagents 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,1432 @@
1
+ import {
2
+ leafDenial,
3
+ toolProvenance,
4
+ type ToolMetadata,
5
+ type CapabilityDenial,
6
+ } from "./capabilities.ts";
7
+ import { randomUUID } from "node:crypto";
8
+ import { mkdir } from "node:fs/promises";
9
+ import { join } from "node:path";
10
+ import {
11
+ createAgentSession,
12
+ createExtensionRuntime,
13
+ DefaultResourceLoader,
14
+ SessionManager,
15
+ SettingsManager,
16
+ type AgentSession,
17
+ type AgentSessionEvent,
18
+ type ExtensionToolContext,
19
+ type ResourceLoader,
20
+ type Skill,
21
+ type ToolDefinition,
22
+ } from "@earendil-works/pi-coding-agent";
23
+ import {
24
+ resultValidator,
25
+ submissionParameters,
26
+ saveOutcomeArtifact,
27
+ type StructuredOutcome,
28
+ } from "./outcomes.ts";
29
+ import type { Validator } from "@earendil-works/pi-schema";
30
+ import {
31
+ allowsModel,
32
+ sameSelection,
33
+ validateModelSelection,
34
+ type ModelSelection,
35
+ } from "./model-policy.ts";
36
+ import { parentModelRuntime } from "./model-adapter.ts";
37
+ import { resolveWorkspace, workspaceArguments } from "./workspace.ts";
38
+ import {
39
+ DEFAULT_POLICY,
40
+ narrowLimits,
41
+ validatePolicy,
42
+ type ExecutionLimits,
43
+ type SchedulingPolicy,
44
+ } from "./scheduling.ts";
45
+ import {
46
+ resolveProfile,
47
+ intersectProfileGrants,
48
+ isWriter,
49
+ READ_TOOLS,
50
+ type Profile,
51
+ type BuiltinTool,
52
+ type CoordinationAction,
53
+ } from "./profiles.ts";
54
+
55
+ const MAX_MESSAGE_BYTES = 4096;
56
+ const MAX_MAILBOX_MESSAGES = 64;
57
+ const MAX_RETAINED_MESSAGES = 512;
58
+ const MAX_WAITERS = 64;
59
+ const MAX_WAIT_MS = 60000;
60
+ const MAX_GENERATIONS = 64;
61
+
62
+ export type DelegationMessage = {
63
+ id: string;
64
+ sender: ChildRef | "root";
65
+ recipient: ChildRef | "root";
66
+ text: string;
67
+ mode: "inbox" | "steer" | "followUp";
68
+ status: "accepted" | "queued" | "handled" | "delivered" | "not-running" | "error";
69
+ error?: string;
70
+ };
71
+ export type CoordinationWait =
72
+ | { status: "timeout" | "cancelled" | "shutdown" }
73
+ | { status: "messages"; messages: DelegationMessage[] }
74
+ | { status: "message"; message: DelegationMessage }
75
+ | {
76
+ status: "outcome";
77
+ outcome: Pick<ChildSnapshot, "id" | "generation" | "status" | "limitReason">;
78
+ };
79
+ export type ChildRef = Readonly<{ id: string; generation: number }>;
80
+ export type ChildSnapshot = Readonly<
81
+ ChildRef & {
82
+ parentId: string;
83
+ parentGeneration?: number;
84
+ depth: number;
85
+ writerLease: boolean;
86
+ limits: ExecutionLimits;
87
+ limitReason?: string;
88
+ toolCalls: number;
89
+ status: "queued" | "setup" | "running" | "draining" | "settled" | "failed" | "cancelled";
90
+ profile: string;
91
+ tools: readonly BuiltinTool[];
92
+ leafTools?: readonly string[];
93
+ skills?: readonly string[];
94
+ capabilityDenials?: readonly CapabilityDenial[];
95
+ cwd?: string;
96
+ assignment: string;
97
+ activity: string;
98
+ progress: string;
99
+ result: string;
100
+ model: { provider: string; id: string };
101
+ effort: string | null;
102
+ prewalk?: {
103
+ target: ModelSelection;
104
+ status: "armed" | "handed-off" | "suppressed-exact" | "no-op";
105
+ };
106
+ telemetry: {
107
+ requests: number | null;
108
+ tokens: ReturnType<AgentSession["getSessionStats"]>["tokens"] | null;
109
+ cost: number | null;
110
+ context: ReturnType<AgentSession["getContextUsage"]> | null;
111
+ };
112
+ outcome?: StructuredOutcome;
113
+ artifacts?: { text?: string; structured?: string; error?: string };
114
+ error?: string;
115
+ transcript?: string;
116
+ disposed: boolean;
117
+ createdAt: number;
118
+ endedAt: number | null;
119
+ }
120
+ >;
121
+ export type RegistryEvent = Readonly<{
122
+ type: "changed" | "sdk";
123
+ child: ChildSnapshot;
124
+ event?: AgentSessionEvent;
125
+ }>;
126
+ export type StartTask = {
127
+ cwd?: string;
128
+ model?: ModelSelection;
129
+ resultSchema?: unknown;
130
+ assignment: string;
131
+ profile: Profile;
132
+ skills: Skill[];
133
+ context: ExtensionToolContext;
134
+ assertTools: (names: readonly string[]) => void;
135
+ toolMetadata?: () => readonly ToolMetadata[];
136
+ availableSkills?: () => readonly Skill[];
137
+ capabilityDenials?: CapabilityDenial[];
138
+ leafProvenance?: Readonly<Record<string, string>>;
139
+ callerId: string;
140
+ parent?: ChildRef;
141
+ limits?: Partial<ExecutionLimits>;
142
+ coordinationTools?: (actor: ChildRef) => ToolDefinition[];
143
+ nestedTool?: (parent: ChildRef) => ToolDefinition;
144
+ };
145
+ type ChildSelection = Pick<
146
+ ExtensionToolContext,
147
+ "model" | "modelRegistry" | "scopedModels" | "thinkingLevel" | "cwd"
148
+ >;
149
+ type Entry = {
150
+ validator?: Validator;
151
+ input: StartTask;
152
+ selection: ChildSelection;
153
+ done: Promise<void>;
154
+ finish: () => void;
155
+ timer?: ReturnType<typeof setTimeout>;
156
+ spawned: number;
157
+ state: ChildSnapshot;
158
+ finalizedMessages?: AgentSession["messages"];
159
+ setup?: Promise<void>;
160
+ run?: Promise<void>;
161
+ session?: AgentSession;
162
+ unsubscribe?: () => void;
163
+ cancelled: boolean;
164
+ callerId: string;
165
+ };
166
+
167
+ function admittedModel(entry: Entry, selection: ModelSelection): boolean {
168
+ return entry.input.profile.models === undefined
169
+ ? sameSelection(selection, {
170
+ ...entry.state.model,
171
+ effort: entry.state.effort as ModelSelection["effort"],
172
+ })
173
+ : allowsModel(entry.input.profile, selection);
174
+ }
175
+
176
+ export function childResources(prompt: string, skills: Skill[] = []): ResourceLoader {
177
+ const extensions = { extensions: [], errors: [], runtime: createExtensionRuntime() };
178
+ return {
179
+ getExtensions: () => extensions,
180
+ getSkills: () => ({ skills, diagnostics: [] }),
181
+ getPrompts: () => ({ prompts: [], diagnostics: [] }),
182
+ getThemes: () => ({ themes: [], diagnostics: [] }),
183
+ getAgentsFiles: () => ({ agentsFiles: [] }),
184
+ getSystemPrompt: () =>
185
+ prompt +
186
+ "\nYou are a delegated child. Complete only this assignment; use only explicitly granted tools and spawn roles.",
187
+ getSystemPromptSource: () => undefined,
188
+ getAppendSystemPrompt: () => [],
189
+ getAppendSystemPromptSources: () => [],
190
+ extendResources: () => {},
191
+ reload: async () => {},
192
+ };
193
+ }
194
+
195
+ /** Authoritative parent-lifetime registry. IDs are never reused; controls require their generation. */
196
+ export class DelegationCoordinator {
197
+ private entries = new Map<string, Entry>();
198
+ private queue = new Set<Entry>();
199
+ private history = new Map<string, { state: ChildSnapshot; messages: AgentSession["messages"] }>();
200
+ private presence = new Map<string, "active" | "idle" | "parked" | "stopped">();
201
+ private coordinationListeners = new Set<() => void>();
202
+ private coordinationMessages: DelegationMessage[] = [];
203
+ private mailboxes = new Map<string, DelegationMessage[]>();
204
+ private waits = new Map<string, Set<string>>();
205
+ private waiterCount = 0;
206
+ private notifyCoordination(): void {
207
+ for (const listener of this.coordinationListeners) listener();
208
+ }
209
+ agents() {
210
+ return this.list().map((state) => ({
211
+ id: state.id,
212
+ generation: state.generation,
213
+ presence: this.presence.get(state.id),
214
+ }));
215
+ }
216
+ generations(id: string): ChildSnapshot[] {
217
+ return [...this.history.values()]
218
+ .map((record) => record.state)
219
+ .concat(this.list())
220
+ .filter((state) => state.id === id)
221
+ .map((state) => structuredClone(state));
222
+ }
223
+ private listeners = new Set<(event: RegistryEvent) => void>();
224
+ private parentMutations = new Set<string>();
225
+ private writer?: string;
226
+ private closing = false;
227
+ private shutdownPromise?: Promise<void>;
228
+ readonly root: string;
229
+ readonly parentId: string;
230
+ constructor(root: string, parentId: string) {
231
+ this.root = root;
232
+ this.parentId = parentId;
233
+ }
234
+ private policy: SchedulingPolicy = { ...DEFAULT_POLICY };
235
+ configure(policy: SchedulingPolicy): void {
236
+ this.policy = validatePolicy(policy);
237
+ this.pump();
238
+ }
239
+ list(): ChildSnapshot[] {
240
+ return [...this.entries.values()].map((entry) => structuredClone(entry.state));
241
+ }
242
+ snapshot(ref?: ChildRef): ChildSnapshot | undefined {
243
+ if (ref) {
244
+ const historical = this.history.get(this.key(ref));
245
+ if (historical) return structuredClone(historical.state);
246
+ }
247
+ const entry = ref ? this.entry(ref) : [...this.entries.values()].at(-1);
248
+ return entry && structuredClone(entry.state);
249
+ }
250
+ private entry(ref: ChildRef): Entry {
251
+ const entry = this.entries.get(ref.id);
252
+ if (!entry || entry.state.generation !== ref.generation)
253
+ throw new Error("Unknown or stale delegation identity");
254
+ return entry;
255
+ }
256
+ subscribe(listener: (event: RegistryEvent) => void): () => void {
257
+ this.listeners.add(listener);
258
+ return () => {
259
+ this.listeners.delete(listener);
260
+ };
261
+ }
262
+ private update(entry: Entry, patch: Partial<ChildSnapshot>, event?: AgentSessionEvent): void {
263
+ entry.state = { ...entry.state, ...patch };
264
+ this.notifyCoordination();
265
+ for (const listener of this.listeners) {
266
+ // Observers must never own execution or turn a successful run into failure.
267
+ try {
268
+ listener({ type: event ? "sdk" : "changed", child: structuredClone(entry.state), event });
269
+ } catch {
270
+ /* observer isolated */
271
+ }
272
+ }
273
+ }
274
+ /** Reserve at preflight, release on every finalized call (including blocked calls). */
275
+ parentToolStart(
276
+ id: string,
277
+ name: string,
278
+ readOnly = READ_TOOLS.includes(name as (typeof READ_TOOLS)[number]),
279
+ ): void {
280
+ if (readOnly || name === "delegate_task" || name === "delegate_coordinate") return;
281
+ const child = this.writer ? this.entries.get(this.writer) : undefined;
282
+ if (this.writer && (!child || !id.startsWith(child.callerId + "/")))
283
+ throw new Error(
284
+ `Shared checkout writer is held by ${this.writer}; stop or wait for that task`,
285
+ );
286
+ this.parentMutations.add(id);
287
+ }
288
+ parentToolEnd(id: string): void {
289
+ this.parentMutations.delete(id);
290
+ this.pump();
291
+ }
292
+ private active(): Entry[] {
293
+ return [...this.entries.values()].filter(
294
+ (entry) => !entry.state.disposed && entry.state.status !== "queued",
295
+ );
296
+ }
297
+ private validate(input: StartTask): ChildSelection {
298
+ if (this.closing) throw new Error("Parent shut down");
299
+ if (!input.assignment.trim()) throw new Error("A nonempty assignment is required");
300
+ const selection = {
301
+ model: input.context.model,
302
+ modelRegistry: input.context.modelRegistry,
303
+ scopedModels: input.context.scopedModels,
304
+ thinkingLevel: input.context.thinkingLevel,
305
+ cwd: input.cwd ?? input.context.cwd,
306
+ };
307
+ if (!selection.model) throw new Error("A selected parent model is required");
308
+ const requested = input.model ?? input.profile.model;
309
+ const parentState = input.parent ? this.entry(input.parent).state : undefined;
310
+ const effective: ModelSelection = requested ?? {
311
+ ...(parentState?.model ?? { provider: selection.model.provider, id: selection.model.id }),
312
+ effort:
313
+ (parentState?.effort as ModelSelection["effort"] | undefined) ??
314
+ selection.thinkingLevel ??
315
+ "off",
316
+ };
317
+ if (
318
+ (requested && !input.profile.models?.some((item) => sameSelection(item, requested))) ||
319
+ !allowsModel(input.profile, effective)
320
+ )
321
+ throw new Error("Model selection denied by effective profile grants");
322
+ selection.model = validateModelSelection(selection, effective);
323
+ selection.thinkingLevel = effective.effort;
324
+ if (input.profile.prewalk && !input.model) {
325
+ if (!input.profile.models?.some((item) => sameSelection(item, input.profile.prewalk!)))
326
+ throw new Error("Prewalk target requires an exact model grant");
327
+ validateModelSelection(selection, input.profile.prewalk);
328
+ }
329
+ const writer = this.writer ? this.entries.get(this.writer) : undefined;
330
+ if (
331
+ isWriter(input.profile) &&
332
+ [...this.parentMutations].some((id) => !writer || !id.startsWith(writer.callerId + "/"))
333
+ )
334
+ throw new Error("Shared checkout writer conflict: in-flight parent mutation");
335
+ if (input.parent) {
336
+ const parent = this.entry(input.parent);
337
+ if (parent.cancelled || parent.state.disposed)
338
+ throw new Error("Delegating parent is no longer active");
339
+ if (
340
+ !admittedModel(parent, effective) ||
341
+ input.profile.models?.some((model) => !admittedModel(parent, model)) ||
342
+ (input.profile.prewalk && !input.model && !admittedModel(parent, input.profile.prewalk))
343
+ )
344
+ throw new Error("Descendant cannot broaden ancestor model grants");
345
+ if (!parent.input.profile.spawn?.includes(input.profile.name))
346
+ throw new Error("Ancestor spawn role denied");
347
+ if (parent.state.depth >= Math.min(this.policy.depth, parent.state.limits.depth))
348
+ throw new Error("Delegation depth limit");
349
+ if (
350
+ input.profile.tools.some((tool) => !parent.input.profile.tools.includes(tool)) ||
351
+ input.profile.skills.some((skill) => !parent.input.profile.skills.includes(skill)) ||
352
+ input.profile.leafTools?.some((name) => !parent.input.profile.leafTools?.includes(name)) ||
353
+ input.profile.spawn?.some((role) => !parent.input.profile.spawn?.includes(role)) ||
354
+ input.profile.coordination?.some(
355
+ (action) => !parent.input.profile.coordination?.includes(action),
356
+ )
357
+ )
358
+ throw new Error("Descendant cannot broaden ancestor grants");
359
+ let ancestor: Entry | undefined = parent;
360
+ while (ancestor) {
361
+ if (isWriter(input.profile) && isWriter(ancestor.input.profile))
362
+ throw new Error("Nested writer under writer ancestor is unsupported");
363
+ ancestor = this.entries.get(ancestor.state.parentId);
364
+ }
365
+ }
366
+ return selection;
367
+ }
368
+ /** Whole-batch synchronous validation precedes registry insertion, disk setup or dispatch. */
369
+ admit(inputs: StartTask[], replacement?: ChildRef): ChildRef[] {
370
+ const previous = replacement ? this.entry(replacement) : undefined;
371
+ inputs = inputs.map((input) => {
372
+ const cwd = resolveWorkspace(
373
+ input.cwd ??
374
+ previous?.selection.cwd ??
375
+ (input.parent ? this.entry(input.parent).selection.cwd : undefined),
376
+ input.context.cwd,
377
+ );
378
+ if (previous && cwd !== previous.selection.cwd)
379
+ throw new Error("Follow-up must retain its original workspace");
380
+ if (typeof input.assertTools !== "function")
381
+ throw new Error("Delegation requires a parent builtin provenance validator");
382
+ const callable = new Set(input.context.tools.map((tool) => tool.name));
383
+ const capabilityDenials: CapabilityDenial[] = [];
384
+ const leafTools: string[] = [];
385
+ const leafProvenance: Record<string, string> = {};
386
+ if (input.profile.name === "general") {
387
+ const metadata = input.toolMetadata?.() ?? [];
388
+ const sameWorkspace = cwd === resolveWorkspace(undefined, input.context.cwd);
389
+ for (const tool of metadata) {
390
+ if (input.profile.tools.includes(tool.name as BuiltinTool)) continue;
391
+ const reason = !callable.has(tool.name)
392
+ ? "tool is not live callable"
393
+ : leafDenial(tool, input.profile.leafTools ?? [], sameWorkspace);
394
+ if (reason) capabilityDenials.push({ name: tool.name, reason });
395
+ else {
396
+ leafTools.push(tool.name);
397
+ leafProvenance[tool.name] = toolProvenance(tool);
398
+ }
399
+ }
400
+ for (const name of input.profile.leafTools ?? [])
401
+ if (!metadata.some((tool) => tool.name === name))
402
+ capabilityDenials.push({ name, reason: "tool metadata unavailable" });
403
+ }
404
+ const profile = {
405
+ ...input.profile,
406
+ leafTools,
407
+ tools: input.profile.tools.filter((name) => callable.has(name)),
408
+ };
409
+ if (!profile.tools.length)
410
+ throw new Error(
411
+ `Profile ${profile.name} has no callable parent builtin tools; enable an allowed parent tool or choose a usable profile`,
412
+ );
413
+ input.assertTools(profile.tools);
414
+ return { ...input, profile, cwd, capabilityDenials, leafProvenance };
415
+ });
416
+ if (previous) {
417
+ if (
418
+ inputs.length !== 1 ||
419
+ !previous.state.disposed ||
420
+ !["idle", "parked"].includes(this.presence.get(previous.state.id) ?? "")
421
+ )
422
+ throw new Error("Assignment is not idle or parked");
423
+ if (this.key(previous.input.parent) !== this.key(inputs[0].parent))
424
+ throw new Error("Follow-up must retain its exact original parent generation");
425
+ if (previous.state.generation >= MAX_GENERATIONS)
426
+ throw new Error("Generation retention limit");
427
+ const profile = inputs[0].profile;
428
+ if (
429
+ (previous.input.profile.models !== undefined && profile.models === undefined) ||
430
+ profile.models?.some(
431
+ (model) => !previous.input.profile.models?.some((grant) => sameSelection(model, grant)),
432
+ )
433
+ )
434
+ throw new Error("Follow-up cannot broaden previous model grants");
435
+ if (
436
+ profile.name !== previous.input.profile.name ||
437
+ profile.tools.some((tool) => !previous.input.profile.tools.includes(tool)) ||
438
+ profile.skills.some((skill) => !previous.input.profile.skills.includes(skill)) ||
439
+ profile.leafTools?.some((name) => !previous.input.profile.leafTools?.includes(name)) ||
440
+ profile.spawn?.some((role) => !previous.input.profile.spawn?.includes(role)) ||
441
+ profile.coordination?.some(
442
+ (action) => !previous.input.profile.coordination?.includes(action),
443
+ )
444
+ )
445
+ throw new Error("Follow-up cannot broaden previous grants");
446
+ }
447
+ if (!inputs.length || inputs.length > this.policy.batch) throw new Error("Batch size limit");
448
+ const selections = inputs.map((input) => this.validate(input));
449
+ const validators = inputs.map((input) =>
450
+ input.resultSchema === undefined ? undefined : resultValidator(input.resultSchema),
451
+ );
452
+ const limits = inputs.map((input) => {
453
+ const maximum: ExecutionLimits = {
454
+ deadlineMs: this.policy.deadlineMs,
455
+ requests: this.policy.requests,
456
+ tools: this.policy.tools,
457
+ depth: this.policy.depth,
458
+ spawns: this.policy.spawns,
459
+ };
460
+ if (input.parent) {
461
+ const inherited = this.entry(input.parent).state.limits;
462
+ for (const key of Object.keys(maximum) as (keyof ExecutionLimits)[])
463
+ maximum[key] = Math.min(maximum[key], inherited[key]);
464
+ }
465
+ return narrowLimits(maximum, input.limits ?? {});
466
+ });
467
+ const queued = this.queue.size;
468
+ const pending = [...this.queue].map((entry) => entry.input).concat(inputs);
469
+ let slots = Math.max(0, this.policy.concurrency - this.active().length);
470
+ let writerBusy = !!this.writer || !!this.parentMutations.size;
471
+ let dispatchable = 0;
472
+ for (const input of pending) {
473
+ if (!slots || (isWriter(input.profile) && writerBusy)) break;
474
+ slots--;
475
+ dispatchable++;
476
+ if (isWriter(input.profile)) writerBusy = true;
477
+ }
478
+ if (pending.length - dispatchable > this.policy.queue)
479
+ throw new Error("Delegation queue capacity limit");
480
+ const parents = new Map<Entry, number>();
481
+ for (const input of inputs)
482
+ if (input.parent) {
483
+ const parent = this.entry(input.parent);
484
+ parents.set(parent, (parents.get(parent) ?? 0) + 1);
485
+ }
486
+ for (const [parent, count] of parents) {
487
+ if (parent.spawned + count > Math.min(this.policy.spawns, parent.state.limits.spawns))
488
+ throw new Error("Delegation spawn limit");
489
+ // Waiting sessions retain their permits: no lending or hidden overcommit. Refuse
490
+ // nested waits that cannot start immediately instead of queuing a deadlock.
491
+ if (
492
+ queued ||
493
+ this.active().length + inputs.length > this.policy.concurrency ||
494
+ (inputs.some((input) => isWriter(input.profile)) &&
495
+ (this.writer || inputs.filter((input) => isWriter(input.profile)).length > 1))
496
+ )
497
+ throw new Error("Nested capacity unavailable; no permit lending");
498
+ }
499
+ const entries = inputs.map((input, index) => {
500
+ const selection = selections[index];
501
+ let finish!: () => void;
502
+ const done = new Promise<void>((resolve) => {
503
+ finish = resolve;
504
+ });
505
+ const entry: Entry = {
506
+ validator: validators[index],
507
+ input,
508
+ selection,
509
+ done,
510
+ finish,
511
+ spawned: previous?.spawned ?? 0,
512
+ cancelled: false,
513
+ callerId: input.callerId,
514
+ state: {
515
+ id: previous?.state.id ?? randomUUID(),
516
+ generation: (previous?.state.generation ?? 0) + 1,
517
+ parentGeneration: input.parent?.generation,
518
+ parentId: input.parent?.id ?? this.parentId,
519
+ depth: input.parent ? this.entry(input.parent).state.depth + 1 : 1,
520
+ createdAt: Date.now(),
521
+ endedAt: null,
522
+ status: "queued",
523
+ writerLease: false,
524
+ limits: limits[index],
525
+ toolCalls: 0,
526
+ profile: input.profile.name,
527
+ tools: [...input.profile.tools],
528
+ leafTools: [...(input.profile.leafTools ?? [])],
529
+ skills: [...input.profile.skills],
530
+ capabilityDenials: input.capabilityDenials,
531
+ cwd: selection.cwd,
532
+ outcome: validators[index]
533
+ ? { validation: "awaiting", attempts: 0, errors: [], finalized: false }
534
+ : undefined,
535
+ assignment: input.assignment,
536
+ activity: "Queued for capacity / writer admission",
537
+ progress: "",
538
+ result: "",
539
+ disposed: false,
540
+ model: { provider: selection.model!.provider, id: selection.model!.id },
541
+ effort: selection.thinkingLevel ?? null,
542
+ prewalk: input.profile.prewalk
543
+ ? {
544
+ target: input.profile.prewalk,
545
+ status: input.model
546
+ ? "suppressed-exact"
547
+ : sameSelection(input.profile.prewalk, {
548
+ provider: selection.model!.provider,
549
+ id: selection.model!.id,
550
+ effort: selection.thinkingLevel!,
551
+ })
552
+ ? "no-op"
553
+ : "armed",
554
+ }
555
+ : undefined,
556
+ telemetry: { requests: null, tokens: null, cost: null, context: null },
557
+ },
558
+ };
559
+ return entry;
560
+ });
561
+ for (const [parent, count] of parents) parent.spawned += count;
562
+ if (previous)
563
+ this.history.set(this.key(previous.state), {
564
+ state: structuredClone(previous.state),
565
+ messages: previous.finalizedMessages ?? [],
566
+ });
567
+ for (const entry of entries) {
568
+ this.entries.set(entry.state.id, entry);
569
+ this.queue.add(entry);
570
+ this.presence.set(entry.state.id, "active");
571
+ }
572
+ for (const entry of entries) {
573
+ entry.timer = setTimeout(() => {
574
+ this.limit(entry, "deadline");
575
+ }, entry.state.limits.deadlineMs);
576
+ }
577
+ for (const entry of entries) if (!entry.state.disposed) this.update(entry, {});
578
+ this.pump();
579
+ return entries.map((entry) => ({ id: entry.state.id, generation: entry.state.generation }));
580
+ }
581
+ async start(input: StartTask): Promise<ChildRef> {
582
+ const [ref] = this.admit([input]);
583
+ await this.entry(ref).setup;
584
+ return ref;
585
+ }
586
+ renewalParent(ref: ChildRef): ChildRef | undefined {
587
+ const parent = this.entry(ref).input.parent;
588
+ if (parent) {
589
+ const entry = this.entry(parent);
590
+ if (entry.cancelled || entry.state.disposed || entry.state.status !== "running")
591
+ throw new Error("Original parent generation is no longer active");
592
+ }
593
+ return parent && { id: parent.id, generation: parent.generation };
594
+ }
595
+ renew(ref: ChildRef, input: StartTask): ChildRef {
596
+ const previous = this.entry(ref);
597
+ this.renewalParent(ref);
598
+ const profile = intersectProfileGrants(input.profile, previous.input.profile);
599
+ const retained =
600
+ previous.state.prewalk && ["handed-off", "no-op"].includes(previous.state.prewalk.status)
601
+ ? previous.state.prewalk.target
602
+ : previous.input.model;
603
+ const next = this.admit(
604
+ [
605
+ {
606
+ ...input,
607
+ model: retained ?? input.model,
608
+ profile,
609
+ skills: input.skills.filter((skill) => profile.skills.includes(skill.filePath)),
610
+ resultSchema:
611
+ input.resultSchema === undefined ? previous.input.resultSchema : input.resultSchema,
612
+ limits: narrowLimits(previous.state.limits, input.limits ?? {}),
613
+ },
614
+ ],
615
+ ref,
616
+ )[0];
617
+ if (previous.state.prewalk && previous.state.prewalk.status !== "armed")
618
+ this.update(this.entry(next), { prewalk: previous.state.prewalk });
619
+ return next;
620
+ }
621
+ private pump(): void {
622
+ if (this.closing) return;
623
+ for (const entry of this.queue) {
624
+ if (entry.cancelled || entry.state.disposed || entry.state.status !== "queued") continue;
625
+ if (this.active().length >= this.policy.concurrency) break;
626
+ const writer = isWriter(entry.input.profile);
627
+ if (writer && (this.writer || this.parentMutations.size)) break; // strict FIFO
628
+ this.queue.delete(entry);
629
+ if (writer) this.writer = entry.state.id;
630
+ entry.state = {
631
+ ...entry.state,
632
+ status: "setup",
633
+ writerLease: writer,
634
+ activity: "Preparing SDK child",
635
+ };
636
+ entry.setup = this.create(entry, entry.input, entry.selection);
637
+ this.update(entry, {});
638
+ void entry.setup.catch(() => {});
639
+ }
640
+ }
641
+ private limit(entry: Entry, reason: string): void {
642
+ if (
643
+ entry.cancelled ||
644
+ entry.state.disposed ||
645
+ entry.state.limitReason ||
646
+ ["settled", "failed", "cancelled"].includes(entry.state.status)
647
+ )
648
+ return;
649
+ this.update(entry, { limitReason: reason, error: `Execution limit: ${reason}` });
650
+ void this.stop(entry.state);
651
+ }
652
+ private async create(entry: Entry, input: StartTask, selection: ChildSelection): Promise<void> {
653
+ try {
654
+ await mkdir(this.root, { recursive: true });
655
+ const taskDir = join(
656
+ this.root,
657
+ entry.state.generation === 1
658
+ ? entry.state.id
659
+ : `${entry.state.id}-g${entry.state.generation}`,
660
+ );
661
+ await mkdir(taskDir); // exclusive ownership: never reuse an existing task record
662
+ const agentDir = join(taskDir, "agent");
663
+ await mkdir(agentDir);
664
+ const target =
665
+ entry.state.prewalk?.status === "armed"
666
+ ? validateModelSelection(input.context, entry.state.prewalk.target)
667
+ : undefined;
668
+ let policyError: string | undefined;
669
+ const effective = (): ModelSelection => ({
670
+ ...entry.state.model,
671
+ effort: entry.state.effort as ModelSelection["effort"],
672
+ });
673
+ const revalidate = async (selected: ModelSelection, requirePrewalk: boolean) => {
674
+ let current: Entry | undefined = entry;
675
+ while (current) {
676
+ const policy = (
677
+ await resolveProfile(
678
+ current.input.profile.name,
679
+ input.context.cwd,
680
+ input.context.isProjectTrusted(),
681
+ undefined,
682
+ current.input.availableSkills?.() ?? current.input.skills,
683
+ )
684
+ ).profile;
685
+ if (
686
+ (current.input.profile.models !== undefined && policy.models === undefined) ||
687
+ !allowsModel(policy, selected) ||
688
+ !admittedModel(current, selected)
689
+ )
690
+ throw new Error("Current or admitted model policy denied selection");
691
+ if (
692
+ current === entry &&
693
+ requirePrewalk &&
694
+ (!policy.prewalk || !sameSelection(policy.prewalk, entry.state.prewalk!.target))
695
+ )
696
+ throw new Error("Current prewalk grant revoked");
697
+ current = current.input.parent ? this.entry(current.input.parent) : undefined;
698
+ }
699
+ validateModelSelection(input.context, selected);
700
+ };
701
+ const runtime = await parentModelRuntime(
702
+ selection,
703
+ agentDir,
704
+ () => {
705
+ if (policyError) throw new Error(policyError);
706
+ validateModelSelection(input.context, effective());
707
+ if (entry.cancelled || this.closing) throw new Error("Delegation cancelled");
708
+ if ((entry.state.telemetry.requests ?? 0) >= entry.state.limits.requests) {
709
+ this.limit(entry, "requests");
710
+ throw new Error("Direct request limit");
711
+ }
712
+ this.update(entry, {
713
+ telemetry: {
714
+ ...entry.state.telemetry,
715
+ requests: (entry.state.telemetry.requests ?? 0) + 1,
716
+ },
717
+ });
718
+ },
719
+ target ? [target] : [],
720
+ );
721
+ const resources = childResources(
722
+ input.profile.prompt +
723
+ (target
724
+ ? "\nPrewalk: investigate and plan using only granted context/tools. After the first completed edit or write the same conversation switches once to the explicitly granted implementation target; all normal budgets still apply."
725
+ : "") +
726
+ `\nYour delegation identity is ${entry.state.id} generation ${entry.state.generation}; parent ${entry.state.parentId}. Coordination, if granted, is limited to family identities explicitly supplied to you.` +
727
+ (entry.validator
728
+ ? "\nThis assignment requires a structured result. Call submit_result with the actual value; final prose is not a submission."
729
+ : ""),
730
+ input.skills,
731
+ );
732
+ const loader = new DefaultResourceLoader({
733
+ cwd: selection.cwd,
734
+ agentDir,
735
+ settingsManager: SettingsManager.inMemory(),
736
+ noExtensions: true,
737
+ noSkills: true,
738
+ noPromptTemplates: true,
739
+ noThemes: true,
740
+ noContextFiles: true,
741
+ extensionFactories: [
742
+ (pi) => {
743
+ pi.on("turn_start", async () => {
744
+ try {
745
+ await revalidate(effective(), entry.state.prewalk?.status === "armed");
746
+ } catch (error) {
747
+ policyError = String(error);
748
+ }
749
+ });
750
+ pi.on("turn_end", async (event) => {
751
+ if (
752
+ !entry.cancelled &&
753
+ !this.closing &&
754
+ entry.state.prewalk?.status === "armed" &&
755
+ event.toolResults.some(
756
+ (result) =>
757
+ (result.toolName === "edit" || result.toolName === "write") &&
758
+ input.profile.tools.includes(result.toolName),
759
+ )
760
+ ) {
761
+ try {
762
+ const next = entry.state.prewalk.target;
763
+ await revalidate(next, true);
764
+ if (entry.cancelled || this.closing) return;
765
+ await entry.session!.setModel(target!);
766
+ entry.session!.setThinkingLevel(next.effort);
767
+ if (entry.session!.thinkingLevel !== next.effort)
768
+ throw new Error("SDK coerced prewalk effort");
769
+ this.update(entry, {
770
+ model: { provider: next.provider, id: next.id },
771
+ effort: next.effort,
772
+ prewalk: { target: next, status: "handed-off" },
773
+ });
774
+ } catch (error) {
775
+ policyError = String(error);
776
+ }
777
+ }
778
+ });
779
+ },
780
+ ],
781
+ });
782
+ await loader.reload();
783
+ resources.getExtensions = () => loader.getExtensions();
784
+ const tools: ToolDefinition[] = [
785
+ ...input.profile.tools,
786
+ ...(input.profile.leafTools ?? []),
787
+ ].map((name) => {
788
+ const tool = input.context.tools.find((tool) => tool.name === name);
789
+ if (!tool) throw new Error(`Admitted parent tool is no longer callable: ${name}`);
790
+ return {
791
+ name,
792
+ label: name,
793
+ description: tool.description,
794
+ parameters: tool.parameters,
795
+ execute: async (_id, args, signal, onUpdate) => {
796
+ if (entry.cancelled || this.closing) throw new Error("Delegation cancelled");
797
+ if (!input.context.tools.some((tool) => tool.name === name))
798
+ throw new Error(`Admitted parent tool is no longer callable: ${name}`);
799
+ const builtin = input.profile.tools.includes(name as BuiltinTool);
800
+ // Re-read configuration for this generation and every ancestor before dispatch.
801
+ let current: Entry | undefined = entry;
802
+ while (current) {
803
+ const { profile } = await resolveProfile(
804
+ current.input.profile.name,
805
+ input.context.cwd,
806
+ input.context.isProjectTrusted(),
807
+ undefined,
808
+ current.input.availableSkills?.() ?? current.input.skills,
809
+ );
810
+ if (
811
+ builtin
812
+ ? !profile.tools.includes(name as BuiltinTool)
813
+ : !profile.leafTools?.includes(name)
814
+ )
815
+ throw new Error(`Current capability grant revoked: ${name}`);
816
+ current = current.input.parent ? this.entry(current.input.parent) : undefined;
817
+ }
818
+ if (entry.cancelled || this.closing) throw new Error("Delegation cancelled");
819
+ if (builtin) input.assertTools([name]);
820
+ else {
821
+ const metadata = input.toolMetadata?.().find((tool) => tool.name === name);
822
+ if (
823
+ !metadata ||
824
+ leafDenial(
825
+ metadata,
826
+ input.profile.leafTools ?? [],
827
+ selection.cwd === resolveWorkspace(undefined, input.context.cwd),
828
+ ) ||
829
+ toolProvenance(metadata) !== input.leafProvenance?.[name]
830
+ )
831
+ throw new Error(`Leaf capability revoked or reclassified: ${name}`);
832
+ }
833
+ if (!input.context.tools.some((tool) => tool.name === name))
834
+ throw new Error(`Admitted parent tool is no longer callable: ${name}`);
835
+ // The public parent dispatcher preserves validation and permission hooks. Do not
836
+ // call a raw create*Tool.execute here: that would bypass operator policy.
837
+ const outcome = await input.context.executeTool(
838
+ name,
839
+ builtin
840
+ ? workspaceArguments(name as BuiltinTool, args, selection.cwd, input.context.cwd)
841
+ : args,
842
+ { signal, onUpdate },
843
+ );
844
+ return { ...outcome.result, isError: outcome.isError };
845
+ },
846
+ };
847
+ });
848
+ if (entry.validator) {
849
+ const submission: ToolDefinition<typeof submissionParameters> = {
850
+ name: "submit_result",
851
+ label: "Submit result",
852
+ exposure: "model-only",
853
+ description: `Submit the assignment's required structured JSON value exactly once. Invalid values return errors and can be corrected within normal limits. Submission is provisional until prompt/tools/descendants drain; it never overrides cancellation or failure. Required schema: ${JSON.stringify(input.resultSchema)}`,
854
+ parameters: submissionParameters,
855
+ execute: async (_id, args, signal) => {
856
+ signal?.throwIfAborted();
857
+ if (
858
+ entry.cancelled ||
859
+ this.closing ||
860
+ entry.state.disposed ||
861
+ entry.state.status !== "running"
862
+ )
863
+ throw new Error("Submission identity is no longer running");
864
+ const outcome = entry.state.outcome!;
865
+ if (outcome.validation === "submitted")
866
+ throw new Error("A result was already submitted; duplicate submission rejected");
867
+ const checked = entry.validator!.validate(args.value);
868
+ if (!checked.ok) {
869
+ const errors = checked.errors
870
+ .map((error) => `${error.path || "/"}: ${error.message}`)
871
+ .slice(0, 32);
872
+ this.update(entry, {
873
+ outcome: {
874
+ validation: "invalid",
875
+ attempts: outcome.attempts + 1,
876
+ errors,
877
+ finalized: false,
878
+ },
879
+ });
880
+ throw new Error(
881
+ "Invalid structured result; correct and resubmit: " + errors.join("; "),
882
+ );
883
+ }
884
+ this.update(entry, {
885
+ outcome: {
886
+ validation: "submitted",
887
+ attempts: outcome.attempts + 1,
888
+ errors: [],
889
+ value: structuredClone(args.value),
890
+ finalized: false,
891
+ },
892
+ });
893
+ return {
894
+ content: [
895
+ {
896
+ type: "text",
897
+ text: "Structured value submitted; final outcome awaits prompt/tool/descendant drain.",
898
+ },
899
+ ],
900
+ details: {},
901
+ };
902
+ },
903
+ };
904
+ tools.push(submission);
905
+ }
906
+ if (input.profile.coordination?.length && input.coordinationTools)
907
+ tools.push(
908
+ ...input.coordinationTools({ id: entry.state.id, generation: entry.state.generation }),
909
+ );
910
+ if (input.profile.spawn?.length && input.nestedTool)
911
+ tools.push(input.nestedTool(entry.state));
912
+ for (const tool of tools) {
913
+ const execute = tool.execute;
914
+ tool.execute = async (...args) => {
915
+ if (entry.cancelled || this.closing) throw new Error("Delegation cancelled");
916
+ if (entry.state.toolCalls >= entry.state.limits.tools) {
917
+ this.limit(entry, "tools");
918
+ throw new Error("Tool call limit");
919
+ }
920
+ this.update(entry, { toolCalls: entry.state.toolCalls + 1 });
921
+ return execute(...args);
922
+ };
923
+ }
924
+ const { session, modelFallbackMessage } = await createAgentSession({
925
+ cwd: selection.cwd,
926
+ agentDir,
927
+ modelRuntime: runtime,
928
+ model: selection.model,
929
+ thinkingLevel: selection.thinkingLevel,
930
+ scopedModels: [
931
+ { model: selection.model!, thinkingLevel: selection.thinkingLevel },
932
+ ...(target ? [{ model: target, thinkingLevel: input.profile.prewalk!.effort }] : []),
933
+ ],
934
+ resourceLoader: resources,
935
+ tools: tools.map((tool) => tool.name),
936
+ customTools: tools,
937
+ settingsManager: SettingsManager.inMemory({
938
+ compaction: { enabled: false },
939
+ retry: { enabled: false },
940
+ }),
941
+ sessionManager: SessionManager.create(selection.cwd, join(taskDir, "transcripts")),
942
+ });
943
+ entry.session = session;
944
+ if (session.thinkingLevel !== selection.thinkingLevel)
945
+ throw new Error("SDK did not preserve selected effort");
946
+ if (
947
+ modelFallbackMessage ||
948
+ session.model?.id !== selection.model?.id ||
949
+ session.model?.provider !== selection.model?.provider
950
+ )
951
+ throw new Error("SDK did not preserve selected model");
952
+ let markStarted!: () => void;
953
+ const started = new Promise<void>((resolve) => {
954
+ markStarted = resolve;
955
+ });
956
+ entry.unsubscribe = session.subscribe((event) => {
957
+ const patch: { -readonly [K in keyof ChildSnapshot]?: ChildSnapshot[K] } = {};
958
+ if (event.type === "agent_start") {
959
+ patch.status = entry.cancelled ? "draining" : "running";
960
+ markStarted();
961
+ }
962
+ if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
963
+ patch.progress = (entry.state.progress + event.assistantMessageEvent.delta).slice(-4096);
964
+ patch.activity = patch.progress;
965
+ }
966
+ if (event.type === "message_end" && event.message.role === "user") {
967
+ const text =
968
+ typeof event.message.content === "string"
969
+ ? event.message.content
970
+ : event.message.content
971
+ .filter((block) => block.type === "text")
972
+ .map((block) => block.text)
973
+ .join("");
974
+ for (const message of this.coordinationMessages)
975
+ if (
976
+ message.recipient !== "root" &&
977
+ this.key(message.recipient) === this.key(entry.state) &&
978
+ text ===
979
+ `Delegation message ${message.id} from ${message.sender === "root" ? "root" : this.key(message.sender)}: ${message.text}`
980
+ )
981
+ message.status = "delivered";
982
+ this.notifyCoordination();
983
+ }
984
+ if (event.type === "tool_execution_start") patch.activity = `Tool: ${event.toolName}`;
985
+ if (event.type === "message_end" && event.message.role === "assistant") {
986
+ const stats = session.getSessionStats();
987
+ patch.telemetry = {
988
+ ...entry.state.telemetry,
989
+ tokens: stats.tokens,
990
+ cost: stats.cost,
991
+ context: session.getContextUsage() ?? null,
992
+ };
993
+ }
994
+ this.update(entry, patch, event);
995
+ });
996
+ this.update(entry, { transcript: session.sessionFile });
997
+ if (this.closing || entry.cancelled) {
998
+ this.update(entry, { status: "cancelled" });
999
+ this.dispose(entry);
1000
+ return;
1001
+ }
1002
+ entry.run = session
1003
+ .prompt(input.assignment, { expandPromptTemplates: false })
1004
+ .then(async () => {
1005
+ await this.stopMany(this.children(entry).map((child) => child.state));
1006
+ const last = session.messages.filter((message) => message.role === "assistant").at(-1);
1007
+ const failed = !last || last.stopReason === "error";
1008
+ const stats = session.getSessionStats();
1009
+ this.update(entry, { status: "draining", activity: "Finalizing outcome artifacts" });
1010
+ const result = session.getLastAssistantText() ?? "";
1011
+ const artifacts: NonNullable<ChildSnapshot["artifacts"]> = {};
1012
+ try {
1013
+ artifacts.text = await saveOutcomeArtifact(
1014
+ taskDir,
1015
+ "result.txt",
1016
+ last?.content
1017
+ .filter((block) => block.type === "text")
1018
+ .map((block) => block.text)
1019
+ .join("") ?? "",
1020
+ );
1021
+ if (entry.state.outcome?.validation === "submitted")
1022
+ artifacts.structured = await saveOutcomeArtifact(
1023
+ taskDir,
1024
+ "structured.json",
1025
+ JSON.stringify(entry.state.outcome.value),
1026
+ );
1027
+ } catch (error) {
1028
+ artifacts.error = `Outcome artifact persistence failed: ${String(error)}`;
1029
+ }
1030
+ const missing = entry.validator && entry.state.outcome?.validation !== "submitted";
1031
+ this.update(entry, {
1032
+ artifacts,
1033
+ outcome: entry.state.outcome ? { ...entry.state.outcome, finalized: true } : undefined,
1034
+ status:
1035
+ entry.cancelled || last?.stopReason === "aborted"
1036
+ ? "cancelled"
1037
+ : failed || missing || artifacts.error
1038
+ ? "failed"
1039
+ : "settled",
1040
+ result,
1041
+ error: entry.state.limitReason
1042
+ ? entry.state.error
1043
+ : failed
1044
+ ? (last?.errorMessage ?? "No final assistant outcome")
1045
+ : missing
1046
+ ? "Required structured result was not submitted successfully"
1047
+ : artifacts.error,
1048
+ activity: "Prompt completed",
1049
+ telemetry: {
1050
+ requests: entry.state.telemetry.requests,
1051
+ tokens: stats.assistantMessages ? stats.tokens : null,
1052
+ cost: stats.assistantMessages ? stats.cost : null,
1053
+ context: session.getContextUsage() ?? null,
1054
+ },
1055
+ });
1056
+ })
1057
+ .catch(async (error) => {
1058
+ await this.stopMany(this.children(entry).map((child) => child.state));
1059
+ this.update(entry, {
1060
+ status: entry.cancelled ? "cancelled" : "failed",
1061
+ error: entry.state.limitReason ? entry.state.error : String(error),
1062
+ });
1063
+ })
1064
+ .finally(() => this.dispose(entry));
1065
+ // prompt preparation precedes abort-signal ownership; acknowledge only abortable work.
1066
+ await Promise.race([started, entry.run]);
1067
+ } catch (error) {
1068
+ this.update(entry, {
1069
+ status: entry.cancelled || this.closing ? "cancelled" : "failed",
1070
+ error: String(error),
1071
+ });
1072
+ this.dispose(entry);
1073
+ throw error;
1074
+ }
1075
+ }
1076
+ private closeDescendantPresence(entry: Entry): void {
1077
+ for (const child of this.entries.values())
1078
+ if (
1079
+ child.state.parentId === entry.state.id &&
1080
+ child.state.parentGeneration === entry.state.generation
1081
+ ) {
1082
+ this.presence.set(child.state.id, "stopped");
1083
+ this.closeDescendantPresence(child);
1084
+ }
1085
+ }
1086
+ private dispose(entry: Entry): void {
1087
+ if (entry.state.disposed) return;
1088
+ this.queue.delete(entry);
1089
+ clearTimeout(entry.timer);
1090
+ entry.finalizedMessages = structuredClone(entry.session?.messages ?? []);
1091
+ entry.unsubscribe?.();
1092
+ entry.session?.dispose();
1093
+ // Retain immutable evidence, not the disposed SDK session graph, while idle/parked.
1094
+ entry.unsubscribe = undefined;
1095
+ entry.session = undefined;
1096
+ if (this.writer === entry.state.id) this.writer = undefined;
1097
+ this.closeDescendantPresence(entry);
1098
+ this.presence.set(entry.state.id, entry.cancelled || this.closing ? "stopped" : "idle");
1099
+ this.update(entry, {
1100
+ disposed: true,
1101
+ endedAt: Date.now(),
1102
+ writerLease: false,
1103
+ outcome: entry.state.outcome ? { ...entry.state.outcome, finalized: true } : undefined,
1104
+ });
1105
+ entry.finish();
1106
+ this.pump();
1107
+ }
1108
+ private key(ref?: ChildRef): string {
1109
+ return ref ? `${ref.id}/${ref.generation}` : "root";
1110
+ }
1111
+ private actor(ref: ChildRef | undefined, action: CoordinationAction): Entry | undefined {
1112
+ if (this.closing) throw new Error("Parent shut down");
1113
+ if (!ref) return;
1114
+ const actor = this.entry(ref);
1115
+ if (actor.cancelled || actor.state.disposed || actor.state.status !== "running")
1116
+ throw new Error("Sender is no longer running");
1117
+ if (!actor.input.profile.coordination?.includes(action))
1118
+ throw new Error(`Coordination action denied: ${action}`);
1119
+ return actor;
1120
+ }
1121
+ private target(actor: Entry | undefined, ref: ChildRef): Entry {
1122
+ const target = this.entry(ref);
1123
+ // A selected action grants family addressing, never transcript/resource access.
1124
+ if (
1125
+ actor &&
1126
+ target !== actor &&
1127
+ !(
1128
+ target.state.parentId === actor.state.id &&
1129
+ target.state.parentGeneration === actor.state.generation
1130
+ ) &&
1131
+ !(
1132
+ actor.state.parentId === target.state.id &&
1133
+ actor.state.parentGeneration === target.state.generation
1134
+ ) &&
1135
+ !(
1136
+ actor.state.parentId === target.state.parentId &&
1137
+ actor.state.parentGeneration === target.state.parentGeneration
1138
+ )
1139
+ )
1140
+ throw new Error("Coordination target outside granted family");
1141
+ return target;
1142
+ }
1143
+ inbox(actor?: ChildRef): DelegationMessage[] {
1144
+ this.actor(actor, "inbox");
1145
+ return this.takeInbox(actor);
1146
+ }
1147
+ async message(
1148
+ actor: ChildRef | undefined,
1149
+ target: ChildRef | undefined,
1150
+ text: string,
1151
+ mode: "inbox" | "steer" | "followUp" = "inbox",
1152
+ ): Promise<DelegationMessage> {
1153
+ const sender = this.actor(actor, "message");
1154
+ if (typeof text !== "string" || !text.trim() || Buffer.byteLength(text) > MAX_MESSAGE_BYTES)
1155
+ throw new Error("Message requires 1–4096 UTF-8 bytes");
1156
+ if (!target && !sender) throw new Error("Root cannot message itself");
1157
+ const recipient = target ? this.target(sender, target) : undefined;
1158
+ if (!target && sender?.state.parentId !== this.parentId)
1159
+ throw new Error("Only direct children may message root");
1160
+ if (target && actor && this.key(target) === this.key(actor))
1161
+ throw new Error("Self messaging denied");
1162
+ const record: DelegationMessage = {
1163
+ id: randomUUID(),
1164
+ sender: actor ? { ...actor } : "root",
1165
+ recipient: target ? { ...target } : "root",
1166
+ text,
1167
+ mode,
1168
+ status: "accepted",
1169
+ };
1170
+ if (
1171
+ recipient &&
1172
+ (recipient.cancelled || recipient.state.disposed || recipient.state.status !== "running")
1173
+ )
1174
+ return { ...record, status: "not-running" };
1175
+ if (!target && mode !== "inbox") throw new Error("Root accepts inbox messages only");
1176
+ const key = this.key(target);
1177
+ const mailbox = this.mailboxes.get(key) ?? [];
1178
+ if (
1179
+ mailbox.length >= MAX_MAILBOX_MESSAGES ||
1180
+ this.coordinationMessages.length >= MAX_RETAINED_MESSAGES
1181
+ )
1182
+ throw new Error("Parent message retention/mailbox limit");
1183
+ this.coordinationMessages.push(record);
1184
+ mailbox.push(record);
1185
+ this.mailboxes.set(key, mailbox);
1186
+ this.notifyCoordination();
1187
+ if (mode !== "inbox") {
1188
+ try {
1189
+ const content = `Delegation message ${record.id} from ${this.key(actor)}: ${text}`;
1190
+ const disposition = await recipient!.session![mode](content);
1191
+ // The captured recipient/session is never looked up again by ID.
1192
+ if (record.status !== "delivered")
1193
+ record.status = disposition === "queued" ? "queued" : "handled";
1194
+ } catch (error) {
1195
+ record.status = "error";
1196
+ record.error = String(error);
1197
+ }
1198
+ this.notifyCoordination();
1199
+ }
1200
+ return structuredClone(record);
1201
+ }
1202
+ park(ref: ChildRef): void {
1203
+ if (this.closing) throw new Error("Parent shut down");
1204
+ const entry = this.entry(ref);
1205
+ this.renewalParent(ref);
1206
+ if (!entry.state.disposed || this.presence.get(ref.id) !== "idle")
1207
+ throw new Error("Park requires a disposed idle assignment; stop active work explicitly");
1208
+ this.presence.set(ref.id, "parked");
1209
+ this.update(entry, {});
1210
+ }
1211
+ async coordinateWait(
1212
+ actor: ChildRef | undefined,
1213
+ target: ChildRef | undefined,
1214
+ timeoutMs: number,
1215
+ signal?: AbortSignal,
1216
+ messageId?: string,
1217
+ ): Promise<CoordinationWait> {
1218
+ const sender = this.actor(actor, "wait");
1219
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_WAIT_MS)
1220
+ throw new Error("Wait timeout must be 1–60000ms");
1221
+ if (this.waiterCount >= MAX_WAITERS) throw new Error("Parent waiter limit");
1222
+ if (target && messageId) throw new Error("Wait selects either an outcome or message receipt");
1223
+ const receipt = messageId
1224
+ ? this.coordinationMessages.find((message) => message.id === messageId)
1225
+ : undefined;
1226
+ if (
1227
+ messageId &&
1228
+ (!receipt ||
1229
+ (this.key(receipt.sender === "root" ? undefined : receipt.sender) !== this.key(actor) &&
1230
+ this.key(receipt.recipient === "root" ? undefined : receipt.recipient) !==
1231
+ this.key(actor)))
1232
+ )
1233
+ throw new Error("Unknown or foreign message receipt");
1234
+ const destination = target ? this.target(sender, target) : undefined;
1235
+ const pendingReceipt =
1236
+ receipt && !["delivered", "handled", "error", "not-running"].includes(receipt.status);
1237
+ if (sender && pendingReceipt && receipt.recipient === "root")
1238
+ throw new Error("Waiting for a pending root receipt with a retained permit is unsupported");
1239
+ const recipientRef = receipt?.recipient === "root" ? undefined : receipt?.recipient;
1240
+ const receiptEntry = recipientRef ? this.entries.get(recipientRef.id) : undefined;
1241
+ const dependency =
1242
+ destination ??
1243
+ (pendingReceipt && receiptEntry?.state.generation === recipientRef?.generation
1244
+ ? receiptEntry
1245
+ : undefined);
1246
+ const sourceKey = this.key(actor),
1247
+ targetKey = dependency ? this.key(dependency.state) : this.key(target);
1248
+ if (sender && dependency) {
1249
+ if (sender === dependency) throw new Error("Self wait denied");
1250
+ let ancestor: Entry | undefined = sender;
1251
+ while (ancestor) {
1252
+ if (ancestor === dependency) throw new Error("Ancestor wait would deadlock");
1253
+ ancestor = this.entries.get(ancestor.state.parentId);
1254
+ }
1255
+ if (dependency.state.status === "queued")
1256
+ throw new Error("Wait on queued work with retained permit is unsupported");
1257
+ const reaches = (key: string, seen = new Set<string>()): boolean => {
1258
+ if (key === sourceKey) return true;
1259
+ if (seen.has(key)) return false;
1260
+ seen.add(key);
1261
+ const children = [...this.entries.values()]
1262
+ .filter(
1263
+ (entry) =>
1264
+ !entry.state.disposed &&
1265
+ entry.state.parentGeneration !== undefined &&
1266
+ `${entry.state.parentId}/${entry.state.parentGeneration}` === key,
1267
+ )
1268
+ .map((entry) => this.key(entry.state));
1269
+ return [...(this.waits.get(key) ?? []), ...children].some((next) => reaches(next, seen));
1270
+ };
1271
+ if (reaches(targetKey)) throw new Error("Coordination wait cycle");
1272
+ }
1273
+ const edges = this.waits.get(sourceKey) ?? new Set<string>();
1274
+ if (dependency && edges.has(targetKey)) throw new Error("Duplicate active wait");
1275
+ if (dependency) {
1276
+ edges.add(targetKey);
1277
+ this.waits.set(sourceKey, edges);
1278
+ }
1279
+ this.waiterCount++;
1280
+ return new Promise<CoordinationWait>((resolve) => {
1281
+ let finished = false;
1282
+ const finish = (value: CoordinationWait) => {
1283
+ if (finished) return;
1284
+ finished = true;
1285
+ clearTimeout(timer);
1286
+ this.coordinationListeners.delete(check);
1287
+ signal?.removeEventListener("abort", abort);
1288
+ if (dependency) {
1289
+ edges.delete(targetKey);
1290
+ if (!edges.size) this.waits.delete(sourceKey);
1291
+ }
1292
+ this.waiterCount--;
1293
+ resolve(value);
1294
+ };
1295
+ const check = () => {
1296
+ if (this.closing) return finish({ status: "shutdown" });
1297
+ if (
1298
+ sender &&
1299
+ (sender.cancelled ||
1300
+ sender.state.disposed ||
1301
+ this.entries.get(sender.state.id) !== sender)
1302
+ )
1303
+ return finish({ status: "cancelled" });
1304
+ if (receipt) {
1305
+ if (["delivered", "handled", "error", "not-running"].includes(receipt.status))
1306
+ return finish({ status: "message", message: structuredClone(receipt) });
1307
+ if (receipt.recipient !== "root") {
1308
+ const recipient = this.entries.get(receipt.recipient.id);
1309
+ if (
1310
+ !recipient ||
1311
+ recipient.state.generation !== receipt.recipient.generation ||
1312
+ recipient.state.disposed ||
1313
+ recipient.cancelled
1314
+ )
1315
+ return finish({
1316
+ status: "message",
1317
+ message: { ...structuredClone(receipt), status: "not-running" },
1318
+ });
1319
+ }
1320
+ } else if (destination) {
1321
+ if (destination.state.disposed)
1322
+ return finish({
1323
+ status: "outcome",
1324
+ outcome: {
1325
+ id: destination.state.id,
1326
+ generation: destination.state.generation,
1327
+ status: destination.state.status,
1328
+ limitReason: destination.state.limitReason,
1329
+ },
1330
+ });
1331
+ } else if ((this.mailboxes.get(sourceKey)?.length ?? 0) > 0)
1332
+ return finish({ status: "messages", messages: this.takeInbox(actor) });
1333
+ };
1334
+ const abort = () => finish({ status: "cancelled" });
1335
+ const timer = setTimeout(() => finish({ status: "timeout" }), timeoutMs);
1336
+ this.coordinationListeners.add(check);
1337
+ signal?.addEventListener("abort", abort, { once: true });
1338
+ if (signal?.aborted) abort();
1339
+ else check();
1340
+ });
1341
+ }
1342
+ private takeInbox(actor?: ChildRef): DelegationMessage[] {
1343
+ const key = this.key(actor),
1344
+ messages = this.mailboxes.get(key) ?? [];
1345
+ this.mailboxes.delete(key);
1346
+ for (const message of messages) if (message.mode === "inbox") message.status = "delivered";
1347
+ this.notifyCoordination();
1348
+ return structuredClone(messages);
1349
+ }
1350
+ taskInputRead(ref: ChildRef): void {
1351
+ const entry = this.entry(ref);
1352
+ if (
1353
+ entry.cancelled ||
1354
+ entry.state.disposed ||
1355
+ this.closing ||
1356
+ !entry.input.profile.tools.includes("read")
1357
+ )
1358
+ throw new Error("Delegating parent has no active read grant");
1359
+ if (entry.state.toolCalls >= entry.state.limits.tools) {
1360
+ this.limit(entry, "tools");
1361
+ throw new Error("Tool call limit");
1362
+ }
1363
+ this.update(entry, { toolCalls: entry.state.toolCalls + 1 });
1364
+ }
1365
+ async steer(ref: ChildRef, text: string): Promise<"queued" | "handled"> {
1366
+ const entry = this.entry(ref);
1367
+ if (!text.trim()) throw new Error("Nonempty steering text is required");
1368
+ if (entry.state.status !== "running" || entry.cancelled) throw new Error("No running child");
1369
+ return entry.session!.steer(text);
1370
+ }
1371
+ private children(entry: Entry): Entry[] {
1372
+ return [...this.entries.values()].filter(
1373
+ (child) =>
1374
+ child.state.parentId === entry.state.id &&
1375
+ child.state.parentGeneration === entry.state.generation &&
1376
+ !child.state.disposed,
1377
+ );
1378
+ }
1379
+ async stopMany(refs: readonly ChildRef[]): Promise<void> {
1380
+ const entries = refs.map((ref) => this.entry(ref));
1381
+ // Mark the whole cancellation set before any disposal can dequeue a sibling.
1382
+ for (const entry of entries) if (!entry.state.disposed) entry.cancelled = true;
1383
+ await Promise.all(entries.map((entry) => this.stop(entry.state)));
1384
+ }
1385
+ async stop(ref: ChildRef): Promise<void> {
1386
+ const entry = this.entry(ref);
1387
+ this.presence.set(entry.state.id, "stopped");
1388
+ this.closeDescendantPresence(entry);
1389
+ this.notifyCoordination();
1390
+ if (entry.state.disposed) return;
1391
+ entry.cancelled = true;
1392
+ if (entry.state.status === "queued") {
1393
+ this.update(entry, { status: "cancelled", activity: "Cancelled before setup" });
1394
+ this.dispose(entry);
1395
+ return;
1396
+ }
1397
+ this.update(entry, { status: "draining", activity: "Cancelling and draining" });
1398
+ // Start subtree cancellation without waiting for tools queued behind this session.
1399
+ const descendants = this.stopMany(this.children(entry).map((child) => child.state));
1400
+ await entry.setup?.catch(() => {});
1401
+ await Promise.all([descendants, entry.state.disposed ? undefined : entry.session?.abort()]);
1402
+ if (entry.state.disposed) return;
1403
+ await entry.run;
1404
+ this.dispose(entry);
1405
+ }
1406
+ async wait(ref: ChildRef): Promise<ChildSnapshot> {
1407
+ const entry = this.entry(ref);
1408
+ await entry.done;
1409
+ return structuredClone(entry.state);
1410
+ }
1411
+ /** Detached public SDK transcript for one admitted generation; no replay or arbitrary file access. */
1412
+ messages(ref: ChildRef): AgentSession["messages"] {
1413
+ const historical = this.history.get(this.key(ref));
1414
+ if (historical) return structuredClone(historical.messages);
1415
+ const entry = this.entry(ref);
1416
+ return structuredClone(entry.session?.messages ?? entry.finalizedMessages ?? []);
1417
+ }
1418
+ reopenTranscript(ref: ChildRef) {
1419
+ const path = this.snapshot(ref)?.transcript;
1420
+ if (!path) throw new Error("No transcript yet");
1421
+ return SessionManager.open(path).buildSessionContext();
1422
+ }
1423
+ shutdown(): Promise<void> {
1424
+ this.closing = true;
1425
+ this.notifyCoordination();
1426
+ this.shutdownPromise ??= (async () => {
1427
+ await Promise.all(this.list().map((ref) => this.stop(ref)));
1428
+ this.listeners.clear();
1429
+ })();
1430
+ return this.shutdownPromise;
1431
+ }
1432
+ }