@deepseek-ai/dsh-subagent 0.1.2-alpha.5 → 0.1.3-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.
package/lib/index.js CHANGED
@@ -1,13 +1,13 @@
1
- import { AttachmentError, admitPromptContent } from "@deepseek-ai/dsh-attachment";
2
1
  import { scopeTarget } from "@deepseek-ai/dsh-scope";
3
2
  import { assertObjectJsonSchema } from "@deepseek-ai/dsh-tools";
4
3
  import { canonicalClientTimeZone } from "@deepseek-ai/dsh-util-time";
5
4
  import { Remote, RemoteError, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
5
+ import { AttachmentError } from "@deepseek-ai/dsh-attachment";
6
6
  import { z } from "zod";
7
- import { HarnessError, ReasoningEffortId, boundContextSummary, contentHasImage, createUserMessage, errorChain } from "@deepseek-ai/dsh-llm";
7
+ import { HarnessError, ReasoningEffortId, boundContextSummary, contentHasImage, createUserMessage, errorChain, joinAssistantStreamText } from "@deepseek-ai/dsh-llm";
8
8
  import { randomUUID } from "node:crypto";
9
9
  import { foldConsumedWork } from "@deepseek-ai/dsh-agent";
10
- import { Session, SessionLogOffset, SessionSeq } from "@deepseek-ai/dsh-session";
10
+ import { SessionLogOffset, SessionSeq } from "@deepseek-ai/dsh-session";
11
11
  import { brandString } from "@deepseek-ai/dsh-brand";
12
12
  import { snapshotJsonValue } from "@deepseek-ai/dsh-util-values";
13
13
  import { accessSync, constants, statSync } from "node:fs";
@@ -40,7 +40,8 @@ const CONTROL_ID_SCHEMAS = {
40
40
  "subagent.prompt": z.object({
41
41
  parentSessionId: SESSION_ID_SCHEMA,
42
42
  childSessionId: SESSION_ID_SCHEMA,
43
- mode: z.literal("continuable")
43
+ mode: z.literal("continuable"),
44
+ delivery: z.enum(["queue", "steer"])
44
45
  }),
45
46
  "subagent.interrupt": z.object({
46
47
  parentSessionId: SESSION_ID_SCHEMA,
@@ -175,15 +176,16 @@ var AssistantOutputFold = class {
175
176
  partial = [];
176
177
  /**
177
178
  * Fold one session event: a non-empty assistant message becomes the
178
- * candidate final answer, and a `text-delta` chunk extends the streamed
179
- * fallback; every other event contributes nothing.
179
+ * candidate final answer, while its embedded stream and any log-only attempt
180
+ * extend the streamed fallback; every other event contributes nothing.
180
181
  * @param event - the next observed session event.
181
182
  */
182
183
  push(event) {
183
184
  if (event.type === "assistant/message") {
184
185
  const content = event.data.message.content;
185
186
  if (content.length > 0) this.message = content;
186
- } else if (event.type === "assistant/chunk" && event.data.chunk.type === "text-delta") this.pushText(event.data.chunk.text);
187
+ }
188
+ if (event.type === "assistant/message" || event.type === "assistant/attempt") this.pushText(joinAssistantStreamText(event.data.stream));
187
189
  }
188
190
  /**
189
191
  * Extend the streamed fallback with text observed outside session events.
@@ -395,166 +397,6 @@ function renderThrown(value) {
395
397
  }
396
398
  }
397
399
  //#endregion
398
- //#region lib/types/descriptor.js
399
- /**
400
- * The durable subagent-child descriptor: the versioned, model-hidden
401
- * `subagent/descriptor` session event that identifies every session-backed
402
- * subagent and records whether it is one-shot or continuable. Continuable
403
- * descriptors additionally preserve the declared composition required for
404
- * cold resume. Providers append it turn-enclosed in the child's initial turn.
405
- *
406
- * The descriptor deliberately snapshots explicit fields rather than the
407
- * merge-extensible `AgentOptions` object: an unrelated extension value cannot
408
- * make continuation fail merely because it is not JSON, and later composition
409
- * inputs require a deliberate {@link SUBAGENT_DESCRIPTOR_VERSION} change. It
410
- * omits `subagentDepth` — cold resume trusts the persisted header's
411
- * `delegationDepth` as the monotone floor — and `outputSchema`, which belongs
412
- * to one activation's result contract rather than durable child composition.
413
- * Per-activation knobs such as `maxTokens` are omitted for the same reason as
414
- * `outputSchema`: they budget one activation. Cold resume requires the exact
415
- * live parent for authorization but reconstructs child options only from the
416
- * durable descriptor, so it neither restores the prior budget nor inherits
417
- * the parent's current one; the resumed route's defaults apply instead.
418
- *
419
- * @module @deepseek-ai/dsh-subagent/descriptor
420
- */
421
- /**
422
- * The current descriptor format version, stamped into every appended
423
- * `subagent/descriptor` event and required verbatim by {@link foldSubagentDescriptor}.
424
- * Supporting another composition input is a deliberate version change, never
425
- * an implicit extra field.
426
- */
427
- const SUBAGENT_DESCRIPTOR_VERSION = 3;
428
- const DESCRIPTOR_BASE_KEYS = [
429
- "version",
430
- "mode",
431
- "provider",
432
- "label"
433
- ];
434
- const ONE_SHOT_DESCRIPTOR_KEYS = new Set(DESCRIPTOR_BASE_KEYS);
435
- const CONTINUABLE_DESCRIPTOR_KEYS = new Set([
436
- ...DESCRIPTOR_BASE_KEYS,
437
- "agentProvider",
438
- "agentModel",
439
- "agentReasoningEffort",
440
- "persona",
441
- "toolFilter"
442
- ]);
443
- const TOOL_FILTER_KEYS = new Set(["allow", "deny"]);
444
- /** Whether a persisted JSON value is an object record. */
445
- function isRecord(value) {
446
- return typeof value === "object" && value !== null && !Array.isArray(value);
447
- }
448
- /** Reject fields outside one versioned record's declared schema. */
449
- function assertKnownKeys(value, keys, path) {
450
- const unknown = Object.keys(value).find((key) => !keys.has(key));
451
- if (unknown !== void 0) throw new Error(`persisted subagent descriptor ${path} has unknown field "${unknown}"`);
452
- }
453
- /** Read one optional string field from a persisted descriptor record. */
454
- function optionalString(value, key) {
455
- if (!Object.hasOwn(value, key)) return void 0;
456
- const field = value[key];
457
- if (typeof field !== "string") throw new Error(`persisted subagent descriptor ${key} must be a string`);
458
- return field;
459
- }
460
- /** Read one optional string-array field from a persisted tool restriction. */
461
- function optionalStringArray(value, key) {
462
- if (!Object.hasOwn(value, key)) return void 0;
463
- const field = value[key];
464
- if (!Array.isArray(field)) throw new Error(`persisted subagent descriptor toolFilter.${key} must be an array of strings`);
465
- const items = field;
466
- if (items.some((item) => typeof item !== "string")) throw new Error(`persisted subagent descriptor toolFilter.${key} must be an array of strings`);
467
- return items;
468
- }
469
- /** Validate and reconstruct a persisted tool restriction. */
470
- function parseToolFilter(value) {
471
- if (!isRecord(value)) throw new Error("persisted subagent descriptor toolFilter must be an object");
472
- assertKnownKeys(value, TOOL_FILTER_KEYS, "toolFilter");
473
- const allow = optionalStringArray(value, "allow");
474
- const deny = optionalStringArray(value, "deny");
475
- if (allow === void 0 && deny === void 0) throw new Error("persisted subagent descriptor toolFilter must declare allow and/or deny");
476
- return {
477
- ...allow !== void 0 ? { allow } : {},
478
- ...deny !== void 0 ? { deny } : {}
479
- };
480
- }
481
- /** Validate one persisted descriptor payload for the current runtime. */
482
- function parseSubagentDescriptor(value) {
483
- if (!isRecord(value)) throw new Error("persisted subagent descriptor payload must be an object");
484
- const version = value["version"];
485
- if (typeof version !== "number") throw new Error("persisted subagent descriptor version must be a number");
486
- if (version !== 3) return void 0;
487
- const mode = value["mode"];
488
- if (mode !== "one-shot" && mode !== "continuable") throw new Error("persisted subagent descriptor mode must be \"one-shot\" or \"continuable\"");
489
- assertKnownKeys(value, mode === "one-shot" ? ONE_SHOT_DESCRIPTOR_KEYS : CONTINUABLE_DESCRIPTOR_KEYS, "payload");
490
- const provider = value["provider"];
491
- if (typeof provider !== "string") throw new Error("persisted subagent descriptor provider must be a string");
492
- if (mode === "one-shot") {
493
- const label = optionalString(value, "label");
494
- return {
495
- version: 3,
496
- mode,
497
- provider,
498
- ...label !== void 0 ? { label } : {}
499
- };
500
- }
501
- const label = value["label"];
502
- if (typeof label !== "string") throw new Error("persisted subagent descriptor label must be a string");
503
- const agentProvider = optionalString(value, "agentProvider");
504
- const agentModel = optionalString(value, "agentModel");
505
- const agentReasoningEffort = optionalString(value, "agentReasoningEffort");
506
- const persona = optionalString(value, "persona");
507
- const toolFilter = Object.hasOwn(value, "toolFilter") ? parseToolFilter(value["toolFilter"]) : void 0;
508
- return {
509
- version: 3,
510
- mode,
511
- provider,
512
- label,
513
- ...agentProvider !== void 0 ? { agentProvider } : {},
514
- ...agentModel !== void 0 ? { agentModel } : {},
515
- ...agentReasoningEffort !== void 0 ? { agentReasoningEffort } : {},
516
- ...persona !== void 0 ? { persona } : {},
517
- ...toolFilter !== void 0 ? { toolFilter } : {}
518
- };
519
- }
520
- function snapshotSubagentDescriptor(input) {
521
- const snapshot = snapshotJsonValue(input.mode === "one-shot" ? {
522
- version: 3,
523
- mode: input.mode,
524
- provider: input.provider,
525
- ...input.label !== void 0 ? { label: input.label } : {}
526
- } : {
527
- version: 3,
528
- mode: input.mode,
529
- provider: input.provider,
530
- label: input.label,
531
- ...input.agentProvider !== void 0 ? { agentProvider: input.agentProvider } : {},
532
- ...input.agentModel !== void 0 ? { agentModel: input.agentModel } : {},
533
- ...input.agentReasoningEffort !== void 0 ? { agentReasoningEffort: input.agentReasoningEffort } : {},
534
- ...input.persona !== void 0 ? { persona: input.persona } : {},
535
- ...input.toolFilter !== void 0 ? { toolFilter: input.toolFilter } : {}
536
- });
537
- if (snapshot === void 0) throw new Error("subagent descriptor is not losslessly JSON-serializable");
538
- return snapshot;
539
- }
540
- /**
541
- * Fold a persisted child log to its supported descriptor. The first
542
- * `subagent/descriptor` event is authoritative — the establishing provider
543
- * appends exactly one, so a later same-type event cannot rewrite the declared
544
- * composition.
545
- * @param events - the loaded child session events.
546
- * @returns the descriptor, or `undefined` when the log has none or its
547
- * version is not {@link SUBAGENT_DESCRIPTOR_VERSION} (the child cannot be
548
- * classified by this runtime).
549
- * @throws when a current-version persisted payload does not match its complete
550
- * declared schema.
551
- */
552
- function foldSubagentDescriptor(events) {
553
- const event = events.find((candidate) => candidate.type === "subagent/descriptor");
554
- if (event === void 0) return void 0;
555
- return parseSubagentDescriptor(event.data);
556
- }
557
- //#endregion
558
400
  //#region lib/types/child-agent.js
559
401
  /**
560
402
  * Shared in-process child composition: the delegation-depth budget, the
@@ -704,8 +546,8 @@ function applyChildComposition(childCtx, parent, composition) {
704
546
  text: SUBAGENT_DELEGATION_CONTEXT
705
547
  });
706
548
  if (composition.persona !== void 0) childCtx.systemPrompt.section({
707
- name: "deployment:persona",
708
- order: childCtx.systemPrompt.getSectionOrder("DEPLOYMENT_PERSONA"),
549
+ name: "deployment:persona-prefix",
550
+ order: childCtx.systemPrompt.getSectionOrder("DEPLOYMENT_PERSONA_PREFIX"),
709
551
  text: composition.persona
710
552
  });
711
553
  if (composition.toolFilter !== void 0) childCtx.tools.restrict(composition.toolFilter);
@@ -746,144 +588,12 @@ function appendDelegatedPolicyOverrides(childSession, overrides) {
746
588
  });
747
589
  }
748
590
  //#endregion
749
- //#region lib/types/descriptor-seed.js
750
- /**
751
- * Seeding of a continuable child's durable descriptor event: the model-hidden
752
- * record of the child's declared composition before its first request, so a
753
- * later cold resume can reconstruct it from its own log.
754
- *
755
- * @module @deepseek-ai/dsh-subagent/descriptor-seed
756
- */
757
- /**
758
- * Build the child's creation seed: any inherited parent-history prefix followed
759
- * by one model-hidden, between-turn `descriptor` event. Staging through a
760
- * `Session` assigns the sequence number and enforces the same lossless-JSON
761
- * rules the durable log does.
762
- * @param childId - the reserved child session id the staged log belongs to.
763
- * @param seed - the inherited completed-turn prefix, or `undefined` for a fresh child.
764
- * @param descriptor - the snapshotted composition record to persist.
765
- * @returns the complete seed events, contiguous from sequence zero.
766
- */
767
- function seedDescriptorTurn(childId, seed, descriptor) {
768
- const staged = Session.create(childId, seed);
769
- staged.append("subagent/descriptor", descriptor);
770
- return staged.snapshotEvents();
771
- }
772
- //#endregion
773
- //#region lib/types/internal.js
774
- /**
775
- * Continuation integration markers and host adapters outside the public
776
- * Service Definition and model-facing Agent messaging contract.
777
- * @module @deepseek-ai/dsh-subagent/internal
778
- */
779
- /** Process-stable identity carried only by the standard adjacent-Agent messaging tool. */
780
- const adjacentAgentSendMessageTool = Symbol.for("dsh.subagent.adjacentAgentSendMessageTool");
781
- /**
782
- * Test whether one visible definition is the standard adjacent-Agent messaging tool.
783
- * @param definition - the scope-resolved `send_message` candidate.
784
- * @returns whether the definition carries the internal standard-tool identity.
785
- */
786
- function isAdjacentAgentSendMessageTool(definition) {
787
- return definition !== void 0 && definition[adjacentAgentSendMessageTool] === true;
788
- }
789
- /**
790
- * Process-stable symbol-keyed Queue delivery shared by the bundled runtime
791
- * entry and this unbundled internal subpath.
792
- * @internal
793
- */
794
- const queueSubagentPrompt = Symbol.for("dsh.subagent.queuePrompt");
795
- //#endregion
796
- //#region lib/types/continuation.js
591
+ //#region lib/types/continuation-messages.js
797
592
  /**
798
- * Internal continuable-subagent manager: stable child ids, descriptor
799
- * persistence, activation admission, the live ownership graph, cold resume,
800
- * child-first disposal, and settlement delivery to the parent, behind
801
- * `ctx.subagents`.
593
+ * Model-visible messages owned by continuable-subagent orchestration.
802
594
  *
803
- * A continuable child has one durable Session and at most one process-local
804
- * {@link Activation} — one residency epoch for a reconstructed child Agent. An
805
- * Activation is not a request, result, cancellation, or Task boundary: it may
806
- * execute many FIFO turns and stays resident while descendants it created are
807
- * still running. The Agent inbox is the only turn queue, so this manager owns
808
- * residency while the Agent loop owns all turn ordering and execution. No
809
- * continuable path creates a Task or an intermediate result-bearing wrapper.
810
- *
811
- * Because residency is this manager's alone to end, telling the parent that a
812
- * child settled is its job too. An external `subagent/end` listener cannot do
813
- * it correctly: that payload names no parent, the child handle is already
814
- * disposed by then, and the release that wakes the parent's own settlement
815
- * watcher has already run. See {@link SubagentContinuationManager.notifySettlement}.
816
- *
817
- * @module @deepseek-ai/dsh-subagent
595
+ * @module @deepseek-ai/dsh-subagent/continuation-messages
818
596
  */
819
- var __addDisposableResource$1 = function(env, value, async) {
820
- if (value !== null && value !== void 0) {
821
- if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
822
- var dispose, inner;
823
- if (async) {
824
- if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
825
- dispose = value[Symbol.asyncDispose];
826
- }
827
- if (dispose === void 0) {
828
- if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
829
- dispose = value[Symbol.dispose];
830
- if (async) inner = dispose;
831
- }
832
- if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
833
- if (inner) dispose = function() {
834
- try {
835
- inner.call(this);
836
- } catch (e) {
837
- return Promise.reject(e);
838
- }
839
- };
840
- env.stack.push({
841
- value,
842
- dispose,
843
- async
844
- });
845
- } else if (async) env.stack.push({ async: true });
846
- return value;
847
- };
848
- var __disposeResources$1 = (function(SuppressedError) {
849
- return function(env) {
850
- function fail(e) {
851
- env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
852
- env.hasError = true;
853
- }
854
- var r, s = 0;
855
- function next() {
856
- while (r = env.stack.pop()) try {
857
- if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
858
- if (r.dispose) {
859
- var result = r.dispose.call(r.value);
860
- if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) {
861
- fail(e);
862
- return next();
863
- });
864
- } else s |= 1;
865
- } catch (e) {
866
- fail(e);
867
- }
868
- if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
869
- if (env.hasError) throw env.error;
870
- }
871
- return next();
872
- };
873
- })(typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message) {
874
- var e = new Error(message);
875
- return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
876
- });
877
- /**
878
- * Read one Activation's current disposal transaction. This indirection exists
879
- * because TypeScript would otherwise narrow repeated reads of the mutable field
880
- * inside a long-lived closure to constants instead of re-reading runtime state.
881
- * @param activation - the Activation to inspect.
882
- * @returns the in-flight or settled disposal, or `undefined` while resident.
883
- */
884
- function disposalOf(activation) {
885
- return activation.disposal;
886
- }
887
597
  /** Build durable attribution for one adjacent-Agent message. */
888
598
  function agentMessageSource(sender) {
889
599
  return {
@@ -892,19 +602,29 @@ function agentMessageSource(sender) {
892
602
  senderSessionId: sender.id
893
603
  };
894
604
  }
895
- /** Build the model-visible and durable representation of one adjacent-Agent message. */
896
- function agentMessage(sender, content) {
605
+ /**
606
+ * Build the model-visible and durable representation of one adjacent-Agent message.
607
+ * @param sender - exact live Agent that authored the message.
608
+ * @param content - model-visible message blocks supplied by the sender.
609
+ * @returns the durable user-message representation delivered to the recipient.
610
+ */
611
+ function createAgentMessage(sender, content) {
897
612
  return createUserMessage({
898
613
  content: [{
899
614
  type: "text",
900
- text: `Agent ${sender.id} sent a message:`
615
+ text: `Agent ${sender.id} sent a message: `
901
616
  }, ...content],
902
617
  source: agentMessageSource(sender)
903
618
  });
904
619
  }
905
- /** Append adjacent-Agent return guidance to a continuable child's initial task. */
906
- function continuableInitialPrompt(parentId, prompt) {
907
- const encodedParentId = JSON.stringify(parentId);
620
+ /**
621
+ * Append adjacent-Agent return guidance to a continuable child's initial task.
622
+ * @param parentId - durable parent session id named in the guidance.
623
+ * @param prompt - initial model-visible task blocks.
624
+ * @returns task blocks followed by the continuable return guidance.
625
+ */
626
+ function withContinuableReturnGuidance(parentId, prompt) {
627
+ const encodedParentId = JSON.stringify(parentId);
908
628
  return [...prompt, {
909
629
  type: "text",
910
630
  text: `Your parent agent id is ${encodedParentId}. Before you finish, send your result to that agent with send_message({ agent_id: ${encodedParentId}, message: "<self-contained result>" }). The parent shares your workspace but does not automatically receive your transcript, tool output, or reasoning. Send earlier messages as well when a finding changes what the parent should do next; sending a message does not end your turn.`
@@ -931,6 +651,101 @@ function settlementSummary(childId, stopReason) {
931
651
  default: return `${subject} ended abnormally (${String(stopReason)}) before it finished.`;
932
652
  }
933
653
  }
654
+ /**
655
+ * Build the runtime-owned settlement notice delivered to a child's parent.
656
+ * @param childId - durable child session id named in the notice.
657
+ * @param terminal - recorded terminal state for the settled Activation.
658
+ * @returns the durable user-message representation delivered to the parent.
659
+ */
660
+ function createSettlementMessage(childId, terminal) {
661
+ const summary = settlementSummary(childId, terminal.stopReason);
662
+ return createUserMessage({
663
+ content: [{
664
+ type: "text",
665
+ text: summary
666
+ }, ...terminal.output === void 0 ? [{
667
+ type: "text",
668
+ text: "It left no closing message."
669
+ }] : [{
670
+ type: "text",
671
+ text: "Its closing message:"
672
+ }, ...terminal.output]],
673
+ source: {
674
+ kind: "subagent-settled",
675
+ form: "notice",
676
+ summary: boundContextSummary(summary),
677
+ senderSessionId: childId
678
+ }
679
+ });
680
+ }
681
+ //#endregion
682
+ //#region lib/types/inbox.js
683
+ /**
684
+ * Activation-local admission around one continuable subagent's Agent inbox.
685
+ *
686
+ * @module @deepseek-ai/dsh-subagent/inbox
687
+ */
688
+ /** Delegate Queue and Steer to one live Agent until its Activation starts closing. */
689
+ var SubagentInbox = class {
690
+ agent;
691
+ closingPromise;
692
+ /**
693
+ * Wrap one live continuable Agent.
694
+ * @param agent - the Agent whose inbox receives accepted deliveries.
695
+ */
696
+ constructor(agent) {
697
+ this.agent = agent;
698
+ }
699
+ /**
700
+ * Read the Activation's close transaction.
701
+ * @returns the memoized transaction, or `undefined` while delivery remains open.
702
+ */
703
+ get closing() {
704
+ return this.closingPromise;
705
+ }
706
+ /**
707
+ * Read whether the underlying Agent still has accepted work to claim.
708
+ * @returns whether either Agent inbox destination is non-empty.
709
+ */
710
+ get hasPending() {
711
+ return this.agent.inbox.hasPending;
712
+ }
713
+ /**
714
+ * Submit through the Agent only while its Activation remains resident.
715
+ * @param message - the accepted input to submit.
716
+ * @param delivery - whether to queue a distinct turn or steer the nearest step.
717
+ */
718
+ deliver(message, delivery) {
719
+ if (this.closingPromise !== void 0) throw new SubagentError(`subagent "${this.agent.id}" activation is being disposed; the message was not accepted`, "ACTIVATION_CLOSING");
720
+ if (delivery === "steer") this.agent.steer(message);
721
+ else this.agent.followup(message);
722
+ }
723
+ /**
724
+ * Close delivery synchronously and share one asynchronous release.
725
+ * @param release - the one release operation to start after closing admission.
726
+ * @returns the memoized release transaction.
727
+ */
728
+ close(release) {
729
+ const existing = this.closingPromise;
730
+ if (existing !== void 0) return existing;
731
+ const completion = Promise.withResolvers();
732
+ this.closingPromise = completion.promise;
733
+ release().then(completion.resolve, completion.reject);
734
+ return completion.promise;
735
+ }
736
+ };
737
+ //#endregion
738
+ //#region lib/types/continuation-activation.js
739
+ /**
740
+ * Process-local Activation ownership for continuable subagents: admission,
741
+ * parent-child residency, serialized delivery, settlement, and disposal.
742
+ *
743
+ * The continuation manager owns durable request orchestration and delegates
744
+ * every mutable residency decision to this registry, so delivery and teardown
745
+ * share one child lock and one Activation map.
746
+ *
747
+ * @module @deepseek-ai/dsh-subagent/continuation-activation
748
+ */
934
749
  /** Serialize each durable child's delivery, release, and disposal. */
935
750
  var ChildLock = class {
936
751
  tails = /* @__PURE__ */ new Map();
@@ -950,19 +765,15 @@ var ChildLock = class {
950
765
  return result;
951
766
  }
952
767
  };
953
- /**
954
- * The continuable-subagent orchestration service behind `ctx.subagents`. Tool
955
- * schema and host adapters are consumers of this one contract; foreground
956
- * one-shot delegation keeps calling `ctx.subagents.start()` and never enters
957
- * this lifecycle.
958
- */
959
- var SubagentContinuationManager = class {
768
+ /** Own the complete process-local lifetime of continuable child Activations. */
769
+ var ContinuableActivationRegistry = class {
960
770
  ctx;
961
- host;
771
+ observeActivation;
962
772
  /** Child session id → its live Activation. Process-local, never durable. */
963
- activations = /* @__PURE__ */ new Map();
773
+ resident = /* @__PURE__ */ new Map();
964
774
  /** Materializations admitted before drain, tracked through publication or rollback. */
965
775
  materializations = /* @__PURE__ */ new Set();
776
+ /** Per-child serializer shared by delivery, release, and disposal. */
966
777
  locks = new ChildLock();
967
778
  /** Structural Cordis owner of every Activation handle. */
968
779
  ownerCtx;
@@ -974,9 +785,14 @@ var SubagentContinuationManager = class {
974
785
  */
975
786
  closingScopes = /* @__PURE__ */ new Map();
976
787
  draining = false;
977
- constructor(ctx, host) {
788
+ /**
789
+ * Build one registry inside the service's Agent-injected context.
790
+ * @param ctx - context providing Agents, Sessions, and teardown ownership.
791
+ * @param observeActivation - build the lifecycle observer for one residency epoch.
792
+ */
793
+ constructor(ctx, observeActivation) {
978
794
  this.ctx = ctx;
979
- this.host = host;
795
+ this.observeActivation = observeActivation;
980
796
  const scope = ctx.plugin(function activationOwner() {});
981
797
  this.ownerCtx = scope.ctx;
982
798
  ctx.on("agent/disposed", ({ agent }) => {
@@ -988,187 +804,50 @@ var SubagentContinuationManager = class {
988
804
  }.bind(this), "subagents.continuations()");
989
805
  }
990
806
  /**
991
- * Start one continuable background child: reserve its durable identity,
992
- * resolve the provider's detached creation spec, create the child Agent
993
- * through the private activation-owner scope, establish any continuable-parent
994
- * ownership, and submit the initial prompt. Resolves when inbox acceptance
995
- * yields the message id — without waiting for the turn to start or for the
996
- * message to reach the Session log.
997
- *
998
- * Every failure before that acceptance rejects without either id, disposing
999
- * any created handle and rolling back the Activation and parent ownership.
1000
- * The caller signal owns lookup, materialization, and admission only until
1001
- * acceptance; afterwards the manager owns the Activation independently.
1002
- * @param spec - provider, delegation request, and caller cancellation.
1003
- * @returns the durable child id and the accepted initial prompt's message id.
807
+ * Return the live Activation for a durable child id, if resident.
808
+ * @param childId - durable child session id to look up.
809
+ * @returns the process-local Activation, or `undefined` when it is not resident.
1004
810
  */
1005
- async startContinuable(spec) {
1006
- const request = spec.request;
1007
- const parent = request.parent;
1008
- this.assertAdmitting(parent);
1009
- const persistence = this.requirePersistence();
1010
- assertSubagentMaxDepth(request.maxDepth);
1011
- const childId = spec.childId ?? brandString(randomUUID());
1012
- this.assertChildIdAvailable(childId);
1013
- const childDepth = resolveChildDepth(parent, request.maxDepth);
1014
- const agentOptions = resolveChildAgentOptions(parent, request.agentOptions, childDepth);
1015
- const agentProvider = agentOptions.provider;
1016
- const agentModel = agentOptions.model;
1017
- const agentReasoningEffort = agentOptions.reasoningEffort;
1018
- const descriptor = snapshotSubagentDescriptor({
1019
- mode: "continuable",
1020
- provider: spec.provider,
1021
- label: spec.label,
1022
- ...agentProvider !== void 0 ? { agentProvider } : {},
1023
- ...agentModel !== void 0 ? { agentModel } : {},
1024
- ...agentReasoningEffort !== void 0 ? { agentReasoningEffort } : {},
1025
- ...request.persona !== void 0 ? { persona: request.persona } : {},
1026
- ...request.toolFilter !== void 0 ? { toolFilter: request.toolFilter } : {}
1027
- });
1028
- const delegatedPolicies = captureDelegatedPolicyOverrides(parent);
1029
- const prepared = await this.host.prepareContinuable(spec.provider, {
1030
- sessionId: childId,
1031
- parent,
1032
- signal: spec.signal
1033
- });
1034
- spec.signal.throwIfAborted();
1035
- this.assertAdmitting(parent);
1036
- const inheritedEventCount = SessionLogOffset(prepared.seed?.length ?? 0);
1037
- const seed = seedDescriptorTurn(childId, prepared.seed, descriptor);
1038
- return {
1039
- childId,
1040
- messageId: await this.locks.run(childId, async () => {
1041
- spec.signal.throwIfAborted();
1042
- this.assertAdmitting(parent);
1043
- this.assertChildIdAvailable(childId);
1044
- if (spec.childId !== void 0) {
1045
- const persisted = await persistence.listSnapshots(spec.signal);
1046
- spec.signal.throwIfAborted();
1047
- this.assertAdmitting(parent);
1048
- this.assertChildIdAvailable(childId);
1049
- if (persisted.some((snapshot) => snapshot.header.id === childId)) throw new SubagentError(`subagent "${childId}" already exists`, "DUPLICATE_CHILD");
1050
- }
1051
- const activation = await this.materialize({
1052
- childId,
1053
- provider: spec.provider,
1054
- parent,
1055
- create: {
1056
- seed,
1057
- meta: childSessionMeta(parent, childDepth, prepared.seed !== void 0),
1058
- inheritedEventCount,
1059
- delegatedPolicies
1060
- },
1061
- agentOptions,
1062
- composition: {
1063
- persona: request.persona,
1064
- toolFilter: request.toolFilter
1065
- },
1066
- signal: spec.signal
1067
- });
1068
- return this.submitMaterialized(activation, isAdjacentAgentSendMessageTool(this.ctx.get("tools")?.get("send_message", activation.handle.agent)) ? continuableInitialPrompt(parent.id, request.prompt) : request.prompt, {
1069
- source: { kind: "user" },
1070
- signal: spec.signal,
1071
- delivery: "queue"
1072
- }, parent);
1073
- })
1074
- };
1075
- }
1076
- /** Reject one child identity already owned by a live Agent or Session. */
1077
- assertChildIdAvailable(childId) {
1078
- 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
+ get(childId) {
812
+ return this.resident.get(childId);
1079
813
  }
1080
814
  /**
1081
- * Deliver one model-authored message to a direct continuable child or to the
1082
- * sender's direct parent. Both directions use Steer: a running target admits
1083
- * the message at its nearest step boundary, while an idle target starts a
1084
- * turn. A missing direct child cold-resumes through the ordinary continuation
1085
- * lifecycle. The caller signal owns the operation only until inbox acceptance.
1086
- * @param sender - exact live Agent authorizing and originating the message.
1087
- * @param targetId - durable direct-parent or direct-child session id.
1088
- * @param content - model-authored content to deliver.
1089
- * @param options - caller cancellation before acceptance.
1090
- * @returns the accepted message's inbox id.
1091
- * @throws when adjacency, availability, or admission rejects delivery.
815
+ * Reject one child identity already owned by a live Agent or Session.
816
+ * @param childId - proposed durable child session id.
1092
817
  */
1093
- async sendMessage(sender, targetId, content, options) {
1094
- if (this.ctx.agents.get(sender.id) !== sender) throw new SubagentError("message delivery requires the exact live sender agent", "UNAUTHORIZED");
1095
- this.assertAdmitting(sender);
1096
- const senderActivation = this.activations.get(sender.id);
1097
- if (senderActivation !== void 0 && senderActivation.handle.agent === sender && senderActivation.parentSession === targetId) {
1098
- options.signal.throwIfAborted();
1099
- return this.sendToParent(senderActivation, sender, content);
1100
- }
1101
- 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");
1102
- return this.deliverToChild(sender, targetId, content, {
1103
- signal: options.signal,
1104
- delivery: "steer"
1105
- });
818
+ assertChildIdAvailable(childId) {
819
+ 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");
1106
820
  }
1107
821
  /**
1108
- * Queue one human-authored prompt as a distinct direct-child turn.
1109
- * @param parent - exact live direct parent authorizing delivery.
1110
- * @param childId - durable direct-child session id.
1111
- * @param content - human-authored content to deliver.
1112
- * @param source - durable host-protocol provenance.
1113
- * @param signal - caller cancellation before inbox acceptance.
1114
- * @returns the accepted message's inbox id.
822
+ * Pre-register `childId` in a continuation-managed parent's owned set so the
823
+ * parent cannot settle while a caller is still establishing or resuming that
824
+ * child. Returns a releaser for the failure path; it removes only a hold
825
+ * this call added, and leaves ownership in place once a live Activation for
826
+ * the child exists.
827
+ * @param parent - the live direct parent the operation is admitted under.
828
+ * @param childId - the durable child the operation addresses.
829
+ * @returns the failure-path releaser; a no-op when nothing was added.
1115
830
  */
1116
- async queuePrompt(parent, childId, content, source, signal) {
1117
- return this.deliverToChild(parent, childId, content, {
1118
- source,
1119
- signal,
1120
- delivery: "queue"
1121
- });
1122
- }
1123
- /** Route one parent-originated delivery through residency and cold resume. */
1124
- async deliverToChild(parent, childId, content, options) {
1125
- this.assertAdmitting(parent);
1126
- while (true) {
1127
- const live = await this.locks.run(childId, async () => {
1128
- const activation = this.activations.get(childId);
1129
- if (activation === void 0) return this.coldResume(parent, childId, content, options);
1130
- const disposal = activation.disposal;
1131
- /* v8 ignore next 3 -- the send-versus-dispose cutoff: reaching this arm needs a
1132
- * delivery to observe the transaction inside the same critical section that opened it,
1133
- * which no test can schedule deterministically. The behavior is covered end-to-end by
1134
- * "cold-resumes a delivery that lost the race with final disposal". */
1135
- if (disposal !== void 0) return disposal.then(() => void 0, () => void 0);
1136
- if (contentHasImage(content)) {
1137
- await this.assertImageCapable(activation.handle.agent, options.signal);
1138
- if (activation.disposal !== void 0) {
1139
- await Promise.allSettled([activation.disposal]);
1140
- return;
1141
- }
1142
- }
1143
- return this.submitAdmitted(activation, content, options, parent);
1144
- });
1145
- /* v8 ignore start -- only the lost-cutoff arm above returns undefined, so only that
1146
- * race reaches the retry below, which then cold-resumes a new Activation. */
1147
- if (live !== void 0) return live;
1148
- this.assertAdmitting(parent);
1149
- options.signal.throwIfAborted();
1150
- }
831
+ holdOwnership(parent, childId) {
832
+ const parentActivation = this.resident.get(parent.id);
833
+ if (parentActivation === void 0 || parentActivation.handle.agent !== parent) return () => {};
834
+ if (parentActivation.inbox.closing !== void 0) throw new SubagentError(`subagent parent "${parent.id}" is being disposed; the child was not established`, "ACTIVATION_CLOSING");
835
+ if (parentActivation.ownedChildren.has(childId)) return () => {};
836
+ parentActivation.ownedChildren.add(childId);
837
+ return () => {
838
+ const live = this.resident.get(childId);
839
+ /* v8 ignore next 4 -- reaching this arm needs another delivery to establish the child
840
+ * between this operation's failure and its releaser running, which no test can schedule
841
+ * deterministically: the ownership edge then belongs to that live Activation, so the
842
+ * conservative keep leaves it for finishDisposal's releaseOwnership. */
843
+ if (live !== void 0 && live.inbox.closing === void 0) return;
844
+ if (parentActivation.ownedChildren.delete(childId)) this.wake(parentActivation);
845
+ };
1151
846
  }
1152
847
  /**
1153
- * Interrupt one live continuable child's current turn. Admission is
1154
- * synchronous and the effect is asynchronous: this authorizes the caller,
1155
- * requests `Agent.cancel(cause, { keepInbox: true })` on the target, and
1156
- * returns without waiting for the target to observe the signal or reach
1157
- * quiescence. The Activation, its handle, accepted unclaimed inbox work, and
1158
- * already-published descendants are untouched; work already claimed into the
1159
- * interrupted turn is not requeued. Once the interrupted driver is idle, a
1160
- * waking send resumes the parked queue.
1161
- *
1162
- * An absent target is an accepted no-op, which uniformly covers natural
1163
- * completion races, repeated requests, one-shot ids, and unknown ids without
1164
- * consulting the durable catalog. A target whose disposal transaction is
1165
- * already open is likewise an accepted no-op after authorization.
848
+ * Interrupt one live continuable child's current turn under the supplied authority.
1166
849
  * @param targetSessionId - the durable child session id to interrupt.
1167
850
  * @param authority - the human parent address or exact live ancestor Agent.
1168
- * @throws {SubagentError} `UNAUTHORIZED` when the presented authority does
1169
- * not own the live target: a stale or self-targeting ancestor caller, a
1170
- * parent address that is not the live target's durable direct parent, or
1171
- * an ancestor outside the target's recorded live lineage.
1172
851
  */
1173
852
  interrupt(targetSessionId, authority) {
1174
853
  if (authority.kind === "ancestor") {
@@ -1176,81 +855,56 @@ var SubagentContinuationManager = class {
1176
855
  if (this.ctx.agents.get(caller.id) !== caller) throw new SubagentError(`interrupting "${targetSessionId}" requires the exact live ancestor agent`, "UNAUTHORIZED");
1177
856
  if (caller.id === targetSessionId) throw new SubagentError(`agent "${caller.id}" cannot interrupt itself`, "UNAUTHORIZED");
1178
857
  }
1179
- const activation = this.activations.get(targetSessionId);
858
+ const activation = this.resident.get(targetSessionId);
1180
859
  if (activation === void 0) return;
1181
860
  if (authority.kind === "user") {
1182
861
  if (activation.handle.agent.session.header.parentSession !== authority.parentSessionId) throw new SubagentError(`subagent "${targetSessionId}" belongs to another parent session`, "UNAUTHORIZED");
1183
862
  } else if (!activation.ancestry.has(authority.agent)) throw new SubagentError(`subagent "${targetSessionId}" is not a live descendant of agent "${authority.agent.id}"`, "UNAUTHORIZED");
1184
- if (activation.disposal !== void 0) return;
863
+ if (activation.inbox.closing !== void 0) return;
1185
864
  activation.handle.agent.cancel(authority.kind === "user" ? { kind: "user" } : { kind: "parent" }, { keepInbox: true });
1186
865
  }
1187
- /** Deliver one resident continuable child's message to its live direct parent. */
1188
- sendToParent(activation, sender, content) {
1189
- /* v8 ignore next 6 -- only synchronous re-entrant teardown can open this
1190
- * transaction between exact-agent authorization and this no-await span. */
1191
- if (activation.disposal !== void 0) throw new SubagentError(`subagent "${sender.id}" activation is being disposed; the message was not delivered`, "ACTIVATION_CLOSING");
1192
- const parent = this.ctx.agents.get(activation.parentSession);
1193
- if (parent === void 0) throw new SubagentError("direct parent is not live; the message was not delivered", "PARENT_UNAVAILABLE");
1194
- const message = agentMessage(sender, content);
1195
- this.sendWaking(parent, message, () => {
1196
- this.sendAgentMessage(parent, message);
1197
- });
1198
- return message.id;
1199
- }
1200
866
  /**
1201
- * Perform one waking send to a parent, accounted against that parent's own
1202
- * Activation when it has one. Registering the id before the send is what
1203
- * keeps a continuation-managed parent from being judged quiescent in the
1204
- * window between a waking send and the microtask that admits it.
1205
- * @param parent - the exact live parent receiving the waking message.
1206
- * @param message - the message whose id is accounted.
1207
- * @param send - the synchronous waking send to perform.
867
+ * Send through a receiving parent's Activation inbox when it has one.
868
+ * @param parent - exact live Agent receiving the message.
869
+ * @param message - durable user message to deliver.
870
+ * @param delivery - receiving inbox destination.
1208
871
  */
1209
- sendWaking(parent, message, send) {
1210
- const parentActivation = this.activations.get(parent.id);
1211
- if (parentActivation !== void 0 && parentActivation.handle.agent === parent) this.admitWaking(parentActivation, message.id, send);
1212
- else send();
1213
- }
1214
- /** Send one Agent message while translating only the target's own rejection. */
1215
- sendAgentMessage(parent, message) {
1216
- try {
1217
- parent.steer(message);
1218
- } catch (error) {
1219
- throw new SubagentError("direct parent is not live; the message was not delivered", "PARENT_UNAVAILABLE", { cause: error });
872
+ sendWaking(parent, message, delivery) {
873
+ const parentActivation = this.resident.get(parent.id);
874
+ if (parentActivation !== void 0 && parentActivation.handle.agent === parent) {
875
+ try {
876
+ parentActivation.inbox.deliver(message, delivery);
877
+ } finally {
878
+ this.wake(parentActivation);
879
+ }
880
+ return;
1220
881
  }
882
+ if (delivery === "steer") parent.steer(message);
883
+ else parent.followup(message);
1221
884
  }
1222
885
  /**
1223
886
  * Close admission, await every already-admitted materialization through
1224
- * publication or rollback, then dispose the stable live Activation forest
1225
- * child-first. Sibling branches drain independently: one failure is recorded
1226
- * but never prevents the remaining handles from being attempted, and the
1227
- * aggregate rejects only after every branch settles.
1228
- * @returns once materialization is quiescent and every live Activation released its handle.
1229
- * @throws an aggregate error when any branch failed to release.
887
+ * publication or rollback, then dispose the stable live Activation graph
888
+ * child-first.
1230
889
  */
1231
890
  async drain() {
1232
891
  this.draining = true;
1233
892
  await Promise.all([...this.materializations].map((materialization) => materialization.settled));
1234
893
  const owned = /* @__PURE__ */ new Set();
1235
- for (const activation of this.activations.values()) for (const child of activation.ownedChildren) owned.add(child);
1236
- const roots = [...this.activations.values()].filter((activation) => !owned.has(activation.childId));
894
+ for (const activation of this.resident.values()) for (const child of activation.ownedChildren) owned.add(child);
895
+ const roots = [...this.resident.values()].filter((activation) => !owned.has(activation.childId));
1237
896
  await this.disposeRoots(roots, "activation(s)");
1238
897
  }
1239
898
  /**
1240
899
  * Stop only the continuable descendants of exact live host-owned parents.
1241
- * Admission stays closed for those parent trees until each exact parent
1242
- * leaves the Agent registry; unrelated trees and manager-wide admission stay
1243
- * live.
1244
900
  * @param parents - exact live roots whose continuable descendants must stop.
1245
- * @returns once every retained descendant Activation released its handle.
1246
- * @throws an aggregate error after all scoped branches settle when any failed.
1247
901
  */
1248
902
  async drainDescendants(parents) {
1249
903
  const roots = new Set(parents.filter((parent) => this.ctx.agents.get(parent.id) === parent));
1250
904
  if (roots.size === 0) return;
1251
905
  for (const root of roots) this.closingMembers(root).add(root);
1252
906
  const targets = [];
1253
- for (const activation of this.activations.values()) {
907
+ for (const activation of this.resident.values()) {
1254
908
  const lineage = this.liveLineage(activation.handle.agent);
1255
909
  const owners = [...roots].filter((root) => activation.handle.agent !== root && activation.ancestry.has(root));
1256
910
  if (owners.length === 0) continue;
@@ -1277,20 +931,15 @@ var SubagentContinuationManager = class {
1277
931
  await this.disposeRoots(targetRoots, "scoped activation(s)");
1278
932
  }
1279
933
  /**
1280
- * Release selected resident direct children of one exact live parent without
1281
- * closing admission for the parent's other continuable children. Owned
1282
- * descendants are released recursively through the same lifecycle.
934
+ * Release selected resident direct children of one exact live parent.
1283
935
  * @param parent - exact live direct parent authorizing the selected release.
1284
936
  * @param childIds - durable direct-child ids to release when resident.
1285
- * @returns once every selected Activation released its handle.
1286
- * @throws {SubagentError} `UNAUTHORIZED` when a resident target is not the
1287
- * parent's direct continuable child or the parent identity is stale.
1288
937
  */
1289
938
  async drainChildren(parent, childIds) {
1290
939
  if (this.ctx.agents.get(parent.id) !== parent) throw new SubagentError("selected child teardown requires the exact live parent agent", "UNAUTHORIZED");
1291
940
  const targets = [];
1292
941
  for (const childId of new Set(childIds)) {
1293
- const activation = this.activations.get(childId);
942
+ const activation = this.resident.get(childId);
1294
943
  if (activation === void 0) continue;
1295
944
  if (activation.parentSession !== parent.id || !activation.ancestry.has(parent)) throw new SubagentError(`subagent "${childId}" is not a direct child of agent "${parent.id}"`, "UNAUTHORIZED");
1296
945
  targets.push(activation);
@@ -1298,6 +947,75 @@ var SubagentContinuationManager = class {
1298
947
  for (const activation of targets) this.dispose(activation).catch(() => void 0);
1299
948
  await this.disposeRoots(targets, "selected activation(s)");
1300
949
  }
950
+ /**
951
+ * Reject new admission once the registry or this exact parent tree began draining.
952
+ * @param agent - exact live Agent whose lineage determines admission.
953
+ */
954
+ assertAdmitting(agent) {
955
+ const closing = this.closingTeardownFor(agent);
956
+ if (closing === void 0) return;
957
+ 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");
958
+ }
959
+ /**
960
+ * Authorize one operation against the durable direct-parent lineage.
961
+ * @param parent - exact live Agent claiming direct-parent authority.
962
+ * @param childId - durable child session id addressed by the operation.
963
+ * @param parentSession - durable direct-parent id recorded by the child.
964
+ */
965
+ authorizeLineage(parent, childId, parentSession) {
966
+ if (this.ctx.agents.get(parent.id) !== parent) throw new SubagentError(`subagent "${childId}" delivery requires the exact live parent agent`, "UNAUTHORIZED");
967
+ if (parentSession !== parent.id) throw new SubagentError(`subagent "${childId}" belongs to another parent session`, "UNAUTHORIZED");
968
+ }
969
+ /**
970
+ * Create or resume one child Agent and publish its Activation.
971
+ * @param inputs - reconstruction and admission inputs for the residency epoch.
972
+ * @returns the published process-local Activation.
973
+ */
974
+ materialize(inputs) {
975
+ this.assertAdmitting(inputs.parent);
976
+ const settled = Promise.withResolvers();
977
+ const lineage = this.liveLineage(inputs.parent);
978
+ const materialization = {
979
+ lineage,
980
+ settled: settled.promise
981
+ };
982
+ this.materializations.add(materialization);
983
+ return this.materializeTracked(inputs, lineage).finally(() => {
984
+ this.materializations.delete(materialization);
985
+ settled.resolve();
986
+ });
987
+ }
988
+ /**
989
+ * Cross the final admission cutoff and submit without yielding.
990
+ * @param activation - the exact resident child receiving the message.
991
+ * @param message - the already-built durable user message.
992
+ * @param delivery - the Agent inbox destination.
993
+ * @param parent - exact live direct parent authorizing admission.
994
+ * @param signal - caller cancellation before inbox acceptance.
995
+ * @returns the accepted durable message id.
996
+ */
997
+ submitAdmitted(activation, message, delivery, parent, signal) {
998
+ signal.throwIfAborted();
999
+ this.assertAdmitting(parent);
1000
+ this.authorizeLineage(parent, activation.childId, activation.handle.agent.session.header.parentSession);
1001
+ this.acquireOwnership(parent, activation.childId);
1002
+ try {
1003
+ activation.inbox.deliver(message, delivery);
1004
+ } finally {
1005
+ this.wake(activation);
1006
+ }
1007
+ activation.announced = true;
1008
+ return message.id;
1009
+ }
1010
+ /**
1011
+ * Stop and release one Activation through its memoized close transaction.
1012
+ * @param activation - exact residency epoch to close.
1013
+ * @param finalStateFlushed - whether natural settlement already flushed final state.
1014
+ * @returns the shared close transaction.
1015
+ */
1016
+ dispose(activation, finalStateFlushed = false) {
1017
+ return activation.inbox.close(() => this.finishDisposal(activation, finalStateFlushed));
1018
+ }
1301
1019
  /** Dispose independent roots and report every branch failure after all settle. */
1302
1020
  async disposeRoots(roots, failureSubject) {
1303
1021
  const reasons = (await Promise.all(roots.map(async (activation) => {
@@ -1318,11 +1036,7 @@ var SubagentContinuationManager = class {
1318
1036
  this.closingScopes.set(root, members);
1319
1037
  return members;
1320
1038
  }
1321
- /**
1322
- * Return the exact currently resolvable ancestry from `agent` upward. The
1323
- * first element is always the supplied identity, even when it is already
1324
- * stale; each ancestor after it must be the registry's current exact entry.
1325
- */
1039
+ /** Return the exact currently resolvable ancestry from `agent` upward. */
1326
1040
  liveLineage(agent) {
1327
1041
  const lineage = [agent];
1328
1042
  const seen = new Set([agent.id]);
@@ -1336,172 +1050,25 @@ var SubagentContinuationManager = class {
1336
1050
  }
1337
1051
  return lineage;
1338
1052
  }
1339
- /**
1340
- * The teardown that closed continuable admission for this agent's lineage.
1341
- * `'manager'` is the whole manager draining; an Agent is the exact scoped root
1342
- * whose forest is closing.
1343
- * @param agent - the agent whose lineage is tested.
1344
- * @returns the closing teardown, or `undefined` while admission is open.
1345
- */
1053
+ /** Return the teardown that closed continuable admission for this agent's lineage. */
1346
1054
  closingTeardownFor(agent) {
1347
1055
  if (this.draining) return "manager";
1348
1056
  const lineage = this.liveLineage(agent);
1349
1057
  for (const [root, members] of this.closingScopes) if (members.has(agent) || lineage.includes(root)) return root;
1350
1058
  }
1351
- /** Reject new admission once the manager or this exact parent tree began draining. */
1352
- assertAdmitting(agent) {
1353
- const closing = this.closingTeardownFor(agent);
1354
- if (closing === void 0) return;
1355
- 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");
1356
- }
1357
- /**
1358
- * Derive residency from Agent quiescence and the owned-child set. `running`
1359
- * covers an active admission, an open turn, or accepted waking inbox work.
1360
- *
1361
- * `Agent.status` alone is insufficient: it stays `idle` between an accepted
1362
- * waking send and the microtask that admits it, so a synchronous inbox
1363
- * observer would see `settled` while a turn is already queued. `accepted`
1364
- * holds the ids this manager admitted but has not yet seen drained.
1365
- */
1366
- stateOf(activation) {
1367
- if (activation.handle.agent.status === "running" || activation.accepted.size > 0) return "running";
1368
- if (activation.ownedChildren.size > 0) return "waiting";
1369
- return "settled";
1370
- }
1371
- /**
1372
- * Cold-resume a persisted child: retain and authorize its prepared Session, fold the
1373
- * generic descriptor, create the Activation through `ctx.agents.resume()`,
1374
- * and submit the waiting turn. This never dispatches through a subagent
1375
- * provider — the persisted Session already holds the initial prefix and the
1376
- * descriptor is the whole reconstruction input.
1377
- */
1378
- async coldResume(parent, childId, content, options) {
1379
- const env_1 = {
1380
- stack: [],
1381
- error: void 0,
1382
- hasError: false
1383
- };
1384
- try {
1385
- const query = this.requireSessionQuery();
1386
- let observation;
1387
- try {
1388
- observation = await query.observeSession(childId, { signal: options.signal });
1389
- } catch (error) {
1390
- options.signal.throwIfAborted();
1391
- throw new SubagentError(`subagent "${childId}" is unavailable`, "NOT_RESUMABLE", { cause: error });
1059
+ /** Perform one tracked materialization through publication or rollback. */
1060
+ async materializeTracked(inputs, parentLineage) {
1061
+ const { childId, provider, parent, create } = inputs;
1062
+ inputs.signal.throwIfAborted();
1063
+ const setup = (childCtx) => {
1064
+ const child = childCtx.agent;
1065
+ if (create !== void 0) {
1066
+ child.session.append("subagent/descriptor", create.descriptor);
1067
+ appendDelegatedPolicyOverrides(child.session, create.delegatedPolicies);
1392
1068
  }
1393
- const source = __addDisposableResource$1(env_1, observation, false);
1394
- this.assertAdmitting(parent);
1395
- this.authorizeLineage(parent, childId, source.header.parentSession);
1396
- const descriptor = foldSubagentDescriptor(source.events.slice(source.inheritedEventCount));
1397
- 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");
1398
- let activation;
1399
- try {
1400
- activation = await this.materialize({
1401
- childId,
1402
- provider: descriptor.provider,
1403
- parent,
1404
- agentOptions: {
1405
- ...descriptor.agentProvider !== void 0 ? { provider: descriptor.agentProvider } : {},
1406
- ...descriptor.agentModel !== void 0 ? { model: descriptor.agentModel } : {},
1407
- ...descriptor.agentReasoningEffort !== void 0 ? { reasoningEffort: ReasoningEffortId(descriptor.agentReasoningEffort) } : {}
1408
- },
1409
- composition: {
1410
- persona: descriptor.persona,
1411
- toolFilter: descriptor.toolFilter
1412
- },
1413
- signal: options.signal
1414
- });
1415
- } catch (error) {
1416
- options.signal.throwIfAborted();
1417
- if (error instanceof SubagentError) throw error;
1418
- throw new SubagentError(`subagent "${childId}" is unavailable`, "NOT_RESUMABLE", { cause: error });
1419
- }
1420
- return await this.submitMaterialized(activation, content, options, parent);
1421
- } catch (e_1) {
1422
- env_1.error = e_1;
1423
- env_1.hasError = true;
1424
- } finally {
1425
- __disposeResources$1(env_1);
1426
- }
1427
- }
1428
- /**
1429
- * Submit to a freshly materialized Activation or roll it back completely.
1430
- * @param activation - the just-published Activation to admit or release.
1431
- * @param content - the initial or resumed message content.
1432
- * @param options - durable source, scheduling, and pre-acceptance cancellation.
1433
- * @param parent - the live direct parent authorizing admission.
1434
- * @returns the accepted inbox message id.
1435
- */
1436
- async submitMaterialized(activation, content, options, parent) {
1437
- try {
1438
- if (contentHasImage(content)) {
1439
- await this.assertImageCapable(activation.handle.agent, options.signal);
1440
- if (activation.disposal !== void 0) throw new SubagentError(`subagent "${activation.childId}" is closing`, "ACTIVATION_CLOSING");
1441
- }
1442
- return this.submitAdmitted(activation, content, options, parent);
1443
- } catch (error) {
1444
- /* v8 ignore next -- rollback disposal failures must not mask the
1445
- * pre-acceptance signal, drain, or lifecycle failure. */
1446
- await this.dispose(activation).catch(() => void 0);
1447
- throw error;
1448
- }
1449
- }
1450
- /**
1451
- * Refuse image content addressed to a child whose model accepts text only.
1452
- * Callers guard with `contentHasImage`, so text-only delivery never awaits.
1453
- * The check runs inside the per-child delivery lock, before the message
1454
- * exists, so a rejection leaves no partial user message. When the child's
1455
- * route is not fixed by its options (a request-waterfall listener owns it)
1456
- * or no LLM registry is composed, delivery proceeds and the LLM layer's
1457
- * text-only projection replaces each image with its stable placeholder.
1458
- * @param agent - the live or freshly materialized child agent.
1459
- * @param signal - caller cancellation bounding the model-info read.
1460
- * @throws {SubagentError} `MODEL_DOES_NOT_SUPPORT_IMAGES` when the child's resolved model declines image input.
1461
- */
1462
- async assertImageCapable(agent, signal) {
1463
- const { provider, model } = agent.options;
1464
- if (provider === void 0 || model === void 0) return;
1465
- const llm = this.ctx.get("llm");
1466
- /* v8 ignore next -- a deployment without the LLM registry serves no model
1467
- * to refuse against; delivery then defers to the text-only projection. */
1468
- if (llm === void 0) return;
1469
- const info = await llm.resolveModelInfo(provider, model, signal);
1470
- if (info.inputModalities !== void 0 && !info.inputModalities.includes("image")) throw new SubagentError(`Model "${model}" does not support image input.`, "MODEL_DOES_NOT_SUPPORT_IMAGES");
1471
- }
1472
- /**
1473
- * Create or resume the child Agent through the private activation-owner
1474
- * scope, install the handle in a fresh Activation, and register ownership on
1475
- * a continuation-managed parent. Rejection leaves no Activation, no handle,
1476
- * and no ownership membership.
1477
- */
1478
- materialize(inputs) {
1479
- this.assertAdmitting(inputs.parent);
1480
- const settled = Promise.withResolvers();
1481
- const lineage = this.liveLineage(inputs.parent);
1482
- const materialization = {
1483
- lineage,
1484
- settled: settled.promise
1485
- };
1486
- this.materializations.add(materialization);
1487
- return this.materializeTracked(inputs, lineage).finally(() => {
1488
- this.materializations.delete(materialization);
1489
- settled.resolve();
1490
- });
1491
- }
1492
- /**
1493
- * Perform one tracked materialization. The caller keeps the drain barrier
1494
- * registered until this either returns a resident Activation or finishes
1495
- * rollback.
1496
- */
1497
- async materializeTracked(inputs, parentLineage) {
1498
- const { childId, provider, parent, create } = inputs;
1499
- inputs.signal.throwIfAborted();
1500
- const setup = (childCtx) => {
1501
- if (create !== void 0) appendDelegatedPolicyOverrides(childCtx.agent.session, create.delegatedPolicies);
1502
1069
  applyChildComposition(childCtx, parent, inputs.composition);
1503
1070
  };
1504
- const observer = this.host.observeActivation(provider, childId, parent);
1071
+ const observer = this.observeActivation(provider, childId, parent);
1505
1072
  const handle = create === void 0 ? await this.ownerCtx.agents.resume({
1506
1073
  resumeSessionId: childId,
1507
1074
  agentOptions: inputs.agentOptions,
@@ -1510,7 +1077,7 @@ var SubagentContinuationManager = class {
1510
1077
  }) : await this.ownerCtx.agents.create({
1511
1078
  sessionId: childId,
1512
1079
  meta: create.meta,
1513
- seed: create.seed,
1080
+ ...create.seed === void 0 ? {} : { seed: create.seed },
1514
1081
  inheritedEventCount: create.inheritedEventCount,
1515
1082
  agentOptions: inputs.agentOptions,
1516
1083
  signal: inputs.signal,
@@ -1521,27 +1088,23 @@ var SubagentContinuationManager = class {
1521
1088
  parentSession: parent.id,
1522
1089
  provider,
1523
1090
  handle,
1091
+ inbox: new SubagentInbox(handle.agent),
1524
1092
  ancestry: new WeakSet([handle.agent, ...parentLineage]),
1525
1093
  ownedChildren: /* @__PURE__ */ new Set(),
1526
1094
  observer,
1527
- disposal: void 0,
1528
- accepted: /* @__PURE__ */ new Set(),
1529
1095
  announced: false,
1530
1096
  poke: Promise.withResolvers()
1531
1097
  };
1532
- this.activations.set(childId, activation);
1098
+ this.resident.set(childId, activation);
1533
1099
  try {
1534
1100
  inputs.signal.throwIfAborted();
1535
1101
  this.assertAdmitting(parent);
1536
1102
  this.acquireOwnership(parent, childId);
1537
- handle.agent.ctx.on("agent/inbox/claimed", ({ message }) => {
1538
- /* v8 ignore next -- a claim of an id this manager never admitted needs
1539
- * another sender on the same child, which no current path allows. */
1540
- if (activation.accepted.delete(message.id)) this.wake(activation);
1541
- });
1542
- handle.agent.ctx.on("agent/inbox/discarded", ({ message }) => {
1543
- if (activation.accepted.delete(message.id)) this.wake(activation);
1544
- });
1103
+ const wakeOnInboxRemoval = () => {
1104
+ this.wake(activation);
1105
+ };
1106
+ handle.agent.ctx.on("agent/inbox/claimed", wakeOnInboxRemoval);
1107
+ handle.agent.ctx.on("agent/inbox/discarded", wakeOnInboxRemoval);
1545
1108
  observer.start(handle.agent);
1546
1109
  } catch (error) {
1547
1110
  /* v8 ignore next -- rollback failure must not mask the admission failure
@@ -1552,126 +1115,72 @@ var SubagentContinuationManager = class {
1552
1115
  this.watchSettlement(activation);
1553
1116
  return activation;
1554
1117
  }
1555
- /**
1556
- * Release an Activation whose start edge was not published. The memoized
1557
- * transaction remains in the live map until handle disposal settles, so a
1558
- * concurrent drain or delivery observes the same closing boundary.
1559
- */
1118
+ /** Release an Activation whose start edge was not published. */
1560
1119
  rollbackUnpublished(activation) {
1561
- return activation.disposal ??= (async () => {
1120
+ return activation.inbox.close(async () => {
1562
1121
  try {
1563
1122
  await activation.handle.dispose();
1564
1123
  } finally {
1565
- this.activations.delete(activation.childId);
1124
+ this.resident.delete(activation.childId);
1566
1125
  this.releaseOwnership(activation.childId);
1567
1126
  }
1568
- })();
1127
+ });
1569
1128
  }
1570
- /**
1571
- * Register the child in a continuation-managed parent's owned set before the
1572
- * child can run, so that parent cannot settle while the child is live. A
1573
- * top-level or other non-continuation Agent has no Activation and stays
1574
- * outside the waiting graph.
1575
- */
1129
+ /** Register the child in a continuation-managed parent's owned set. */
1576
1130
  acquireOwnership(parent, childId) {
1577
- const parentActivation = this.activations.get(parent.id);
1131
+ const parentActivation = this.resident.get(parent.id);
1578
1132
  if (parentActivation === void 0) return;
1579
- if (parentActivation.disposal !== void 0) throw new SubagentError(`subagent parent "${parent.id}" is being disposed; the child was not established`, "ACTIVATION_CLOSING");
1133
+ if (parentActivation.inbox.closing !== void 0) throw new SubagentError(`subagent parent "${parent.id}" is being disposed; the child was not established`, "ACTIVATION_CLOSING");
1580
1134
  parentActivation.ownedChildren.add(childId);
1581
1135
  }
1582
1136
  /** Remove one child from its live owner's set and let that owner re-check settlement. */
1583
1137
  releaseOwnership(childId) {
1584
- for (const candidate of this.activations.values()) if (candidate.ownedChildren.delete(childId)) this.wake(candidate);
1138
+ for (const candidate of this.resident.values()) if (candidate.ownedChildren.delete(childId)) this.wake(candidate);
1585
1139
  }
1586
- /** Let a settlement watcher re-observe quiescence after ownership or inbox changes. */
1140
+ /** Let a settlement watcher re-check residency after relevant state changes. */
1587
1141
  wake(activation) {
1588
1142
  activation.poke.resolve();
1589
1143
  activation.poke = Promise.withResolvers();
1590
1144
  }
1591
- /**
1592
- * Submit one message as the child's next FIFO turn and return its accepted
1593
- * inbox id. Acceptance is the operation's success boundary; the manager owns
1594
- * the Activation independently afterwards.
1595
- */
1596
- submit(activation, content, options, parent) {
1597
- this.acquireOwnership(parent, activation.childId);
1598
- const message = options.delivery === "steer" ? agentMessage(parent, content) : createUserMessage({
1599
- content,
1600
- source: options.source
1601
- });
1602
- const accepted = this.admitWaking(activation, message.id, () => {
1603
- if (options.delivery === "steer") activation.handle.agent.steer(message);
1604
- else activation.handle.agent.followup(message);
1605
- });
1606
- activation.announced = true;
1607
- return accepted;
1608
- }
1609
- /**
1610
- * Account one waking send across a resident Activation's settlement window.
1611
- * @param activation - Activation receiving waking inbox work.
1612
- * @param messageId - stable identity of the message about to be sent.
1613
- * @param send - synchronous send that publishes one enqueue occurrence.
1614
- * @returns the accepted message id.
1615
- */
1616
- admitWaking(activation, messageId, send) {
1617
- activation.accepted.add(messageId);
1618
- try {
1619
- send();
1620
- } catch (error) {
1621
- activation.accepted.delete(messageId);
1622
- throw error;
1623
- }
1624
- this.wake(activation);
1625
- return messageId;
1626
- }
1627
- /**
1628
- * Cross the final admission cutoff and submit without yielding. Signal abort,
1629
- * manager drain, or Activation disposal that wins before this synchronous
1630
- * span rejects without inbox acceptance.
1631
- */
1632
- submitAdmitted(activation, content, options, parent) {
1633
- options.signal.throwIfAborted();
1634
- this.assertAdmitting(parent);
1635
- /* v8 ignore next 6 -- only a synchronous re-entrant disposer can change
1636
- * this field between the caller's live check and this no-await boundary. */
1637
- if (disposalOf(activation) !== void 0) throw new SubagentError(`subagent "${activation.childId}" activation is being disposed; the message was not accepted`, "ACTIVATION_CLOSING");
1638
- this.authorizeLineage(parent, activation.childId, activation.handle.agent.session.header.parentSession);
1639
- return this.submit(activation, content, options, parent);
1640
- }
1641
- /**
1642
- * Authorize one operation against the durable direct-parent lineage. Other
1643
- * agents, ancestors, teams, workflows, and hosts remain rejected until an
1644
- * explicit authority protocol has a production consumer.
1645
- */
1646
- authorizeLineage(parent, childId, parentSession) {
1647
- if (this.ctx.agents.get(parent.id) !== parent) throw new SubagentError(`subagent "${childId}" delivery requires the exact live parent agent`, "UNAUTHORIZED");
1648
- if (parentSession !== parent.id) throw new SubagentError(`subagent "${childId}" belongs to another parent session`, "UNAUTHORIZED");
1649
- }
1650
- /**
1651
- * Follow one Activation to settlement: wait for Agent quiescence, then for
1652
- * every owned child to complete disposal, and dispose the handle once both
1653
- * hold. A `next-turn` delivered while `waiting` wakes the same Agent and
1654
- * returns it to `running`, so this re-observes rather than settling early.
1655
- */
1145
+ /** Follow one Activation to natural settlement. */
1656
1146
  watchSettlement(activation) {
1657
1147
  (async () => {
1658
- while (disposalOf(activation) === void 0) {
1659
- const poked = activation.poke.promise;
1660
- await Promise.race([activation.handle.agent.whenIdle(), poked]);
1661
- if (disposalOf(activation) !== void 0) return;
1662
- const settling = await this.locks.run(activation.childId, () => {
1663
- if (disposalOf(activation) !== void 0 || this.stateOf(activation) !== "settled") return Promise.resolve({ settling: false });
1664
- return Promise.resolve({
1665
- settling: true,
1666
- done: this.dispose(activation)
1667
- });
1148
+ while (true) {
1149
+ const idleObservation = activation.poke;
1150
+ await activation.handle.agent.whenIdle();
1151
+ if (activation.inbox.closing !== void 0) return;
1152
+ const readiness = await this.locks.run(activation.childId, () => Promise.resolve(this.settlementState(activation, idleObservation)));
1153
+ if (readiness === "closed") return;
1154
+ if (readiness === "retry") continue;
1155
+ if (readiness === "wait") {
1156
+ await idleObservation.promise;
1157
+ continue;
1158
+ }
1159
+ const finalSeq = activation.handle.agent.session.seq;
1160
+ await this.flushFinalState(activation);
1161
+ const attempt = await this.locks.run(activation.childId, () => {
1162
+ const state = this.settlementState(activation, idleObservation);
1163
+ if (state !== "ready") return Promise.resolve(state);
1164
+ if (activation.handle.agent.session.seq !== finalSeq) return Promise.resolve("retry");
1165
+ let done;
1166
+ try {
1167
+ activation.handle.agent.runMaintenance(() => {
1168
+ done = this.dispose(activation, true);
1169
+ return Promise.resolve();
1170
+ });
1171
+ } catch {
1172
+ return Promise.resolve("retry");
1173
+ }
1174
+ return Promise.resolve({ done });
1668
1175
  });
1669
- if (!settling.settling) {
1670
- if (activation.handle.agent.status !== "running") await poked;
1176
+ if (attempt === "closed") return;
1177
+ if (attempt === "retry") continue;
1178
+ if (attempt === "wait") {
1179
+ await idleObservation.promise;
1671
1180
  continue;
1672
1181
  }
1673
1182
  try {
1674
- await settling.done;
1183
+ await attempt.done;
1675
1184
  } catch (error) {
1676
1185
  this.ctx.logger.warn(`subagent "${activation.childId}" activation teardown failed: ${errorChain(error)}`);
1677
1186
  }
@@ -1679,53 +1188,44 @@ var SubagentContinuationManager = class {
1679
1188
  }
1680
1189
  })();
1681
1190
  }
1682
- /**
1683
- * Stop one Activation immediately, then release it child-first. The memoized
1684
- * transaction is installed before cancellation or recursive callbacks, so
1685
- * admission and reentrant teardown converge on the same owner.
1686
- *
1687
- * The final session flush is best effort and never prevents handle disposal
1688
- * or ownership release, because retaining a child would permanently pin its
1689
- * ancestors in `waiting`.
1690
- * @param activation - the residency epoch to stop and release.
1691
- * @returns the one disposal transaction owned by this Activation.
1692
- */
1693
- dispose(activation) {
1694
- const existing = activation.disposal;
1695
- if (existing !== void 0) return existing;
1696
- const completion = Promise.withResolvers();
1697
- activation.disposal = completion.promise;
1698
- this.finishDisposal(activation).then(completion.resolve, completion.reject);
1699
- return completion.promise;
1191
+ /** Classify one Inbox and owned-child observation without reading Agent execution state. */
1192
+ settlementState(activation, observation) {
1193
+ if (activation.inbox.closing !== void 0) return "closed";
1194
+ if (activation.poke !== observation) return "retry";
1195
+ if (activation.inbox.hasPending || activation.ownedChildren.size > 0) return "wait";
1196
+ return "ready";
1700
1197
  }
1701
- /**
1702
- * Propagate stop synchronously, then finish the child-first release.
1703
- * @param activation - the Activation whose disposal transaction is installed.
1704
- * @returns once the handle and ownership edge are released.
1705
- */
1706
- async finishDisposal(activation) {
1198
+ /** Propagate stop synchronously, then finish the child-first release. */
1199
+ async finishDisposal(activation, finalStateFlushed) {
1707
1200
  this.wake(activation);
1708
1201
  const { childId } = activation;
1709
- activation.handle.agent.cancel({ kind: "parent" });
1710
- const idle = activation.handle.agent.whenIdle();
1711
- const childDisposals = [...activation.ownedChildren].map((child) => this.activations.get(child)).filter((child) => child !== void 0).map((child) => this.dispose(child));
1712
1202
  const failures = [];
1713
- try {
1714
- const reasons = (await Promise.all(childDisposals.map(async (disposal) => {
1715
- try {
1716
- await disposal;
1717
- return;
1718
- } catch (error) {
1719
- return error;
1720
- }
1721
- }))).filter((reason) => reason !== void 0);
1722
- if (reasons.length > 0) failures.push(new SubagentError(`subagent "${childId}" child teardown failed: ${reasons.map((reason) => errorChain(reason)).join("; ")}`, "ACTIVATION_TEARDOWN_FAILED"));
1723
- await idle;
1724
- await this.flushFinalState(activation);
1203
+ if (finalStateFlushed) try {
1725
1204
  activation.observer.capture(activation.handle.agent);
1726
1205
  } catch (error) {
1727
1206
  failures.push(new SubagentError(`subagent "${childId}" activation teardown failed: ${errorChain(error)}`, "ACTIVATION_TEARDOWN_FAILED", { cause: error }));
1728
1207
  }
1208
+ else {
1209
+ activation.handle.agent.cancel({ kind: "parent" });
1210
+ const idle = activation.handle.agent.whenIdle();
1211
+ const childDisposals = [...activation.ownedChildren].map((child) => this.resident.get(child)).filter((child) => child !== void 0).map((child) => this.dispose(child));
1212
+ try {
1213
+ const reasons = (await Promise.all(childDisposals.map(async (disposal) => {
1214
+ try {
1215
+ await disposal;
1216
+ return;
1217
+ } catch (error) {
1218
+ return error;
1219
+ }
1220
+ }))).filter((reason) => reason !== void 0);
1221
+ if (reasons.length > 0) failures.push(new SubagentError(`subagent "${childId}" child teardown failed: ${reasons.map((reason) => errorChain(reason)).join("; ")}`, "ACTIVATION_TEARDOWN_FAILED"));
1222
+ await idle;
1223
+ await this.flushFinalState(activation);
1224
+ activation.observer.capture(activation.handle.agent);
1225
+ } catch (error) {
1226
+ failures.push(new SubagentError(`subagent "${childId}" activation teardown failed: ${errorChain(error)}`, "ACTIVATION_TEARDOWN_FAILED", { cause: error }));
1227
+ }
1228
+ }
1729
1229
  try {
1730
1230
  await activation.handle.dispose();
1731
1231
  } catch (error) {
@@ -1734,72 +1234,29 @@ var SubagentContinuationManager = class {
1734
1234
  let failure;
1735
1235
  if (failures.length === 1) failure = failures[0];
1736
1236
  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) });
1737
- this.activations.delete(childId);
1237
+ this.resident.delete(childId);
1738
1238
  this.notifySettlement(activation, activation.observer.terminal(failure));
1739
1239
  this.releaseOwnership(childId);
1740
1240
  activation.observer.settle(failure);
1741
1241
  if (failure !== void 0) throw failure;
1742
1242
  }
1743
- /**
1744
- * Tell the durable direct parent that this child produced everything it is
1745
- * going to. Unconditional for every child the caller received an id for: it
1746
- * does not consider whether the child reported, because the cases that most
1747
- * need it — a token ceiling, a model failure, cancellation, teardown — are
1748
- * exactly the ones where the child never got to choose. A materialization
1749
- * rolled back before its first acceptance stays silent, since the caller was
1750
- * told that child was not established. A parent that is no longer live is not
1751
- * an error; the child's own Session remains the durable record either way.
1752
- * A parent whose own lineage is already closing receives the notice without a
1753
- * wake, because teardown is not a reason to start a turn.
1754
- *
1755
- * Never blocks disposal. A delivery failure is logged and dropped, because
1756
- * retaining a child to retry a notice would pin its whole ancestry in
1757
- * `waiting` forever.
1758
- * @param activation - the settling Activation, still owned by its parent.
1759
- * @param terminal - how this epoch ended, as the terminal edge will report it.
1760
- */
1243
+ /** Tell the durable direct parent how this Activation ended. */
1761
1244
  notifySettlement(activation, terminal) {
1762
1245
  if (!activation.announced) return;
1763
1246
  try {
1764
1247
  const parent = this.ctx.agents.get(activation.parentSession);
1765
1248
  if (parent === void 0) return;
1766
- const summary = settlementSummary(activation.childId, terminal.stopReason);
1767
- const message = createUserMessage({
1768
- content: [{
1769
- type: "text",
1770
- text: summary
1771
- }, ...terminal.output === void 0 ? [{
1772
- type: "text",
1773
- text: "It left no closing message."
1774
- }] : [{
1775
- type: "text",
1776
- text: "Its closing message:"
1777
- }, ...terminal.output]],
1778
- source: {
1779
- kind: "subagent-settled",
1780
- form: "notice",
1781
- summary: boundContextSummary(summary),
1782
- senderSessionId: activation.childId
1783
- }
1784
- });
1249
+ const message = createSettlementMessage(activation.childId, terminal);
1785
1250
  if (this.closingTeardownFor(parent) !== void 0) {
1786
1251
  parent.inject(message);
1787
1252
  return;
1788
1253
  }
1789
- this.sendWaking(parent, message, () => {
1790
- if (parent.status === "idle") parent.followup(message);
1791
- else parent.steer(message);
1792
- });
1254
+ this.sendWaking(parent, message, parent.status === "idle" ? "queue" : "steer");
1793
1255
  } catch (error) {
1794
1256
  this.ctx.logger.warn(`subagent "${activation.childId}" settlement notice was not delivered to its parent: ` + errorChain(error));
1795
1257
  }
1796
1258
  }
1797
- /**
1798
- * Request a best-effort final session flush after the child is quiescent.
1799
- * Listener failure is logged because flush participation cannot identify a
1800
- * particular persistence backend, and teardown must still release ownership.
1801
- * @param activation - the Activation whose final events should be flushed.
1802
- */
1259
+ /** Request a best-effort final session flush before closing natural-settlement admission. */
1803
1260
  async flushFinalState(activation) {
1804
1261
  const child = activation.handle.agent;
1805
1262
  try {
@@ -1808,6 +1265,591 @@ var SubagentContinuationManager = class {
1808
1265
  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)}`);
1809
1266
  }
1810
1267
  }
1268
+ };
1269
+ //#endregion
1270
+ //#region lib/types/descriptor.js
1271
+ /**
1272
+ * The durable subagent-child descriptor: the versioned, model-hidden
1273
+ * `subagent/descriptor` session event that identifies every session-backed
1274
+ * subagent and records whether it is one-shot or continuable. Continuable
1275
+ * descriptors additionally preserve the declared composition required for
1276
+ * cold resume. Providers append it turn-enclosed in the child's initial turn.
1277
+ *
1278
+ * The descriptor deliberately snapshots explicit fields rather than the
1279
+ * merge-extensible `AgentOptions` object: an unrelated extension value cannot
1280
+ * make continuation fail merely because it is not JSON, and later composition
1281
+ * inputs require a deliberate {@link SUBAGENT_DESCRIPTOR_VERSION} change. It
1282
+ * omits `subagentDepth` — cold resume trusts the persisted header's
1283
+ * `delegationDepth` as the monotone floor — and `outputSchema`, which belongs
1284
+ * to one activation's result contract rather than durable child composition.
1285
+ * Per-activation knobs such as `maxTokens` are omitted for the same reason as
1286
+ * `outputSchema`: they budget one activation. Cold resume requires the exact
1287
+ * live parent for authorization but reconstructs child options only from the
1288
+ * durable descriptor, so it neither restores the prior budget nor inherits
1289
+ * the parent's current one; the resumed route's defaults apply instead.
1290
+ *
1291
+ * @module @deepseek-ai/dsh-subagent/descriptor
1292
+ */
1293
+ /**
1294
+ * The current descriptor format version, stamped into every appended
1295
+ * `subagent/descriptor` event and required verbatim by {@link foldSubagentDescriptor}.
1296
+ * Supporting another composition input is a deliberate version change, never
1297
+ * an implicit extra field.
1298
+ */
1299
+ const SUBAGENT_DESCRIPTOR_VERSION = 3;
1300
+ const DESCRIPTOR_BASE_KEYS = [
1301
+ "version",
1302
+ "mode",
1303
+ "provider",
1304
+ "label"
1305
+ ];
1306
+ const ONE_SHOT_DESCRIPTOR_KEYS = new Set(DESCRIPTOR_BASE_KEYS);
1307
+ const CONTINUABLE_DESCRIPTOR_KEYS = new Set([
1308
+ ...DESCRIPTOR_BASE_KEYS,
1309
+ "agentProvider",
1310
+ "agentModel",
1311
+ "agentReasoningEffort",
1312
+ "persona",
1313
+ "toolFilter"
1314
+ ]);
1315
+ const TOOL_FILTER_KEYS = new Set(["allow", "deny"]);
1316
+ /** Whether a persisted JSON value is an object record. */
1317
+ function isRecord(value) {
1318
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1319
+ }
1320
+ /** Reject fields outside one versioned record's declared schema. */
1321
+ function assertKnownKeys(value, keys, path) {
1322
+ const unknown = Object.keys(value).find((key) => !keys.has(key));
1323
+ if (unknown !== void 0) throw new Error(`persisted subagent descriptor ${path} has unknown field "${unknown}"`);
1324
+ }
1325
+ /** Read one optional string field from a persisted descriptor record. */
1326
+ function optionalString(value, key) {
1327
+ if (!Object.hasOwn(value, key)) return void 0;
1328
+ const field = value[key];
1329
+ if (typeof field !== "string") throw new Error(`persisted subagent descriptor ${key} must be a string`);
1330
+ return field;
1331
+ }
1332
+ /** Read one optional string-array field from a persisted tool restriction. */
1333
+ function optionalStringArray(value, key) {
1334
+ if (!Object.hasOwn(value, key)) return void 0;
1335
+ const field = value[key];
1336
+ if (!Array.isArray(field)) throw new Error(`persisted subagent descriptor toolFilter.${key} must be an array of strings`);
1337
+ const items = field;
1338
+ if (items.some((item) => typeof item !== "string")) throw new Error(`persisted subagent descriptor toolFilter.${key} must be an array of strings`);
1339
+ return items;
1340
+ }
1341
+ /** Validate and reconstruct a persisted tool restriction. */
1342
+ function parseToolFilter(value) {
1343
+ if (!isRecord(value)) throw new Error("persisted subagent descriptor toolFilter must be an object");
1344
+ assertKnownKeys(value, TOOL_FILTER_KEYS, "toolFilter");
1345
+ const allow = optionalStringArray(value, "allow");
1346
+ const deny = optionalStringArray(value, "deny");
1347
+ if (allow === void 0 && deny === void 0) throw new Error("persisted subagent descriptor toolFilter must declare allow and/or deny");
1348
+ return {
1349
+ ...allow !== void 0 ? { allow } : {},
1350
+ ...deny !== void 0 ? { deny } : {}
1351
+ };
1352
+ }
1353
+ /** Validate one persisted descriptor payload for the current runtime. */
1354
+ function parseSubagentDescriptor(value) {
1355
+ if (!isRecord(value)) throw new Error("persisted subagent descriptor payload must be an object");
1356
+ const version = value["version"];
1357
+ if (typeof version !== "number") throw new Error("persisted subagent descriptor version must be a number");
1358
+ if (version !== 3) return void 0;
1359
+ const mode = value["mode"];
1360
+ if (mode !== "one-shot" && mode !== "continuable") throw new Error("persisted subagent descriptor mode must be \"one-shot\" or \"continuable\"");
1361
+ assertKnownKeys(value, mode === "one-shot" ? ONE_SHOT_DESCRIPTOR_KEYS : CONTINUABLE_DESCRIPTOR_KEYS, "payload");
1362
+ const provider = value["provider"];
1363
+ if (typeof provider !== "string") throw new Error("persisted subagent descriptor provider must be a string");
1364
+ if (mode === "one-shot") {
1365
+ const label = optionalString(value, "label");
1366
+ return {
1367
+ version: 3,
1368
+ mode,
1369
+ provider,
1370
+ ...label !== void 0 ? { label } : {}
1371
+ };
1372
+ }
1373
+ const label = value["label"];
1374
+ if (typeof label !== "string") throw new Error("persisted subagent descriptor label must be a string");
1375
+ const agentProvider = optionalString(value, "agentProvider");
1376
+ const agentModel = optionalString(value, "agentModel");
1377
+ const agentReasoningEffort = optionalString(value, "agentReasoningEffort");
1378
+ const persona = optionalString(value, "persona");
1379
+ const toolFilter = Object.hasOwn(value, "toolFilter") ? parseToolFilter(value["toolFilter"]) : void 0;
1380
+ return {
1381
+ version: 3,
1382
+ mode,
1383
+ provider,
1384
+ label,
1385
+ ...agentProvider !== void 0 ? { agentProvider } : {},
1386
+ ...agentModel !== void 0 ? { agentModel } : {},
1387
+ ...agentReasoningEffort !== void 0 ? { agentReasoningEffort } : {},
1388
+ ...persona !== void 0 ? { persona } : {},
1389
+ ...toolFilter !== void 0 ? { toolFilter } : {}
1390
+ };
1391
+ }
1392
+ function snapshotSubagentDescriptor(input) {
1393
+ const snapshot = snapshotJsonValue(input.mode === "one-shot" ? {
1394
+ version: 3,
1395
+ mode: input.mode,
1396
+ provider: input.provider,
1397
+ ...input.label !== void 0 ? { label: input.label } : {}
1398
+ } : {
1399
+ version: 3,
1400
+ mode: input.mode,
1401
+ provider: input.provider,
1402
+ label: input.label,
1403
+ ...input.agentProvider !== void 0 ? { agentProvider: input.agentProvider } : {},
1404
+ ...input.agentModel !== void 0 ? { agentModel: input.agentModel } : {},
1405
+ ...input.agentReasoningEffort !== void 0 ? { agentReasoningEffort: input.agentReasoningEffort } : {},
1406
+ ...input.persona !== void 0 ? { persona: input.persona } : {},
1407
+ ...input.toolFilter !== void 0 ? { toolFilter: input.toolFilter } : {}
1408
+ });
1409
+ if (snapshot === void 0) throw new Error("subagent descriptor is not losslessly JSON-serializable");
1410
+ return snapshot;
1411
+ }
1412
+ /**
1413
+ * Fold a persisted child log to its supported descriptor. The first
1414
+ * `subagent/descriptor` event is authoritative — the establishing provider
1415
+ * appends exactly one, so a later same-type event cannot rewrite the declared
1416
+ * composition.
1417
+ * @param events - the loaded child session events.
1418
+ * @returns the descriptor, or `undefined` when the log has none or its
1419
+ * version is not {@link SUBAGENT_DESCRIPTOR_VERSION} (the child cannot be
1420
+ * classified by this runtime).
1421
+ * @throws when a current-version persisted payload does not match its complete
1422
+ * declared schema.
1423
+ */
1424
+ function foldSubagentDescriptor(events) {
1425
+ const event = events.find((candidate) => candidate.type === "subagent/descriptor");
1426
+ if (event === void 0) return void 0;
1427
+ return parseSubagentDescriptor(event.data);
1428
+ }
1429
+ //#endregion
1430
+ //#region lib/types/internal.js
1431
+ /**
1432
+ * Continuation integration markers and host adapters outside the public
1433
+ * Service Definition and model-facing Agent messaging contract.
1434
+ * @module @deepseek-ai/dsh-subagent/internal
1435
+ */
1436
+ /** Process-stable identity carried only by the standard adjacent-Agent messaging tool. */
1437
+ const adjacentAgentSendMessageTool = Symbol.for("dsh.subagent.adjacentAgentSendMessageTool");
1438
+ /**
1439
+ * Test whether one visible definition is the standard adjacent-Agent messaging tool.
1440
+ * @param definition - the scope-resolved `send_message` candidate.
1441
+ * @returns whether the definition carries the internal standard-tool identity.
1442
+ */
1443
+ function isAdjacentAgentSendMessageTool(definition) {
1444
+ return definition !== void 0 && definition[adjacentAgentSendMessageTool] === true;
1445
+ }
1446
+ /**
1447
+ * Process-stable symbol-keyed host delivery shared by the bundled runtime
1448
+ * entry and this unbundled internal subpath.
1449
+ * @internal
1450
+ */
1451
+ const deliverSubagentPrompt = Symbol.for("dsh.subagent.deliverPrompt");
1452
+ //#endregion
1453
+ //#region lib/types/continuation.js
1454
+ /**
1455
+ * Continuable-subagent orchestration behind `ctx.subagents`: stable child ids,
1456
+ * descriptor persistence, provider preparation, cold resume, authorization,
1457
+ * and message routing. {@link ContinuableActivationRegistry} owns the mutable
1458
+ * process-local Activation graph and its settlement and disposal lifecycle.
1459
+ *
1460
+ * A continuable child has one durable Session and at most one process-local
1461
+ * Activation. The Agent inbox is the only turn queue, so this manager owns
1462
+ * durable orchestration while the Agent loop owns all turn ordering and
1463
+ * execution. No continuable path creates a Task or an intermediate
1464
+ * result-bearing wrapper.
1465
+ *
1466
+ * @module @deepseek-ai/dsh-subagent
1467
+ */
1468
+ var __addDisposableResource$1 = function(env, value, async) {
1469
+ if (value !== null && value !== void 0) {
1470
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
1471
+ var dispose, inner;
1472
+ if (async) {
1473
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
1474
+ dispose = value[Symbol.asyncDispose];
1475
+ }
1476
+ if (dispose === void 0) {
1477
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
1478
+ dispose = value[Symbol.dispose];
1479
+ if (async) inner = dispose;
1480
+ }
1481
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
1482
+ if (inner) dispose = function() {
1483
+ try {
1484
+ inner.call(this);
1485
+ } catch (e) {
1486
+ return Promise.reject(e);
1487
+ }
1488
+ };
1489
+ env.stack.push({
1490
+ value,
1491
+ dispose,
1492
+ async
1493
+ });
1494
+ } else if (async) env.stack.push({ async: true });
1495
+ return value;
1496
+ };
1497
+ var __disposeResources$1 = (function(SuppressedError) {
1498
+ return function(env) {
1499
+ function fail(e) {
1500
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
1501
+ env.hasError = true;
1502
+ }
1503
+ var r, s = 0;
1504
+ function next() {
1505
+ while (r = env.stack.pop()) try {
1506
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
1507
+ if (r.dispose) {
1508
+ var result = r.dispose.call(r.value);
1509
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) {
1510
+ fail(e);
1511
+ return next();
1512
+ });
1513
+ } else s |= 1;
1514
+ } catch (e) {
1515
+ fail(e);
1516
+ }
1517
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
1518
+ if (env.hasError) throw env.error;
1519
+ }
1520
+ return next();
1521
+ };
1522
+ })(typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message) {
1523
+ var e = new Error(message);
1524
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
1525
+ });
1526
+ /**
1527
+ * The continuable-subagent orchestration service behind `ctx.subagents`. Tool
1528
+ * schema and host adapters are consumers of this one contract; foreground
1529
+ * one-shot delegation keeps calling `ctx.subagents.start()` and never enters
1530
+ * this lifecycle.
1531
+ */
1532
+ var SubagentContinuationManager = class {
1533
+ ctx;
1534
+ host;
1535
+ activations;
1536
+ constructor(ctx, host) {
1537
+ this.ctx = ctx;
1538
+ this.host = host;
1539
+ this.activations = new ContinuableActivationRegistry(ctx, (provider, childId, parent) => host.observeActivation(provider, childId, parent));
1540
+ }
1541
+ /**
1542
+ * Start one continuable background child and resolve at initial inbox acceptance.
1543
+ * Every earlier failure disposes any created handle and rolls back Activation
1544
+ * and parent ownership without returning either id.
1545
+ * @param spec - provider, delegation request, and caller cancellation.
1546
+ * @returns the durable child id and accepted initial prompt message id.
1547
+ */
1548
+ async startContinuable(spec) {
1549
+ const request = spec.request;
1550
+ const parent = request.parent;
1551
+ this.activations.assertAdmitting(parent);
1552
+ const persistence = this.requirePersistence();
1553
+ assertSubagentMaxDepth(request.maxDepth);
1554
+ const childId = spec.childId ?? brandString(randomUUID());
1555
+ this.activations.assertChildIdAvailable(childId);
1556
+ const childDepth = resolveChildDepth(parent, request.maxDepth);
1557
+ const agentOptions = resolveChildAgentOptions(parent, request.agentOptions, childDepth);
1558
+ const agentProvider = agentOptions.provider;
1559
+ const agentModel = agentOptions.model;
1560
+ const agentReasoningEffort = agentOptions.reasoningEffort;
1561
+ const descriptor = snapshotSubagentDescriptor({
1562
+ mode: "continuable",
1563
+ provider: spec.provider,
1564
+ label: spec.label,
1565
+ ...agentProvider !== void 0 ? { agentProvider } : {},
1566
+ ...agentModel !== void 0 ? { agentModel } : {},
1567
+ ...agentReasoningEffort !== void 0 ? { agentReasoningEffort } : {},
1568
+ ...request.persona !== void 0 ? { persona: request.persona } : {},
1569
+ ...request.toolFilter !== void 0 ? { toolFilter: request.toolFilter } : {}
1570
+ });
1571
+ const delegatedPolicies = captureDelegatedPolicyOverrides(parent);
1572
+ const releaseHold = this.activations.holdOwnership(parent, childId);
1573
+ try {
1574
+ const prepared = await this.host.prepareContinuable(spec.provider, {
1575
+ sessionId: childId,
1576
+ parent,
1577
+ signal: spec.signal
1578
+ });
1579
+ spec.signal.throwIfAborted();
1580
+ this.activations.assertAdmitting(parent);
1581
+ const inheritedEventCount = SessionLogOffset(prepared.seed?.length ?? 0);
1582
+ const seed = prepared.seed;
1583
+ return {
1584
+ childId,
1585
+ messageId: await this.activations.locks.run(childId, async () => {
1586
+ spec.signal.throwIfAborted();
1587
+ this.activations.assertAdmitting(parent);
1588
+ this.activations.assertChildIdAvailable(childId);
1589
+ if (spec.childId !== void 0) {
1590
+ const persisted = await persistence.stat(childId, { signal: spec.signal });
1591
+ spec.signal.throwIfAborted();
1592
+ this.activations.assertAdmitting(parent);
1593
+ this.activations.assertChildIdAvailable(childId);
1594
+ if (persisted !== void 0) throw new SubagentError(`subagent "${childId}" already exists`, "DUPLICATE_CHILD");
1595
+ }
1596
+ const activation = await this.activations.materialize({
1597
+ childId,
1598
+ provider: spec.provider,
1599
+ parent,
1600
+ create: {
1601
+ seed,
1602
+ meta: childSessionMeta(parent, childDepth, prepared.seed !== void 0),
1603
+ inheritedEventCount,
1604
+ delegatedPolicies,
1605
+ descriptor
1606
+ },
1607
+ agentOptions,
1608
+ composition: {
1609
+ persona: request.persona,
1610
+ toolFilter: request.toolFilter
1611
+ },
1612
+ signal: spec.signal
1613
+ });
1614
+ return this.submitMaterialized(activation, isAdjacentAgentSendMessageTool(this.ctx.get("tools")?.get("send_message", activation.handle.agent)) ? withContinuableReturnGuidance(parent.id, request.prompt) : request.prompt, {
1615
+ source: { kind: "user" },
1616
+ signal: spec.signal,
1617
+ delivery: "queue"
1618
+ }, parent);
1619
+ })
1620
+ };
1621
+ } catch (error) {
1622
+ releaseHold();
1623
+ throw error;
1624
+ }
1625
+ }
1626
+ /**
1627
+ * Deliver one model-authored message to a direct continuable child or to the
1628
+ * sender's direct parent. A missing direct child cold-resumes through the
1629
+ * ordinary continuation lifecycle.
1630
+ * @param sender - exact live Agent authorizing and originating the message.
1631
+ * @param targetId - durable direct-parent or direct-child session id.
1632
+ * @param content - model-authored content to deliver.
1633
+ * @param options - caller cancellation before acceptance.
1634
+ * @returns the accepted message's inbox id.
1635
+ */
1636
+ async sendMessage(sender, targetId, content, options) {
1637
+ if (this.ctx.agents.get(sender.id) !== sender) throw new SubagentError("message delivery requires the exact live sender agent", "UNAUTHORIZED");
1638
+ this.activations.assertAdmitting(sender);
1639
+ const senderActivation = this.activations.get(sender.id);
1640
+ if (senderActivation !== void 0 && senderActivation.handle.agent === sender && senderActivation.parentSession === targetId) {
1641
+ options.signal.throwIfAborted();
1642
+ return this.sendToParent(senderActivation, sender, content);
1643
+ }
1644
+ 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");
1645
+ return this.deliverToChild(sender, targetId, content, {
1646
+ signal: options.signal,
1647
+ delivery: "steer"
1648
+ });
1649
+ }
1650
+ /**
1651
+ * Queue one human-authored prompt as a distinct direct-child turn.
1652
+ * @param parent - exact live direct parent authorizing delivery.
1653
+ * @param childId - durable direct-child session id.
1654
+ * @param content - model-visible prompt blocks.
1655
+ * @param source - durable attribution for the human prompt.
1656
+ * @param signal - caller cancellation before inbox acceptance.
1657
+ * @returns the accepted durable message id.
1658
+ */
1659
+ async queuePrompt(parent, childId, content, source, signal) {
1660
+ return this.deliverToChild(parent, childId, content, {
1661
+ source,
1662
+ signal,
1663
+ delivery: "queue"
1664
+ });
1665
+ }
1666
+ /**
1667
+ * Steer one host-authored prompt to a direct continuable child.
1668
+ * @param parent - exact live direct parent authorizing delivery.
1669
+ * @param childId - durable direct-child session id.
1670
+ * @param content - model-visible prompt blocks.
1671
+ * @param source - durable attribution for the host prompt.
1672
+ * @param signal - caller cancellation before inbox acceptance.
1673
+ * @returns the accepted durable message id.
1674
+ */
1675
+ async steerPrompt(parent, childId, content, source, signal) {
1676
+ return this.deliverToChild(parent, childId, content, {
1677
+ source,
1678
+ signal,
1679
+ delivery: "steer"
1680
+ });
1681
+ }
1682
+ /** Route one parent-originated delivery through residency and cold resume. */
1683
+ async deliverToChild(parent, childId, content, options) {
1684
+ this.activations.assertAdmitting(parent);
1685
+ const releaseHold = this.activations.holdOwnership(parent, childId);
1686
+ try {
1687
+ return await this.deliverFollowup(parent, childId, content, options);
1688
+ } catch (error) {
1689
+ releaseHold();
1690
+ throw error;
1691
+ }
1692
+ }
1693
+ /** The delivery loop behind {@link deliverToChild}, run under the parent hold. */
1694
+ async deliverFollowup(parent, childId, content, options) {
1695
+ while (true) {
1696
+ const live = await this.activations.locks.run(childId, async () => {
1697
+ const activation = this.activations.get(childId);
1698
+ if (activation === void 0) return this.coldResume(parent, childId, content, options);
1699
+ const disposal = activation.inbox.closing;
1700
+ /* v8 ignore next 3 -- the send-versus-dispose cutoff needs a delivery to
1701
+ * observe the transaction inside the same critical section that opened it. */
1702
+ if (disposal !== void 0) return disposal.then(() => void 0, () => void 0);
1703
+ if (contentHasImage(content)) {
1704
+ await this.assertImageCapable(activation.handle.agent, options.signal);
1705
+ if (activation.inbox.closing !== void 0) {
1706
+ await Promise.allSettled([activation.inbox.closing]);
1707
+ return;
1708
+ }
1709
+ }
1710
+ return this.submitAdmitted(activation, content, options, parent);
1711
+ });
1712
+ /* v8 ignore start -- only a delivery that lost the disposal cutoff retries. */
1713
+ if (live !== void 0) return live;
1714
+ this.activations.assertAdmitting(parent);
1715
+ options.signal.throwIfAborted();
1716
+ }
1717
+ }
1718
+ /**
1719
+ * Interrupt one live continuable child's current turn. Admission is
1720
+ * synchronous and the cancellation effect is asynchronous. An absent or
1721
+ * already-closing target is an accepted no-op after authority checks.
1722
+ * @param targetSessionId - the durable child session id to interrupt.
1723
+ * @param authority - the human parent address or exact live ancestor Agent.
1724
+ */
1725
+ interrupt(targetSessionId, authority) {
1726
+ this.activations.interrupt(targetSessionId, authority);
1727
+ }
1728
+ /** Deliver one resident continuable child's message to its live direct parent. */
1729
+ sendToParent(activation, sender, content) {
1730
+ /* v8 ignore next 6 -- only synchronous re-entrant teardown can open this
1731
+ * transaction between exact-agent authorization and this no-await span. */
1732
+ if (activation.inbox.closing !== void 0) throw new SubagentError(`subagent "${sender.id}" activation is being disposed; the message was not delivered`, "ACTIVATION_CLOSING");
1733
+ const parent = this.ctx.agents.get(activation.parentSession);
1734
+ if (parent === void 0) throw new SubagentError("direct parent is not live; the message was not delivered", "PARENT_UNAVAILABLE");
1735
+ const message = createAgentMessage(sender, content);
1736
+ this.sendAgentMessage(parent, message);
1737
+ return message.id;
1738
+ }
1739
+ /** Send one Agent message while translating only the target's own rejection. */
1740
+ sendAgentMessage(parent, message) {
1741
+ try {
1742
+ this.activations.sendWaking(parent, message, "steer");
1743
+ } catch (error) {
1744
+ throw new SubagentError("direct parent is not live; the message was not delivered", "PARENT_UNAVAILABLE", { cause: error });
1745
+ }
1746
+ }
1747
+ /** Close manager-wide admission and release every live Activation. */
1748
+ async drain() {
1749
+ await this.activations.drain();
1750
+ }
1751
+ /**
1752
+ * Stop only the continuable descendants of exact live host-owned parents.
1753
+ * @param parents - exact live roots whose continuable descendants must stop.
1754
+ */
1755
+ async drainDescendants(parents) {
1756
+ await this.activations.drainDescendants(parents);
1757
+ }
1758
+ /**
1759
+ * Release selected resident direct children of one exact live parent.
1760
+ * @param parent - exact live direct parent authorizing the selected release.
1761
+ * @param childIds - durable direct-child ids to release when resident.
1762
+ */
1763
+ async drainChildren(parent, childIds) {
1764
+ await this.activations.drainChildren(parent, childIds);
1765
+ }
1766
+ /**
1767
+ * Cold-resume a persisted child and submit the waiting turn. The descriptor
1768
+ * supplies every reconstruction input; no subagent provider is dispatched.
1769
+ */
1770
+ async coldResume(parent, childId, content, options) {
1771
+ const env_1 = {
1772
+ stack: [],
1773
+ error: void 0,
1774
+ hasError: false
1775
+ };
1776
+ try {
1777
+ const query = this.requireSessionQuery();
1778
+ let observation;
1779
+ try {
1780
+ observation = await query.observeSession(childId, { signal: options.signal });
1781
+ } catch (error) {
1782
+ options.signal.throwIfAborted();
1783
+ throw new SubagentError(`subagent "${childId}" is unavailable`, "NOT_RESUMABLE", { cause: error });
1784
+ }
1785
+ const source = __addDisposableResource$1(env_1, observation, false);
1786
+ this.activations.assertAdmitting(parent);
1787
+ this.activations.authorizeLineage(parent, childId, source.header.parentSession);
1788
+ const descriptor = foldSubagentDescriptor(source.events.slice(source.inheritedEventCount));
1789
+ 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");
1790
+ let activation;
1791
+ try {
1792
+ activation = await this.activations.materialize({
1793
+ childId,
1794
+ provider: descriptor.provider,
1795
+ parent,
1796
+ agentOptions: {
1797
+ ...descriptor.agentProvider !== void 0 ? { provider: descriptor.agentProvider } : {},
1798
+ ...descriptor.agentModel !== void 0 ? { model: descriptor.agentModel } : {},
1799
+ ...descriptor.agentReasoningEffort !== void 0 ? { reasoningEffort: ReasoningEffortId(descriptor.agentReasoningEffort) } : {}
1800
+ },
1801
+ composition: {
1802
+ persona: descriptor.persona,
1803
+ toolFilter: descriptor.toolFilter
1804
+ },
1805
+ signal: options.signal
1806
+ });
1807
+ } catch (error) {
1808
+ options.signal.throwIfAborted();
1809
+ if (error instanceof SubagentError) throw error;
1810
+ throw new SubagentError(`subagent "${childId}" is unavailable`, "NOT_RESUMABLE", { cause: error });
1811
+ }
1812
+ return await this.submitMaterialized(activation, content, options, parent);
1813
+ } catch (e_1) {
1814
+ env_1.error = e_1;
1815
+ env_1.hasError = true;
1816
+ } finally {
1817
+ __disposeResources$1(env_1);
1818
+ }
1819
+ }
1820
+ /** Submit to a freshly materialized Activation or roll it back completely. */
1821
+ async submitMaterialized(activation, content, options, parent) {
1822
+ try {
1823
+ if (contentHasImage(content)) {
1824
+ await this.assertImageCapable(activation.handle.agent, options.signal);
1825
+ if (activation.inbox.closing !== void 0) throw new SubagentError(`subagent "${activation.childId}" is closing`, "ACTIVATION_CLOSING");
1826
+ }
1827
+ return this.submitAdmitted(activation, content, options, parent);
1828
+ } catch (error) {
1829
+ /* v8 ignore next -- rollback disposal failures must not mask the
1830
+ * pre-acceptance signal, drain, or lifecycle failure. */
1831
+ await this.activations.dispose(activation).catch(() => void 0);
1832
+ throw error;
1833
+ }
1834
+ }
1835
+ /** Build and submit one message across the final synchronous admission cutoff. */
1836
+ submitAdmitted(activation, content, options, parent) {
1837
+ const message = options.source === void 0 ? createAgentMessage(parent, content) : createUserMessage({
1838
+ content,
1839
+ source: options.source
1840
+ });
1841
+ return this.activations.submitAdmitted(activation, message, options.delivery, parent, options.signal);
1842
+ }
1843
+ /** Refuse image content for a child whose fixed model accepts text only. */
1844
+ async assertImageCapable(agent, signal) {
1845
+ const { provider, model } = agent.options;
1846
+ if (provider === void 0 || model === void 0) return;
1847
+ const llm = this.ctx.get("llm");
1848
+ /* v8 ignore next -- without an LLM registry, delivery defers to projection. */
1849
+ if (llm === void 0) return;
1850
+ const info = await llm.resolveModelInfo(provider, model, signal);
1851
+ if (info.inputModalities !== void 0 && !info.inputModalities.includes("image")) throw new SubagentError(`Model "${model}" does not support image input.`, "MODEL_DOES_NOT_SUPPORT_IMAGES");
1852
+ }
1811
1853
  /** Resolve the persistence service continuable children require, or fail loud. */
1812
1854
  requirePersistence() {
1813
1855
  const persistence = this.ctx.get("sessionPersistence");
@@ -2319,7 +2361,7 @@ const subagentIdentityProjectionDefinition = {
2319
2361
  * working-directory resolution (config override, else the delegating parent
2320
2362
  * session's workspace), the never-reject result settlement, and the standard
2321
2363
  * run-handle publication. Backends compose these with their own wire drivers;
2322
- * the process machinery itself (spawn, env scrub, tree-scoped teardown)
2364
+ * the process machinery itself (spawn, env scrub, managed-range teardown)
2323
2365
  * belongs to the `dsh-subprocess` seam.
2324
2366
  *
2325
2367
  * @module @deepseek-ai/dsh-subagent/out-of-process
@@ -2751,7 +2793,7 @@ let SubagentRuntime = (() => {
2751
2793
  return this.requireContinuations().sendMessage(sender, targetId, content, options);
2752
2794
  }
2753
2795
  /**
2754
- * Queue one host-protocol message as a distinct direct-child turn.
2796
+ * Deliver one host-protocol message to a direct continuable child.
2755
2797
  * Symbol-keyed so host adapters can preserve their own provenance without
2756
2798
  * widening the public Service Definition or impersonating an Agent sender.
2757
2799
  * @param parent - exact live direct parent authorizing delivery.
@@ -2759,10 +2801,11 @@ let SubagentRuntime = (() => {
2759
2801
  * @param content - host-authored content to deliver.
2760
2802
  * @param source - durable host-protocol provenance.
2761
2803
  * @param signal - caller cancellation before inbox acceptance.
2804
+ * @param delivery - Queue as a distinct turn or Steer at the nearest step.
2762
2805
  * @returns the accepted message's inbox id.
2763
2806
  */
2764
- [queueSubagentPrompt](parent, childId, content, source, signal) {
2765
- return this.requireContinuations().queuePrompt(parent, childId, content, source, signal);
2807
+ [deliverSubagentPrompt](parent, childId, content, source, signal, delivery) {
2808
+ return delivery === "steer" ? this.requireContinuations().steerPrompt(parent, childId, content, source, signal) : this.requireContinuations().queuePrompt(parent, childId, content, source, signal);
2766
2809
  }
2767
2810
  /**
2768
2811
  * Interrupt one live continuable child's current turn under a human parent
@@ -2875,11 +2918,12 @@ let SubagentRuntime = (() => {
2875
2918
  * Deliver one browser-authored message to a continuable child through the
2876
2919
  * exact live direct parent, retaining the caller-minted request identity and
2877
2920
  * validated browser zone on the accepted message. Success identifies the
2878
- * message the child's FIFO inbox accepted; later execution is independent of
2879
- * this call.
2921
+ * message the child's inbox accepted; later execution is independent of this
2922
+ * call. Queue delivery targets a later turn; steer delivery targets the
2923
+ * nearest step and retains the Agent loop's best-effort fallback semantics.
2880
2924
  * Image parts are admitted and persisted through the attachment store
2881
2925
  * before delivery, and the child's model must accept image input.
2882
- * @param request - durable address, minted identity, content, and optional browser zone.
2926
+ * @param request - durable address, delivery, minted identity, content, and optional browser zone.
2883
2927
  * @param signal - carrier cancellation, owning the call until inbox acceptance.
2884
2928
  * @returns the accepted message's inbox identity.
2885
2929
  * @throws {RemoteError} `gateway/bad-request`, `subagent/attachment-invalid`,
@@ -2888,7 +2932,7 @@ let SubagentRuntime = (() => {
2888
2932
  * `subagent/delivery-unavailable`, `gateway/cancelled`, or `gateway/internal`.
2889
2933
  */
2890
2934
  async prompt(request, signal) {
2891
- const { parentSessionId, childSessionId, clientTimeZone } = request;
2935
+ const { parentSessionId, childSessionId, clientTimeZone, delivery } = request;
2892
2936
  validateControlRequest("subagent.prompt", request);
2893
2937
  const canonicalTimeZone = clientTimeZone === void 0 ? void 0 : canonicalClientTimeZone(clientTimeZone);
2894
2938
  if (clientTimeZone !== void 0 && canonicalTimeZone === void 0) throw new RemoteError("subagent/invalid-time-zone", "clientTimeZone must be UTC or a valid IANA Area/Location name", { value: clientTimeZone });
@@ -2908,9 +2952,9 @@ let SubagentRuntime = (() => {
2908
2952
  else {
2909
2953
  const attachments = this.ctx.get("attachments");
2910
2954
  if (attachments === void 0) throw new Error("subagent image prompt requires an attachment store");
2911
- content = await admitPromptContent(attachments, request.content);
2955
+ content = await attachments.admitPromptContent(request.content);
2912
2956
  }
2913
- return { messageId: await this[queueSubagentPrompt](parent, childSessionId, content, source, signal) };
2957
+ return { messageId: await this[deliverSubagentPrompt](parent, childSessionId, content, source, signal, delivery) };
2914
2958
  } catch (error) {
2915
2959
  return rejectPrompt(error, childSessionId, signal);
2916
2960
  }
@@ -3063,4 +3107,4 @@ let SubagentRuntime = (() => {
3063
3107
  };
3064
3108
  })();
3065
3109
  //#endregion
3066
- 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, seedDescriptorTurn, settleRun, settleRunResult, snapshotSubagentDescriptor, subprocessRunHandle, validateConfiguredCwd };
3110
+ 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 };