@deepseek-ai/dsh-subagent 0.0.1-rc.1 → 0.0.1-rc.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/README.i18n.yaml +2 -2
- package/README.md +27 -5
- package/README.zh.md +26 -4
- package/lib/index.js +213 -33
- package/lib/types/assistant-output.d.ts +48 -0
- package/lib/types/assistant-output.js +72 -0
- package/lib/types/continuation.d.ts +62 -1
- package/lib/types/continuation.js +167 -18
- package/lib/types/index.d.ts +3 -2
- package/lib/types/index.js +2 -1
- package/lib/types/lifecycle.d.ts +21 -1
- package/lib/types/lifecycle.js +37 -29
- package/lib/types/types.d.ts +19 -4
- package/package.json +33 -33
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:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: edddb39a2c197eb7c8acb27f5b7f6b8c6016c517
|
|
6
|
+
README.zh.md: e28a8794cfb227dd475ea2b4c42605a90ec7fa1d
|
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ Every in-process child is composed by one call, `applyChildComposition(childCtx,
|
|
|
44
44
|
|
|
45
45
|
`childSessionMeta()` records the joined preset id on the child's durable header for the same reason a top-level session records its own: the preset decides the tool schemas and prompt sections the model saw, so a cold read of the child's history has to rebuild that composition rather than the deployment default. It is read from the parent's live scope chain, not from the parent header, because a parent that switched preset while blank runs on the newer composition while its header still names the older one.
|
|
46
46
|
|
|
47
|
-
Continuable creation is the optional `SubagentProvider.prepareContinuable?()` method: its presence is the capability check, so the service rejects a configured continuable start on a provider without it, while a provider that has it may still serve ordinary one-shot delegations. The method returns only a detached `ContinuableCreateSpec` (`{ seed? }`) — data, never a capability: it carries no Agent, `AgentHandle`, prompt delivery, result, disposal, or resume operation, because the continuation manager owns identity reservation, composition, Agent creation, prompt delivery, cold resume, ownership, and disposal after preparation. A one-shot `SubagentRun` represents one disposable foreground delegation with one result and no cold-resume operation.
|
|
47
|
+
Continuable creation is the optional `SubagentProvider.prepareContinuable?()` method: its presence is the capability check, so the service rejects a configured continuable start on a provider without it, while a provider that has it may still serve ordinary one-shot delegations. The method returns only a detached `ContinuableCreateSpec` (`{ seed? }`) — data, never a capability: it carries no Agent, `AgentHandle`, prompt delivery, result, disposal, or resume operation, because the continuation manager owns identity reservation, composition, Agent creation, prompt delivery, cold resume, ownership, and disposal after preparation. A one-shot `SubagentRun` represents one disposable foreground delegation with one result and no cold-resume operation. The service may invoke one provider concurrently for distinct siblings: each start or preparation owns its mutable state and cancellation path, and one operation's failure, result, or cleanup must not settle or release another. A provider may queue its own capacity internally without changing that independence contract.
|
|
48
48
|
|
|
49
49
|
## The durable descriptor
|
|
50
50
|
|
|
@@ -64,7 +64,7 @@ Both in-process delegation paths fix the child's permission scope at the delegat
|
|
|
64
64
|
|
|
65
65
|
`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
66
|
|
|
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.
|
|
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
68
|
|
|
69
69
|
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
70
|
|
|
@@ -76,6 +76,14 @@ The manager derives three internal residency conditions from Agent quiescence an
|
|
|
76
76
|
|
|
77
77
|
The manager reserves the child identity, resolves the durable descriptor, calls `ctx.agents.create()` (or `ctx.agents.resume()` for cold resume) through a private activation-owner scope, installs the returned `AgentHandle` in the Activation, establishes any continuable-parent ownership, and then submits the prompt. Cold resume never dispatches through a provider because the persisted Session already holds the initial prefix and the folded descriptor is the whole reconstruction input.
|
|
78
78
|
|
|
79
|
+
### Settlement delivery
|
|
80
|
+
|
|
81
|
+
When a resident Activation settles, the manager tells the child's durable direct parent, in the parent's own turn stream, that the child produced everything it is going to. Delivery is unconditional for every child whose id a caller actually received: it does not consider whether the child called `report`, because the endings that most need an account — a token ceiling, a model failure, cancellation, teardown — are exactly the ones where the child never got to choose. A materialization rolled back before its first accepted message stays silent, since that caller was told the child was not established. The message carries the epoch's stop reason, its final assistant content when it produced any, and durable provenance `{ kind: 'subagent-settled', form: 'notice', senderSessionId: <child-id> }` — a different source kind from a child-authored `subagent-report`, so a transcript never credits the child with words the runtime wrote.
|
|
82
|
+
|
|
83
|
+
Two ordering rules make the delivery reliable rather than lucky, and both are why this belongs to the manager instead of an external `subagent/end` listener. First, the send happens **before** the child's ownership release, while the parent still counts the child and is therefore structurally unable to be judged settled. Second, a parent that is itself a resident Activation receives the message through the same waking-admission accounting as a report, so the window between the synchronous send and the microtask that admits it is not mistaken for quiescence — `Agent.status` folds context maintenance into `idle`, and a waking send behind maintenance only arms a deferred wake. Without either rule the parent can be disposed with the notice still in an inbox that `cancel()` clears, which loses it silently.
|
|
84
|
+
|
|
85
|
+
An idle parent receives the notice as one ordinary later turn. A busy parent is steered into its nearest step boundary instead, so several children settling together cost one step rather than one turn each; steering rather than injecting also means a driver that retires between the status read and the send still claims the message. A parent whose own lineage is already draining receives the notice by injection, with no wake at all: `Agent.followup()` on a quiescent parent starts a turn and `cancel()` does not arm against a later one, so waking during teardown would spend a model request on an Agent its host is about to dispose — once per tree layer, since each layer's notice then wakes the layer above it. The injected message reaches a parent that is still reading its inbox, and the log records the account either way, but it does not outlive that parent's own disposal: `AgentHandle.dispose()` is a `keepInbox: false` cancel, which durably cancels an unclaimed notice. A resumed parent therefore has no pending notice to read: `list_agents` tells it which children exist and whether each is live or stored, while the outcome itself stays in the child's own Session, which a `send_message` reaches by resuming that child. A parent that has left the registry is not an error: the notice is dropped and the child's own Session remains the durable record. Delivery never blocks or fails teardown — a rejected send is logged, because retaining a child to retry a notice would pin its whole ancestry in `waiting` forever.
|
|
86
|
+
|
|
79
87
|
A continuation-managed parent Activation records each child Session id in an `ownedChildren` set before the child can run and disposes only after every owned child Activation completes `AgentHandle` disposal (child-first). Teardown propagates Agent cancellation top-down before awaiting slow descendants, while handle release remains child-first. Top-level and other non-continuation Agents have no Activation and stay outside this waiting graph. Final settlement awaits a best-effort `ctx.sessions.flush(child.session)` before handle disposal. A listener rejection is logged without failing the Activation because listener participation does not identify a persistence backend; the persisted state may therefore be missing or stale on resume.
|
|
80
88
|
|
|
81
89
|
## Lifecycle events
|
|
@@ -94,17 +102,31 @@ When `ctx.sessionProjections` is available, the service registers two projection
|
|
|
94
102
|
|
|
95
103
|
## Collection model
|
|
96
104
|
|
|
97
|
-
The model-facing tool collects synchronously by default: it awaits the child result and disposes the run before returning. One-shot background delegation registers a plain Task in the tool, whose generic status, collection, and cancellation tools own later interaction, and persists its model-supplied `description` as the optional display label. Continuable background delegation calls `ctx.subagents.startContinuable()` and returns only the durable child id; the child owns its own turns from inbox acceptance, so there is no Task and no result promise — a caller sends later work with the `send_message` follow-up tool, and `interrupt()` stops only the current turn without disposing the child, and the durable child Session remains the source of the child's detailed output. The continuation manager exists only while `ctx.agents` is available, and session persistence is resolved per continuation operation. Independently, `listChildren()` enumerates the live-preferred merge of the live session store and optional session persistence — live-only when persistence is absent, since a cold child cannot be resumed then either — and serves each child's durable mode/label from the registered `subagent` projection unit: the registry's watermark snapshot for a live child; for a cold one, a durable projection-cache row when it serves an own-suffix identity — its `seq` gate proves the value postdates the fork seed, where a child's own descriptor is immutable once appended — else one bounded-concurrency persistence inspection folded through the registry, whose result must still name the enumerated lifecycle (a re-published id degrades to a `corrupt` diagnostic). A throwing cache read renders no verdict — the cache is derived data — and silently falls through to that authoritative re-fold. The projection fold is the single classification authority; listing parses no descriptor itself. A served identity produces a child row; a settled candidate whose fold served no identity is a `corrupt` diagnostic, a failed inspection is a transient `unavailable` retried on the next listing, and a running candidate without an identity yet is omitted (the creation window before its descriptor is appended). It never consults the continuation manager, Agent registrations, Activations, or providers. Each child row derives its read-time `hasChildren` hint from merged headers carrying durable `origin: 'subagent'`; it does not read descendant event logs, and the descriptor-backed child catalog remains authoritative when expanded. Service consumers such as a UI can retain both modes and choose a fallback for an unlabeled one-shot child; the model-facing `list_agents` tool projects only `continuable` entries and refines status through the live Agent registry (`running`/`idle`/`
|
|
105
|
+
The model-facing tool collects synchronously by default: it awaits the child result and disposes the run before returning. One-shot background delegation registers a plain Task in the tool, whose generic status, collection, and cancellation tools own later interaction, and persists its model-supplied `description` as the optional display label. Continuable background delegation calls `ctx.subagents.startContinuable()` and returns only the durable child id; the child owns its own turns from inbox acceptance, so there is no Task and no result promise — a caller sends later work with the `send_message` follow-up tool, and `interrupt()` stops only the current turn without disposing the child, and the durable child Session remains the source of the child's detailed output. The continuation manager exists only while `ctx.agents` is available, and session persistence is resolved per continuation operation. Independently, `listChildren()` enumerates the live-preferred merge of the live session store and optional session persistence — live-only when persistence is absent, since a cold child cannot be resumed then either — and serves each child's durable mode/label from the registered `subagent` projection unit: the registry's watermark snapshot for a live child; for a cold one, a durable projection-cache row when it serves an own-suffix identity — its `seq` gate proves the value postdates the fork seed, where a child's own descriptor is immutable once appended — else one bounded-concurrency persistence inspection folded through the registry, whose result must still name the enumerated lifecycle (a re-published id degrades to a `corrupt` diagnostic). A throwing cache read renders no verdict — the cache is derived data — and silently falls through to that authoritative re-fold. The projection fold is the single classification authority; listing parses no descriptor itself. A served identity produces a child row; a settled candidate whose fold served no identity is a `corrupt` diagnostic, a failed inspection is a transient `unavailable` retried on the next listing, and a running candidate without an identity yet is omitted (the creation window before its descriptor is appended). It never consults the continuation manager, Agent registrations, Activations, or providers. Each child row derives its read-time `hasChildren` hint from merged headers carrying durable `origin: 'subagent'`; it does not read descendant event logs, and the descriptor-backed child catalog remains authoritative when expanded. Service consumers such as a UI can retain both modes and choose a fallback for an unlabeled one-shot child; the model-facing `list_agents` tool projects only `continuable` entries and refines status through the live Agent registry and maps storage-only to its resumable-not-terminal `ready` (`running`/`idle`/`ready`) and walks `listDescendants()` for its `descendants` scope. The listing forwards the caller's signal to every persistence read, checks cancellation around each of those awaits, and reports every observed abort as `SubagentError` code `CANCELLED`; an unmounted projection registry fails loud with `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE`, and a missing session store with `SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE`. See the [background subagent tasks Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md), the [continuable background subagents Agent Note](../../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md), the [durable catalog Agent Note](../../../.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md), the [merged-service Agent Note](../../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md), the [capability-seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md), and `src/types.ts` for the complete contracts.
|
|
98
106
|
|
|
99
107
|
Continuable Activations await a best-effort final session flush without treating listener participation as durability confirmation. One-shot runs retain best-effort session checkpointing, so a completed one-shot child is discoverable after disposal only when its session actually reached persistence; the service does not invent a catalog entry from Task history when that checkpoint is absent.
|
|
100
108
|
|
|
101
109
|
## Model Experience
|
|
102
110
|
|
|
111
|
+
### Settlement notice
|
|
112
|
+
|
|
113
|
+
#### What the model sees
|
|
114
|
+
|
|
115
|
+
One user-role parent message opening with the outcome — `Background subagent <child-id> finished and will do no further work unless you send it more.`, or the matching line for a child that was stopped, ran out of room, declined, or failed — followed by `Its closing message:` and the child's final assistant content, or `It left no closing message.` when it produced none. This is the service's only direct parent-side contribution; delegation schemas, parent continuation and discovery, and the child-scoped `report` belong to `dsh-tool-subagent`, `dsh-tool-subagent-control`, and `dsh-tool-subagent-report`.
|
|
116
|
+
|
|
117
|
+
#### Token effect
|
|
118
|
+
|
|
119
|
+
One notice per settled Activation in the parent's request, sized by the child's final message. A child that both reports and settles costs the parent both.
|
|
120
|
+
|
|
121
|
+
#### KV Cache effect
|
|
122
|
+
|
|
123
|
+
Append-only in the parent: the notice follows its reusable request prefix. Reaching an idle parent starts one independent model request; reaching a busy one does not.
|
|
124
|
+
|
|
103
125
|
### Child delegation-scope statement
|
|
104
126
|
|
|
105
127
|
#### What the model sees
|
|
106
128
|
|
|
107
|
-
Every in-process child's runtime-context snapshot carries the `subagent:delegation` statement below, after the sandbox-policy and approval-policy sentences
|
|
129
|
+
Every in-process child's runtime-context snapshot carries the `subagent:delegation` statement below, after the sandbox-policy and approval-policy sentences.
|
|
108
130
|
|
|
109
131
|
##### The delegation-scope statement
|
|
110
132
|
|
|
@@ -129,4 +151,4 @@ Prefix-stable within a child: the statement never changes during the child's lif
|
|
|
129
151
|
- **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.
|
|
130
152
|
- **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.
|
|
131
153
|
- **No durable report mailbox** — reports require a live direct parent and provide acceptance identity rather than exactly-once delivery or a read receipt.
|
|
132
|
-
- **Lifecycle events are observe-only** — a run-affecting `subagent/end` continuation or decision
|
|
154
|
+
- **Lifecycle events are observe-only** — a run-affecting `subagent/end` continuation or decision API waits for a concrete consumer.
|
package/README.zh.md
CHANGED
|
@@ -44,7 +44,7 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
|
|
|
44
44
|
|
|
45
45
|
`childSessionMeta()` 把所加入的 preset id 记在子 agent 的持久化 header 上,理由与顶层会话记录自己的那一个相同:preset 决定了模型所见的工具 schema 与提示段,因此冷读子 agent 的历史时必须重建那份组装,而不是部署默认值。该值从父方**活着的** scope 链读取,而不是从父方 header 读取,因为在空白期切换过 preset 的父方运行在更新的那份组装上,而它的 header 仍写着旧的那个。
|
|
46
46
|
|
|
47
|
-
可继续创建对应可选的 `SubagentProvider.prepareContinuable?()` 方法:方法是否存在就是能力检查,因此服务会在没有该方法的提供方上拒绝已配置的可继续启动,而具备该方法的提供方仍可服务普通一次性委派。该方法只返回分离的 `ContinuableCreateSpec`(`{ seed? }`)——这是数据,绝非能力:它不携带任何 Agent、`AgentHandle`、提示词投递、结果、dispose 或恢复操作,因为准备之后,继续执行管理器拥有身份预留、组合、Agent 创建、提示词投递、冷恢复、所有权和 dispose。一次性 `SubagentRun` 表示一次可 dispose
|
|
47
|
+
可继续创建对应可选的 `SubagentProvider.prepareContinuable?()` 方法:方法是否存在就是能力检查,因此服务会在没有该方法的提供方上拒绝已配置的可继续启动,而具备该方法的提供方仍可服务普通一次性委派。该方法只返回分离的 `ContinuableCreateSpec`(`{ seed? }`)——这是数据,绝非能力:它不携带任何 Agent、`AgentHandle`、提示词投递、结果、dispose 或恢复操作,因为准备之后,继续执行管理器拥有身份预留、组合、Agent 创建、提示词投递、冷恢复、所有权和 dispose。一次性 `SubagentRun` 表示一次可 dispose 的前台委派,只有一个结果,且没有冷恢复操作。服务可以针对不同的同级子 agent 并发调用同一提供方:每次启动或准备都拥有各自的可变状态和取消路径,一项操作的失败、结果或清理不得使另一项操作结算或释放。提供方可以在内部按自身容量排队,但不得改变这项独立性约定。
|
|
48
48
|
|
|
49
49
|
## 持久化描述符
|
|
50
50
|
|
|
@@ -64,7 +64,7 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
|
|
|
64
64
|
|
|
65
65
|
`provider.start(request): Promise<SubagentRun>` 是所有权转移边界;委派工具也会在其由 Task 支撑的一次性后台路径中使用它。兑现前,提供方拥有设置过程,并且每次失败时都必须取消、回滚并使未发布资源完全停稳。兑现后,调用方拥有该运行,并且必须在每条路径上调用 `dispose()`;剩余提示词和轮次工作属于 `SubagentRun.result`。
|
|
66
66
|
|
|
67
|
-
`SubagentRun.result` 兑现为 `{ output, structured?, stopReason }`。子 agent 级失败会以非 `completed` 原因兑现;只有 seam 无法表示的基础设施故障才可以拒绝。`dispose()` 是幂等的,会取消剩余工作,并等待结果结算以及子 agent 资源完全停稳。`result` 的 rejection 仍归 `result` 通道;只有独立的资源释放失败会使 `dispose()`
|
|
67
|
+
`SubagentRun.result` 兑现为 `{ output, structured?, stopReason }`。子 agent 级失败会以非 `completed` 原因兑现;只有 seam 无法表示的基础设施故障才可以拒绝。`dispose()` 是幂等的,会取消剩余工作,并等待结果结算以及子 agent 资源完全停稳。`result` 的 rejection 仍归 `result` 通道;只有独立的资源释放失败会使 `dispose()` 拒绝。`output` 与 `subagent/end` 事件的 `lastAssistantMessage` 使用导出的 `AssistantOutputFold`/`finalAssistantOutput` 辅助函数选取子 agent 最后一条非空 assistant 消息;若没有这类消息,则选取其累积的 assistant 文本。子 agent 两种输出均未产生时,`output` 为 `[]`,该事件字段缺省(结果约定归 [`SubagentResult.output`](../../../docs/subsystems/subagent.md#the-terminal-result-subagentresult) 所有)。
|
|
68
68
|
|
|
69
69
|
本地运行会在 `start()` 兑现前发布普通的子 agent/会话,把该共享会话 id 作为 `SubagentRun.id` 返回,以 `SubagentRun.localAgent` 公开准确的子 agent,把 `request.parent.session.id` 记录到子 agent 的 `parentSession` header,并在其初始轮次内追加已解析的描述符。远程提供方则生成 parent 作用域的生命周期 id,并返回 `localAgent: undefined`;由于没有本地 child 会话,其一次性运行不会进入基于追踪的枚举结果。
|
|
70
70
|
|
|
@@ -76,6 +76,14 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
|
|
|
76
76
|
|
|
77
77
|
管理器预留子 agent 身份、解析持久化描述符,通过私有的 activation-owner 作用域调用 `ctx.agents.create()`(冷恢复时为 `ctx.agents.resume()`),把返回的 `AgentHandle` 安装到 Activation 中,建立任何可继续父级所有权,然后提交提示词。冷恢复绝不通过提供方分发,因为持久化 Session 已持有初始前缀,折叠后的描述符即是全部重建输入。
|
|
78
78
|
|
|
79
|
+
### 结算投递
|
|
80
|
+
|
|
81
|
+
当一个驻留 Activation 结算时,管理器会在父级自身的轮次流中告知该子级持久化的直接父级:这个子级已经产出它将产出的全部内容。对每个调用方真正拿到过 id 的子级,这条投递都是无条件的:它不考虑该子级是否调用过 `report`,因为最需要这条投递的结束方式——token 上限、模型失败、取消、拆卸——恰恰是子级根本没有机会选择的那些。在第一条消息被接受之前就回滚的物化保持静默,因为那位调用方已被告知该子级未建立。消息会携带该 epoch 的终止原因、它产出过的最终 assistant 内容,以及持久化来源 `{ kind: 'subagent-settled', form: 'notice', senderSessionId: <child-id> }`——与子级自撰的 `subagent-report` 是不同的来源 kind,因此 transcript(文本记录)绝不会把运行时写下的话算到子级头上。
|
|
82
|
+
|
|
83
|
+
有两条顺序规则让这条投递可靠而非侥幸,它们也正是这件事属于管理器而非外部 `subagent/end` listener 的原因。第一,发送发生在子级所有权释放**之前**,此时父级仍然计入该子级,因此在结构上不可能被判定为已结算。第二,本身就是驻留 Activation 的父级会通过与 report 相同的唤醒准入记账接收该消息,因此同步发送与承认它的那个 microtask 之间的窗口不会被误判为静止——`Agent.status` 会把上下文维护折叠成 `idle`,而维护期间的唤醒发送只会预置一次延后唤醒。缺少其中任一条规则,父级都可能在通知仍留在 inbox 时被 dispose,而 `cancel()` 会清空该 inbox,于是通知被静默丢失。
|
|
84
|
+
|
|
85
|
+
空闲父级会以一个普通的后续轮次收到该通知。繁忙父级则被 steer 到其最近的 step 边界,因此同时结算的多个子级只消耗一个 step,而不是各自一个轮次;采用 steer 而非 inject 还意味着:即便驱动在状态读取与发送之间退出,该消息仍会被认领。若父级自身所在的谱系已在 draining,则该通知改为 inject 送达,完全不唤醒:对静息父级调用 `Agent.followup()` 会开启一个轮次,而 `cancel()` 不会对之后的轮次设防,因此在拆卸期间唤醒,会在宿主即将 dispose 的 Agent 上白花一次模型请求——而且每层树各一次,因为每层自己的通知又会唤醒它上面那层。被 inject 的消息会送达仍在读取自身 inbox 的父级,而无论如何日志都会记录这份记账;但它不会比该父级自身的 dispose 活得更久:`AgentHandle.dispose()` 是一次 `keepInbox: false` 的 cancel,会持久地取消尚未被认领的通知。因此 resume 后的父级没有待处理通知可读:`list_agents` 只告诉它有哪些子级、各自是在线还是仅存于存储;结局本身留在子级自己的 Session 里,一次 `send_message` 会通过 resume 该子级把它取回。已离开注册表的父级不算错误:通知被丢弃,子级自身的 Session 仍是持久记录。投递绝不会阻塞或使拆卸失败——发送被拒只会记录日志,因为为了重试一条通知而保留子级,会把它的整条祖先链永久钉在 `waiting` 上。
|
|
86
|
+
|
|
79
87
|
受继续执行管理的父级 Activation 会在子 agent 能够运行之前,把每个子 agent 的 Session id 记录到 `ownedChildren` 集合中,并且只有在每个所拥有的子 agent Activation 完成 `AgentHandle` dispose 之后才会 dispose(子先于父)。拆卸会先自顶向下传播 Agent 取消,再等待缓慢的后代,而 handle 释放仍保持 child-first。顶层及其他非继续执行的 Agent 没有 Activation,处于该等待图之外。最终结算会在 dispose handle 前等待 best-effort 的 `ctx.sessions.flush(child.session)`。listener rejection 会被记录,但不会使 Activation 失败,因为 listener 是否参与无法标识持久化后端;因此,恢复时持久化状态可能缺失或陈旧。
|
|
80
88
|
|
|
81
89
|
## 生命周期事件
|
|
@@ -94,17 +102,31 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
|
|
|
94
102
|
|
|
95
103
|
## 收集模型
|
|
96
104
|
|
|
97
|
-
面向模型的工具默认同步收集:先等待子 agent 结果,再 dispose 运行,然后才返回。一次性后台委派会在工具中注册普通 Task,其通用状态、收集和取消工具负责后续交互,并将模型提供的 `description` 持久化为可选显示标签。可继续后台委派会调用 `ctx.subagents.startContinuable()`,只返回持久化子 agent id;子 agent 自 inbox 接受起就拥有自己的轮次,因此没有 Task、也没有结果 promise——调用方通过 `send_message` 后续操作工具发送后续工作,`interrupt()` 只停止当前轮次而不 dispose 子 agent,而持久化子 agent Session 仍是子 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
|
|
105
|
+
面向模型的工具默认同步收集:先等待子 agent 结果,再 dispose 运行,然后才返回。一次性后台委派会在工具中注册普通 Task,其通用状态、收集和取消工具负责后续交互,并将模型提供的 `description` 持久化为可选显示标签。可继续后台委派会调用 `ctx.subagents.startContinuable()`,只返回持久化子 agent id;子 agent 自 inbox 接受起就拥有自己的轮次,因此没有 Task、也没有结果 promise——调用方通过 `send_message` 后续操作工具发送后续工作,`interrupt()` 只停止当前轮次而不 dispose 子 agent,而持久化子 agent Session 仍是子 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`。
|
|
98
106
|
|
|
99
107
|
可继续 Activation 会等待 best-effort 的最终会话 flush,但不会把 listener 参与视为持久性确认。一次性运行保留尽力执行的会话检查点,因此已完成的一次性 child 只有在其会话确实进入持久化存储时,才可在 dispose 后继续被发现;如果该检查点缺失,服务不会根据 Task 历史虚构目录条目。
|
|
100
108
|
|
|
101
109
|
## 模型体验
|
|
102
110
|
|
|
111
|
+
### 结算通知
|
|
112
|
+
|
|
113
|
+
#### 模型看到的内容
|
|
114
|
+
|
|
115
|
+
一条用户角色的父级消息,开头是结果本身——`Background subagent <child-id> finished and will do no further work unless you send it more.`,或子级被停止、耗尽额度、拒绝任务或失败时的对应句子——随后是 `Its closing message:` 与子级的最终 assistant 内容;若子级没有产出内容,则是 `It left no closing message.`。这是本服务面向父级的唯一直接贡献;委派 schema、父级延续与发现以及子级作用域的 `report` 分别归 `dsh-tool-subagent`、`dsh-tool-subagent-control` 和 `dsh-tool-subagent-report` 所有。
|
|
116
|
+
|
|
117
|
+
#### Token 影响
|
|
118
|
+
|
|
119
|
+
父级请求中,每个已结算的 Activation 一条通知,长度取决于子级的最终消息。既上报又结算的子级会让父级同时支付两份。
|
|
120
|
+
|
|
121
|
+
#### KV Cache 影响
|
|
122
|
+
|
|
123
|
+
在父级中仅追加:通知位于其可复用请求前缀之后。到达空闲父级会启动一次独立的模型请求,到达繁忙父级则不会。
|
|
124
|
+
|
|
103
125
|
### 子级委派范围声明
|
|
104
126
|
|
|
105
127
|
#### 模型看到的内容
|
|
106
128
|
|
|
107
|
-
每个进程内子 agent 的运行时上下文快照都携带下方的 `subagent:delegation`
|
|
129
|
+
每个进程内子 agent 的运行时上下文快照都携带下方的 `subagent:delegation` 声明,位于沙箱策略与审批策略语句之后。
|
|
108
130
|
|
|
109
131
|
##### 委派范围声明
|
|
110
132
|
|
package/lib/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Service } from "@deepseek-ai/cordis";
|
|
2
2
|
import { scopeTarget } from "@deepseek-ai/dsh-scope";
|
|
3
3
|
import { assertObjectJsonSchema } from "@deepseek-ai/dsh-tools";
|
|
4
|
-
import { HarnessError, createUserMessage, errorChain } from "@deepseek-ai/dsh-llm";
|
|
4
|
+
import { HarnessError, boundContextSummary, createUserMessage, errorChain } from "@deepseek-ai/dsh-llm";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
|
-
import {
|
|
6
|
+
import { foldConsumedWork } from "@deepseek-ai/dsh-agent";
|
|
7
|
+
import { Session, SessionId, snapshotJsonValue } from "@deepseek-ai/dsh-session";
|
|
7
8
|
import { z } from "zod";
|
|
8
9
|
import { accessSync, constants, statSync } from "node:fs";
|
|
9
10
|
import { isAbsolute, resolve } from "node:path";
|
|
@@ -52,6 +53,71 @@ function assertSubagentMaxDepth(maxDepth) {
|
|
|
52
53
|
if (maxDepth !== void 0 && (typeof maxDepth !== "number" || !Number.isSafeInteger(maxDepth) || maxDepth < 0 || Object.is(maxDepth, -0))) throw new TypeError("subagent maxDepth must be a non-negative safe integer");
|
|
53
54
|
}
|
|
54
55
|
//#endregion
|
|
56
|
+
//#region lib/types/assistant-output.js
|
|
57
|
+
/**
|
|
58
|
+
* Canonical selection of a child's final assistant output. Backend run results
|
|
59
|
+
* and `subagent/end.lastAssistantMessage` apply the same rule: select the last
|
|
60
|
+
* non-empty assistant message. An empty-content message records usage only
|
|
61
|
+
* when the loop appends it after a max-tokens step with no executable blocks,
|
|
62
|
+
* so it does not replace earlier output. If no non-empty message exists,
|
|
63
|
+
* select the accumulated assistant text. Selection is independent of the
|
|
64
|
+
* run's stop reason.
|
|
65
|
+
*
|
|
66
|
+
* @module @deepseek-ai/dsh-subagent/assistant-output
|
|
67
|
+
*/
|
|
68
|
+
/**
|
|
69
|
+
* Incremental fold of the selection rule, for backends that observe a child's
|
|
70
|
+
* output as it streams: session-event backends {@link push} each event, and
|
|
71
|
+
* transports without session events (ACP content chunks) {@link pushText} raw
|
|
72
|
+
* text into the same streamed fallback.
|
|
73
|
+
*/
|
|
74
|
+
var AssistantOutputFold = class {
|
|
75
|
+
message;
|
|
76
|
+
partial = [];
|
|
77
|
+
/**
|
|
78
|
+
* Fold one session event: a non-empty assistant message becomes the
|
|
79
|
+
* candidate final answer, and a `text-delta` chunk extends the streamed
|
|
80
|
+
* fallback; every other event contributes nothing.
|
|
81
|
+
* @param event - the next observed session event.
|
|
82
|
+
*/
|
|
83
|
+
push(event) {
|
|
84
|
+
if (event.type === "assistant/message") {
|
|
85
|
+
const content = event.data.message.content;
|
|
86
|
+
if (content.length > 0) this.message = content;
|
|
87
|
+
} else if (event.type === "assistant/chunk" && event.data.chunk.type === "text-delta") this.pushText(event.data.chunk.text);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Extend the streamed fallback with text observed outside session events.
|
|
91
|
+
* @param text - the next streamed text piece (an empty piece is a no-op).
|
|
92
|
+
*/
|
|
93
|
+
pushText(text) {
|
|
94
|
+
if (text.length > 0) this.partial.push(text);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Select the final output folded so far.
|
|
98
|
+
* @returns the last non-empty assistant message, else the accumulated
|
|
99
|
+
* streamed text, or `undefined` when the child produced neither.
|
|
100
|
+
*/
|
|
101
|
+
collect() {
|
|
102
|
+
if (this.message !== void 0) return this.message;
|
|
103
|
+
const text = this.partial.join("");
|
|
104
|
+
return text.length > 0 ? [{
|
|
105
|
+
type: "text",
|
|
106
|
+
text
|
|
107
|
+
}] : void 0;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Apply the selection rule to one complete child-owned event suffix.
|
|
112
|
+
* @param events - the child-owned events (after any seed or epoch boundary).
|
|
113
|
+
* @returns the selected output, or `undefined` when the child produced none.
|
|
114
|
+
*/
|
|
115
|
+
function finalAssistantOutput(events) {
|
|
116
|
+
const fold = new AssistantOutputFold();
|
|
117
|
+
for (const event of events) fold.push(event);
|
|
118
|
+
return fold.collect();
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
55
121
|
//#region lib/types/types.js
|
|
56
122
|
/**
|
|
57
123
|
* The seam's consumer-facing contracts: request, result, and capability types
|
|
@@ -133,7 +199,7 @@ function observeRun(emit, provider, parent, run) {
|
|
|
133
199
|
emit("subagent/end", {
|
|
134
200
|
...identity,
|
|
135
201
|
stopReason: result.stopReason,
|
|
136
|
-
lastAssistantMessage: result.output
|
|
202
|
+
...result.output.length === 0 ? {} : { lastAssistantMessage: result.output }
|
|
137
203
|
}, parent);
|
|
138
204
|
}, () => {
|
|
139
205
|
emit("subagent/end", {
|
|
@@ -164,6 +230,7 @@ function createActivationObserver(emit, provider, childId, parent) {
|
|
|
164
230
|
};
|
|
165
231
|
let boundary = 0;
|
|
166
232
|
let captured = { stopReason: "completed" };
|
|
233
|
+
const terminal = (failure) => failure === void 0 ? captured : { stopReason: "error" };
|
|
167
234
|
return {
|
|
168
235
|
start: (child) => {
|
|
169
236
|
boundary = child.session.events.length;
|
|
@@ -171,54 +238,55 @@ function createActivationObserver(emit, provider, childId, parent) {
|
|
|
171
238
|
},
|
|
172
239
|
capture: (child) => {
|
|
173
240
|
const own = child.session.events.slice(boundary);
|
|
174
|
-
const output =
|
|
241
|
+
const output = finalAssistantOutput(own);
|
|
175
242
|
captured = {
|
|
176
243
|
stopReason: epochStopReason(own),
|
|
177
244
|
...output === void 0 ? {} : { output }
|
|
178
245
|
};
|
|
179
246
|
},
|
|
247
|
+
terminal,
|
|
180
248
|
settle: (failure) => {
|
|
181
|
-
const
|
|
249
|
+
const { stopReason, output } = terminal(failure);
|
|
182
250
|
emit("subagent/end", {
|
|
183
251
|
...identity,
|
|
184
|
-
stopReason
|
|
252
|
+
stopReason,
|
|
185
253
|
...output === void 0 ? {} : { lastAssistantMessage: output }
|
|
186
254
|
}, parent);
|
|
187
255
|
}
|
|
188
256
|
};
|
|
189
257
|
}
|
|
190
258
|
/**
|
|
191
|
-
* Why this child's
|
|
192
|
-
* The child's own
|
|
193
|
-
* about whether the model errored, hit its
|
|
194
|
-
* deriving the reason from disposal would
|
|
259
|
+
* Why this child's epoch ended, for the terminal lifecycle edge and the
|
|
260
|
+
* manager's own parent delivery. The child's own log is authoritative:
|
|
261
|
+
* teardown succeeding says nothing about whether the model errored, hit its
|
|
262
|
+
* token ceiling, or was cancelled, so deriving the reason from disposal would
|
|
263
|
+
* report failed work as completed.
|
|
264
|
+
*
|
|
265
|
+
* {@link foldConsumedWork} supplies both halves the raw turn sequence cannot:
|
|
266
|
+
* which turn accounts for the work this epoch consumed, and whether accepted
|
|
267
|
+
* work was cancelled after it without any turn opening over it. A recorded
|
|
268
|
+
* failure still wins over a cancellation — stopping a child that had already
|
|
269
|
+
* failed does not turn its failure into a cancellation.
|
|
195
270
|
* @param events - this epoch's own event suffix.
|
|
196
|
-
* @returns its terminal stop reason; `completed`
|
|
271
|
+
* @returns its terminal stop reason; `completed` only for an epoch that both
|
|
272
|
+
* closed cleanly and had nothing left to run.
|
|
197
273
|
*/
|
|
198
274
|
function epochStopReason(events) {
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
switch (reason.kind) {
|
|
275
|
+
const { end, droppedUnrun } = foldConsumedWork(events);
|
|
276
|
+
switch (end?.data.reason.kind) {
|
|
202
277
|
case "max-tokens": return "max-tokens";
|
|
203
278
|
case "aborted":
|
|
204
279
|
case "interrupted": return "aborted";
|
|
205
280
|
case "error": return "error";
|
|
206
|
-
case "
|
|
281
|
+
case "blocked": return "refusal";
|
|
282
|
+
case void 0:
|
|
283
|
+
case "completed": return droppedUnrun ? "aborted" : "completed";
|
|
207
284
|
/* v8 ignore next 3 -- `TurnEndReason` is merge-extensible, so this arm needs a
|
|
208
285
|
* backend that adds a variant; treating an unnameable reason as success would
|
|
209
286
|
* report failed work as completed. */
|
|
210
287
|
default: return "error";
|
|
211
288
|
}
|
|
212
289
|
}
|
|
213
|
-
/**
|
|
214
|
-
* The child's last assistant message content, for one Activation's terminal
|
|
215
|
-
* lifecycle edge. Absent when no assistant message reached the log.
|
|
216
|
-
* @param events - this epoch's own event suffix.
|
|
217
|
-
* @returns its final assistant content, or `undefined` when it produced none.
|
|
218
|
-
*/
|
|
219
|
-
function lastAssistantOutput(events) {
|
|
220
|
-
return events.findLast((event) => event.type === "assistant/message")?.data.message.content;
|
|
221
|
-
}
|
|
222
290
|
/** Render any listener-thrown value without letting coercion escape containment. */
|
|
223
291
|
function renderThrown(value) {
|
|
224
292
|
try {
|
|
@@ -577,7 +645,8 @@ function seedDescriptorTurn(childId, seed, descriptor) {
|
|
|
577
645
|
/**
|
|
578
646
|
* Internal continuable-subagent manager: stable child ids, descriptor
|
|
579
647
|
* persistence, activation admission, the live ownership graph, cold resume,
|
|
580
|
-
*
|
|
648
|
+
* child-first disposal, and settlement delivery to the parent, behind
|
|
649
|
+
* `ctx.subagents`.
|
|
581
650
|
*
|
|
582
651
|
* A continuable child has one durable Session and at most one process-local
|
|
583
652
|
* {@link Activation} — one residency epoch for a reconstructed child Agent. An
|
|
@@ -587,6 +656,12 @@ function seedDescriptorTurn(childId, seed, descriptor) {
|
|
|
587
656
|
* residency while the Agent loop owns all turn ordering and execution. No
|
|
588
657
|
* continuable path creates a Task or an intermediate result-bearing wrapper.
|
|
589
658
|
*
|
|
659
|
+
* Because residency is this manager's alone to end, telling the parent that a
|
|
660
|
+
* child settled is its job too. An external `subagent/end` listener cannot do
|
|
661
|
+
* it correctly: that payload names no parent, the child handle is already
|
|
662
|
+
* disposed by then, and the release that wakes the parent's own settlement
|
|
663
|
+
* watcher has already run. See {@link SubagentContinuationManager.notifySettlement}.
|
|
664
|
+
*
|
|
590
665
|
* @module @deepseek-ai/dsh-subagent
|
|
591
666
|
*/
|
|
592
667
|
/**
|
|
@@ -599,6 +674,27 @@ function seedDescriptorTurn(childId, seed, descriptor) {
|
|
|
599
674
|
function disposalOf(activation) {
|
|
600
675
|
return activation.disposal;
|
|
601
676
|
}
|
|
677
|
+
/**
|
|
678
|
+
* One line telling a parent that a background child is finished and why, in
|
|
679
|
+
* the parent's own task vocabulary.
|
|
680
|
+
* @param childId - the durable child the parent knows by id.
|
|
681
|
+
* @param stopReason - how the child's last ordinary turn ended.
|
|
682
|
+
* @returns the model-facing opening line of the settlement notice.
|
|
683
|
+
*/
|
|
684
|
+
function settlementSummary(childId, stopReason) {
|
|
685
|
+
const subject = `Background subagent ${childId}`;
|
|
686
|
+
switch (stopReason) {
|
|
687
|
+
case "completed": return `${subject} finished and will do no further work unless you send it more.`;
|
|
688
|
+
case "aborted": return `${subject} was stopped before it finished.`;
|
|
689
|
+
case "max-tokens": return `${subject} ran out of room before it finished.`;
|
|
690
|
+
case "refusal": return `${subject} declined the task.`;
|
|
691
|
+
case "error": return `${subject} failed before it finished.`;
|
|
692
|
+
/* v8 ignore next 4 -- `SubagentResult['stopReason']` is merge-extensible, so this arm
|
|
693
|
+
* needs a backend that adds a variant; an unnameable ending is reported as unfinished
|
|
694
|
+
* rather than silently as success. */
|
|
695
|
+
default: return `${subject} ended abnormally (${String(stopReason)}) before it finished.`;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
602
698
|
/** Serialize each durable child's delivery, release, and disposal. */
|
|
603
699
|
var ChildLock = class {
|
|
604
700
|
tails = /* @__PURE__ */ new Map();
|
|
@@ -845,13 +941,26 @@ var SubagentContinuationManager = class {
|
|
|
845
941
|
senderSessionId: activation.childId
|
|
846
942
|
}
|
|
847
943
|
});
|
|
848
|
-
|
|
849
|
-
if (delivery === "wakeup" && parentActivation !== void 0 && parentActivation.handle.agent === parent) this.admitWaking(parentActivation, message.id, () => {
|
|
944
|
+
if (delivery === "wakeup") this.sendWaking(parent, message, () => {
|
|
850
945
|
this.sendReport(parent, message, delivery);
|
|
851
946
|
});
|
|
852
947
|
else this.sendReport(parent, message, delivery);
|
|
853
948
|
return message.id;
|
|
854
949
|
}
|
|
950
|
+
/**
|
|
951
|
+
* Perform one waking send to a parent, accounted against that parent's own
|
|
952
|
+
* Activation when it has one. Registering the id before the send is what
|
|
953
|
+
* keeps a continuation-managed parent from being judged quiescent in the
|
|
954
|
+
* window between `followup()` and the microtask that admits it.
|
|
955
|
+
* @param parent - the exact live parent receiving the waking message.
|
|
956
|
+
* @param message - the message whose id is accounted.
|
|
957
|
+
* @param send - the synchronous waking send to perform.
|
|
958
|
+
*/
|
|
959
|
+
sendWaking(parent, message, send) {
|
|
960
|
+
const parentActivation = this.activations.get(parent.id);
|
|
961
|
+
if (parentActivation !== void 0 && parentActivation.handle.agent === parent) this.admitWaking(parentActivation, message.id, send);
|
|
962
|
+
else send();
|
|
963
|
+
}
|
|
855
964
|
/** Send one report while translating only the parent's own rejection. */
|
|
856
965
|
sendReport(parent, message, delivery) {
|
|
857
966
|
try {
|
|
@@ -956,11 +1065,23 @@ var SubagentContinuationManager = class {
|
|
|
956
1065
|
}
|
|
957
1066
|
return lineage;
|
|
958
1067
|
}
|
|
1068
|
+
/**
|
|
1069
|
+
* The teardown that closed continuable admission for this agent's lineage.
|
|
1070
|
+
* `'manager'` is the whole manager draining; an Agent is the exact scoped root
|
|
1071
|
+
* whose forest is closing.
|
|
1072
|
+
* @param agent - the agent whose lineage is tested.
|
|
1073
|
+
* @returns the closing teardown, or `undefined` while admission is open.
|
|
1074
|
+
*/
|
|
1075
|
+
closingTeardownFor(agent) {
|
|
1076
|
+
if (this.draining) return "manager";
|
|
1077
|
+
const lineage = this.liveLineage(agent);
|
|
1078
|
+
for (const [root, members] of this.closingScopes) if (members.has(agent) || lineage.includes(root)) return root;
|
|
1079
|
+
}
|
|
959
1080
|
/** Reject new admission once the manager or this exact parent tree began draining. */
|
|
960
1081
|
assertAdmitting(agent) {
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1082
|
+
const closing = this.closingTeardownFor(agent);
|
|
1083
|
+
if (closing === void 0) return;
|
|
1084
|
+
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");
|
|
964
1085
|
}
|
|
965
1086
|
/**
|
|
966
1087
|
* Derive residency from Agent quiescence and the owned-child set. `running`
|
|
@@ -1088,6 +1209,7 @@ var SubagentContinuationManager = class {
|
|
|
1088
1209
|
});
|
|
1089
1210
|
const activation = {
|
|
1090
1211
|
childId,
|
|
1212
|
+
parentSession: parent.id,
|
|
1091
1213
|
provider,
|
|
1092
1214
|
handle,
|
|
1093
1215
|
ancestry: new WeakSet([handle.agent, ...parentLineage]),
|
|
@@ -1095,6 +1217,7 @@ var SubagentContinuationManager = class {
|
|
|
1095
1217
|
observer,
|
|
1096
1218
|
disposal: void 0,
|
|
1097
1219
|
accepted: /* @__PURE__ */ new Set(),
|
|
1220
|
+
announced: false,
|
|
1098
1221
|
poke: Promise.withResolvers()
|
|
1099
1222
|
};
|
|
1100
1223
|
this.activations.set(childId, activation);
|
|
@@ -1167,9 +1290,11 @@ var SubagentContinuationManager = class {
|
|
|
1167
1290
|
content,
|
|
1168
1291
|
source
|
|
1169
1292
|
});
|
|
1170
|
-
|
|
1293
|
+
const accepted = this.admitWaking(activation, message.id, () => {
|
|
1171
1294
|
activation.handle.agent.followup(message);
|
|
1172
1295
|
});
|
|
1296
|
+
activation.announced = true;
|
|
1297
|
+
return accepted;
|
|
1173
1298
|
}
|
|
1174
1299
|
/**
|
|
1175
1300
|
* Account one waking send across a resident Activation's settlement window.
|
|
@@ -1300,11 +1425,66 @@ var SubagentContinuationManager = class {
|
|
|
1300
1425
|
if (failures.length === 1) failure = failures[0];
|
|
1301
1426
|
else if (failures.length > 1) failure = new SubagentError(`subagent "${childId}" activation teardown failed at ${failures.length} boundaries: ` + failures.map((item) => errorChain(item)).join("; "), "ACTIVATION_TEARDOWN_FAILED", { cause: new AggregateError(failures) });
|
|
1302
1427
|
this.activations.delete(childId);
|
|
1428
|
+
this.notifySettlement(activation, activation.observer.terminal(failure));
|
|
1303
1429
|
this.releaseOwnership(childId);
|
|
1304
1430
|
activation.observer.settle(failure);
|
|
1305
1431
|
if (failure !== void 0) throw failure;
|
|
1306
1432
|
}
|
|
1307
1433
|
/**
|
|
1434
|
+
* Tell the durable direct parent that this child produced everything it is
|
|
1435
|
+
* going to. Unconditional for every child the caller received an id for: it
|
|
1436
|
+
* does not consider whether the child reported, because the cases that most
|
|
1437
|
+
* need it — a token ceiling, a model failure, cancellation, teardown — are
|
|
1438
|
+
* exactly the ones where the child never got to choose. A materialization
|
|
1439
|
+
* rolled back before its first acceptance stays silent, since the caller was
|
|
1440
|
+
* told that child was not established. A parent that is no longer live is not
|
|
1441
|
+
* an error; the child's own Session remains the durable record either way.
|
|
1442
|
+
* A parent whose own lineage is already closing receives the notice without a
|
|
1443
|
+
* wake, because teardown is not a reason to start a turn.
|
|
1444
|
+
*
|
|
1445
|
+
* Never blocks disposal. A delivery failure is logged and dropped, because
|
|
1446
|
+
* retaining a child to retry a notice would pin its whole ancestry in
|
|
1447
|
+
* `waiting` forever.
|
|
1448
|
+
* @param activation - the settling Activation, still owned by its parent.
|
|
1449
|
+
* @param terminal - how this epoch ended, as the terminal edge will report it.
|
|
1450
|
+
*/
|
|
1451
|
+
notifySettlement(activation, terminal) {
|
|
1452
|
+
if (!activation.announced) return;
|
|
1453
|
+
try {
|
|
1454
|
+
const parent = this.ctx.agents.get(activation.parentSession);
|
|
1455
|
+
if (parent === void 0) return;
|
|
1456
|
+
const summary = settlementSummary(activation.childId, terminal.stopReason);
|
|
1457
|
+
const message = createUserMessage({
|
|
1458
|
+
content: [{
|
|
1459
|
+
type: "text",
|
|
1460
|
+
text: summary
|
|
1461
|
+
}, ...terminal.output === void 0 ? [{
|
|
1462
|
+
type: "text",
|
|
1463
|
+
text: "It left no closing message."
|
|
1464
|
+
}] : [{
|
|
1465
|
+
type: "text",
|
|
1466
|
+
text: "Its closing message:"
|
|
1467
|
+
}, ...terminal.output]],
|
|
1468
|
+
source: {
|
|
1469
|
+
kind: "subagent-settled",
|
|
1470
|
+
form: "notice",
|
|
1471
|
+
summary: boundContextSummary(summary),
|
|
1472
|
+
senderSessionId: activation.childId
|
|
1473
|
+
}
|
|
1474
|
+
});
|
|
1475
|
+
if (this.closingTeardownFor(parent) !== void 0) {
|
|
1476
|
+
parent.inject(message);
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
this.sendWaking(parent, message, () => {
|
|
1480
|
+
if (parent.status === "idle") parent.followup(message);
|
|
1481
|
+
else parent.steer(message);
|
|
1482
|
+
});
|
|
1483
|
+
} catch (error) {
|
|
1484
|
+
this.ctx.logger.warn(`subagent "${activation.childId}" settlement notice was not delivered to its parent: ` + errorChain(error));
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1308
1488
|
* Request a best-effort final session flush after the child is quiescent.
|
|
1309
1489
|
* Listener failure is logged because flush participation cannot identify a
|
|
1310
1490
|
* particular persistence backend, and teardown must still release ownership.
|
|
@@ -2079,7 +2259,7 @@ async function settleRun(run) {
|
|
|
2079
2259
|
//#region lib/types/index.js
|
|
2080
2260
|
/**
|
|
2081
2261
|
* Service Definition for the subagent capability seam (`ctx.subagents`): a named-provider registry plus a
|
|
2082
|
-
* capability-validating asynchronous start
|
|
2262
|
+
* capability-validating asynchronous start API. Providers establish a
|
|
2083
2263
|
* child before returning its run, so fulfillment is the single publication and
|
|
2084
2264
|
* ownership-transfer boundary.
|
|
2085
2265
|
*
|
|
@@ -2389,4 +2569,4 @@ var SubagentService = class extends Service {
|
|
|
2389
2569
|
}
|
|
2390
2570
|
};
|
|
2391
2571
|
//#endregion
|
|
2392
|
-
export { NO_START_CAPABILITIES, SUBAGENT_DESCRIPTOR_VERSION, SubagentDepthError, SubagentError, SubagentRunId, SubagentService, SubagentService as default, appendDelegatedPolicyOverrides, applyChildComposition, assertPositiveFinite, assertSubagentMaxDepth, assertUsableCwd, captureDelegatedPolicyOverrides, childSessionMeta, delegationDepthOf, foldSubagentDescriptor, resolveChildAgentOptions, resolveChildCwd, resolveChildDepth, seedDescriptorTurn, settleRun, settleRunResult, snapshotSubagentDescriptor, subprocessRunHandle, validateConfiguredCwd };
|
|
2572
|
+
export { AssistantOutputFold, NO_START_CAPABILITIES, SUBAGENT_DESCRIPTOR_VERSION, SubagentDepthError, SubagentError, SubagentRunId, SubagentService, SubagentService as default, appendDelegatedPolicyOverrides, applyChildComposition, assertPositiveFinite, assertSubagentMaxDepth, assertUsableCwd, captureDelegatedPolicyOverrides, childSessionMeta, delegationDepthOf, finalAssistantOutput, foldSubagentDescriptor, resolveChildAgentOptions, resolveChildCwd, resolveChildDepth, seedDescriptorTurn, settleRun, settleRunResult, snapshotSubagentDescriptor, subprocessRunHandle, validateConfiguredCwd };
|