@deepseek-ai/dsh-subagent 0.2.0-rc.2 → 0.2.1-alpha.2

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 (44) hide show
  1. package/README.i18n.yaml +36 -36
  2. package/README.md +33 -27
  3. package/README.zh.md +33 -27
  4. package/lib/index.js +1436 -1304
  5. package/lib/typert.host.js +73 -60
  6. package/lib/typert.remote-client.js +2 -2
  7. package/lib/types/activation.d.ts +114 -0
  8. package/lib/types/activation.js +112 -0
  9. package/lib/types/catalog.d.ts +14 -6
  10. package/lib/types/catalog.js +34 -30
  11. package/lib/types/child-agent.d.ts +1 -3
  12. package/lib/types/child-agent.js +1 -3
  13. package/lib/types/continuation-messages.d.ts +4 -3
  14. package/lib/types/continuation-messages.js +13 -5
  15. package/lib/types/control-types.d.ts +7 -0
  16. package/lib/types/descriptor.d.ts +7 -46
  17. package/lib/types/descriptor.js +20 -21
  18. package/lib/types/index.d.ts +52 -53
  19. package/lib/types/index.js +75 -95
  20. package/lib/types/internal.d.ts +1 -1
  21. package/lib/types/lifecycle.d.ts +28 -58
  22. package/lib/types/lifecycle.js +9 -74
  23. package/lib/types/list-children.js +1 -1
  24. package/lib/types/manager.d.ts +247 -0
  25. package/lib/types/manager.js +1294 -0
  26. package/lib/types/out-of-process.d.ts +2 -28
  27. package/lib/types/out-of-process.js +3 -44
  28. package/lib/types/projection-types.d.ts +3 -0
  29. package/lib/types/structured.d.ts +49 -0
  30. package/lib/types/structured.js +130 -0
  31. package/lib/types/types.d.ts +57 -81
  32. package/lib/types/types.js +1 -1
  33. package/package.json +53 -63
  34. package/lib/invariant.js +0 -76
  35. package/lib/types/continuation-activation.d.ts +0 -267
  36. package/lib/types/continuation-activation.js +0 -700
  37. package/lib/types/continuation.d.ts +0 -124
  38. package/lib/types/continuation.js +0 -455
  39. package/lib/types/inbox.d.ts +0 -43
  40. package/lib/types/inbox.js +0 -61
  41. package/lib/types/invariant.d.ts +0 -13
  42. package/lib/types/invariant.js +0 -91
  43. package/lib/types/run-settlement.d.ts +0 -17
  44. package/lib/types/run-settlement.js +0 -70
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
2
  import { scopeTarget } from "@deepseek-ai/dsh-scope";
3
- import { assertObjectJsonSchema } from "@deepseek-ai/dsh-tools";
3
+ import { ToolArgsError, assertObjectJsonSchema, validateJsonSchemaValue } from "@deepseek-ai/dsh-tools";
4
4
  import { canonicalClientTimeZone } from "@deepseek-ai/dsh-util-time";
5
5
  import { Remote, RemoteError, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
6
6
  import { AttachmentError } from "@deepseek-ai/dsh-attachment";
@@ -8,12 +8,12 @@ import { z as z$1 } from "zod";
8
8
  import { HarnessError, ReasoningEffortId, boundContextSummary, contentHasImage, createUserMessage, errorChain, joinAssistantStreamText } from "@deepseek-ai/dsh-llm";
9
9
  import { randomUUID } from "node:crypto";
10
10
  import { foldConsumedWork } from "@deepseek-ai/dsh-agent";
11
- import { SessionLogOffset, SessionSeq } from "@deepseek-ai/dsh-session";
11
+ import { isAbsolute, resolve } from "node:path";
12
12
  import { brandString } from "@deepseek-ai/dsh-brand";
13
- import { snapshotJsonValue } from "@deepseek-ai/dsh-util-values";
13
+ import { SessionLogOffset, SessionSeq } from "@deepseek-ai/dsh-session";
14
+ import { assertNever, snapshotJsonValue } from "@deepseek-ai/dsh-util-values";
14
15
  import { appendChunkedList, chunkedListSchema, iterateChunkedList } from "@deepseek-ai/dsh-chunked-list";
15
16
  import { accessSync, constants, statSync } from "node:fs";
16
- import { isAbsolute, resolve } from "node:path";
17
17
  //#region lib/types/error.js
18
18
  /**
19
19
  * Typed failures shared by subagent service and provider operations.
@@ -121,79 +121,13 @@ function assertSubagentMaxDepth(maxDepth) {
121
121
  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");
122
122
  }
123
123
  //#endregion
124
- //#region lib/types/assistant-output.js
125
- /**
126
- * Canonical selection of a child's final assistant output. Backend run results
127
- * and `subagent/end.lastAssistantMessage` apply the same rule: select the last
128
- * non-empty assistant message. An empty-content message records usage only
129
- * when the loop appends it after a max-tokens step with no executable blocks,
130
- * so it does not replace earlier output. If no non-empty message exists,
131
- * select the accumulated assistant text. Selection is independent of the
132
- * run's stop reason.
133
- *
134
- * @module @deepseek-ai/dsh-subagent/assistant-output
135
- */
136
- /**
137
- * Incremental fold of the selection rule, for backends that observe a child's
138
- * output as it streams: session-event backends {@link push} each event, and
139
- * transports without session events (ACP content chunks) {@link pushText} raw
140
- * text into the same streamed fallback.
141
- */
142
- var AssistantOutputFold = class {
143
- message;
144
- partial = [];
145
- /**
146
- * Fold one session event: a non-empty assistant message becomes the
147
- * candidate final answer, while its embedded stream and any log-only attempt
148
- * extend the streamed fallback; every other event contributes nothing.
149
- * @param event - the next observed session event.
150
- */
151
- push(event) {
152
- if (event.type === "assistant/message") {
153
- const content = event.data.message.content;
154
- if (content.length > 0) this.message = content;
155
- }
156
- if (event.type === "assistant/message" || event.type === "assistant/attempt") this.pushText(joinAssistantStreamText(event.data.stream));
157
- }
158
- /**
159
- * Extend the streamed fallback with text observed outside session events.
160
- * @param text - the next streamed text piece (an empty piece is a no-op).
161
- */
162
- pushText(text) {
163
- if (text.length > 0) this.partial.push(text);
164
- }
165
- /**
166
- * Select the final output folded so far.
167
- * @returns the last non-empty assistant message, else the accumulated
168
- * streamed text, or `undefined` when the child produced neither.
169
- */
170
- collect() {
171
- if (this.message !== void 0) return this.message;
172
- const text = this.partial.join("");
173
- return text.length > 0 ? [{
174
- type: "text",
175
- text
176
- }] : void 0;
177
- }
178
- };
179
- /**
180
- * Apply the selection rule to one complete child-owned event suffix.
181
- * @param events - the child-owned events (after any seed or epoch boundary).
182
- * @returns the selected output, or `undefined` when the child produced none.
183
- */
184
- function finalAssistantOutput(events) {
185
- const fold = new AssistantOutputFold();
186
- for (const event of events) fold.push(event);
187
- return fold.collect();
188
- }
189
- //#endregion
190
124
  //#region lib/types/types.js
191
125
  /**
192
126
  * The seam's consumer-facing contracts: request, result, and capability types
193
127
  * for {@link SubagentProvider}, plus the `subagent/start` and `subagent/end`
194
128
  * payloads that plugins and hosts observe. Internal control interfaces belong
195
129
  * with their implementation — the lifecycle observer in `./lifecycle.ts`, the
196
- * continuation host in `./continuation.ts` — so this module stays the published
130
+ * manager host in `./manager.ts` — so this module stays the published
197
131
  * surface rather than a bag of everything type-shaped.
198
132
  *
199
133
  * @module @deepseek-ai/dsh-subagent/types
@@ -209,17 +143,10 @@ function SubagentRunId(id) {
209
143
  //#endregion
210
144
  //#region lib/types/lifecycle.js
211
145
  /**
212
- * Lifecycle-edge publication for both subagent shapes: the contained emitter,
213
- * the one-shot run observer, and the continuable Activation observer.
146
+ * Lifecycle-edge publication for managed subagent activations.
214
147
  *
215
- * The public payload contracts ({@link SubagentRunInfo},
216
- * {@link SubagentRunEndInfo}) live in `./types.ts` with the rest of the seam's
217
- * consumer-facing types; this module owns only the implementation and the
218
- * package-private {@link ActivationObserver} the continuation manager consumes.
219
- * Keeping the internal control interface out of the published surface is
220
- * deliberate: the observer's `start`/`capture`/`settle` ordering is a contract
221
- * between this module and one in-package caller, not something a plugin may
222
- * depend on.
148
+ * Public event payloads live in `./types.ts`; the package-private observer
149
+ * retains only the identity shared by each start/end pair.
223
150
  *
224
151
  * @module @deepseek-ai/dsh-subagent/lifecycle
225
152
  */
@@ -250,40 +177,7 @@ function createLifecycleEmitter(ctx, carrier) {
250
177
  };
251
178
  }
252
179
  /**
253
- * Emit the start/end lifecycle pair for one accepted one-shot run.
254
- * @param emit - the contained lifecycle emitter.
255
- * @param provider - the provider that established the run.
256
- * @param parent - the delegating parent keying scoped dispatch.
257
- * @param run - the published run whose settlement closes the pair.
258
- * @returns the same run, unchanged.
259
- */
260
- function observeRun(emit, provider, parent, run) {
261
- const identity = {
262
- runId: SubagentRunId(randomUUID()),
263
- provider,
264
- id: run.id,
265
- local: run.localAgent !== void 0
266
- };
267
- run.result.then((result) => {
268
- emit("subagent/end", {
269
- ...identity,
270
- stopReason: result.stopReason,
271
- ...result.output.length === 0 ? {} : { lastAssistantMessage: result.output }
272
- }, parent);
273
- }, () => {
274
- emit("subagent/end", {
275
- ...identity,
276
- stopReason: "error"
277
- }, parent);
278
- });
279
- emit("subagent/start", identity, parent);
280
- return run;
281
- }
282
- /**
283
- * Build the observer for one continuable Activation's residency epoch. Observers
284
- * see the same vocabulary as a one-shot run, so a child's start and settlement
285
- * remain observable without exposing whether the manager materialized, woke, or
286
- * cold-resumed it. Creation failure before residency emits no lifecycle edge.
180
+ * Build the observer for one local or external activation's residency epoch. Creation failure before residency emits no lifecycle edge.
287
181
  * @param emit - the contained lifecycle emitter.
288
182
  * @param provider - the provider name recorded in the durable descriptor.
289
183
  * @param childId - the durable child session id.
@@ -295,31 +189,18 @@ function createActivationObserver(emit, provider, childId, parent) {
295
189
  runId: SubagentRunId(randomUUID()),
296
190
  provider,
297
191
  id: childId,
298
- local: true
192
+ local: false
299
193
  };
300
- let boundary = SessionLogOffset(0);
301
- let captured = { stopReason: "completed" };
302
- const terminal = (failure) => failure === void 0 ? captured : { stopReason: "error" };
303
194
  return {
304
195
  start: (child) => {
305
- boundary = child.session.seq;
196
+ identity.local = child !== void 0;
306
197
  emit("subagent/start", identity, parent);
307
198
  },
308
- capture: (child) => {
309
- const own = child.session.snapshotEvents(boundary);
310
- const output = finalAssistantOutput(own);
311
- captured = {
312
- stopReason: epochStopReason(own),
313
- ...output === void 0 ? {} : { output }
314
- };
315
- },
316
- terminal,
317
- settle: (failure) => {
318
- const { stopReason, output } = terminal(failure);
199
+ settle: ({ stopReason, output }) => {
319
200
  emit("subagent/end", {
320
201
  ...identity,
321
202
  stopReason,
322
- ...output === void 0 ? {} : { lastAssistantMessage: output }
203
+ ...output.length === 0 ? {} : { lastAssistantMessage: output }
323
204
  }, parent);
324
205
  }
325
206
  };
@@ -370,9 +251,7 @@ function renderThrown(value) {
370
251
  /**
371
252
  * Shared in-process child composition: the delegation-depth budget, the
372
253
  * durable session metadata, the resolved child `AgentOptions`, the delegated
373
- * policy seed, and the scoped setup a child agent needs. Both the one-shot
374
- * provider driver and the continuation manager compose children this way, so
375
- * depth accounting, lineage stamping, and delegation policy have one home.
254
+ * policy seed, and the scoped setup used by local activations.
376
255
  *
377
256
  * @module @deepseek-ai/dsh-subagent/child-agent
378
257
  */
@@ -608,12 +487,13 @@ function withContinuableReturnGuidance(parentId, prompt) {
608
487
  * the parent's own task vocabulary.
609
488
  * @param childId - the durable child the parent knows by id.
610
489
  * @param stopReason - how the child's last ordinary turn ended.
490
+ * @param continuable - whether the child accepts a later message.
611
491
  * @returns the model-facing opening line of the settlement notice.
612
492
  */
613
- function settlementSummary(childId, stopReason) {
493
+ function settlementSummary(childId, stopReason, continuable) {
614
494
  const subject = `Background subagent ${childId}`;
615
495
  switch (stopReason) {
616
- case "completed": return `${subject} finished and will do no further work unless you send it more.`;
496
+ case "completed": return continuable ? `${subject} finished and will do no further work unless you send it more.` : `${subject} finished. It cannot receive follow-up messages.`;
617
497
  case "aborted": return `${subject} was stopped before it finished.`;
618
498
  case "max-tokens": return `${subject} ran out of room before it finished.`;
619
499
  case "refusal": return `${subject} declined the task.`;
@@ -628,22 +508,34 @@ function settlementSummary(childId, stopReason) {
628
508
  * Build the runtime-owned settlement notice from the child's nonempty closing text.
629
509
  * @param childId - durable child session id named in the notice.
630
510
  * @param terminal - recorded terminal state for the settled Activation.
511
+ * @param continuable - whether the child can accept another task.
631
512
  * @returns the durable user-message representation delivered to the parent.
632
513
  */
633
- function createSettlementMessage(childId, terminal) {
634
- const summary = settlementSummary(childId, terminal.stopReason);
635
- const closingText = (terminal.output ?? []).flatMap((block) => block.type === "text" && block.text.length > 0 ? [block] : []);
514
+ function createSettlementMessage(childId, terminal, continuable = true) {
515
+ const summary = settlementSummary(childId, terminal.stopReason, continuable);
516
+ const closingText = terminal.output.flatMap((block) => block.type === "text" && block.text.length > 0 ? [block] : []);
636
517
  return createUserMessage({
637
- content: [{
638
- type: "text",
639
- text: summary
640
- }, ...closingText.length === 0 ? [{
641
- type: "text",
642
- text: "It left no closing message."
643
- }] : [{
644
- type: "text",
645
- text: "Its closing message:"
646
- }, ...closingText]],
518
+ content: [
519
+ {
520
+ type: "text",
521
+ text: summary
522
+ },
523
+ ...closingText.length === 0 ? [{
524
+ type: "text",
525
+ text: "It left no closing message."
526
+ }] : [{
527
+ type: "text",
528
+ text: "Its closing message:"
529
+ }, ...closingText],
530
+ ...terminal.structured !== void 0 ? [{
531
+ type: "text",
532
+ text: `Structured result: ${JSON.stringify(terminal.structured)}`
533
+ }] : [],
534
+ ...terminal.diagnostic !== void 0 ? [{
535
+ type: "text",
536
+ text: terminal.diagnostic
537
+ }] : []
538
+ ],
647
539
  source: {
648
540
  kind: "subagent-settled",
649
541
  form: "notice",
@@ -653,731 +545,107 @@ function createSettlementMessage(childId, terminal) {
653
545
  });
654
546
  }
655
547
  //#endregion
656
- //#region lib/types/inbox.js
548
+ //#region lib/types/descriptor.js
657
549
  /**
658
- * Activation-local admission around one continuable subagent's Agent inbox.
550
+ * The durable subagent-child descriptor: the versioned, model-hidden
551
+ * `subagent/descriptor` session event that identifies every session-backed
552
+ * subagent, including historical one-shot records. The manager appends each
553
+ * continuable child's descriptor inside its initial turn, preserving the
554
+ * declared composition required for cold resume.
555
+ *
556
+ * The descriptor deliberately snapshots explicit fields rather than the
557
+ * merge-extensible `AgentOptions` object: an unrelated extension value cannot
558
+ * make continuation fail merely because it is not JSON, and later composition
559
+ * inputs require a deliberate {@link SUBAGENT_DESCRIPTOR_VERSION} change. It
560
+ * omits `subagentDepth` — cold resume trusts the persisted header's
561
+ * `delegationDepth` as the monotone floor — and `outputSchema`, which belongs
562
+ * to one activation's result contract rather than durable child composition.
563
+ * Per-activation knobs such as `maxTokens` are omitted for the same reason as
564
+ * `outputSchema`: they budget one activation. Cold resume requires the exact
565
+ * live parent for authorization but reconstructs child options only from the
566
+ * durable descriptor, so it neither restores the prior budget nor inherits
567
+ * the parent's current one; the resumed route's defaults apply instead.
659
568
  *
660
- * @module @deepseek-ai/dsh-subagent/inbox
569
+ * @module @deepseek-ai/dsh-subagent/descriptor
661
570
  */
662
- /** Delegate Queue and Steer to one live Agent until its Activation starts closing. */
663
- var SubagentInbox = class {
664
- agent;
665
- closingPromise;
666
- /**
667
- * Wrap one live continuable Agent.
668
- * @param agent - the Agent whose inbox receives accepted deliveries.
669
- */
670
- constructor(agent) {
671
- this.agent = agent;
672
- }
673
- /**
674
- * Read the Activation's close transaction.
675
- * @returns the memoized transaction, or `undefined` while delivery remains open.
676
- */
677
- get closing() {
678
- return this.closingPromise;
679
- }
680
- /**
681
- * Read whether the underlying Agent still has accepted work to claim.
682
- * @returns whether either Agent inbox destination is non-empty.
683
- */
684
- get hasPending() {
685
- return this.agent.inbox.nextTurn.length > 0 || this.agent.inbox.nextStep.length > 0;
686
- }
687
- /**
688
- * Submit through the Agent only while its Activation remains resident.
689
- * @param message - the accepted input to submit.
690
- * @param delivery - whether to queue a distinct turn or steer the nearest step.
691
- */
692
- deliver(message, delivery) {
693
- if (this.closingPromise !== void 0) throw new SubagentError(`subagent "${this.agent.id}" activation is being disposed; the message was not accepted`, "ACTIVATION_CLOSING");
694
- if (delivery === "steer") this.agent.steer(message);
695
- else this.agent.followup(message);
696
- }
697
- /**
698
- * Close delivery synchronously and share one asynchronous release.
699
- * @param release - the one release operation to start after closing admission.
700
- * @returns the memoized release transaction.
701
- */
702
- close(release) {
703
- const existing = this.closingPromise;
704
- if (existing !== void 0) return existing;
705
- const completion = Promise.withResolvers();
706
- this.closingPromise = completion.promise;
707
- release().then(completion.resolve, completion.reject);
708
- return completion.promise;
709
- }
710
- };
711
- //#endregion
712
- //#region lib/types/continuation-activation.js
713
571
  /**
714
- * Process-local Activation ownership for continuable subagents: admission,
715
- * parent-child residency, serialized delivery, settlement, and disposal.
716
- *
717
- * The continuation manager owns durable request orchestration and delegates
718
- * every mutable residency decision to this registry, so delivery and teardown
719
- * share one child lock and one Activation map.
720
- *
721
- * @module @deepseek-ai/dsh-subagent/continuation-activation
572
+ * The current descriptor format version, stamped into every appended
573
+ * `subagent/descriptor` event and required verbatim by {@link foldSubagentDescriptor}.
574
+ * Supporting another composition input is a deliberate version change, never
575
+ * an implicit extra field.
722
576
  */
723
- /** Process-local slots shared through uninterrupted continuable parent links. */
724
- var ActivationPool = class {
725
- slots = /* @__PURE__ */ new Set();
726
- /** Reserve before reconstruction; the returned release also tolerates unpublished rollback. */
727
- reserve(capacity) {
728
- if (this.slots.size >= capacity) throw new SubagentError(`subagent limit reached (active child limit: ${capacity}); wait for an existing child to finish or complete this work with the current agents`, "ACTIVATION_LIMIT_REACHED");
729
- const slot = Symbol();
730
- this.slots.add(slot);
731
- return () => {
732
- this.slots.delete(slot);
733
- };
734
- }
735
- };
736
- /** Serialize each durable child's delivery, release, and disposal. */
737
- var ChildLock = class {
738
- tails = /* @__PURE__ */ new Map();
739
- /**
740
- * Run `operation` after every previously queued operation for `childId`.
741
- * @param childId - the durable child whose operations are linearized.
742
- * @param operation - the critical section to run in order.
743
- * @returns the operation's own settlement.
744
- */
745
- run(childId, operation) {
746
- const result = (this.tails.get(childId) ?? Promise.resolve()).then(operation, operation);
747
- const tail = result.then(() => void 0, () => void 0);
748
- this.tails.set(childId, tail);
749
- tail.then(() => {
750
- if (this.tails.get(childId) === tail) this.tails.delete(childId);
751
- });
752
- return result;
753
- }
754
- };
755
- /** Own the complete process-local lifetime of continuable child Activations. */
756
- var ContinuableActivationRegistry = class {
757
- ctx;
758
- observeActivation;
759
- maxActiveSubagents;
760
- /** Child session id → its live Activation. Process-local, never durable. */
761
- resident = /* @__PURE__ */ new Map();
762
- /** Root identities retain their pool across child settlement without retaining dead roots. */
763
- rootPools = /* @__PURE__ */ new WeakMap();
764
- /** Materializations admitted before drain, tracked through publication or rollback. */
765
- materializations = /* @__PURE__ */ new Set();
766
- /** Per-child serializer shared by delivery, release, and disposal. */
767
- locks = new ChildLock();
768
- /** Structural Cordis owner of every Activation handle. */
769
- ownerCtx;
770
- /**
771
- * Exact roots whose host teardown has begun, with the live lineage members
772
- * observed under each root. Entries remain until that exact root leaves the
773
- * Agent registry, closing admission throughout its host's teardown without
774
- * poisoning a later same-id replacement.
775
- */
776
- closingScopes = /* @__PURE__ */ new Map();
777
- draining = false;
778
- /**
779
- * Build one registry inside the service's Agent-injected context.
780
- * @param ctx - context providing Agents, Sessions, and teardown ownership.
781
- * @param observeActivation - build the lifecycle observer for one residency epoch.
782
- */
783
- constructor(ctx, observeActivation, maxActiveSubagents) {
784
- this.ctx = ctx;
785
- this.observeActivation = observeActivation;
786
- this.maxActiveSubagents = maxActiveSubagents;
787
- const scope = ctx.plugin(function activationOwner() {});
788
- this.ownerCtx = scope.ctx;
789
- ctx.on("agent/disposed", ({ agent }) => {
790
- this.closingScopes.delete(agent);
791
- });
792
- ctx.effect(function* () {
793
- yield scope.dispose;
794
- yield () => this.drain();
795
- }.bind(this), "subagents.continuations()");
796
- }
797
- /**
798
- * Return the live Activation for a durable child id, if resident.
799
- * @param childId - durable child session id to look up.
800
- * @returns the process-local Activation, or `undefined` when it is not resident.
801
- */
802
- get(childId) {
803
- return this.resident.get(childId);
804
- }
805
- /**
806
- * Reject one child identity already owned by a live Agent or Session.
807
- * @param childId - proposed durable child session id.
808
- */
809
- assertChildIdAvailable(childId) {
810
- if (this.ctx.agents.get(childId) !== void 0 || this.ctx.get("sessions")?.get(childId) !== void 0) throw new SubagentError(`subagent "${childId}" already exists`, "DUPLICATE_CHILD");
811
- }
812
- /**
813
- * Pre-register `childId` in a continuation-managed parent's owned set so the
814
- * parent cannot settle while a caller is still establishing or resuming that
815
- * child. Returns a releaser for the failure path; it removes only a hold
816
- * this call added, and leaves ownership in place once a live Activation for
817
- * the child exists.
818
- * @param parent - the live direct parent the operation is admitted under.
819
- * @param childId - the durable child the operation addresses.
820
- * @returns the failure-path releaser; a no-op when nothing was added.
821
- */
822
- holdOwnership(parent, childId) {
823
- const parentActivation = this.resident.get(parent.id);
824
- if (parentActivation === void 0 || parentActivation.handle.agent !== parent) return () => {};
825
- if (parentActivation.inbox.closing !== void 0) throw new SubagentError(`subagent parent "${parent.id}" is being disposed; the child was not established`, "ACTIVATION_CLOSING");
826
- if (parentActivation.ownedChildren.has(childId)) return () => {};
827
- parentActivation.ownedChildren.add(childId);
828
- return () => {
829
- const live = this.resident.get(childId);
830
- /* v8 ignore next 4 -- reaching this arm needs another delivery to establish the child
831
- * between this operation's failure and its releaser running, which no test can schedule
832
- * deterministically: the ownership edge then belongs to that live Activation, so the
833
- * conservative keep leaves it for finishDisposal's releaseOwnership. */
834
- if (live !== void 0 && live.inbox.closing === void 0) return;
835
- if (parentActivation.ownedChildren.delete(childId)) this.wake(parentActivation);
836
- };
837
- }
838
- /**
839
- * Interrupt one live continuable child's current turn under the supplied authority.
840
- * @param targetSessionId - the durable child session id to interrupt.
841
- * @param authority - the human parent address or exact live ancestor Agent.
842
- */
843
- interrupt(targetSessionId, authority) {
844
- if (authority.kind === "ancestor") {
845
- const caller = authority.agent;
846
- if (this.ctx.agents.get(caller.id) !== caller) throw new SubagentError(`interrupting "${targetSessionId}" requires the exact live ancestor agent`, "UNAUTHORIZED");
847
- if (caller.id === targetSessionId) throw new SubagentError(`agent "${caller.id}" cannot interrupt itself`, "UNAUTHORIZED");
848
- }
849
- const activation = this.resident.get(targetSessionId);
850
- if (activation === void 0) return;
851
- if (authority.kind === "user") {
852
- if (activation.handle.agent.session.header.parentSession !== authority.parentSessionId) throw new SubagentError(`subagent "${targetSessionId}" belongs to another parent session`, "UNAUTHORIZED");
853
- } else if (!activation.ancestry.has(authority.agent)) throw new SubagentError(`subagent "${targetSessionId}" is not a live descendant of agent "${authority.agent.id}"`, "UNAUTHORIZED");
854
- if (activation.inbox.closing !== void 0) return;
855
- activation.handle.agent.cancel(authority.kind === "user" ? { kind: "user" } : { kind: "parent" }, { keepInbox: true });
856
- }
857
- /**
858
- * Send through a receiving parent's Activation inbox when it has one.
859
- * @param parent - exact live Agent receiving the message.
860
- * @param message - durable user message to deliver.
861
- * @param delivery - receiving inbox destination.
862
- */
863
- sendWaking(parent, message, delivery) {
864
- const parentActivation = this.resident.get(parent.id);
865
- if (parentActivation !== void 0 && parentActivation.handle.agent === parent) {
866
- try {
867
- parentActivation.inbox.deliver(message, delivery);
868
- } finally {
869
- this.wake(parentActivation);
870
- }
871
- return;
872
- }
873
- if (delivery === "steer") parent.steer(message);
874
- else parent.followup(message);
875
- }
876
- /**
877
- * Close admission, await every already-admitted materialization through
878
- * publication or rollback, then dispose the stable live Activation graph
879
- * child-first.
880
- */
881
- async drain() {
882
- this.draining = true;
883
- await Promise.all([...this.materializations].map((materialization) => materialization.settled));
884
- const owned = /* @__PURE__ */ new Set();
885
- for (const activation of this.resident.values()) for (const child of activation.ownedChildren) owned.add(child);
886
- const roots = [...this.resident.values()].filter((activation) => !owned.has(activation.childId));
887
- await this.disposeRoots(roots, "activation(s)");
888
- }
889
- /**
890
- * Stop only the continuable descendants of exact live host-owned parents.
891
- * @param parents - exact live roots whose continuable descendants must stop.
892
- */
893
- async drainDescendants(parents) {
894
- const roots = new Set(parents.filter((parent) => this.ctx.agents.get(parent.id) === parent));
895
- if (roots.size === 0) return;
896
- for (const root of roots) this.closingMembers(root).add(root);
897
- const targets = [];
898
- for (const activation of this.resident.values()) {
899
- const lineage = this.liveLineage(activation.handle.agent);
900
- const owners = [...roots].filter((root) => activation.handle.agent !== root && activation.ancestry.has(root));
901
- if (owners.length === 0) continue;
902
- targets.push(activation);
903
- for (const owner of owners) {
904
- const members = this.closingMembers(owner);
905
- members.add(activation.handle.agent);
906
- for (const agent of lineage) members.add(agent);
907
- }
908
- }
909
- const materializations = [...this.materializations].filter((materialization) => {
910
- const owners = [...roots].filter((root) => materialization.lineage.includes(root));
911
- for (const owner of owners) {
912
- const members = this.closingMembers(owner);
913
- for (const agent of materialization.lineage) members.add(agent);
914
- }
915
- return owners.length > 0;
916
- });
917
- const ownedTargets = /* @__PURE__ */ new Set();
918
- for (const activation of targets) for (const child of activation.ownedChildren) ownedTargets.add(child);
919
- const targetRoots = targets.filter((activation) => !ownedTargets.has(activation.childId));
920
- for (const activation of targets) this.dispose(activation).catch(() => void 0);
921
- await Promise.all(materializations.map((materialization) => materialization.settled));
922
- await this.disposeRoots(targetRoots, "scoped activation(s)");
923
- }
924
- /**
925
- * Release selected resident direct children of one exact live parent.
926
- * @param parent - exact live direct parent authorizing the selected release.
927
- * @param childIds - durable direct-child ids to release when resident.
928
- */
929
- async drainChildren(parent, childIds) {
930
- if (this.ctx.agents.get(parent.id) !== parent) throw new SubagentError("selected child teardown requires the exact live parent agent", "UNAUTHORIZED");
931
- const targets = [];
932
- for (const childId of new Set(childIds)) {
933
- const activation = this.resident.get(childId);
934
- if (activation === void 0) continue;
935
- if (activation.parentSession !== parent.id || !activation.ancestry.has(parent)) throw new SubagentError(`subagent "${childId}" is not a direct child of agent "${parent.id}"`, "UNAUTHORIZED");
936
- targets.push(activation);
937
- }
938
- for (const activation of targets) this.dispose(activation).catch(() => void 0);
939
- await this.disposeRoots(targets, "selected activation(s)");
940
- }
941
- /**
942
- * Reject new admission once the registry or this exact parent tree began draining.
943
- * @param agent - exact live Agent whose lineage determines admission.
944
- */
945
- assertAdmitting(agent) {
946
- const closing = this.closingTeardownFor(agent);
947
- if (closing === void 0) return;
948
- throw new SubagentError(closing === "manager" ? "continuable subagents are draining; the operation was not admitted" : `continuable subagents below parent "${closing.id}" are draining; the operation was not admitted`, "DRAINING");
949
- }
950
- /**
951
- * Authorize one operation against the durable direct-parent lineage.
952
- * @param parent - exact live Agent claiming direct-parent authority.
953
- * @param childId - durable child session id addressed by the operation.
954
- * @param parentSession - durable direct-parent id recorded by the child.
955
- */
956
- authorizeLineage(parent, childId, parentSession) {
957
- if (this.ctx.agents.get(parent.id) !== parent) throw new SubagentError(`subagent "${childId}" delivery requires the exact live parent agent`, "UNAUTHORIZED");
958
- if (parentSession !== parent.id) throw new SubagentError(`subagent "${childId}" belongs to another parent session`, "UNAUTHORIZED");
959
- }
960
- /**
961
- * Create or resume one child Agent and publish its Activation.
962
- * @param inputs - reconstruction and admission inputs for the residency epoch.
963
- * @returns the published process-local Activation.
964
- */
965
- materialize(inputs) {
966
- this.assertAdmitting(inputs.parent);
967
- inputs.signal.throwIfAborted();
968
- const lineage = this.liveLineage(inputs.parent);
969
- const pool = this.resident.get(inputs.parent.id)?.pool ?? this.rootPool(inputs.parent);
970
- const releaseSlot = pool.reserve(this.maxActiveSubagents());
971
- const settled = Promise.withResolvers();
972
- const materialization = {
973
- lineage,
974
- settled: settled.promise
975
- };
976
- this.materializations.add(materialization);
977
- return this.materializeTracked(inputs, lineage, pool, releaseSlot).catch((error) => {
978
- releaseSlot();
979
- throw error;
980
- }).finally(() => {
981
- this.materializations.delete(materialization);
982
- settled.resolve();
983
- });
984
- }
985
- /**
986
- * Cross the final admission cutoff and submit without yielding.
987
- * @param activation - the exact resident child receiving the message.
988
- * @param message - the already-built durable user message.
989
- * @param delivery - the Agent inbox destination.
990
- * @param parent - exact live direct parent authorizing admission.
991
- * @param signal - caller cancellation before inbox acceptance.
992
- * @returns the accepted durable message id.
993
- */
994
- submitAdmitted(activation, message, delivery, parent, signal) {
995
- signal.throwIfAborted();
996
- this.assertAdmitting(parent);
997
- this.authorizeLineage(parent, activation.childId, activation.handle.agent.session.header.parentSession);
998
- this.acquireOwnership(parent, activation.childId);
999
- try {
1000
- activation.inbox.deliver(message, delivery);
1001
- } finally {
1002
- this.wake(activation);
1003
- }
1004
- return message.id;
1005
- }
1006
- /**
1007
- * Stop and release one Activation through its memoized close transaction.
1008
- * @param activation - exact residency epoch to close.
1009
- * @param finalStateFlushed - whether natural settlement already flushed final state.
1010
- * @returns the shared close transaction.
1011
- */
1012
- dispose(activation, finalStateFlushed = false) {
1013
- return activation.inbox.close(() => this.finishDisposal(activation, finalStateFlushed));
1014
- }
1015
- /** Dispose independent roots and report every branch failure after all settle. */
1016
- async disposeRoots(roots, failureSubject) {
1017
- const reasons = (await Promise.all(roots.map(async (activation) => {
1018
- try {
1019
- await this.dispose(activation);
1020
- return;
1021
- } catch (error) {
1022
- return error;
1023
- }
1024
- }))).filter((failure) => failure !== void 0);
1025
- if (reasons.length > 0) throw new SubagentError(`continuable subagent teardown failed for ${reasons.length} ${failureSubject}: ` + reasons.map((reason) => errorChain(reason)).join("; "), "ACTIVATION_TEARDOWN_FAILED");
1026
- }
1027
- /** Return the retained member set for one exact scoped-teardown root. */
1028
- closingMembers(root) {
1029
- const existing = this.closingScopes.get(root);
1030
- if (existing !== void 0) return existing;
1031
- const members = /* @__PURE__ */ new Set();
1032
- this.closingScopes.set(root, members);
1033
- return members;
1034
- }
1035
- /** Return the exact currently resolvable ancestry from `agent` upward. */
1036
- liveLineage(agent) {
1037
- const lineage = [agent];
1038
- const seen = new Set([agent.id]);
1039
- let parentSession = agent.session.header.parentSession;
1040
- while (parentSession !== void 0) {
1041
- const parent = this.ctx.agents.get(parentSession);
1042
- if (parent === void 0 || seen.has(parent.id)) break;
1043
- lineage.push(parent);
1044
- seen.add(parent.id);
1045
- parentSession = parent.session.header.parentSession;
1046
- }
1047
- return lineage;
1048
- }
1049
- /** Return the teardown that closed continuable admission for this agent's lineage. */
1050
- closingTeardownFor(agent) {
1051
- if (this.draining) return "manager";
1052
- const lineage = this.liveLineage(agent);
1053
- for (const [root, members] of this.closingScopes) if (members.has(agent) || lineage.includes(root)) return root;
1054
- }
1055
- /** Resolve a root's pool once; descendants inherit their resident parent's pool directly. */
1056
- rootPool(parent) {
1057
- let pool = this.rootPools.get(parent);
1058
- if (pool === void 0) {
1059
- pool = new ActivationPool();
1060
- this.rootPools.set(parent, pool);
1061
- }
1062
- return pool;
1063
- }
1064
- /** Perform one tracked materialization through publication or rollback. */
1065
- async materializeTracked(inputs, parentLineage, pool, releaseSlot) {
1066
- const { childId, provider, parent, create } = inputs;
1067
- inputs.signal.throwIfAborted();
1068
- const setup = (childCtx, child) => {
1069
- if (create !== void 0) {
1070
- child.session.append("subagent/descriptor", create.descriptor);
1071
- appendDelegatedPolicyOverrides(child.session, create.delegatedPolicies);
1072
- }
1073
- applyChildComposition(childCtx, parent, inputs.composition);
1074
- };
1075
- const observer = this.observeActivation(provider, childId, parent);
1076
- const handle = create === void 0 ? await this.ownerCtx.agents.resume({
1077
- resumeSessionId: childId,
1078
- parentAgent: parent,
1079
- agentOptions: inputs.agentOptions,
1080
- signal: inputs.signal,
1081
- setup
1082
- }) : await this.ownerCtx.agents.create({
1083
- sessionId: childId,
1084
- parentAgent: parent,
1085
- meta: create.meta,
1086
- ...create.seed === void 0 ? {} : { seed: create.seed },
1087
- inheritedEventCount: create.inheritedEventCount,
1088
- agentOptions: inputs.agentOptions,
1089
- signal: inputs.signal,
1090
- setup
1091
- });
1092
- const activation = {
1093
- pool,
1094
- releaseSlot,
1095
- childId,
1096
- parentSession: parent.id,
1097
- provider,
1098
- handle,
1099
- inbox: new SubagentInbox(handle.agent),
1100
- ancestry: new WeakSet([handle.agent, ...parentLineage]),
1101
- ownedChildren: /* @__PURE__ */ new Set(),
1102
- observer,
1103
- announced: false,
1104
- poke: Promise.withResolvers()
1105
- };
1106
- this.resident.set(childId, activation);
1107
- try {
1108
- inputs.signal.throwIfAborted();
1109
- this.assertAdmitting(parent);
1110
- this.acquireOwnership(parent, childId);
1111
- const wakeOnInboxRemoval = () => {
1112
- this.wake(activation);
1113
- };
1114
- handle.agent.ctx.on("agent/inbox/claimed", wakeOnInboxRemoval);
1115
- handle.agent.ctx.on("agent/inbox/discarded", wakeOnInboxRemoval);
1116
- observer.start(handle.agent);
1117
- } catch (error) {
1118
- /* v8 ignore next -- rollback failure must not mask the admission failure
1119
- * that prevented this operation from returning an accepted message id. */
1120
- await this.rollbackUnpublished(activation).catch(() => void 0);
1121
- throw error;
1122
- }
1123
- this.watchSettlement(activation);
1124
- return activation;
1125
- }
1126
- /** Release an Activation whose start edge was not published. */
1127
- rollbackUnpublished(activation) {
1128
- return activation.inbox.close(async () => {
1129
- try {
1130
- await activation.handle.dispose();
1131
- } finally {
1132
- this.resident.delete(activation.childId);
1133
- activation.releaseSlot();
1134
- this.releaseOwnership(activation.childId);
1135
- }
1136
- });
1137
- }
1138
- /** Register the child in a continuation-managed parent's owned set. */
1139
- acquireOwnership(parent, childId) {
1140
- const parentActivation = this.resident.get(parent.id);
1141
- if (parentActivation === void 0) return;
1142
- if (parentActivation.inbox.closing !== void 0) throw new SubagentError(`subagent parent "${parent.id}" is being disposed; the child was not established`, "ACTIVATION_CLOSING");
1143
- parentActivation.ownedChildren.add(childId);
1144
- }
1145
- /** Remove one child from its live owner's set and let that owner re-check settlement. */
1146
- releaseOwnership(childId) {
1147
- for (const candidate of this.resident.values()) if (candidate.ownedChildren.delete(childId)) this.wake(candidate);
1148
- }
1149
- /** Let a settlement watcher re-check residency after relevant state changes. */
1150
- wake(activation) {
1151
- activation.poke.resolve();
1152
- activation.poke = Promise.withResolvers();
1153
- }
1154
- /** Follow one Activation to natural settlement. */
1155
- watchSettlement(activation) {
1156
- (async () => {
1157
- while (true) {
1158
- const idleObservation = activation.poke;
1159
- await activation.handle.agent.whenIdle();
1160
- if (activation.inbox.closing !== void 0) return;
1161
- const readiness = await this.locks.run(activation.childId, () => Promise.resolve(this.settlementState(activation, idleObservation)));
1162
- if (readiness === "closed") return;
1163
- if (readiness === "retry") continue;
1164
- if (readiness === "wait") {
1165
- await idleObservation.promise;
1166
- continue;
1167
- }
1168
- const finalSeq = activation.handle.agent.session.seq;
1169
- await this.flushFinalState(activation);
1170
- const attempt = await this.locks.run(activation.childId, () => {
1171
- const state = this.settlementState(activation, idleObservation);
1172
- if (state !== "ready") return Promise.resolve(state);
1173
- if (activation.handle.agent.session.seq !== finalSeq) return Promise.resolve("retry");
1174
- let done;
1175
- try {
1176
- activation.handle.agent.runMaintenance(() => {
1177
- done = this.dispose(activation, true);
1178
- return Promise.resolve();
1179
- });
1180
- } catch {
1181
- return Promise.resolve("retry");
1182
- }
1183
- return Promise.resolve({ done });
1184
- });
1185
- if (attempt === "closed") return;
1186
- if (attempt === "retry") continue;
1187
- if (attempt === "wait") {
1188
- await idleObservation.promise;
1189
- continue;
1190
- }
1191
- try {
1192
- await attempt.done;
1193
- } catch (error) {
1194
- this.ctx.logger.warn(`subagent "${activation.childId}" activation teardown failed: ${errorChain(error)}`);
1195
- }
1196
- return;
1197
- }
1198
- })();
1199
- }
1200
- /** Classify one Inbox and owned-child observation without reading Agent execution state. */
1201
- settlementState(activation, observation) {
1202
- if (activation.inbox.closing !== void 0) return "closed";
1203
- if (activation.poke !== observation) return "retry";
1204
- if (activation.inbox.hasPending || activation.ownedChildren.size > 0) return "wait";
1205
- return "ready";
1206
- }
1207
- /** Propagate stop synchronously, then finish the child-first release. */
1208
- async finishDisposal(activation, finalStateFlushed) {
1209
- this.wake(activation);
1210
- const { childId } = activation;
1211
- const failures = [];
1212
- if (finalStateFlushed) try {
1213
- activation.observer.capture(activation.handle.agent);
1214
- } catch (error) {
1215
- failures.push(new SubagentError(`subagent "${childId}" activation teardown failed: ${errorChain(error)}`, "ACTIVATION_TEARDOWN_FAILED", { cause: error }));
1216
- }
1217
- else {
1218
- activation.handle.agent.cancel({ kind: "parent" });
1219
- const idle = activation.handle.agent.whenIdle();
1220
- const childDisposals = [...activation.ownedChildren].map((child) => this.resident.get(child)).filter((child) => child !== void 0).map((child) => this.dispose(child));
1221
- try {
1222
- const reasons = (await Promise.all(childDisposals.map(async (disposal) => {
1223
- try {
1224
- await disposal;
1225
- return;
1226
- } catch (error) {
1227
- return error;
1228
- }
1229
- }))).filter((reason) => reason !== void 0);
1230
- if (reasons.length > 0) failures.push(new SubagentError(`subagent "${childId}" child teardown failed: ${reasons.map((reason) => errorChain(reason)).join("; ")}`, "ACTIVATION_TEARDOWN_FAILED"));
1231
- await idle;
1232
- await this.flushFinalState(activation);
1233
- activation.observer.capture(activation.handle.agent);
1234
- } catch (error) {
1235
- failures.push(new SubagentError(`subagent "${childId}" activation teardown failed: ${errorChain(error)}`, "ACTIVATION_TEARDOWN_FAILED", { cause: error }));
1236
- }
1237
- }
1238
- try {
1239
- await activation.handle.dispose();
1240
- } catch (error) {
1241
- failures.push(new SubagentError(`subagent "${childId}" activation handle disposal failed: ${errorChain(error)}`, "ACTIVATION_TEARDOWN_FAILED", { cause: error }));
1242
- }
1243
- let failure;
1244
- if (failures.length === 1) failure = failures[0];
1245
- 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) });
1246
- this.resident.delete(childId);
1247
- activation.releaseSlot();
1248
- this.notifySettlement(activation, activation.observer.terminal(failure));
1249
- this.releaseOwnership(childId);
1250
- activation.observer.settle(failure);
1251
- if (failure !== void 0) throw failure;
1252
- }
1253
- /** Tell the durable direct parent how this Activation ended. */
1254
- notifySettlement(activation, terminal) {
1255
- if (!activation.announced) return;
1256
- try {
1257
- const parent = this.ctx.agents.get(activation.parentSession);
1258
- if (parent === void 0) return;
1259
- const message = createSettlementMessage(activation.childId, terminal);
1260
- if (this.closingTeardownFor(parent) !== void 0) {
1261
- parent.inject(message);
1262
- return;
1263
- }
1264
- this.sendWaking(parent, message, parent.status === "idle" ? "queue" : "steer");
1265
- } catch (error) {
1266
- this.ctx.logger.warn(`subagent "${activation.childId}" settlement notice was not delivered to its parent: ` + errorChain(error));
1267
- }
1268
- }
1269
- /** Request a best-effort final session flush before closing natural-settlement admission. */
1270
- async flushFinalState(activation) {
1271
- const child = activation.handle.agent;
1272
- try {
1273
- await child.ctx.sessions.flush(child.session);
1274
- } catch (error) {
1275
- 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)}`);
1276
- }
1277
- }
1278
- };
1279
- //#endregion
1280
- //#region lib/types/descriptor.js
1281
- /**
1282
- * The durable subagent-child descriptor: the versioned, model-hidden
1283
- * `subagent/descriptor` session event that identifies every session-backed
1284
- * subagent and records whether it is one-shot or continuable. Continuable
1285
- * descriptors additionally preserve the declared composition required for
1286
- * cold resume. Providers append it turn-enclosed in the child's initial turn.
1287
- *
1288
- * The descriptor deliberately snapshots explicit fields rather than the
1289
- * merge-extensible `AgentOptions` object: an unrelated extension value cannot
1290
- * make continuation fail merely because it is not JSON, and later composition
1291
- * inputs require a deliberate {@link SUBAGENT_DESCRIPTOR_VERSION} change. It
1292
- * omits `subagentDepth` — cold resume trusts the persisted header's
1293
- * `delegationDepth` as the monotone floor — and `outputSchema`, which belongs
1294
- * to one activation's result contract rather than durable child composition.
1295
- * Per-activation knobs such as `maxTokens` are omitted for the same reason as
1296
- * `outputSchema`: they budget one activation. Cold resume requires the exact
1297
- * live parent for authorization but reconstructs child options only from the
1298
- * durable descriptor, so it neither restores the prior budget nor inherits
1299
- * the parent's current one; the resumed route's defaults apply instead.
1300
- *
1301
- * @module @deepseek-ai/dsh-subagent/descriptor
1302
- */
1303
- /**
1304
- * The current descriptor format version, stamped into every appended
1305
- * `subagent/descriptor` event and required verbatim by {@link foldSubagentDescriptor}.
1306
- * Supporting another composition input is a deliberate version change, never
1307
- * an implicit extra field.
1308
- */
1309
- const SUBAGENT_DESCRIPTOR_VERSION = 3;
1310
- const DESCRIPTOR_BASE_KEYS = [
1311
- "version",
1312
- "mode",
1313
- "provider",
1314
- "label"
1315
- ];
1316
- const ONE_SHOT_DESCRIPTOR_KEYS = new Set(DESCRIPTOR_BASE_KEYS);
1317
- const CONTINUABLE_DESCRIPTOR_KEYS = new Set([
1318
- ...DESCRIPTOR_BASE_KEYS,
1319
- "agentProvider",
1320
- "agentModel",
1321
- "agentReasoningEffort",
1322
- "persona",
1323
- "toolFilter"
1324
- ]);
1325
- const TOOL_FILTER_KEYS = new Set(["allow", "deny"]);
1326
- /** Whether a persisted JSON value is an object record. */
1327
- function isRecord(value) {
1328
- return typeof value === "object" && value !== null && !Array.isArray(value);
1329
- }
1330
- /** Reject fields outside one versioned record's declared schema. */
1331
- function assertKnownKeys(value, keys, path) {
1332
- const unknown = Object.keys(value).find((key) => !keys.has(key));
1333
- if (unknown !== void 0) throw new Error(`persisted subagent descriptor ${path} has unknown field "${unknown}"`);
1334
- }
1335
- /** Read one optional string field from a persisted descriptor record. */
1336
- function optionalString(value, key) {
1337
- if (!Object.hasOwn(value, key)) return void 0;
1338
- const field = value[key];
1339
- if (typeof field !== "string") throw new Error(`persisted subagent descriptor ${key} must be a string`);
1340
- return field;
1341
- }
1342
- /** Read one optional string-array field from a persisted tool restriction. */
1343
- function optionalStringArray(value, key) {
1344
- if (!Object.hasOwn(value, key)) return void 0;
1345
- const field = value[key];
1346
- if (!Array.isArray(field)) throw new Error(`persisted subagent descriptor toolFilter.${key} must be an array of strings`);
1347
- const items = field;
1348
- if (items.some((item) => typeof item !== "string")) throw new Error(`persisted subagent descriptor toolFilter.${key} must be an array of strings`);
1349
- return items;
1350
- }
1351
- /** Validate and reconstruct a persisted tool restriction. */
1352
- function parseToolFilter(value) {
1353
- if (!isRecord(value)) throw new Error("persisted subagent descriptor toolFilter must be an object");
1354
- assertKnownKeys(value, TOOL_FILTER_KEYS, "toolFilter");
1355
- const allow = optionalStringArray(value, "allow");
1356
- const deny = optionalStringArray(value, "deny");
1357
- if (allow === void 0 && deny === void 0) throw new Error("persisted subagent descriptor toolFilter must declare allow and/or deny");
1358
- return {
1359
- ...allow !== void 0 ? { allow } : {},
1360
- ...deny !== void 0 ? { deny } : {}
1361
- };
1362
- }
1363
- /** Validate one persisted descriptor payload for the current runtime. */
1364
- function parseSubagentDescriptor(value) {
1365
- if (!isRecord(value)) throw new Error("persisted subagent descriptor payload must be an object");
1366
- const version = value["version"];
1367
- if (typeof version !== "number") throw new Error("persisted subagent descriptor version must be a number");
1368
- if (version !== 3) return void 0;
1369
- const mode = value["mode"];
1370
- if (mode !== "one-shot" && mode !== "continuable") throw new Error("persisted subagent descriptor mode must be \"one-shot\" or \"continuable\"");
1371
- assertKnownKeys(value, mode === "one-shot" ? ONE_SHOT_DESCRIPTOR_KEYS : CONTINUABLE_DESCRIPTOR_KEYS, "payload");
1372
- const provider = value["provider"];
1373
- if (typeof provider !== "string") throw new Error("persisted subagent descriptor provider must be a string");
1374
- if (mode === "one-shot") {
1375
- const label = optionalString(value, "label");
1376
- return {
1377
- version: 3,
1378
- mode,
1379
- provider,
1380
- ...label !== void 0 ? { label } : {}
577
+ const SUBAGENT_DESCRIPTOR_VERSION = 3;
578
+ const DESCRIPTOR_BASE_KEYS = [
579
+ "version",
580
+ "mode",
581
+ "provider",
582
+ "label"
583
+ ];
584
+ const ONE_SHOT_DESCRIPTOR_KEYS = new Set(DESCRIPTOR_BASE_KEYS);
585
+ const CONTINUABLE_DESCRIPTOR_KEYS = new Set([
586
+ ...DESCRIPTOR_BASE_KEYS,
587
+ "agentProvider",
588
+ "agentModel",
589
+ "agentReasoningEffort",
590
+ "persona",
591
+ "toolFilter"
592
+ ]);
593
+ const TOOL_FILTER_KEYS = new Set(["allow", "deny"]);
594
+ /** Whether a persisted JSON value is an object record. */
595
+ function isRecord(value) {
596
+ return typeof value === "object" && value !== null && !Array.isArray(value);
597
+ }
598
+ /** Reject fields outside one versioned record's declared schema. */
599
+ function assertKnownKeys(value, keys, path) {
600
+ const unknown = Object.keys(value).find((key) => !keys.has(key));
601
+ if (unknown !== void 0) throw new Error(`persisted subagent descriptor ${path} has unknown field "${unknown}"`);
602
+ }
603
+ /** Read one optional string field from a persisted descriptor record. */
604
+ function optionalString(value, key) {
605
+ if (!Object.hasOwn(value, key)) return void 0;
606
+ const field = value[key];
607
+ if (typeof field !== "string") throw new Error(`persisted subagent descriptor ${key} must be a string`);
608
+ return field;
609
+ }
610
+ /** Read one optional string-array field from a persisted tool restriction. */
611
+ function optionalStringArray(value, key) {
612
+ if (!Object.hasOwn(value, key)) return void 0;
613
+ const field = value[key];
614
+ if (!Array.isArray(field)) throw new Error(`persisted subagent descriptor toolFilter.${key} must be an array of strings`);
615
+ const items = field;
616
+ if (items.some((item) => typeof item !== "string")) throw new Error(`persisted subagent descriptor toolFilter.${key} must be an array of strings`);
617
+ return items;
618
+ }
619
+ /** Validate and reconstruct a persisted tool restriction. */
620
+ function parseToolFilter(value) {
621
+ if (!isRecord(value)) throw new Error("persisted subagent descriptor toolFilter must be an object");
622
+ assertKnownKeys(value, TOOL_FILTER_KEYS, "toolFilter");
623
+ const allow = optionalStringArray(value, "allow");
624
+ const deny = optionalStringArray(value, "deny");
625
+ if (allow === void 0 && deny === void 0) throw new Error("persisted subagent descriptor toolFilter must declare allow and/or deny");
626
+ return {
627
+ ...allow !== void 0 ? { allow } : {},
628
+ ...deny !== void 0 ? { deny } : {}
629
+ };
630
+ }
631
+ /** Validate one persisted descriptor payload for the current runtime. */
632
+ function parseSubagentDescriptor(value) {
633
+ if (!isRecord(value)) throw new Error("persisted subagent descriptor payload must be an object");
634
+ const version = value["version"];
635
+ if (typeof version !== "number") throw new Error("persisted subagent descriptor version must be a number");
636
+ if (version !== 3) return void 0;
637
+ const mode = value["mode"];
638
+ if (mode !== "one-shot" && mode !== "continuable") throw new Error("persisted subagent descriptor mode must be \"one-shot\" or \"continuable\"");
639
+ assertKnownKeys(value, mode === "one-shot" ? ONE_SHOT_DESCRIPTOR_KEYS : CONTINUABLE_DESCRIPTOR_KEYS, "payload");
640
+ const provider = value["provider"];
641
+ if (typeof provider !== "string") throw new Error("persisted subagent descriptor provider must be a string");
642
+ if (mode === "one-shot") {
643
+ const label = optionalString(value, "label");
644
+ return {
645
+ version: 3,
646
+ mode,
647
+ provider,
648
+ ...label !== void 0 ? { label } : {}
1381
649
  };
1382
650
  }
1383
651
  const label = value["label"];
@@ -1399,13 +667,14 @@ function parseSubagentDescriptor(value) {
1399
667
  ...toolFilter !== void 0 ? { toolFilter } : {}
1400
668
  };
1401
669
  }
670
+ /**
671
+ * Validate and detach a continuable descriptor input.
672
+ * @param input - the caller-collected continuable composition fields.
673
+ * @returns the versioned, detached continuable descriptor payload.
674
+ * @throws when a field is not losslessly JSON-serializable.
675
+ */
1402
676
  function snapshotSubagentDescriptor(input) {
1403
- const snapshot = snapshotJsonValue(input.mode === "one-shot" ? {
1404
- version: 3,
1405
- mode: input.mode,
1406
- provider: input.provider,
1407
- ...input.label !== void 0 ? { label: input.label } : {}
1408
- } : {
677
+ const snapshot = snapshotJsonValue({
1409
678
  version: 3,
1410
679
  mode: input.mode,
1411
680
  provider: input.provider,
@@ -1451,12 +720,17 @@ const continuableCatalogSchema = z$1.object({
1451
720
  mode: z$1.literal("continuable"),
1452
721
  label: z$1.string()
1453
722
  }).strict();
723
+ const externalCatalogSchema = oneShotCatalogSchema.extend({
724
+ version: z$1.literal(2),
725
+ mode: z$1.literal("external")
726
+ });
1454
727
  const unknownCatalogSchema = oneShotCatalogSchema.extend({
1455
728
  version: z$1.literal(1),
1456
729
  mode: z$1.literal("unknown")
1457
730
  });
1458
731
  const eventDataSchema = z$1.union([
1459
732
  oneShotCatalogSchema,
733
+ externalCatalogSchema,
1460
734
  continuableCatalogSchema,
1461
735
  unknownCatalogSchema
1462
736
  ]);
@@ -1469,6 +743,14 @@ const viewSchema = z$1.array(z$1.union([
1469
743
  id: sessionIdSchema,
1470
744
  createdAt: oneShotCatalogSchema.shape.childCreatedAt
1471
745
  }),
746
+ externalCatalogSchema.omit({
747
+ version: true,
748
+ childId: true,
749
+ childCreatedAt: true
750
+ }).extend({
751
+ id: sessionIdSchema,
752
+ createdAt: externalCatalogSchema.shape.childCreatedAt
753
+ }),
1472
754
  continuableCatalogSchema.omit({
1473
755
  version: true,
1474
756
  childId: true,
@@ -1491,102 +773,383 @@ const stateSchema = z$1.object({
1491
773
  head: chunkedListSchema(eventDataSchema).optional()
1492
774
  }).strict();
1493
775
  /**
1494
- * Materialize complete and unknown-mode child identities from parent catalog events.
1495
- * @param state - parent catalog fold state.
1496
- * @returns current direct-child rows in parent catalog event order.
776
+ * Materialize complete and unknown-mode child identities from parent catalog events.
777
+ * @param state - parent catalog fold state.
778
+ * @returns current direct-child rows in parent catalog event order.
779
+ */
780
+ function subagentCatalogEntries(state) {
781
+ const entries = [];
782
+ for (const data of iterateChunkedList(state.head)) {
783
+ const { version: _version, childId, childCreatedAt, ...descriptor } = data;
784
+ entries.push({
785
+ id: childId,
786
+ createdAt: childCreatedAt,
787
+ ...descriptor
788
+ });
789
+ }
790
+ return entries;
791
+ }
792
+ /** Parent-owned direct-child catalog projection; invalid own facts reject restoration. */
793
+ const subagentCatalogProjectionDefinition = {
794
+ key: "subagentCatalog",
795
+ stateSchema,
796
+ init: (_header, inheritedEventCount) => ({ inheritedEventCount }),
797
+ apply: (state, event) => {
798
+ if (event.type !== "subagent/catalog" || event.seq < state.inheritedEventCount) return state;
799
+ return {
800
+ ...state,
801
+ head: appendChunkedList(state.head, eventDataSchema.parse(event.data))
802
+ };
803
+ },
804
+ stateVersion: 6,
805
+ wire: {
806
+ viewSchema,
807
+ view: subagentCatalogEntries
808
+ }
809
+ };
810
+ /**
811
+ * Append a complete direct-child discovery fact to its parent Session.
812
+ * @param parent - durable direct parent receiving the discovery fact.
813
+ * @param child - established child's immutable Session metadata.
814
+ * @param descriptor - mode-discriminated creation label frozen with the child.
815
+ */
816
+ function establishCatalogChild(parent, child, descriptor) {
817
+ parent.append("subagent/catalog", {
818
+ version: 0,
819
+ childId: child.id,
820
+ childCreatedAt: child.createdAt,
821
+ mode: descriptor.mode,
822
+ label: descriptor.label
823
+ });
824
+ }
825
+ /** Catalog payload version emitted by external execution creation. */
826
+ const EXTERNAL_SUBAGENT_CATALOG_VERSION = 2;
827
+ /**
828
+ * Record an external execution without a local child Session.
829
+ * @param parent - durable direct parent receiving the discovery fact.
830
+ * @param childId - identity returned by the external provider.
831
+ * @param label - display label assigned by the caller.
832
+ */
833
+ function establishExternalCatalogChild(parent, childId, label) {
834
+ parent.append("subagent/catalog", {
835
+ version: EXTERNAL_SUBAGENT_CATALOG_VERSION,
836
+ childId,
837
+ childCreatedAt: Date.now(),
838
+ mode: "external",
839
+ label
840
+ });
841
+ }
842
+ //#endregion
843
+ //#region lib/types/internal.js
844
+ /**
845
+ * Continuation integration markers and host adapters outside the public
846
+ * Service Definition and model-facing Agent messaging contract.
847
+ * @module @deepseek-ai/dsh-subagent/internal
848
+ */
849
+ /** Process-stable identity carried only by the standard adjacent-Agent messaging tool. */
850
+ const adjacentAgentSendMessageTool = Symbol.for("dsh.subagent.adjacentAgentSendMessageTool");
851
+ /**
852
+ * Test whether one visible definition is the standard adjacent-Agent messaging tool.
853
+ * @param definition - the scope-resolved `send_message` candidate.
854
+ * @returns whether the definition carries the internal standard-tool identity.
855
+ */
856
+ function isAdjacentAgentSendMessageTool(definition) {
857
+ return definition !== void 0 && definition[adjacentAgentSendMessageTool] === true;
858
+ }
859
+ /**
860
+ * Process-stable symbol-keyed host delivery shared by the bundled runtime
861
+ * entry and this unbundled internal subpath.
862
+ * @internal
863
+ */
864
+ const deliverSubagentPrompt = Symbol.for("dsh.subagent.deliverPrompt");
865
+ //#endregion
866
+ //#region lib/types/structured.js
867
+ /**
868
+ * Child-scoped structured-output tool, prompt instruction, terminal guard, and authoritative
869
+ * result capture for in-process subagents. Each child registers its real schema on its own
870
+ * scope, so concurrent runs do not interact and disposal leaves no global residue. The prompt
871
+ * contribution is ordinary reconstructed request state.
872
+ *
873
+ * Capture commits only after the authoritative `tools/result` succeeds; PTC mode capture also
874
+ * waits for the enclosing `run_code` result. The terminal result marker and monotonic tool
875
+ * guard prevent later calls from reopening a completed structured run.
876
+ * @module @deepseek-ai/dsh-subagent/structured
877
+ */
878
+ /** The model-facing tool name a structured child must call to finish. */
879
+ const STRUCTURED_OUTPUT_TOOL = "structured_output";
880
+ /**
881
+ * The instruction registered as the child's trailing scoped prompt section:
882
+ * the demand travels with the
883
+ * tool, as ordinary prompt state of exactly one agent.
884
+ */
885
+ const STRUCTURED_OUTPUT_INSTRUCTION = `When you have your final answer, you MUST report it by calling the \`${STRUCTURED_OUTPUT_TOOL}\` tool with arguments matching its parameter schema exactly. Do not finish with a plain text answer: only the tool call counts as your result.`;
886
+ /**
887
+ * Attach the scoped capture tool, instruction, and enforcement to a child during
888
+ * its creation window. Child disposal removes every registration.
889
+ * @param childCtx - the child agent's scope context (`setup`'s argument).
890
+ * @param schema - the trusted, already-asserted schema subset to enforce (see
891
+ * `assertObjectJsonSchema` in dsh-tools).
892
+ * @param canComplete - whether accepted input and owned child work have finished.
893
+ * @returns the attachment handle (read `captured()` after the child settles).
894
+ */
895
+ function attachStructuredRuntime(childCtx, schema, canComplete) {
896
+ /**
897
+ * Validated values staged by the capture tool body, awaiting THEIR OWN
898
+ * authoritative `tools/result` notification. The execution object's identity
899
+ * uniquely identifies a trip through the pipeline: adapter call ids may
900
+ * repeat across steps, but another execution can never reach this Map
901
+ * entry. This is distinct from the opaque `ToolExecutionToken` used to
902
+ * correlate nested transports. The final notification always deletes its own
903
+ * stage, whether the result succeeded or failed.
904
+ */
905
+ const staged = /* @__PURE__ */ new Map();
906
+ /** Successful nested capture waiting for its enclosing transport to commit. */
907
+ let pending;
908
+ let captured;
909
+ const schemaEntry = {
910
+ name: STRUCTURED_OUTPUT_TOOL,
911
+ description: "Report your final structured result. Call this exactly once, when your answer is complete; the arguments must match this tool's parameter schema exactly.",
912
+ parameters: { ...schema }
913
+ };
914
+ childCtx.tools.register({
915
+ ...schemaEntry,
916
+ output: {
917
+ schema: {
918
+ type: "object",
919
+ properties: { recorded: {
920
+ type: "boolean",
921
+ const: true
922
+ } },
923
+ required: ["recorded"],
924
+ additionalProperties: false
925
+ },
926
+ render: () => [{
927
+ type: "text",
928
+ text: "Structured output recorded."
929
+ }]
930
+ },
931
+ execute(args, exec) {
932
+ const violations = validateJsonSchemaValue(schema, args);
933
+ if (violations.length > 0) throw new ToolArgsError(violations);
934
+ if (!canComplete()) throw new Error("Wait for all delegated child tasks to finish and process pending messages before submitting structured output.");
935
+ staged.set(exec, { value: args });
936
+ exec.concludeTurn();
937
+ return Promise.resolve({ recorded: true });
938
+ }
939
+ });
940
+ childCtx.systemPrompt.section({
941
+ name: `tool:${STRUCTURED_OUTPUT_TOOL}`,
942
+ order: childCtx.systemPrompt.getSectionOrder("STRUCTURED_OUTPUT"),
943
+ text: STRUCTURED_OUTPUT_INSTRUCTION
944
+ });
945
+ childCtx.tools.guard((exec) => captured === void 0 && pending === void 0 ? void 0 : `structured output already recorded: the run is complete, so \`${exec.name}\` is not executed`);
946
+ childCtx.on("tools/result", function(exec, result) {
947
+ if (exec.name === "structured_output") {
948
+ const entry = staged.get(exec);
949
+ if (entry === void 0) return;
950
+ staged.delete(exec);
951
+ if (result.isError) return;
952
+ if (exec.parent === void 0) {
953
+ /* v8 ignore else -- sequential agent-loop dispatch lets the guard block every later supported call */
954
+ if (captured === void 0) captured = { value: entry.value };
955
+ } else if (captured === void 0 && pending === void 0) pending = {
956
+ parent: exec.parent,
957
+ value: entry.value
958
+ };
959
+ return;
960
+ }
961
+ if (pending?.parent !== exec.token) return;
962
+ const entry = pending;
963
+ pending = void 0;
964
+ if (result.isError) return;
965
+ /* v8 ignore else -- PTC mode serializes outer executions, so the guard blocks every later supported call */
966
+ if (captured === void 0) captured = { value: entry.value };
967
+ });
968
+ return {
969
+ captured: () => captured,
970
+ acceptsInput: () => staged.size === 0 && pending === void 0 && captured === void 0
971
+ };
972
+ }
973
+ //#endregion
974
+ //#region lib/types/assistant-output.js
975
+ /**
976
+ * Canonical selection of a child's final assistant output. Backend run results
977
+ * and `subagent/end.lastAssistantMessage` apply the same rule: select the last
978
+ * non-empty assistant message. An empty-content message records usage only
979
+ * when the loop appends it after a max-tokens step with no executable blocks,
980
+ * so it does not replace earlier output. If no non-empty message exists,
981
+ * select the accumulated assistant text. Selection is independent of the
982
+ * run's stop reason.
983
+ *
984
+ * @module @deepseek-ai/dsh-subagent/assistant-output
985
+ */
986
+ /**
987
+ * Incremental fold of the selection rule, for backends that observe a child's
988
+ * output as it streams: session-event backends {@link push} each event, and
989
+ * transports without session events (ACP content chunks) {@link pushText} raw
990
+ * text into the same streamed fallback.
991
+ */
992
+ var AssistantOutputFold = class {
993
+ message;
994
+ partial = [];
995
+ /**
996
+ * Fold one session event: a non-empty assistant message becomes the
997
+ * candidate final answer, while its embedded stream and any log-only attempt
998
+ * extend the streamed fallback; every other event contributes nothing.
999
+ * @param event - the next observed session event.
1000
+ */
1001
+ push(event) {
1002
+ if (event.type === "assistant/message") {
1003
+ const content = event.data.message.content;
1004
+ if (content.length > 0) this.message = content;
1005
+ }
1006
+ if (event.type === "assistant/message" || event.type === "assistant/attempt") this.pushText(joinAssistantStreamText(event.data.stream));
1007
+ }
1008
+ /**
1009
+ * Extend the streamed fallback with text observed outside session events.
1010
+ * @param text - the next streamed text piece (an empty piece is a no-op).
1011
+ */
1012
+ pushText(text) {
1013
+ if (text.length > 0) this.partial.push(text);
1014
+ }
1015
+ /**
1016
+ * Select the final output folded so far.
1017
+ * @returns the last non-empty assistant message, else the accumulated
1018
+ * streamed text, or `undefined` when the child produced neither.
1019
+ */
1020
+ collect() {
1021
+ if (this.message !== void 0) return this.message;
1022
+ const text = this.partial.join("");
1023
+ return text.length > 0 ? [{
1024
+ type: "text",
1025
+ text
1026
+ }] : void 0;
1027
+ }
1028
+ };
1029
+ /**
1030
+ * Apply the selection rule to one complete child-owned event suffix.
1031
+ * @param events - the child-owned events (after any seed or epoch boundary).
1032
+ * @returns the selected output, or `undefined` when the child produced none.
1497
1033
  */
1498
- function subagentCatalogEntries(state) {
1499
- const entries = [];
1500
- for (const data of iterateChunkedList(state.head)) entries.push(data.mode !== "continuable" ? {
1501
- id: data.childId,
1502
- createdAt: data.childCreatedAt,
1503
- mode: data.mode,
1504
- ...data.label === void 0 ? {} : { label: data.label }
1505
- } : {
1506
- id: data.childId,
1507
- createdAt: data.childCreatedAt,
1508
- mode: data.mode,
1509
- label: data.label
1510
- });
1511
- return entries;
1034
+ function finalAssistantOutput(events) {
1035
+ const fold = new AssistantOutputFold();
1036
+ for (const event of events) fold.push(event);
1037
+ return fold.collect();
1512
1038
  }
1513
- /** Parent-owned direct-child catalog projection; invalid own facts reject restoration. */
1514
- const subagentCatalogProjectionDefinition = {
1515
- key: "subagentCatalog",
1516
- stateSchema,
1517
- init: (_header, inheritedEventCount) => ({ inheritedEventCount }),
1518
- apply: (state, event) => {
1519
- if (event.type !== "subagent/catalog" || event.seq < state.inheritedEventCount) return state;
1520
- return {
1521
- ...state,
1522
- head: appendChunkedList(state.head, eventDataSchema.parse(event.data))
1039
+ //#endregion
1040
+ //#region lib/types/activation.js
1041
+ /** Execution-period records, capacity slots, child serialization, and local output capture.
1042
+ * @module @deepseek-ai/dsh-subagent/activation
1043
+ */
1044
+ /** Process-local slots shared through uninterrupted continuable parent links. */
1045
+ var ActivationPool = class {
1046
+ slots = /* @__PURE__ */ new Set();
1047
+ /**
1048
+ * Reserve before reconstruction; release also tolerates unpublished rollback.
1049
+ * @param capacity - the maximum simultaneous activations in this pool.
1050
+ * @returns an idempotent slot release.
1051
+ */
1052
+ reserve(capacity) {
1053
+ if (this.slots.size >= capacity) throw new SubagentError(`subagent limit reached (active child limit: ${capacity}); wait for an existing child to finish or complete this work with the current agents`, "ACTIVATION_LIMIT_REACHED");
1054
+ const slot = Symbol();
1055
+ this.slots.add(slot);
1056
+ return () => {
1057
+ this.slots.delete(slot);
1523
1058
  };
1524
- },
1525
- stateVersion: 3,
1526
- wire: {
1527
- viewSchema,
1528
- view: subagentCatalogEntries
1529
1059
  }
1530
1060
  };
1531
1061
  /**
1532
- * Append a complete direct-child discovery fact to its parent Session.
1533
- * @param parent - durable direct parent receiving the discovery fact.
1534
- * @param child - established child's immutable Session metadata.
1535
- * @param descriptor - mode-discriminated creation label frozen with the child.
1062
+ * Read the live Agent retained by local execution.
1063
+ * @param activation - the managed local or external execution.
1064
+ * @returns the local Agent, absent for an external run.
1536
1065
  */
1537
- function establishCatalogChild(parent, child, descriptor) {
1538
- parent.append("subagent/catalog", descriptor.mode === "one-shot" ? {
1539
- version: 0,
1540
- childId: child.id,
1541
- childCreatedAt: child.createdAt,
1542
- mode: descriptor.mode,
1543
- ...descriptor.label === void 0 ? {} : { label: descriptor.label }
1544
- } : {
1545
- version: 0,
1546
- childId: child.id,
1547
- childCreatedAt: child.createdAt,
1548
- mode: descriptor.mode,
1549
- label: descriptor.label
1550
- });
1066
+ function activationAgent(activation) {
1067
+ return activation.kind === "local" ? activation.handle.agent : void 0;
1551
1068
  }
1552
- //#endregion
1553
- //#region lib/types/internal.js
1554
1069
  /**
1555
- * Continuation integration markers and host adapters outside the public
1556
- * Service Definition and model-facing Agent messaging contract.
1557
- * @module @deepseek-ai/dsh-subagent/internal
1070
+ * Require local residency before accepting further input.
1071
+ * @param activation - execution targeted by message delivery.
1072
+ * @returns the local activation.
1073
+ * @throws when the external backend cannot accept another message.
1558
1074
  */
1559
- /** Process-stable identity carried only by the standard adjacent-Agent messaging tool. */
1560
- const adjacentAgentSendMessageTool = Symbol.for("dsh.subagent.adjacentAgentSendMessageTool");
1075
+ function requireLocalActivation(activation) {
1076
+ switch (activation.kind) {
1077
+ case "local": return activation;
1078
+ case "external": throw new SubagentError(`subagent "${activation.childId}" does not accept follow-up input`, "NOT_CONTINUABLE");
1079
+ /* v8 ignore next 2 -- Both execution kinds are handled above. */
1080
+ default: return assertNever(activation);
1081
+ }
1082
+ }
1561
1083
  /**
1562
- * Test whether one visible definition is the standard adjacent-Agent messaging tool.
1563
- * @param definition - the scope-resolved `send_message` candidate.
1564
- * @returns whether the definition carries the internal standard-tool identity.
1084
+ * Retain local failures whose turn ending could not be committed.
1085
+ * @param activation - exact local Agent and epoch receiving the scoped observers.
1086
+ * @param wake - notify its settlement watcher when failure changes readiness.
1565
1087
  */
1566
- function isAdjacentAgentSendMessageTool(definition) {
1567
- return definition !== void 0 && definition[adjacentAgentSendMessageTool] === true;
1088
+ function observeLocalFailure(activation, wake) {
1089
+ const child = activation.handle.agent;
1090
+ child.ctx.on("agent/error", ({ agent }) => {
1091
+ if (agent !== child) return;
1092
+ activation.failureAt = child.session.seq;
1093
+ wake();
1094
+ });
1095
+ child.ctx.on("session/event", (session, event) => {
1096
+ if (session === child.session && event.type === "turn/end" && activation.failureAt !== void 0 && event.seq >= activation.failureAt) activation.failureAt = void 0;
1097
+ });
1568
1098
  }
1569
1099
  /**
1570
- * Process-stable symbol-keyed host delivery shared by the bundled runtime
1571
- * entry and this unbundled internal subpath.
1572
- * @internal
1573
- */
1574
- const deliverSubagentPrompt = Symbol.for("dsh.subagent.deliverPrompt");
1100
+ * Capture local output and retain failures without a later committed turn ending.
1101
+ * @param activation - the settled local execution and any structured result.
1102
+ * @returns final output, stop reason, and committed structured value.
1103
+ */
1104
+ function captureLocalResult(activation) {
1105
+ const events = activation.handle.agent.session.snapshotEvents(activation.outputStart);
1106
+ const output = finalAssistantOutput(events) ?? [];
1107
+ const stopReason = activation.failureAt === void 0 ? epochStopReason(events) : "error";
1108
+ const structured = activation.structured;
1109
+ if (structured !== void 0) {
1110
+ const captured = structured.captured();
1111
+ if (captured !== void 0) return {
1112
+ output,
1113
+ stopReason,
1114
+ structured: captured.value
1115
+ };
1116
+ if (stopReason === "completed") return {
1117
+ output,
1118
+ stopReason: "error"
1119
+ };
1120
+ }
1121
+ return {
1122
+ output,
1123
+ stopReason
1124
+ };
1125
+ }
1126
+ /** Serialize each durable child's delivery, release, and disposal. */
1127
+ var ChildLock = class {
1128
+ tails = /* @__PURE__ */ new Map();
1129
+ /**
1130
+ * Run `operation` after every previously queued operation for `childId`.
1131
+ * @param childId - the durable child whose operations are linearized.
1132
+ * @param operation - the critical section to run in order.
1133
+ * @returns the operation's own settlement.
1134
+ */
1135
+ run(childId, operation) {
1136
+ const result = (this.tails.get(childId) ?? Promise.resolve()).then(operation, operation);
1137
+ const tail = result.then(() => void 0, () => void 0);
1138
+ this.tails.set(childId, tail);
1139
+ tail.then(() => {
1140
+ if (this.tails.get(childId) === tail) this.tails.delete(childId);
1141
+ });
1142
+ return result;
1143
+ }
1144
+ };
1575
1145
  //#endregion
1576
- //#region lib/types/continuation.js
1146
+ //#region lib/types/manager.js
1577
1147
  /**
1578
- * Continuable-subagent orchestration behind `ctx.subagents`: stable child ids,
1579
- * descriptor persistence, provider preparation, cold resume, authorization,
1580
- * and message routing. {@link ContinuableActivationRegistry} owns the mutable
1581
- * process-local Activation graph and its settlement and disposal lifecycle.
1148
+ * Owns subagent startup, message admission, cold resume, and execution lifetimes.
1149
+ * One child lock orders input acceptance and closure; one activation map owns
1150
+ * capacity, parent relationships, and resource release across all providers.
1582
1151
  *
1583
- * A continuable child has one durable Session and at most one process-local
1584
- * Activation. The Agent inbox is the only turn queue, so this manager owns
1585
- * durable orchestration while the Agent loop owns all turn ordering and
1586
- * execution. No continuable path creates a Task or an intermediate
1587
- * result-bearing wrapper.
1588
- *
1589
- * @module @deepseek-ai/dsh-subagent
1152
+ * @module @deepseek-ai/dsh-subagent/manager
1590
1153
  */
1591
1154
  var __addDisposableResource$2 = function(env, value, async) {
1592
1155
  if (value !== null && value !== void 0) {
@@ -1646,20 +1209,183 @@ var __disposeResources$2 = (function(SuppressedError) {
1646
1209
  var e = new Error(message);
1647
1210
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
1648
1211
  });
1649
- /**
1650
- * The continuable-subagent orchestration service behind `ctx.subagents`. Tool
1651
- * schema and host adapters are consumers of this one contract; foreground
1652
- * one-shot delegation keeps calling `ctx.subagents.start()` and never enters
1653
- * this lifecycle.
1654
- */
1655
- var SubagentContinuationManager = class {
1212
+ /** Render only the safe outer message from execution or teardown failures. */
1213
+ function failureMessage(error) {
1214
+ return error instanceof Error ? error.message : "unknown teardown failure";
1215
+ }
1216
+ /** Own subagent requests and the resources of every live activation. */
1217
+ var SubagentManager = class {
1656
1218
  ctx;
1657
1219
  host;
1658
- activations;
1220
+ maxActiveSubagents;
1221
+ /** Child session id → its live Activation. Process-local, never durable. */
1222
+ resident = /* @__PURE__ */ new Map();
1223
+ /** Root identities retain their pool across child settlement without retaining dead roots. */
1224
+ rootPools = /* @__PURE__ */ new WeakMap();
1225
+ /** Materializations admitted before drain, tracked through publication or rollback. */
1226
+ materializations = /* @__PURE__ */ new Set();
1227
+ /** Per-child serializer shared by delivery, release, and disposal. */
1228
+ locks = new ChildLock();
1229
+ /** Structural Cordis owner of every Activation handle. */
1230
+ ownerCtx;
1231
+ /**
1232
+ * Exact roots whose host teardown has begun, with the live lineage members
1233
+ * observed under each root. Entries remain until that exact root leaves the
1234
+ * Agent registry, closing admission throughout its host's teardown without
1235
+ * poisoning a later same-id replacement.
1236
+ */
1237
+ closingScopes = /* @__PURE__ */ new Map();
1238
+ draining = false;
1239
+ /**
1240
+ * Build one manager inside the service's Agent-injected context.
1241
+ * @param ctx - context providing Agents, Sessions, and teardown ownership.
1242
+ * @param host - provider dispatch and lifecycle observation owned by the service.
1243
+ * @param maxActiveSubagents - the current configured child capacity.
1244
+ */
1659
1245
  constructor(ctx, host, maxActiveSubagents) {
1660
1246
  this.ctx = ctx;
1661
1247
  this.host = host;
1662
- this.activations = new ContinuableActivationRegistry(ctx, (provider, childId, parent) => host.observeActivation(provider, childId, parent), maxActiveSubagents);
1248
+ this.maxActiveSubagents = maxActiveSubagents;
1249
+ const scope = ctx.plugin(function activationOwner() {});
1250
+ this.ownerCtx = scope.ctx;
1251
+ ctx.on("agent/disposed", ({ agent }) => {
1252
+ this.closingScopes.delete(agent);
1253
+ });
1254
+ ctx.effect(function* () {
1255
+ yield scope.dispose;
1256
+ yield () => this.drain();
1257
+ }.bind(this), "subagents.manager()");
1258
+ }
1259
+ /**
1260
+ * Interrupt one live continuable child's current turn under the supplied authority.
1261
+ * @param targetSessionId - the durable child session id to interrupt.
1262
+ * @param authority - the human parent address or exact live ancestor Agent.
1263
+ */
1264
+ interrupt(targetSessionId, authority) {
1265
+ if (authority.kind === "ancestor") {
1266
+ const caller = authority.agent;
1267
+ if (this.ctx.agents.get(caller.id) !== caller) throw new SubagentError(`interrupting "${targetSessionId}" requires the exact live ancestor agent`, "UNAUTHORIZED");
1268
+ if (caller.id === targetSessionId) throw new SubagentError(`agent "${caller.id}" cannot interrupt itself`, "UNAUTHORIZED");
1269
+ }
1270
+ const activation = this.resident.get(targetSessionId);
1271
+ if (activation === void 0) return;
1272
+ if (authority.kind === "user") {
1273
+ if (activation.parent.id !== authority.parentSessionId) throw new SubagentError(`subagent "${targetSessionId}" belongs to another parent session`, "UNAUTHORIZED");
1274
+ } else if (!activation.ancestry.has(authority.agent)) throw new SubagentError(`subagent "${targetSessionId}" is not a live descendant of agent "${authority.agent.id}"`, "UNAUTHORIZED");
1275
+ if (activation.closing !== void 0) return;
1276
+ const kind = authority.kind === "user" ? "user" : "parent";
1277
+ if (activation.kind === "local") activation.handle.agent.cancel({ kind }, { keepInbox: true });
1278
+ else {
1279
+ activation.controller.abort({ kind });
1280
+ this.dispose(activation).catch(() => void 0);
1281
+ }
1282
+ }
1283
+ /**
1284
+ * Close admission, await every already-admitted materialization through
1285
+ * publication or rollback, then dispose the stable live Activation graph
1286
+ * child-first.
1287
+ */
1288
+ async drain() {
1289
+ this.draining = true;
1290
+ for (const pending of this.materializations) pending.controller.abort();
1291
+ await Promise.all([...this.materializations].map((materialization) => materialization.settled));
1292
+ const owned = /* @__PURE__ */ new Set();
1293
+ for (const activation of this.resident.values()) for (const child of activation.ownedChildren) owned.add(child);
1294
+ const roots = [...this.resident.values()].filter((activation) => !owned.has(activation.childId));
1295
+ await this.disposeRoots(roots, "activation(s)");
1296
+ }
1297
+ /**
1298
+ * Join progressing child work, leaving idle parked inboxes resident.
1299
+ * @param parent - exact live parent whose descendants are observed.
1300
+ * @returns whether work was joined and the host must recheck parent completion.
1301
+ */
1302
+ async waitForChildren(parent) {
1303
+ let observed = false;
1304
+ while (true) {
1305
+ const pending = [...this.materializations].filter((item) => item.lineage.includes(parent));
1306
+ const children = [...this.resident.values()].filter((item) => activationAgent(item) !== parent && item.ancestry.has(parent));
1307
+ observed ||= children.some((item) => item.kind === "local" && item.handle.agent.status !== "idle");
1308
+ await Promise.all(children.flatMap((item) => item.kind === "local" ? [item.handle.agent.whenIdle()] : []));
1309
+ const waits = pending.map((item) => item.settled);
1310
+ for (const item of children) {
1311
+ if (item.kind === "external" || item.closing !== void 0) {
1312
+ waits.push(item.result.promise.catch(() => void 0));
1313
+ continue;
1314
+ }
1315
+ if (item.handle.agent.status !== "idle") {
1316
+ waits.push(item.handle.agent.whenIdle());
1317
+ continue;
1318
+ }
1319
+ if (item.ownedChildren.size > 0) {
1320
+ if ([...item.ownedChildren].some((id) => !this.resident.has(id))) waits.push(item.poke.promise);
1321
+ continue;
1322
+ }
1323
+ const inbox = item.handle.agent.inbox;
1324
+ if (item.failureAt === void 0 && (inbox.nextTurn.length > 0 || inbox.nextStep.length > 0)) continue;
1325
+ waits.push(Promise.race([item.result.promise.catch(() => void 0), item.poke.promise]));
1326
+ }
1327
+ if (waits.length > 0) {
1328
+ observed = true;
1329
+ await Promise.all(waits);
1330
+ continue;
1331
+ }
1332
+ if ([...this.materializations].some((item) => item.lineage.includes(parent) && !pending.includes(item)) || [...this.resident.values()].some((item) => activationAgent(item) !== parent && item.ancestry.has(parent) && !children.includes(item))) continue;
1333
+ return observed;
1334
+ }
1335
+ }
1336
+ /**
1337
+ * Stop the managed descendants of exact live host-owned parents.
1338
+ * @param parents - exact live roots whose managed descendants must stop.
1339
+ */
1340
+ async drainDescendants(parents) {
1341
+ const roots = new Set(parents.filter((parent) => this.ctx.agents.get(parent.id) === parent));
1342
+ if (roots.size === 0) return;
1343
+ for (const root of roots) this.closingMembers(root).add(root);
1344
+ const targets = [];
1345
+ for (const activation of this.resident.values()) {
1346
+ const child = activationAgent(activation);
1347
+ const lineage = this.liveLineage(child ?? activation.parent);
1348
+ const owners = [...roots].filter((root) => child !== root && activation.ancestry.has(root));
1349
+ if (owners.length === 0) continue;
1350
+ targets.push(activation);
1351
+ for (const owner of owners) {
1352
+ const members = this.closingMembers(owner);
1353
+ if (child !== void 0) members.add(child);
1354
+ for (const agent of lineage) members.add(agent);
1355
+ }
1356
+ }
1357
+ const materializations = [...this.materializations].filter((materialization) => {
1358
+ const owners = [...roots].filter((root) => materialization.lineage.includes(root));
1359
+ for (const owner of owners) {
1360
+ const members = this.closingMembers(owner);
1361
+ for (const agent of materialization.lineage) members.add(agent);
1362
+ }
1363
+ if (owners.length > 0) materialization.controller.abort();
1364
+ return owners.length > 0;
1365
+ });
1366
+ const ownedTargets = /* @__PURE__ */ new Set();
1367
+ for (const activation of targets) for (const child of activation.ownedChildren) ownedTargets.add(child);
1368
+ const targetRoots = targets.filter((activation) => !ownedTargets.has(activation.childId));
1369
+ for (const activation of targets) this.dispose(activation).catch(() => void 0);
1370
+ await Promise.all(materializations.map((materialization) => materialization.settled));
1371
+ await this.disposeRoots(targetRoots, "scoped activation(s)");
1372
+ }
1373
+ /**
1374
+ * Release selected resident direct children of one exact live parent.
1375
+ * @param parent - exact live direct parent authorizing the selected release.
1376
+ * @param childIds - durable direct-child ids to release when resident.
1377
+ */
1378
+ async drainChildren(parent, childIds) {
1379
+ if (this.ctx.agents.get(parent.id) !== parent) throw new SubagentError("selected child teardown requires the exact live parent agent", "UNAUTHORIZED");
1380
+ const targets = [];
1381
+ for (const childId of new Set(childIds)) {
1382
+ const activation = this.resident.get(childId);
1383
+ if (activation === void 0) continue;
1384
+ if (activation.parent.id !== parent.id || !activation.ancestry.has(parent)) throw new SubagentError(`subagent "${childId}" is not a direct child of agent "${parent.id}"`, "UNAUTHORIZED");
1385
+ targets.push(activation);
1386
+ }
1387
+ for (const activation of targets) this.dispose(activation).catch(() => void 0);
1388
+ await this.disposeRoots(targets, "selected activation(s)");
1663
1389
  }
1664
1390
  /**
1665
1391
  * Start one continuable background child and resolve at initial inbox acceptance.
@@ -1668,14 +1394,14 @@ var SubagentContinuationManager = class {
1668
1394
  * @param spec - provider, delegation request, and caller cancellation.
1669
1395
  * @returns the durable child id and accepted initial prompt message id.
1670
1396
  */
1671
- async startContinuable(spec) {
1397
+ async startLocal(spec) {
1672
1398
  const request = spec.request;
1673
1399
  const parent = request.parent;
1674
- this.activations.assertAdmitting(parent);
1675
- const persistence = this.requirePersistence();
1400
+ this.assertAdmitting(parent);
1401
+ const persistence = spec.delivery === "caller" ? this.ctx.get("sessionPersistence") : this.requirePersistence();
1676
1402
  assertSubagentMaxDepth(request.maxDepth);
1677
1403
  const childId = spec.childId ?? brandString(randomUUID());
1678
- this.activations.assertChildIdAvailable(childId);
1404
+ this.assertChildIdAvailable(childId);
1679
1405
  const childDepth = resolveChildDepth(parent, request.maxDepth);
1680
1406
  const agentOptions = resolveChildAgentOptions(parent, request.agentOptions, childDepth);
1681
1407
  const agentProvider = agentOptions.provider;
@@ -1692,123 +1418,677 @@ var SubagentContinuationManager = class {
1692
1418
  ...request.toolFilter !== void 0 ? { toolFilter: request.toolFilter } : {}
1693
1419
  });
1694
1420
  const delegatedPolicies = captureDelegatedPolicyOverrides(parent);
1695
- const releaseHold = this.activations.holdOwnership(parent, childId);
1421
+ const releaseHold = this.holdOwnership(parent, childId);
1696
1422
  try {
1423
+ const cwd = await this.resolveDirectory(request, spec.signal);
1697
1424
  const prepared = await this.host.prepareContinuable(spec.provider, {
1698
1425
  sessionId: childId,
1426
+ cwd,
1699
1427
  parent,
1700
1428
  signal: spec.signal
1701
1429
  });
1702
1430
  spec.signal.throwIfAborted();
1703
- this.activations.assertAdmitting(parent);
1431
+ this.assertAdmitting(parent);
1704
1432
  const inheritedEventCount = SessionLogOffset(prepared.seed?.length ?? 0);
1705
1433
  const seed = prepared.seed;
1434
+ let established;
1435
+ const messageId = await this.locks.run(childId, async () => {
1436
+ spec.signal.throwIfAborted();
1437
+ this.assertAdmitting(parent);
1438
+ this.assertChildIdAvailable(childId);
1439
+ if (spec.childId !== void 0) {
1440
+ const persisted = await persistence?.stat(childId, { signal: spec.signal });
1441
+ spec.signal.throwIfAborted();
1442
+ this.assertAdmitting(parent);
1443
+ this.assertChildIdAvailable(childId);
1444
+ if (persisted !== void 0) throw new SubagentError(`subagent "${childId}" already exists`, "DUPLICATE_CHILD");
1445
+ }
1446
+ const activation = await this.materialize({
1447
+ kind: "local",
1448
+ childId,
1449
+ provider: spec.provider,
1450
+ parent,
1451
+ create: {
1452
+ cwd,
1453
+ seed,
1454
+ meta: childSessionMeta(parent, childDepth, prepared.seed !== void 0),
1455
+ inheritedEventCount,
1456
+ delegatedPolicies,
1457
+ descriptor
1458
+ },
1459
+ agentOptions,
1460
+ composition: {
1461
+ persona: request.persona,
1462
+ toolFilter: request.toolFilter
1463
+ },
1464
+ signal: spec.signal,
1465
+ delivery: spec.delivery,
1466
+ outputSchema: spec.request.outputSchema
1467
+ });
1468
+ established = activation;
1469
+ const child = requireLocalActivation(activation).handle.agent;
1470
+ const childHeader = child.session.header;
1471
+ return await this.submitMaterialized(activation, spec.delivery !== "caller" && isAdjacentAgentSendMessageTool(this.ctx.get("tools")?.get("send_message", child)) ? withContinuableReturnGuidance(parent.id, request.prompt) : request.prompt, {
1472
+ source: { kind: "user" },
1473
+ signal: spec.signal,
1474
+ delivery: "queue"
1475
+ }, parent, () => {
1476
+ establishCatalogChild(parent.session, childHeader, descriptor);
1477
+ });
1478
+ });
1706
1479
  return {
1480
+ ...this.receipt(established),
1707
1481
  childId,
1708
- messageId: await this.activations.locks.run(childId, async () => {
1709
- spec.signal.throwIfAborted();
1710
- this.activations.assertAdmitting(parent);
1711
- this.activations.assertChildIdAvailable(childId);
1712
- if (spec.childId !== void 0) {
1713
- const persisted = await persistence.stat(childId, { signal: spec.signal });
1714
- spec.signal.throwIfAborted();
1715
- this.activations.assertAdmitting(parent);
1716
- this.activations.assertChildIdAvailable(childId);
1717
- if (persisted !== void 0) throw new SubagentError(`subagent "${childId}" already exists`, "DUPLICATE_CHILD");
1718
- }
1719
- const activation = await this.activations.materialize({
1720
- childId,
1721
- provider: spec.provider,
1722
- parent,
1723
- create: {
1724
- seed,
1725
- meta: childSessionMeta(parent, childDepth, prepared.seed !== void 0),
1726
- inheritedEventCount,
1727
- delegatedPolicies,
1728
- descriptor
1729
- },
1730
- agentOptions,
1731
- composition: {
1732
- persona: request.persona,
1733
- toolFilter: request.toolFilter
1734
- },
1735
- signal: spec.signal
1736
- });
1737
- const childHeader = activation.handle.agent.session.header;
1738
- return await this.submitMaterialized(activation, isAdjacentAgentSendMessageTool(this.ctx.get("tools")?.get("send_message", activation.handle.agent)) ? withContinuableReturnGuidance(parent.id, request.prompt) : request.prompt, {
1739
- source: { kind: "user" },
1740
- signal: spec.signal,
1741
- delivery: "queue"
1742
- }, parent, () => {
1743
- establishCatalogChild(parent.session, childHeader, descriptor);
1744
- });
1482
+ messageId
1483
+ };
1484
+ } catch (error) {
1485
+ releaseHold();
1486
+ throw error;
1487
+ }
1488
+ }
1489
+ /**
1490
+ * Start an external backend under activation ownership.
1491
+ * @param spec - task, provider, cancellation, and delivery policy.
1492
+ * @returns the accepted activation and its result/disposal capabilities.
1493
+ */
1494
+ async startExternal(spec) {
1495
+ const parent = spec.request.parent;
1496
+ this.assertAdmitting(parent);
1497
+ const pendingId = brandString(randomUUID());
1498
+ const releaseHold = this.holdOwnership(parent, pendingId);
1499
+ let activation;
1500
+ try {
1501
+ const cwd = await this.resolveDirectory(spec.request, spec.signal);
1502
+ activation = await this.materialize({
1503
+ kind: "external",
1504
+ childId: pendingId,
1505
+ provider: spec.provider,
1506
+ parent,
1507
+ signal: spec.signal,
1508
+ delivery: spec.delivery,
1509
+ start: (signal) => this.host.startExternal(spec.provider, {
1510
+ ...spec.request,
1511
+ cwd,
1512
+ label: spec.label,
1513
+ signal
1745
1514
  })
1515
+ });
1516
+ this.assertAdmitting(parent);
1517
+ this.assertLiveParent(parent, activation.childId);
1518
+ spec.signal.throwIfAborted();
1519
+ if (spec.delivery === "parent") establishExternalCatalogChild(parent.session, activation.childId, spec.label);
1520
+ this.announce(activation);
1521
+ return this.receipt(activation);
1522
+ } catch (error) {
1523
+ if (activation !== void 0) try {
1524
+ await this.dispose(activation);
1525
+ } catch (cleanupError) {
1526
+ this.ctx.logger.warn(`subagent "${activation.childId}" admission rollback failed: ${failureMessage(cleanupError)}`);
1527
+ }
1528
+ throw error;
1529
+ } finally {
1530
+ releaseHold();
1531
+ }
1532
+ }
1533
+ /** Capture the explicit or inherited directory while the parent still owns startup. */
1534
+ async resolveDirectory(request, signal) {
1535
+ const cwd = request.cwd !== void 0 && isAbsolute(request.cwd) ? request.cwd : resolve(await this.ctx.workingDirectory.ensure(request.parent, signal), request.cwd ?? ".");
1536
+ signal.throwIfAborted();
1537
+ return cwd;
1538
+ }
1539
+ /**
1540
+ * Deliver one model-authored message to a direct continuable child or to the
1541
+ * sender's direct parent. A missing direct child cold-resumes through the
1542
+ * ordinary continuation lifecycle.
1543
+ * @param sender - exact live Agent authorizing and originating the message.
1544
+ * @param targetId - durable direct-parent or direct-child session id.
1545
+ * @param content - model-authored content to deliver.
1546
+ * @param options - caller cancellation before acceptance.
1547
+ * @returns the accepted message's inbox id.
1548
+ */
1549
+ async sendMessage(sender, targetId, content, options) {
1550
+ if (this.ctx.agents.get(sender.id) !== sender) throw new SubagentError("message delivery requires the exact live sender agent", "UNAUTHORIZED");
1551
+ this.assertAdmitting(sender);
1552
+ const senderActivation = this.resident.get(sender.id);
1553
+ if (senderActivation !== void 0 && senderActivation.kind === "local" && senderActivation.handle.agent === sender && senderActivation.parent.id === targetId) {
1554
+ options.signal.throwIfAborted();
1555
+ return this.sendToParent(senderActivation, sender, content);
1556
+ }
1557
+ if (sender.session.header.parentSession === targetId) throw new SubagentError(`agent "${sender.id}" is not a resident continuable child and cannot send to parent "${targetId}"`, "UNAUTHORIZED");
1558
+ return this.deliverToChild(sender, targetId, content, {
1559
+ signal: options.signal,
1560
+ delivery: "steer"
1561
+ });
1562
+ }
1563
+ /**
1564
+ * Queue one human-authored prompt as a distinct direct-child turn.
1565
+ * @param parent - exact live direct parent authorizing delivery.
1566
+ * @param childId - durable direct-child session id.
1567
+ * @param content - model-visible prompt blocks.
1568
+ * @param source - durable attribution for the human prompt.
1569
+ * @param signal - caller cancellation before inbox acceptance.
1570
+ * @returns the accepted durable message id.
1571
+ */
1572
+ async queuePrompt(parent, childId, content, source, signal) {
1573
+ return this.deliverToChild(parent, childId, content, {
1574
+ source,
1575
+ signal,
1576
+ delivery: "queue"
1577
+ });
1578
+ }
1579
+ /**
1580
+ * Steer one host-authored prompt to a direct continuable child.
1581
+ * @param parent - exact live direct parent authorizing delivery.
1582
+ * @param childId - durable direct-child session id.
1583
+ * @param content - model-visible prompt blocks.
1584
+ * @param source - durable attribution for the host prompt.
1585
+ * @param signal - caller cancellation before inbox acceptance.
1586
+ * @returns the accepted durable message id.
1587
+ */
1588
+ async steerPrompt(parent, childId, content, source, signal) {
1589
+ return this.deliverToChild(parent, childId, content, {
1590
+ source,
1591
+ signal,
1592
+ delivery: "steer"
1593
+ });
1594
+ }
1595
+ /** Admit local input synchronously before any teardown can begin. */
1596
+ deliver(activation, message, delivery) {
1597
+ if (activation.closing !== void 0) throw new SubagentError("subagent activation is being disposed; the message was not accepted", "ACTIVATION_CLOSING");
1598
+ const local = requireLocalActivation(activation);
1599
+ if (local.structured?.acceptsInput() === false) throw new SubagentError("subagent is submitting or has submitted its structured result; the message was not accepted", "INPUT_CLOSED");
1600
+ if (delivery === "steer") local.handle.agent.steer(message);
1601
+ else local.handle.agent.followup(message);
1602
+ }
1603
+ /**
1604
+ * Reject one child identity already owned by a live Agent or Session.
1605
+ * @param childId - proposed durable child session id.
1606
+ */
1607
+ assertChildIdAvailable(childId) {
1608
+ if (this.resident.has(childId) || this.ctx.agents.get(childId) !== void 0 || this.ctx.get("sessions")?.get(childId) !== void 0) throw new SubagentError(`subagent "${childId}" already exists`, "DUPLICATE_CHILD");
1609
+ }
1610
+ /**
1611
+ * Pre-register `childId` in a continuation-managed parent's owned set so the
1612
+ * parent cannot settle while a caller is still establishing or resuming that
1613
+ * child. Returns a releaser for the failure path; it removes only a hold
1614
+ * this call added, and leaves ownership in place once a live Activation for
1615
+ * the child exists.
1616
+ * @param parent - the live direct parent the operation is admitted under.
1617
+ * @param childId - the durable child the operation addresses.
1618
+ * @returns the failure-path releaser; a no-op when nothing was added.
1619
+ */
1620
+ holdOwnership(parent, childId) {
1621
+ const parentActivation = this.resident.get(parent.id);
1622
+ if (parentActivation === void 0 || activationAgent(parentActivation) !== parent) return () => {};
1623
+ if (parentActivation.closing !== void 0) throw new SubagentError(`subagent parent "${parent.id}" is being disposed; the child was not established`, "ACTIVATION_CLOSING");
1624
+ if (parentActivation.ownedChildren.has(childId)) return () => {};
1625
+ parentActivation.ownedChildren.add(childId);
1626
+ return () => {
1627
+ const live = this.resident.get(childId);
1628
+ /* v8 ignore next 4 -- reaching this arm needs another delivery to establish the child
1629
+ * between this operation's failure and its releaser running, which no test can schedule
1630
+ * deterministically: the ownership edge then belongs to that live Activation, so the
1631
+ * conservative keep leaves it for finishDisposal's releaseOwnership. */
1632
+ if (live !== void 0 && live.closing === void 0) return;
1633
+ if (parentActivation.ownedChildren.delete(childId)) this.wake(parentActivation);
1634
+ };
1635
+ }
1636
+ /**
1637
+ * Apply activation admission to a resident parent before delivering input.
1638
+ * @param parent - exact live Agent receiving the message.
1639
+ * @param message - durable user message to deliver.
1640
+ * @param delivery - receiving inbox destination.
1641
+ */
1642
+ sendWaking(parent, message, delivery) {
1643
+ const parentActivation = this.resident.get(parent.id);
1644
+ if (parentActivation !== void 0 && activationAgent(parentActivation) === parent) {
1645
+ try {
1646
+ this.deliver(parentActivation, message, delivery);
1647
+ } finally {
1648
+ this.wake(parentActivation);
1649
+ }
1650
+ return;
1651
+ }
1652
+ if (delivery === "steer") parent.steer(message);
1653
+ else parent.followup(message);
1654
+ }
1655
+ /**
1656
+ * Reject new admission once the manager or this exact parent tree began draining.
1657
+ * @param agent - exact live Agent whose lineage determines admission.
1658
+ */
1659
+ assertAdmitting(agent) {
1660
+ const closing = this.closingTeardownFor(agent);
1661
+ if (closing === void 0) return;
1662
+ throw new SubagentError(closing === "manager" ? "subagents are draining; the operation was not admitted" : `subagents below parent "${closing.id}" are draining; the operation was not admitted`, "DRAINING");
1663
+ }
1664
+ /**
1665
+ * Authorize one operation against the durable direct-parent lineage.
1666
+ * @param parent - exact live Agent claiming direct-parent authority.
1667
+ * @param childId - durable child session id addressed by the operation.
1668
+ * @param parentSession - durable direct-parent id recorded by the child.
1669
+ */
1670
+ authorizeLineage(parent, childId, parentSession) {
1671
+ this.assertLiveParent(parent, childId);
1672
+ if (parentSession !== parent.id) throw new SubagentError(`subagent "${childId}" belongs to another parent session`, "UNAUTHORIZED");
1673
+ }
1674
+ /** Reject parent authority after the live registry entry changes. */
1675
+ assertLiveParent(parent, childId) {
1676
+ if (this.ctx.agents.get(parent.id) !== parent) throw new SubagentError(`subagent "${childId}" delivery requires the exact live parent agent`, "UNAUTHORIZED");
1677
+ }
1678
+ /**
1679
+ * Create or resume one child Agent and publish its Activation.
1680
+ * @param inputs - reconstruction and admission inputs for the residency epoch.
1681
+ * @returns the published process-local Activation.
1682
+ */
1683
+ materialize(inputs) {
1684
+ this.assertAdmitting(inputs.parent);
1685
+ inputs.signal.throwIfAborted();
1686
+ const lineage = this.liveLineage(inputs.parent);
1687
+ const pool = this.resident.get(inputs.parent.id)?.pool ?? this.rootPool(inputs.parent);
1688
+ const releaseSlot = pool.reserve(this.maxActiveSubagents());
1689
+ const settled = Promise.withResolvers();
1690
+ const controller = new AbortController();
1691
+ const materialization = {
1692
+ lineage,
1693
+ settled: settled.promise,
1694
+ controller
1695
+ };
1696
+ this.materializations.add(materialization);
1697
+ return this.materializeTracked({
1698
+ ...inputs,
1699
+ signal: AbortSignal.any([inputs.signal, controller.signal])
1700
+ }, lineage, pool, releaseSlot).catch((error) => {
1701
+ releaseSlot();
1702
+ if (error instanceof Error && error.name === "AbortError") this.assertAdmitting(inputs.parent);
1703
+ throw error;
1704
+ }).finally(() => {
1705
+ this.materializations.delete(materialization);
1706
+ settled.resolve();
1707
+ });
1708
+ }
1709
+ /**
1710
+ * Stop and release one Activation through its memoized close transaction.
1711
+ * @param activation - exact residency epoch to close.
1712
+ * @returns the shared close transaction.
1713
+ */
1714
+ dispose(activation) {
1715
+ return this.close(activation, () => this.finishDisposal(activation, true));
1716
+ }
1717
+ /** Close admission synchronously and share one release, including startup rollback. */
1718
+ close(activation, release) {
1719
+ if (activation.closing !== void 0) return activation.closing;
1720
+ const completion = Promise.withResolvers();
1721
+ activation.closing = completion.promise;
1722
+ release().then(completion.resolve, completion.reject);
1723
+ return completion.promise;
1724
+ }
1725
+ /** Dispose independent roots and report every branch failure after all settle. */
1726
+ async disposeRoots(roots, failureSubject) {
1727
+ const reasons = (await Promise.all(roots.map(async (activation) => {
1728
+ try {
1729
+ await this.dispose(activation);
1730
+ return;
1731
+ } catch (error) {
1732
+ return error;
1733
+ }
1734
+ }))).filter((failure) => failure !== void 0);
1735
+ if (reasons.length > 0) throw new SubagentError(`subagent teardown failed for ${reasons.length} ${failureSubject}: ` + reasons.map((reason) => failureMessage(reason)).join("; "), "ACTIVATION_TEARDOWN_FAILED");
1736
+ }
1737
+ /** Return the retained member set for one exact scoped-teardown root. */
1738
+ closingMembers(root) {
1739
+ const existing = this.closingScopes.get(root);
1740
+ if (existing !== void 0) return existing;
1741
+ const members = /* @__PURE__ */ new Set();
1742
+ this.closingScopes.set(root, members);
1743
+ return members;
1744
+ }
1745
+ /** Return the exact currently resolvable ancestry from `agent` upward. */
1746
+ liveLineage(agent) {
1747
+ const lineage = [agent];
1748
+ const seen = new Set([agent.id]);
1749
+ let parentSession = agent.session.header.parentSession;
1750
+ while (parentSession !== void 0) {
1751
+ const parent = this.ctx.agents.get(parentSession);
1752
+ if (parent === void 0 || seen.has(parent.id)) break;
1753
+ lineage.push(parent);
1754
+ seen.add(parent.id);
1755
+ parentSession = parent.session.header.parentSession;
1756
+ }
1757
+ return lineage;
1758
+ }
1759
+ /** Return the teardown that closed continuable admission for this agent's lineage. */
1760
+ closingTeardownFor(agent) {
1761
+ if (this.draining) return "manager";
1762
+ const lineage = this.liveLineage(agent);
1763
+ for (const [root, members] of this.closingScopes) if (members.has(agent) || lineage.includes(root)) return root;
1764
+ }
1765
+ /** Resolve a root's pool once; descendants inherit their resident parent's pool directly. */
1766
+ rootPool(parent) {
1767
+ let pool = this.rootPools.get(parent);
1768
+ if (pool === void 0) {
1769
+ pool = new ActivationPool();
1770
+ this.rootPools.set(parent, pool);
1771
+ }
1772
+ return pool;
1773
+ }
1774
+ /** Perform one tracked materialization through publication or rollback. */
1775
+ async materializeTracked(inputs, parentLineage, pool, releaseSlot) {
1776
+ const { provider, parent } = inputs;
1777
+ let childId = inputs.childId;
1778
+ inputs.signal.throwIfAborted();
1779
+ let resource;
1780
+ if (inputs.kind === "external") {
1781
+ const controller = new AbortController();
1782
+ const cancelStartup = () => {
1783
+ controller.abort(inputs.signal.reason);
1784
+ };
1785
+ inputs.signal.addEventListener("abort", cancelStartup, { once: true });
1786
+ try {
1787
+ const run = await inputs.start(controller.signal);
1788
+ resource = {
1789
+ kind: "external",
1790
+ run,
1791
+ controller
1792
+ };
1793
+ run.result.catch(() => void 0);
1794
+ childId = run.id;
1795
+ try {
1796
+ this.assertChildIdAvailable(childId);
1797
+ } catch (error) {
1798
+ await run.dispose();
1799
+ throw error;
1800
+ }
1801
+ } finally {
1802
+ inputs.signal.removeEventListener("abort", cancelStartup);
1803
+ }
1804
+ } else {
1805
+ const { create } = inputs;
1806
+ let structured;
1807
+ const setup = async (childCtx, child) => {
1808
+ if (create !== void 0) {
1809
+ child.session.append("subagent/descriptor", create.descriptor);
1810
+ appendDelegatedPolicyOverrides(child.session, create.delegatedPolicies);
1811
+ const workingDirectory = childCtx.get("workingDirectory");
1812
+ if (workingDirectory === void 0) throw new Error("local subagents require the working-directory service");
1813
+ await workingDirectory.set(child, create.cwd, inputs.signal);
1814
+ }
1815
+ applyChildComposition(childCtx, parent, inputs.composition);
1816
+ if (inputs.outputSchema !== void 0) structured = attachStructuredRuntime(childCtx, inputs.outputSchema, () => this.resident.get(childId)?.ownedChildren.size === 0 && child.inbox.nextStep.length === 0 && child.inbox.nextTurn.length === 0);
1817
+ };
1818
+ const handle = create === void 0 ? await this.ownerCtx.agents.resume({
1819
+ resumeSessionId: childId,
1820
+ parentAgent: parent,
1821
+ agentOptions: inputs.agentOptions,
1822
+ signal: inputs.signal,
1823
+ setup
1824
+ }) : await this.ownerCtx.agents.create({
1825
+ sessionId: childId,
1826
+ parentAgent: parent,
1827
+ meta: create.meta,
1828
+ ...create.seed === void 0 ? {} : { seed: create.seed },
1829
+ inheritedEventCount: create.inheritedEventCount,
1830
+ agentOptions: inputs.agentOptions,
1831
+ signal: inputs.signal,
1832
+ setup
1833
+ });
1834
+ resource = {
1835
+ kind: "local",
1836
+ handle,
1837
+ outputStart: handle.agent.session.seq,
1838
+ structured,
1839
+ failureAt: void 0,
1840
+ poke: Promise.withResolvers()
1841
+ };
1842
+ }
1843
+ const observer = this.host.observeActivation(provider, childId, parent);
1844
+ const activation = {
1845
+ pool,
1846
+ releaseSlot,
1847
+ childId,
1848
+ ...resource,
1849
+ parent,
1850
+ delivery: inputs.delivery ?? "parent",
1851
+ result: Promise.withResolvers(),
1852
+ closing: void 0,
1853
+ ancestry: new WeakSet(resource.kind === "external" ? parentLineage : [resource.handle.agent, ...parentLineage]),
1854
+ ownedChildren: /* @__PURE__ */ new Set(),
1855
+ observer,
1856
+ announced: false
1857
+ };
1858
+ activation.result.promise.catch(() => void 0);
1859
+ this.resident.set(childId, activation);
1860
+ try {
1861
+ this.assertAdmitting(parent);
1862
+ inputs.signal.throwIfAborted();
1863
+ if (this.ctx.agents.get(parent.id) !== parent) throw new SubagentError("subagent parent is no longer live", "UNAUTHORIZED");
1864
+ this.acquireOwnership(parent, childId);
1865
+ const wakeSettlement = () => {
1866
+ this.wake(activation);
1746
1867
  };
1868
+ if (activation.kind === "local") {
1869
+ observeLocalFailure(activation, wakeSettlement);
1870
+ activation.handle.agent.ctx.on("agent/inbox/inserted", wakeSettlement);
1871
+ activation.handle.agent.ctx.on("agent/inbox/claimed", wakeSettlement);
1872
+ activation.handle.agent.ctx.on("agent/inbox/discarded", wakeSettlement);
1873
+ }
1874
+ observer.start(activationAgent(activation));
1747
1875
  } catch (error) {
1748
- releaseHold();
1876
+ /* v8 ignore next -- rollback failure must not mask the admission failure
1877
+ * that prevented this operation from returning an accepted message id. */
1878
+ await this.rollbackUnpublished(activation, error).catch(() => void 0);
1749
1879
  throw error;
1750
1880
  }
1881
+ return activation;
1751
1882
  }
1752
1883
  /**
1753
- * Deliver one model-authored message to a direct continuable child or to the
1754
- * sender's direct parent. A missing direct child cold-resumes through the
1755
- * ordinary continuation lifecycle.
1756
- * @param sender - exact live Agent authorizing and originating the message.
1757
- * @param targetId - durable direct-parent or direct-child session id.
1758
- * @param content - model-authored content to deliver.
1759
- * @param options - caller cancellation before acceptance.
1760
- * @returns the accepted message's inbox id.
1884
+ * Begin settlement observation after initial acceptance is recorded.
1885
+ * @param activation - accepted activation to observe.
1761
1886
  */
1762
- async sendMessage(sender, targetId, content, options) {
1763
- if (this.ctx.agents.get(sender.id) !== sender) throw new SubagentError("message delivery requires the exact live sender agent", "UNAUTHORIZED");
1764
- this.activations.assertAdmitting(sender);
1765
- const senderActivation = this.activations.get(sender.id);
1766
- if (senderActivation !== void 0 && senderActivation.handle.agent === sender && senderActivation.parentSession === targetId) {
1767
- options.signal.throwIfAborted();
1768
- return this.sendToParent(senderActivation, sender, content);
1887
+ announce(activation) {
1888
+ if (activation.announced) return;
1889
+ activation.announced = true;
1890
+ if (activation.kind === "local") this.watchLocalSettlement(activation);
1891
+ else {
1892
+ const finish = () => this.close(activation, () => this.finishDisposal(activation, false));
1893
+ activation.run.result.then(finish, finish).catch((error) => {
1894
+ this.reportTeardownFailure(activation, error);
1895
+ });
1769
1896
  }
1770
- if (sender.session.header.parentSession === targetId) throw new SubagentError(`agent "${sender.id}" is not a resident continuable child and cannot send to parent "${targetId}"`, "UNAUTHORIZED");
1771
- return this.deliverToChild(sender, targetId, content, {
1772
- signal: options.signal,
1773
- delivery: "steer"
1774
- });
1775
1897
  }
1776
- /**
1777
- * Queue one human-authored prompt as a distinct direct-child turn.
1778
- * @param parent - exact live direct parent authorizing delivery.
1779
- * @param childId - durable direct-child session id.
1780
- * @param content - model-visible prompt blocks.
1781
- * @param source - durable attribution for the human prompt.
1782
- * @param signal - caller cancellation before inbox acceptance.
1783
- * @returns the accepted durable message id.
1784
- */
1785
- async queuePrompt(parent, childId, content, source, signal) {
1786
- return this.deliverToChild(parent, childId, content, {
1787
- source,
1788
- signal,
1789
- delivery: "queue"
1898
+ /** Release an Activation whose start edge was not published. */
1899
+ rollbackUnpublished(activation, error) {
1900
+ return this.close(activation, async () => {
1901
+ try {
1902
+ await (activation.kind === "local" ? activation.handle.dispose() : activation.run.dispose());
1903
+ } finally {
1904
+ this.resident.delete(activation.childId);
1905
+ activation.releaseSlot();
1906
+ this.releaseOwnership(activation.childId);
1907
+ activation.result.reject(error);
1908
+ }
1790
1909
  });
1791
1910
  }
1792
- /**
1793
- * Steer one host-authored prompt to a direct continuable child.
1794
- * @param parent - exact live direct parent authorizing delivery.
1795
- * @param childId - durable direct-child session id.
1796
- * @param content - model-visible prompt blocks.
1797
- * @param source - durable attribution for the host prompt.
1798
- * @param signal - caller cancellation before inbox acceptance.
1799
- * @returns the accepted durable message id.
1800
- */
1801
- async steerPrompt(parent, childId, content, source, signal) {
1802
- return this.deliverToChild(parent, childId, content, {
1803
- source,
1804
- signal,
1805
- delivery: "steer"
1806
- });
1911
+ /** Register the child in a continuation-managed parent's owned set. */
1912
+ acquireOwnership(parent, childId) {
1913
+ const parentActivation = this.resident.get(parent.id);
1914
+ if (parentActivation === void 0) return;
1915
+ if (parentActivation.closing !== void 0) throw new SubagentError(`subagent parent "${parent.id}" is being disposed; the child was not established`, "ACTIVATION_CLOSING");
1916
+ parentActivation.ownedChildren.add(childId);
1917
+ this.wake(parentActivation);
1918
+ }
1919
+ /** Remove one child from its live owner's set and let that owner re-check settlement. */
1920
+ releaseOwnership(childId) {
1921
+ for (const candidate of this.resident.values()) if (candidate.ownedChildren.delete(childId)) this.wake(candidate);
1922
+ }
1923
+ /** Let a settlement watcher re-check residency after relevant state changes. */
1924
+ wake(activation) {
1925
+ if (activation.kind === "external") return;
1926
+ activation.poke.resolve();
1927
+ activation.poke = Promise.withResolvers();
1928
+ }
1929
+ /** Follow one Activation to natural settlement. */
1930
+ watchLocalSettlement(activation) {
1931
+ (async () => {
1932
+ while (true) {
1933
+ const idleObservation = activation.poke;
1934
+ await activation.handle.agent.whenIdle();
1935
+ if (activation.closing !== void 0) return;
1936
+ const readiness = await this.locks.run(activation.childId, () => Promise.resolve(this.settlementState(activation, idleObservation)));
1937
+ if (readiness === "closed") return;
1938
+ if (readiness === "retry") continue;
1939
+ if (readiness === "wait") {
1940
+ await idleObservation.promise;
1941
+ continue;
1942
+ }
1943
+ const finalSeq = activation.handle.agent.session.seq;
1944
+ await this.flushFinalState(activation);
1945
+ const attempt = await this.locks.run(activation.childId, () => {
1946
+ const state = this.settlementState(activation, idleObservation);
1947
+ if (state !== "ready") return Promise.resolve(state);
1948
+ if (activation.handle.agent.session.seq !== finalSeq) return Promise.resolve("retry");
1949
+ let done;
1950
+ try {
1951
+ activation.handle.agent.runMaintenance(() => {
1952
+ done = this.close(activation, () => this.finishDisposal(activation, false));
1953
+ return Promise.resolve();
1954
+ });
1955
+ } catch (_error) {
1956
+ return Promise.resolve("retry");
1957
+ }
1958
+ return Promise.resolve({ done });
1959
+ });
1960
+ if (typeof attempt === "string") {
1961
+ if (attempt === "closed") return;
1962
+ continue;
1963
+ }
1964
+ try {
1965
+ await attempt.done;
1966
+ } catch (error) {
1967
+ this.reportTeardownFailure(activation, error);
1968
+ }
1969
+ return;
1970
+ }
1971
+ })();
1972
+ }
1973
+ /** Report disposal failure once after the natural settlement observer finishes. */
1974
+ reportTeardownFailure(activation, error) {
1975
+ this.ctx.logger.warn(`subagent "${activation.childId}" activation teardown failed: ${failureMessage(error)}`);
1976
+ }
1977
+ /** Check pending input and owned children before closing admission. */
1978
+ settlementState(activation, observation) {
1979
+ if (activation.closing !== void 0) return "closed";
1980
+ if (activation.poke !== observation) return "retry";
1981
+ const inbox = activation.handle.agent.inbox;
1982
+ if (activation.ownedChildren.size > 0) return "wait";
1983
+ if (activation.failureAt === void 0 && (inbox.nextTurn.length > 0 || inbox.nextStep.length > 0)) return "wait";
1984
+ return "ready";
1985
+ }
1986
+ /** Propagate stop synchronously and await descendant startup rollback before release. */
1987
+ async finishDisposal(activation, stop) {
1988
+ this.wake(activation);
1989
+ const { childId } = activation;
1990
+ const failures = [];
1991
+ let result = {
1992
+ output: [],
1993
+ stopReason: "error"
1994
+ };
1995
+ let resultFailure;
1996
+ let externalDisposal;
1997
+ const child = activationAgent(activation);
1998
+ const detail = (error) => child === void 0 ? failureMessage(error) : errorChain(error);
1999
+ try {
2000
+ if (activation.kind === "external") {
2001
+ if (stop) {
2002
+ activation.controller.abort({ kind: "parent" });
2003
+ externalDisposal = Promise.resolve().then(() => activation.run.dispose());
2004
+ externalDisposal.catch(() => void 0);
2005
+ }
2006
+ result = await activation.run.result;
2007
+ } else {
2008
+ if (stop) {
2009
+ activation.handle.agent.cancel({ kind: "parent" });
2010
+ const idle = activation.handle.agent.whenIdle();
2011
+ const children = [...activation.ownedChildren].map((child) => this.resident.get(child)).filter((child) => child !== void 0);
2012
+ const pending = [...this.materializations].filter((item) => item.lineage.includes(activation.handle.agent));
2013
+ const childDisposals = children.map((child) => this.dispose(child));
2014
+ for (const item of pending) item.controller.abort();
2015
+ childDisposals.push(...pending.map((item) => item.settled));
2016
+ const reasons = (await Promise.all(childDisposals.map(async (disposal) => {
2017
+ try {
2018
+ await disposal;
2019
+ return;
2020
+ } catch (error) {
2021
+ return error;
2022
+ }
2023
+ }))).filter((reason) => reason !== void 0);
2024
+ if (reasons.length > 0) failures.push(new SubagentError(`subagent "${childId}" child teardown failed: ${reasons.map((reason) => failureMessage(reason)).join("; ")}`, "ACTIVATION_TEARDOWN_FAILED"));
2025
+ await idle;
2026
+ await this.flushFinalState(activation);
2027
+ }
2028
+ result = captureLocalResult(activation);
2029
+ }
2030
+ } catch (error) {
2031
+ resultFailure = { error };
2032
+ failures.push(new SubagentError(`subagent "${childId}" activation teardown failed: ${detail(error)}`, "ACTIVATION_TEARDOWN_FAILED", { cause: error }));
2033
+ }
2034
+ try {
2035
+ if (activation.kind === "local") await activation.handle.dispose();
2036
+ else await (externalDisposal ?? activation.run.dispose());
2037
+ } catch (error) {
2038
+ failures.push(new SubagentError(`subagent "${childId}" activation handle disposal failed: ${detail(error)}`, "ACTIVATION_TEARDOWN_FAILED", { cause: error }));
2039
+ }
2040
+ let failure;
2041
+ if (failures.length === 1) failure = failures[0];
2042
+ else if (failures.length > 1) failure = new SubagentError(`subagent "${childId}" activation teardown failed at ${failures.length} boundaries: ` + failures.map((item) => detail(item)).join("; "), "ACTIVATION_TEARDOWN_FAILED", { cause: new AggregateError(failures) });
2043
+ this.resident.delete(childId);
2044
+ activation.releaseSlot();
2045
+ const terminal = failure === void 0 ? result : {
2046
+ output: [],
2047
+ stopReason: "error"
2048
+ };
2049
+ this.notifySettlement(activation, result);
2050
+ this.releaseOwnership(childId);
2051
+ activation.observer.settle(terminal);
2052
+ if (resultFailure === void 0) activation.result.resolve(result);
2053
+ else activation.result.reject(resultFailure.error);
2054
+ if (failure !== void 0) throw failure;
2055
+ }
2056
+ /** Deliver the captured execution result to the durable direct parent after cleanup. */
2057
+ notifySettlement(activation, terminal) {
2058
+ if (!activation.announced || activation.delivery === "caller") return;
2059
+ try {
2060
+ const parent = this.ctx.agents.get(activation.parent.id);
2061
+ if (parent !== activation.parent) return;
2062
+ const message = createSettlementMessage(activation.childId, terminal, activationAgent(activation) !== void 0);
2063
+ if (this.closingTeardownFor(parent) !== void 0) {
2064
+ parent.inject(message);
2065
+ return;
2066
+ }
2067
+ this.sendWaking(parent, message, parent.status === "idle" ? "queue" : "steer");
2068
+ } catch (error) {
2069
+ this.ctx.logger.warn(`subagent "${activation.childId}" settlement notice was not delivered to its parent: ` + errorChain(error));
2070
+ }
2071
+ }
2072
+ /** Request a best-effort final session flush before closing natural-settlement admission. */
2073
+ async flushFinalState(activation) {
2074
+ try {
2075
+ await activation.handle.agent.ctx.sessions.flush(activation.handle.agent.session);
2076
+ } catch (error) {
2077
+ 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)}`);
2078
+ }
2079
+ }
2080
+ /** Return capabilities bound to an exact activation instead of a future same-id instance. */
2081
+ receipt(activation) {
2082
+ return {
2083
+ childId: activation.childId,
2084
+ result: activation.result.promise,
2085
+ dispose: () => this.dispose(activation)
2086
+ };
1807
2087
  }
1808
2088
  /** Route one parent-originated delivery through residency and cold resume. */
1809
2089
  async deliverToChild(parent, childId, content, options) {
1810
- this.activations.assertAdmitting(parent);
1811
- const releaseHold = this.activations.holdOwnership(parent, childId);
2090
+ this.assertAdmitting(parent);
2091
+ const releaseHold = this.holdOwnership(parent, childId);
1812
2092
  try {
1813
2093
  return await this.deliverFollowup(parent, childId, content, options);
1814
2094
  } catch (error) {
@@ -1819,47 +2099,38 @@ var SubagentContinuationManager = class {
1819
2099
  /** The delivery loop behind {@link deliverToChild}, run under the parent hold. */
1820
2100
  async deliverFollowup(parent, childId, content, options) {
1821
2101
  while (true) {
1822
- const live = await this.activations.locks.run(childId, async () => {
1823
- const activation = this.activations.get(childId);
2102
+ const live = await this.locks.run(childId, async () => {
2103
+ const activation = this.resident.get(childId);
1824
2104
  if (activation === void 0) return this.coldResume(parent, childId, content, options);
1825
- const disposal = activation.inbox.closing;
2105
+ requireLocalActivation(activation);
2106
+ const disposal = activation.closing;
1826
2107
  /* v8 ignore next 3 -- the send-versus-dispose cutoff needs a delivery to
1827
2108
  * observe the transaction inside the same critical section that opened it. */
1828
2109
  if (disposal !== void 0) return disposal.then(() => void 0, () => void 0);
1829
2110
  if (contentHasImage(content)) {
1830
- await this.assertImageCapable(activation.handle.agent, options.signal);
1831
- if (activation.inbox.closing !== void 0) {
1832
- await Promise.allSettled([activation.inbox.closing]);
2111
+ await this.assertImageCapable(requireLocalActivation(activation).handle.agent, options.signal);
2112
+ if (activation.closing !== void 0) {
2113
+ await Promise.allSettled([activation.closing]);
1833
2114
  return;
1834
2115
  }
1835
2116
  }
1836
2117
  const messageId = this.submitAdmitted(activation, content, options, parent);
1837
- activation.announced = true;
2118
+ this.announce(activation);
1838
2119
  return messageId;
1839
2120
  });
1840
2121
  /* v8 ignore start -- only a delivery that lost the disposal cutoff retries. */
1841
2122
  if (live !== void 0) return live;
1842
- this.activations.assertAdmitting(parent);
2123
+ this.assertAdmitting(parent);
1843
2124
  options.signal.throwIfAborted();
1844
2125
  }
1845
2126
  }
1846
- /**
1847
- * Interrupt one live continuable child's current turn. Admission is
1848
- * synchronous and the cancellation effect is asynchronous. An absent or
1849
- * already-closing target is an accepted no-op after authority checks.
1850
- * @param targetSessionId - the durable child session id to interrupt.
1851
- * @param authority - the human parent address or exact live ancestor Agent.
1852
- */
1853
- interrupt(targetSessionId, authority) {
1854
- this.activations.interrupt(targetSessionId, authority);
1855
- }
1856
2127
  /** Deliver one resident continuable child's message to its live direct parent. */
1857
2128
  sendToParent(activation, sender, content) {
1858
2129
  /* v8 ignore next 6 -- only synchronous re-entrant teardown can open this
1859
2130
  * transaction between exact-agent authorization and this no-await span. */
1860
- if (activation.inbox.closing !== void 0) throw new SubagentError(`subagent "${sender.id}" activation is being disposed; the message was not delivered`, "ACTIVATION_CLOSING");
1861
- const parent = this.ctx.agents.get(activation.parentSession);
1862
- if (parent === void 0) throw new SubagentError("direct parent is not live; the message was not delivered", "PARENT_UNAVAILABLE");
2131
+ if (activation.closing !== void 0) throw new SubagentError(`subagent "${sender.id}" activation is being disposed; the message was not delivered`, "ACTIVATION_CLOSING");
2132
+ const parent = this.ctx.agents.get(activation.parent.id);
2133
+ if (parent !== activation.parent) throw new SubagentError("direct parent is not live; the message was not delivered", "PARENT_UNAVAILABLE");
1863
2134
  const message = createAgentMessage(sender, content);
1864
2135
  this.sendAgentMessage(parent, message);
1865
2136
  return message.id;
@@ -1867,30 +2138,11 @@ var SubagentContinuationManager = class {
1867
2138
  /** Send one Agent message while translating only the target's own rejection. */
1868
2139
  sendAgentMessage(parent, message) {
1869
2140
  try {
1870
- this.activations.sendWaking(parent, message, "steer");
2141
+ this.sendWaking(parent, message, "steer");
1871
2142
  } catch (error) {
1872
2143
  throw new SubagentError("direct parent is not live; the message was not delivered", "PARENT_UNAVAILABLE", { cause: error });
1873
2144
  }
1874
2145
  }
1875
- /** Close manager-wide admission and release every live Activation. */
1876
- async drain() {
1877
- await this.activations.drain();
1878
- }
1879
- /**
1880
- * Stop only the continuable descendants of exact live host-owned parents.
1881
- * @param parents - exact live roots whose continuable descendants must stop.
1882
- */
1883
- async drainDescendants(parents) {
1884
- await this.activations.drainDescendants(parents);
1885
- }
1886
- /**
1887
- * Release selected resident direct children of one exact live parent.
1888
- * @param parent - exact live direct parent authorizing the selected release.
1889
- * @param childIds - durable direct-child ids to release when resident.
1890
- */
1891
- async drainChildren(parent, childIds) {
1892
- await this.activations.drainChildren(parent, childIds);
1893
- }
1894
2146
  /**
1895
2147
  * Cold-resume a persisted child and submit the waiting turn. The descriptor
1896
2148
  * supplies every reconstruction input; no subagent provider is dispatched.
@@ -1911,13 +2163,14 @@ var SubagentContinuationManager = class {
1911
2163
  throw new SubagentError(`subagent "${childId}" is unavailable`, "NOT_RESUMABLE", { cause: error });
1912
2164
  }
1913
2165
  const source = __addDisposableResource$2(env_1, observation, false);
1914
- this.activations.assertAdmitting(parent);
1915
- this.activations.authorizeLineage(parent, childId, source.header.parentSession);
2166
+ this.assertAdmitting(parent);
2167
+ this.authorizeLineage(parent, childId, source.header.parentSession);
1916
2168
  const descriptor = foldSubagentDescriptor(source.events.slice(source.inheritedEventCount));
1917
2169
  if (descriptor === void 0 || descriptor.mode !== "continuable") throw new SubagentError(`subagent "${childId}" has no supported continuation state and cannot be resumed; choose a different target`, "NOT_RESUMABLE");
1918
2170
  let activation;
1919
2171
  try {
1920
- activation = await this.activations.materialize({
2172
+ activation = await this.materialize({
2173
+ kind: "local",
1921
2174
  childId,
1922
2175
  provider: descriptor.provider,
1923
2176
  parent,
@@ -1949,16 +2202,16 @@ var SubagentContinuationManager = class {
1949
2202
  async submitMaterialized(activation, content, options, parent, commit) {
1950
2203
  try {
1951
2204
  if (contentHasImage(content)) {
1952
- await this.assertImageCapable(activation.handle.agent, options.signal);
1953
- if (activation.inbox.closing !== void 0) throw new SubagentError(`subagent "${activation.childId}" is closing`, "ACTIVATION_CLOSING");
2205
+ await this.assertImageCapable(requireLocalActivation(activation).handle.agent, options.signal);
2206
+ if (activation.closing !== void 0) throw new SubagentError(`subagent "${activation.childId}" is closing`, "ACTIVATION_CLOSING");
1954
2207
  }
1955
2208
  const messageId = this.submitAdmitted(activation, content, options, parent);
1956
2209
  commit?.();
1957
- activation.announced = true;
2210
+ this.announce(activation);
1958
2211
  return messageId;
1959
2212
  } catch (error) {
1960
2213
  try {
1961
- await this.activations.dispose(activation);
2214
+ await this.dispose(activation);
1962
2215
  } catch (cleanupError) {
1963
2216
  this.ctx.logger.warn(`subagent continuation: disposal after admission or catalog append failure also failed: ${String(cleanupError)}`);
1964
2217
  }
@@ -1971,7 +2224,16 @@ var SubagentContinuationManager = class {
1971
2224
  content,
1972
2225
  source: options.source
1973
2226
  });
1974
- return this.activations.submitAdmitted(activation, message, options.delivery, parent, options.signal);
2227
+ options.signal.throwIfAborted();
2228
+ this.assertAdmitting(parent);
2229
+ this.authorizeLineage(parent, activation.childId, activation.parent.id);
2230
+ this.acquireOwnership(parent, activation.childId);
2231
+ try {
2232
+ this.deliver(activation, message, options.delivery);
2233
+ } finally {
2234
+ this.wake(activation);
2235
+ }
2236
+ return message.id;
1975
2237
  }
1976
2238
  /** Refuse image content for a child whose fixed model accepts text only. */
1977
2239
  async assertImageCapable(agent, signal) {
@@ -2124,7 +2386,7 @@ async function listDescendants(ctx, rootSessionId, signal) {
2124
2386
  visited.add(entry.id);
2125
2387
  let children;
2126
2388
  try {
2127
- children = await readChildren(entry.id);
2389
+ children = entry.mode === "external" ? [] : await readChildren(entry.id);
2128
2390
  } catch (error) {
2129
2391
  if (error instanceof SubagentError) throw error;
2130
2392
  const code = error instanceof Error && "code" in error ? error.code : void 0;
@@ -2482,8 +2744,7 @@ const subagentIdentityProjectionDefinition = {
2482
2744
  * Provider-side vocabulary for OUT-OF-PROCESS subagent backends — the pieces
2483
2745
  * that enforce this seam's own contracts around a child in another process:
2484
2746
  * the no-capabilities advertisement, timing-bound validation, child
2485
- * working-directory resolution (config override, else the delegating parent
2486
- * session's workspace), the never-reject result settlement, and the standard
2747
+ * working-directory validation, the never-reject result settlement, and the standard
2487
2748
  * run-handle publication. Backends compose these with their own wire drivers;
2488
2749
  * the process machinery itself (spawn, env scrub, managed-range teardown)
2489
2750
  * belongs to the `dsh-subprocess` seam.
@@ -2566,39 +2827,6 @@ function assertUsableCwd(prefix, label, cwd) {
2566
2827
  if (!isEnterableDirectory(cwd)) throw new Error(`${prefix}: ${label} is not an accessible directory: ${cwd}`);
2567
2828
  return cwd;
2568
2829
  }
2569
- /**
2570
- * Validate a configured `cwd` override ONCE, at plugin load: reject the empty
2571
- * string (`path.resolve('')` is the process cwd — it would silently
2572
- * reintroduce the launch-directory fallback this resolution removes),
2573
- * interpret a relative path against the harness launch directory, and require
2574
- * an enterable directory.
2575
- * @param prefix - the consuming plugin's diagnostic prefix.
2576
- * @param cwd - the configured override, or `undefined` when the config omits it.
2577
- * @returns the validated absolute override, or `undefined` when omitted.
2578
- */
2579
- function validateConfiguredCwd(prefix, cwd) {
2580
- if (cwd === void 0) return void 0;
2581
- if (cwd === "") throw new Error(`${prefix}: config cwd must not be empty — omit the key to inherit the parent session cwd`);
2582
- return assertUsableCwd(prefix, "config cwd", resolve(cwd));
2583
- }
2584
- /**
2585
- * Resolve the child's working directory at start: the deployment override
2586
- * when configured (already validated at load), else the parent session's
2587
- * workspace cwd (validated here, its earliest resolvable point). Fails loud
2588
- * when neither exists — falling back to the harness process cwd would
2589
- * silently bind the child to the server's launch directory instead of the
2590
- * delegating session's workspace (one server process serves many sessions,
2591
- * each with its own cwd).
2592
- * @param prefix - the consuming plugin's diagnostic prefix.
2593
- * @param configured - the load-validated override, or `undefined`.
2594
- * @param parentCwd - the delegating parent session's workspace cwd, if any.
2595
- * @returns the absolute child working directory.
2596
- */
2597
- function resolveChildCwd(prefix, configured, parentCwd) {
2598
- if (configured !== void 0) return configured;
2599
- 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`);
2600
- return assertUsableCwd(prefix, "parent session cwd", parentCwd);
2601
- }
2602
2830
  /** Normalize an unknown thrown value to an Error (the catch binding is `unknown`). */
2603
2831
  function toError(value) {
2604
2832
  /* v8 ignore next */
@@ -2646,13 +2874,12 @@ async function settleRunResult(parts) {
2646
2874
  * local cancellation — there is no assumption the child cooperates — and then
2647
2875
  * awaits the backend's teardown to actual exit.
2648
2876
  * @param parts - the run identity, result, cancellation wiring, and teardown.
2649
- * @returns the seam run handle (`localAgent` is `undefined` for remote runs).
2877
+ * @returns the external run handle.
2650
2878
  */
2651
2879
  function subprocessRunHandle(parts) {
2652
2880
  let disposal;
2653
2881
  return {
2654
2882
  id: parts.id,
2655
- localAgent: void 0,
2656
2883
  result: parts.result,
2657
2884
  dispose() {
2658
2885
  if (disposal !== void 0) return disposal;
@@ -2664,80 +2891,6 @@ function subprocessRunHandle(parts) {
2664
2891
  };
2665
2892
  }
2666
2893
  //#endregion
2667
- //#region lib/types/run-settlement.js
2668
- /**
2669
- * Settlement of one ONE-SHOT subagent run into a background-Task outcome. Only
2670
- * the one-shot background path uses Jobs; continuable children have no Task,
2671
- * no per-message result, and no Task cancellation.
2672
- *
2673
- * @module @deepseek-ai/dsh-subagent/run-settlement
2674
- */
2675
- /** Flatten a child's final output blocks to the task's final text. */
2676
- function finalText(blocks) {
2677
- return blocks.filter((block) => block.type === "text").map((block) => block.text).join("");
2678
- }
2679
- /** Render a failed stop reason with optional provider-authored detail. */
2680
- function failureDetail(result) {
2681
- const stopReason = result.stopReason;
2682
- return result.diagnostic === void 0 ? stopReason : `${stopReason}; diagnostic: ${result.diagnostic}`;
2683
- }
2684
- /**
2685
- * Map a child result to the task outcome: completed carries final text, local
2686
- * cancellation (`aborted` without a diagnostic) is killed, and provider-
2687
- * diagnosed remote aborts plus every other reason are failed without partial
2688
- * output.
2689
- * @param result - child terminal result.
2690
- * @returns outcome for the `ctx.jobs` registration.
2691
- */
2692
- function runOutcome(result) {
2693
- switch (result.stopReason) {
2694
- case "completed": return {
2695
- status: "completed",
2696
- result: finalText(result.output)
2697
- };
2698
- case "aborted": return result.diagnostic === void 0 ? { status: "killed" } : {
2699
- status: "failed",
2700
- detail: failureDetail(result)
2701
- };
2702
- case "error":
2703
- case "max-tokens":
2704
- case "refusal": return {
2705
- status: "failed",
2706
- detail: failureDetail(result)
2707
- };
2708
- default: return {
2709
- status: "failed",
2710
- detail: failureDetail(result)
2711
- };
2712
- }
2713
- }
2714
- /**
2715
- * Await the child result, dispose the run, then return its task outcome. Result
2716
- * and disposal failures become `failed`; when both fail, both details survive.
2717
- * @param run - live run to settle and release.
2718
- * @returns outcome after child resources are released.
2719
- */
2720
- async function settleRun(run) {
2721
- let outcome;
2722
- try {
2723
- outcome = runOutcome(await run.result);
2724
- } catch (error) {
2725
- outcome = {
2726
- status: "failed",
2727
- detail: String(error)
2728
- };
2729
- }
2730
- try {
2731
- await run.dispose();
2732
- } catch (error) {
2733
- return {
2734
- status: "failed",
2735
- detail: `${outcome.detail === void 0 ? "" : `${outcome.detail}; `}dispose failed: ${String(error)}`
2736
- };
2737
- }
2738
- return outcome;
2739
- }
2740
- //#endregion
2741
2894
  //#region lib/types/index.js
2742
2895
  var __runInitializers = function(thisArg, initializers, value) {
2743
2896
  var useValue = arguments.length > 2;
@@ -2777,7 +2930,7 @@ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializ
2777
2930
  if (target) Object.defineProperty(target, contextIn.name, descriptor);
2778
2931
  done = true;
2779
2932
  };
2780
- /** Named provider registry with one-shot runs, durable discovery, and continuable-child operations. */
2933
+ /** Named provider registry with managed activations, durable discovery, and local child messaging. */
2781
2934
  let SubagentRuntime = (() => {
2782
2935
  let _classSuper = TypertRemoteService;
2783
2936
  let _instanceExtraInitializers = [];
@@ -2822,8 +2975,9 @@ let SubagentRuntime = (() => {
2822
2975
  maxDepth: z.number().step(1).min(0).max(Number.MAX_SAFE_INTEGER).default(1).volatile(),
2823
2976
  maxActiveSubagents: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(8).volatile()
2824
2977
  });
2978
+ static inject = ["workingDirectory"];
2825
2979
  providers = /* @__PURE__ */ new Map();
2826
- continuations;
2980
+ manager;
2827
2981
  /**
2828
2982
  * The contained lifecycle-edge publisher. Built here because scoped dispatch
2829
2983
  * keys its carrier by this exact service instance, whose own context filter
@@ -2835,21 +2989,23 @@ let SubagentRuntime = (() => {
2835
2989
  this.config = config;
2836
2990
  this.emitLifecycle = createLifecycleEmitter(this.ctx, (parent) => scopeTarget(this, parent));
2837
2991
  ctx.inject(["agents"], (childCtx) => {
2838
- const manager = new SubagentContinuationManager(childCtx, {
2992
+ const manager = new SubagentManager(childCtx, {
2993
+ startExternal: (name, request) => {
2994
+ return this.expectProvider(name).start(request);
2995
+ },
2839
2996
  prepareContinuable: (name, request) => this.prepareContinuable(name, request),
2840
2997
  observeActivation: (provider, childId, parent) => this.observeActivation(provider, childId, parent)
2841
2998
  }, () => this.config.maxActiveSubagents.get());
2842
- this.continuations = manager;
2999
+ this.manager = manager;
2843
3000
  childCtx.effect(() => () => {
2844
3001
  /* v8 ignore else -- one injected binding owns the slot until its fiber disposes. */
2845
- if (this.continuations === manager) this.continuations = void 0;
2846
- }, "subagents.continuationBinding()");
3002
+ if (this.manager === manager) this.manager = void 0;
3003
+ }, "subagents.managerBinding()");
2847
3004
  });
2848
3005
  ctx.inject(["sessionProjections"], (projectionCtx) => {
2849
- const projections = projectionCtx.sessionProjections;
2850
- projections.register(subagentCatalogProjectionDefinition);
2851
- projections.register(subagentTimingProjectionDefinition);
2852
- projections.register(subagentIdentityProjectionDefinition);
3006
+ projectionCtx.sessionProjections.register(subagentCatalogProjectionDefinition);
3007
+ projectionCtx.sessionProjections.register(subagentTimingProjectionDefinition);
3008
+ projectionCtx.sessionProjections.register(subagentIdentityProjectionDefinition);
2853
3009
  });
2854
3010
  ctx.inject(["agents"], (agentsCtx) => {
2855
3011
  installSubagentArchiveAdmission(agentsCtx);
@@ -2868,16 +3024,32 @@ let SubagentRuntime = (() => {
2868
3024
  return depth;
2869
3025
  }
2870
3026
  /**
2871
- * Establish one durable continuable child and deliver its initial prompt.
2872
- * Resolves when the child's inbox accepts that prompt, without waiting for the
2873
- * turn to start or for the message to reach the Session log; any earlier
2874
- * failure rejects with no ids and rolls back the child entirely.
2875
- * @param spec - provider, delegation request, and caller cancellation.
2876
- * @returns the durable child id and the accepted prompt's message id.
2877
- * @throws when continuation services are unavailable or materialization fails.
3027
+ * Establish a managed child with backend-specific execution capabilities.
3028
+ * @param spec - task, backend, and result recipient.
3029
+ * @returns identities, execution result, and exact-activation disposal. A reserved
3030
+ * childId requires a local backend and guarantees an accepted messageId.
2878
3031
  */
2879
- async startContinuable(spec) {
2880
- return this.requireContinuations().startContinuable(spec);
3032
+ async startActivation(spec) {
3033
+ const provider = this.expectProvider(spec.provider);
3034
+ if (spec.childId !== void 0 && provider.prepareContinuable === void 0) throw new SubagentError("reserved child ids require a local backend", "UNSUPPORTED_CAPABILITY");
3035
+ assertSubagentMaxDepth(spec.request.maxDepth);
3036
+ this.assertCapabilities(provider, {
3037
+ ...spec.request,
3038
+ signal: spec.signal
3039
+ });
3040
+ const manager = this.requireManager();
3041
+ if (this.ctx.get("agents")?.get(spec.request.parent.id) !== spec.request.parent) throw new SubagentError("subagent creation requires the exact live parent agent", "UNAUTHORIZED");
3042
+ if (spec.request.outputSchema !== void 0) assertObjectJsonSchema(spec.request.outputSchema);
3043
+ return provider.prepareContinuable === void 0 ? manager.startExternal(spec) : manager.startLocal(spec);
3044
+ }
3045
+ /**
3046
+ * Join progressing descendants without cancelling them. Idle descendants whose
3047
+ * inboxes require a later wake stay resident and do not delay host completion.
3048
+ * @param parent - the exact parent whose descendant work is observed.
3049
+ * @returns whether work was joined; hosts recheck parent idle after true.
3050
+ */
3051
+ async waitForChildren(parent) {
3052
+ return this.manager?.waitForChildren(parent) ?? false;
2881
3053
  }
2882
3054
  /**
2883
3055
  * Steer one model-authored message to the sender's direct parent or direct
@@ -2894,7 +3066,7 @@ let SubagentRuntime = (() => {
2894
3066
  * or the message was not admitted.
2895
3067
  */
2896
3068
  async sendMessage(sender, targetId, content, options) {
2897
- return this.requireContinuations().sendMessage(sender, targetId, content, options);
3069
+ return this.requireManager().sendMessage(sender, targetId, content, options);
2898
3070
  }
2899
3071
  /**
2900
3072
  * Deliver one host-protocol message to a direct continuable child.
@@ -2909,16 +3081,16 @@ let SubagentRuntime = (() => {
2909
3081
  * @returns the accepted message's inbox id.
2910
3082
  */
2911
3083
  [deliverSubagentPrompt](parent, childId, content, source, signal, delivery) {
2912
- return delivery === "steer" ? this.requireContinuations().steerPrompt(parent, childId, content, source, signal) : this.requireContinuations().queuePrompt(parent, childId, content, source, signal);
3084
+ return delivery === "steer" ? this.requireManager().steerPrompt(parent, childId, content, source, signal) : this.requireManager().queuePrompt(parent, childId, content, source, signal);
2913
3085
  }
2914
3086
  /**
2915
- * Interrupt one live continuable child's current turn under a human parent
3087
+ * Interrupt one live child's current execution under a human parent
2916
3088
  * address or an exact live ancestor Agent. Fire-and-return: the cancel
2917
3089
  * signal is issued before this returns, but the target may keep running
2918
3090
  * until it observes the signal. Unclaimed pending inbox work, the Activation,
2919
3091
  * and published descendants are preserved; claimed work is not requeued.
2920
- * Once the interrupted driver is idle, a waking send resumes the parked FIFO
2921
- * queue. An absent target — including a one-shot or unknown id —
3092
+ * Once the interrupted Agent is idle, a waking send resumes the parked FIFO
3093
+ * queue. External backends stop their single execution. An absent target
2922
3094
  * is an accepted no-op, as is a manager-less composition, which cannot own a
2923
3095
  * live Activation.
2924
3096
  * @param targetSessionId - the durable child session id to interrupt.
@@ -2927,35 +3099,35 @@ let SubagentRuntime = (() => {
2927
3099
  * live target.
2928
3100
  */
2929
3101
  interrupt(targetSessionId, authority) {
2930
- this.continuations?.interrupt(targetSessionId, authority);
3102
+ this.manager?.interrupt(targetSessionId, authority);
2931
3103
  }
2932
3104
  /**
2933
- * Close continuable admission below exact live parent Agents, stop only their
3105
+ * Close subagent admission below exact live parent Agents, stop only their
2934
3106
  * visible descendant Activations synchronously, then await admitted scoped
2935
3107
  * materializations and release those forests child-first. The scoped cutoff
2936
3108
  * lasts until each exact parent leaves the registry; unrelated parent trees
2937
3109
  * remain live.
2938
3110
  * @param parents - exact host-owned parent Agents entering teardown.
2939
- * @returns once every retained descendant Activation released its `AgentHandle`.
3111
+ * @returns once every retained descendant activation released its execution handle.
2940
3112
  * @throws an aggregate error after all branches settle when any failed.
2941
3113
  */
2942
- async drainContinuableDescendants(parents) {
2943
- const manager = this.continuations;
3114
+ async drainDescendants(parents) {
3115
+ const manager = this.manager;
2944
3116
  if (manager === void 0) return;
2945
3117
  await manager.drainDescendants(parents);
2946
3118
  }
2947
3119
  /**
2948
- * Release selected resident continuable direct children of one exact live
3120
+ * Release selected resident direct children of one exact live
2949
3121
  * parent. Other children of the same parent remain admitted and resident.
2950
3122
  * Absent targets and a manager-less composition are accepted no-ops.
2951
3123
  * @param parent - exact live direct parent authorizing the selected release.
2952
3124
  * @param childIds - durable direct-child ids to release when resident.
2953
- * @returns once every selected Activation released its `AgentHandle`.
3125
+ * @returns once every selected activation released its execution handle.
2954
3126
  * @throws {SubagentError} `UNAUTHORIZED` when a resident target belongs to a
2955
3127
  * different parent or the supplied parent identity is stale.
2956
3128
  */
2957
- async drainContinuableChildren(parent, childIds) {
2958
- const manager = this.continuations;
3129
+ async drainChildren(parent, childIds) {
3130
+ const manager = this.manager;
2959
3131
  if (manager === void 0) return;
2960
3132
  await manager.drainChildren(parent, childIds);
2961
3133
  }
@@ -2974,7 +3146,8 @@ let SubagentRuntime = (() => {
2974
3146
  /**
2975
3147
  * Recursively list reachable parent catalogs in stable pre-order, preserving
2976
3148
  * each catalog's event order. Each row carries its catalog parent and depth;
2977
- * one-shot and unknown-mode children remain traversal nodes. Unknown modes
3149
+ * external children are leaves; one-shot and unknown-mode children remain
3150
+ * traversal nodes. Unknown modes
2978
3151
  * produce unsupported diagnostics. Unreadable child catalogs produce corrupt
2979
3152
  * or unavailable diagnostics and stop only that branch. Root read failures,
2980
3153
  * missing services or projections, and cancellation reject the whole listing.
@@ -3069,12 +3242,14 @@ let SubagentRuntime = (() => {
3069
3242
  /**
3070
3243
  * Register a provider under its name. Registration is effect-scoped and HMR
3071
3244
  * safe; removing a provider blocks new starts but does not revoke runs that
3072
- * were already returned to their holders.
3245
+ * were already returned to their holders. Providers without either execution
3246
+ * method are rejected with UNSUPPORTED_CAPABILITY before registration.
3073
3247
  * @param provider - the trusted provider implementation.
3074
3248
  * @returns the exact Cordis effect disposer.
3075
3249
  */
3076
3250
  registerProvider(provider) {
3077
3251
  const name = provider.name;
3252
+ if (provider.start === void 0 && provider.prepareContinuable === void 0) throw new SubagentError(`subagent provider "${name}" must implement start or prepareContinuable`, "UNSUPPORTED_CAPABILITY");
3078
3253
  return this.ctx.effect(function* () {
3079
3254
  if (this.providers.has(name)) throw new SubagentError(`a subagent provider named "${name}" is already registered`, "DUPLICATE_PROVIDER");
3080
3255
  this.providers.set(name, provider);
@@ -3101,55 +3276,12 @@ let SubagentRuntime = (() => {
3101
3276
  return [...this.providers.keys()];
3102
3277
  }
3103
3278
  /**
3104
- * Establish a published child on the named provider. Capability and semantic
3105
- * checks run before delegation. Provider ownership lasts until its promise
3106
- * fulfills; a rejection therefore has no run for the caller to dispose and
3107
- * emits no run lifecycle events. Post-publication turn and infrastructure
3108
- * failures settle through the returned run.
3109
- * A catalog append failure disposes the run and handles its result rejection;
3110
- * the caller receives the catalog error even if disposal also fails.
3111
- * @param name - the provider to use.
3112
- * @param request - child label, prompt, parent, signal, and optional capabilities.
3113
- * @returns the published holder-owned run.
3114
- */
3115
- async start(name, request) {
3116
- const provider = this.expectProvider(name);
3117
- this.assertCapabilities(provider, request);
3118
- assertSubagentMaxDepth(request.maxDepth);
3119
- if (request.outputSchema !== void 0) assertObjectJsonSchema(request.outputSchema);
3120
- const descriptor = snapshotSubagentDescriptor({
3121
- mode: "one-shot",
3122
- provider: name,
3123
- ...request.label !== void 0 ? { label: request.label } : {}
3124
- });
3125
- const resolved = {
3126
- ...request,
3127
- descriptor
3128
- };
3129
- const run = await provider.start(resolved);
3130
- const child = run.localAgent?.session;
3131
- if (child !== void 0) try {
3132
- establishCatalogChild(request.parent.session, child.header, descriptor);
3133
- } catch (error) {
3134
- run.result.catch(() => void 0);
3135
- try {
3136
- await run.dispose();
3137
- } catch (cleanupError) {
3138
- this.ctx.logger.warn(`subagent: disposal after catalog append failure also failed: ${String(cleanupError)}`);
3139
- }
3140
- throw error;
3141
- }
3142
- return observeRun(this.emitLifecycle, name, request.parent, run);
3143
- }
3144
- /**
3145
3279
  * Resolve one provider's detached continuable-creation contribution. Method
3146
3280
  * presence on the provider IS the capability, so a provider without it is
3147
3281
  * rejected before the manager reserves any child resources.
3148
3282
  */
3149
3283
  async prepareContinuable(name, request) {
3150
- const provider = this.expectProvider(name);
3151
- if (provider.prepareContinuable === void 0) throw new SubagentError(`subagent provider "${provider.name}" does not support continuable children (no prepareContinuable capability)`, "UNSUPPORTED_CAPABILITY");
3152
- return provider.prepareContinuable(request);
3284
+ return this.expectProvider(name).prepareContinuable(request);
3153
3285
  }
3154
3286
  /** Look up a provider for dispatch or fail loud. */
3155
3287
  expectProvider(name) {
@@ -3157,10 +3289,10 @@ let SubagentRuntime = (() => {
3157
3289
  if (provider === void 0) throw new SubagentError(`no subagent provider registered for "${name}"`, "NO_PROVIDER");
3158
3290
  return provider;
3159
3291
  }
3160
- /** Resolve the optional continuable-subagent manager or fail loud. */
3161
- requireContinuations() {
3162
- if (this.continuations === void 0) throw new SubagentError("continuable subagents require the agents service", "CONTINUATION_UNAVAILABLE");
3163
- return this.continuations;
3292
+ /** Resolve the subagent manager or fail loud. */
3293
+ requireManager() {
3294
+ if (this.manager === void 0) throw new SubagentError("continuable subagents require the agents service", "CONTINUATION_UNAVAILABLE");
3295
+ return this.manager;
3164
3296
  }
3165
3297
  /**
3166
3298
  * Build the lifecycle observer for one continuable Activation's residency
@@ -3198,4 +3330,4 @@ let SubagentRuntime = (() => {
3198
3330
  };
3199
3331
  })();
3200
3332
  //#endregion
3201
- export { AssistantOutputFold, NO_START_CAPABILITIES, SUBAGENT_DESCRIPTOR_VERSION, SubagentDepthError, SubagentError, SubagentRunId, SubagentRuntime, SubagentRuntime as default, appendDelegatedPolicyOverrides, applyChildComposition, assertPositiveFinite, assertSubagentMaxDepth, assertUsableCwd, captureDelegatedPolicyOverrides, childSessionMeta, delegationDepthOf, finalAssistantOutput, foldSubagentDescriptor, parentAgentOptionsForDelegation, resolveChildAgentOptions, resolveChildCwd, resolveChildDepth, settleRun, settleRunResult, snapshotSubagentDescriptor, subprocessRunHandle, validateConfiguredCwd };
3333
+ export { AssistantOutputFold, NO_START_CAPABILITIES, STRUCTURED_OUTPUT_TOOL, SUBAGENT_DESCRIPTOR_VERSION, SubagentDepthError, SubagentError, SubagentRunId, SubagentRuntime, SubagentRuntime as default, appendDelegatedPolicyOverrides, applyChildComposition, assertPositiveFinite, assertSubagentMaxDepth, assertUsableCwd, captureDelegatedPolicyOverrides, childSessionMeta, delegationDepthOf, finalAssistantOutput, foldSubagentDescriptor, parentAgentOptionsForDelegation, resolveChildAgentOptions, resolveChildDepth, settleRunResult, snapshotSubagentDescriptor, subprocessRunHandle };