@deepseek-ai/dsh-subagent 0.1.0-rc.7 → 0.1.1-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/subagent/subagent/README.md
5
- README.md: ed4a9123a2dfa5b2fa5abc67f4513547feb3d140
6
- README.zh.md: 3ad2ee5738a210a776d1f0b2746dcbd21d46144c
5
+ README.md: e84a6b486253e81ccf7e7df12c4149e6df4ed9f2
6
+ README.zh.md: e289863531c1686cedeccadfa76e2661dfa9bfc8
package/README.md CHANGED
@@ -16,12 +16,13 @@ The [subagent family overview](../README.md) maps implementations and model-faci
16
16
  | `getProvider(name)` | Return the provider, or `undefined` when absent. |
17
17
  | `list()` | Return provider names in insertion order. |
18
18
  | `start(name, request)` | Validate an ordinary caller request, resolve its detached `one-shot` descriptor, then await the provider until a real one-shot child is published. Fulfillment returns a holder-owned `SubagentRun`; rejection means the provider has already cleaned every unpublished startup resource, while post-publication turn or infrastructure faults settle through the run. Continuable children never enter through this operation. |
19
- | `startContinuable(spec)` | Establish one durable continuable child and deliver its initial prompt. Resolves with `{ childId, messageId }` when the child's inbox accepts that prompt, without waiting for the turn to start or for the message to reach the Session log; any earlier failure rejects with no ids and rolls the child back entirely. Requires `ctx.agents`, session persistence, and a provider with the `prepareContinuable` capability. |
19
+ | `startContinuable(spec)` | Establish one durable continuable child and deliver its initial prompt. Resolves with `{ childId, messageId }` when the child's inbox accepts that prompt, without waiting for the turn to start or for the message to reach the Session log; any earlier failure rejects with no ids and rolls the child back entirely. A caller-reserved `childId` is rejected when the live registries or configured persistence already own it. Requires `ctx.agents`, session persistence, and a provider with the `prepareContinuable` capability. |
20
20
  | `followup(parent, childId, content, { source, signal })` | Deliver one later message from the exact live direct parent as the child's next FIFO turn, matching `Agent.followup()` terminology, and return the accepted `MessageId`. A resident child's inbox accepts it directly (waking a waiting Activation); an absent one cold-resumes from its persisted Session. Requires `ctx.agents`; cold resume also requires session persistence. |
21
21
  | `interrupt(targetSessionId, authority)` | Interrupt one live continuable child's current turn under a human durable parent address (`{ kind: 'user', parentSessionId }`) or an exact live ancestor Agent (`{ kind: 'ancestor', agent }`). Admission is synchronous and the effect asynchronous: it issues `Agent.cancel(cause, { keepInbox: true })` and returns without waiting for the target to observe the signal. Unclaimed pending inbox work, the Activation, and published descendants are preserved; work already claimed into the interrupted turn is not requeued. An absent target is an accepted no-op; a wrong parent address or a stale, self-targeting, or non-ancestor caller rejects with `UNAUTHORIZED`. |
22
- | `reportFrom(child, content, { delivery, signal })` | Deliver one selected message from the exact live continuable child to its exact live direct parent and return the accepted stable `MessageId`. Quiet delivery injects context; waking delivery submits one later parent turn. |
22
+ | `reportFrom(child, content, { delivery, signal })` | Deliver one selected message from the exact live continuable child to its exact live direct parent and return the accepted stable `MessageId`. Quiet delivery injects next-step context without waking; next-step delivery steers and wakes the parent. |
23
23
  | `registerContinuableSetup(contribution)` | Compose an optional deployment capability into each continuable child's unpublished scope, with immediate revocation from resident children. |
24
24
  | `drainContinuableDescendants(parents)` | Close admission below exact live host-owned parent Agents, stop only their visible continuable descendants, await materializations admitted below those roots through publication or rollback, then release the selected forests child-first. The cutoff lasts until each exact parent leaves the registry; unrelated parent forests and manager-wide admission remain live. |
25
+ | `drainContinuableChildren(parent, childIds)` | Release only the named resident continuable direct children of one exact live parent, recursively and child-first. It does not close admission or touch siblings, accepts absent ids as no-ops, and rejects a resident child owned by another parent. This is teardown, so unlike `interrupt()` it does not preserve pending inbox work. |
25
26
  | `listChildren(parentSessionId, signal?)` | List direct session-backed subagents with their `one-shot`/`continuable` mode, `running`/`inactive` activity, origin-classified one-level `hasChildren` hint, and per-child diagnostics, ordered by `createdAt` then id, without loading or resuming them. Reads the live session store and optional session persistence directly (live-only enumeration when persistence is absent) and requires the mounted `sessionProjections` registry; it does not require `ctx.agents`, the continuation manager, or any query service. |
26
27
  | `listDescendants(rootSessionId, signal?)` | Flatten the root's complete session tree in stable pre-order from the same live-preferred corpus, adding each subagent entry's durable `parentId` and root-relative `depth`. Ordinary sessions and one-shot children remain traversal nodes so continuable descendants below them are discovered. Identity, diagnostics, dependencies, and cancellation follow `listChildren()`. |
27
28
 
@@ -64,7 +65,7 @@ Both in-process delegation paths fix the child's permission scope at the delegat
64
65
 
65
66
  `provider.start(request): Promise<SubagentRun>` is the ownership-transfer boundary; the delegation tool also uses it inside its one-shot Task-backed background path. Before fulfillment, the provider owns setup and must cancel, roll back, and quiesce unpublished resources on every failure. After fulfillment, the caller owns the run and must call `dispose()` on every path; remaining prompt and turn work belongs to `SubagentRun.result`.
66
67
 
67
- `SubagentRun.result` resolves to `{ output, structured?, stopReason }`. Child-level failures resolve with a non-`completed` reason; only an infrastructure fault that the seam cannot represent may reject. `dispose()` is idempotent, cancels remaining work, and waits for both result settlement and child-resource quiescence. A result rejection remains on `result`; `dispose()` rejects only for an independent resource-release failure. `output` and the `subagent/end` event's `lastAssistantMessage` use the exported `AssistantOutputFold`/`finalAssistantOutput` helpers to select the child's last non-empty assistant message, or its accumulated assistant text when no such message exists. `output` is `[]` and the event field is absent when the child produced neither ([`SubagentResult.output`](../../../docs/subsystems/subagent.md#the-terminal-result-subagentresult) owns the result contract).
68
+ `SubagentRun.result` resolves to `{ output, structured?, diagnostic?, stopReason }`. Child-level failures resolve with a non-`completed` reason; only an infrastructure fault that the seam cannot represent may reject. A provider may add a safe `diagnostic` to a non-completed result after removing tool inputs, file contents, environment values, credentials, and raw protocol payloads and limiting the complete text to 4096 UTF-8 bytes. The common result type does not define provider categories or lifecycle stages: an out-of-process provider may derive fixed display text from its version-pinned structured product facts and an observed process outcome, while consumers render that text without parsing it. The field is not assistant output: consumers present it separately, and it does not enter `subagent/end.lastAssistantMessage`. `dispose()` is idempotent, cancels remaining work, and waits for both result settlement and child-resource quiescence. A result rejection remains on `result`; `dispose()` rejects only for an independent resource-release failure. `output` and the `subagent/end` event's `lastAssistantMessage` use the exported `AssistantOutputFold`/`finalAssistantOutput` helpers to select the child's last non-empty assistant message, or its accumulated assistant text when no such message exists. `output` is `[]` and the event field is absent when the child produced neither ([`SubagentResult`](../../../docs/subsystems/subagent.md#the-terminal-result-subagentresult) owns the terminal result contract).
68
69
 
69
70
  A local run publishes an ordinary child agent/session before `start()` fulfills, returns that shared session id as `SubagentRun.id`, exposes the exact child as `SubagentRun.localAgent`, records `request.parent.session.id` in the child's `parentSession` header, and appends the resolved descriptor inside its initial turn. Remote providers instead mint a parent-scoped lifecycle id and return `localAgent: undefined`; without a local child session, their one-shot runs are not part of trace-backed enumeration.
70
71
 
@@ -146,7 +147,7 @@ Prefix-stable within a child: the statement never changes during the child's lif
146
147
 
147
148
  - **ACP children remain one-shot and are not trace-enumerable** — an ACP run has no local child session in the parent's session corpus. An ACP `prepareContinuable` requires persisting the remote session id in provider-specific descriptor data and a per-child continuation advertisement, since ACP `loadSession` support is negotiated per child rather than established by the method's presence. Remote providers also require a separate Activation ownership contract with equivalent authenticated control and child-first quiescence before they support continuable children.
148
149
  - **No host-user continuation** — `followup()` requires the exact live direct parent. Only `interrupt()` accepts a durable parent-address user authority, because stopping a turn is idempotent and delivers no content; a future host adapter needs a concrete authenticated interaction before the seam gains a user delivery capability.
149
- - **No current-turn steering** — continuable messages and waking reports enqueue later turns; neither redirects an open turn.
150
+ - **Continuation messages never steer** — parent-to-child continuation messages enqueue later child turns. Child-to-parent reports are independent next-step input and may extend the parent's open turn.
150
151
  - **Wake gap during cancellation convergence** — a waking follow-up accepted after the interrupt signal is issued but before the active driver becomes idle remains queued until another waking send. Issue #1838 owns the agent-loop wake latch, which also affects ordinary session cancellation.
151
152
  - **Process-local residency** — the Activation inbox and ownership graph do not coordinate two harness processes; concurrent access to one persistence store still requires a durable mailbox and cross-process lease protocol.
152
153
  - **No replay of accepted-but-unlogged messages** — only messages written to the child Session log are reconstructable with the source that supplied them. A crash may lose an accepted initial prompt or follow-up that never reached the log; a later authorized message can cold-resume the child, but the lost message is not replayed automatically.
package/README.zh.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  subagent seam 允许一个 agent(智能体)通过具名提供方把工作委派给子 agent。调用方统一使用 `ctx.subagents` 服务 API;提供方决定子 agent 在当前进程、其他进程,还是通过未来的传输方式运行。
6
6
 
7
- [subagent 家族概述](../README.md)列出了实现和面向模型的消费方。本包负责提供方注册表、共享请求和结果约定、持久描述符以及可继续子级编排。多个具名提供方可以在该约定背后共存。
7
+ [subagent 家族概述](../README.zh.md)列出了实现和面向模型的消费方。本包负责提供方注册表、共享请求和结果约定、持久描述符以及可继续子级编排。多个具名提供方可以在该约定背后共存。
8
8
 
9
9
  ## 服务 API
10
10
 
@@ -16,12 +16,13 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
16
16
  | `getProvider(name)` | 返回提供方;不存在时返回 `undefined`。 |
17
17
  | `list()` | 按插入顺序返回提供方名称。 |
18
18
  | `start(name, request)` | 校验普通调用方请求,解析其已分离的 `one-shot` 描述符,然后等待提供方发布真正的一次性子 agent。兑现时返回由持有方拥有的 `SubagentRun`;如果调用被拒绝,提供方已经清理所有尚未发布的启动资源。发布后的轮次故障或基础设施故障则通过该 run 结算。可继续子 agent 绝不通过此操作进入。 |
19
- | `startContinuable(spec)` | 建立一个持久化的可继续子 agent,并投递其初始提示词。子 agent 的 inbox 一接受该提示词,调用就会兑现为 `{ childId, messageId }`,无需等待轮次开始,也无需等待消息写入会话日志。在此之前发生的任何失败都会使调用被拒绝,不返回任何 id,并完全回滚该子 agent。要求 `ctx.agents`、会话持久化以及具备 `prepareContinuable` 能力的提供方。 |
19
+ | `startContinuable(spec)` | 建立一个持久化的可继续子 agent,并投递其初始提示词。子 agent 的 inbox 一接受该提示词,调用就会兑现为 `{ childId, messageId }`,无需等待轮次开始,也无需等待消息写入会话日志。在此之前发生的任何失败都会使调用被拒绝,不返回任何 id,并完全回滚该子 agent。如果在线注册表或已配置的持久化已经占用调用方预留的 `childId`,则拒绝该身份。要求 `ctx.agents`、会话持久化以及具备 `prepareContinuable` 能力的提供方。 |
20
20
  | `followup(parent, childId, content, { source, signal })` | 将来自确切在线直接父级的一条后续消息作为子 agent 的下一个 FIFO 轮次投递,术语与 `Agent.followup()` 一致,并返回被接受的 `MessageId`。驻留中的子 agent 由其 inbox 直接接受(唤醒处于 waiting 的 Activation);不驻留的则从其持久化会话冷恢复。要求 `ctx.agents`;冷恢复还要求会话持久化。 |
21
21
  | `interrupt(targetSessionId, authority)` | 凭人类出示的持久化父级地址 `{ kind: 'user', parentSessionId }`,或确切在线的祖先 Agent `{ kind: 'ancestor', agent }` 进行授权,中断一个在线可继续子级的当前轮次。准入判定同步完成,但取消异步生效:该操作发出 `Agent.cancel(cause, { keepInbox: true })` 后立即返回,不等待目标观察到信号。尚未领取的待处理 inbox 工作、Activation 和已发布的后代均会保留;已经领取到被中断轮次中的工作不会重新入队。目标不存在时视为已接受的空操作;错误的父级地址,或陈旧、指向自身、并非祖先的调用方,会以 `UNAUTHORIZED` 被拒绝。 |
22
- | `reportFrom(child, content, { delivery, signal })` | 从确切在线可继续 child 向其确切在线直接 parent 投递一条选中消息,并返回已接受的稳定 `MessageId`。静默投递会注入上下文;唤醒投递会提交一个后续 parent 轮次。 |
22
+ | `reportFrom(child, content, { delivery, signal })` | 从确切在线可继续 child 向其确切在线直接 parent 投递一条选中消息,并返回已接受的稳定 `MessageId`。静默投递会注入不唤醒的 next-step 上下文;next-step 投递会 steering 并唤醒 parent。 |
23
23
  | `registerContinuableSetup(contribution)` | 把一项可选部署能力组合到每个可继续 child 尚未发布的作用域中,并支持从驻留 child 立即撤销。 |
24
24
  | `drainContinuableDescendants(parents)` | 在由 host 拥有的确切在线父级 Agent 之下关闭准入,只停止这些父级可见的可继续后代;等待已在这些根节点下获准的物化过程完成发布或回滚后,再按子级优先顺序释放所选的各棵树。该截止状态会持续到每个确切父级离开注册表;无关的父级树仍在线,管理器全局准入仍保持开放。 |
25
+ | `drainContinuableChildren(parent, childIds)` | 只释放一个确切在线父级的具名驻留可继续直接子级,并递归保持子级优先顺序。它不关闭准入、不影响同级子级,缺失的 id 视为空操作;若驻留子级属于其他父级,则拒绝。这是拆卸操作,因此与 `interrupt()` 不同,它不会保留待处理的 inbox 工作。 |
25
26
  | `listChildren(parentSessionId, signal?)` | 按 `createdAt`、再按 id 的顺序列出由会话支撑的直接 subagent,包括其 `one-shot`/`continuable` 模式、`running`/`inactive` 活动状态、根据 origin 分类得出的一层 `hasChildren` 提示,以及每个子级的诊断信息,且不会加载或恢复它们。该操作直接读取在线会话存储和可选的会话持久化(没有持久化时只枚举在线子级),并要求已挂载 `sessionProjections` 注册表;不要求 `ctx.agents`、继续执行管理器或任何查询服务。 |
26
27
  | `listDescendants(rootSessionId, signal?)` | 从同一份在线优先语料按稳定 pre-order 展平根的完整会话树,并为每个 subagent 条目附加持久 `parentId` 与相对根的 `depth`。普通会话与一次性 child 仍作为遍历节点,因此其下的可继续后代仍可发现。身份、diagnostic、依赖与取消约定均沿用 `listChildren()`。 |
27
28
 
@@ -40,7 +41,7 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
40
41
  - `toolFilter`:应用请求的子 agent 工具限制;
41
42
  - `persona`:应用每个子 agent 独立的 persona。
42
43
 
43
- 每个进程内子 agent 都通过一次 `applyChildComposition(childCtx, parent, composition)` 调用完成组装:先加入父级的 agent-preset 组合,再应用子 agent 自己的 persona 和工具限制。加入父级组合正是子 agent 获得能力的途径:所有面向模型的行都位于 agent 平面,完全没有加入任何组合的子 agent 抵达模型时会看到空的工具注册表(见 [`dsh-agent-presets`](../../preset/agent-presets/README.md))。将父级作为参数是刻意设计:这让“组装子 agent 却不做该加入”在各调用点无法表达,而这正是这一次调用所要杜绝的缺陷。未组装 preset roster 的部署不加入任何组合、也不需要加入;其面向模型的行位于宿主组合中,子 agent 已能通过工具注册表的全局层解析到它们。
44
+ 每个进程内子 agent 都通过一次 `applyChildComposition(childCtx, parent, composition)` 调用完成组装:先加入父级的 agent-preset 组合,再应用子 agent 自己的 persona 和工具限制。加入父级组合正是子 agent 获得能力的途径:所有面向模型的行都位于 agent 平面,完全没有加入任何组合的子 agent 抵达模型时会看到空的工具注册表(见 [`dsh-agent-presets`](../../preset/agent-presets/README.zh.md))。将父级作为参数是刻意设计:这让“组装子 agent 却不做该加入”在各调用点无法表达,而这正是这一次调用所要杜绝的缺陷。未组装 preset roster 的部署不加入任何组合、也不需要加入;其面向模型的行位于宿主组合中,子 agent 已能通过工具注册表的全局层解析到它们。
44
45
 
45
46
  `childSessionMeta()` 把所加入的 preset id 记在子 agent 的持久化 header 上,理由与顶层会话记录自己的那一个相同:preset 决定了模型所见的工具 schema 与提示段,因此冷读子 agent 的历史时必须重建那份组装,而不是部署默认值。该值从父方**活着的** scope 链读取,而不是从父方 header 读取,因为在空白期切换过 preset 的父方运行在更新的那份组装上,而它的 header 仍写着旧的那个。
46
47
 
@@ -56,15 +57,17 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
56
57
 
57
58
  `inheritsParentContext` 只用于描述,不能强制执行。它仅说明子 agent 是否能看到父级已完成的对话历史(`fork` 可以;`spawn` 和各进程外一次性提供方不可以),不表示是否继承工具、服务或权限。
58
59
 
60
+ <a id="delegated-policy"></a>
61
+
59
62
  ## 委派策略
60
63
 
61
- 两条进程内委派路径都会通过共享的子 agent 辅助函数,在委派边界固定子 agent 的权限范围。`captureDelegatedPolicyOverrides(parent)` 会为父会话的显式沙箱覆盖项(`sandboxPolicy.overrideOf()`)创建快照,并在审批能力已组合时将子 agent 的审批策略固定为 `'never'`,无论父级自身采用何种策略。这样,被委派的子 agent 只能在继承的沙箱范围内行动,每次审批请求(例如 `sandbox_permissions` 升权)都会被确定性拒绝,而不会等待无人处理的提示(这两个服务都是可选的 `ctx.get` 消费方)。`appendDelegatedPolicyOverrides()` 则在未发布的设置阶段、在任何 fork 种子之后,把每个值作为一条 `source: 'delegation'` 的 `sandbox/mode` 或 `approval/policy` 事件写入子 agent 自己的日志。因此,新捕获的策略会覆盖种子中的陈旧状态,而子 agent 的生效策略始终可以仅凭其日志重建。沙箱的部署默认值绝不复制:未切换的父级不会记录 `sandbox/mode`,其子 agent 会动态跟随部署默认值。可继续启动会在第一次 await 前捕获策略,并且只为全新物化写入这些委派事件;冷恢复只会重放已持久化的委派事件,不会重新捕获父级策略,因此创建之后的父级切换绝不会追溯性地改变持久化子 agent。每个进程内子 agent 还会收到一条作用域内的运行时上下文声明(`subagent:delegation`),告知其权限范围已固定,需要更宽访问的任务应以上报限制收尾,而不是重试。参见[一次性](../../../.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md)与[可继续](../../../.agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.md)两篇委派策略 Agent Note。
64
+ 两条进程内委派路径都会通过共享的子 agent 辅助函数,在委派边界固定子 agent 的权限范围。`captureDelegatedPolicyOverrides(parent)` 会为父会话的显式沙箱覆盖项(`sandboxPolicy.overrideOf()`)创建快照,并在审批能力已组合时将子 agent 的审批策略固定为 `'never'`,无论父级自身采用何种策略。这样,被委派的子 agent 只能在继承的沙箱范围内行动,每次审批请求(例如 `sandbox_permissions` 升权)都会被确定性拒绝,而不会等待无人处理的提示(这两个服务都是可选的 `ctx.get` 消费方)。`appendDelegatedPolicyOverrides()` 则在未发布的设置阶段、在任何 fork 种子之后,把每个值作为一条 `source: 'delegation'` 的 `sandbox/mode` 或 `approval/policy` 事件写入子 agent 自己的日志。因此,新捕获的策略会覆盖种子中的陈旧状态,而子 agent 的生效策略始终可以仅凭其日志重建。沙箱的部署默认值绝不复制:未切换的父级不会记录 `sandbox/mode`,其子 agent 会动态跟随部署默认值。可继续启动会在第一次 await 前捕获策略,并且只为全新物化写入这些委派事件;冷恢复只会重放已持久化的委派事件,不会重新捕获父级策略,因此创建之后的父级切换绝不会追溯性地改变持久化子 agent。每个进程内子 agent 还会收到一条作用域内的运行时上下文声明(`subagent:delegation`),告知其权限范围已固定,需要更宽访问的任务应以上报限制收尾,而不是重试。参见[一次性](../../../.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.zh.md)与[可继续](../../../.agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.zh.md)两篇委派策略 Agent Note。
62
65
 
63
66
  ## 一次性所有权与生命周期
64
67
 
65
68
  `provider.start(request): Promise<SubagentRun>` 是所有权转移边界;委派工具也会在其由 Task 支撑的一次性后台路径中使用它。兑现前,提供方拥有设置过程,并且在任何失败路径上都必须取消、回滚并使尚未发布的资源完全停稳。兑现后,run 的所有权转移给调用方;调用方必须在每条路径上调用 `dispose()`。剩余提示词和轮次工作属于 `SubagentRun.result`。
66
69
 
67
- `SubagentRun.result` 兑现为 `{ output, structured?, stopReason }`。子 agent 级失败会以非 `completed` 原因兑现;只有 seam 无法表示的基础设施故障才可以拒绝。`dispose()` 是幂等的,会取消剩余工作,并等待结果结算以及子 agent 资源完全停稳。result 的拒绝只通过 `result` 本身报告;只有独立的资源释放失败,才会使 `dispose()` 被拒绝。`output` 与 `subagent/end` 事件的 `lastAssistantMessage` 使用导出的 `AssistantOutputFold`/`finalAssistantOutput` 辅助函数选取子 agent 最后一条非空 assistant 消息;若没有这类消息,则选取其累积的 assistant 文本。子 agent 两种输出均未产生时,`output` 为 `[]`,该事件字段缺省(结果约定归 [`SubagentResult.output`](../../../docs/subsystems/subagent.md#the-terminal-result-subagentresult) 所有)。
70
+ `SubagentRun.result` 兑现为 `{ output, structured?, diagnostic?, stopReason }`。子 agent 级失败会以非 `completed` 原因兑现;只有 seam 无法表示的基础设施故障才可以拒绝。提供方可以为非完成结果附加安全的 `diagnostic`:它会先排除工具输入、文件内容、环境值、凭证与原始协议载荷,并把完整文本限制在 4096 个 UTF-8 字节以内。共享结果类型不定义提供方类别或生命周期阶段:进程外提供方可以从锁定版本产品提供的结构化事实与已观测的进程结果派生固定展示文本,而消费方只负责原样呈现,不解析该文本。该字段不是 assistant 输出;消费方会将它分开呈现,它也不会进入 `subagent/end.lastAssistantMessage`。`dispose()` 是幂等的,会取消剩余工作,并等待结果结算以及子 agent 资源完全停稳。result 的拒绝只通过 `result` 本身报告;只有独立的资源释放失败,才会使 `dispose()` 被拒绝。`output` 与 `subagent/end` 事件的 `lastAssistantMessage` 使用导出的 `AssistantOutputFold`/`finalAssistantOutput` 辅助函数选取子 agent 最后一条非空 assistant 消息;若没有这类消息,则选取其累积的 assistant 文本。子 agent 两种输出均未产生时,`output` 为 `[]`,该事件字段缺省(终态结果约定归 [`SubagentResult`](../../../docs/subsystems/subagent.zh.md#the-terminal-result-subagentresult) 所有)。
68
71
 
69
72
  本地运行会在 `start()` 兑现前发布普通的子 agent/会话,把该共享会话 id 作为 `SubagentRun.id` 返回,以 `SubagentRun.localAgent` 公开准确的子 agent,把 `request.parent.session.id` 记录到子 agent 的 `parentSession` header,并在其初始轮次内追加已解析的描述符。远程提供方则生成 parent 作用域的生命周期 id,并返回 `localAgent: undefined`;由于没有本地 child 会话,其一次性运行不会进入基于追踪的枚举结果。
70
73
 
@@ -102,12 +105,14 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
102
105
 
103
106
  ## 收集模型
104
107
 
105
- 面向模型的工具默认同步收集:先等待子 agent 结果,再 dispose 运行,然后才返回。一次性后台委派会在工具中注册普通 Task,其通用状态、收集和取消工具负责后续交互,并将模型提供的 `description` 持久化为可选显示标签。可继续后台委派会调用 `ctx.subagents.startContinuable()`,只返回持久化子 agent id;子 agent 自 inbox 接受起就拥有自己的轮次,因此没有 Task、也没有结果 promise——调用方通过 `send_message` 后续操作工具发送后续工作,`interrupt()` 只停止当前轮次而不 dispose 子 agent,而持久化子 agent 会话仍是子 agent 详细输出的来源。只有 `ctx.agents` 可用时,继续执行管理器才会存在,而会话持久化按每项继续执行操作解析。与此独立,`listChildren()` 枚举在线会话存储与可选会话持久化的在线优先合并——持久化缺席时仅枚举在线 child,因为那时冷 child 本就无法恢复——并由已注册的 `subagent` 投影单元供给每个 child 的持久化模式与标签:在线 child 取注册表的水位快照;冷 child 先取可选投影缓存的持久化行,且仅当其 `seq` 门证明该值折叠自 child 自身后缀(fork 种子之后——自有描述符一经追加即不可变)才直接采用,否则经一次有界并发的持久化 inspect 再经注册表折叠,且 inspect 结果必须仍指向枚举时的生命周期(同 id 被重新发布的会话降级为 `corrupt` diagnostic)。缓存读取抛出异常时,不会据此作出分类判断,因为缓存只是派生数据;静默落到该权威重折。分类结果完全以投影折叠为准;列表操作本身不解析描述符。取得身份值即产出 child 行;已定局而折叠未产出身份的候选是 `corrupt` diagnostic,inspect 失败是瞬时的 `unavailable`(下次列表重试),运行中而暂无身份值的候选整行省略(描述符尚未追加的创建窗口)。它不查询继续执行管理器、Agent 注册信息、Activation 或提供方。每个 child 行都会根据合并结果中携带持久化 `origin: 'subagent'` 的 header 派生读取时的 `hasChildren` 提示;它不会读取后代事件日志,展开后仍以描述符支撑的 child 目录为权威依据。UI 等服务消费方可以保留两种模式,并为无标签的一次性 child 选择回退展示;面向模型的 `list_agents` 工具只投影 `continuable` 条目,通过在线 Agent 注册表细化状态,并把仅存于存储的状态映射为可恢复而非终态的 `ready`(`running`/`idle`/`ready`),并在 `descendants` scope 下遍历 `listDescendants()`。列表操作会把调用方的取消信号转发到每次持久化读取,在这些 await 前后检查取消,并将每次检测到的中止报告为 `SubagentError` 错误码 `CANCELLED`;投影注册表未挂载则以 `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE` 响亮失败,会话存储缺失则以 `SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE` 响亮失败。完整约定见[后台 subagent 任务 Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md)、[可继续后台 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md)、[持久化目录 Agent Note](../../../.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md)、[服务合并 Agent Note](../../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md)、[能力 seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)和 `src/types.ts`。
108
+ 面向模型的工具默认同步收集:先等待子 agent 结果,再 dispose 运行,然后才返回。一次性后台委派会在工具中注册普通 Task,其通用状态、收集和取消工具负责后续交互,并将模型提供的 `description` 持久化为可选显示标签。可继续后台委派会调用 `ctx.subagents.startContinuable()`,只返回持久化子 agent id;子 agent 自 inbox 接受起就拥有自己的轮次,因此没有 Task、也没有结果 promise——调用方通过 `send_message` 后续操作工具发送后续工作,`interrupt()` 只停止当前轮次而不 dispose 子 agent,而持久化子 agent 会话仍是子 agent 详细输出的来源。只有 `ctx.agents` 可用时,继续执行管理器才会存在,而会话持久化按每项继续执行操作解析。与此独立,`listChildren()` 枚举在线会话存储与可选会话持久化的在线优先合并——持久化缺席时仅枚举在线 child,因为那时冷 child 本就无法恢复——并由已注册的 `subagent` 投影单元供给每个 child 的持久化模式与标签:在线 child 取注册表的水位快照;冷 child 先取可选投影缓存的持久化行,且仅当其 `seq` 门证明该值折叠自 child 自身后缀(fork 种子之后——自有描述符一经追加即不可变)才直接采用,否则经一次有界并发的持久化 inspect 再经注册表折叠,且 inspect 结果必须仍指向枚举时的生命周期(同 id 被重新发布的会话降级为 `corrupt` diagnostic)。缓存读取抛出异常时,不会据此作出分类判断,因为缓存只是派生数据;静默落到该权威重折。分类结果完全以投影折叠为准;列表操作本身不解析描述符。取得身份值即产出 child 行;已定局而折叠未产出身份的候选是 `corrupt` diagnostic,inspect 失败是瞬时的 `unavailable`(下次列表重试),运行中而暂无身份值的候选整行省略(描述符尚未追加的创建窗口)。它不查询继续执行管理器、Agent 注册信息、Activation 或提供方。每个 child 行都会根据合并结果中携带持久化 `origin: 'subagent'` 的 header 派生读取时的 `hasChildren` 提示;它不会读取后代事件日志,展开后仍以描述符支撑的 child 目录为权威依据。UI 等服务消费方可以保留两种模式,并为无标签的一次性 child 选择回退展示;面向模型的 `list_agents` 工具只投影 `continuable` 条目,通过在线 Agent 注册表细化状态,并把仅存于存储的状态映射为可恢复而非终态的 `ready`(`running`/`idle`/`ready`),并在 `descendants` scope 下遍历 `listDescendants()`。列表操作会把调用方的取消信号转发到每次持久化读取,在这些 await 前后检查取消,并将每次检测到的中止报告为 `SubagentError` 错误码 `CANCELLED`;投影注册表未挂载则以 `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE` 响亮失败,会话存储缺失则以 `SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE` 响亮失败。完整约定见[后台 subagent 任务 Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.zh.md)、[可继续后台 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.zh.md)、[持久化目录 Agent Note](../../../.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.zh.md)、[服务合并 Agent Note](../../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.zh.md)、[能力 seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.zh.md)和 `src/types.ts`。
106
109
 
107
110
  可继续 Activation 会等待 best-effort 的最终会话 flush,但不会把 listener 参与视为持久性确认。一次性运行保留尽力执行的会话检查点,因此已完成的一次性 child 只有在其会话确实进入持久化存储时,才可在 dispose 后继续被发现;如果该检查点缺失,服务不会根据 Task 历史虚构目录条目。
108
111
 
109
112
  ## 模型体验
110
113
 
114
+ <a id="settlement-notice"></a>
115
+
111
116
  ### 结算通知
112
117
 
113
118
  #### 模型看到的内容
@@ -146,7 +151,7 @@ You are a delegated subagent: your permission scope was fixed when you were star
146
151
 
147
152
  - **ACP 子 agent 仍为一次性,且无法通过追踪枚举**:ACP 运行在 parent 会话语料中没有本地 child 会话。ACP 的 `prepareContinuable` 需要在提供方专用描述符数据中持久化远端会话 id,以及逐子 agent 的继续执行能力声明,因为 ACP 的 `loadSession` 支持按子 agent 协商,而不是通过方法是否存在来确定。远程提供方还需要一份独立的 Activation 所有权约定,具备等效的经认证控制和子先于父的完全停稳保证,才能支持可继续子 agent。
148
153
  - **无 host-user 继续执行**:`followup()` 要求确切在线直接父级。只有 `interrupt()` 接受持久化 parent 地址形式的用户授权,因为停止一个轮次是幂等的且不投递任何内容;未来 host 适配器需要具体的经认证交互,才能让该 seam 获得用户投递能力。
149
- - **不对当前轮次进行 steering**:可继续消息和唤醒式 report 会排入后续轮次,均不会重定向正在进行的轮次。
154
+ - **继续执行消息绝不 steering**:parent 到 child 的继续执行消息会排入后续 child 轮次。child 到 parent 的 report 是独立的 next-step 输入,可能延长 parent 已打开的轮次。
150
155
  - **取消收敛期间存在唤醒缺口**:中断信号发出后、活动 driver 进入 idle 前被接受的唤醒型 follow-up 会保持排队,直到另一条唤醒发送到达。Issue #1838 负责 agent-loop 的唤醒锁存;普通会话取消也受此影响。
151
156
  - **驻留仅限进程内**:Activation inbox 与所有权图不会在两个 harness 进程之间协调;对单个持久化存储的并发访问仍然需要持久化邮箱和跨进程租约协议。
152
157
  - **不回放已接受但未记录的消息**:只有写入子 agent 会话日志的消息才能连同提供该消息的来源一起重建。崩溃可能丢失从未写入日志、已被接受的初始提示词或后续消息;此后一条经授权的消息可以冷恢复该子 agent,但丢失的消息不会自动回放。
package/lib/index.js CHANGED
@@ -772,9 +772,10 @@ var SubagentContinuationManager = class {
772
772
  const request = spec.request;
773
773
  const parent = request.parent;
774
774
  this.assertAdmitting(parent);
775
- this.requirePersistence();
775
+ const persistence = this.requirePersistence();
776
776
  assertSubagentMaxDepth(request.maxDepth);
777
- const childId = SessionId(randomUUID());
777
+ const childId = spec.childId ?? SessionId(randomUUID());
778
+ this.assertChildIdAvailable(childId);
778
779
  const childDepth = resolveChildDepth(parent, request.maxDepth);
779
780
  const agentProvider = request.agentOptions?.provider ?? parent.options.provider;
780
781
  const agentModel = request.agentOptions?.model ?? parent.options.model;
@@ -800,6 +801,16 @@ var SubagentContinuationManager = class {
800
801
  return {
801
802
  childId,
802
803
  messageId: await this.locks.run(childId, async () => {
804
+ spec.signal.throwIfAborted();
805
+ this.assertAdmitting(parent);
806
+ this.assertChildIdAvailable(childId);
807
+ if (spec.childId !== void 0) {
808
+ const persisted = await persistence.listSnapshots(spec.signal);
809
+ spec.signal.throwIfAborted();
810
+ this.assertAdmitting(parent);
811
+ this.assertChildIdAvailable(childId);
812
+ if (persisted.some((snapshot) => snapshot.header.id === childId)) throw new SubagentError(`subagent "${childId}" already exists`, "DUPLICATE_CHILD");
813
+ }
803
814
  const activation = await this.materialize({
804
815
  childId,
805
816
  provider: spec.provider,
@@ -820,6 +831,10 @@ var SubagentContinuationManager = class {
820
831
  })
821
832
  };
822
833
  }
834
+ /** Reject one child identity already owned by a live Agent or Session. */
835
+ assertChildIdAvailable(childId) {
836
+ 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");
837
+ }
823
838
  /**
824
839
  * Deliver one later message to a known continuable child as its next FIFO
825
840
  * turn. Routing depends only on Activation residency: a `running` Activation
@@ -941,7 +956,7 @@ var SubagentContinuationManager = class {
941
956
  senderSessionId: activation.childId
942
957
  }
943
958
  });
944
- if (delivery === "wakeup") this.sendWaking(parent, message, () => {
959
+ if (delivery === "next-step") this.sendWaking(parent, message, () => {
945
960
  this.sendReport(parent, message, delivery);
946
961
  });
947
962
  else this.sendReport(parent, message, delivery);
@@ -951,7 +966,7 @@ var SubagentContinuationManager = class {
951
966
  * Perform one waking send to a parent, accounted against that parent's own
952
967
  * Activation when it has one. Registering the id before the send is what
953
968
  * keeps a continuation-managed parent from being judged quiescent in the
954
- * window between `followup()` and the microtask that admits it.
969
+ * window between a waking send and the microtask that admits it.
955
970
  * @param parent - the exact live parent receiving the waking message.
956
971
  * @param message - the message whose id is accounted.
957
972
  * @param send - the synchronous waking send to perform.
@@ -964,7 +979,7 @@ var SubagentContinuationManager = class {
964
979
  /** Send one report while translating only the parent's own rejection. */
965
980
  sendReport(parent, message, delivery) {
966
981
  try {
967
- if (delivery === "wakeup") parent.followup(message);
982
+ if (delivery === "next-step") parent.steer(message);
968
983
  else parent.inject(message);
969
984
  } catch (error) {
970
985
  throw new SubagentError("direct parent is not live; report was not delivered", "PARENT_UNAVAILABLE", { cause: error });
@@ -1027,6 +1042,28 @@ var SubagentContinuationManager = class {
1027
1042
  await Promise.all(materializations.map((materialization) => materialization.settled));
1028
1043
  await this.disposeRoots(targetRoots, "scoped activation(s)");
1029
1044
  }
1045
+ /**
1046
+ * Release selected resident direct children of one exact live parent without
1047
+ * closing admission for the parent's other continuable children. Owned
1048
+ * descendants are released recursively through the same lifecycle.
1049
+ * @param parent - exact live direct parent authorizing the selected release.
1050
+ * @param childIds - durable direct-child ids to release when resident.
1051
+ * @returns once every selected Activation released its handle.
1052
+ * @throws {SubagentError} `UNAUTHORIZED` when a resident target is not the
1053
+ * parent's direct continuable child or the parent identity is stale.
1054
+ */
1055
+ async drainChildren(parent, childIds) {
1056
+ if (this.ctx.agents.get(parent.id) !== parent) throw new SubagentError("selected child teardown requires the exact live parent agent", "UNAUTHORIZED");
1057
+ const targets = [];
1058
+ for (const childId of new Set(childIds)) {
1059
+ const activation = this.activations.get(childId);
1060
+ if (activation === void 0) continue;
1061
+ if (activation.parentSession !== parent.id || !activation.ancestry.has(parent)) throw new SubagentError(`subagent "${childId}" is not a direct child of agent "${parent.id}"`, "UNAUTHORIZED");
1062
+ targets.push(activation);
1063
+ }
1064
+ for (const activation of targets) this.dispose(activation).catch(() => void 0);
1065
+ await this.disposeRoots(targets, "selected activation(s)");
1066
+ }
1030
1067
  /** Dispose independent roots and report every branch failure after all settle. */
1031
1068
  async disposeRoots(roots, failureSubject) {
1032
1069
  const reasons = (await Promise.all(roots.map(async (activation) => {
@@ -1911,6 +1948,25 @@ function sameLifecycle(meta, expected) {
1911
1948
  function assertListingNotCancelled(signal) {
1912
1949
  if (signal?.aborted) throw new SubagentError("subagent listing was cancelled", "CANCELLED");
1913
1950
  }
1951
+ //#endregion
1952
+ //#region lib/types/projection.js
1953
+ /**
1954
+ * Pure session projections for subagent identity (mode/label) and active-turn
1955
+ * duration.
1956
+ *
1957
+ * @module @deepseek-ai/dsh-subagent/projection
1958
+ */
1959
+ const activeIntervalSchema = z.object({
1960
+ since: z.number().int().nonnegative(),
1961
+ through: z.number().int().nonnegative()
1962
+ }).strict();
1963
+ const projectionSchema = z.object({
1964
+ settledMs: z.number().int().nonnegative(),
1965
+ active: activeIntervalSchema.optional()
1966
+ }).strict().transform(({ settledMs, active }) => ({
1967
+ settledMs,
1968
+ ...active === void 0 ? {} : { active }
1969
+ }));
1914
1970
  /**
1915
1971
  * Fold turn boundaries around the child's own durable descriptor.
1916
1972
  *
@@ -1921,12 +1977,11 @@ function assertListingNotCancelled(signal) {
1921
1977
  */
1922
1978
  const subagentTimingProjectionDefinition = {
1923
1979
  key: "subagentTiming",
1924
- schema: z.object({
1980
+ stateSchema: z.object({
1925
1981
  settledMs: z.number().int().nonnegative(),
1926
- active: z.object({
1927
- since: z.number().int().nonnegative(),
1928
- through: z.number().int().nonnegative()
1929
- }).strict().optional()
1982
+ active: activeIntervalSchema.optional(),
1983
+ pendingTurnStart: z.number().int().nonnegative().optional(),
1984
+ descriptorSeen: z.boolean()
1930
1985
  }).strict(),
1931
1986
  init: () => ({
1932
1987
  descriptorSeen: false,
@@ -1976,13 +2031,16 @@ const subagentTimingProjectionDefinition = {
1976
2031
  }
1977
2032
  };
1978
2033
  },
1979
- view: (state) => ({
1980
- settledMs: state.settledMs,
1981
- ...state.active === void 0 ? {} : { active: state.active }
1982
- }),
2034
+ wire: {
2035
+ viewSchema: projectionSchema,
2036
+ view: (state) => ({
2037
+ settledMs: state.settledMs,
2038
+ ...state.active === void 0 ? {} : { active: state.active }
2039
+ })
2040
+ },
1983
2041
  stateVersion: 2
1984
2042
  };
1985
- const identitySchema = z.discriminatedUnion("mode", [z.object({
2043
+ const identityValueSchema = z.discriminatedUnion("mode", [z.object({
1986
2044
  mode: z.literal("one-shot"),
1987
2045
  label: z.string().optional(),
1988
2046
  seq: z.number().int().nonnegative()
@@ -1990,7 +2048,9 @@ const identitySchema = z.discriminatedUnion("mode", [z.object({
1990
2048
  mode: z.literal("continuable"),
1991
2049
  label: z.string(),
1992
2050
  seq: z.number().int().nonnegative()
1993
- }).strict()]).nullable();
2051
+ }).strict()]);
2052
+ const identitySchema = identityValueSchema.nullable();
2053
+ const identityStateSchema = z.object({ identity: identityValueSchema.optional() }).strict();
1994
2054
  /** Interpret one `subagent/descriptor` event's identity; no value when the payload cannot be trusted. */
1995
2055
  function descriptorIdentity(event) {
1996
2056
  let descriptor;
@@ -2023,14 +2083,17 @@ function descriptorIdentity(event) {
2023
2083
  */
2024
2084
  const subagentIdentityProjectionDefinition = {
2025
2085
  key: "subagent",
2026
- schema: identitySchema,
2086
+ stateSchema: identityStateSchema,
2027
2087
  init: () => ({}),
2028
2088
  apply: (state, event) => {
2029
2089
  if (event.type !== "subagent/descriptor") return state;
2030
2090
  const identity = descriptorIdentity(event);
2031
2091
  return identity === void 0 ? {} : { identity };
2032
2092
  },
2033
- view: (state) => state.identity ?? null,
2093
+ wire: {
2094
+ viewSchema: identitySchema,
2095
+ view: (state) => state.identity ?? null
2096
+ },
2034
2097
  stateVersion: 2
2035
2098
  };
2036
2099
  //#endregion
@@ -2047,6 +2110,23 @@ const subagentIdentityProjectionDefinition = {
2047
2110
  *
2048
2111
  * @module @deepseek-ai/dsh-subagent/out-of-process
2049
2112
  */
2113
+ /** Maximum UTF-8 size of {@link SubagentResult.diagnostic}. */
2114
+ const MAX_SUBAGENT_DIAGNOSTIC_BYTES = 4096;
2115
+ const DIAGNOSTIC_TRUNCATION_SUFFIX = "\n[diagnostic truncated]";
2116
+ const utf8Encoder = new TextEncoder();
2117
+ const utf8Decoder = new TextDecoder();
2118
+ /**
2119
+ * Limit provider-authored failure detail without splitting a UTF-8 sequence.
2120
+ * @param diagnostic - safe diagnostic text produced by the provider.
2121
+ * @returns the original text, or a visibly truncated value within the limit.
2122
+ */
2123
+ function limitSubagentDiagnostic(diagnostic) {
2124
+ const bytes = utf8Encoder.encode(diagnostic);
2125
+ if (bytes.byteLength <= MAX_SUBAGENT_DIAGNOSTIC_BYTES) return diagnostic;
2126
+ let prefixBytes = MAX_SUBAGENT_DIAGNOSTIC_BYTES - utf8Encoder.encode(DIAGNOSTIC_TRUNCATION_SUFFIX).byteLength;
2127
+ while ((bytes[prefixBytes] & 192) === 128) prefixBytes -= 1;
2128
+ return utf8Decoder.decode(bytes.subarray(0, prefixBytes)) + DIAGNOSTIC_TRUNCATION_SUFFIX;
2129
+ }
2050
2130
  /**
2051
2131
  * The capability advertisement of an out-of-process backend: NONE. A child in
2052
2132
  * another process cannot honor parent-enforced start features
@@ -2160,8 +2240,11 @@ async function settleRunResult(parts) {
2160
2240
  try {
2161
2241
  parts.onError?.(toError(error), "error");
2162
2242
  } catch {}
2243
+ const collected = parts.collectDiagnostic?.();
2244
+ const diagnostic = collected === void 0 ? void 0 : limitSubagentDiagnostic(collected);
2163
2245
  return {
2164
2246
  output: parts.collectOutput(),
2247
+ ...diagnostic === void 0 ? {} : { diagnostic },
2165
2248
  stopReason: "error"
2166
2249
  };
2167
2250
  } finally {
@@ -2204,6 +2287,11 @@ function subprocessRunHandle(parts) {
2204
2287
  function finalText(blocks) {
2205
2288
  return blocks.filter((block) => block.type === "text").map((block) => block.text).join("");
2206
2289
  }
2290
+ /** Render a failed stop reason with optional provider-authored detail. */
2291
+ function failureDetail(result) {
2292
+ const stopReason = result.stopReason;
2293
+ return result.diagnostic === void 0 ? stopReason : `${stopReason}; diagnostic: ${result.diagnostic}`;
2294
+ }
2207
2295
  /**
2208
2296
  * Map a child result to the task outcome: completed carries final text,
2209
2297
  * aborted is killed, and every other reason is failed without partial output.
@@ -2221,11 +2309,11 @@ function runOutcome(result) {
2221
2309
  case "max-tokens":
2222
2310
  case "refusal": return {
2223
2311
  status: "failed",
2224
- detail: result.stopReason
2312
+ detail: failureDetail(result)
2225
2313
  };
2226
2314
  default: return {
2227
2315
  status: "failed",
2228
- detail: String(result.stopReason)
2316
+ detail: failureDetail(result)
2229
2317
  };
2230
2318
  }
2231
2319
  }
@@ -2409,6 +2497,21 @@ var SubagentRuntime = class extends Service {
2409
2497
  await manager.drainDescendants(parents);
2410
2498
  }
2411
2499
  /**
2500
+ * Release selected resident continuable direct children of one exact live
2501
+ * parent. Other children of the same parent remain admitted and resident.
2502
+ * Absent targets and a manager-less composition are accepted no-ops.
2503
+ * @param parent - exact live direct parent authorizing the selected release.
2504
+ * @param childIds - durable direct-child ids to release when resident.
2505
+ * @returns once every selected Activation released its `AgentHandle`.
2506
+ * @throws {SubagentError} `UNAUTHORIZED` when a resident target belongs to a
2507
+ * different parent or the supplied parent identity is stale.
2508
+ */
2509
+ async drainContinuableChildren(parent, childIds) {
2510
+ const manager = this.continuations;
2511
+ if (manager === void 0) return;
2512
+ await manager.drainChildren(parent, childIds);
2513
+ }
2514
+ /**
2412
2515
  * Enumerate the parent's direct session-backed subagents without loading or
2413
2516
  * resuming an Agent and without any query service: the listing merges the live
2414
2517
  * session store with optional session persistence (live-preferred) and
@@ -68,7 +68,7 @@ declare module '@deepseek-ai/dsh-llm' {
68
68
  }
69
69
  }
70
70
  /** Deployment scheduling policy for accepted child reports. */
71
- export type SubagentReportDelivery = 'quiet' | 'wakeup';
71
+ export type SubagentReportDelivery = 'quiet' | 'next-step';
72
72
  /** Options for one continuable child's report to its direct parent. */
73
73
  export interface SubagentReportOptions {
74
74
  /** Already-resolved parent scheduling policy. */
@@ -82,6 +82,12 @@ export interface ContinuableStartSpec {
82
82
  readonly provider: string;
83
83
  /** The initial delegation's short `description`, persisted as the child's creation label. */
84
84
  readonly label: string;
85
+ /**
86
+ * Optional caller-reserved child identity. Omission preserves the manager's
87
+ * UUID allocation; supplying one lets a durable parent record provisioning
88
+ * before child materialization without a second identity handshake.
89
+ */
90
+ readonly childId?: SessionId;
85
91
  /**
86
92
  * The delegation request. The manager reserves the stable child id, resolves
87
93
  * the durable descriptor, and composes the child itself.
@@ -182,6 +188,8 @@ export declare class SubagentContinuationManager {
182
188
  * @returns the durable child id and the accepted initial prompt's message id.
183
189
  */
184
190
  startContinuable(spec: ContinuableStartSpec): Promise<ContinuableStart>;
191
+ /** Reject one child identity already owned by a live Agent or Session. */
192
+ private assertChildIdAvailable;
185
193
  /**
186
194
  * Deliver one later message to a known continuable child as its next FIFO
187
195
  * turn. Routing depends only on Activation residency: a `running` Activation
@@ -245,7 +253,7 @@ export declare class SubagentContinuationManager {
245
253
  * Perform one waking send to a parent, accounted against that parent's own
246
254
  * Activation when it has one. Registering the id before the send is what
247
255
  * keeps a continuation-managed parent from being judged quiescent in the
248
- * window between `followup()` and the microtask that admits it.
256
+ * window between a waking send and the microtask that admits it.
249
257
  * @param parent - the exact live parent receiving the waking message.
250
258
  * @param message - the message whose id is accounted.
251
259
  * @param send - the synchronous waking send to perform.
@@ -273,6 +281,17 @@ export declare class SubagentContinuationManager {
273
281
  * @throws an aggregate error after all scoped branches settle when any failed.
274
282
  */
275
283
  drainDescendants(parents: readonly Agent[]): Promise<void>;
284
+ /**
285
+ * Release selected resident direct children of one exact live parent without
286
+ * closing admission for the parent's other continuable children. Owned
287
+ * descendants are released recursively through the same lifecycle.
288
+ * @param parent - exact live direct parent authorizing the selected release.
289
+ * @param childIds - durable direct-child ids to release when resident.
290
+ * @returns once every selected Activation released its handle.
291
+ * @throws {SubagentError} `UNAUTHORIZED` when a resident target is not the
292
+ * parent's direct continuable child or the parent identity is stale.
293
+ */
294
+ drainChildren(parent: Agent, childIds: readonly SessionId[]): Promise<void>;
276
295
  /** Dispose independent roots and report every branch failure after all settle. */
277
296
  private disposeRoots;
278
297
  /** Return the retained member set for one exact scoped-teardown root. */
@@ -154,9 +154,10 @@ export class SubagentContinuationManager {
154
154
  const request = spec.request;
155
155
  const parent = request.parent;
156
156
  this.assertAdmitting(parent);
157
- this.requirePersistence();
157
+ const persistence = this.requirePersistence();
158
158
  assertSubagentMaxDepth(request.maxDepth);
159
- const childId = SessionId(randomUUID());
159
+ const childId = spec.childId ?? SessionId(randomUUID());
160
+ this.assertChildIdAvailable(childId);
160
161
  const childDepth = resolveChildDepth(parent, request.maxDepth);
161
162
  // Snapshot before any await: invalid descriptor JSON rejects the call
162
163
  // before a child exists, and the detached value is what reaches the log.
@@ -184,6 +185,18 @@ export class SubagentContinuationManager {
184
185
  const lineageSeedLength = prepared.seed?.length ?? 0;
185
186
  const seed = seedDescriptorTurn(childId, prepared.seed, descriptor);
186
187
  const messageId = await this.locks.run(childId, async () => {
188
+ spec.signal.throwIfAborted();
189
+ this.assertAdmitting(parent);
190
+ this.assertChildIdAvailable(childId);
191
+ if (spec.childId !== undefined) {
192
+ const persisted = await persistence.listSnapshots(spec.signal);
193
+ spec.signal.throwIfAborted();
194
+ this.assertAdmitting(parent);
195
+ this.assertChildIdAvailable(childId);
196
+ if (persisted.some(snapshot => snapshot.header.id === childId)) {
197
+ throw new SubagentError(`subagent "${childId}" already exists`, 'DUPLICATE_CHILD');
198
+ }
199
+ }
187
200
  const activation = await this.materialize({
188
201
  childId,
189
202
  provider: spec.provider,
@@ -197,6 +210,12 @@ export class SubagentContinuationManager {
197
210
  });
198
211
  return { childId, messageId };
199
212
  }
213
+ /** Reject one child identity already owned by a live Agent or Session. */
214
+ assertChildIdAvailable(childId) {
215
+ if (this.ctx.agents.get(childId) !== undefined || this.ctx.get('sessions')?.get(childId) !== undefined) {
216
+ throw new SubagentError(`subagent "${childId}" already exists`, 'DUPLICATE_CHILD');
217
+ }
218
+ }
200
219
  /**
201
220
  * Deliver one later message to a known continuable child as its next FIFO
202
221
  * turn. Routing depends only on Activation residency: a `running` Activation
@@ -347,7 +366,7 @@ export class SubagentContinuationManager {
347
366
  senderSessionId: activation.childId,
348
367
  },
349
368
  });
350
- if (delivery === 'wakeup') {
369
+ if (delivery === 'next-step') {
351
370
  this.sendWaking(parent, message, () => { this.sendReport(parent, message, delivery); });
352
371
  }
353
372
  else {
@@ -359,7 +378,7 @@ export class SubagentContinuationManager {
359
378
  * Perform one waking send to a parent, accounted against that parent's own
360
379
  * Activation when it has one. Registering the id before the send is what
361
380
  * keeps a continuation-managed parent from being judged quiescent in the
362
- * window between `followup()` and the microtask that admits it.
381
+ * window between a waking send and the microtask that admits it.
363
382
  * @param parent - the exact live parent receiving the waking message.
364
383
  * @param message - the message whose id is accounted.
365
384
  * @param send - the synchronous waking send to perform.
@@ -376,8 +395,8 @@ export class SubagentContinuationManager {
376
395
  /** Send one report while translating only the parent's own rejection. */
377
396
  sendReport(parent, message, delivery) {
378
397
  try {
379
- if (delivery === 'wakeup')
380
- parent.followup(message);
398
+ if (delivery === 'next-step')
399
+ parent.steer(message);
381
400
  else
382
401
  parent.inject(message);
383
402
  }
@@ -471,6 +490,38 @@ export class SubagentContinuationManager {
471
490
  await Promise.all(materializations.map(materialization => materialization.settled));
472
491
  await this.disposeRoots(targetRoots, 'scoped activation(s)');
473
492
  }
493
+ /**
494
+ * Release selected resident direct children of one exact live parent without
495
+ * closing admission for the parent's other continuable children. Owned
496
+ * descendants are released recursively through the same lifecycle.
497
+ * @param parent - exact live direct parent authorizing the selected release.
498
+ * @param childIds - durable direct-child ids to release when resident.
499
+ * @returns once every selected Activation released its handle.
500
+ * @throws {SubagentError} `UNAUTHORIZED` when a resident target is not the
501
+ * parent's direct continuable child or the parent identity is stale.
502
+ */
503
+ async drainChildren(parent, childIds) {
504
+ if (this.ctx.agents.get(parent.id) !== parent) {
505
+ throw new SubagentError('selected child teardown requires the exact live parent agent', 'UNAUTHORIZED');
506
+ }
507
+ const targets = [];
508
+ for (const childId of new Set(childIds)) {
509
+ const activation = this.activations.get(childId);
510
+ if (activation === undefined)
511
+ continue;
512
+ if (activation.parentSession !== parent.id || !activation.ancestry.has(parent)) {
513
+ throw new SubagentError(`subagent "${childId}" is not a direct child of agent "${parent.id}"`, 'UNAUTHORIZED');
514
+ }
515
+ targets.push(activation);
516
+ }
517
+ // Open every transaction before the first await so cancellation propagates
518
+ // across the selected roots in one synchronous span.
519
+ for (const activation of targets) {
520
+ const disposal = this.dispose(activation);
521
+ void disposal.catch(() => undefined);
522
+ }
523
+ await this.disposeRoots(targets, 'selected activation(s)');
524
+ }
474
525
  /** Dispose independent roots and report every branch failure after all settle. */
475
526
  async disposeRoots(roots, failureSubject) {
476
527
  const failures = await Promise.all(roots.map(async (activation) => {
@@ -814,7 +865,7 @@ export class SubagentContinuationManager {
814
865
  * @returns the accepted message id.
815
866
  */
816
867
  admitWaking(activation, messageId, send) {
817
- // `Agent.followup()` publishes inbox events synchronously, so observers must
868
+ // Waking Agent sends publish inbox events synchronously, so observers must
818
869
  // see this Activation as busy before the call begins.
819
870
  activation.accepted.add(messageId);
820
871
  try {
@@ -182,6 +182,17 @@ export declare class SubagentRuntime extends Service {
182
182
  * @throws an aggregate error after all branches settle when any failed.
183
183
  */
184
184
  drainContinuableDescendants(parents: readonly Agent[]): Promise<void>;
185
+ /**
186
+ * Release selected resident continuable direct children of one exact live
187
+ * parent. Other children of the same parent remain admitted and resident.
188
+ * Absent targets and a manager-less composition are accepted no-ops.
189
+ * @param parent - exact live direct parent authorizing the selected release.
190
+ * @param childIds - durable direct-child ids to release when resident.
191
+ * @returns once every selected Activation released its `AgentHandle`.
192
+ * @throws {SubagentError} `UNAUTHORIZED` when a resident target belongs to a
193
+ * different parent or the supplied parent identity is stale.
194
+ */
195
+ drainContinuableChildren(parent: Agent, childIds: readonly SessionId[]): Promise<void>;
185
196
  /**
186
197
  * Enumerate the parent's direct session-backed subagents without loading or
187
198
  * resuming an Agent and without any query service: the listing merges the live
@@ -173,6 +173,22 @@ export class SubagentRuntime extends Service {
173
173
  return;
174
174
  await manager.drainDescendants(parents);
175
175
  }
176
+ /**
177
+ * Release selected resident continuable direct children of one exact live
178
+ * parent. Other children of the same parent remain admitted and resident.
179
+ * Absent targets and a manager-less composition are accepted no-ops.
180
+ * @param parent - exact live direct parent authorizing the selected release.
181
+ * @param childIds - durable direct-child ids to release when resident.
182
+ * @returns once every selected Activation released its `AgentHandle`.
183
+ * @throws {SubagentError} `UNAUTHORIZED` when a resident target belongs to a
184
+ * different parent or the supplied parent identity is stale.
185
+ */
186
+ async drainContinuableChildren(parent, childIds) {
187
+ const manager = this.continuations;
188
+ if (manager === undefined)
189
+ return;
190
+ await manager.drainChildren(parent, childIds);
191
+ }
176
192
  /**
177
193
  * Enumerate the parent's direct session-backed subagents without loading or
178
194
  * resuming an Agent and without any query service: the listing merges the live
@@ -69,6 +69,8 @@ export interface RunResultSettlement {
69
69
  attempt: () => Promise<SubagentResult>;
70
70
  /** Snapshot the provider exposes when cancellation or failure wins settlement. */
71
71
  collectOutput: () => ContentBlock[];
72
+ /** Snapshot safe provider-authored detail when a failure wins settlement. */
73
+ collectDiagnostic?: (() => string | undefined) | undefined;
72
74
  /** Whether local cancellation settled before the attempt's outcome is observed. */
73
75
  cancelled: () => boolean;
74
76
  /** Diagnostic sink for a failure flattened to a stop reason; a throw from it is contained. */
@@ -12,6 +12,28 @@
12
12
  */
13
13
  import { accessSync, constants, statSync } from 'node:fs';
14
14
  import { isAbsolute, resolve } from 'node:path';
15
+ /** Maximum UTF-8 size of {@link SubagentResult.diagnostic}. */
16
+ const MAX_SUBAGENT_DIAGNOSTIC_BYTES = 4_096;
17
+ const DIAGNOSTIC_TRUNCATION_SUFFIX = '\n[diagnostic truncated]';
18
+ const utf8Encoder = new TextEncoder();
19
+ const utf8Decoder = new TextDecoder();
20
+ /**
21
+ * Limit provider-authored failure detail without splitting a UTF-8 sequence.
22
+ * @param diagnostic - safe diagnostic text produced by the provider.
23
+ * @returns the original text, or a visibly truncated value within the limit.
24
+ */
25
+ function limitSubagentDiagnostic(diagnostic) {
26
+ const bytes = utf8Encoder.encode(diagnostic);
27
+ if (bytes.byteLength <= MAX_SUBAGENT_DIAGNOSTIC_BYTES)
28
+ return diagnostic;
29
+ const suffixBytes = utf8Encoder.encode(DIAGNOSTIC_TRUNCATION_SUFFIX).byteLength;
30
+ let prefixBytes = MAX_SUBAGENT_DIAGNOSTIC_BYTES - suffixBytes;
31
+ while ((bytes[prefixBytes] & 0b1100_0000) === 0b1000_0000) {
32
+ prefixBytes -= 1;
33
+ }
34
+ return utf8Decoder.decode(bytes.subarray(0, prefixBytes))
35
+ + DIAGNOSTIC_TRUNCATION_SUFFIX;
36
+ }
15
37
  /**
16
38
  * The capability advertisement of an out-of-process backend: NONE. A child in
17
39
  * another process cannot honor parent-enforced start features
@@ -148,7 +170,15 @@ export async function settleRunResult(parts) {
148
170
  catch {
149
171
  // The diagnostic sink cannot reject the run result.
150
172
  }
151
- return { output: parts.collectOutput(), stopReason: 'error' };
173
+ const collected = parts.collectDiagnostic?.();
174
+ const diagnostic = collected === undefined
175
+ ? undefined
176
+ : limitSubagentDiagnostic(collected);
177
+ return {
178
+ output: parts.collectOutput(),
179
+ ...diagnostic === undefined ? {} : { diagnostic },
180
+ stopReason: 'error',
181
+ };
152
182
  }
153
183
  finally {
154
184
  parts.signal.removeEventListener('abort', parts.onAbort);
@@ -4,21 +4,29 @@
4
4
  *
5
5
  * @module @deepseek-ai/dsh-subagent/projection
6
6
  */
7
- import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection';
8
- import type { SubagentIdentityProjection } from './projection-types.ts';
9
- interface TimingState {
7
+ import { z } from 'zod';
8
+ import type { SessionEvent } from '@deepseek-ai/dsh-session';
9
+ import type { SubagentIdentityProjection, SubagentTimingProjection } from './projection-types.ts';
10
+ /** Fold state for a subagent's latest timing snapshot. */
11
+ export interface TimingState {
10
12
  /** Milliseconds accumulated across completed post-descriptor turns. */
11
13
  settledMs: number;
12
14
  /** Current open interval kept paired inside the fold. */
13
15
  active?: {
14
16
  since: number;
15
17
  through: number;
16
- };
18
+ } | undefined;
17
19
  /** Latest pre-descriptor turn start, promoted when the child's own descriptor arrives. */
18
- pendingTurnStart?: number;
20
+ pendingTurnStart?: number | undefined;
19
21
  /** Whether the fold has crossed a descriptor in this logical log. */
20
22
  descriptorSeen: boolean;
21
23
  }
24
+ declare module '@deepseek-ai/dsh-session-projection/types' {
25
+ interface SessionProjectionStateMap {
26
+ subagentTiming: TimingState;
27
+ subagent: IdentityState;
28
+ }
29
+ }
22
30
  /**
23
31
  * Fold turn boundaries around the child's own durable descriptor.
24
32
  *
@@ -27,10 +35,39 @@ interface TimingState {
27
35
  * admits only a child with exactly one descriptor in its own suffix, making
28
36
  * the final reset the child's authoritative timing origin.
29
37
  */
30
- export declare const subagentTimingProjectionDefinition: ProjectionDefinition<'subagentTiming', TimingState>;
38
+ export declare const subagentTimingProjectionDefinition: {
39
+ key: "subagentTiming";
40
+ stateSchema: z.ZodType<TimingState, unknown, z.core.$ZodTypeInternals<TimingState, unknown>>;
41
+ init: () => {
42
+ descriptorSeen: false;
43
+ settledMs: number;
44
+ };
45
+ apply: (state: NoInfer<TimingState>, event: SessionEvent) => {
46
+ /** Milliseconds accumulated across completed post-descriptor turns. */
47
+ settledMs: number;
48
+ /** Current open interval kept paired inside the fold. */
49
+ active?: {
50
+ since: number;
51
+ through: number;
52
+ } | undefined;
53
+ /** Whether the fold has crossed a descriptor in this logical log. */
54
+ descriptorSeen: boolean;
55
+ };
56
+ wire: {
57
+ viewSchema: z.ZodType<SubagentTimingProjection, unknown, z.core.$ZodTypeInternals<SubagentTimingProjection, unknown>>;
58
+ view: (state: NoInfer<TimingState>) => {
59
+ active?: {
60
+ since: number;
61
+ through: number;
62
+ };
63
+ settledMs: number;
64
+ };
65
+ };
66
+ stateVersion: number;
67
+ };
31
68
  interface IdentityState {
32
69
  /** Identity from the last valid descriptor; absent before one, and after an invalid one. */
33
- identity?: SubagentIdentityProjection;
70
+ identity?: SubagentIdentityProjection | undefined;
34
71
  }
35
72
  /**
36
73
  * Fold the durable mode/label identity from `subagent/descriptor` events,
@@ -43,6 +80,16 @@ interface IdentityState {
43
80
  * holding the earlier identity replaces it instead of keeping it stale;
44
81
  * `null` ⟺ no valid descriptor, with the causes deliberately undistinguished.
45
82
  */
46
- export declare const subagentIdentityProjectionDefinition: ProjectionDefinition<'subagent', IdentityState>;
83
+ export declare const subagentIdentityProjectionDefinition: {
84
+ key: "subagent";
85
+ stateSchema: z.ZodType<IdentityState, unknown, z.core.$ZodTypeInternals<IdentityState, unknown>>;
86
+ init: () => {};
87
+ apply: (state: NoInfer<IdentityState>, event: SessionEvent) => IdentityState;
88
+ wire: {
89
+ viewSchema: z.ZodNullable<z.ZodType<SubagentIdentityProjection, unknown, z.core.$ZodTypeInternals<SubagentIdentityProjection, unknown>>>;
90
+ view: (state: NoInfer<IdentityState>) => SubagentIdentityProjection | null;
91
+ };
92
+ stateVersion: number;
93
+ };
47
94
  export {};
48
95
  //# sourceMappingURL=projection.d.ts.map
@@ -6,14 +6,22 @@
6
6
  */
7
7
  import { z } from 'zod';
8
8
  import { foldSubagentDescriptor } from "./descriptor.js";
9
- // Zod's optional output includes explicit `undefined`; with
10
- // exactOptionalPropertyTypes the public interface permits omission only.
9
+ const activeIntervalSchema = z.object({
10
+ since: z.number().int().nonnegative(),
11
+ through: z.number().int().nonnegative(),
12
+ }).strict();
11
13
  const projectionSchema = z.object({
12
14
  settledMs: z.number().int().nonnegative(),
13
- active: z.object({
14
- since: z.number().int().nonnegative(),
15
- through: z.number().int().nonnegative(),
16
- }).strict().optional(),
15
+ active: activeIntervalSchema.optional(),
16
+ }).strict().transform(({ settledMs, active }) => ({
17
+ settledMs,
18
+ ...active === undefined ? {} : { active },
19
+ }));
20
+ const timingStateSchema = z.object({
21
+ settledMs: z.number().int().nonnegative(),
22
+ active: activeIntervalSchema.optional(),
23
+ pendingTurnStart: z.number().int().nonnegative().optional(),
24
+ descriptorSeen: z.boolean(),
17
25
  }).strict();
18
26
  /**
19
27
  * Fold turn boundaries around the child's own durable descriptor.
@@ -25,7 +33,7 @@ const projectionSchema = z.object({
25
33
  */
26
34
  export const subagentTimingProjectionDefinition = {
27
35
  key: 'subagentTiming',
28
- schema: projectionSchema,
36
+ stateSchema: timingStateSchema,
29
37
  init: () => ({ descriptorSeen: false, settledMs: 0 }),
30
38
  apply: (state, event) => {
31
39
  if (event.type === 'turn/start') {
@@ -62,10 +70,13 @@ export const subagentTimingProjectionDefinition = {
62
70
  return state;
63
71
  return { ...state, active: { ...state.active, through: event.time } };
64
72
  },
65
- view: state => ({
66
- settledMs: state.settledMs,
67
- ...(state.active === undefined ? {} : { active: state.active }),
68
- }),
73
+ wire: {
74
+ viewSchema: projectionSchema,
75
+ view: state => ({
76
+ settledMs: state.settledMs,
77
+ ...(state.active === undefined ? {} : { active: state.active }),
78
+ }),
79
+ },
69
80
  stateVersion: 2,
70
81
  };
71
82
  // The cast bridges only the optional-label arm: Zod's optional output
@@ -73,7 +84,7 @@ export const subagentTimingProjectionDefinition = {
73
84
  // from the public interface. The no-value state itself is the serializable
74
85
  // `null` arm — never `undefined` — so every registry read and push frame
75
86
  // survives JSON.stringify losslessly.
76
- const identitySchema = z.discriminatedUnion('mode', [
87
+ const identityValueSchema = z.discriminatedUnion('mode', [
77
88
  z.object({
78
89
  mode: z.literal('one-shot'),
79
90
  label: z.string().optional(),
@@ -84,7 +95,11 @@ const identitySchema = z.discriminatedUnion('mode', [
84
95
  label: z.string(),
85
96
  seq: z.number().int().nonnegative(),
86
97
  }).strict(),
87
- ]).nullable();
98
+ ]);
99
+ const identitySchema = identityValueSchema.nullable();
100
+ const identityStateSchema = z.object({
101
+ identity: identityValueSchema.optional(),
102
+ }).strict();
88
103
  /** Interpret one `subagent/descriptor` event's identity; no value when the payload cannot be trusted. */
89
104
  function descriptorIdentity(event) {
90
105
  let descriptor;
@@ -119,7 +134,7 @@ function descriptorIdentity(event) {
119
134
  */
120
135
  export const subagentIdentityProjectionDefinition = {
121
136
  key: 'subagent',
122
- schema: identitySchema,
137
+ stateSchema: identityStateSchema,
123
138
  init: () => ({}),
124
139
  apply: (state, event) => {
125
140
  if (event.type !== 'subagent/descriptor')
@@ -127,7 +142,7 @@ export const subagentIdentityProjectionDefinition = {
127
142
  const identity = descriptorIdentity(event);
128
143
  return identity === undefined ? {} : { identity };
129
144
  },
130
- view: state => state.identity ?? null,
145
+ wire: { viewSchema: identitySchema, view: state => state.identity ?? null },
131
146
  // Bumped when the identity gained its `seq` field: an older checkpoint row
132
147
  // would replay into a value the schema rejects, so it must refold instead.
133
148
  stateVersion: 2,
@@ -12,6 +12,13 @@ function finalText(blocks) {
12
12
  .map(block => block.text)
13
13
  .join('');
14
14
  }
15
+ /** Render a failed stop reason with optional provider-authored detail. */
16
+ function failureDetail(result) {
17
+ const stopReason = result.stopReason;
18
+ return result.diagnostic === undefined
19
+ ? stopReason
20
+ : `${stopReason}; diagnostic: ${result.diagnostic}`;
21
+ }
15
22
  /**
16
23
  * Map a child result to the task outcome: completed carries final text,
17
24
  * aborted is killed, and every other reason is failed without partial output.
@@ -27,10 +34,10 @@ function runOutcome(result) {
27
34
  case 'error':
28
35
  case 'max-tokens':
29
36
  case 'refusal':
30
- return { status: 'failed', detail: result.stopReason };
31
- // Merge-extensible reasons remain failures with their raw detail.
37
+ return { status: 'failed', detail: failureDetail(result) };
38
+ // Merge-extensible reasons remain failures with provider-authored detail.
32
39
  default:
33
- return { status: 'failed', detail: String(result.stopReason) };
40
+ return { status: 'failed', detail: failureDetail(result) };
34
41
  }
35
42
  }
36
43
  /**
@@ -218,6 +218,13 @@ export interface SubagentResult {
218
218
  * schema-agnostic.
219
219
  */
220
220
  readonly structured?: unknown;
221
+ /**
222
+ * Provider-authored, non-assistant failure detail for a non-`completed`
223
+ * result. Providers keep this text free of tool inputs, file contents,
224
+ * environment values, credentials, and raw protocol payloads, and limit it
225
+ * to 4096 UTF-8 bytes. Consumers present it separately from {@link output}.
226
+ */
227
+ readonly diagnostic?: string;
221
228
  /** Why the run ended. A non-`completed` reason means `output` may be partial. */
222
229
  readonly stopReason: SubagentStopReason;
223
230
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-subagent",
3
3
  "description": "Abstract subagent seam (ctx.subagents): named-provider registry for delegating to child agents",
4
- "version": "0.1.0-rc.7",
4
+ "version": "0.1.1-rc.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -40,22 +40,22 @@
40
40
  "zod": "^4.4.3"
41
41
  },
42
42
  "peerDependencies": {
43
- "@deepseek-ai/dsh-agent": "^0.1.0-rc.7",
44
- "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.7",
45
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.7",
46
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.7",
47
- "@deepseek-ai/dsh-sandbox": "^0.1.0-rc.7",
48
- "@deepseek-ai/dsh-sandbox-policy": "^0.1.0-rc.7",
49
- "@deepseek-ai/dsh-scope": "^0.1.0-rc.7",
50
- "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.7",
51
- "@deepseek-ai/dsh-session": "^0.1.0-rc.7",
52
- "@deepseek-ai/dsh-session-projection-cache": "^0.1.0-rc.7",
53
- "@deepseek-ai/dsh-session-projection": "^0.1.0-rc.7",
54
- "@deepseek-ai/dsh-jobs": "^0.1.0-rc.7",
55
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.7",
56
- "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.7",
57
- "@deepseek-ai/dsh-brand": "^0.1.0-rc.7",
58
- "@deepseek-ai/cordis": "^4.0.1"
43
+ "@deepseek-ai/dsh-agent-presets": "^0.1.1-rc.1",
44
+ "@deepseek-ai/dsh-agent": "^0.1.1-rc.1",
45
+ "@deepseek-ai/dsh-brand": "^0.1.1-rc.1",
46
+ "@deepseek-ai/dsh-invariants": "^0.1.1-rc.1",
47
+ "@deepseek-ai/dsh-llm": "^0.1.1-rc.1",
48
+ "@deepseek-ai/dsh-sandbox": "^0.1.1-rc.1",
49
+ "@deepseek-ai/dsh-sandbox-policy": "^0.1.1-rc.1",
50
+ "@deepseek-ai/dsh-scope": "^0.1.1-rc.1",
51
+ "@deepseek-ai/dsh-session": "^0.1.1-rc.1",
52
+ "@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.1",
53
+ "@deepseek-ai/dsh-session-projection-cache": "^0.1.1-rc.1",
54
+ "@deepseek-ai/dsh-jobs": "^0.1.1-rc.1",
55
+ "@deepseek-ai/dsh-tools": "^0.1.1-rc.1",
56
+ "@deepseek-ai/dsh-user-approval": "^0.1.1-rc.1",
57
+ "@deepseek-ai/cordis": "^4.0.1",
58
+ "@deepseek-ai/dsh-session-projection": "^0.1.1-rc.1"
59
59
  },
60
60
  "peerDependenciesMeta": {
61
61
  "@deepseek-ai/dsh-agent-presets": {
@@ -84,23 +84,23 @@
84
84
  }
85
85
  },
86
86
  "devDependencies": {
87
- "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.7",
88
- "@deepseek-ai/dsh-brand": "^0.1.0-rc.7",
89
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.7",
90
- "@deepseek-ai/dsh-sandbox": "^0.1.0-rc.7",
91
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.7",
92
- "@deepseek-ai/dsh-agent": "^0.1.0-rc.7",
93
- "@deepseek-ai/dsh-sandbox-policy": "^0.1.0-rc.7",
94
- "@deepseek-ai/dsh-session": "^0.1.0-rc.7",
95
- "@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.7",
96
- "@deepseek-ai/dsh-session-projection": "^0.1.0-rc.7",
97
- "@deepseek-ai/dsh-scope": "^0.1.0-rc.7",
98
- "@deepseek-ai/dsh-session-projection-cache": "^0.1.0-rc.7",
99
- "@deepseek-ai/dsh-storage": "^0.1.0-rc.7",
100
- "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.7",
101
- "@deepseek-ai/cordis": "^4.0.1",
102
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.7",
103
- "@deepseek-ai/dsh-storage-domain": "^0.1.0-rc.7",
104
- "@deepseek-ai/dsh-jobs": "^0.1.0-rc.7"
87
+ "@deepseek-ai/dsh-agent": "^0.1.1-rc.1",
88
+ "@deepseek-ai/dsh-agent-presets": "^0.1.1-rc.1",
89
+ "@deepseek-ai/dsh-brand": "^0.1.1-rc.1",
90
+ "@deepseek-ai/dsh-invariants": "^0.1.1-rc.1",
91
+ "@deepseek-ai/dsh-sandbox": "^0.1.1-rc.1",
92
+ "@deepseek-ai/dsh-llm": "^0.1.1-rc.1",
93
+ "@deepseek-ai/dsh-sandbox-policy": "^0.1.1-rc.1",
94
+ "@deepseek-ai/dsh-session": "^0.1.1-rc.1",
95
+ "@deepseek-ai/dsh-scope": "^0.1.1-rc.1",
96
+ "@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.1",
97
+ "@deepseek-ai/dsh-session-projection": "^0.1.1-rc.1",
98
+ "@deepseek-ai/dsh-session-projection-cache": "^0.1.1-rc.1",
99
+ "@deepseek-ai/dsh-storage": "^0.1.1-rc.1",
100
+ "@deepseek-ai/dsh-storage-domain": "^0.1.1-rc.1",
101
+ "@deepseek-ai/dsh-tools": "^0.1.1-rc.1",
102
+ "@deepseek-ai/dsh-user-approval": "^0.1.1-rc.1",
103
+ "@deepseek-ai/dsh-jobs": "^0.1.1-rc.1",
104
+ "@deepseek-ai/cordis": "^4.0.1"
105
105
  }
106
106
  }