@deepseek-ai/dsh-subagent 0.0.1-rc.1

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