fix(subagent): cancel pending live delivery

This commit is contained in:
Dudu-0223
2026-07-27 19:49:22 +08:00
committed by imccyu
parent fea31a012d
commit 644bf00b86
19 changed files with 320 additions and 65 deletions

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
subagent.md: 2497dbab9cfc8304eb7aaeba7109404ac614bbff
subagent.zh.md: 2d96e9bc635951746e72ed58a7c3638dc2598cc2
# pnpm run verify-translation-pairing --write docs/core-data-structures/subagent.md
subagent.md: 1321429ac9e6280878016601646dd08981ab2b40
subagent.zh.md: 072b2d2c1635d7c2c59b5a24d2bafc6ee32f8422

View File

@@ -105,7 +105,7 @@ interface SubagentStartRequest {
## Continuable children: `SubagentContinuation` and `SubagentResumeRequest`
A **continuable background subagent** is a durable child session with a series of Task-backed activations. `SubagentService.startContinuable()` allocates the stable child id, snapshots the versioned `subagent/descriptor` payload, and passes both through the resolved start request; the provider publishes exactly that id and appends the descriptor inside the child's first turn. `SubagentService.sendMessage()` loads and authorizes a stopped child before dispatching a fully resolved resume request through the raw `resume()` operation, or steers its live activation. An internal manager owns descriptor lookup and Task association only while `ctx.tasks` and `ctx.agents` exist; persistence is required per continuation operation, not to load the provider registry. `startContinuable()` returns both identities, while `sendMessage()` reports whether the message `steered` the existing Task or `started` a fresh one. Every sender supplies a `MessageSource`; the optional model-facing tool uses `CoordinatorMessageSource`, while a human adapter uses `{ kind: 'user' }`.
A **continuable background subagent** is a durable child session with a series of Task-backed activations. `SubagentService.startContinuable()` allocates the stable child id, snapshots the versioned `subagent/descriptor` payload, and passes both through the resolved start request; the provider publishes exactly that id and appends the descriptor before the initial prompt is admitted. `SubagentService.sendMessage()` loads and authorizes a stopped child before dispatching a fully resolved resume request through the raw `resume()` operation, or steers its live activation. An internal manager owns descriptor lookup and Task association only while `ctx.tasks` and `ctx.agents` exist; persistence is required per continuation operation, not to load the provider registry. `startContinuable()` returns both identities, while `sendMessage()` reports whether the message `steered` the existing Task or `started` a fresh one. Every sender supplies a `MessageSource` and cancellation signal; abort while live delivery awaits admission cancels the shared activation and rejects after quiescence. The optional model-facing tool uses `CoordinatorMessageSource` and its tool-execution signal, while a human adapter uses `{ kind: 'user' }` and its interaction signal.
```ts type-equiv
/** Attribution for a model coordinator's follow-up to one of its children. */

View File

@@ -4,13 +4,13 @@
subagent seam一个 agent智能体将工作委派给子 agent。与 [bash](bash.md) 一样,它是**一项可选能力**,不属于 agent loop智能体循环主干因此其词汇定义在此而非 [core.md](core.md) 中。但它在一个维度上与其他所有 seam 不同:**同一上下文中可共存多个提供方实现**,按名称注册(`ctx.subagents`),而 bash 只允许一个执行器。注册表的形状参照 [LLM大语言模型适配器注册表](llm-streaming.md),而非单服务的 bash 执行器。
接口:[dsh-subagent](../../packages/subagent/subagent)`ctx.subagents` + 下文词汇。实现为三个兄弟包package`dsh-subagent-spawn``-fork``-acp`;面向模型的消费方 [dsh-tool-subagent](../../packages/subagent/tool-subagent)。提案与设计理由见 [subagent Agent Noteagent 决策记录)](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)。
接口:[dsh-subagent](../../packages/subagent/subagent)`ctx.subagents` + 下文词汇。实现为三个兄弟包package`dsh-subagent-spawn``-fork``-acp`;面向模型的消费方包括 [dsh-tool-subagent](../../packages/subagent/tool-subagent)(按提供方委派)和 [dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control)(可选的全局 `send_message`)。同一个 `ctx.subagents` 服务通过由 Task 支撑的内部管理器负责可继续子 agent 编排。设计理由见 [subagent Agent Noteagent 决策记录)](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)、[可继续后台 subagent Agent Note](../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md)和[服务合并 Agent Note](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md)
源码:[`packages/subagent/subagent/src/types.ts`](../../packages/subagent/subagent/src/types.ts)
源码:[`packages/subagent/subagent/src/types.ts`](../../packages/subagent/subagent/src/types.ts)、[`packages/subagent/subagent/src/index.ts`](../../packages/subagent/subagent/src/index.ts)和 [`packages/subagent/subagent/src/continuation.ts`](../../packages/subagent/subagent/src/continuation.ts)
## 两类能力,两种发现方式
提供方通过一个静态描述符公布其**启动时**特性,服务在 run 存在之前即行检查;如果请求依赖提供方不具备的特性,会被大声拒绝(`SubagentError('UNSUPPORTED_CAPABILITY')`),绝不会被接受后静默忽略。**运行时**特性steering中途引导、恢复)则是 [`SubagentRun`](#a-live-run-subagentrun) 上的可选方法——方法的存在即为能力TypeScript 的类型收窄即为发现机制
提供方通过一个静态描述符公布其**启动时**特性,服务在 run 存在之前即行检查;如果请求依赖提供方不具备的特性,会被大声拒绝(`SubagentError('UNSUPPORTED_CAPABILITY')`),绝不会被接受后静默忽略。**运行时**特性则是可选方法方法存在即为能力TypeScript 的类型收窄即为发现机制:提供确认语义的在线 steering中途引导是 [`SubagentRun.steer`](#a-live-run-subagentrun),从持久化存储恢复是 [`SubagentProvider.resume`](#the-provider-seam-subagentprovider)
```ts type-equiv
/**
@@ -18,9 +18,10 @@ subagent seam一个 agent智能体将工作委派给子 agent。与 [ba
* {@link SubagentProvider.start}: a request that needs a capability the chosen provider lacks
* is rejected with a typed error rather than accepted-then-ignored (the "fail loud, no silent
* degradation" rule). These static flags cover features needed before a run exists; runtime
* capabilities such as steering and resume are optional {@link SubagentRun} methods whose presence
* is the capability. Each flag corresponds one-to-one to a {@link SubagentStartRequest} option:
* `depthLimit` to `maxDepth`; the other names match.
* capabilities are optional methods whose presence is the capability — confirmed live steering
* is {@link SubagentRun.steer} and persisted cold resume is {@link SubagentProvider.resume}. Each
* flag corresponds one-to-one to a {@link SubagentStartRequest} option: `depthLimit` to
* `maxDepth`; the other names match.
*/
interface SubagentCapabilities {
readonly outputSchema: boolean
@@ -88,11 +89,82 @@ interface SubagentStartRequest {
* persona (strict `{{…}}` interpolation against the registered variables).
*/
readonly persona?: string
/**
* Continuable-child intent, resolved by `ctx.subagents` before start.
* The provider MUST publish exactly `sessionId` as the child identity
* instead of allocating one internally, and MUST append the snapshotted
* `descriptor` as the child's turn-enclosed `subagent/descriptor` event
* before its first request. Requires {@link SubagentProvider.resume} (the
* continuation capability); the service rejects the request otherwise.
*/
readonly continuation?: SubagentContinuation
}
```
`signal` 是就绪前后唯一的取消通道。[subagent 组合控制 Agent Note](../../.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md)规定 persona、live 全局工具过滤、绝对深度以及「可见性而非权限」的设计理由。
## 可继续子 agent`SubagentContinuation` 与 `SubagentResumeRequest`
**可继续后台 subagent** 是一份持久化子 agent 会话,由一系列由 Task 支撑的激活组成。`SubagentService.startContinuable()` 会分配稳定的子 agent id、对版本化的 `subagent/descriptor` payload 建立快照,并通过已解析的启动请求传入二者;提供方会准确发布该 id并在初始 prompt 获准前追加描述符。`SubagentService.sendMessage()` 会先加载并授权已停止的子 agent再通过底层 `resume()` 操作分发完全解析的恢复请求,或引导其实时激活。只有 `ctx.tasks` 和 `ctx.agents` 存在时,内部管理器才会负责描述符查找与 Task 关联;每项继续执行操作都要求持久化,而加载提供方注册表不要求持久化。`startContinuable()` 返回两个标识,`sendMessage()` 则报告消息是对现有 Task 执行了 `steered`,还是 `started` 一个新 Task。每个发送方都会提供 `MessageSource` 和取消信号;若在在线投递等待准入期间中止该信号,则会取消共享激活,并在其完全停稳后拒绝调用。可选的面向模型工具使用 `CoordinatorMessageSource` 及其工具执行信号,人工适配器则使用 `{ kind: 'user' }` 及其交互信号。
```ts type-equiv
/** Attribution for a model coordinator's follow-up to one of its children. */
interface CoordinatorMessageSource {
readonly kind: 'coordinator'
/** Session id of the agent whose tool call produced the follow-up. */
readonly senderSessionId: SessionId
}
```
```ts type-equiv
/**
* The resolved continuable-child identity and durable composition record a
* continuation caller attaches to a start request.
*/
interface SubagentContinuation {
/** Service-allocated stable child session id, published verbatim. */
readonly sessionId: SessionId
/** Snapshotted descriptor persisted in the child log for cold resume. */
readonly descriptor: SubagentDescriptorData
}
```
```ts type-equiv
/**
* What a caller asks for when resuming a persisted continuable child. The
* continuation manager loads the child log, folds and authorizes its descriptor,
* and passes this fully resolved request to
* {@link SubagentService.resume}, which dispatches to
* {@link SubagentProvider.resume}. The provider reconstructs the declared
* composition under the live parent's scope and drives one turn with `prompt`.
*/
interface SubagentResumeRequest {
/** The persisted child session id to resume. */
readonly sessionId: SessionId
/** The follow-up message that starts the resumed activation's turn. */
readonly prompt: ContentBlock[]
/** Attribution retained when the follow-up becomes the resumed turn's user-role message. */
readonly source: MessageSource
/**
* The live parent agent — the direct parent recorded in the persisted child
* header. In-process backends reconstruct the child under this agent's
* currently loaded scope.
*/
readonly parent: Agent
/**
* Activation-owned cancellation signal, created before descriptor lookup.
* Same pre/post-publication contract as {@link SubagentStartRequest.signal}:
* an abort before publication rejects after rollback quiescence, and an
* abort afterward cancels the published child turn.
*/
readonly signal: AbortSignal
/** The folded durable descriptor whose composition the provider reconstructs. */
readonly descriptor: SubagentDescriptorData
}
```
描述符([descriptor.ts](../../packages/subagent/subagent/src/descriptor.ts) 中的 `SubagentDescriptorData`)会对显式字段建立快照,包括提供方名称、已解析的子 agent `agentOptions.provider`/`model`,以及可选的 `persona`/`toolFilter`;它绝不会对可通过合并扩展的 `AgentOptions` 对象建立快照,因此无关的扩展值不会破坏继续执行,后续新增组合配置输入则必须明确更改版本。描述符省略 `subagentDepth`(从持久化存储恢复时,以持久化 header 中的 `delegationDepth` 为单调下界)和 `outputSchema`(单次激活的结果契约,而非持久化组合配置)。`subagent/descriptor` 事件只进入日志:不含 `surfaceOp`,绝不进入模型历史,并由仅追加日志跨压缩保留。
## 终态结果:`SubagentResult`
一次 run 的最终产出,由 `SubagentRun.result` resolve。`structured` 仅在请求了 `outputSchema` 且成功满足时才存在;请求 schema 不保证一定能得到它,当子 agent 失败或结束时未产出有效 capture 时,提供方可能返回 `stopReason: 'error'`。非 `completed` 的 `stopReason` 意味着 `output` 可能不完整——消费方将其映射为 `isError` 的工具结果,而非将部分输出报告为成功。
@@ -144,7 +216,7 @@ interface SubagentStopReasonMap {
## 活跃 run`SubagentRun`
`SubagentRun` 是消费方持有的、指向一个就绪子 agent 的句柄。消费方 await `result` 并始终 dispose(资源释放)该 run直至其完全停稳。子 agent 失败时以非 completed 的 stop reason resolve只有不可表示的基础设施故障才会 reject。可选的 `sendMessage` 和 `resume` 方法通过自身的存在公布运行时能力
`SubagentRun` 是消费方持有的、指向一个就绪子 agent 的句柄;它表示一次可 dispose资源释放的激活绝不是持久化子 agent handle。消费方 await `result` 并始终 dispose 该 run直至其完全停稳。子 agent 失败时以非 completed 的 stop reason resolve只有不可表示的基础设施故障才会 reject。可继续结果为 completed 还表示提供方已确认本次激活的最终状态具备持久性;必需检查点失败则会 reject。可选且提供确认语义的 `steer` 方法通过自身的存在公布在线投递功能,并且只有在请求快照准入该消息后才会兑现。从持久化存储恢复属于提供方级操作:`SubagentProvider.resume` 会根据子 agent 的持久化会话重建一个新 run因为进程内 run 在 dispose 或进程重启后就不再存在
```ts type-equiv
/**
@@ -169,8 +241,10 @@ interface SubagentRun {
* Resolves with the child's terminal {@link SubagentResult} when the run
* settles. Does NOT reject on a child-level failure — a model/transport
* failure resolves with `stopReason: 'error'` so the consumer maps it to an
* `isError` tool result. Rejects only on an infrastructure fault the seam
* cannot represent as a stop reason.
* `isError` tool result. For a continuable activation, a completed result
* also means the provider confirmed the activation's final state durable.
* Rejects on an infrastructure fault the seam cannot represent as a stop
* reason, including a failed required durability checkpoint.
*/
readonly result: Promise<SubagentResult>
/**
@@ -179,15 +253,16 @@ interface SubagentRun {
*/
dispose(): Promise<void>
/**
* OPTIONAL (steering capability): send additional content to the running
* child between steps. Present only on providers that support live steering.
* OPTIONAL (confirmed live-steering capability): submit additional content
* to the active child and fulfill only after a committed request snapshot
* admits it. Rejects when terminal policy, cancellation, disposal, or a lost
* settlement race prevents admission; it never falls through to a queued
* untracked turn or cold resume. A run represents one disposable activation,
* so resuming a settled child goes through {@link SubagentProvider.resume}.
* `source` is retained on the admitted steering message without changing its
* user role in model history.
*/
sendMessage?(content: ContentBlock[]): void
/**
* OPTIONAL (resume capability): send a follow-up task to a settled child,
* continuing its session, and return a fresh run for the continuation.
*/
resume?(content: ContentBlock[]): Promise<SubagentRun>
steer?(content: ContentBlock[], source: MessageSource): Promise<void>
}
```
@@ -223,10 +298,21 @@ interface SubagentProvider {
* promise rejects. Ownership transfers to the caller only on fulfillment.
*/
start(request: SubagentStartRequest): Promise<SubagentRun>
/**
* OPTIONAL (continuation capability): reconstruct a persisted continuable
* child from its own transcript and declared descriptor, drive one
* follow-up turn, and return a fresh run. Method presence is the capability
* — the service rejects `resume` dispatch and continuable starts on
* providers without it. Same publication contract as {@link start}: if
* reconstruction fails or `request.signal` aborts before fulfillment, the
* provider rolls its creation transaction back to quiescence before
* rejecting; after fulfillment the same signal cancels the published run.
*/
resume?(request: SubagentResumeRequest): Promise<SubagentRun>
}
```
`start()` 仅在 run 就绪时 fulfill。服务铸造唯一 `runId`,从提供方的确切 `localAgent` 快照 `local`观察结果emit `subagent/start`,并返回同一个 runrejection 意味着提供方已清理,且不会 emit 生命周期事件对。配对的 `subagent/end` 携带相同标识与最终输出或基础设施失败。两个事件都仅用于观察,每个 listener 异常都会被独立隔离。
`start()` 仅在 run 就绪时 fulfill`resume()` 采用相同的发布与生命周期观察契约。服务铸造唯一 `runId`,从提供方的确切 `localAgent` 快照 `local`观察结果emit `subagent/start`,并返回同一个 runrejection 意味着提供方已清理,且不会 emit 生命周期事件对。配对的 `subagent/end` 携带相同标识与最终输出或基础设施失败。两个事件都仅用于观察,每个 listener 异常都会被独立隔离。
## 进程内后端:深度与种子