feat(subagent): add explicit child reports

This commit is contained in:
Dudu-0223
2026-07-31 22:45:21 +08:00
committed by Tianyi Cui
parent a1b3bebb61
commit 431fb4b035
76 changed files with 3831 additions and 143 deletions

View File

@@ -2,5 +2,5 @@
# 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 .agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md
2026-07-28-continuable-subagent-conversations.md: e3feee395d3e2f9229a76b6b33b4909b406166ee
2026-07-28-continuable-subagent-conversations.zh.md: f1c8f4cdb093a07946b1eeb24e71ac1cf92e75e4
2026-07-28-continuable-subagent-conversations.md: a5a900c3bb30a8d965aabc0bf498f8399ee70e7a
2026-07-28-continuable-subagent-conversations.zh.md: bde8e874ba27950dc0fab8449c6ba2ee49737d41

View File

@@ -34,7 +34,7 @@ The continuation manager owns activation admission, authority checks, the live o
### Materialization and public operations
The named subagent provider participates only in preparing the initial creation spec, where `spawn` and `fork` differ. Its optional `prepareContinuable(request): Promise<ContinuableCreateSpec>` method is the continuable-creation capability. The returned spec contains only detached provider-specific creation inputs such as the optional parent-history seed; it contains no Agent, `AgentHandle`, prompt delivery, result, disposal, or resume operation. The manager reserves the child identity, resolves the durable descriptor and common Agent setup, calls `ctx.agents.create()` through a private activation-owner scope, installs the returned `AgentHandle` into the Activation, establishes any continuable-parent ownership, and then calls `Agent.followup(initialPrompt)`. Inbox acceptance yields an `MessageId`; at that boundary `ctx.subagents.startContinuable()` returns `{ childId, messageId }` without waiting for the turn to start or for the message to enter the Session log.
The named subagent provider participates only in preparing the initial creation spec, where `spawn` and `fork` differ. Its optional `prepareContinuable(request): Promise<ContinuableCreateSpec>` method is the continuable-creation capability. The returned spec contains only detached provider-specific creation inputs such as the optional parent-history seed; it contains no Agent, `AgentHandle`, prompt delivery, result, disposal, or resume operation. The manager reserves the child identity, resolves the durable descriptor and common Agent setup, calls `ctx.agents.create()` through a private activation-owner scope, installs the returned `AgentHandle` into the Activation, establishes any continuable-parent ownership, and then calls `Agent.followup(initialPrompt)`. Inbox acceptance yields a `MessageId`; at that boundary `ctx.subagents.startContinuable()` returns `{ childId, messageId }` without waiting for the turn to start or for the message to enter the Session log.
Any failure before inbox acceptance rejects without returning either id. Agent creation provides rollback before handle transfer; after transfer, the manager keeps one closing transaction visible to concurrent delivery and drain, disposes the created handle, removes the Activation, and rolls back any parent `ownedChildren` membership before rejecting. Failure before the residency start edge publishes no terminal edge, while failure after a published start closes the lifecycle pair through normal disposal.
@@ -44,7 +44,7 @@ Cold resume does not dispatch through a subagent provider. The continuation mana
`SubagentProvider.start()` and `SubagentRun` remain exclusively on the unchanged one-shot path. A continuable Activation directly owns its `AgentHandle` and never creates, wraps, or retains a `SubagentRun`; `SubagentRun.steer?()` is therefore absent.
`ctx.subagents.followup(parent, childId, content, { source, signal })` remains the sole continuation-message operation. The exact live parent Agent authorizes delivery; cold resume checks that authority before reconstruction and every path checks it again in the final no-await inbox-admission span, so a parent unregistered or replaced during materialization cannot authorize delivery. `source` remains durable provenance and grants no authority. The model-facing `send_message` tool keeps only its stable `subagent_id` and `message` fields and always submits a follow-up turn. Both start and follow-up return the accepted `MessageId`, and neither reports how the manager materialized the Activation.
`ctx.subagents.followup(parent, childId, content, { source, signal })` remains the sole parent-to-child continuation-message operation. The exact live parent Agent authorizes delivery; cold resume checks that authority before reconstruction and every path checks it again in the final no-await inbox-admission span, so a parent unregistered or replaced during materialization cannot authorize delivery. `source` remains durable provenance and grants no authority. The model-facing `send_message` tool keeps only its stable `subagent_id` and `message` fields and always submits a follow-up turn. Both start and follow-up return the accepted `MessageId`, and neither reports how the manager materialized the Activation.
For start and follow-up, the caller signal owns lookup, materialization, and admission only until inbox acceptance. After the operation returns its `MessageId`, the manager owns the Activation independently; later caller cancellation does not cancel the accepted turn or dispose the child.
@@ -111,11 +111,9 @@ Top-level teardown is host-owned rather than represented as another Activation.
The activation-owner scope exists because ordinary Cordis owner effects unwind in reverse registration order, which cannot express the dynamic child graph. Manager initialization registers the private scope's structural disposer first and its drain disposer afterward, so reverse unwind invokes the drain before releasing that scope; merely registering a cleanup effect on the same scope as later Agent handles would allow structural handle disposal to bypass child-first ordering. Each materialization registers its barrier participant and snapshots its exact live ancestry before starting the inner transaction, then remains tracked until it installs an Activation or fully rolls back. The Activation retains weak membership of that ancestry, so an intermediate Agent may leave the registry without hiding a still-live descendant from its host root. Each Activation installs one memoized disposal promise before cancellation or recursive callbacks, allowing scoped host shutdown, global manager unload, child release, and normal settlement to converge without double release. Cancellation propagates top-down before slow descendant cleanup; handle release remains child-first. Sibling branches drain independently; one disposal failure is recorded but does not prevent the manager from attempting the remaining selected handles, and the aggregate drain reports failure after all selected branches settle. Durable child Sessions survive this process-local teardown.
### Deferred report delivery
### Report delivery extension
This version exposes no `report` tool and provides no child-to-parent content delivery or automatic parent wakeup. The durable child Session remains the source of the child's detailed output.
A later proposal may add an ordinary model-facing `report(output)` tool that can be called zero or multiple times in one turn. Its delivery policy may distinguish quiet parent injection from waking the parent; recipient selection, acknowledgement, durability, and retry semantics are deferred with that tool. Adding report delivery does not require another Activation state or execution queue.
The optional child-scoped `report(output)` tool was added later without changing Activation residency or adding another queue. It can be called zero or multiple times per turn, derives the live direct parent rather than accepting a recipient, and selects quiet injection or a waking parent follow-up through deployment config. The [report-tool Agent Note](2026-07-30-continuable-subagent-report-tool.md) owns its authority, acknowledgement, setup-contribution, and delivery contracts.
### Deferred steering
@@ -147,7 +145,7 @@ Session and descriptor persistence survive restart. Activation state, Agent inbo
This version covers continuable in-process children and leaves one-shot delegation unchanged. Remote providers require a separate Activation handle with equivalent authenticated control and child-first quiescence contracts before they can support the same behavior.
It adds no host-user continuation, subagent steering operation, report tool, child-to-parent content delivery, automatic parent wakeup, durable mailbox, cross-process lease, automatic replay of interrupted inbox work, team authority, workflow authority, public subagent cancellation operation, public residency query, new live-Activation or descendant limit, or runtime cache. Existing delegation-depth policy remains unchanged.
It adds no host-user continuation, subagent steering operation, durable mailbox, cross-process lease, automatic replay of interrupted inbox work, team authority, workflow authority, public subagent cancellation operation, public residency query, new live-Activation or descendant limit, or runtime cache. Existing delegation-depth policy remains unchanged. Optional child-to-parent reporting is a later consumer of this lifecycle rather than part of the base continuable capability.
## Alternatives considered
@@ -159,7 +157,7 @@ It adds no host-user continuation, subagent steering operation, report tool, chi
**Let the provider create, resume, or deliver through an Agent handle.** Initial providers own only `prepareContinuable()` and its detached creation-spec distinction: whether a child begins fresh or with a parent prefix. The manager must call `ctx.agents.create()` through its private activation-owner scope so that scope is a structural owner of every handle. A persisted in-process Session already contains the initial prefix and generic reconstruction descriptor, while delivery belongs to the Agent inbox. Giving providers any later handle, `SubagentRun`, or message ownership would preserve a seam with no shipped behavior to own.
**Add report delivery now.** A repeatable model-facing tool is compatible with this lifecycle, but quiet versus waking delivery, recipient selection, acknowledgement, durability, and retry behavior are independent product choices. Deferring the tool keeps the first version focused on conversation admission and residency without constraining that later policy.
**Make report delivery part of the base lifecycle.** Repeatable child-to-parent reporting is compatible with this lifecycle, but quiet versus waking delivery, acknowledgement, durability, and retry behavior are independent product choices. The later report package remains optional and consumes an explicit child-setup seam, so continuable residency does not silently grant a return channel.
**Treat `SessionHeader.parentSession` as live ownership.** Durable lineage does not prove that the historical parent currently owns the child. Membership in the live parent's `ownedChildren` records the process-local relationship without changing durable provenance.
@@ -195,12 +193,13 @@ The implementation pins these behaviors:
- Every continuation-managed parent Activation disposes only after all directly owned child Activations complete `AgentHandle` disposal; top-level Agents do not join the waiting graph.
- Final Activation settlement awaits `ctx.sessions.flush(child.session)` as a best-effort barrier, logs rejection without interpreting listener participation as durability proof, then disposes the child handle and releases parent ownership so a flush failure cannot leak a `waiting` Activation.
- Manager teardown closes admission globally; a host owning selected top-level Agents instead closes admission only below their exact identities until those roots leave the registry. Both track admitted materializations by exact ancestry, install one memoized disposal cutoff per selected visible Activation, propagate cancellation top-down, release handles child-first, await every selected branch despite individual failures, and only then dispose the corresponding top-level Agents or manager scope.
- This version exposes no `report` tool, child-to-parent content delivery, or automatic parent wakeup.
- The base lifecycle has no implicit report behavior; the optional report package contributes an explicit child-scoped tool through the setup seam.
- Session logs reconstruct only messages that were actually written, with their admitted provenance; inbox-accepted but unlogged messages have no restart guarantee.
- No continuable-subagent path creates or depends on a Task, `TaskId`, Task completion notice, Task cancellation, or intermediate result-bearing execution wrapper.
- Unit coverage pins the `startContinuable()` inbox-acceptance return boundary, complete rollback for each pre-acceptance and lifecycle-publication failure, global and parent-scoped drain quiescence for materialization caught between Agent publication and Activation registration, sibling-forest isolation, exact ancestry after an intermediate Agent leaves the registry, provider-independent cold resume, final exact-parent reauthorization after cold-resume materialization, caller-signal and teardown ownership on both sides of acceptance, and the absence of automatic replay for accepted-but-unlogged messages.
- Unit coverage pins the residency-only routing table, single-inbox ordering, `MessageId` correlation through inbox events, follow-up during an open turn, waiting wakeup, cold resume, ownership registration and release, child-first disposal, send-versus-dispose races, best-effort final flush with absent and failing listeners, and the absence of public subagent cancellation, steering, and report tools.
- A keyless assembled-app snapshot covers parent delegation and follow-up queueing, the absence of subagent steering, report delivery, and automatic parent wakeup, retained waiting `AgentHandle`, and child-first disposal.
- Unit coverage pins the residency-only routing table, single-inbox ordering, `MessageId` correlation through inbox events, follow-up during an open turn, waiting wakeup, cold resume, ownership registration and release, child-first disposal, send-versus-dispose races, best-effort final flush with absent and failing listeners, and the absence of public subagent cancellation and steering.
- Report-package unit coverage separately pins child-only visibility, setup revocation, authority, delivery modes, stable message identity, and lifecycle races.
- A keyless assembled-app snapshot covers parent delegation and follow-up queueing, the absence of subagent steering and implicit report delivery, retained waiting `AgentHandle`, and child-first disposal. A separate report snapshot covers the optional explicit return channel.
### Accepted costs
@@ -210,7 +209,7 @@ Retaining an Activation while descendants run consumes Agent resources proportio
The process-local inbox and ownership graph do not coordinate two harness processes. Deployments allowing concurrent access to one persistence store still require a durable lease and mailbox protocol.
Without report delivery, completing a child turn neither sends its content to nor wakes the historical parent. The output remains in the durable child Session until a caller inspects that transcript or submits another authorized turn. A later report tool may add quiet or waking delivery without changing the Activation lifecycle.
Without the optional report package, completing a child turn neither sends its content to nor wakes the historical parent. With the package, only an explicit `report` call sends selected content; quiet delivery does not wake the parent, while waking delivery enqueues one later turn. In every case the detailed child output remains in its durable Session.
Queueing every continuation message means a parent cannot correct an in-progress child turn immediately; the correction runs as the next turn. A later UI steering action may reduce that latency without changing follow-up ordering.

View File

@@ -44,7 +44,7 @@ inbox 接受消息前发生任何失败,操作都会在不返回任何 id 的
`SubagentProvider.start()``SubagentRun` 只保留在不变的 one-shot 路径上。可继续激活直接持有自身的 `AgentHandle`,绝不创建、包装或保留 `SubagentRun`;因此,`SubagentRun.steer?()` 不存在。
`ctx.subagents.followup(parent, childId, content, { source, signal })` 仍是唯一的继续执行消息操作。确切的在线 parent Agent 授权投递;冷恢复会在重建前检查该权限,每条路径还会在最终无 await 的 inbox 准入区间再次检查,因此在物化期间被注销或替换的 parent 无法授权投递。`source` 仍是持久化来源信息,不赋予任何权限。面向模型的 `send_message` 工具只保留稳定的 `subagent_id``message` 字段,并始终提交一个 follow-up 轮次。start 和 follow-up 都返回已接受的 `MessageId`,两者都不报告管理器如何物化激活。
`ctx.subagents.followup(parent, childId, content, { source, signal })` 仍是唯一的从 parent 到 child 的继续执行消息操作。确切的在线 parent Agent 授权投递;冷恢复会在重建前检查该权限,每条路径还会在最终无 await 的 inbox 准入区间再次检查,因此在物化期间被注销或替换的 parent 无法授权投递。`source` 仍是持久化来源信息,不赋予任何权限。面向模型的 `send_message` 工具只保留稳定的 `subagent_id``message` 字段,并始终提交一个 follow-up 轮次。start 和 follow-up 都返回已接受的 `MessageId`,两者都不报告管理器如何物化激活。
对于 start 和 follow-up调用方 signal 只在 inbox 接受消息前持有查找、物化和准入。操作返回 `MessageId` 后,管理器会独立持有该激活;调用方之后的取消不会取消已接受的轮次,也不会 dispose child。
@@ -111,11 +111,9 @@ Agent inbox 是唯一队列。每条继续执行消息都使用 `Agent.followup(
activation-owner 作用域之所以存在,是因为普通 Cordis owner effect 按注册逆序撤销,无法表达动态 child 图。管理器初始化时先注册私有作用域的结构化 disposer再注册自身的 drain disposer使逆序撤销先执行 drain、再释放该作用域如果只在与后续 Agent handle 相同的作用域上注册 cleanup effect结构化 handle dispose 就可能绕过 child-first 顺序。每个物化过程都会在启动内部事务前注册其屏障参与项,并对其确切的在线祖先建立快照,然后保持跟踪,直到安装 Activation 或完全回滚。Activation 会以弱引用方式记录其属于这组祖先,因此中间 Agent 即使离开注册表,也不会让仍在线的后代脱离宿主根节点的可见范围。每个 Activation 都会在取消或递归回调前安装一个记忆化的 dispose promise使限定作用域的宿主关闭、全局管理器卸载、child 释放和正常结算能够汇合而不会重复释放。取消会在等待缓慢的后代清理之前自顶向下传播handle 释放仍是 child-first。同级分支独立 drain系统会记录单次 dispose 失败,但仍会尝试其余选中 handle聚合 drain 则在所有选中分支结算后报告失败。这次进程内拆卸不会销毁持久化 child 会话。
### 延后的报告投递
### 报告投递扩展
本版本不暴露 `report` 工具,也不提供从 child 到 parent 的内容投递或自动唤醒 parent。持久化 child 会话仍是 child 详细输出的来源
后续提案可以增加一个普通的面向模型 `report(output)` 工具;模型在一个轮次中可以调用它零次或多次。其投递策略可以区分静默注入 parent 与唤醒 parent接收方选择、确认、持久性和重试语义均与该工具一并延后决定。增加报告投递无需引入另一个激活状态或执行队列。
可选的 child 作用域 `report(output)` 工具不会改变 Activation 驻留状态,也不会增加另一条队列。它每轮可调用零次或多次,不允许指定接收方,而是推导在线的直接 parent投递采用静默注入还是唤醒 parent follow-up由部署配置选择。[report 工具 Agent Note](2026-07-30-continuable-subagent-report-tool.md)规定其权限、确认、设置贡献和投递契约
### 延后的 steering中途引导
@@ -147,7 +145,7 @@ activation-owner 作用域之所以存在,是因为普通 Cordis owner effect
本版本覆盖可继续的进程内 child一次性委派保持不变。远程提供方必须具备单独的激活 handle以及等价的认证控制与 child-first 完全停稳契约,才能支持同样的行为。
它不新增 host-user 继续执行、subagent steering 操作、报告工具、从 child 到 parent 的内容投递、自动唤醒 parent、持久化邮箱、跨进程 lease、中断 inbox 工作的自动回放、团队权限、工作流权限、公开 subagent 取消操作、公开驻留查询、新的在线激活数量或后代总数限制,以及运行时缓存。现有委派深度策略保持不变。
它不新增 host-user 继续执行、subagent steering 操作、持久化邮箱、跨进程 lease、中断 inbox 工作的自动回放、团队权限、工作流权限、公开 subagent 取消操作、公开驻留查询、新的在线激活数量或后代总数限制,以及运行时缓存。现有委派深度策略保持不变。可选的 child 到 parent 报告是后续消费该生命周期的功能,不属于基础可继续能力。
## 曾考虑的替代方案
@@ -159,7 +157,7 @@ activation-owner 作用域之所以存在,是因为普通 Cordis owner effect
**让提供方通过 Agent handle 创建、恢复 child 或投递消息。** 初始提供方只持有 `prepareContinuable()` 及其分离式创建规格这一项差异child 是全新启动,还是带有 parent 前缀。管理器必须通过私有 activation-owner 作用域自行调用 `ctx.agents.create()`,使该作用域成为每个 handle 的结构化所有者。持久化的进程内会话已经包含初始前缀及通用重建描述符,消息投递则属于 Agent inbox。让提供方持有任何后续 handle、`SubagentRun` 或消息所有权,会保留一条没有已发布行为可承载的 seam。
**现在就增加报告投递。** 可重复调用的面向模型工具与该生命周期兼容,但静默投递还是唤醒投递、接收方选择、确认、持久性和重试行为都是独立的产品决策。延后该工具,可以让首个版本专注于会话准入与驻留,又不限制后续策略
**将报告投递纳入基础生命周期。** 可重复的 child 到 parent 报告与该生命周期兼容,但静默投递还是唤醒投递、确认、持久性和重试行为都是独立的产品决策。后续的 report 包保持可选,并消费一条显式 child 设置 seam因此可继续驻留不会默认授予返回通道
**将 `SessionHeader.parentSession` 视为在线所有权。** 持久化谱系不能证明历史 parent 当前持有 child。在线 parent 的 `ownedChildren` 成员关系会记录进程内关系,而不改变持久化来源。
@@ -195,12 +193,13 @@ activation-owner 作用域之所以存在,是因为普通 Cordis owner effect
- 每个由继续执行管理器管理的 parent 激活只会在直接持有的所有 child 激活完成 `AgentHandle` dispose 后进行 dispose顶层 Agent 不加入等待图。
- Activation 最终结算会等待 `ctx.sessions.flush(child.session)`,将其作为 best-effort 屏障;它会记录 rejection但不会把 listener 参与解释为持久性证明,然后 dispose child handle 并释放 parent 所有权,使 flush 失败不会泄漏 `waiting` Activation。
- 管理器拆卸会全局关闭准入;拥有选定顶层 Agent 的宿主则只关闭这些确切身份之下的准入,直到这些根离开注册表。两者都会按确切祖先关系跟踪已获准的物化过程,为每个选中的可见 Activation 安装一个记忆化 dispose 截止点,自顶向下传播取消,按 child-first 顺序释放 handle即使个别分支失败也会等待所有选中分支之后才 dispose 对应的顶层 Agent 或管理器作用域。
- 本版本不暴露 `report` 工具,不提供从 child 到 parent 的内容投递,也不自动唤醒 parent
- 基础生命周期不暴露隐式报告行为;可选的 report 包通过 setup seam 贡献一个显式的 child 作用域工具
- 会话日志只能根据准入来源重建实际写入的消息;已被 inbox 接受但未写入日志的消息没有重启保证。
- 可继续 subagent 路径不创建或依赖 Task、`TaskId`、Task 完成通知、Task 取消或中间的带结果执行包装层。
- 单元覆盖固定 `startContinuable()` 在 inbox 接受消息时的返回边界、每条接受前和生命周期发布失败路径的完整回滚、全局和限定到 parent 作用域的 drain 都会等待夹在 Agent 发布与 Activation 注册之间的物化过程完全停稳、同级森林隔离、中间 Agent 离开注册表后的确切祖先关系、不依赖提供方的冷恢复、冷恢复物化后的最终确切 parent 再授权、接受前后两个阶段的调用方 signal 与拆卸所有权,以及已接受但未写入日志的消息不会自动回放。
- 单元覆盖固定仅由驻留状态决定的路由表、单 inbox 顺序、通过 inbox 事件关联 `MessageId`、在开放轮次期间 follow-up、等待唤醒、冷恢复、所有权注册与释放、child-first dispose、发送与 dispose 的竞争、没有 listener 和 listener 失败时的 best-effort 最终 flush以及不存在公开 subagent 取消steering 和报告工具这一事实
- 一项无密钥整套应用快照覆盖 parent 委派和 follow-up 排队、不存在 subagent steering、报告投递和自动唤醒 parent、保留等待中的 `AgentHandle` 以及 child-first dispose
- 单元覆盖固定仅由驻留状态决定的路由表、单 inbox 顺序、通过 inbox 事件关联 `MessageId`、在开放轮次期间 follow-up、等待唤醒、冷恢复、所有权注册与释放、child-first dispose、发送与 dispose 的竞争、没有 listener 和 listener 失败时的 best-effort 最终 flush以及不存在公开 subagent 取消steering。
- report 包的单元覆盖会分别固定仅 child 可见性、setup 撤销、权限、投递模式、稳定消息身份和生命周期竞争
- 一项无密钥整套应用快照覆盖 parent 委派和 follow-up 排队、不存在 subagent steering 和隐式 report 投递、保留 waiting 中的 `AgentHandle` 以及 child-first dispose。另一项 report 快照覆盖可选的显式返回通道。
### 已接受的代价
@@ -210,7 +209,7 @@ activation-owner 作用域之所以存在,是因为普通 Cordis owner effect
进程内 inbox 和所有权图无法协调两个 harness 进程。允许多个进程并发访问同一持久化存储的部署,仍需要持久化 lease 和邮箱协议。
没有报告投递时,完成 child 轮次既不会把内容发送给历史 parent也不会唤醒它。输出会保留在持久化 child 会话中,直至调用方检查该 transcript 或提交另一个经过授权的轮次。后续报告工具可以增加静默投递或唤醒投递,而无需改变激活生命周期
未安装可选 report 包时,完成 child 轮次既不会把内容发送给历史 parent也不会唤醒它。安装后,只有显式调用 `report` 才会发送选中内容;静默投递不唤醒 parent唤醒投递则会排入一个后续轮次。无论如何child 的详细输出都会保留在其持久化会话中
将每条继续执行消息排队,意味着 parent 无法立即纠正正在进行的 child 轮次;纠正操作会在下一个轮次执行。后续 UI steering 操作可以缩短该延迟,而不改变 follow-up 排序。

View File

@@ -0,0 +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 .agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.md
2026-07-30-continuable-subagent-report-tool.md: 24922cfe88084bb0f9fea8c9363980a224b875da
2026-07-30-continuable-subagent-report-tool.zh.md: bb0b1847f157dba6116526851194cf1228e52e49

View File

@@ -0,0 +1,116 @@
# Agent Note: Continuable subagent report tool
Status: implemented
English | [中文](2026-07-30-continuable-subagent-report-tool.zh.md)
## Problem
Continuable in-process subagents can receive later parent messages, retain descendants, settle, and cold-resume, but the base lifecycle gives them no way to send selected content back to their direct parent. Their complete output already remains reconstructable from the durable child Session, so the missing capability is explicit delivery rather than result storage.
Treating every final assistant message as an implicit result would conflate turn completion with reporting. A long-lived child may have nothing useful to report in one turn, may report progress several times in another, and must remain available after reporting. Recipient authority, quiet versus waking delivery, acknowledgement, durability, and retry behavior therefore need one explicit contract.
## Decision
Add the independently installed `@deepseek-ai/dsh-tool-subagent-report` package. It contributes an ordinary model-facing `report` tool to each continuable in-process child Activation. A child may call it zero or multiple times in a turn. Success neither concludes the turn, settles the Activation, nor prevents later parent follow-ups, and finishing a turn never reports automatically.
The feature is a collaboration control, not a result-bearing execution wrapper. It adds no Task, `SubagentRun`, result promise, Activation state, delivery queue, or replay path.
### Model-facing contract
`report` accepts exactly `{ output: string }` and returns exactly `{ messageId: string }`. It accepts no child id, recipient id, or delivery mode. `exec.agent` binds the tool call to the reporting child, the service derives the sole recipient from durable `parentSession`, and deployment config owns scheduling.
`messageId` is the stable `MessageId` of the user-role message accepted by the parent. It is not an `InboxItemId`: quiet delivery creates no inbox occurrence, while waking delivery creates one occurrence for the same stable message. It is also not a read receipt, parent-log acknowledgement, turn-completion receipt, or persistence flush.
The description states that reporting is explicit, repeatable, direct-parent-only, and non-terminal. It warns that a failed tool result may still follow an accepted send because a later `tools/post-execute` failure can replace the result. Without an idempotency key, stronger wording would encourage duplicate retries after ambiguous failure.
The tool uses generic rendering with no locations. Its acknowledgement includes `messageId`. Scope-local registration keeps presentation and execution aligned: roots, one-shot children, remote providers, sibling scopes, and agentless execution neither see nor execute `report`. It installs after the child's global `toolFilter`, so a delegation allow-list cannot accidentally remove the structural return channel; deployments that require no return channel omit the package.
### Service authority
The subagent seam exposes `ctx.subagents.reportFrom(child, content, { delivery, signal }): Promise<MessageId>`. The exact live child Agent is the sender credential. The continuation manager accepts only an Activation whose `handle.agent === child`, derives its direct parent from the child's durable header, and requires that id to resolve to a live parent Agent in the final synchronous authorization-and-send span. The API accepts no caller-selected recipient, ancestor, or provenance.
Roots, one-shot children, forged objects, stale Agents, and same-id replacements fail with `UNAUTHORIZED`. A closing child Activation fails with `ACTIVATION_CLOSING`; manager drain and pre-acceptance cancellation retain their existing lifecycle errors. A missing or send-rejecting direct parent fails with `PARENT_UNAVAILABLE` and `direct parent is not live; report was not delivered`. Failure returns no id, cold-resumes no parent, writes no offline mailbox, and mutates no absent-parent Session.
Nested reporting crosses exactly one edge. A grandchild reports to its direct child parent, never to the top-level coordinator. That intermediate child may explicitly report a derived update later.
### Delivery policy
The package validates `reportDelivery: 'quiet' | 'wakeup'`; the default is `quiet`.
Quiet delivery calls `parent.inject()`. It adds model-visible context without starting a parent model request: an idle parent appends before the call returns, while an admitting or running parent stages the report for the next safe log position. It creates no inbox occurrence and therefore no synthetic continuation-manager acceptance record.
Waking delivery calls `parent.followup()`. It creates one ordinary FIFO parent turn, wakes a parked parent driver, and never steers an open turn. When that parent is itself a continuable Activation, the send uses the manager's existing admission accounting so the parent cannot settle between synchronous enqueue and the admission microtask.
Both modes frame one user-role message as `Background subagent <child-id> reported:` followed by the exact `output`. Durable provenance is `{ kind: 'subagent-report', senderSessionId: child.id }`. Normal Agent ordering governs concurrent sends; the subagent layer creates no second queue.
### Acknowledgement and recovery
Success means the exact live parent synchronously accepted the message. An idle quiet injection is already appended at that boundary, while staged quiet context becomes reconstructable only when it reaches its normal log boundary. Waking delivery has an inbox occurrence whose id remains separate from the returned stable message id.
The first version provides no durable mailbox, idempotency key, delivery receipt, retry protocol, or exactly-once claim. A process failure can leave the caller uncertain, and retry after an unknown outcome may duplicate a report. The durable child transcript remains the recovery source when the parent is unavailable.
### Composition and lifecycle
The subagent seam adds `registerContinuableSetup(contribution): () => void`, backed by `SubagentActivationSetupRegistry`. Each synchronous contribution receives the unpublished child context and returns the disposer for its installation. The continuation manager first applies base child composition, then current contributions in registration order through the same setup closure used for fresh creation and cold resume.
The registry owns registration, per-child installation records, setup rollback, child-scope cleanup, and immediate revocation. A throwing or concurrently revoked contribution rejects before Activation publication and rolls back the batch. New registrations affect a resident child only on its next Activation; removing a registration first closes it to new setup and then revokes every provisioning or resident installation immediately. Registration disposal and child-context disposal are idempotent and attempt every release before aggregating failures.
This seam keeps the continuation manager unaware of tool names. The report package installs only `report`; `@deepseek-ai/dsh-tool-subagent-control` independently installs parent-side `send_message` and `list_agents`. A deployment can install either direction, both, or neither. Providers remain data-only, durable descriptors do not snapshot report availability or delivery mode, and cold resume uses the deployment's current contributions and policy.
### Snapshot coverage
The ACP snapshot harness adds `waitForSubagentTurnEnd`, selecting the Nth harvested child by the same order as `session.N.jsonl`. It waits for a closed child turn containing a request header so a continuable child's earlier descriptor-seed turn cannot satisfy the boundary. This lets the assembled quiet-mode scenario wait for the child-side report without inventing a parent-visible signal.
The authored snapshot starts a continuable child, executes the real scope-local `report` tool, confirms that the idle parent is not woken, and then submits a later parent prompt that consumes the framed report. It declares child schema pin `1`, so the otherwise non-global `report` schema is checked against `tool-schemas.1.expected.json` while the root keeps the default schema pin. The generated tool catalog separately mints a child scope to include the same scope-local schema.
## Alternatives considered
### Automatically deliver every final answer
Automatic delivery cannot represent zero reports, progress reports, or several selected updates. It also couples reporting to settlement and can duplicate content already reported explicitly.
### Always wake the parent
Waking on every report creates unsolicited turns and can cascade through nested subagents. Quiet delivery matches background coordination better as the default, while deployments that require immediate action can select wakeup.
### Let the child choose the delivery mode
Giving the model a mode argument grants it control over scheduler pressure and makes behavior deployment-dependent. The child chooses content and timing; deployment config chooses whether that content starts another Agent turn.
### Register a global tool
A global `report` would advertise an unusable capability to roots, one-shot children, remote children, and agentless callers. Execution-time rejection would make schema visibility disagree with authority.
### Combine both directions in the control package
`send_message` and `report` have different audiences, scopes, configuration, and lifecycle. Independent packages let deployments grant either direction without implying the other.
### Persist an offline parent mailbox
Mutating or cold-resuming an absent parent requires a new durable addressing, authorization, conflict, acknowledgement, and replay protocol. Requiring a live direct parent keeps the first version on the existing Agent send path.
### Reintroduce a Task or result promise
A result-bearing wrapper makes one report or one turn appear terminal and recreates the lifetime mismatch that continuable Activations removed. Explicit repeatable sends need no intermediate execution object.
## Consequences
- A continuable in-process child exposes exactly one scope-local `report` schema only while the report package's contribution is installed; unrelated Agents never expose it.
- The tool returns the parent message's stable `MessageId`. Quiet delivery has no `InboxItemId`; waking delivery has a separate inbox occurrence.
- Only the exact resident child may report, and only to the exact live direct parent derived from durable lineage. The service has no recipient parameter or offline fallback.
- Quiet delivery is the validated default and never starts a parent request. Wakeup creates exactly one later FIFO turn and never steers an open turn.
- Child cancellation or disposal after parent acceptance does not retract the report. Before acceptance, child disposal, drain, parent loss, or caller cancellation rejects the operation.
- Fresh and resumed Activations compose current setup contributions before publication. Grants wait for the next Activation; revocation is immediate for resident children.
- Unit coverage pins visibility, allow-list behavior, both delivery modes, stable identity and provenance, nested routing, invalid senders, absent parents, cancellation, drain, revocation races, and the absence of Tasks or implicit final reporting.
- The keyless assembled snapshot proves the real child tool, quiet non-wakeup behavior, durable parent framing, and later parent consumption.
### Accepted risks
The acceptance boundary is weaker than durable end-to-end delivery. A crash can leave the result ambiguous, and retries may duplicate reports.
Wakeup mode can amplify model work when nested children report frequently. Deployment ownership and a quiet default limit but do not remove that risk.
Registry presence is the parent liveness signal. A host-owned parent whose `AgentHandle.dispose()` has started but has not yet unwound its scope can still accept and append a report that it will not act on in this process. Closing that gap requires an Agent-level disposal-start signal rather than subagent-layer inference.
The final setup-revocation check runs after `ctx.agents.create()` or `ctx.agents.resume()` returns, after lower-level Agent and Session publication. Revocation in this window rolls back the handle and prevents the subagent Activation start edge but may leave a persisted Session. Moving the cutoff before lower-level publication requires a future Agent-creation setup transaction seam.

View File

@@ -0,0 +1,116 @@
# Agent Noteagent 决策记录):可继续 subagent 报告工具
Status: implemented
[English](2026-07-30-continuable-subagent-report-tool.md) | 中文
## 问题
可继续的进程内 subagent 能够接收 parent 后续发来的消息、保留后代、结算并冷恢复,但基础生命周期无法让它们将选中内容发送给直接 parent。child 的完整输出已可从持久化会话中重建,因此缺失的能力是显式投递,而非结果存储。
如果将每条 assistant 最终消息都视为隐式结果,就会混淆轮次完成与报告。长期运行的 child 可能在某个轮次中无内容可报告,也可能在另一个轮次多次报告进展,而且报告后必须仍可继续工作。因此,接收方权限、静默投递与唤醒投递、确认、持久性和重试行为都需要一份显式契约。
## 决策
新增可独立安装的 `@deepseek-ai/dsh-tool-subagent-report`package。它会向每个可继续进程内 child Activation 贡献一个普通的面向模型 `report` 工具。child 在一个轮次中可调用零次或多次。调用成功既不会结束该轮次或结算 Activation也不会阻止 parent 之后继续 follow-up完成轮次也绝不会自动报告。
该功能是协作控制,不是承载结果的执行包装层。它不新增 Task、`SubagentRun`、结果 promise、Activation 状态、投递队列或回放路径。
### 面向模型的契约
`report` 只接受 `{ output: string }`,也只返回 `{ messageId: string }`。它不接受 child id、接收方 id 或投递模式。`exec.agent` 将工具调用绑定到发送报告的 child服务从持久化 `parentSession` 中推导唯一接收方,调度则由部署配置决定。
`messageId` 是 parent 接受的用户角色消息所对应的稳定 `MessageId`。它不是 `InboxItemId`:静默投递不创建 inbox 条目实例唤醒投递则会为同一条稳定消息创建一个条目实例。它也不是已读回执、parent 日志确认、轮次完成回执或持久化 flush。
工具描述会明确报告操作是显式、可重复、仅限直接 parent 且不会结束轮次的。它还会警告:发送被接受后,后续 `tools/post-execute` 失败可能替换工具结果,因此工具结果失败时内容仍可能已经送达。没有幂等键时,更强的表述会诱导调用方在结果不明确的失败后重复重试。
该工具使用不带 location 的通用渲染,其确认中包含 `messageId`。作用域局部注册使呈现与执行保持一致root、one-shot child、远程提供方、同级作用域和无 agent 执行既不能看到,也不能执行 `report`。它会在 child 的全局 `toolFilter` 之后安装,因此委派 allow-list 不会意外移除这条结构性返回通道;不需要返回通道的部署不安装该包。
### 服务权限
subagent seam 暴露 `ctx.subagents.reportFrom(child, content, { delivery, signal }): Promise<MessageId>`。确切的在线 child Agent 是发送方凭据。继续执行管理器只接受 `handle.agent === child` 的 Activation从 child 的持久化 header 中推导其直接 parent并要求该 id 在最终的同步授权与发送区间解析为一个在线 parent Agent。该 API 不接受由调用方选择的接收方、祖先或来源信息。
root、one-shot child、伪造对象、陈旧 Agent 和同 id 替换对象都以 `UNAUTHORIZED` 失败。正在关闭的 child Activation 以 `ACTIVATION_CLOSING` 失败;管理器 drain 和接受前取消保留既有的生命周期错误。直接 parent 不存在或拒绝接受时,以 `PARENT_UNAVAILABLE``direct parent is not live; report was not delivered` 失败。失败不返回 id不冷恢复 parent不写入离线邮箱也不会修改缺失 parent 的会话。
嵌套报告恰好跨越一条边。grandchild 会向其直接 child parent 报告,绝不会直接向顶层 coordinator 报告。中间 child 可以稍后显式报告自己归纳的更新。
### 投递策略
该包会校验 `reportDelivery: 'quiet' | 'wakeup'`,默认值为 `quiet`
静默投递调用 `parent.inject()`。它会添加模型可见上下文,但不启动 parent 模型请求:若 parent 空闲,则在调用返回前追加消息;若 parent 正在准入或运行,则暂存报告,留到下一个安全日志位置。该模式不创建 inbox 条目实例,因此也不会产生虚构的继续执行管理器接受记录。
唤醒投递调用 `parent.followup()`。它会创建一个普通的 FIFO parent 轮次,唤醒已驻留的 parent driver且绝不 steering 已开始的轮次。当该 parent 本身也是可继续 Activation 时,发送会使用管理器现有的准入计数,防止 parent 在同步入队与准入微任务之间结算。
两种模式都会将一条用户角色消息封装为 `Background subagent <child-id> reported:`,后面跟随完全原样的 `output`。持久化来源信息为 `{ kind: 'subagent-report', senderSessionId: child.id }`。并发发送的顺序由 Agent 的常规规则决定subagent 层不会创建第二条队列。
### 确认与恢复
成功表示确切的在线 parent 已同步接受该消息。空闲 parent 在接受静默注入时已经完成追加,而暂存的静默上下文只有到达正常日志边界后才可重建。唤醒投递包含一个 inbox 条目实例,其 id 与返回的稳定消息 id 保持分离。
首个版本不提供持久化邮箱、幂等键、投递回执、重试协议或恰好一次保证。进程故障可能让调用方无法确定结果在结果未知时重试则可能重复报告。parent 不可用时,持久化 child transcript 仍是恢复来源。
### 组合与生命周期
subagent seam 新增 `registerContinuableSetup(contribution): () => void`,由 `SubagentActivationSetupRegistry` 支撑。每个同步贡献都会接收尚未发布的 child 上下文,并返回其安装的 disposer。继续执行管理器首先应用基础 child 组合,然后通过同一个用于首次创建与冷恢复的设置闭包,按注册顺序应用当前贡献。
注册表负责注册、每个 child 的安装记录、设置回滚、child 作用域清理和立即撤销。某项贡献抛出异常或被并发撤销时,会在 Activation 发布前拒绝操作并回滚该批次。新注册项只会在驻留 child 的下一个 Activation 生效;移除注册项时,会先将它对新设置关闭,再立即撤销为正在配置或驻留的每个 child 安装的实例。注册 dispose 与 child 上下文 dispose 都是幂等的,两者都会先尝试每项释放,再聚合失败。
该 seam 使继续执行管理器无需知道工具名。report 包只安装 `report``@deepseek-ai/dsh-tool-subagent-control` 则独立安装 parent 侧的 `send_message``list_agents`。部署时可安装任一方向、同时安装两者或两者均不安装。提供方仍只负责数据,持久化描述符不会对 report 可用性或投递模式建立快照,冷恢复则使用部署当前的贡献与策略。
### 快照覆盖
ACPAgent Client Protocol快照 harness 新增 `waitForSubagentTurnEnd`,按与 `session.N.jsonl` 相同的顺序选择第 N 个已收集 child。它会等待一个包含请求 header 的已闭合 child 轮次,以防可继续 child 早期播种描述符的轮次错误满足该边界。这样,整体组装的静默模式场景无需伪造 parent 可见信号,就能等待 child 侧报告。
手写快照会启动一个可继续 child执行真实的作用域局部 `report` 工具,确认空闲 parent 未被唤醒,然后提交一条后续 parent 提示词,使其消费封装后的报告。它声明 child schema pin `1`,因此本不属于全局的 `report` schema 会与 `tool-schemas.1.expected.json` 比对root 则继续使用默认 schema pin。生成的工具目录会另外铸造一个 child 作用域,以收录同一个作用域局部 schema。
## 曾考虑的替代方案
### 自动投递每个最终回答
自动投递无法表示零次报告、进展报告或多次精选更新。它还会将报告与结算耦合,并可能重复投递已显式报告的内容。
### 始终唤醒 parent
每次报告都唤醒 parent 会产生未经请求的轮次,还可能沿嵌套 subagent 级联扩散。静默投递更适合作为后台协调的默认值,而需要立即处理的部署可选择 wakeup。
### 允许 child 选择投递模式
向模型提供 mode 参数会赋予其控制调度器压力的能力并使行为依赖部署。child 只决定内容和时机;该内容是否启动另一个 Agent 轮次,由部署配置决定。
### 注册全局工具
全局 `report` 会向 root、one-shot child、远程 child 和无 agent 调用方公布一项无法使用的能力。到执行时才拒绝,会使 schema 可见性与权限不一致。
### 将两个方向合并到 control 包
`send_message``report` 的受众、作用域、配置和生命周期各不相同。独立的包可让部署授予任意一个方向,而不暗示也授予另一个方向。
### 持久化离线 parent 邮箱
修改或冷恢复不在线的 parent需要一套新的持久化寻址、权限、冲突、确认和回放协议。要求直接 parent 在线,可以让首个版本继续使用现有 Agent 发送路径。
### 重新引入 Task 或结果 promise
承载结果的包装层会让一次报告或一个轮次看似具有终止性,并重新引入可继续 Activation 已经移除的生命周期不匹配。显式、可重复的发送无需中间执行对象。
## 影响
- 只有安装 report 包贡献时,可继续进程内 child 才会恰好暴露一个作用域局部 `report` schema无关 Agent 永远不会暴露该 schema。
- 工具返回 parent 消息的稳定 `MessageId`。静默投递没有 `InboxItemId`;唤醒投递会产生一个单独的 inbox 条目实例。
- 只有确切的驻留 child 才能报告,且只能报告给根据持久化谱系推导的确切在线直接 parent。服务不接受接收方参数也不提供离线 fallback。
- 静默投递是校验后的默认模式,绝不会启动 parent 请求。wakeup 会恰好创建一个后续 FIFO 轮次,绝不 steering 已开始的轮次。
- parent 接受后取消或 dispose child 不会撤回报告。接受前child dispose、drain、parent 丢失或调用方取消都会拒绝操作。
- 新建和恢复的 Activation 都会在发布前组合当前设置贡献。新授权等待下一个 Activation 才生效,而已驻留 child 的授权撤销立即生效。
- 单元覆盖固定可见性、allow-list 行为、两种投递模式、稳定身份与来源信息、嵌套路由、无效发送方、缺失的 parent、取消、drain、撤销竞争以及不存在 Task 或隐式最终报告。
- 无密钥整体组装快照证明真实 child 工具、静默且不唤醒的行为、持久化 parent 封装,以及 parent 后续消费。
### 已接受的风险
该接受边界弱于持久化端到端投递。崩溃可能导致结果不明,重试则可能重复报告。
wakeup 模式可能在嵌套 child 频繁报告时放大模型工作量。由部署所有者控制并默认静默,可以限制该风险,但无法完全消除。
注册表中的存在性就是 parent 在线信号。宿主拥有的 parent 如果已开始 `AgentHandle.dispose()` 但尚未展开其作用域,仍可能接受并追加一条本进程不会再处理的报告。要弥合这个缺口,需要 Agent 层面的 dispose 开始信号,不能由 subagent 层推断。
最终 setup 撤销检查发生在 `ctx.agents.create()``ctx.agents.resume()` 返回之后,此时底层 Agent 和 Session 已经发布。在该窗口内撤销会回滚 handle并阻止 subagent Activation 的 start 边,但可能留下持久化 Session。若要把截止点移到底层发布之前需要未来提供 Agent 创建 setup 事务 seam。