@hydraharness/harness-subagent 0.1.1-rc.10

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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +155 -0
  3. package/lib/index.js +2750 -0
  4. package/lib/invariant.js +76 -0
  5. package/lib/types/activation-setup-registry.d.ts +57 -0
  6. package/lib/types/activation-setup-registry.js +148 -0
  7. package/lib/types/assistant-output.d.ts +48 -0
  8. package/lib/types/assistant-output.js +72 -0
  9. package/lib/types/budget.d.ts +34 -0
  10. package/lib/types/budget.js +54 -0
  11. package/lib/types/child-agent.d.ts +139 -0
  12. package/lib/types/child-agent.js +178 -0
  13. package/lib/types/client.d.ts +7 -0
  14. package/lib/types/client.js +7 -0
  15. package/lib/types/continuation.d.ts +457 -0
  16. package/lib/types/continuation.js +1157 -0
  17. package/lib/types/depth.d.ts +31 -0
  18. package/lib/types/depth.js +39 -0
  19. package/lib/types/descriptor-seed.d.ts +21 -0
  20. package/lib/types/descriptor-seed.js +24 -0
  21. package/lib/types/descriptor.d.ts +139 -0
  22. package/lib/types/descriptor.js +189 -0
  23. package/lib/types/error.d.ts +11 -0
  24. package/lib/types/error.js +14 -0
  25. package/lib/types/index.d.ts +296 -0
  26. package/lib/types/index.js +374 -0
  27. package/lib/types/invariant.d.ts +13 -0
  28. package/lib/types/invariant.js +91 -0
  29. package/lib/types/lifecycle.d.ts +113 -0
  30. package/lib/types/lifecycle.js +177 -0
  31. package/lib/types/list-children.d.ts +112 -0
  32. package/lib/types/list-children.js +316 -0
  33. package/lib/types/out-of-process.d.ts +117 -0
  34. package/lib/types/out-of-process.js +211 -0
  35. package/lib/types/projection-types.d.ts +60 -0
  36. package/lib/types/projection-types.js +7 -0
  37. package/lib/types/projection.d.ts +95 -0
  38. package/lib/types/projection.js +150 -0
  39. package/lib/types/run-settlement.d.ts +17 -0
  40. package/lib/types/run-settlement.js +66 -0
  41. package/lib/types/types.d.ts +315 -0
  42. package/lib/types/types.js +19 -0
  43. package/package.json +112 -0
package/lib/index.js ADDED
@@ -0,0 +1,2750 @@
1
+ import { Service } from "@hydraharness/cordis";
2
+ import z from "@hydraharness/schemastery";
3
+ import { HarnessError, boundContextSummary, createUserMessage, errorChain } from "@hydraharness/harness-llm";
4
+ import { bindScopeParent, scopeOf, scopeParentOf, scopeTarget } from "@hydraharness/harness-scope";
5
+ import { assertObjectJsonSchema } from "@hydraharness/harness-tools";
6
+ import { randomUUID } from "node:crypto";
7
+ import { foldConsumedWork } from "@hydraharness/harness-agent";
8
+ import { Session, SessionId, snapshotJsonValue } from "@hydraharness/harness-session";
9
+ import { z as z$1 } from "zod";
10
+ import { accessSync, constants, statSync } from "node:fs";
11
+ import { isAbsolute, resolve } from "node:path";
12
+ //#region lib/types/error.js
13
+ /**
14
+ * Typed failures shared by subagent service and provider operations.
15
+ *
16
+ * @module @hydraharness/harness-subagent
17
+ */
18
+ /** Typed failure for the subagent seam. */
19
+ var SubagentError = class extends HarnessError {
20
+ constructor(message, code, options) {
21
+ super(message, code, options);
22
+ this.name = "SubagentError";
23
+ }
24
+ };
25
+ //#endregion
26
+ //#region lib/types/budget.js
27
+ /**
28
+ * Resolve the live root whose log owns a tree's cumulative budgets.
29
+ * @param ctx - context with the session store.
30
+ * @param agent - current caller.
31
+ * @returns the root session; refuses a detached child whose ancestry cannot be established.
32
+ */
33
+ function delegationRoot(ctx, agent) {
34
+ let session = agent.session;
35
+ const visited = /* @__PURE__ */ new Set();
36
+ while (session.header.origin === "subagent") {
37
+ if (visited.has(session.id)) throw new SubagentError("cyclic delegation ancestry", "BUDGET_ANCESTRY_UNAVAILABLE");
38
+ visited.add(session.id);
39
+ const parent = session.header.parentSession === void 0 ? void 0 : ctx.get("sessions")?.get(session.header.parentSession);
40
+ if (parent === void 0) throw new SubagentError("delegation budget requires the live parent session", "BUDGET_ANCESTRY_UNAVAILABLE");
41
+ session = parent;
42
+ }
43
+ return session;
44
+ }
45
+ /**
46
+ * Reserve capacity before asynchronous setup; a release belongs to the run's settlement.
47
+ * @param ctx - service context.
48
+ * @param limits - optional configured caps.
49
+ * @returns synchronous admission function with an idempotent release.
50
+ */
51
+ function delegationAdmission(ctx, limits) {
52
+ for (const value of Object.values(limits)) if (value !== void 0 && (!Number.isSafeInteger(value) || value < 1)) throw new Error("subagent limits must be positive safe integers");
53
+ const active = /* @__PURE__ */ new WeakMap();
54
+ return (parent, creating) => {
55
+ if (limits.maxActivePerTree === void 0 && limits.maxChildrenPerTree === void 0) return () => {};
56
+ const root = delegationRoot(ctx, parent);
57
+ const slots = active.get(root) ?? /* @__PURE__ */ new Set();
58
+ if (limits.maxActivePerTree !== void 0 && slots.size >= limits.maxActivePerTree) throw new SubagentError("delegation tree is at its active child limit; wait for a child to settle", "SUBAGENT_CAPACITY");
59
+ if (creating && limits.maxChildrenPerTree !== void 0) {
60
+ if (root.events.filter((event) => event.type === "subagent/admission").length >= limits.maxChildrenPerTree) throw new SubagentError("delegation tree exhausted its child creation budget", "SUBAGENT_BUDGET_EXHAUSTED");
61
+ root.append("subagent/admission", { parentId: parent.id });
62
+ }
63
+ const slot = {};
64
+ slots.add(slot);
65
+ active.set(root, slots);
66
+ return () => {
67
+ slots.delete(slot);
68
+ };
69
+ };
70
+ }
71
+ //#endregion
72
+ //#region lib/types/depth.js
73
+ /**
74
+ * Delegation-depth accounting: the recursion budget a parent passes to its
75
+ * children. Kept apart from the service so composition helpers can read it
76
+ * without importing the registry.
77
+ *
78
+ * @module @hydraharness/harness-subagent/depth
79
+ */
80
+ /**
81
+ * Read an agent's delegation depth, treating absence as top-level depth zero.
82
+ * The persisted session header is authoritative and monotone: runtime
83
+ * `AgentOptions.subagentDepth` may DEEPEN the count but can never lower it —
84
+ * a resumed child arrives with fresh options, and counting it from zero would
85
+ * let it delegate as if it were top-level.
86
+ * @param agent - the agent whose header and options carry the depth.
87
+ * @returns its non-negative safe-integer depth.
88
+ * @throws if the runtime `AgentOptions.subagentDepth` is not a non-negative safe integer.
89
+ */
90
+ function delegationDepthOf(agent) {
91
+ const runtime = agent.options.subagentDepth;
92
+ if (runtime !== void 0 && (!Number.isSafeInteger(runtime) || runtime < 0 || Object.is(runtime, -0))) throw new TypeError("agent subagentDepth must be a non-negative safe integer");
93
+ return Math.max(agent.session.header.delegationDepth ?? 0, runtime ?? 0);
94
+ }
95
+ /**
96
+ * Reject a recursion cap that cannot represent an exact delegation depth.
97
+ * @param maxDepth - the optional runtime value to validate.
98
+ */
99
+ function assertSubagentMaxDepth(maxDepth) {
100
+ if (maxDepth !== void 0 && (typeof maxDepth !== "number" || !Number.isSafeInteger(maxDepth) || maxDepth < 0 || Object.is(maxDepth, -0))) throw new TypeError("subagent maxDepth must be a non-negative safe integer");
101
+ }
102
+ //#endregion
103
+ //#region lib/types/assistant-output.js
104
+ /**
105
+ * Canonical selection of a child's final assistant output. Backend run results
106
+ * and `subagent/end.lastAssistantMessage` apply the same rule: select the last
107
+ * non-empty assistant message. An empty-content message records usage only
108
+ * when the loop appends it after a max-tokens step with no executable blocks,
109
+ * so it does not replace earlier output. If no non-empty message exists,
110
+ * select the accumulated assistant text. Selection is independent of the
111
+ * run's stop reason.
112
+ *
113
+ * @module @hydraharness/harness-subagent/assistant-output
114
+ */
115
+ /**
116
+ * Incremental fold of the selection rule, for backends that observe a child's
117
+ * output as it streams: session-event backends {@link push} each event, and
118
+ * transports without session events (ACP content chunks) {@link pushText} raw
119
+ * text into the same streamed fallback.
120
+ */
121
+ var AssistantOutputFold = class {
122
+ message;
123
+ partial = [];
124
+ /**
125
+ * Fold one session event: a non-empty assistant message becomes the
126
+ * candidate final answer, and a `text-delta` chunk extends the streamed
127
+ * fallback; every other event contributes nothing.
128
+ * @param event - the next observed session event.
129
+ */
130
+ push(event) {
131
+ if (event.type === "assistant/message") {
132
+ const content = event.data.message.content;
133
+ if (content.length > 0) this.message = content;
134
+ } else if (event.type === "assistant/chunk" && event.data.chunk.type === "text-delta") this.pushText(event.data.chunk.text);
135
+ }
136
+ /**
137
+ * Extend the streamed fallback with text observed outside session events.
138
+ * @param text - the next streamed text piece (an empty piece is a no-op).
139
+ */
140
+ pushText(text) {
141
+ if (text.length > 0) this.partial.push(text);
142
+ }
143
+ /**
144
+ * Select the final output folded so far.
145
+ * @returns the last non-empty assistant message, else the accumulated
146
+ * streamed text, or `undefined` when the child produced neither.
147
+ */
148
+ collect() {
149
+ if (this.message !== void 0) return this.message;
150
+ const text = this.partial.join("");
151
+ return text.length > 0 ? [{
152
+ type: "text",
153
+ text
154
+ }] : void 0;
155
+ }
156
+ };
157
+ /**
158
+ * Apply the selection rule to one complete child-owned event suffix.
159
+ * @param events - the child-owned events (after any seed or epoch boundary).
160
+ * @returns the selected output, or `undefined` when the child produced none.
161
+ */
162
+ function finalAssistantOutput(events) {
163
+ const fold = new AssistantOutputFold();
164
+ for (const event of events) fold.push(event);
165
+ return fold.collect();
166
+ }
167
+ //#endregion
168
+ //#region lib/types/types.js
169
+ /**
170
+ * The seam's consumer-facing contracts: request, result, and capability types
171
+ * for {@link SubagentProvider}, plus the `subagent/start` and `subagent/end`
172
+ * payloads that plugins and hosts observe. Internal control interfaces belong
173
+ * with their implementation — the lifecycle observer in `./lifecycle.ts`, the
174
+ * continuation host in `./continuation.ts` — so this module stays the published
175
+ * surface rather than a bag of everything type-shaped.
176
+ *
177
+ * @module @hydraharness/harness-subagent/types
178
+ */
179
+ /**
180
+ * Brand a string as a {@link SubagentRunId}.
181
+ * @param id - the raw run id.
182
+ * @returns the same string, branded.
183
+ */
184
+ function SubagentRunId(id) {
185
+ return id;
186
+ }
187
+ //#endregion
188
+ //#region lib/types/lifecycle.js
189
+ /**
190
+ * Lifecycle-edge publication for both subagent shapes: the contained emitter,
191
+ * the one-shot run observer, and the continuable Activation observer.
192
+ *
193
+ * The public payload contracts ({@link SubagentRunInfo},
194
+ * {@link SubagentRunEndInfo}) live in `./types.ts` with the rest of the seam's
195
+ * consumer-facing types; this module owns only the implementation and the
196
+ * package-private {@link ActivationObserver} the continuation manager consumes.
197
+ * Keeping the internal control interface out of the published surface is
198
+ * deliberate: the observer's `start`/`capture`/`settle` ordering is a contract
199
+ * between this module and one in-package caller, not something a plugin may
200
+ * depend on.
201
+ *
202
+ * @module @hydraharness/harness-subagent/lifecycle
203
+ */
204
+ /**
205
+ * Build the contained lifecycle emitter this seam publishes every edge through.
206
+ * Every listener is independently contained: a synchronous throw or a rejected
207
+ * returned promise is logged without starving peer listeners, changing the run,
208
+ * or — for provider removal, which fires from a disposer — breaking teardown.
209
+ * @param ctx - the service's own context, owning dispatch and the logger.
210
+ * @param carrier - resolve the scoped dispatch carrier for one delegating parent.
211
+ * @returns the emitter both observers and the provider registry publish through.
212
+ */
213
+ function createLifecycleEmitter(ctx, carrier) {
214
+ return (name, info, parent) => {
215
+ const dispatchArgs = parent === void 0 ? [name, info] : [
216
+ carrier(parent),
217
+ name,
218
+ info
219
+ ];
220
+ for (const callback of ctx.events.dispatch("emit", dispatchArgs)) try {
221
+ const returned = callback(info);
222
+ Promise.resolve(returned).catch((error) => {
223
+ ctx.logger.warn(`subagent: ${name} listener rejected: ${renderThrown(error)}`);
224
+ });
225
+ } catch (error) {
226
+ ctx.logger.warn(`subagent: ${name} listener threw: ${renderThrown(error)}`);
227
+ }
228
+ };
229
+ }
230
+ /**
231
+ * Emit the start/end lifecycle pair for one accepted one-shot run.
232
+ * @param emit - the contained lifecycle emitter.
233
+ * @param provider - the provider that established the run.
234
+ * @param parent - the delegating parent keying scoped dispatch.
235
+ * @param run - the published run whose settlement closes the pair.
236
+ * @returns the same run, unchanged.
237
+ */
238
+ function observeRun(emit, provider, parent, run) {
239
+ const identity = {
240
+ runId: SubagentRunId(randomUUID()),
241
+ provider,
242
+ id: run.id,
243
+ local: run.localAgent !== void 0
244
+ };
245
+ run.result.then((result) => {
246
+ emit("subagent/end", {
247
+ ...identity,
248
+ stopReason: result.stopReason,
249
+ ...result.output.length === 0 ? {} : { lastAssistantMessage: result.output }
250
+ }, parent);
251
+ }, () => {
252
+ emit("subagent/end", {
253
+ ...identity,
254
+ stopReason: "error"
255
+ }, parent);
256
+ });
257
+ emit("subagent/start", identity, parent);
258
+ return run;
259
+ }
260
+ /**
261
+ * Build the observer for one continuable Activation's residency epoch. Observers
262
+ * see the same vocabulary as a one-shot run, so a child's start and settlement
263
+ * remain observable without exposing whether the manager materialized, woke, or
264
+ * cold-resumed it. Creation failure before residency emits no lifecycle edge.
265
+ * @param emit - the contained lifecycle emitter.
266
+ * @param provider - the provider name recorded in the durable descriptor.
267
+ * @param childId - the durable child session id.
268
+ * @param parent - the exact live direct parent keying scoped dispatch.
269
+ * @returns the observer whose edges this epoch publishes.
270
+ */
271
+ function createActivationObserver(emit, provider, childId, parent) {
272
+ const identity = {
273
+ runId: SubagentRunId(randomUUID()),
274
+ provider,
275
+ id: childId,
276
+ local: true
277
+ };
278
+ let boundary = 0;
279
+ let captured = { stopReason: "completed" };
280
+ const terminal = (failure) => failure === void 0 ? captured : { stopReason: "error" };
281
+ return {
282
+ start: (child) => {
283
+ boundary = child.session.events.length;
284
+ emit("subagent/start", identity, parent);
285
+ },
286
+ capture: (child) => {
287
+ const own = child.session.events.slice(boundary);
288
+ const output = finalAssistantOutput(own);
289
+ captured = {
290
+ stopReason: epochStopReason(own),
291
+ ...output === void 0 ? {} : { output }
292
+ };
293
+ },
294
+ terminal,
295
+ settle: (failure) => {
296
+ const { stopReason, output } = terminal(failure);
297
+ emit("subagent/end", {
298
+ ...identity,
299
+ stopReason,
300
+ ...output === void 0 ? {} : { lastAssistantMessage: output }
301
+ }, parent);
302
+ }
303
+ };
304
+ }
305
+ /**
306
+ * Why this child's epoch ended, for the terminal lifecycle edge and the
307
+ * manager's own parent delivery. The child's own log is authoritative:
308
+ * teardown succeeding says nothing about whether the model errored, hit its
309
+ * token ceiling, or was cancelled, so deriving the reason from disposal would
310
+ * report failed work as completed.
311
+ *
312
+ * {@link foldConsumedWork} supplies both halves the raw turn sequence cannot:
313
+ * which turn accounts for the work this epoch consumed, and whether accepted
314
+ * work was cancelled after it without any turn opening over it. A recorded
315
+ * failure still wins over a cancellation — stopping a child that had already
316
+ * failed does not turn its failure into a cancellation.
317
+ * @param events - this epoch's own event suffix.
318
+ * @returns its terminal stop reason; `completed` only for an epoch that both
319
+ * closed cleanly and had nothing left to run.
320
+ */
321
+ function epochStopReason(events) {
322
+ const { end, droppedUnrun } = foldConsumedWork(events);
323
+ switch (end?.data.reason.kind) {
324
+ case "max-tokens": return "max-tokens";
325
+ case "aborted":
326
+ case "interrupted": return "aborted";
327
+ case "error": return "error";
328
+ case "blocked": return "refusal";
329
+ case void 0:
330
+ case "completed": return droppedUnrun ? "aborted" : "completed";
331
+ /* v8 ignore next 3 -- `TurnEndReason` is merge-extensible, so this arm needs a
332
+ * backend that adds a variant; treating an unnameable reason as success would
333
+ * report failed work as completed. */
334
+ default: return "error";
335
+ }
336
+ }
337
+ /** Render any listener-thrown value without letting coercion escape containment. */
338
+ function renderThrown(value) {
339
+ try {
340
+ return value instanceof Error ? `${value.name}: ${value.message}` : String(value);
341
+ } catch {
342
+ return "<unrenderable thrown value>";
343
+ }
344
+ }
345
+ //#endregion
346
+ //#region lib/types/descriptor.js
347
+ /**
348
+ * The durable subagent-child descriptor: the versioned, model-hidden
349
+ * `subagent/descriptor` session event that identifies every session-backed
350
+ * subagent and records whether it is one-shot or continuable. Continuable
351
+ * descriptors additionally preserve the declared composition required for
352
+ * cold resume. Providers append it turn-enclosed in the child's initial turn.
353
+ *
354
+ * The descriptor deliberately snapshots explicit fields rather than the
355
+ * merge-extensible `AgentOptions` object: an unrelated extension value cannot
356
+ * make continuation fail merely because it is not JSON, and later composition
357
+ * inputs require a deliberate {@link SUBAGENT_DESCRIPTOR_VERSION} change. It
358
+ * omits `subagentDepth` — cold resume trusts the persisted header's
359
+ * `delegationDepth` as the monotone floor — and `outputSchema`, which belongs
360
+ * to one activation's result contract rather than durable child composition.
361
+ * Per-activation knobs such as `maxTokens` are omitted for the same reason as
362
+ * `outputSchema`: they budget one activation. Cold resume requires the exact
363
+ * live parent for authorization but reconstructs child options only from the
364
+ * durable descriptor, so it neither restores the prior budget nor inherits
365
+ * the parent's current one; the resumed route's defaults apply instead.
366
+ *
367
+ * @module @hydraharness/harness-subagent/descriptor
368
+ */
369
+ /**
370
+ * The current descriptor format version, stamped into every appended
371
+ * `subagent/descriptor` event and required verbatim by {@link foldSubagentDescriptor}.
372
+ * Supporting another composition input is a deliberate version change, never
373
+ * an implicit extra field.
374
+ */
375
+ const SUBAGENT_DESCRIPTOR_VERSION = 2;
376
+ const DESCRIPTOR_BASE_KEYS = [
377
+ "version",
378
+ "mode",
379
+ "provider",
380
+ "label"
381
+ ];
382
+ const ONE_SHOT_DESCRIPTOR_KEYS = new Set(DESCRIPTOR_BASE_KEYS);
383
+ const CONTINUABLE_DESCRIPTOR_KEYS = new Set([
384
+ ...DESCRIPTOR_BASE_KEYS,
385
+ "agentProvider",
386
+ "agentModel",
387
+ "persona",
388
+ "toolFilter"
389
+ ]);
390
+ const TOOL_FILTER_KEYS = new Set(["allow", "deny"]);
391
+ /** Whether a persisted JSON value is an object record. */
392
+ function isRecord(value) {
393
+ return typeof value === "object" && value !== null && !Array.isArray(value);
394
+ }
395
+ /** Reject fields outside one versioned record's declared schema. */
396
+ function assertKnownKeys(value, keys, path) {
397
+ const unknown = Object.keys(value).find((key) => !keys.has(key));
398
+ if (unknown !== void 0) throw new Error(`persisted subagent descriptor ${path} has unknown field "${unknown}"`);
399
+ }
400
+ /** Read one optional string field from a persisted descriptor record. */
401
+ function optionalString(value, key) {
402
+ if (!Object.hasOwn(value, key)) return void 0;
403
+ const field = value[key];
404
+ if (typeof field !== "string") throw new Error(`persisted subagent descriptor ${key} must be a string`);
405
+ return field;
406
+ }
407
+ /** Read one optional string-array field from a persisted tool restriction. */
408
+ function optionalStringArray(value, key) {
409
+ if (!Object.hasOwn(value, key)) return void 0;
410
+ const field = value[key];
411
+ if (!Array.isArray(field)) throw new Error(`persisted subagent descriptor toolFilter.${key} must be an array of strings`);
412
+ const items = field;
413
+ if (items.some((item) => typeof item !== "string")) throw new Error(`persisted subagent descriptor toolFilter.${key} must be an array of strings`);
414
+ return items;
415
+ }
416
+ /** Validate and reconstruct a persisted tool restriction. */
417
+ function parseToolFilter(value) {
418
+ if (!isRecord(value)) throw new Error("persisted subagent descriptor toolFilter must be an object");
419
+ assertKnownKeys(value, TOOL_FILTER_KEYS, "toolFilter");
420
+ const allow = optionalStringArray(value, "allow");
421
+ const deny = optionalStringArray(value, "deny");
422
+ if (allow === void 0 && deny === void 0) throw new Error("persisted subagent descriptor toolFilter must declare allow and/or deny");
423
+ return {
424
+ ...allow !== void 0 ? { allow } : {},
425
+ ...deny !== void 0 ? { deny } : {}
426
+ };
427
+ }
428
+ /** Validate one persisted descriptor payload for the current runtime. */
429
+ function parseSubagentDescriptor(value) {
430
+ if (!isRecord(value)) throw new Error("persisted subagent descriptor payload must be an object");
431
+ const version = value["version"];
432
+ if (typeof version !== "number") throw new Error("persisted subagent descriptor version must be a number");
433
+ if (version !== 2) return void 0;
434
+ const mode = value["mode"];
435
+ if (mode !== "one-shot" && mode !== "continuable") throw new Error("persisted subagent descriptor mode must be \"one-shot\" or \"continuable\"");
436
+ assertKnownKeys(value, mode === "one-shot" ? ONE_SHOT_DESCRIPTOR_KEYS : CONTINUABLE_DESCRIPTOR_KEYS, "payload");
437
+ const provider = value["provider"];
438
+ if (typeof provider !== "string") throw new Error("persisted subagent descriptor provider must be a string");
439
+ if (mode === "one-shot") {
440
+ const label = optionalString(value, "label");
441
+ return {
442
+ version: 2,
443
+ mode,
444
+ provider,
445
+ ...label !== void 0 ? { label } : {}
446
+ };
447
+ }
448
+ const label = value["label"];
449
+ if (typeof label !== "string") throw new Error("persisted subagent descriptor label must be a string");
450
+ const agentProvider = optionalString(value, "agentProvider");
451
+ const agentModel = optionalString(value, "agentModel");
452
+ const persona = optionalString(value, "persona");
453
+ const toolFilter = Object.hasOwn(value, "toolFilter") ? parseToolFilter(value["toolFilter"]) : void 0;
454
+ return {
455
+ version: 2,
456
+ mode,
457
+ provider,
458
+ label,
459
+ ...agentProvider !== void 0 ? { agentProvider } : {},
460
+ ...agentModel !== void 0 ? { agentModel } : {},
461
+ ...persona !== void 0 ? { persona } : {},
462
+ ...toolFilter !== void 0 ? { toolFilter } : {}
463
+ };
464
+ }
465
+ function snapshotSubagentDescriptor(input) {
466
+ const snapshot = snapshotJsonValue(input.mode === "one-shot" ? {
467
+ version: 2,
468
+ mode: input.mode,
469
+ provider: input.provider,
470
+ ...input.label !== void 0 ? { label: input.label } : {}
471
+ } : {
472
+ version: 2,
473
+ mode: input.mode,
474
+ provider: input.provider,
475
+ label: input.label,
476
+ ...input.agentProvider !== void 0 ? { agentProvider: input.agentProvider } : {},
477
+ ...input.agentModel !== void 0 ? { agentModel: input.agentModel } : {},
478
+ ...input.persona !== void 0 ? { persona: input.persona } : {},
479
+ ...input.toolFilter !== void 0 ? { toolFilter: input.toolFilter } : {}
480
+ });
481
+ if (snapshot === void 0) throw new Error("subagent descriptor is not losslessly JSON-serializable");
482
+ return snapshot;
483
+ }
484
+ /**
485
+ * Fold a persisted child log to its supported descriptor. The first
486
+ * `subagent/descriptor` event is authoritative — the establishing provider
487
+ * appends exactly one, so a later same-type event cannot rewrite the declared
488
+ * composition.
489
+ * @param events - the loaded child session events.
490
+ * @returns the descriptor, or `undefined` when the log has none or its
491
+ * version is not {@link SUBAGENT_DESCRIPTOR_VERSION} (the child cannot be
492
+ * classified by this runtime).
493
+ * @throws when a current-version persisted payload does not match its complete
494
+ * declared schema.
495
+ */
496
+ function foldSubagentDescriptor(events) {
497
+ const event = events.find((candidate) => candidate.type === "subagent/descriptor");
498
+ if (event === void 0) return void 0;
499
+ return parseSubagentDescriptor(event.data);
500
+ }
501
+ //#endregion
502
+ //#region lib/types/child-agent.js
503
+ /**
504
+ * Shared in-process child composition: the delegation-depth budget, the
505
+ * durable session metadata, the resolved child `AgentOptions`, the delegated
506
+ * policy seed, and the scoped setup a child agent needs. Both the one-shot
507
+ * provider driver and the continuation manager compose children this way, so
508
+ * depth accounting, lineage stamping, and delegation policy have one home.
509
+ *
510
+ * @module @hydraharness/harness-subagent/child-agent
511
+ */
512
+ /** Thrown when starting a child would exceed the requested depth cap. */
513
+ var SubagentDepthError = class extends Error {
514
+ attemptedDepth;
515
+ maxDepth;
516
+ constructor(attemptedDepth, maxDepth) {
517
+ super(`subagent depth ${attemptedDepth} exceeds maxDepth ${maxDepth}`);
518
+ this.attemptedDepth = attemptedDepth;
519
+ this.maxDepth = maxDepth;
520
+ this.name = "SubagentDepthError";
521
+ }
522
+ };
523
+ /**
524
+ * Resolve the child's delegation depth from its parent and enforce an optional
525
+ * cap. The persisted parent header is the monotone floor, so a resumed parent
526
+ * cannot delegate as if it were top-level.
527
+ * @param parent - the delegating parent agent.
528
+ * @param maxDepth - optional absolute cap the resolved depth must not exceed.
529
+ * @returns the child's non-negative safe-integer depth.
530
+ * @throws {SubagentDepthError} when the resolved depth exceeds `maxDepth`.
531
+ * @throws {RangeError} when the resolved depth leaves the safe-integer range.
532
+ */
533
+ function resolveChildDepth(parent, maxDepth) {
534
+ const childDepth = delegationDepthOf(parent) + 1;
535
+ if (!Number.isSafeInteger(childDepth)) throw new RangeError("subagent child depth exceeds the safe-integer range");
536
+ if (maxDepth !== void 0 && childDepth > maxDepth) throw new SubagentDepthError(childDepth, maxDepth);
537
+ return childDepth;
538
+ }
539
+ /**
540
+ * Resolve the child's `AgentOptions`: the parent's provider/model/maxTokens
541
+ * route unless the request overrides it, stamped with the child's own
542
+ * delegation depth.
543
+ * @param parent - the delegating parent whose route the child inherits.
544
+ * @param requested - per-child overrides, if any.
545
+ * @param childDepth - the resolved delegation depth to stamp.
546
+ * @returns the resolved options for `ctx.agents.create()`.
547
+ */
548
+ function resolveChildAgentOptions(parent, requested, childDepth) {
549
+ const parentProvider = parent.options.provider;
550
+ const parentModel = parent.options.model;
551
+ const parentMaxTokens = parent.options.maxTokens;
552
+ return {
553
+ ...parentProvider !== void 0 ? { provider: parentProvider } : {},
554
+ ...parentModel !== void 0 ? { model: parentModel } : {},
555
+ ...parentMaxTokens !== void 0 ? { maxTokens: parentMaxTokens } : {},
556
+ ...requested,
557
+ subagentDepth: childDepth
558
+ };
559
+ }
560
+ /**
561
+ * Build the child session's durable creation metadata: the parent's workspace,
562
+ * its direct lineage, coarse product origin, the recursion budget that must
563
+ * survive persistence, the seed boundary that separates inherited parent
564
+ * history from child work, and the composition the child runs under.
565
+ *
566
+ * The preset is read from the parent's LIVE scope chain rather than from its
567
+ * header, because a parent that switched preset while blank runs on the newer
568
+ * composition and its header still names the older one. Recording it is what
569
+ * makes a child's history reconstructable: without it a cold read of the child
570
+ * resolves the deployment default and rebuilds turns under a tool set the
571
+ * child never had.
572
+ * @param parent - the delegating parent agent.
573
+ * @param childDepth - the resolved delegation depth to persist.
574
+ * @param lineageSeedLength - how many leading events came from the parent's log.
575
+ * @returns the `meta` for `ctx.agents.create()`.
576
+ */
577
+ function childSessionMeta(parent, childDepth, lineageSeedLength) {
578
+ const parentHeader = parent.session.header;
579
+ const agentPreset = parent.ctx.get("agentPresets")?.composedPreset(parent.ctx);
580
+ return {
581
+ ...parentHeader.cwd !== void 0 ? { cwd: parentHeader.cwd } : {},
582
+ ...agentPreset === void 0 ? {} : { agentPreset },
583
+ parentSession: parentHeader.id,
584
+ origin: "subagent",
585
+ delegationDepth: childDepth,
586
+ ...lineageSeedLength > 0 ? { seedLength: lineageSeedLength } : {}
587
+ };
588
+ }
589
+ /**
590
+ * Model-facing delegation-scope statement for every in-process child. A
591
+ * runtime-context contribution rather than a system-prompt section, so the
592
+ * deployment's system prompt stays uniform across parents and children.
593
+ */
594
+ const SUBAGENT_DELEGATION_CONTEXT = "You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it.";
595
+ /**
596
+ * Compose one child inside its creation window: join its parent's preset,
597
+ * register the fixed delegation-scope statement, then apply the child's own
598
+ * shadowing persona section and tool restriction, all owned by the child's
599
+ * scope and therefore invisible to its parent and siblings. Creation and cold
600
+ * resume both pass through here.
601
+ *
602
+ * The join comes first and the child's own registrations second, which is the
603
+ * order the layering already implies — the nearest scope wins a name, and a
604
+ * per-child restriction intersects with everything its chain admits — but
605
+ * stating it here keeps the two steps from being read as independent.
606
+ *
607
+ * The join and the per-child registrations live in ONE call because a child
608
+ * composed without the join is exactly the defect this function exists to
609
+ * prevent: with every model-facing row on the agent plane, a child that joins
610
+ * no preset sees an empty tool registry and none of its parent's prompt
611
+ * sections. Taking the parent as a parameter is what makes that omission
612
+ * unrepresentable at the call sites.
613
+ * @param childCtx - the child agent's scoped creation context.
614
+ * @param parent - the delegating parent whose composition the child joins.
615
+ * @param composition - the per-child persona and tool filter to install.
616
+ */
617
+ function applyChildComposition(childCtx, parent, composition) {
618
+ const preset = childCtx.get("agentPresets")?.composeFrom(childCtx, parent.ctx);
619
+ const parentKey = scopeOf(parent.ctx);
620
+ const parentScope = parentKey === void 0 ? void 0 : scopeParentOf(parentKey);
621
+ if (preset === void 0 && parentScope !== void 0) {
622
+ const childScope = scopeOf(childCtx);
623
+ if (childScope === void 0) throw new Error("subagent: child composition requires an agent scope");
624
+ bindScopeParent(childScope, parentScope);
625
+ }
626
+ childCtx.systemPrompt.context({
627
+ name: "subagent:delegation",
628
+ order: 120,
629
+ text: SUBAGENT_DELEGATION_CONTEXT
630
+ });
631
+ if (composition.persona !== void 0) childCtx.systemPrompt.section({
632
+ name: "deployment:persona",
633
+ order: 0,
634
+ text: composition.persona
635
+ });
636
+ if (composition.toolFilter !== void 0) childCtx.tools.restrict(composition.toolFilter);
637
+ }
638
+ /**
639
+ * Capture the policy to seed into one delegation. Call synchronously before
640
+ * the child start's first await: a later parent switch belongs to the
641
+ * parent's future, not to this child. Only the parent session's explicit
642
+ * sandbox override is captured — never deployment defaults or one-shot
643
+ * grants — and the approval policy is pinned to `'never'` regardless of the
644
+ * parent's own policy.
645
+ * @param parent - the delegating parent agent.
646
+ * @returns the sandbox override (or `undefined` without one) and the approval pin.
647
+ */
648
+ function captureDelegatedPolicyOverrides(parent) {
649
+ return {
650
+ sandboxMode: parent.ctx.get("sandboxPolicy")?.overrideOf(parent.session),
651
+ approvalPolicy: parent.ctx.get("approval") === void 0 ? void 0 : "never"
652
+ };
653
+ }
654
+ /**
655
+ * Append the captured delegation policy onto the child's own log as
656
+ * `source: 'delegation'` events inside the unpublished creation window, so the
657
+ * child's effective policy is reconstructable from its log alone. Appends land
658
+ * after any fork seed, so fresh policy wins stale seed state; later child
659
+ * switches still win over these events.
660
+ * @param childSession - the unpublished child's session.
661
+ * @param overrides - the policy captured at delegation.
662
+ */
663
+ function appendDelegatedPolicyOverrides(childSession, overrides) {
664
+ if (overrides.sandboxMode !== void 0) childSession.append("sandbox/mode", {
665
+ mode: overrides.sandboxMode,
666
+ source: "delegation"
667
+ });
668
+ if (overrides.approvalPolicy !== void 0) childSession.append("approval/policy", {
669
+ policy: overrides.approvalPolicy,
670
+ source: "delegation"
671
+ });
672
+ }
673
+ //#endregion
674
+ //#region lib/types/descriptor-seed.js
675
+ /**
676
+ * Seeding of a continuable child's durable descriptor event: the model-hidden
677
+ * record of the child's declared composition before its first request, so a
678
+ * later cold resume can reconstruct it from its own log.
679
+ *
680
+ * @module @hydraharness/harness-subagent/descriptor-seed
681
+ */
682
+ /**
683
+ * Build the child's creation seed: any inherited parent-history prefix followed
684
+ * by one model-hidden, between-turn `descriptor` event. Staging through a
685
+ * `Session` assigns the sequence number and enforces the same lossless-JSON
686
+ * rules the durable log does.
687
+ * @param childId - the reserved child session id the staged log belongs to.
688
+ * @param seed - the inherited completed-turn prefix, or `undefined` for a fresh child.
689
+ * @param descriptor - the snapshotted composition record to persist.
690
+ * @returns the complete seed events, contiguous from sequence zero.
691
+ */
692
+ function seedDescriptorTurn(childId, seed, descriptor) {
693
+ const staged = Session.create(childId, seed);
694
+ staged.append("subagent/descriptor", descriptor);
695
+ return [...staged.events];
696
+ }
697
+ //#endregion
698
+ //#region lib/types/continuation.js
699
+ /**
700
+ * Internal continuable-subagent manager: stable child ids, descriptor
701
+ * persistence, activation admission, the live ownership graph, cold resume,
702
+ * child-first disposal, and settlement delivery to the parent, behind
703
+ * `ctx.subagents`.
704
+ *
705
+ * A continuable child has one durable Session and at most one process-local
706
+ * {@link Activation} — one residency epoch for a reconstructed child Agent. An
707
+ * Activation is not a request, result, cancellation, or Task boundary: it may
708
+ * execute many FIFO turns and stays resident while descendants it created are
709
+ * still running. The Agent inbox is the only turn queue, so this manager owns
710
+ * residency while the Agent loop owns all turn ordering and execution. No
711
+ * continuable path creates a Task or an intermediate result-bearing wrapper.
712
+ *
713
+ * Because residency is this manager's alone to end, telling the parent that a
714
+ * child settled is its job too. An external `subagent/end` listener cannot do
715
+ * it correctly: that payload names no parent, the child handle is already
716
+ * disposed by then, and the release that wakes the parent's own settlement
717
+ * watcher has already run. See {@link SubagentContinuationManager.notifySettlement}.
718
+ *
719
+ * @module @hydraharness/harness-subagent
720
+ */
721
+ /**
722
+ * Read one Activation's current disposal transaction. This indirection exists
723
+ * because TypeScript would otherwise narrow repeated reads of the mutable field
724
+ * inside a long-lived closure to constants instead of re-reading runtime state.
725
+ * @param activation - the Activation to inspect.
726
+ * @returns the in-flight or settled disposal, or `undefined` while resident.
727
+ */
728
+ function disposalOf(activation) {
729
+ return activation.disposal;
730
+ }
731
+ /**
732
+ * One line telling a parent that a background child is finished and why, in
733
+ * the parent's own task vocabulary.
734
+ * @param childId - the durable child the parent knows by id.
735
+ * @param stopReason - how the child's last ordinary turn ended.
736
+ * @returns the model-facing opening line of the settlement notice.
737
+ */
738
+ function settlementSummary(childId, stopReason) {
739
+ const subject = `Background subagent ${childId}`;
740
+ switch (stopReason) {
741
+ case "completed": return `${subject} finished and will do no further work unless you send it more.`;
742
+ case "aborted": return `${subject} was stopped before it finished.`;
743
+ case "max-tokens": return `${subject} ran out of room before it finished.`;
744
+ case "refusal": return `${subject} declined the task.`;
745
+ case "error": return `${subject} failed before it finished.`;
746
+ /* v8 ignore next 4 -- `SubagentResult['stopReason']` is merge-extensible, so this arm
747
+ * needs a backend that adds a variant; an unnameable ending is reported as unfinished
748
+ * rather than silently as success. */
749
+ default: return `${subject} ended abnormally (${String(stopReason)}) before it finished.`;
750
+ }
751
+ }
752
+ /** Serialize each durable child's delivery, release, and disposal. */
753
+ var ChildLock = class {
754
+ tails = /* @__PURE__ */ new Map();
755
+ /**
756
+ * Run `operation` after every previously queued operation for `childId`.
757
+ * @param childId - the durable child whose operations are linearized.
758
+ * @param operation - the critical section to run in order.
759
+ * @returns the operation's own settlement.
760
+ */
761
+ run(childId, operation) {
762
+ const result = (this.tails.get(childId) ?? Promise.resolve()).then(operation, operation);
763
+ const tail = result.then(() => void 0, () => void 0);
764
+ this.tails.set(childId, tail);
765
+ tail.then(() => {
766
+ if (this.tails.get(childId) === tail) this.tails.delete(childId);
767
+ });
768
+ return result;
769
+ }
770
+ };
771
+ /**
772
+ * The continuable-subagent orchestration service behind `ctx.subagents`. Tool
773
+ * schema and host adapters are consumers of this one contract; foreground
774
+ * one-shot delegation keeps calling `ctx.subagents.start()` and never enters
775
+ * this lifecycle.
776
+ */
777
+ var SubagentContinuationManager = class {
778
+ ctx;
779
+ host;
780
+ setupRegistry;
781
+ /** Child session id → its live Activation. Process-local, never durable. */
782
+ activations = /* @__PURE__ */ new Map();
783
+ /** Materializations admitted before drain, tracked through publication or rollback. */
784
+ materializations = /* @__PURE__ */ new Set();
785
+ locks = new ChildLock();
786
+ /** Structural Cordis owner of every Activation handle. */
787
+ ownerCtx;
788
+ /**
789
+ * Exact roots whose host teardown has begun, with the live lineage members
790
+ * observed under each root. Entries remain until that exact root leaves the
791
+ * Agent registry, closing admission throughout its host's teardown without
792
+ * poisoning a later same-id replacement.
793
+ */
794
+ closingScopes = /* @__PURE__ */ new Map();
795
+ draining = false;
796
+ constructor(ctx, host, setupRegistry) {
797
+ this.ctx = ctx;
798
+ this.host = host;
799
+ this.setupRegistry = setupRegistry;
800
+ const scope = ctx.plugin(function activationOwner() {});
801
+ this.ownerCtx = scope.ctx;
802
+ ctx.on("agent/disposed", ({ agent }) => {
803
+ this.closingScopes.delete(agent);
804
+ });
805
+ ctx.effect(function* () {
806
+ yield scope.dispose;
807
+ yield () => this.drain();
808
+ }.bind(this), "subagents.continuations()");
809
+ }
810
+ /**
811
+ * Start one continuable background child: reserve its durable identity,
812
+ * resolve the provider's detached creation spec, create the child Agent
813
+ * through the private activation-owner scope, establish any continuable-parent
814
+ * ownership, and submit the initial prompt. Resolves when inbox acceptance
815
+ * yields the message id — without waiting for the turn to start or for the
816
+ * message to reach the Session log.
817
+ *
818
+ * Every failure before that acceptance rejects without either id, disposing
819
+ * any created handle and rolling back the Activation and parent ownership.
820
+ * The caller signal owns lookup, materialization, and admission only until
821
+ * acceptance; afterwards the manager owns the Activation independently.
822
+ * @param spec - provider, delegation request, and caller cancellation.
823
+ * @returns the durable child id and the accepted initial prompt's message id.
824
+ */
825
+ async startContinuable(spec) {
826
+ const request = spec.request;
827
+ const parent = request.parent;
828
+ this.assertAdmitting(parent);
829
+ const persistence = this.requirePersistence();
830
+ assertSubagentMaxDepth(request.maxDepth);
831
+ const childId = spec.childId ?? SessionId(randomUUID());
832
+ this.assertChildIdAvailable(childId);
833
+ const childDepth = resolveChildDepth(parent, request.maxDepth);
834
+ const agentProvider = request.agentOptions?.provider ?? parent.options.provider;
835
+ const agentModel = request.agentOptions?.model ?? parent.options.model;
836
+ const descriptor = snapshotSubagentDescriptor({
837
+ mode: "continuable",
838
+ provider: spec.provider,
839
+ label: spec.label,
840
+ ...agentProvider !== void 0 ? { agentProvider } : {},
841
+ ...agentModel !== void 0 ? { agentModel } : {},
842
+ ...request.persona !== void 0 ? { persona: request.persona } : {},
843
+ ...request.toolFilter !== void 0 ? { toolFilter: request.toolFilter } : {}
844
+ });
845
+ const delegatedPolicies = captureDelegatedPolicyOverrides(parent);
846
+ const prepared = await this.host.prepareContinuable(spec.provider, {
847
+ sessionId: childId,
848
+ parent,
849
+ signal: spec.signal
850
+ });
851
+ spec.signal.throwIfAborted();
852
+ this.assertAdmitting(parent);
853
+ const lineageSeedLength = prepared.seed?.length ?? 0;
854
+ const seed = seedDescriptorTurn(childId, prepared.seed, descriptor);
855
+ return {
856
+ childId,
857
+ messageId: await this.locks.run(childId, async () => {
858
+ spec.signal.throwIfAborted();
859
+ this.assertAdmitting(parent);
860
+ this.assertChildIdAvailable(childId);
861
+ if (spec.childId !== void 0) {
862
+ const persisted = await persistence.listSnapshots(spec.signal);
863
+ spec.signal.throwIfAborted();
864
+ this.assertAdmitting(parent);
865
+ this.assertChildIdAvailable(childId);
866
+ if (persisted.some((snapshot) => snapshot.header.id === childId)) throw new SubagentError(`subagent "${childId}" already exists`, "DUPLICATE_CHILD");
867
+ }
868
+ const activation = await this.materialize({
869
+ childId,
870
+ provider: spec.provider,
871
+ parent,
872
+ create: {
873
+ seed,
874
+ meta: childSessionMeta(parent, childDepth, lineageSeedLength),
875
+ delegatedPolicies
876
+ },
877
+ agentOptions: resolveChildAgentOptions(parent, request.agentOptions, childDepth),
878
+ composition: {
879
+ persona: request.persona,
880
+ toolFilter: request.toolFilter
881
+ },
882
+ signal: spec.signal
883
+ });
884
+ return this.submitMaterialized(activation, request.prompt, { kind: "user" }, parent, spec.signal);
885
+ })
886
+ };
887
+ }
888
+ /** Reject one child identity already owned by a live Agent or Session. */
889
+ assertChildIdAvailable(childId) {
890
+ if (this.ctx.agents.get(childId) !== void 0 || this.ctx.get("sessions")?.get(childId) !== void 0) throw new SubagentError(`subagent "${childId}" already exists`, "DUPLICATE_CHILD");
891
+ }
892
+ /**
893
+ * Deliver one later message to a known continuable child as its next FIFO
894
+ * turn. Routing depends only on Activation residency: a `running` Activation
895
+ * enqueues, a `waiting` one wakes the same Agent, and an absent one
896
+ * cold-resumes a new Activation from the persisted Session. The Agent inbox
897
+ * is the only queue, so every accepted message has one observable order.
898
+ *
899
+ * The caller signal owns lookup, materialization, and admission only until
900
+ * inbox acceptance; afterwards the accepted turn cannot be cancelled through
901
+ * this service.
902
+ * @param parent - the exact live direct parent authorizing this delivery.
903
+ * @param childId - the durable child session id.
904
+ * @param content - the user-role content to deliver.
905
+ * @param options - the message source fields and caller cancellation.
906
+ * @returns the accepted message's inbox id.
907
+ * @throws when parent authority, availability, or admission rejects the delivery.
908
+ */
909
+ async followup(parent, childId, content, options) {
910
+ this.assertAdmitting(parent);
911
+ while (true) {
912
+ const live = await this.locks.run(childId, async () => {
913
+ const activation = this.activations.get(childId);
914
+ if (activation === void 0) return this.coldResume(parent, childId, content, options);
915
+ /* v8 ignore next 3 -- the send-versus-dispose cutoff: reaching this arm needs a
916
+ * delivery to observe the transaction inside the same critical section that opened it,
917
+ * which no test can schedule deterministically. The behavior is covered end-to-end by
918
+ * "cold-resumes a delivery that lost the race with final disposal". */
919
+ if (activation.disposal !== void 0) return activation.disposal.then(() => void 0, () => void 0);
920
+ return this.submitAdmitted(activation, content, options.source, parent, options.signal);
921
+ });
922
+ /* v8 ignore start -- only the lost-cutoff arm above returns undefined, so only that
923
+ * race reaches the retry below, which then cold-resumes a new Activation. */
924
+ if (live !== void 0) return live;
925
+ this.assertAdmitting(parent);
926
+ options.signal.throwIfAborted();
927
+ }
928
+ }
929
+ /**
930
+ * Interrupt one live continuable child's current turn. Admission is
931
+ * synchronous and the effect is asynchronous: this authorizes the caller,
932
+ * requests `Agent.cancel(cause, { keepInbox: true })` on the target, and
933
+ * returns without waiting for the target to observe the signal or reach
934
+ * quiescence. The Activation, its handle, accepted unclaimed inbox work, and
935
+ * already-published descendants are untouched; work already claimed into the
936
+ * interrupted turn is not requeued. Once the interrupted driver is idle, a
937
+ * waking send resumes the parked queue.
938
+ *
939
+ * An absent target is an accepted no-op, which uniformly covers natural
940
+ * completion races, repeated requests, one-shot ids, and unknown ids without
941
+ * consulting the durable catalog. A target whose disposal transaction is
942
+ * already open is likewise an accepted no-op after authorization.
943
+ * @param targetSessionId - the durable child session id to interrupt.
944
+ * @param authority - the human parent address or exact live ancestor Agent.
945
+ * @throws {SubagentError} `UNAUTHORIZED` when the presented authority does
946
+ * not own the live target: a stale or self-targeting ancestor caller, a
947
+ * parent address that is not the live target's durable direct parent, or
948
+ * an ancestor outside the target's recorded live lineage.
949
+ */
950
+ interrupt(targetSessionId, authority) {
951
+ if (authority.kind === "ancestor") {
952
+ const caller = authority.agent;
953
+ if (this.ctx.agents.get(caller.id) !== caller) throw new SubagentError(`interrupting "${targetSessionId}" requires the exact live ancestor agent`, "UNAUTHORIZED");
954
+ if (caller.id === targetSessionId) throw new SubagentError(`agent "${caller.id}" cannot interrupt itself`, "UNAUTHORIZED");
955
+ }
956
+ const activation = this.activations.get(targetSessionId);
957
+ if (activation === void 0) return;
958
+ if (authority.kind === "user") {
959
+ if (activation.handle.agent.session.header.parentSession !== authority.parentSessionId) throw new SubagentError(`subagent "${targetSessionId}" belongs to another parent session`, "UNAUTHORIZED");
960
+ } else if (!activation.ancestry.has(authority.agent)) throw new SubagentError(`subagent "${targetSessionId}" is not a live descendant of agent "${authority.agent.id}"`, "UNAUTHORIZED");
961
+ if (activation.disposal !== void 0) return;
962
+ activation.handle.agent.cancel(authority.kind === "user" ? { kind: "user" } : { kind: "parent" }, { keepInbox: true });
963
+ }
964
+ /**
965
+ * Deliver explicitly selected content from one resident continuable child to
966
+ * its durable direct parent. Sender authorization, parent resolution, and
967
+ * send acceptance share one no-await span. Reporting neither concludes the
968
+ * child's turn nor changes its Activation lifetime.
969
+ * @param child - exact live reporting child; this is the authority credential.
970
+ * @param content - selected model-facing content.
971
+ * @param options - scheduling policy and pre-acceptance cancellation.
972
+ * @returns the stable identity of the message accepted by the parent.
973
+ * @throws {SubagentError} when the sender is unauthorized, the parent is not
974
+ * live, or continuation admission is closing.
975
+ */
976
+ async reportFrom(child, content, options) {
977
+ options.signal.throwIfAborted();
978
+ this.assertAdmitting(child);
979
+ const activation = this.authorizeReporter(child);
980
+ const parent = this.resolveReportParent(child);
981
+ return this.deliverReport(activation, parent, content, options.delivery);
982
+ }
983
+ /** Authorize only the exact Agent of one resident Activation. */
984
+ authorizeReporter(child) {
985
+ const activation = this.activations.get(child.id);
986
+ if (activation === void 0 || activation.handle.agent !== child) throw new SubagentError(`agent "${child.id}" is not a live continuable subagent and cannot report`, "UNAUTHORIZED");
987
+ /* v8 ignore next 6 -- only a synchronous re-entrant disposer can open this
988
+ * transaction between exact-agent authorization and this no-await cutoff. */
989
+ if (activation.disposal !== void 0) throw new SubagentError(`subagent "${child.id}" activation is being disposed; the report was not delivered`, "ACTIVATION_CLOSING");
990
+ return activation;
991
+ }
992
+ /** Resolve the reporting child's live direct parent from durable lineage. */
993
+ resolveReportParent(child) {
994
+ const parentId = child.session.header.parentSession;
995
+ /* v8 ignore next -- every continuation-managed child has direct-parent metadata. */
996
+ const parent = parentId === void 0 ? void 0 : this.ctx.agents.get(parentId);
997
+ if (parent === void 0) throw new SubagentError("direct parent is not live; report was not delivered", "PARENT_UNAVAILABLE");
998
+ return parent;
999
+ }
1000
+ /** Deliver one framed report through the selected parent scheduling preset. */
1001
+ deliverReport(activation, parent, content, delivery) {
1002
+ const message = createUserMessage({
1003
+ content: [{
1004
+ type: "text",
1005
+ text: `Background subagent ${activation.childId} reported:`
1006
+ }, ...content],
1007
+ source: {
1008
+ kind: "subagent-report",
1009
+ form: "relay",
1010
+ senderSessionId: activation.childId
1011
+ }
1012
+ });
1013
+ if (delivery === "next-step") this.sendWaking(parent, message, () => {
1014
+ this.sendReport(parent, message, delivery);
1015
+ });
1016
+ else this.sendReport(parent, message, delivery);
1017
+ return message.id;
1018
+ }
1019
+ /**
1020
+ * Perform one waking send to a parent, accounted against that parent's own
1021
+ * Activation when it has one. Registering the id before the send is what
1022
+ * keeps a continuation-managed parent from being judged quiescent in the
1023
+ * window between a waking send and the microtask that admits it.
1024
+ * @param parent - the exact live parent receiving the waking message.
1025
+ * @param message - the message whose id is accounted.
1026
+ * @param send - the synchronous waking send to perform.
1027
+ */
1028
+ sendWaking(parent, message, send) {
1029
+ const parentActivation = this.activations.get(parent.id);
1030
+ if (parentActivation !== void 0 && parentActivation.handle.agent === parent) this.admitWaking(parentActivation, message.id, send);
1031
+ else send();
1032
+ }
1033
+ /** Send one report while translating only the parent's own rejection. */
1034
+ sendReport(parent, message, delivery) {
1035
+ try {
1036
+ if (delivery === "next-step") parent.steer(message);
1037
+ else parent.inject(message);
1038
+ } catch (error) {
1039
+ throw new SubagentError("direct parent is not live; report was not delivered", "PARENT_UNAVAILABLE", { cause: error });
1040
+ }
1041
+ }
1042
+ /**
1043
+ * Close admission, await every already-admitted materialization through
1044
+ * publication or rollback, then dispose the stable live Activation forest
1045
+ * child-first. Sibling branches drain independently: one failure is recorded
1046
+ * but never prevents the remaining handles from being attempted, and the
1047
+ * aggregate rejects only after every branch settles.
1048
+ * @returns once materialization is quiescent and every live Activation released its handle.
1049
+ * @throws an aggregate error when any branch failed to release.
1050
+ */
1051
+ async drain() {
1052
+ this.draining = true;
1053
+ await Promise.all([...this.materializations].map((materialization) => materialization.settled));
1054
+ const owned = /* @__PURE__ */ new Set();
1055
+ for (const activation of this.activations.values()) for (const child of activation.ownedChildren) owned.add(child);
1056
+ const roots = [...this.activations.values()].filter((activation) => !owned.has(activation.childId));
1057
+ await this.disposeRoots(roots, "activation(s)");
1058
+ }
1059
+ /**
1060
+ * Stop only the continuable descendants of exact live host-owned parents.
1061
+ * Admission stays closed for those parent trees until each exact parent
1062
+ * leaves the Agent registry; unrelated trees and manager-wide admission stay
1063
+ * live.
1064
+ * @param parents - exact live roots whose continuable descendants must stop.
1065
+ * @returns once every retained descendant Activation released its handle.
1066
+ * @throws an aggregate error after all scoped branches settle when any failed.
1067
+ */
1068
+ async drainDescendants(parents) {
1069
+ const roots = new Set(parents.filter((parent) => this.ctx.agents.get(parent.id) === parent));
1070
+ if (roots.size === 0) return;
1071
+ for (const root of roots) this.closingMembers(root).add(root);
1072
+ const targets = [];
1073
+ for (const activation of this.activations.values()) {
1074
+ const lineage = this.liveLineage(activation.handle.agent);
1075
+ const owners = [...roots].filter((root) => activation.handle.agent !== root && activation.ancestry.has(root));
1076
+ if (owners.length === 0) continue;
1077
+ targets.push(activation);
1078
+ for (const owner of owners) {
1079
+ const members = this.closingMembers(owner);
1080
+ members.add(activation.handle.agent);
1081
+ for (const agent of lineage) members.add(agent);
1082
+ }
1083
+ }
1084
+ const materializations = [...this.materializations].filter((materialization) => {
1085
+ const owners = [...roots].filter((root) => materialization.lineage.includes(root));
1086
+ for (const owner of owners) {
1087
+ const members = this.closingMembers(owner);
1088
+ for (const agent of materialization.lineage) members.add(agent);
1089
+ }
1090
+ return owners.length > 0;
1091
+ });
1092
+ const ownedTargets = /* @__PURE__ */ new Set();
1093
+ for (const activation of targets) for (const child of activation.ownedChildren) ownedTargets.add(child);
1094
+ const targetRoots = targets.filter((activation) => !ownedTargets.has(activation.childId));
1095
+ for (const activation of targets) this.dispose(activation).catch(() => void 0);
1096
+ await Promise.all(materializations.map((materialization) => materialization.settled));
1097
+ await this.disposeRoots(targetRoots, "scoped activation(s)");
1098
+ }
1099
+ /**
1100
+ * Release selected resident direct children of one exact live parent without
1101
+ * closing admission for the parent's other continuable children. Owned
1102
+ * descendants are released recursively through the same lifecycle.
1103
+ * @param parent - exact live direct parent authorizing the selected release.
1104
+ * @param childIds - durable direct-child ids to release when resident.
1105
+ * @returns once every selected Activation released its handle.
1106
+ * @throws {SubagentError} `UNAUTHORIZED` when a resident target is not the
1107
+ * parent's direct continuable child or the parent identity is stale.
1108
+ */
1109
+ async drainChildren(parent, childIds) {
1110
+ if (this.ctx.agents.get(parent.id) !== parent) throw new SubagentError("selected child teardown requires the exact live parent agent", "UNAUTHORIZED");
1111
+ const targets = [];
1112
+ for (const childId of new Set(childIds)) {
1113
+ const activation = this.activations.get(childId);
1114
+ if (activation === void 0) continue;
1115
+ if (activation.parentSession !== parent.id || !activation.ancestry.has(parent)) throw new SubagentError(`subagent "${childId}" is not a direct child of agent "${parent.id}"`, "UNAUTHORIZED");
1116
+ targets.push(activation);
1117
+ }
1118
+ for (const activation of targets) this.dispose(activation).catch(() => void 0);
1119
+ await this.disposeRoots(targets, "selected activation(s)");
1120
+ }
1121
+ /** Dispose independent roots and report every branch failure after all settle. */
1122
+ async disposeRoots(roots, failureSubject) {
1123
+ const reasons = (await Promise.all(roots.map(async (activation) => {
1124
+ try {
1125
+ await this.dispose(activation);
1126
+ return;
1127
+ } catch (error) {
1128
+ return error;
1129
+ }
1130
+ }))).filter((failure) => failure !== void 0);
1131
+ if (reasons.length > 0) throw new SubagentError(`continuable subagent teardown failed for ${reasons.length} ${failureSubject}: ` + reasons.map((reason) => errorChain(reason)).join("; "), "ACTIVATION_TEARDOWN_FAILED");
1132
+ }
1133
+ /** Return the retained member set for one exact scoped-teardown root. */
1134
+ closingMembers(root) {
1135
+ const existing = this.closingScopes.get(root);
1136
+ if (existing !== void 0) return existing;
1137
+ const members = /* @__PURE__ */ new Set();
1138
+ this.closingScopes.set(root, members);
1139
+ return members;
1140
+ }
1141
+ /**
1142
+ * Return the exact currently resolvable ancestry from `agent` upward. The
1143
+ * first element is always the supplied identity, even when it is already
1144
+ * stale; each ancestor after it must be the registry's current exact entry.
1145
+ */
1146
+ liveLineage(agent) {
1147
+ const lineage = [agent];
1148
+ const seen = new Set([agent.id]);
1149
+ let parentSession = agent.session.header.parentSession;
1150
+ while (parentSession !== void 0) {
1151
+ const parent = this.ctx.agents.get(parentSession);
1152
+ if (parent === void 0 || seen.has(parent.id)) break;
1153
+ lineage.push(parent);
1154
+ seen.add(parent.id);
1155
+ parentSession = parent.session.header.parentSession;
1156
+ }
1157
+ return lineage;
1158
+ }
1159
+ /**
1160
+ * The teardown that closed continuable admission for this agent's lineage.
1161
+ * `'manager'` is the whole manager draining; an Agent is the exact scoped root
1162
+ * whose forest is closing.
1163
+ * @param agent - the agent whose lineage is tested.
1164
+ * @returns the closing teardown, or `undefined` while admission is open.
1165
+ */
1166
+ closingTeardownFor(agent) {
1167
+ if (this.draining) return "manager";
1168
+ const lineage = this.liveLineage(agent);
1169
+ for (const [root, members] of this.closingScopes) if (members.has(agent) || lineage.includes(root)) return root;
1170
+ }
1171
+ /** Reject new admission once the manager or this exact parent tree began draining. */
1172
+ assertAdmitting(agent) {
1173
+ const closing = this.closingTeardownFor(agent);
1174
+ if (closing === void 0) return;
1175
+ throw new SubagentError(closing === "manager" ? "continuable subagents are draining; the operation was not admitted" : `continuable subagents below parent "${closing.id}" are draining; the operation was not admitted`, "DRAINING");
1176
+ }
1177
+ /**
1178
+ * Derive residency from Agent quiescence and the owned-child set. `running`
1179
+ * covers an active admission, an open turn, or accepted waking inbox work.
1180
+ *
1181
+ * `Agent.status` alone is insufficient: it stays `idle` between an accepted
1182
+ * waking send and the microtask that admits it, so a synchronous inbox
1183
+ * observer would see `settled` while a turn is already queued. `accepted`
1184
+ * holds the ids this manager admitted but has not yet seen drained.
1185
+ */
1186
+ stateOf(activation) {
1187
+ if (activation.handle.agent.status === "running" || activation.accepted.size > 0) return "running";
1188
+ if (activation.ownedChildren.size > 0) return "waiting";
1189
+ return "settled";
1190
+ }
1191
+ /**
1192
+ * Cold-resume a persisted child: inspect and authorize its Session, fold the
1193
+ * generic descriptor, create the Activation through `ctx.agents.resume()`,
1194
+ * and submit the waiting turn. This never dispatches through a subagent
1195
+ * provider — the persisted Session already holds the initial prefix and the
1196
+ * descriptor is the whole reconstruction input.
1197
+ */
1198
+ async coldResume(parent, childId, content, options) {
1199
+ const persistence = this.requirePersistence();
1200
+ let loaded;
1201
+ try {
1202
+ loaded = await persistence.inspect(childId, options.signal);
1203
+ } catch (error) {
1204
+ options.signal.throwIfAborted();
1205
+ throw new SubagentError(`subagent "${childId}" is unavailable`, "NOT_RESUMABLE", { cause: error });
1206
+ }
1207
+ options.signal.throwIfAborted();
1208
+ this.assertAdmitting(parent);
1209
+ this.authorizeLineage(parent, childId, loaded.meta.parentSession);
1210
+ const descriptor = foldSubagentDescriptor(loaded.events.slice(loaded.meta.seedLength ?? 0));
1211
+ if (descriptor === void 0 || descriptor.mode !== "continuable") throw new SubagentError(`subagent "${childId}" has no supported continuation state and cannot be resumed; do not retry send_message with this id`, "NOT_RESUMABLE");
1212
+ let activation;
1213
+ try {
1214
+ activation = await this.materialize({
1215
+ childId,
1216
+ provider: descriptor.provider,
1217
+ parent,
1218
+ agentOptions: {
1219
+ ...descriptor.agentProvider !== void 0 ? { provider: descriptor.agentProvider } : {},
1220
+ ...descriptor.agentModel !== void 0 ? { model: descriptor.agentModel } : {}
1221
+ },
1222
+ composition: {
1223
+ persona: descriptor.persona,
1224
+ toolFilter: descriptor.toolFilter
1225
+ },
1226
+ signal: options.signal
1227
+ });
1228
+ } catch (error) {
1229
+ options.signal.throwIfAborted();
1230
+ if (error instanceof SubagentError) throw error;
1231
+ throw new SubagentError(`subagent "${childId}" is unavailable`, "NOT_RESUMABLE", { cause: error });
1232
+ }
1233
+ return this.submitMaterialized(activation, content, options.source, parent, options.signal);
1234
+ }
1235
+ /**
1236
+ * Submit to a freshly materialized Activation or roll it back completely.
1237
+ * @param activation - the just-published Activation to admit or release.
1238
+ * @param content - the initial or resumed message content.
1239
+ * @param source - durable fields naming who supplied the accepted message.
1240
+ * @param parent - the live direct parent authorizing admission.
1241
+ * @param signal - caller cancellation owning admission until acceptance.
1242
+ * @returns the accepted inbox message id.
1243
+ */
1244
+ async submitMaterialized(activation, content, source, parent, signal) {
1245
+ try {
1246
+ return this.submitAdmitted(activation, content, source, parent, signal);
1247
+ } catch (error) {
1248
+ /* v8 ignore next -- rollback disposal failures must not mask the
1249
+ * pre-acceptance signal, drain, or lifecycle failure. */
1250
+ await this.dispose(activation).catch(() => void 0);
1251
+ throw error;
1252
+ }
1253
+ }
1254
+ /**
1255
+ * Create or resume the child Agent through the private activation-owner
1256
+ * scope, install the handle in a fresh Activation, and register ownership on
1257
+ * a continuation-managed parent. Rejection leaves no Activation, no handle,
1258
+ * and no ownership membership.
1259
+ */
1260
+ materialize(inputs) {
1261
+ this.assertAdmitting(inputs.parent);
1262
+ const settled = Promise.withResolvers();
1263
+ const lineage = this.liveLineage(inputs.parent);
1264
+ const releaseBudget = this.host.admit(inputs.parent, inputs.create !== void 0);
1265
+ const materialization = {
1266
+ lineage,
1267
+ settled: settled.promise
1268
+ };
1269
+ this.materializations.add(materialization);
1270
+ return this.materializeTracked(inputs, lineage, releaseBudget).catch((error) => {
1271
+ releaseBudget();
1272
+ throw error;
1273
+ }).finally(() => {
1274
+ this.materializations.delete(materialization);
1275
+ settled.resolve();
1276
+ });
1277
+ }
1278
+ /**
1279
+ * Perform one tracked materialization. The caller keeps the drain barrier
1280
+ * registered until this either returns a resident Activation or finishes
1281
+ * rollback.
1282
+ */
1283
+ async materializeTracked(inputs, parentLineage, releaseBudget) {
1284
+ const { childId, provider, parent, create } = inputs;
1285
+ inputs.signal.throwIfAborted();
1286
+ const setup = (childCtx) => {
1287
+ if (create !== void 0) appendDelegatedPolicyOverrides(childCtx.agent.session, create.delegatedPolicies);
1288
+ applyChildComposition(childCtx, parent, inputs.composition);
1289
+ return this.setupRegistry.apply(childCtx);
1290
+ };
1291
+ const observer = this.host.observeActivation(provider, childId, parent);
1292
+ const handle = create === void 0 ? await this.ownerCtx.agents.resume({
1293
+ resumeSessionId: childId,
1294
+ agentOptions: inputs.agentOptions,
1295
+ signal: inputs.signal,
1296
+ setup
1297
+ }) : await this.ownerCtx.agents.create({
1298
+ sessionId: childId,
1299
+ meta: create.meta,
1300
+ seed: create.seed,
1301
+ agentOptions: inputs.agentOptions,
1302
+ signal: inputs.signal,
1303
+ setup
1304
+ });
1305
+ const activation = {
1306
+ releaseBudget,
1307
+ childId,
1308
+ parentSession: parent.id,
1309
+ provider,
1310
+ handle,
1311
+ ancestry: new WeakSet([handle.agent, ...parentLineage]),
1312
+ ownedChildren: /* @__PURE__ */ new Set(),
1313
+ observer,
1314
+ disposal: void 0,
1315
+ accepted: /* @__PURE__ */ new Set(),
1316
+ announced: false,
1317
+ poke: Promise.withResolvers()
1318
+ };
1319
+ this.activations.set(childId, activation);
1320
+ try {
1321
+ inputs.signal.throwIfAborted();
1322
+ this.assertAdmitting(parent);
1323
+ this.acquireOwnership(parent, childId);
1324
+ handle.agent.ctx.on("agent/inbox/claimed", ({ message }) => {
1325
+ /* v8 ignore next -- a claim of an id this manager never admitted needs
1326
+ * another sender on the same child, which no current path allows. */
1327
+ if (activation.accepted.delete(message.id)) this.wake(activation);
1328
+ });
1329
+ handle.agent.ctx.on("agent/inbox/discarded", ({ message }) => {
1330
+ if (activation.accepted.delete(message.id)) this.wake(activation);
1331
+ });
1332
+ observer.start(handle.agent);
1333
+ } catch (error) {
1334
+ /* v8 ignore next -- rollback failure must not mask the admission failure
1335
+ * that prevented this operation from returning an accepted message id. */
1336
+ await this.rollbackUnpublished(activation).catch(() => void 0);
1337
+ throw error;
1338
+ }
1339
+ this.watchSettlement(activation);
1340
+ return activation;
1341
+ }
1342
+ /**
1343
+ * Release an Activation whose start edge was not published. The memoized
1344
+ * transaction remains in the live map until handle disposal settles, so a
1345
+ * concurrent drain or delivery observes the same closing boundary.
1346
+ */
1347
+ rollbackUnpublished(activation) {
1348
+ return activation.disposal ??= (async () => {
1349
+ try {
1350
+ await activation.handle.dispose();
1351
+ } finally {
1352
+ this.activations.delete(activation.childId);
1353
+ this.releaseOwnership(activation.childId);
1354
+ }
1355
+ })();
1356
+ }
1357
+ /**
1358
+ * Register the child in a continuation-managed parent's owned set before the
1359
+ * child can run, so that parent cannot settle while the child is live. A
1360
+ * top-level or other non-continuation Agent has no Activation and stays
1361
+ * outside the waiting graph.
1362
+ */
1363
+ acquireOwnership(parent, childId) {
1364
+ const parentActivation = this.activations.get(parent.id);
1365
+ if (parentActivation === void 0) return;
1366
+ if (parentActivation.disposal !== void 0) throw new SubagentError(`subagent parent "${parent.id}" is being disposed; the child was not established`, "ACTIVATION_CLOSING");
1367
+ parentActivation.ownedChildren.add(childId);
1368
+ }
1369
+ /** Remove one child from its live owner's set and let that owner re-check settlement. */
1370
+ releaseOwnership(childId) {
1371
+ for (const candidate of this.activations.values()) if (candidate.ownedChildren.delete(childId)) this.wake(candidate);
1372
+ }
1373
+ /** Let a settlement watcher re-observe quiescence after ownership or inbox changes. */
1374
+ wake(activation) {
1375
+ activation.poke.resolve();
1376
+ activation.poke = Promise.withResolvers();
1377
+ }
1378
+ /**
1379
+ * Submit one message as the child's next FIFO turn and return its accepted
1380
+ * inbox id. Acceptance is the operation's success boundary; the manager owns
1381
+ * the Activation independently afterwards.
1382
+ */
1383
+ submit(activation, content, source, parent) {
1384
+ this.acquireOwnership(parent, activation.childId);
1385
+ const message = createUserMessage({
1386
+ content,
1387
+ source
1388
+ });
1389
+ const accepted = this.admitWaking(activation, message.id, () => {
1390
+ activation.handle.agent.followup(message);
1391
+ });
1392
+ activation.announced = true;
1393
+ return accepted;
1394
+ }
1395
+ /**
1396
+ * Account one waking send across a resident Activation's settlement window.
1397
+ * @param activation - Activation receiving waking inbox work.
1398
+ * @param messageId - stable identity of the message about to be sent.
1399
+ * @param send - synchronous send that publishes one enqueue occurrence.
1400
+ * @returns the accepted message id.
1401
+ */
1402
+ admitWaking(activation, messageId, send) {
1403
+ activation.accepted.add(messageId);
1404
+ try {
1405
+ send();
1406
+ } catch (error) {
1407
+ activation.accepted.delete(messageId);
1408
+ throw error;
1409
+ }
1410
+ this.wake(activation);
1411
+ return messageId;
1412
+ }
1413
+ /**
1414
+ * Cross the final admission cutoff and submit without yielding. Signal abort,
1415
+ * manager drain, or Activation disposal that wins before this synchronous
1416
+ * span rejects without inbox acceptance.
1417
+ */
1418
+ submitAdmitted(activation, content, source, parent, signal) {
1419
+ signal.throwIfAborted();
1420
+ this.assertAdmitting(parent);
1421
+ /* v8 ignore next 6 -- only a synchronous re-entrant disposer can change
1422
+ * this field between the caller's live check and this no-await boundary. */
1423
+ if (disposalOf(activation) !== void 0) throw new SubagentError(`subagent "${activation.childId}" activation is being disposed; the message was not accepted`, "ACTIVATION_CLOSING");
1424
+ this.authorizeLineage(parent, activation.childId, activation.handle.agent.session.header.parentSession);
1425
+ return this.submit(activation, content, source, parent);
1426
+ }
1427
+ /**
1428
+ * Authorize one operation against the durable direct-parent lineage. Other
1429
+ * agents, ancestors, teams, workflows, and hosts remain rejected until an
1430
+ * explicit authority protocol has a production consumer.
1431
+ */
1432
+ authorizeLineage(parent, childId, parentSession) {
1433
+ if (this.ctx.agents.get(parent.id) !== parent) throw new SubagentError(`subagent "${childId}" delivery requires the exact live parent agent`, "UNAUTHORIZED");
1434
+ if (parentSession !== parent.id) throw new SubagentError(`subagent "${childId}" belongs to another parent session`, "UNAUTHORIZED");
1435
+ }
1436
+ /**
1437
+ * Follow one Activation to settlement: wait for Agent quiescence, then for
1438
+ * every owned child to complete disposal, and dispose the handle once both
1439
+ * hold. A `next-turn` delivered while `waiting` wakes the same Agent and
1440
+ * returns it to `running`, so this re-observes rather than settling early.
1441
+ */
1442
+ watchSettlement(activation) {
1443
+ (async () => {
1444
+ while (disposalOf(activation) === void 0) {
1445
+ const poked = activation.poke.promise;
1446
+ await Promise.race([activation.handle.agent.whenIdle(), poked]);
1447
+ if (disposalOf(activation) !== void 0) return;
1448
+ const settling = await this.locks.run(activation.childId, () => {
1449
+ if (disposalOf(activation) !== void 0 || this.stateOf(activation) !== "settled") return Promise.resolve({ settling: false });
1450
+ return Promise.resolve({
1451
+ settling: true,
1452
+ done: this.dispose(activation)
1453
+ });
1454
+ });
1455
+ if (!settling.settling) {
1456
+ if (activation.handle.agent.status !== "running") await poked;
1457
+ continue;
1458
+ }
1459
+ try {
1460
+ await settling.done;
1461
+ } catch (error) {
1462
+ this.ctx.logger.warn(`subagent "${activation.childId}" activation teardown failed: ${errorChain(error)}`);
1463
+ }
1464
+ return;
1465
+ }
1466
+ })();
1467
+ }
1468
+ /**
1469
+ * Stop one Activation immediately, then release it child-first. The memoized
1470
+ * transaction is installed before cancellation or recursive callbacks, so
1471
+ * admission and reentrant teardown converge on the same owner.
1472
+ *
1473
+ * The final session flush is best effort and never prevents handle disposal
1474
+ * or ownership release, because retaining a child would permanently pin its
1475
+ * ancestors in `waiting`.
1476
+ * @param activation - the residency epoch to stop and release.
1477
+ * @returns the one disposal transaction owned by this Activation.
1478
+ */
1479
+ dispose(activation) {
1480
+ const existing = activation.disposal;
1481
+ if (existing !== void 0) return existing;
1482
+ const completion = Promise.withResolvers();
1483
+ activation.disposal = completion.promise;
1484
+ this.finishDisposal(activation).then(completion.resolve, completion.reject);
1485
+ return completion.promise;
1486
+ }
1487
+ /**
1488
+ * Propagate stop synchronously, then finish the child-first release.
1489
+ * @param activation - the Activation whose disposal transaction is installed.
1490
+ * @returns once the handle and ownership edge are released.
1491
+ */
1492
+ async finishDisposal(activation) {
1493
+ this.wake(activation);
1494
+ const { childId } = activation;
1495
+ activation.handle.agent.cancel({ kind: "parent" });
1496
+ const idle = activation.handle.agent.whenIdle();
1497
+ const childDisposals = [...activation.ownedChildren].map((child) => this.activations.get(child)).filter((child) => child !== void 0).map((child) => this.dispose(child));
1498
+ const failures = [];
1499
+ try {
1500
+ const reasons = (await Promise.all(childDisposals.map(async (disposal) => {
1501
+ try {
1502
+ await disposal;
1503
+ return;
1504
+ } catch (error) {
1505
+ return error;
1506
+ }
1507
+ }))).filter((reason) => reason !== void 0);
1508
+ if (reasons.length > 0) failures.push(new SubagentError(`subagent "${childId}" child teardown failed: ${reasons.map((reason) => errorChain(reason)).join("; ")}`, "ACTIVATION_TEARDOWN_FAILED"));
1509
+ await idle;
1510
+ await this.flushFinalState(activation);
1511
+ activation.observer.capture(activation.handle.agent);
1512
+ } catch (error) {
1513
+ failures.push(new SubagentError(`subagent "${childId}" activation teardown failed: ${errorChain(error)}`, "ACTIVATION_TEARDOWN_FAILED", { cause: error }));
1514
+ }
1515
+ try {
1516
+ await activation.handle.dispose();
1517
+ } catch (error) {
1518
+ failures.push(new SubagentError(`subagent "${childId}" activation handle disposal failed: ${errorChain(error)}`, "ACTIVATION_TEARDOWN_FAILED", { cause: error }));
1519
+ }
1520
+ let failure;
1521
+ if (failures.length === 1) failure = failures[0];
1522
+ else if (failures.length > 1) failure = new SubagentError(`subagent "${childId}" activation teardown failed at ${failures.length} boundaries: ` + failures.map((item) => errorChain(item)).join("; "), "ACTIVATION_TEARDOWN_FAILED", { cause: new AggregateError(failures) });
1523
+ this.activations.delete(childId);
1524
+ activation.releaseBudget();
1525
+ this.notifySettlement(activation, activation.observer.terminal(failure));
1526
+ this.releaseOwnership(childId);
1527
+ activation.observer.settle(failure);
1528
+ if (failure !== void 0) throw failure;
1529
+ }
1530
+ /**
1531
+ * Tell the durable direct parent that this child produced everything it is
1532
+ * going to. Unconditional for every child the caller received an id for: it
1533
+ * does not consider whether the child reported, because the cases that most
1534
+ * need it — a token ceiling, a model failure, cancellation, teardown — are
1535
+ * exactly the ones where the child never got to choose. A materialization
1536
+ * rolled back before its first acceptance stays silent, since the caller was
1537
+ * told that child was not established. A parent that is no longer live is not
1538
+ * an error; the child's own Session remains the durable record either way.
1539
+ * A parent whose own lineage is already closing receives the notice without a
1540
+ * wake, because teardown is not a reason to start a turn.
1541
+ *
1542
+ * Never blocks disposal. A delivery failure is logged and dropped, because
1543
+ * retaining a child to retry a notice would pin its whole ancestry in
1544
+ * `waiting` forever.
1545
+ * @param activation - the settling Activation, still owned by its parent.
1546
+ * @param terminal - how this epoch ended, as the terminal edge will report it.
1547
+ */
1548
+ notifySettlement(activation, terminal) {
1549
+ if (!activation.announced) return;
1550
+ try {
1551
+ const parent = this.ctx.agents.get(activation.parentSession);
1552
+ if (parent === void 0) return;
1553
+ const summary = settlementSummary(activation.childId, terminal.stopReason);
1554
+ const message = createUserMessage({
1555
+ content: [{
1556
+ type: "text",
1557
+ text: summary
1558
+ }, ...terminal.output === void 0 ? [{
1559
+ type: "text",
1560
+ text: "It left no closing message."
1561
+ }] : [{
1562
+ type: "text",
1563
+ text: "Its closing message:"
1564
+ }, ...terminal.output]],
1565
+ source: {
1566
+ kind: "subagent-settled",
1567
+ form: "notice",
1568
+ summary: boundContextSummary(summary),
1569
+ senderSessionId: activation.childId
1570
+ }
1571
+ });
1572
+ if (this.closingTeardownFor(parent) !== void 0) {
1573
+ parent.inject(message);
1574
+ return;
1575
+ }
1576
+ this.sendWaking(parent, message, () => {
1577
+ if (parent.status === "idle") parent.followup(message);
1578
+ else parent.steer(message);
1579
+ });
1580
+ } catch (error) {
1581
+ this.ctx.logger.warn(`subagent "${activation.childId}" settlement notice was not delivered to its parent: ` + errorChain(error));
1582
+ }
1583
+ }
1584
+ /**
1585
+ * Request a best-effort final session flush after the child is quiescent.
1586
+ * Listener failure is logged because flush participation cannot identify a
1587
+ * particular persistence backend, and teardown must still release ownership.
1588
+ * @param activation - the Activation whose final events should be flushed.
1589
+ */
1590
+ async flushFinalState(activation) {
1591
+ const child = activation.handle.agent;
1592
+ try {
1593
+ await child.ctx.sessions.flush(child.session);
1594
+ } catch (error) {
1595
+ this.ctx.logger.warn(`subagent "${activation.childId}" best-effort final session flush failed; the persisted state may be unavailable or stale on resume: ${errorChain(error)}`);
1596
+ }
1597
+ }
1598
+ /** Resolve the persistence service continuable children require, or fail loud. */
1599
+ requirePersistence() {
1600
+ const persistence = this.ctx.get("sessionPersistence");
1601
+ if (persistence === void 0) throw new SubagentError("continuable subagents require session persistence (load a hydra-session-persistence backend)", "PERSISTENCE_UNAVAILABLE");
1602
+ return persistence;
1603
+ }
1604
+ };
1605
+ //#endregion
1606
+ //#region lib/types/activation-setup-registry.js
1607
+ /**
1608
+ * Internal registry of deployment capabilities composed into every continuable
1609
+ * child's unpublished creation context.
1610
+ *
1611
+ * A contribution grants a child-scoped capability without teaching the
1612
+ * continuation manager which capabilities exist. The manager owns residency;
1613
+ * this registry owns the join between plugin lifetime, unpublished setup, and
1614
+ * Activation disposal, so no installation outlives either owner and no removed
1615
+ * contribution can be installed after revocation reports completion.
1616
+ *
1617
+ * @module @hydraharness/harness-subagent/activation-setup-registry
1618
+ */
1619
+ /** Re-read mutable removal state after a contribution may have revoked itself. */
1620
+ function isRemoved(registration) {
1621
+ return registration.removed;
1622
+ }
1623
+ /**
1624
+ * Owns continuable-child setup registrations, installations, rollback, child
1625
+ * cleanup, and immediate live revocation.
1626
+ */
1627
+ var SubagentActivationSetupRegistry = class {
1628
+ /** Live contributions in installation order. */
1629
+ registrations = /* @__PURE__ */ new Set();
1630
+ /** Child context to its live installations. */
1631
+ byChild = /* @__PURE__ */ new Map();
1632
+ /**
1633
+ * Register one contribution.
1634
+ * @param contribution - synchronous child-scope installer.
1635
+ * @returns an idempotent registration undo.
1636
+ * @throws after attempting every installation when any disposer fails.
1637
+ */
1638
+ register(contribution) {
1639
+ const registration = {
1640
+ contribution,
1641
+ removed: false,
1642
+ installations: /* @__PURE__ */ new Set()
1643
+ };
1644
+ this.registrations.add(registration);
1645
+ return () => {
1646
+ if (registration.removed) return;
1647
+ registration.removed = true;
1648
+ this.registrations.delete(registration);
1649
+ this.releaseAll([...registration.installations], "contribution removal");
1650
+ };
1651
+ }
1652
+ /**
1653
+ * Install every live contribution into one unpublished child context.
1654
+ * @param childCtx - the child's unpublished scoped context.
1655
+ * @returns the provisioning commit consumed at Agent publication.
1656
+ */
1657
+ apply(childCtx) {
1658
+ const state = {
1659
+ installations: [],
1660
+ invalidated: false
1661
+ };
1662
+ try {
1663
+ for (const registration of [...this.registrations]) {
1664
+ /* v8 ignore next -- only a synchronous re-entrant revocation of an
1665
+ * already-snapshotted registration reaches this guard. */
1666
+ if (registration.removed) continue;
1667
+ const installation = {
1668
+ registration,
1669
+ childCtx,
1670
+ dispose: registration.contribution(childCtx),
1671
+ released: false,
1672
+ transaction: state
1673
+ };
1674
+ registration.installations.add(installation);
1675
+ state.installations.push(installation);
1676
+ let indexed = this.byChild.get(childCtx);
1677
+ if (indexed === void 0) {
1678
+ indexed = /* @__PURE__ */ new Set();
1679
+ this.byChild.set(childCtx, indexed);
1680
+ }
1681
+ indexed.add(installation);
1682
+ if (isRemoved(registration)) this.release(installation);
1683
+ }
1684
+ } catch (error) {
1685
+ try {
1686
+ this.releaseAll([...state.installations], "setup rollback");
1687
+ } catch (releaseFailure) {}
1688
+ throw error;
1689
+ }
1690
+ childCtx.effect(() => () => {
1691
+ this.releaseChild(childCtx);
1692
+ }, "subagents.activationSetup()");
1693
+ return { commit: () => {
1694
+ if (state.invalidated) throw new SubagentError("a continuable-subagent setup contribution was revoked while this child was being built; the child was not established", "ACTIVATION_SETUP_REVOKED");
1695
+ for (const installation of state.installations) installation.transaction = void 0;
1696
+ } };
1697
+ }
1698
+ /** Release every remaining installation owned by one disposed child scope. */
1699
+ releaseChild(childCtx) {
1700
+ const indexed = this.byChild.get(childCtx) ?? [];
1701
+ this.releaseAll([...indexed], "child scope disposal");
1702
+ }
1703
+ /**
1704
+ * Release a batch completely before reporting disposer failures.
1705
+ * @param installations - records to release.
1706
+ * @param during - operation name for diagnostics.
1707
+ */
1708
+ releaseAll(installations, during) {
1709
+ const failures = [];
1710
+ for (const installation of installations) try {
1711
+ this.release(installation);
1712
+ } catch (error) {
1713
+ failures.push(error);
1714
+ }
1715
+ if (failures.length === 0) return;
1716
+ throw new SubagentError(`continuable-subagent setup ${during} failed to release ${failures.length} installation(s): ` + failures.map((failure) => errorChain(failure)).join("; "), "ACTIVATION_SETUP_RELEASE_FAILED");
1717
+ }
1718
+ /** Drop one installation from both indices and dispose it exactly once. */
1719
+ release(installation) {
1720
+ if (installation.released) return;
1721
+ installation.released = true;
1722
+ installation.registration.installations.delete(installation);
1723
+ const indexed = this.byChild.get(installation.childCtx);
1724
+ /* v8 ignore next 4 -- every live installation is indexed until this method removes it. */
1725
+ if (indexed !== void 0) {
1726
+ indexed.delete(installation);
1727
+ if (indexed.size === 0) this.byChild.delete(installation.childCtx);
1728
+ }
1729
+ if (installation.transaction !== void 0) installation.transaction.invalidated = true;
1730
+ installation.dispose();
1731
+ }
1732
+ };
1733
+ //#endregion
1734
+ //#region lib/types/list-children.js
1735
+ /**
1736
+ * Read-only enumeration of durable subagent children and descendant trees
1737
+ * straight from the live session store and optional session persistence — no
1738
+ * query service. Candidates come from one live-preferred corpus; each child's
1739
+ * mode/label is the registered `subagent` projection unit's value, resolved
1740
+ * down a three-rung ladder: the registry's watermark cache for a live child,
1741
+ * a durable projection-cache row when it serves an own-suffix identity (the
1742
+ * seq gate), and one persistence inspection folded through the registry
1743
+ * otherwise, validated against the enumerated lifecycle. The projection fold
1744
+ * is the single classification authority — this module parses no descriptor
1745
+ * itself. Absent persistence, enumeration is live-only: a cold child is
1746
+ * unreachable for resume anyway, so its absence is capability absence, not an
1747
+ * error. The module owns no catalog state and does not consult Activation,
1748
+ * Agent-registry, continuation-manager, or provider state.
1749
+ *
1750
+ * @module @hydraharness/harness-subagent
1751
+ */
1752
+ /**
1753
+ * Concurrent cold inspections per listing; a constant because it bounds one
1754
+ * read-only scan of local media, not deployment behavior. Should a networked
1755
+ * persistence backend appear, promote it to a validated `Config` field.
1756
+ */
1757
+ const COLD_READ_CONCURRENCY = 4;
1758
+ /**
1759
+ * Enumerate one parent's origin-classified direct children from the
1760
+ * live-preferred merge of `ctx.sessions` and optional session persistence,
1761
+ * serving each identity from the `subagent` projection unit: the registry's
1762
+ * watermark snapshot for a live child; for a cold one, a durable
1763
+ * projection-cache row when it serves an own-suffix identity (the seq gate),
1764
+ * else one bounded-concurrency persistence inspection folded through the
1765
+ * registry.
1766
+ * @see SubagentRuntime.listChildren for the public cancellation and failure contract.
1767
+ * @param ctx - context carrying the session store, the projection registry,
1768
+ * optional persistence, and the optional projection cache.
1769
+ * @param parentSessionId - parent session whose direct children are listed.
1770
+ * @param signal - caller-owned cancellation observed around every persistence read.
1771
+ * @returns children and per-child diagnostics ordered by `createdAt`, then id.
1772
+ * @throws {@link SubagentError} when the projection registry or the session
1773
+ * store is not mounted, or the caller cancels the listing.
1774
+ */
1775
+ async function listChildren(ctx, parentSessionId, signal) {
1776
+ const listing = await prepareListing(ctx, signal);
1777
+ return (await resolveCandidateRows([...listing.corpus.values()].filter((record) => record.header.parentSession === parentSessionId && record.header.origin === "subagent").sort(compareCorpusRecords), listing, signal)).filter((row) => row !== void 0);
1778
+ }
1779
+ /**
1780
+ * Enumerate every session-backed subagent below one root in stable pre-order.
1781
+ * Ordinary sessions and one-shot children remain traversal nodes, so a
1782
+ * continuable child below either is still discovered. Classification uses the
1783
+ * same projection-backed runtime as {@link listChildren}; no Agent is loaded or
1784
+ * resumed.
1785
+ * @see SubagentRuntime.listDescendants for the public cancellation and failure contract.
1786
+ * @param ctx - context carrying the session store, projection registry, and optional persistence/cache.
1787
+ * @param rootSessionId - session whose complete descendant tree is listed.
1788
+ * @param signal - caller-owned cancellation observed around every persistence read.
1789
+ * @returns interpreted subagents with durable direct-parent and root-relative depth.
1790
+ * @throws {@link SubagentError} under the same conditions as {@link listChildren}.
1791
+ */
1792
+ async function listDescendants(ctx, rootSessionId, signal) {
1793
+ const listing = await prepareListing(ctx, signal);
1794
+ const positioned = descendantCandidates(listing.corpus, rootSessionId);
1795
+ const rows = await resolveCandidateRows(positioned.map((candidate) => candidate.record), listing, signal);
1796
+ const entries = [];
1797
+ positioned.forEach((position, index) => {
1798
+ const row = rows[index];
1799
+ if (row !== void 0) entries.push({
1800
+ ...row,
1801
+ parentId: position.parentId,
1802
+ depth: position.depth
1803
+ });
1804
+ });
1805
+ return entries;
1806
+ }
1807
+ /** Resolve listing services once and build one live-preferred session corpus. */
1808
+ async function prepareListing(ctx, signal) {
1809
+ const projections = ctx.get("sessionProjections");
1810
+ if (projections === void 0) throw new SubagentError("listing subagents requires the sessionProjections registry (load @hydraharness/harness-session-projection)", "SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE");
1811
+ const sessions = ctx.get("sessions");
1812
+ if (sessions === void 0) throw new SubagentError("listing subagents requires the session store (load @hydraharness/harness-session)", "SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE");
1813
+ assertListingNotCancelled(signal);
1814
+ const persistence = ctx.get("sessionPersistence");
1815
+ const cache = ctx.get("sessionProjectionCache");
1816
+ let persistedHeaders = [];
1817
+ if (persistence !== void 0) {
1818
+ try {
1819
+ persistedHeaders = await persistence.list(signal);
1820
+ } catch (error) {
1821
+ assertListingNotCancelled(signal);
1822
+ throw error;
1823
+ }
1824
+ assertListingNotCancelled(signal);
1825
+ }
1826
+ const corpus = /* @__PURE__ */ new Map();
1827
+ for (const header of persistedHeaders) corpus.set(header.id, {
1828
+ header,
1829
+ live: void 0
1830
+ });
1831
+ for (const session of sessions.list()) corpus.set(session.header.id, {
1832
+ header: session.header,
1833
+ live: session
1834
+ });
1835
+ const subagentParents = /* @__PURE__ */ new Set();
1836
+ for (const record of corpus.values()) if (record.header.origin === "subagent" && record.header.parentSession !== void 0) subagentParents.add(record.header.parentSession);
1837
+ return {
1838
+ projections,
1839
+ persistence,
1840
+ cache,
1841
+ corpus,
1842
+ subagentParents
1843
+ };
1844
+ }
1845
+ /** Resolve projection-backed rows for aligned candidates with bounded cold reads. */
1846
+ async function resolveCandidateRows(candidates, listing, signal) {
1847
+ const { projections, persistence, cache, subagentParents } = listing;
1848
+ const rows = Array.from({ length: candidates.length });
1849
+ const coldReads = [];
1850
+ candidates.forEach((candidate, index) => {
1851
+ const childId = candidate.header.id;
1852
+ if (candidate.live === void 0) {
1853
+ coldReads.push({
1854
+ index,
1855
+ header: candidate.header
1856
+ });
1857
+ return;
1858
+ }
1859
+ let identity;
1860
+ try {
1861
+ identity = projections.snapshot(candidate.live).values.subagent;
1862
+ } catch {
1863
+ rows[index] = {
1864
+ kind: "diagnostic",
1865
+ id: childId,
1866
+ reason: "corrupt"
1867
+ };
1868
+ return;
1869
+ }
1870
+ if (identity === void 0 || identity === null) return;
1871
+ rows[index] = childRow(childId, identity, "running", subagentParents.has(childId));
1872
+ });
1873
+ if (persistence !== void 0 && coldReads.length > 0) {
1874
+ const queue = [...coldReads];
1875
+ await Promise.all(Array.from({ length: Math.min(COLD_READ_CONCURRENCY, queue.length) }, async () => {
1876
+ for (let job = queue.shift(); job !== void 0; job = queue.shift()) rows[job.index] = await resolveColdIdentity(persistence, projections, cache, job.header, subagentParents.has(job.header.id), signal);
1877
+ }));
1878
+ }
1879
+ assertListingNotCancelled(signal);
1880
+ return rows;
1881
+ }
1882
+ /** Build origin-classified candidates from the complete tree without recursion. */
1883
+ function descendantCandidates(corpus, rootSessionId) {
1884
+ const children = /* @__PURE__ */ new Map();
1885
+ for (const record of corpus.values()) {
1886
+ const parentId = record.header.parentSession;
1887
+ if (parentId === void 0) continue;
1888
+ const siblings = children.get(parentId);
1889
+ if (siblings === void 0) children.set(parentId, [record]);
1890
+ else siblings.push(record);
1891
+ }
1892
+ for (const siblings of children.values()) siblings.sort(compareCorpusRecords);
1893
+ const positioned = [];
1894
+ const stack = (children.get(rootSessionId) ?? []).map((record) => ({
1895
+ record,
1896
+ parentId: rootSessionId,
1897
+ depth: 1
1898
+ })).reverse();
1899
+ const visited = new Set([rootSessionId]);
1900
+ while (stack.length > 0) {
1901
+ const position = stack.pop();
1902
+ const id = position.record.header.id;
1903
+ if (visited.has(id)) continue;
1904
+ visited.add(id);
1905
+ if (position.record.header.origin === "subagent") positioned.push(position);
1906
+ const descendants = children.get(id) ?? [];
1907
+ for (const record of [...descendants].reverse()) stack.push({
1908
+ record,
1909
+ parentId: id,
1910
+ depth: position.depth + 1
1911
+ });
1912
+ }
1913
+ return positioned;
1914
+ }
1915
+ /** Compare siblings by durable creation time, then id. */
1916
+ function compareCorpusRecords(a, b) {
1917
+ return a.header.createdAt - b.header.createdAt || a.header.id.localeCompare(b.header.id);
1918
+ }
1919
+ /**
1920
+ * Resolve one cold candidate down the remaining ladder: a durable
1921
+ * projection-cache row when it serves an own-suffix identity (the seq gate),
1922
+ * otherwise one persistence inspection folded through the projection
1923
+ * registry (the same detached recipe the API proxy uses for detached session
1924
+ * projections). A failed inspection is one transient `unavailable` row
1925
+ * retried on the next listing; an inspection naming another lifecycle, and a
1926
+ * settled log the fold cannot identify — or that makes any registered unit
1927
+ * throw — are final, so they report `corrupt`.
1928
+ */
1929
+ async function resolveColdIdentity(persistence, projections, cache, header, hasChildren, signal) {
1930
+ const childId = header.id;
1931
+ if (cache !== void 0) {
1932
+ let cached;
1933
+ try {
1934
+ cached = cache.cachedSnapshot(header)?.values.subagent;
1935
+ } catch {
1936
+ cached = void 0;
1937
+ }
1938
+ if (cached !== void 0 && cached !== null && cached.seq >= (header.seedLength ?? 0)) return childRow(childId, cached, "inactive", hasChildren);
1939
+ }
1940
+ assertListingNotCancelled(signal);
1941
+ let inspected;
1942
+ try {
1943
+ inspected = await persistence.inspect(childId, signal);
1944
+ } catch {
1945
+ assertListingNotCancelled(signal);
1946
+ return {
1947
+ kind: "diagnostic",
1948
+ id: childId,
1949
+ reason: "unavailable"
1950
+ };
1951
+ }
1952
+ assertListingNotCancelled(signal);
1953
+ if (!sameLifecycle(inspected.meta, header)) return {
1954
+ kind: "diagnostic",
1955
+ id: childId,
1956
+ reason: "corrupt"
1957
+ };
1958
+ let identity;
1959
+ try {
1960
+ identity = projections.restore({}, inspected.events, 0).snapshot.values.subagent;
1961
+ } catch {
1962
+ return {
1963
+ kind: "diagnostic",
1964
+ id: childId,
1965
+ reason: "corrupt"
1966
+ };
1967
+ }
1968
+ if (identity === void 0 || identity === null) return {
1969
+ kind: "diagnostic",
1970
+ id: childId,
1971
+ reason: "corrupt"
1972
+ };
1973
+ return childRow(childId, identity, "inactive", hasChildren);
1974
+ }
1975
+ /** Materialize one served identity as its child row. */
1976
+ function childRow(id, identity, activity, hasChildren) {
1977
+ return identity.mode === "one-shot" ? {
1978
+ kind: "child",
1979
+ id,
1980
+ mode: "one-shot",
1981
+ ...identity.label !== void 0 ? { label: identity.label } : {},
1982
+ activity,
1983
+ hasChildren
1984
+ } : {
1985
+ kind: "child",
1986
+ id,
1987
+ mode: "continuable",
1988
+ label: identity.label,
1989
+ activity,
1990
+ hasChildren
1991
+ };
1992
+ }
1993
+ /** Immutable header fields that distinguish one session lifecycle from another under the same id. */
1994
+ const LIFECYCLE_WITNESS_KEYS = [
1995
+ "version",
1996
+ "id",
1997
+ "createdAt",
1998
+ "cwd",
1999
+ "parentSession",
2000
+ "seedLength",
2001
+ "delegationDepth"
2002
+ ];
2003
+ /** Whether an inspected log still belongs to the enumerated lifecycle. */
2004
+ function sameLifecycle(meta, expected) {
2005
+ return LIFECYCLE_WITNESS_KEYS.every((key) => meta[key] === expected[key]);
2006
+ }
2007
+ /** Stop a listing at its next cancellation checkpoint. */
2008
+ function assertListingNotCancelled(signal) {
2009
+ if (signal?.aborted) throw new SubagentError("subagent listing was cancelled", "CANCELLED");
2010
+ }
2011
+ //#endregion
2012
+ //#region lib/types/projection.js
2013
+ /**
2014
+ * Pure session projections for subagent identity (mode/label) and active-turn
2015
+ * duration.
2016
+ *
2017
+ * @module @hydraharness/harness-subagent/projection
2018
+ */
2019
+ const activeIntervalSchema = z$1.object({
2020
+ since: z$1.number().int().nonnegative(),
2021
+ through: z$1.number().int().nonnegative()
2022
+ }).strict();
2023
+ const projectionSchema = z$1.object({
2024
+ settledMs: z$1.number().int().nonnegative(),
2025
+ active: activeIntervalSchema.optional()
2026
+ }).strict().transform(({ settledMs, active }) => ({
2027
+ settledMs,
2028
+ ...active === void 0 ? {} : { active }
2029
+ }));
2030
+ /**
2031
+ * Fold turn boundaries around the child's own durable descriptor.
2032
+ *
2033
+ * A fork seed may contain an ancestor descriptor and completed turns. Every
2034
+ * descriptor therefore resets the accumulated state; the healthy catalog
2035
+ * admits only a child with exactly one descriptor in its own suffix, making
2036
+ * the final reset the child's authoritative timing origin.
2037
+ */
2038
+ const subagentTimingProjectionDefinition = {
2039
+ key: "subagentTiming",
2040
+ stateSchema: z$1.object({
2041
+ settledMs: z$1.number().int().nonnegative(),
2042
+ active: activeIntervalSchema.optional(),
2043
+ pendingTurnStart: z$1.number().int().nonnegative().optional(),
2044
+ descriptorSeen: z$1.boolean()
2045
+ }).strict(),
2046
+ init: () => ({
2047
+ descriptorSeen: false,
2048
+ settledMs: 0
2049
+ }),
2050
+ apply: (state, event) => {
2051
+ if (event.type === "turn/start") return state.descriptorSeen ? {
2052
+ ...state,
2053
+ active: {
2054
+ since: event.time,
2055
+ through: event.time
2056
+ }
2057
+ } : {
2058
+ ...state,
2059
+ pendingTurnStart: event.time
2060
+ };
2061
+ if (event.type === "subagent/descriptor") {
2062
+ const activeSince = state.active?.since ?? state.pendingTurnStart;
2063
+ return {
2064
+ descriptorSeen: true,
2065
+ settledMs: 0,
2066
+ ...activeSince === void 0 ? {} : { active: {
2067
+ since: activeSince,
2068
+ through: event.time
2069
+ } }
2070
+ };
2071
+ }
2072
+ if (event.type === "turn/end") {
2073
+ if (!state.descriptorSeen) {
2074
+ if (state.pendingTurnStart === void 0) return state;
2075
+ const { pendingTurnStart: _closed, ...next } = state;
2076
+ return next;
2077
+ }
2078
+ if (state.active === void 0) return state;
2079
+ const { active, ...rest } = state;
2080
+ return {
2081
+ ...rest,
2082
+ settledMs: state.settledMs + Math.max(0, event.time - active.since)
2083
+ };
2084
+ }
2085
+ if (state.active === void 0) return state;
2086
+ return {
2087
+ ...state,
2088
+ active: {
2089
+ ...state.active,
2090
+ through: event.time
2091
+ }
2092
+ };
2093
+ },
2094
+ wire: {
2095
+ viewSchema: projectionSchema,
2096
+ view: (state) => ({
2097
+ settledMs: state.settledMs,
2098
+ ...state.active === void 0 ? {} : { active: state.active }
2099
+ })
2100
+ },
2101
+ stateVersion: 2
2102
+ };
2103
+ const identityValueSchema = z$1.discriminatedUnion("mode", [z$1.object({
2104
+ mode: z$1.literal("one-shot"),
2105
+ label: z$1.string().optional(),
2106
+ seq: z$1.number().int().nonnegative()
2107
+ }).strict(), z$1.object({
2108
+ mode: z$1.literal("continuable"),
2109
+ label: z$1.string(),
2110
+ seq: z$1.number().int().nonnegative()
2111
+ }).strict()]);
2112
+ const identitySchema = identityValueSchema.nullable();
2113
+ const identityStateSchema = z$1.object({ identity: identityValueSchema.optional() }).strict();
2114
+ /** Interpret one `subagent/descriptor` event's identity; no value when the payload cannot be trusted. */
2115
+ function descriptorIdentity(event) {
2116
+ let descriptor;
2117
+ try {
2118
+ descriptor = foldSubagentDescriptor([event]);
2119
+ } catch {
2120
+ descriptor = void 0;
2121
+ }
2122
+ if (descriptor === void 0) return void 0;
2123
+ return descriptor.mode === "one-shot" ? {
2124
+ mode: "one-shot",
2125
+ ...descriptor.label !== void 0 ? { label: descriptor.label } : {},
2126
+ seq: event.seq
2127
+ } : {
2128
+ mode: "continuable",
2129
+ label: descriptor.label,
2130
+ seq: event.seq
2131
+ };
2132
+ }
2133
+ /**
2134
+ * Fold the durable mode/label identity from `subagent/descriptor` events,
2135
+ * last-wins: a fork seed may replay an ancestor's descriptor, and the child's
2136
+ * own descriptor must override it — the same reset discipline as
2137
+ * {@link subagentTimingProjectionDefinition}. A malformed or unknown-version
2138
+ * payload resets to the `null` sentinel instead of throwing, so a fork of a
2139
+ * healthy ancestor never inherits an identity its own descriptor failed to
2140
+ * establish — and the reset survives every JSON push frame, so a consumer
2141
+ * holding the earlier identity replaces it instead of keeping it stale;
2142
+ * `null` ⟺ no valid descriptor, with the causes deliberately undistinguished.
2143
+ */
2144
+ const subagentIdentityProjectionDefinition = {
2145
+ key: "subagent",
2146
+ stateSchema: identityStateSchema,
2147
+ init: () => ({}),
2148
+ apply: (state, event) => {
2149
+ if (event.type !== "subagent/descriptor") return state;
2150
+ const identity = descriptorIdentity(event);
2151
+ return identity === void 0 ? {} : { identity };
2152
+ },
2153
+ wire: {
2154
+ viewSchema: identitySchema,
2155
+ view: (state) => state.identity ?? null
2156
+ },
2157
+ stateVersion: 2
2158
+ };
2159
+ //#endregion
2160
+ //#region lib/types/out-of-process.js
2161
+ /**
2162
+ * Provider-side vocabulary for OUT-OF-PROCESS subagent backends — the pieces
2163
+ * that enforce this seam's own contracts around a child in another process:
2164
+ * the no-capabilities advertisement, timing-bound validation, child
2165
+ * working-directory resolution (config override, else the delegating parent
2166
+ * session's workspace), the never-reject result settlement, and the standard
2167
+ * run-handle publication. Backends compose these with their own wire drivers;
2168
+ * the process machinery itself (spawn, env scrub, tree-scoped teardown)
2169
+ * belongs to the `@hydraharness/harness-subprocess` seam.
2170
+ *
2171
+ * @module @hydraharness/harness-subagent/out-of-process
2172
+ */
2173
+ /** Maximum UTF-8 size of {@link SubagentResult.diagnostic}. */
2174
+ const MAX_SUBAGENT_DIAGNOSTIC_BYTES = 4096;
2175
+ const DIAGNOSTIC_TRUNCATION_SUFFIX = "\n[diagnostic truncated]";
2176
+ const utf8Encoder = new TextEncoder();
2177
+ const utf8Decoder = new TextDecoder();
2178
+ /**
2179
+ * Limit provider-authored failure detail without splitting a UTF-8 sequence.
2180
+ * @param diagnostic - safe diagnostic text produced by the provider.
2181
+ * @returns the original text, or a visibly truncated value within the limit.
2182
+ */
2183
+ function limitSubagentDiagnostic(diagnostic) {
2184
+ const bytes = utf8Encoder.encode(diagnostic);
2185
+ if (bytes.byteLength <= MAX_SUBAGENT_DIAGNOSTIC_BYTES) return diagnostic;
2186
+ let prefixBytes = MAX_SUBAGENT_DIAGNOSTIC_BYTES - utf8Encoder.encode(DIAGNOSTIC_TRUNCATION_SUFFIX).byteLength;
2187
+ while ((bytes[prefixBytes] & 192) === 128) prefixBytes -= 1;
2188
+ return utf8Decoder.decode(bytes.subarray(0, prefixBytes)) + DIAGNOSTIC_TRUNCATION_SUFFIX;
2189
+ }
2190
+ /**
2191
+ * The capability advertisement of an out-of-process backend: NONE. A child in
2192
+ * another process cannot honor parent-enforced start features
2193
+ * (`outputSchema`/`maxDepth`/`toolFilter`/`persona`), so the service rejects a
2194
+ * request needing any of them before `start` runs — never accepted-then-ignored.
2195
+ */
2196
+ const NO_START_CAPABILITIES = Object.freeze({
2197
+ outputSchema: false,
2198
+ depthLimit: false,
2199
+ toolFilter: false,
2200
+ persona: false
2201
+ });
2202
+ /**
2203
+ * Assert a configured timing bound is a positive finite number (it bounds a
2204
+ * teardown or shutdown wait; zero, negative, or NaN would skip or wedge it).
2205
+ * @param prefix - the consuming plugin's diagnostic prefix (e.g. `subagent-acp`).
2206
+ * @param name - the config field name, for the diagnostic.
2207
+ * @param value - the configured value.
2208
+ */
2209
+ function assertPositiveFinite(prefix, name, value) {
2210
+ if (!Number.isFinite(value) || value <= 0) throw new Error(`${prefix}: ${name} must be a positive finite number`);
2211
+ }
2212
+ /**
2213
+ * Whether `path` names an existing directory the harness can ENTER. The
2214
+ * search-permission probe matters: `statSync().isDirectory()` is true for a
2215
+ * mode-600 directory, but a subprocess cwd needs `X_OK` or spawn fails EACCES.
2216
+ */
2217
+ function isEnterableDirectory(path) {
2218
+ try {
2219
+ if (!statSync(path).isDirectory()) return false;
2220
+ accessSync(path, constants.X_OK);
2221
+ return true;
2222
+ } catch {
2223
+ return false;
2224
+ }
2225
+ }
2226
+ /**
2227
+ * Assert `cwd` can actually host the child: absolute (it doubles as the
2228
+ * child's workspace identity, and a relative path would be re-anchored to the
2229
+ * server process's launch directory) and an existing directory (fail here,
2230
+ * before the process boundary, instead of as an ambiguous spawn ENOENT).
2231
+ * @param prefix - the consuming plugin's diagnostic prefix.
2232
+ * @param label - which source supplied the value, for the diagnostic.
2233
+ * @param cwd - the candidate working directory.
2234
+ * @returns `cwd`, validated.
2235
+ */
2236
+ function assertUsableCwd(prefix, label, cwd) {
2237
+ if (!isAbsolute(cwd)) throw new Error(`${prefix}: ${label} must be an absolute path: ${cwd}`);
2238
+ if (!isEnterableDirectory(cwd)) throw new Error(`${prefix}: ${label} is not an accessible directory: ${cwd}`);
2239
+ return cwd;
2240
+ }
2241
+ /**
2242
+ * Validate a configured `cwd` override ONCE, at plugin load: reject the empty
2243
+ * string (`path.resolve('')` is the process cwd — it would silently
2244
+ * reintroduce the launch-directory fallback this resolution removes),
2245
+ * interpret a relative path against the harness launch directory, and require
2246
+ * an enterable directory.
2247
+ * @param prefix - the consuming plugin's diagnostic prefix.
2248
+ * @param cwd - the configured override, or `undefined` when the config omits it.
2249
+ * @returns the validated absolute override, or `undefined` when omitted.
2250
+ */
2251
+ function validateConfiguredCwd(prefix, cwd) {
2252
+ if (cwd === void 0) return void 0;
2253
+ if (cwd === "") throw new Error(`${prefix}: config cwd must not be empty — omit the key to inherit the parent session cwd`);
2254
+ return assertUsableCwd(prefix, "config cwd", resolve(cwd));
2255
+ }
2256
+ /**
2257
+ * Resolve the child's working directory at start: the deployment override
2258
+ * when configured (already validated at load), else the parent session's
2259
+ * workspace cwd (validated here, its earliest resolvable point). Fails loud
2260
+ * when neither exists — falling back to the harness process cwd would
2261
+ * silently bind the child to the server's launch directory instead of the
2262
+ * delegating session's workspace (one server process serves many sessions,
2263
+ * each with its own cwd).
2264
+ * @param prefix - the consuming plugin's diagnostic prefix.
2265
+ * @param configured - the load-validated override, or `undefined`.
2266
+ * @param parentCwd - the delegating parent session's workspace cwd, if any.
2267
+ * @returns the absolute child working directory.
2268
+ */
2269
+ function resolveChildCwd(prefix, configured, parentCwd) {
2270
+ if (configured !== void 0) return configured;
2271
+ if (parentCwd === void 0) throw new Error(`${prefix}: no working directory for the child — configure \`cwd\` or delegate from a parent session that has one`);
2272
+ return assertUsableCwd(prefix, "parent session cwd", parentCwd);
2273
+ }
2274
+ /** Normalize an unknown thrown value to an Error (the catch binding is `unknown`). */
2275
+ function toError(value) {
2276
+ /* v8 ignore next */
2277
+ return value instanceof Error ? value : new Error(String(value));
2278
+ }
2279
+ /**
2280
+ * Settle an out-of-process run result under the seam contract: `result` never
2281
+ * rejects after publication. A normally completed or rejected attempt resolves
2282
+ * as `aborted` when cancellation already settled locally; another rejection is
2283
+ * flattened to `stopReason: 'error'` through the contained diagnostic sink.
2284
+ * The abort listener is removed on every path.
2285
+ * @param parts - the attempt, output snapshot, cancellation state, sink, and signal wiring.
2286
+ * @returns the terminal result (never a rejection).
2287
+ */
2288
+ async function settleRunResult(parts) {
2289
+ try {
2290
+ const result = await parts.attempt();
2291
+ return parts.cancelled() ? {
2292
+ output: parts.collectOutput(),
2293
+ stopReason: "aborted"
2294
+ } : result;
2295
+ } catch (error) {
2296
+ if (parts.cancelled()) return {
2297
+ output: parts.collectOutput(),
2298
+ stopReason: "aborted"
2299
+ };
2300
+ try {
2301
+ parts.onError?.(toError(error), "error");
2302
+ } catch {}
2303
+ const collected = parts.collectDiagnostic?.();
2304
+ const diagnostic = collected === void 0 ? void 0 : limitSubagentDiagnostic(collected);
2305
+ return {
2306
+ output: parts.collectOutput(),
2307
+ ...diagnostic === void 0 ? {} : { diagnostic },
2308
+ stopReason: "error"
2309
+ };
2310
+ } finally {
2311
+ parts.signal.removeEventListener("abort", parts.onAbort);
2312
+ }
2313
+ }
2314
+ /**
2315
+ * Publish the seam run handle for an out-of-process child. `dispose()` is
2316
+ * idempotent (one memoized teardown): it removes the abort listener, settles
2317
+ * local cancellation — there is no assumption the child cooperates — and then
2318
+ * awaits the backend's teardown to actual exit.
2319
+ * @param parts - the run identity, result, cancellation wiring, and teardown.
2320
+ * @returns the seam run handle (`localAgent` is `undefined` for remote runs).
2321
+ */
2322
+ function subprocessRunHandle(parts) {
2323
+ let disposal;
2324
+ return {
2325
+ id: parts.id,
2326
+ localAgent: void 0,
2327
+ result: parts.result,
2328
+ dispose() {
2329
+ if (disposal !== void 0) return disposal;
2330
+ parts.signal.removeEventListener("abort", parts.onAbort);
2331
+ parts.requestCancel();
2332
+ disposal = parts.teardown();
2333
+ return disposal;
2334
+ }
2335
+ };
2336
+ }
2337
+ //#endregion
2338
+ //#region lib/types/run-settlement.js
2339
+ /**
2340
+ * Settlement of one ONE-SHOT subagent run into a background-Task outcome. Only
2341
+ * the one-shot background path uses Jobs; continuable children have no Task,
2342
+ * no per-message result, and no Task cancellation.
2343
+ *
2344
+ * @module @hydraharness/harness-subagent/run-settlement
2345
+ */
2346
+ /** Flatten a child's final output blocks to the task's final text. */
2347
+ function finalText(blocks) {
2348
+ return blocks.filter((block) => block.type === "text").map((block) => block.text).join("");
2349
+ }
2350
+ /** Render a failed stop reason with optional provider-authored detail. */
2351
+ function failureDetail(result) {
2352
+ const stopReason = result.stopReason;
2353
+ return result.diagnostic === void 0 ? stopReason : `${stopReason}; diagnostic: ${result.diagnostic}`;
2354
+ }
2355
+ /**
2356
+ * Map a child result to the task outcome: completed carries final text,
2357
+ * aborted is killed, and every other reason is failed without partial output.
2358
+ * @param result - child terminal result.
2359
+ * @returns outcome for the `ctx.jobs` registration.
2360
+ */
2361
+ function runOutcome(result) {
2362
+ switch (result.stopReason) {
2363
+ case "completed": return {
2364
+ status: "completed",
2365
+ output: finalText(result.output)
2366
+ };
2367
+ case "aborted": return { status: "killed" };
2368
+ case "error":
2369
+ case "max-tokens":
2370
+ case "refusal": return {
2371
+ status: "failed",
2372
+ detail: failureDetail(result)
2373
+ };
2374
+ default: return {
2375
+ status: "failed",
2376
+ detail: failureDetail(result)
2377
+ };
2378
+ }
2379
+ }
2380
+ /**
2381
+ * Await the child result, dispose the run, then return its task outcome. Result
2382
+ * and disposal failures become `failed`; when both fail, both details survive.
2383
+ * @param run - live run to settle and release.
2384
+ * @returns outcome after child resources are released.
2385
+ */
2386
+ async function settleRun(run) {
2387
+ let outcome;
2388
+ try {
2389
+ outcome = runOutcome(await run.result);
2390
+ } catch (error) {
2391
+ outcome = {
2392
+ status: "failed",
2393
+ detail: String(error)
2394
+ };
2395
+ }
2396
+ try {
2397
+ await run.dispose();
2398
+ } catch (error) {
2399
+ return {
2400
+ status: "failed",
2401
+ detail: `${outcome.detail === void 0 ? "" : `${outcome.detail}; `}dispose failed: ${String(error)}`
2402
+ };
2403
+ }
2404
+ return outcome;
2405
+ }
2406
+ //#endregion
2407
+ //#region lib/types/index.js
2408
+ /**
2409
+ * Service Definition for the subagent capability seam (`ctx.subagents`): a named-provider registry plus a
2410
+ * capability-validating asynchronous start API. Providers establish a
2411
+ * child before returning its run, so fulfillment is the single publication and
2412
+ * ownership-transfer boundary.
2413
+ *
2414
+ * Unlike the bash seam (one executor per context, second load throws), MULTIPLE
2415
+ * providers coexist here: each registers under a unique name and a caller picks
2416
+ * one by name. The shape mirrors the LLM adapter registry
2417
+ * (`LlmRuntime.registerAdapter`), not the single-service bash executor.
2418
+ *
2419
+ * This package owns the Service Definition role of the capability seam. Service Providers
2420
+ * (`@hydraharness/harness-subagent-spawn-in-process`, `-fork`, `-acp`) and the model-facing
2421
+ * consumer (`@hydraharness/harness-tool-subagent`) are separate packages.
2422
+ *
2423
+ * Public operations express caller intent: `start` returns one published owned
2424
+ * one-shot run, `startContinuable` establishes a durable continuable child, and
2425
+ * `followup` delivers later content without exposing whether the child is
2426
+ * resident. Continuable children never become a {@link SubagentRun}: the
2427
+ * continuation manager holds their `AgentHandle` directly and orders every turn
2428
+ * through the child's own inbox, so providers contribute only the detached
2429
+ * creation spec and see no handle, turn, or teardown. Child and descendant
2430
+ * discovery read the live session store and optional session persistence
2431
+ * directly and do not require that continuation runtime.
2432
+ *
2433
+ * Same-process providers are trusted typed collaborators. Requests, provider
2434
+ * descriptors, results, and lifecycle payloads are borrowed immutable values;
2435
+ * serialization and hostile-input validation belong at real process, worker,
2436
+ * persistence, and model boundaries.
2437
+ *
2438
+ * @module @hydraharness/harness-subagent
2439
+ */
2440
+ /** Named provider registry with one-shot runs, durable discovery, and continuable-child operations. */
2441
+ var SubagentRuntime = class extends Service {
2442
+ providers = /* @__PURE__ */ new Map();
2443
+ continuations;
2444
+ /** Deployment contributions composed into unpublished continuable children. */
2445
+ setupRegistry = new SubagentActivationSetupRegistry();
2446
+ /**
2447
+ * The contained lifecycle-edge publisher. Built here because scoped dispatch
2448
+ * keys its carrier by this exact service instance, whose own context filter
2449
+ * composes into the carrier.
2450
+ */
2451
+ emitLifecycle;
2452
+ static Config = z.object({
2453
+ maxActivePerTree: z.number(),
2454
+ maxChildrenPerTree: z.number()
2455
+ });
2456
+ admit;
2457
+ constructor(ctx, config = {}) {
2458
+ super(ctx, "subagents");
2459
+ this.admit = delegationAdmission(ctx, config);
2460
+ this.emitLifecycle = createLifecycleEmitter(this.ctx, (parent) => scopeTarget(this, parent));
2461
+ ctx.inject(["agents"], (childCtx) => {
2462
+ const manager = new SubagentContinuationManager(childCtx, {
2463
+ admit: this.admit,
2464
+ prepareContinuable: (name, request) => this.prepareContinuable(name, request),
2465
+ observeActivation: (provider, childId, parent) => this.observeActivation(provider, childId, parent)
2466
+ }, this.setupRegistry);
2467
+ this.continuations = manager;
2468
+ childCtx.effect(() => () => {
2469
+ /* v8 ignore else -- one injected binding owns the slot until its fiber disposes. */
2470
+ if (this.continuations === manager) this.continuations = void 0;
2471
+ }, "subagents.continuationBinding()");
2472
+ });
2473
+ ctx.inject(["sessionProjections"], (projectionCtx) => {
2474
+ projectionCtx.sessionProjections.register(subagentTimingProjectionDefinition);
2475
+ projectionCtx.sessionProjections.register(subagentIdentityProjectionDefinition);
2476
+ });
2477
+ }
2478
+ /**
2479
+ * Establish one durable continuable child and deliver its initial prompt.
2480
+ * Resolves when the child's inbox accepts that prompt, without waiting for the
2481
+ * turn to start or for the message to reach the Session log; any earlier
2482
+ * failure rejects with no ids and rolls back the child entirely.
2483
+ * @param spec - provider, delegation request, and caller cancellation.
2484
+ * @returns the durable child id and the accepted prompt's message id.
2485
+ * @throws when continuation services are unavailable or materialization fails.
2486
+ */
2487
+ async startContinuable(spec) {
2488
+ return this.requireContinuations().startContinuable(spec);
2489
+ }
2490
+ /**
2491
+ * Deliver one later message to a continuable child as its next FIFO turn. A
2492
+ * resident child's Agent inbox accepts it directly (waking a `waiting`
2493
+ * Activation), while an absent one is cold-resumed from its persisted
2494
+ * Session. The Agent inbox is the only queue, so every accepted message has
2495
+ * one observable order.
2496
+ * @param parent - the exact live direct parent authorizing this delivery.
2497
+ * @param childId - durable child session id.
2498
+ * @param content - user-role content to deliver.
2499
+ * @param options - the message source fields and caller cancellation, which stops the
2500
+ * operation only before inbox acceptance.
2501
+ * @returns the accepted message's inbox id.
2502
+ * @throws when continuation services are unavailable, parent authority is
2503
+ * rejected, or the message was not admitted.
2504
+ */
2505
+ async followup(parent, childId, content, options) {
2506
+ return this.requireContinuations().followup(parent, childId, content, options);
2507
+ }
2508
+ /**
2509
+ * Interrupt one live continuable child's current turn under a human parent
2510
+ * address or an exact live ancestor Agent. Fire-and-return: the cancel
2511
+ * signal is issued before this returns, but the target may keep running
2512
+ * until it observes the signal. Unclaimed pending inbox work, the Activation,
2513
+ * and published descendants are preserved; claimed work is not requeued.
2514
+ * Once the interrupted driver is idle, a waking send resumes the parked FIFO
2515
+ * queue. An absent target — including a one-shot or unknown id —
2516
+ * is an accepted no-op, as is a manager-less composition, which cannot own a
2517
+ * live Activation.
2518
+ * @param targetSessionId - the durable child session id to interrupt.
2519
+ * @param authority - the human parent address or exact live ancestor Agent.
2520
+ * @throws {SubagentError} `UNAUTHORIZED` when the authority does not own the
2521
+ * live target.
2522
+ */
2523
+ interrupt(targetSessionId, authority) {
2524
+ this.continuations?.interrupt(targetSessionId, authority);
2525
+ }
2526
+ /**
2527
+ * Deliver selected content from one live continuable child to its durable
2528
+ * direct parent. The child is the authority credential; callers cannot name a
2529
+ * recipient. Reporting does not conclude the child's turn or Activation.
2530
+ * @param child - exact live reporting child.
2531
+ * @param content - selected model-facing content.
2532
+ * @param options - parent scheduling and pre-acceptance cancellation.
2533
+ * @returns the stable identity of the parent-accepted message.
2534
+ * @throws when continuation services are unavailable, sender authorization
2535
+ * fails, or the direct parent is not live.
2536
+ */
2537
+ async reportFrom(child, content, options) {
2538
+ return this.requireContinuations().reportFrom(child, content, options);
2539
+ }
2540
+ /**
2541
+ * Compose one deployment capability into every continuable child's
2542
+ * unpublished creation context on fresh creation and cold resume. Grants wait
2543
+ * for the next Activation; removing the contribution revokes every resident
2544
+ * installation immediately.
2545
+ * @param contribution - synchronous child-scope installer.
2546
+ * @returns the exact Cordis effect disposer.
2547
+ */
2548
+ registerContinuableSetup(contribution) {
2549
+ return this.ctx.effect(() => this.setupRegistry.register(contribution), "subagents.registerContinuableSetup()");
2550
+ }
2551
+ /**
2552
+ * Close continuable admission below exact live parent Agents, stop only their
2553
+ * visible descendant Activations synchronously, then await admitted scoped
2554
+ * materializations and release those forests child-first. The scoped cutoff
2555
+ * lasts until each exact parent leaves the registry; unrelated parent trees
2556
+ * remain live.
2557
+ * @param parents - exact host-owned parent Agents entering teardown.
2558
+ * @returns once every retained descendant Activation released its `AgentHandle`.
2559
+ * @throws an aggregate error after all branches settle when any failed.
2560
+ */
2561
+ async drainContinuableDescendants(parents) {
2562
+ const manager = this.continuations;
2563
+ if (manager === void 0) return;
2564
+ await manager.drainDescendants(parents);
2565
+ }
2566
+ /**
2567
+ * Release selected resident continuable direct children of one exact live
2568
+ * parent. Other children of the same parent remain admitted and resident.
2569
+ * Absent targets and a manager-less composition are accepted no-ops.
2570
+ * @param parent - exact live direct parent authorizing the selected release.
2571
+ * @param childIds - durable direct-child ids to release when resident.
2572
+ * @returns once every selected Activation released its `AgentHandle`.
2573
+ * @throws {SubagentError} `UNAUTHORIZED` when a resident target belongs to a
2574
+ * different parent or the supplied parent identity is stale.
2575
+ */
2576
+ async drainContinuableChildren(parent, childIds) {
2577
+ const manager = this.continuations;
2578
+ if (manager === void 0) return;
2579
+ await manager.drainChildren(parent, childIds);
2580
+ }
2581
+ /**
2582
+ * Enumerate the parent's direct session-backed subagents without loading or
2583
+ * resuming an Agent and without any query service: the listing merges the live
2584
+ * session store with optional session persistence (live-preferred) and
2585
+ * serves each child's durable mode/label from the registered `subagent`
2586
+ * projection unit down a three-rung ladder — the registry's watermark
2587
+ * snapshot for a live child; for a cold one, a durable projection-cache
2588
+ * row when the optional cache serves an own-suffix identity (its `seq`
2589
+ * gate proves the value postdates the fork seed, where a child's own
2590
+ * descriptor is immutable once appended), else one persistence inspection
2591
+ * folded through the registry. The
2592
+ * projection fold is the single classification authority; per-child
2593
+ * diagnostics relay a fold that served no identity or a failed inspection,
2594
+ * never a list-time descriptor parse. Absent persistence, enumeration is
2595
+ * live-only (a cold child cannot be resumed then either, so its absence is
2596
+ * capability absence, not an error). This service consults no Agent
2597
+ * registrations, Activations, or providers.
2598
+ *
2599
+ * Every persistence read receives `signal`, and the listing rechecks
2600
+ * cancellation around each of those awaits. Read rejections that settle
2601
+ * after an abort become a stable `SubagentError` with code `CANCELLED`.
2602
+ * @param parentSessionId - parent session whose direct children are listed.
2603
+ * @param signal - caller-owned cancellation forwarded to persistence reads
2604
+ * and observed around every read await.
2605
+ * @returns children and per-child diagnostics ordered by `createdAt`, then id.
2606
+ * @throws {@link SubagentError} when the projection registry or the session
2607
+ * store is not mounted, or the caller cancels the listing.
2608
+ */
2609
+ listChildren(parentSessionId, signal) {
2610
+ return listChildren(this.ctx, parentSessionId, signal);
2611
+ }
2612
+ /**
2613
+ * Enumerate the root's complete session-backed subagent tree in stable
2614
+ * pre-order from one live-preferred corpus, without loading or resuming an
2615
+ * Agent. Ordinary sessions and one-shot children remain traversal nodes so
2616
+ * continuable descendants below them are discovered; each returned entry
2617
+ * adds its durable `parentId` and root-relative `depth`. Identity resolution,
2618
+ * diagnostics, optional persistence, and cancellation follow the same
2619
+ * projection-backed contract as {@link listChildren}.
2620
+ * @param rootSessionId - session whose complete descendant tree is listed.
2621
+ * @param signal - caller-owned cancellation forwarded to persistence reads
2622
+ * and observed around every read await.
2623
+ * @returns children and per-candidate diagnostics with tree position, in
2624
+ * stable pre-order.
2625
+ * @throws {@link SubagentError} under the same conditions as {@link listChildren}.
2626
+ */
2627
+ listDescendants(rootSessionId, signal) {
2628
+ return listDescendants(this.ctx, rootSessionId, signal);
2629
+ }
2630
+ /**
2631
+ * Register a provider under its name. Registration is effect-scoped and HMR
2632
+ * safe; removing a provider blocks new starts but does not revoke runs that
2633
+ * were already returned to their holders.
2634
+ * @param provider - the trusted provider implementation.
2635
+ * @returns the exact Cordis effect disposer.
2636
+ */
2637
+ registerProvider(provider) {
2638
+ const name = provider.name;
2639
+ return this.ctx.effect(function* () {
2640
+ if (this.providers.has(name)) throw new SubagentError(`a subagent provider named "${name}" is already registered`, "DUPLICATE_PROVIDER");
2641
+ this.providers.set(name, provider);
2642
+ yield () => {
2643
+ this.providers.delete(name);
2644
+ this.emitLifecycle("subagent/provider-removed", name);
2645
+ };
2646
+ this.ctx.emit("subagent/provider-added", provider);
2647
+ }.bind(this), "subagents.registerProvider()");
2648
+ }
2649
+ /**
2650
+ * Look up a provider by name.
2651
+ * @param name - the provider name.
2652
+ * @returns the provider, or undefined when absent.
2653
+ */
2654
+ getProvider(name) {
2655
+ return this.providers.get(name);
2656
+ }
2657
+ /**
2658
+ * List registered provider names in insertion order.
2659
+ * @returns the registered names.
2660
+ */
2661
+ list() {
2662
+ return [...this.providers.keys()];
2663
+ }
2664
+ /**
2665
+ * Establish a published child on the named provider. Capability and semantic
2666
+ * checks run before delegation. Provider ownership lasts until its promise
2667
+ * fulfills; a rejection therefore has no run for the caller to dispose and
2668
+ * emits no run lifecycle events. Post-publication turn and infrastructure
2669
+ * failures settle through the returned run.
2670
+ * @param name - the provider to use.
2671
+ * @param request - child label, prompt, parent, signal, and optional capabilities.
2672
+ * @returns the published holder-owned run.
2673
+ */
2674
+ async start(name, request) {
2675
+ const provider = this.expectProvider(name);
2676
+ this.assertCapabilities(provider, request);
2677
+ assertSubagentMaxDepth(request.maxDepth);
2678
+ if (request.outputSchema !== void 0) assertObjectJsonSchema(request.outputSchema);
2679
+ const descriptor = snapshotSubagentDescriptor({
2680
+ mode: "one-shot",
2681
+ provider: name,
2682
+ ...request.label !== void 0 ? { label: request.label } : {}
2683
+ });
2684
+ const resolved = {
2685
+ ...request,
2686
+ descriptor
2687
+ };
2688
+ const release = this.admit(request.parent, true);
2689
+ try {
2690
+ const run = await provider.start(resolved);
2691
+ run.result.then(release, release);
2692
+ return observeRun(this.emitLifecycle, name, request.parent, run);
2693
+ } catch (error) {
2694
+ release();
2695
+ throw error;
2696
+ }
2697
+ }
2698
+ /**
2699
+ * Resolve one provider's detached continuable-creation contribution. Method
2700
+ * presence on the provider IS the capability, so a provider without it is
2701
+ * rejected before the manager reserves any child resources.
2702
+ */
2703
+ async prepareContinuable(name, request) {
2704
+ const provider = this.expectProvider(name);
2705
+ if (provider.prepareContinuable === void 0) throw new SubagentError(`subagent provider "${provider.name}" does not support continuable children (no prepareContinuable capability)`, "UNSUPPORTED_CAPABILITY");
2706
+ return provider.prepareContinuable(request);
2707
+ }
2708
+ /** Look up a provider for dispatch or fail loud. */
2709
+ expectProvider(name) {
2710
+ const provider = this.providers.get(name);
2711
+ if (provider === void 0) throw new SubagentError(`no subagent provider registered for "${name}"`, "NO_PROVIDER");
2712
+ return provider;
2713
+ }
2714
+ /** Resolve the optional continuable-subagent manager or fail loud. */
2715
+ requireContinuations() {
2716
+ if (this.continuations === void 0) throw new SubagentError("continuable subagents require the agents service", "CONTINUATION_UNAVAILABLE");
2717
+ return this.continuations;
2718
+ }
2719
+ /**
2720
+ * Build the lifecycle observer for one continuable Activation's residency
2721
+ * epoch, so the manager publishes its edges without owning event dispatch.
2722
+ */
2723
+ observeActivation(provider, childId, parent) {
2724
+ return createActivationObserver(this.emitLifecycle, provider, childId, parent);
2725
+ }
2726
+ /** Reject the first requested capability that the provider lacks. */
2727
+ assertCapabilities(provider, request) {
2728
+ const needs = [
2729
+ {
2730
+ when: request.outputSchema !== void 0,
2731
+ cap: "outputSchema"
2732
+ },
2733
+ {
2734
+ when: request.maxDepth !== void 0,
2735
+ cap: "depthLimit"
2736
+ },
2737
+ {
2738
+ when: request.toolFilter !== void 0,
2739
+ cap: "toolFilter"
2740
+ },
2741
+ {
2742
+ when: request.persona !== void 0,
2743
+ cap: "persona"
2744
+ }
2745
+ ];
2746
+ for (const { when, cap } of needs) if (when && !provider.capabilities[cap]) throw new SubagentError(`subagent provider "${provider.name}" does not support the "${cap}" capability`, "UNSUPPORTED_CAPABILITY");
2747
+ }
2748
+ };
2749
+ //#endregion
2750
+ export { AssistantOutputFold, NO_START_CAPABILITIES, SUBAGENT_DESCRIPTOR_VERSION, SubagentDepthError, SubagentError, SubagentRunId, SubagentRuntime, SubagentRuntime as default, appendDelegatedPolicyOverrides, applyChildComposition, assertPositiveFinite, assertSubagentMaxDepth, assertUsableCwd, captureDelegatedPolicyOverrides, childSessionMeta, delegationDepthOf, delegationRoot, finalAssistantOutput, foldSubagentDescriptor, resolveChildAgentOptions, resolveChildCwd, resolveChildDepth, seedDescriptorTurn, settleRun, settleRunResult, snapshotSubagentDescriptor, subprocessRunHandle, validateConfiguredCwd };