diff --git a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.i18n.yaml index 385a4b6621..436e346da5 100644 --- a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.i18n.yaml @@ -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/architecture/2026-08-10-fork-children-stay-one-shot.md -2026-08-10-fork-children-stay-one-shot.md: 10e2607e55b67671519fd18c26d0ae8fc0ca7268 -2026-08-10-fork-children-stay-one-shot.zh.md: d9a5561ed6fb8a32ca6c47553a49e2948eb59256 +2026-08-10-fork-children-stay-one-shot.md: 44b947a3e0580263f1973aaf24534b7b2f01c0b6 +2026-08-10-fork-children-stay-one-shot.zh.md: 4a9e5ab10ba1f437e4a8caf6c08f08412113c23d diff --git a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md index 10e2607e55..44b947a3e0 100644 --- a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md +++ b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md @@ -42,7 +42,7 @@ The reintroduction condition is recorded as a `TODO(fork-continuable-prefix-reus - A forked child's request prefix stays byte-identical to its parent's unless the deployment configures `persona` or `toolFilter` on the fork delegation tool, so the token cost of seeding buys provider-side reuse again. - The fork provider's continuable path has no production caller and no assembled-composition coverage. It keeps its package-level tests, and the seam still accepts it, so a bundle or `--patch` overlay can reintroduce it with no code change and no warning. - `subagent_fork`'s model-visible schema changes: the continuable background wording is replaced by the one-shot task wording in the base bundle, and disappears entirely from the two examples. The affected keyless snapshot tool-schema sidecars are re-recorded in the same change. -- The report obligation's reach narrows to spawned children in shipped deployments. Its default `wakeup` scheduling, authority model, and coverage are unchanged. +- The report obligation's reach narrows to spawned children in shipped deployments. Its default `next-step` scheduling, authority model, and coverage remain independent of fork composition. ### Accepted risks diff --git a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md index d9a5561ed6..4a9e5ab10b 100644 --- a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md @@ -42,7 +42,7 @@ one-shot child——前台与后台皆然——经由 `SubagentRuntime.start()` - 除非部署在 fork 委派工具上配置了 `persona` 或 `toolFilter`,fork child 的请求前缀与其 parent 逐字节相同,因此初始内容的 token 成本重新换来了提供方侧的复用。 - fork 提供方的可继续路径没有生产调用方,也没有整体组装层面的覆盖。它保留自己的包内测试,seam 也仍然接受它,因此某个组合包或 `--patch` 覆盖层可以无需改动代码、也不会有任何警告地把它重新引入。 - `subagent_fork` 面向模型的 schema 发生变化:base 组合包中可继续的后台措辞被 one-shot 的 task 措辞取代,在两个示例中则完全消失。受影响的无密钥快照工具 schema 伴随文件在同一次改动中重新记录。 -- 在随附部署中,report 义务的覆盖范围收窄到 spawn 出的 child。它的 `wakeup` 默认调度、权限模型与覆盖均保持不变。 +- 在随附部署中,report 义务的覆盖范围收窄到 spawn 出的 child。它的 `next-step` 默认调度、权限模型与覆盖仍独立于 fork 组合。 ### 已接受的风险 diff --git a/.agents/notes/implemented/bug-fix/2026-08-17-subagent-report-settlement-ordering.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-17-subagent-report-settlement-ordering.i18n.yaml new file mode 100644 index 0000000000..a43c34d0d1 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-17-subagent-report-settlement-ordering.i18n.yaml @@ -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/bug-fix/2026-08-17-subagent-report-settlement-ordering.md +2026-08-17-subagent-report-settlement-ordering.md: 30dfab5e96a7cea2ef6d4f03f480d17a86c5e775 +2026-08-17-subagent-report-settlement-ordering.zh.md: 658eb18e3a8cb40734136af32c6c62faef066a6e diff --git a/.agents/notes/implemented/bug-fix/2026-08-17-subagent-report-settlement-ordering.md b/.agents/notes/implemented/bug-fix/2026-08-17-subagent-report-settlement-ordering.md new file mode 100644 index 0000000000..30dfab5e96 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-17-subagent-report-settlement-ordering.md @@ -0,0 +1,44 @@ +# Agent Note: Subagent reports precede their settlement notices + +Status: implemented + +English | [中文](2026-08-17-subagent-report-settlement-ordering.zh.md) + +## Problem + +A continuable child can explicitly report selected content and later produce an unconditional manager-authored settlement notice. Report delivery used `Agent.followup()` and entered the parent's `next-turn` queue, while settlement delivery to a running parent used `Agent.steer()` and entered `next-step`. The first step of a turn claims the complete `next-step` batch before one `next-turn` message, so the later settlement notice could reach the model before the earlier report. The assembled report scenario required `reportDelivery: quiet` to avoid that nondeterministic interleaving. [Issue #2600](https://github.com/deepseek-harness/deepseek-harness/issues/2600) records the defect. + +The report tool tells a child to report whenever a finding changes what its parent should do next. Deferring that message to a later turn contradicted the tool's scheduling meaning and separated causally ordered messages across queues with different claim priority. + +## Decision + +`SubagentReportDelivery` is `'quiet' | 'next-step'`, and `next-step` is the default. Next-step delivery calls `parent.steer()`, so a running parent reads the report at its nearest safe step boundary and an idle parent starts a turn. Quiet delivery continues to call `parent.inject()` and enters the same queue without waking an idle parent. + +The continuation manager retains `sendWaking()` and `admitWaking()` around next-step reports delivered to resident continuable parents. Their purpose is waking-send admission accounting, independent of whether the message targets a step or a turn: the receiving Activation remains live between synchronous inbox insertion and the microtask that observes the wake. + +### Ordering across parent states + +A running parent receives an accepted report and the child's later settlement notice in the same `next-step` FIFO. If the parent becomes idle before settlement arrives, it has already claimed the report; settlement may then open a later turn without reversing the observed order. + +During parent maintenance, the report occupies `next-step` and latches a wake, while settlement may occupy `next-turn` because maintenance reports idle status. The initial claim still takes next-step input before the queued turn. Waking input submitted after cancellation is redirected by `Agent.send()` to `next-turn`, so report and settlement follow the core agent's cancellation convergence rather than bypassing it. + +### Verification + +The report package holds a parent inside an active model request, submits a child report, settles that child, and asserts the pending parent batch is ordered `subagent-report`, then `subagent-settled`, with no queued later turn. Separate coverage pins repeated reports as one FIFO next-step batch, idle-parent wakeup, and waking admission accounting for a continuable parent. + +The assembled ACP report scenario uses the shipped default. Its scheduling fence keeps the child behind the parent's delegation turn and holds the parent in maintenance until settlement follows the report. The report latches the wake while the settlement notice queues a turn; when maintenance ends, the parent claims next-step input before next-turn input and observes both notices in causal order without a quiet-delivery overlay. + +## Alternatives considered + +**Keep the `wakeup` name but change its implementation to `steer()`.** The existing public description defined `wakeup` as one later parent turn. Reusing the value for a different inbox target would leave configuration unable to state the behavior it selects. The pre-release configuration instead names `next-step` directly. + +**Expose `quiet | next-step | next-turn`.** A next-turn report still permits a later next-step settlement notice to overtake it. Preserving report-before-settlement would require a cross-queue ordering barrier, and no current deployment requires next-turn isolation strongly enough to own that mechanism. + +**Move settlement notices to `next-turn`.** Settlement batching deliberately uses the next-step queue so several children finishing together cost one parent step instead of one turn each. Moving settlement would increase latency and model work to retain a report scheduling mode with no current consumer. + +## Consequences + +- A report may extend an open parent turn. It never interrupts the active model request or tool execution; the agent loop admits it only at a step boundary. +- Reports accepted together share one next-step batch, preserving FIFO order and reducing the turn amplification of the former one-turn-per-report behavior. +- The `wakeup` configuration value is rejected rather than retained as an alias. This repository has no external pre-release compatibility promise for Cordis configuration. +- `quiet` remains the deployment escape for reports that must not wake a parked parent, with the existing risk that no model reads them until another waking input arrives. diff --git a/.agents/notes/implemented/bug-fix/2026-08-17-subagent-report-settlement-ordering.zh.md b/.agents/notes/implemented/bug-fix/2026-08-17-subagent-report-settlement-ordering.zh.md new file mode 100644 index 0000000000..658eb18e3a --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-17-subagent-report-settlement-ordering.zh.md @@ -0,0 +1,44 @@ +# Agent Note: Subagent report 先于其结算通知 + +Status: implemented + +[English](2026-08-17-subagent-report-settlement-ordering.md) | 中文 + +## 问题 + +可继续 child 可以显式上报选中内容,之后还会产生一条由管理器撰写且无条件投递的结算通知。报告投递曾使用 `Agent.followup()` 并进入 parent 的 `next-turn` 队列,而面向运行中 parent 的结算投递使用 `Agent.steer()` 并进入 `next-step`。一个轮次的第一个 step 会先领取完整 `next-step` 批次,再领取一条 `next-turn` 消息,因此较晚的结算通知可能先于较早的报告到达模型。整体组装的报告场景必须使用 `reportDelivery: quiet`,才能避开这种不确定交错。[Issue #2600](https://github.com/deepseek-harness/deepseek-harness/issues/2600)记录了该缺陷。 + +report 工具要求 child 在发现会改变 parent 下一步动作的信息时上报。把这条消息推迟到后续轮次,既违背了工具的调度含义,也让具有因果顺序的消息分散到领取优先级不同的队列中。 + +## 决策 + +`SubagentReportDelivery` 为 `'quiet' | 'next-step'`,默认值为 `next-step`。Next-step 投递调用 `parent.steer()`,因此运行中的 parent 会在最近的安全 step 边界读取报告,空闲 parent 则会启动一个轮次。静默投递继续调用 `parent.inject()`,进入同一队列但不唤醒空闲 parent。 + +对于投递到驻留可继续 parent 的 next-step 报告,继续执行管理器会保留外围的 `sendWaking()` 与 `admitWaking()`。它们负责唤醒发送的准入记账,与消息面向 step 还是 turn 无关:接收方 Activation 在同步插入 inbox 与观察该唤醒的微任务之间保持在线。 + +### 不同 parent 状态下的顺序 + +运行中的 parent 会在同一个 `next-step` FIFO 中接收已接受的报告和该 child 稍后的结算通知。若 parent 在结算到达前变为空闲,它已经领取了报告;结算随后可以开启一个更晚的轮次,而不会反转观察顺序。 + +parent 处于 maintenance 时,报告占据 `next-step` 并锁存一次唤醒,而结算可能因为 maintenance 呈现空闲状态而占据 `next-turn`。首次领取仍会先取 next-step 输入,再取排队轮次。取消后提交的唤醒输入会由 `Agent.send()` 重定向到 `next-turn`,因此报告和结算会遵循核心 agent 的取消收敛,而不会绕过它。 + +### 验证 + +report 包把 parent 保持在一个活动模型请求中,提交 child 报告,再让该 child 结算,并断言等待中的 parent 批次按 `subagent-report`、`subagent-settled` 排序,且没有排队的后续轮次。独立覆盖还会固定重复报告形成一个 FIFO next-step 批次、空闲 parent 唤醒,以及可继续 parent 的唤醒准入记账。 + +整体组装的 ACP 报告场景使用随附默认值。调度围栏让 child 等到 parent 的委派轮次之后,并让 parent 保持 maintenance,直至结算跟在报告之后到达。报告会锁存唤醒,结算通知则排入后续轮次;maintenance 结束时,parent 先领取 next-step 输入、再领取 next-turn 输入,因此无需静默投递 overlay 也能按因果顺序观察两条通知。 + +## 备选方案 + +**保留 `wakeup` 名称,但把其实现改为 `steer()`。** 既有公开描述把 `wakeup` 定义为一个后续 parent 轮次。让该值复用于不同的 inbox 目标,会使配置无法准确说明自己选择的行为。预发布配置因此直接使用 `next-step` 名称。 + +**暴露 `quiet | next-step | next-turn`。** Next-turn 报告仍可能被稍后的 next-step 结算通知超越。要保住报告先于结算,需要跨队列顺序屏障;当前没有任何部署对 next-turn 隔离的需求强到足以承担该机制。 + +**把结算通知移到 `next-turn`。** 结算批处理刻意使用 next-step 队列,使多个一起结束的 child 只花费 parent 的一个 step,而不是各自一个轮次。移动结算会增加延迟和模型工作量,只为保留一个没有当前消费方的报告调度模式。 + +## 后果 + +- 报告可能延长已打开的 parent 轮次。它绝不会打断活动模型请求或工具执行;agent loop 只会在 step 边界准入它。 +- 一起接受的报告会共享一个 next-step 批次,保持 FIFO 顺序,并减少原先每份报告各占一个轮次所造成的轮次放大。 +- `wakeup` 配置值会被拒绝,而不是保留为别名。本仓库对预发布 Cordis 配置不作外部兼容承诺。 +- 对于不得唤醒停驻 parent 的报告,`quiet` 仍是部署退路,同时保留既有风险:在另一条唤醒输入到达之前,没有模型会读取这些报告。 diff --git a/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.i18n.yaml b/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.i18n.yaml index 00ab095a71..145c22563f 100644 --- a/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.i18n.yaml @@ -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: f00b3c8bf4da08363ca0b46ddda581811fbda214 -2026-07-28-continuable-subagent-conversations.zh.md: 6e1d279db8fec6cfb08371275a4a1fe97e9d7da0 +2026-07-28-continuable-subagent-conversations.md: f456bacbf775bf914b47051e19639811e2385f65 +2026-07-28-continuable-subagent-conversations.zh.md: b7f2080b157285e5928022b4ef9b9bf411c70191 diff --git a/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md b/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md index f00b3c8bf4..f456bacbf7 100644 --- a/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md +++ b/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md @@ -157,7 +157,7 @@ It adds no host-user continuation, subagent steering operation, durable mailbox, **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 retain provider ownership with no shipped behavior to justify it. -**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 hook, so continuable residency does not silently grant a return channel. +**Make report delivery part of the base lifecycle.** Repeatable child-to-parent reporting is compatible with this lifecycle, but quiet versus next-step delivery, acknowledgement, durability, and retry behavior are independent product choices. The later report package remains optional and consumes an explicit child-setup hook, so continuable residency does not silently grant a return channel. **Treat `SessionHeader.parentSession` as live ownership.** Durable lineage does not prove that the recorded parent currently owns the child. Membership in the live parent's `ownedChildren` records the process-local relationship without changing the durable parent id. @@ -209,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 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. +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 next-step delivery wakes it and joins its nearest step boundary. 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. diff --git a/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.zh.md b/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.zh.md index 6e1d279db8..b7f2080b15 100644 --- a/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.zh.md +++ b/.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.zh.md @@ -157,7 +157,7 @@ activation-owner 作用域之所以存在,是因为普通 Cordis owner effect **让提供方通过 Agent handle 创建、恢复 child 或投递消息。** 初始提供方只持有 `prepareContinuable()` 及其分离式创建规格这一项差异:child 是全新启动,还是带有 parent 前缀。管理器必须通过私有 activation-owner 作用域自行调用 `ctx.agents.create()`,使该作用域成为每个 handle 的结构化所有者。持久化的进程内会话已经包含初始前缀及通用重建描述符,消息投递则属于 Agent inbox。让提供方持有任何后续 handle、`SubagentRun` 或消息所有权,会让提供方保留所有权,却没有已发布行为需要它。 -**将报告投递纳入基础生命周期。** 可重复的 child 到 parent 报告与该生命周期兼容,但静默投递还是唤醒投递、确认、持久性和重试行为都是独立的产品决策。后续的 report 包保持可选,并消费一个显式的 child 设置钩子,因此可继续驻留不会默认授予返回通道。 +**将报告投递纳入基础生命周期。** 可重复的 child 到 parent 报告与该生命周期兼容,但静默投递还是 next-step 投递、确认、持久性和重试行为都是独立的产品决策。后续的 report 包保持可选,并消费一个显式的 child 设置钩子,因此可继续驻留不会默认授予返回通道。 **将 `SessionHeader.parentSession` 视为在线所有权。** 持久化谱系不能证明已记录的 parent 当前持有 child。在线 parent 的 `ownedChildren` 成员关系会记录进程内关系,而不改变持久化 parent id。 @@ -209,7 +209,7 @@ activation-owner 作用域之所以存在,是因为普通 Cordis owner effect 进程内 inbox 和所有权图无法协调两个 harness 进程。允许多个进程并发访问同一持久化存储的部署,仍需要持久化 lease 和邮箱协议。 -未安装可选 report 包时,完成 child 轮次既不会把内容发送给历史 parent,也不会唤醒它。安装后,只有显式调用 `report` 才会发送选中内容;静默投递不唤醒 parent,唤醒投递则会排入一个后续轮次。无论如何,child 的详细输出都会保留在其持久化会话中。 +未安装可选 report 包时,完成 child 轮次既不会把内容发送给历史 parent,也不会唤醒它。安装后,只有显式调用 `report` 才会发送选中内容;静默投递不唤醒 parent,next-step 投递则会唤醒它并加入最近的 step 边界。无论如何,child 的详细输出都会保留在其持久化会话中。 将每条继续执行消息排队,意味着 parent 无法立即纠正正在进行的 child 轮次;纠正操作会在下一个轮次执行。后续 UI steering 操作可以缩短该延迟,而不改变 follow-up 排序。 diff --git a/.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.i18n.yaml b/.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.i18n.yaml index 7b2c3e6523..031760f25d 100644 --- a/.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.i18n.yaml @@ -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-30-continuable-subagent-report-tool.md -2026-07-30-continuable-subagent-report-tool.md: f120e65facc9c2549afd8aed589c6fc54921ef99 -2026-07-30-continuable-subagent-report-tool.zh.md: f35764cc82896f23ff50b07790078f125763f5c0 +2026-07-30-continuable-subagent-report-tool.md: 07d17f18f318a86070d9b8612512fa3c2a3815e2 +2026-07-30-continuable-subagent-report-tool.zh.md: 3d9947f2c12c6d2e34633113b67051894cac9f75 diff --git a/.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.md b/.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.md index f120e65fac..07d17f18f3 100644 --- a/.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.md +++ b/.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.md @@ -8,7 +8,7 @@ English | [中文](2026-07-30-continuable-subagent-report-tool.zh.md) 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. +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 next-step delivery, acknowledgement, durability, and retry behavior therefore need one explicit contract. ## Decision @@ -20,7 +20,7 @@ The feature is a collaboration control, not a result-bearing execution wrapper. `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. +`messageId` is the stable `MessageId` of the user-role message accepted into the parent's inbox. It is not a read receipt, parent-log acknowledgement, turn-completion receipt, or persistence flush. The description states that reporting is required before finishing, 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. @@ -36,17 +36,17 @@ Nested reporting crosses exactly one edge. A grandchild reports to its direct ch ### Delivery policy -The package validates `reportDelivery: 'quiet' | 'wakeup'`; the default is `wakeup` ([why the default reversed](2026-08-06-continuable-child-report-obligation.md)). +The package validates `reportDelivery: 'quiet' | 'next-step'`; the default is `next-step` ([ordering decision](../bug-fix/2026-08-17-subagent-report-settlement-ordering.md)). -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. +Quiet delivery calls `parent.inject()`. It adds model-visible next-step context without waking an idle parent; a running parent stages the report for the next safe log position. -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. +Next-step delivery calls `parent.steer()`. It wakes a parked parent and joins a running parent's nearest step boundary. When that parent is itself a continuable Activation, the send uses the manager's existing admission accounting so the parent cannot settle between synchronous inbox insertion and the admission microtask. Reports share the next-step FIFO with a later settlement notice, preserving their accepted causal order. Both modes frame one user-role message as `Background subagent reported:` followed by the exact `output`. The durable message source 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. +Success means the exact live parent synchronously accepted the message. The context becomes reconstructable only when it reaches its normal log boundary; a next-step delivery has woken the parent, while quiet delivery may remain pending. The inbox message 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. @@ -62,7 +62,7 @@ This seam keeps the continuation manager unaware of tool names. The report packa 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 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, observes the one ordinary parent turn the default waking delivery creates, and then submits a later parent prompt that consumes the framed report. It declares child pins `1`, so the otherwise non-global `report` schema and the child's own prompt are checked against `tool-schemas.1.expected.json` and `system-prompt.1.expected.md` while the root keeps the class pins. The generated tool catalog separately mints a child scope to include the same scope-local schema. +The authored snapshot starts a continuable child, executes the real scope-local `report` tool, and observes default next-step delivery before the manager's later settlement notice. A snapshot-only maintenance fence holds the parent until both messages are pending, proving next-step input is claimed before queued next-turn input when the parent resumes. It declares child pins `1`, so the otherwise non-global `report` schema and the child's own prompt are checked against `tool-schemas.1.expected.json` and `system-prompt.1.expected.md` while the root keeps the class pins. The generated tool catalog separately mints a child scope to include the same scope-local schema. ## Alternatives considered @@ -76,7 +76,7 @@ Waking on every report creates unsolicited turns and can cascade through nested ### 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. +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 wakes the parent. ### Register a global tool @@ -101,18 +101,18 @@ A post-creation revocation check can reject the Activation only after the Agent ## 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. +- The tool returns the parent message's stable `MessageId`; its inbox occurrence is not a separate public identity. - 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. -- Waking delivery is the validated default: it creates exactly one later FIFO turn and never steers an open turn. Quiet delivery never starts a parent request. +- Next-step delivery is the validated default: it wakes an idle parent or extends a running parent's turn at the nearest step boundary. Quiet delivery never wakes an idle parent. - 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 message and sender identities, nested routing, invalid senders, absent parents, cancellation, drain, revocation races, and the absence of Jobs or implicit final reporting. -- The keyless assembled snapshot proves the real child tool, the one waking parent turn, durable parent framing, and later parent consumption. +- The keyless assembled snapshot proves the real child tool, default next-step ordering before settlement, and durable parent framing. ### 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. -Waking delivery can amplify model work when nested children report frequently. Deployment ownership through `reportDelivery` bounds but does not remove that risk. +Next-step delivery can amplify model work when nested children report frequently. Reports waiting together share one step, and deployment ownership through `reportDelivery` bounds but does 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. diff --git a/.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.zh.md b/.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.zh.md index f35764cc82..3d9947f2c1 100644 --- a/.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.zh.md +++ b/.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.zh.md @@ -8,7 +8,7 @@ Status: implemented 可继续的进程内 subagent 能够接收 parent 后续发来的消息、保留后代、结算并冷恢复,但基础生命周期无法让它们将选中内容发送给直接 parent。child 的完整输出已可从持久化会话中重建,因此缺失的能力是显式投递,而非结果存储。 -如果将每条 assistant 最终消息都视为隐式结果,就会混淆轮次完成与报告。长期运行的 child 可能在某个轮次中无内容可报告,也可能在另一个轮次多次报告进展,而且报告后必须仍可继续工作。因此,接收方权限、静默投递与唤醒投递、确认、持久性和重试行为都需要一份显式约定。 +如果将每条 assistant 最终消息都视为隐式结果,就会混淆轮次完成与报告。长期运行的 child 可能在某个轮次中无内容可报告,也可能在另一个轮次多次报告进展,而且报告后必须仍可继续工作。因此,接收方权限、静默投递与 next-step 投递、确认、持久性和重试行为都需要一份显式约定。 ## 决策 @@ -20,7 +20,7 @@ Status: implemented `report` 只接受 `{ output: string }`,也只返回 `{ messageId: string }`。它不接受 child id、接收方 id 或投递模式。`exec.agent` 将工具调用绑定到发送报告的 child;服务从持久化 `parentSession` 中推导唯一接收方,调度则由部署配置决定。 -`messageId` 是 parent 接受的用户角色消息所对应的稳定 `MessageId`。它不是 `InboxItemId`:静默投递不创建 inbox 条目实例,唤醒投递则会为同一条稳定消息创建一个条目实例。它也不是已读回执、parent 日志确认、轮次完成回执或持久化 flush。 +`messageId` 是已接受进入 parent inbox 的用户角色消息所对应的稳定 `MessageId`。它不是已读回执、parent 日志确认、轮次完成回执或持久化 flush。 工具描述会明确报告操作在结束前必须执行、可重复、仅限直接 parent 且不会结束轮次。它还会警告:发送被接受后,后续 `tools/post-execute` 失败可能替换工具结果,因此工具结果失败时内容仍可能已经送达。没有幂等键时,更强的表述会诱导调用方在结果不明确的失败后重复重试。 @@ -36,17 +36,17 @@ root、one-shot child、伪造对象、陈旧 Agent 和同 id 替换对象都以 ### 投递策略 -该包会校验 `reportDelivery: 'quiet' | 'wakeup'`,默认值为 `wakeup`(见[默认值反转的理由](2026-08-06-continuable-child-report-obligation.md))。 +该包会校验 `reportDelivery: 'quiet' | 'next-step'`,默认值为 `next-step`(见[顺序决策](../bug-fix/2026-08-17-subagent-report-settlement-ordering.md))。 -静默投递调用 `parent.inject()`。它会添加模型可见上下文,但不启动 parent 模型请求:若 parent 空闲,则在调用返回前追加消息;若 parent 正在准入或运行,则暂存报告,留到下一个安全日志位置。该模式不创建 inbox 条目实例,因此也不会产生虚构的继续执行管理器接受记录。 +静默投递调用 `parent.inject()`。它会添加模型可见的 next-step 上下文,但不唤醒空闲 parent;运行中的 parent 会把报告暂存到下一个安全日志位置。 -唤醒投递调用 `parent.followup()`。它会创建一个普通的 FIFO parent 轮次,唤醒已停驻的 parent driver,且绝不 steering(中途引导)已开始的轮次。当该 parent 本身也是可继续 Activation 时,发送会使用管理器现有的准入计数,防止 parent 在同步入队与准入微任务之间结算。 +Next-step 投递调用 `parent.steer()`。它会唤醒停驻的 parent,并加入运行中 parent 最近的 step 边界。当该 parent 本身也是可继续 Activation 时,发送会使用管理器现有的准入记账,防止 parent 在同步插入 inbox 与准入微任务之间结算。报告与稍后的结算通知共享 next-step FIFO,从而保持其被接受时的因果顺序。 两种模式都会将一条用户角色消息封装为 `Background subagent reported:`,后面跟随完全原样的 `output`。持久化消息来源为 `{ kind: 'subagent-report', senderSessionId: child.id }`。并发发送的顺序由 Agent 的常规规则决定;subagent 层不会创建第二条队列。 ### 确认与恢复 -成功表示确切的在线 parent 已同步接受该消息。空闲 parent 在接受静默注入时已经完成追加,而暂存的静默上下文只有到达正常日志边界后才可重建。唤醒投递包含一个 inbox 条目实例,其 id 与返回的稳定消息 id 保持分离。 +成功表示确切的在线 parent 已同步接受该消息。上下文只有到达正常日志边界后才可重建;next-step 投递已经唤醒 parent,而静默投递可能继续等待。inbox 消息 id 不会成为另一个公开身份。 首个版本不提供持久化邮箱、幂等键、投递回执、重试协议或恰好一次保证。进程故障可能让调用方无法确定结果,在结果未知时重试则可能重复报告。parent 不可用时,持久化 child transcript(文本记录)仍是恢复来源。 @@ -62,7 +62,7 @@ subagent seam 新增 `registerContinuableSetup(contribution): () => void`,由 ACP(Agent Client Protocol)快照 harness 新增 `waitForSubagentTurnEnd`,按与 `session.N.jsonl` 相同的顺序选择第 N 个已收集 child。它会等待一个包含请求 header 的已闭合 child 轮次,以防可继续 child 早期播种描述符的轮次错误满足该边界。这样,整体组装的场景无需伪造 parent 可见信号,就能等待 child 侧报告。 -手写快照会启动一个可继续 child,执行真实的作用域局部 `report` 工具,观察默认唤醒投递所产生的那一个普通 parent 轮次,然后提交一条后续 parent 提示词,使其消费封装后的报告。它声明 child pin `1`,因此本不属于全局的 `report` schema 与该 child 自身的提示词会分别与 `tool-schemas.1.expected.json` 和 `system-prompt.1.expected.md` 比对,root 则继续使用类别 pin。生成的工具目录会另外铸造一个 child 作用域,以收录同一个作用域局部 schema。 +手写快照会启动一个可继续 child,执行真实的作用域局部 `report` 工具,并观察默认 next-step 投递先于管理器稍后的结算通知。一个仅用于快照的 maintenance 围栏会保持 parent,直至两条消息都处于待领取状态,从而证明 parent 恢复时先领取 next-step 输入、再领取排队的 next-turn 输入。它声明 child pin `1`,因此本不属于全局的 `report` schema 与该 child 自身的提示词会分别与 `tool-schemas.1.expected.json` 和 `system-prompt.1.expected.md` 比对,root 则继续使用类别 pin。生成的工具目录会另外铸造一个 child 作用域,以收录同一个作用域局部 schema。 ## 曾考虑的替代方案 @@ -76,7 +76,7 @@ ACP(Agent Client Protocol)快照 harness 新增 `waitForSubagentTurnEnd`, ### 允许 child 选择投递模式 -向模型提供 mode 参数会赋予其控制调度器压力的能力,并使行为依赖部署。child 只决定内容和时机;该内容是否启动另一个 Agent 轮次,由部署配置决定。 +向模型提供 mode 参数会赋予其控制调度器压力的能力,并使行为依赖部署。child 只决定内容和时机;该内容是否唤醒 parent,由部署配置决定。 ### 注册全局工具 @@ -101,18 +101,18 @@ ACP(Agent Client Protocol)快照 harness 新增 `waitForSubagentTurnEnd`, ## 影响 - 只有安装 report 包贡献时,可继续进程内 child 才会恰好暴露一个作用域局部 `report` schema;无关 Agent 永远不会暴露该 schema。 -- 工具返回 parent 消息的稳定 `MessageId`。静默投递没有 `InboxItemId`;唤醒投递会产生一个单独的 inbox 条目实例。 +- 工具返回 parent 消息的稳定 `MessageId`;其 inbox 中的出现不会成为另一个公开身份。 - 只有确切的驻留 child 才能报告,且只能报告给根据持久化谱系推导的确切在线直接 parent。服务不接受接收方参数,也不提供离线 fallback。 -- 唤醒投递是校验后的默认模式:它会恰好创建一个后续 FIFO 轮次,绝不 steering 已开始的轮次。静默投递则绝不会启动 parent 请求。 +- Next-step 投递是校验后的默认模式:它会唤醒空闲 parent,或在最近的 step 边界延长运行中 parent 的轮次。静默投递绝不会唤醒空闲 parent。 - parent 接受后取消或 dispose child 不会撤回报告。接受前,child dispose、drain、parent 丢失或调用方取消都会拒绝操作。 - 新建和恢复的 Activation 都会在发布前组合当前设置贡献。新授权等待下一个 Activation 才生效,而已驻留 child 的授权撤销立即生效。 - 单元覆盖固定可见性、allow-list 行为、两种投递模式、稳定的消息与发送方身份、嵌套路由、无效发送方、缺失的 parent、取消、drain、撤销竞争,以及不存在 Task 或隐式最终报告。 -- 无密钥整体组装快照证明真实 child 工具、那一个被唤醒的 parent 轮次、持久化 parent 封装,以及 parent 后续消费。 +- 无密钥整体组装快照证明真实 child 工具、默认 next-step 顺序先于结算,以及持久化 parent 封装。 ### 已接受的风险 该接受边界弱于持久化端到端投递。崩溃可能导致结果不明,重试则可能重复报告。 -唤醒投递可能在嵌套 child 频繁报告时放大模型工作量。通过 `reportDelivery` 交由部署所有者控制,可以限制该风险,但无法完全消除。 +嵌套 child 频繁报告时,next-step 投递可能放大模型工作量。一起等待的报告会共享一个 step,通过 `reportDelivery` 交由部署所有者控制也会限制该风险,但无法完全消除。 注册表中的存在性就是 parent 在线信号。宿主拥有的 parent 如果已开始 `AgentHandle.dispose()` 但尚未完成其作用域清理,仍可能接受并追加一条本进程不会再处理的报告。要弥合这个缺口,需要 Agent 层面的 dispose 开始信号,不能由 subagent 层推断。 diff --git a/.agents/notes/implemented/feature/2026-08-06-continuable-child-report-obligation.i18n.yaml b/.agents/notes/implemented/feature/2026-08-06-continuable-child-report-obligation.i18n.yaml index 652a4711f1..59063cae6b 100644 --- a/.agents/notes/implemented/feature/2026-08-06-continuable-child-report-obligation.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-06-continuable-child-report-obligation.i18n.yaml @@ -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-08-06-continuable-child-report-obligation.md -2026-08-06-continuable-child-report-obligation.md: f152ec1b8c353f094f2ba70785112eb1e165c510 -2026-08-06-continuable-child-report-obligation.zh.md: 4ec17e4642ffac385e6ce5464f41a3f4b3bebdbf +2026-08-06-continuable-child-report-obligation.md: e771e81831147dd02a6c32a543c8d8944c2ec2f4 +2026-08-06-continuable-child-report-obligation.zh.md: 7b4f8e7f5dead9f6b5803236e3593551c66da12d diff --git a/.agents/notes/implemented/feature/2026-08-06-continuable-child-report-obligation.md b/.agents/notes/implemented/feature/2026-08-06-continuable-child-report-obligation.md index f152ec1b8c..e771e81831 100644 --- a/.agents/notes/implemented/feature/2026-08-06-continuable-child-report-obligation.md +++ b/.agents/notes/implemented/feature/2026-08-06-continuable-child-report-obligation.md @@ -17,7 +17,7 @@ The return channel is an instruction the child receives, not a capability it may - the `report` tool, whose description now states that the child calls it once before finishing with a self-contained final result, and earlier for progress that changes what the parent should do next; - a `tool:report` system-prompt section at order 117 carrying the same obligation in the child's own voice, so a child that never reads tool descriptions closely still receives it. -`reportDelivery` now defaults to `wakeup`. An accepted report creates exactly one ordinary later parent turn and wakes a parked parent driver; it still never steers an open turn. `quiet` remains available for deployments that prefer unread reports over turn amplification. +`reportDelivery` defaults to `next-step`. An accepted report wakes a parked parent driver or joins a running parent's nearest step boundary, matching the instruction to report findings that change the parent's next action. `quiet` remains available for deployments that prefer unread reports over model-work amplification. The [report/settlement ordering decision](../bug-fix/2026-08-17-subagent-report-settlement-ordering.md) owns the scheduling rationale. ### Why the section and the description both exist @@ -33,7 +33,7 @@ That boundary is deliberate: prompt text can only reach a child that is still ru ### Snapshot coverage -The assembled ACP `subagent-report` scenario now exercises the shipped default: the child reports, the parked parent takes one ordinary turn on that report, and a later prompt still reads the report back out of the durable log. Because the child's scope now composes a prompt the class pin cannot describe, the snapshot harness gained `pinsChildSystemPrompts`, the exact counterpart of the existing `pinsChildToolSchemas`: it moves one child fixture's prompt into `system-prompt..expected.md`, leaves every other request-header field to the class pin, requires the sidecar exactly when declared, and rejects a sidecar identical to that class pin so a redundant copy cannot drift. +The assembled ACP `subagent-report` scenario exercises the shipped default: the child reports while the parent is in maintenance, the later settlement notice queues behind it, and the resumed parent claims the next-step report before next-turn settlement. Because the child's scope composes a prompt the class pin cannot describe, the snapshot harness has `pinsChildSystemPrompts`, the exact counterpart of `pinsChildToolSchemas`: it moves one child fixture's prompt into `system-prompt..expected.md`, leaves every other request-header field to the class pin, requires the sidecar exactly when declared, and rejects a sidecar identical to that class pin so a redundant copy cannot drift. ## Alternatives considered @@ -48,13 +48,13 @@ The assembled ACP `subagent-report` scenario now exercises the shipped default: ## Consequences - Every continuable in-process child with this package loaded carries one extra prompt section and a longer `report` description in every request; no other Agent's request changes. -- The default deployment wakes the parent once per accepted report. A nested tree that reports frequently consumes extra parent turns; `quiet` is the documented escape. +- The default deployment wakes the parent once per accepted report. A nested tree that reports frequently consumes extra parent requests, while reports waiting together share one step; `quiet` is the documented escape. - `installReportTool` requires `ctx.systemPrompt` in the child scope, so the package declares `systemPrompt` in `inject` and fails at load rather than at the next child materialization. - Unit coverage pins the new default, two load-bearing instruction phrases, the section's child-only scope against both the parent and a sibling, and rollback or revocation of both registrations. - Three assembled ACP scenarios with continuable children pin the complete instruction text through the new sidecar; a future change to any child-scoped section fails those scenarios instead of passing silently. ### Accepted risks -Waking by default amplifies model work in deep trees. The deployment owns that through `reportDelivery`, and the amplification is bounded by one turn per accepted report. +Next-step delivery by default amplifies model work in deep trees. The deployment owns that through `reportDelivery`; reports waiting together share one step, and one accepted report causes at most one wake. A child can still finish without reporting, and this change cannot detect it. Only the runtime's own [settlement account](2026-08-06-manager-owned-subagent-settlement-delivery.md) closes that case. diff --git a/.agents/notes/implemented/feature/2026-08-06-continuable-child-report-obligation.zh.md b/.agents/notes/implemented/feature/2026-08-06-continuable-child-report-obligation.zh.md index 4ec17e4642..7b4f8e7f5d 100644 --- a/.agents/notes/implemented/feature/2026-08-06-continuable-child-report-obligation.zh.md +++ b/.agents/notes/implemented/feature/2026-08-06-continuable-child-report-obligation.zh.md @@ -17,7 +17,7 @@ Status: implemented - `report` 工具,其描述现在说明 child 要在结束前调用一次并给出自足的最终结果,并在部分进展会改变 parent 下一步动作时提前调用; - 一个 order 为 117 的 `tool:report` 系统提示词 section,用 child 自己的语气承载同一条义务,使从不细读工具描述的 child 仍能收到它。 -`reportDelivery` 的默认值现在是 `wakeup`。一条被接受的报告恰好创建一个普通的后续 parent 轮次并唤醒停驻的 parent 驱动;它仍然绝不 steering(中途引导)已开始的轮次。对于宁可让报告无人阅读也要避免轮次放大的部署,`quiet` 依旧可用。 +`reportDelivery` 的默认值为 `next-step`。一条被接受的报告会唤醒停驻的 parent driver,或加入运行中 parent 最近的 step 边界,与发现会改变 parent 下一步动作时上报的指令一致。对于宁可让报告无人阅读也要避免模型工作量放大的部署,`quiet` 依旧可用。[报告与结算顺序决策](../bug-fix/2026-08-17-subagent-report-settlement-ordering.md)负责调度理由。 ### 为什么 section 与描述同时存在 @@ -33,7 +33,7 @@ Status: implemented ### 快照覆盖 -整体组装的 ACP `subagent-report` 场景现在演练随附的默认行为:child 上报,停驻的 parent 就该报告执行一个普通轮次,随后的提示词仍能从持久化日志中把报告读回来。由于该 child 的作用域现在组合出类别 pin 无法描述的提示词,快照 harness 新增了 `pinsChildSystemPrompts`,它与既有 `pinsChildToolSchemas` 完全对称:把一个 child fixture 的提示词移入 `system-prompt..expected.md`,其余请求 header 字段仍归类别 pin 所有,要求 sidecar 恰好在声明时存在,并拒绝与该类别 pin 完全相同的 sidecar,使冗余副本无法悄悄漂移。 +整体组装的 ACP `subagent-report` 场景演练随附的默认行为:child 在 parent 处于 maintenance 时上报,稍后的结算通知排在其后,而恢复的 parent 会先领取 next-step 报告、再领取 next-turn 结算。由于该 child 的作用域组合出类别 pin 无法描述的提示词,快照 harness 提供 `pinsChildSystemPrompts`,它与 `pinsChildToolSchemas` 完全对称:把一个 child fixture 的提示词移入 `system-prompt..expected.md`,其余请求 header 字段仍归类别 pin 所有,要求 sidecar 恰好在声明时存在,并拒绝与该类别 pin 完全相同的 sidecar,使冗余副本无法悄悄漂移。 ## 备选方案 @@ -48,13 +48,13 @@ Status: implemented ## 后果 - 加载本包后,每个可继续进程内 child 的每次请求都会多出一个提示词 section 和一段更长的 `report` 描述;其他任何 Agent 的请求都不变。 -- 默认部署会为每条被接受的报告唤醒 parent 一次。频繁上报的嵌套树会消耗额外的 parent 轮次;`quiet` 是有文档记载的退路。 +- 默认部署会为每条被接受的报告唤醒 parent 一次。频繁上报的嵌套树会消耗额外的 parent 请求,而一起等待的报告会共享一个 step;`quiet` 是有文档记载的退路。 - `installReportTool` 需要 child 作用域中的 `ctx.systemPrompt`,因此本包在 `inject` 中声明 `systemPrompt`,从而在加载时失败,而不是等到下一次 child 物化时。 - 单元覆盖固定了新默认值、两处关键指令措辞、该 section 相对 parent 与同级均仅限 child 的作用域,以及两项注册在安装回滚或撤销时的清理。 - 三个带可继续 child 的整体组装 ACP 场景通过新的 sidecar 逐字固定完整的 child 提示词;今后任何对 child 作用域 section 的改动都会让这些场景失败,而不是悄悄通过。 ### 已接受的风险 -默认唤醒会在深层树中放大模型工作量。部署通过 `reportDelivery` 掌握该取舍,且放大幅度以每条被接受报告一个轮次为界。 +默认 next-step 投递会在深层树中放大模型工作量。部署通过 `reportDelivery` 掌握该取舍;一起等待的报告会共享一个 step,且每条被接受的报告至多产生一次唤醒。 child 仍可能不上报就结束,本次改动无法检测这一点。只有运行时自己的[结算记账](2026-08-06-manager-owned-subagent-settlement-delivery.md)才能补上这一情形。 diff --git a/.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.i18n.yaml b/.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.i18n.yaml index 40eb28f89b..96312b7694 100644 --- a/.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.i18n.yaml @@ -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-08-06-manager-owned-subagent-settlement-delivery.md -2026-08-06-manager-owned-subagent-settlement-delivery.md: b3e7d3794cd075d1ef7d20568b99457520bc1fde -2026-08-06-manager-owned-subagent-settlement-delivery.zh.md: b4f1e8e120cf88ceb5d9a696fbb58d61ecff09d8 +2026-08-06-manager-owned-subagent-settlement-delivery.md: 27daa6d5150950efb50bf23dea945498651d2c09 +2026-08-06-manager-owned-subagent-settlement-delivery.zh.md: 19abce4fc9c151872d1e02e4ca2efa3953be065b diff --git a/.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.md b/.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.md index b3e7d3794c..27daa6d515 100644 --- a/.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.md +++ b/.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.md @@ -62,7 +62,7 @@ Three assembled ACP scenarios cover the notice: a child that never reports, a ch A keyless headless Loader snapshot covers the user-visible path end to end. Its replay parent omits `run_in_background` to exercise the continuable background default, never calls `list_agents`, `send_message`, or Task tools, consumes the manager-authored `subagent-settled` notice, and produces its final answer. The child never calls `report`, so the transcript cannot pass through the cooperative report path. A test-only Loader fence holds the parent's post-spawn request until the real manager notice enters its inbox, removing platform scheduling from the transcript without synthesizing the notice. -`subagent-report` needed one more concession. With the shipped waking report default, that scenario has two independent parent wakes — the report and the settlement — and whether the second extends the first's turn or opens its own is a genuine coin flip that measured 50/50 across runs. No authored transcript can hold both orders. Its overlay therefore pins `reportDelivery: quiet`, leaving settlement as the only wake, and a snapshot-only pre-step fence holds the child until the parent's spawn turn ends so that wake opens one deterministic turn claiming both messages. The waking report default keeps its coverage in the report package's own tests. +The `subagent-report` scenario uses the default next-step report delivery. A snapshot-only fence holds the child until the parent's spawn turn ends, then holds the parent in maintenance until settlement follows the report. The resumed parent claims the next-step report before the queued next-turn settlement. The [report/settlement ordering decision](../bug-fix/2026-08-17-subagent-report-settlement-ordering.md) owns this cross-state ordering. The refusal and interruption wordings are pinned verbatim in unit tests rather than in a replayed transcript: producing them needs a rejecting policy plugin or a cancellation fenced at a step boundary, which the keyless assemblies do not otherwise carry, and the assembled scenarios already pin the notice pathway itself end to end. @@ -87,7 +87,7 @@ The refusal and interruption wordings are pinned verbatim in unit tests rather t - `Activation` carries `parentSession` and `announced`. The first exists because the child handle is disposed before delivery; the second is what keeps a rolled-back materialization silent. - `foldConsumedWork()` replaces `dsh-session`'s `findLastMessageTurnEnd()` and moves to `dsh-agent`, which owns the inbox marker it reads; the one-shot in-process path folds the same answer and does not classify a cut-short one-shot child as `completed`. - Unit coverage pins the unconditional contract, each terminal reason, idle and busy scheduling, the batch, the maintenance regression, the pre-release ordering, a parent that is gone, and a rejected send that must not fail teardown. -- Three ACP scenarios use an explicit settlement fence, and `subagent-report` has a config overlay that pins quiet report delivery. +- Three ACP scenarios use an explicit settlement fence, and `subagent-report` pins the default report-before-settlement next-step order. - A keyless headless Loader snapshot pins background start → manager-authored settlement notice → final parent answer with no polling or child `report` call. ### Accepted risks @@ -100,4 +100,4 @@ Stop-reason attribution is a best effort over the log's existing splice vocabula Turn amplification is real for deep or wide trees, and it is not configurable by design. The step-boundary batch bounds it for simultaneous settlement but not for children that settle apart. -Two independent waking sources cannot be ordered in an authored transcript. The assembled coverage pins each separately rather than their interleaving. +Reports and their later settlement notices are ordered through the parent's next-step FIFO. Independent settlements from sibling children retain their actual delivery order rather than a synthetic sibling ordering. diff --git a/.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.zh.md b/.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.zh.md index b4f1e8e120..19abce4fc9 100644 --- a/.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.zh.md +++ b/.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.zh.md @@ -62,7 +62,7 @@ Status: implemented 另有一个无密钥的 headless Loader 快照端到端覆盖用户可见路径。其重放父级省略 `run_in_background` 以覆盖可继续后台默认路径,从不调用 `list_agents`、`send_message` 或 Task 工具,消费管理器写入的 `subagent-settled` 通知,并给出最终答案。child 从不调用 `report`,因此该 transcript 不可能经由协作式上报路径通过。一个仅用于测试的 Loader 栅栏会把父级启动后的请求保持到真实管理器通知进入其 inbox 为止,从 transcript 中排除平台调度差异,但不会伪造该通知。 -`subagent-report` 还需要多做一步让步。在随附的唤醒上报默认值下,该场景有两个互相独立的父级唤醒——上报与结算——而第二个究竟是延长第一个的轮次还是另开一个轮次,是一枚真正的硬币,多次运行实测约为五五开。任何手写 transcript 都无法同时容纳两种顺序。因此它的 overlay 固定 `reportDelivery: quiet`,使结算成为唯一唤醒;另一个仅用于快照的 pre-step 栅栏会把 child 保持到父级启动轮次结束,使这次唤醒开启一个确定轮次并同时认领两条消息。唤醒上报默认值的覆盖则保留在 report 包自身的测试中。 +`subagent-report` 场景使用默认 next-step 报告投递。一个仅用于快照的围栏会让 child 等到 parent 的派生轮次结束,随后让 parent 保持 maintenance,直至结算跟在报告之后到达。恢复的 parent 会先领取 next-step 报告、再领取排队的 next-turn 结算。[报告与结算顺序决策](../bug-fix/2026-08-17-subagent-report-settlement-ordering.md)负责说明这种跨状态顺序。 拒绝与中断两种措辞在单元测试中逐字钉死,而不进入重放 transcript:触发它们需要一个会拒绝的策略插件、或一次在 step 边界被栅栏卡住的取消,而无密钥组装本身并不携带这些;通知通路本身已由整体组装场景端到端钉住。 @@ -87,7 +87,7 @@ Status: implemented - `Activation` 携带 `parentSession` 与 `announced`。前者存在是因为 child handle 在投递前已被 dispose;后者让被回滚的物化保持静默。 - `foldConsumedWork()` 取代 `dsh-session` 的 `findLastMessageTurnEnd()`,并迁移到 `dsh-agent`——它拥有该 fold 所读取的 inbox 标记;一次性 in-process 路径折叠同一个答案,不会把被中途切断的一次性 child 归类为 `completed`。 - 单元覆盖固定了无条件约定、每种终止原因、空闲与繁忙两种调度、批量语义、维护期回归、释放前顺序、父级已消失,以及一次不得让拆卸失败的发送被拒。 -- 三个 ACP 场景使用显式的结算栅栏,`subagent-report` 带有固定静默上报投递的配置 overlay。 +- 三个 ACP 场景使用显式的结算围栏,`subagent-report` 固定默认的报告先于结算的 next-step 顺序。 - 一个无密钥的 headless Loader 快照固定了「后台启动 → 管理器写入的结算通知 → 父级最终答案」路径,其中没有轮询,也没有 child `report` 调用。 ### 已接受的风险 @@ -100,4 +100,4 @@ Status: implemented 对于深或宽的树,轮次放大是真实存在的,而且按设计不可配置。step 边界的批量语义只能约束同时结算的情形,无法约束分散结算的 child。 -两个互相独立的唤醒源无法在手写 transcript 中排序。整体组装覆盖分别固定它们,而不固定它们的交错。 +报告与其稍后的结算通知通过 parent 的 next-step FIFO 排序。来自同级 child 的独立结算保留其实际投递顺序,不会虚构同级间的顺序。 diff --git a/.agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.i18n.yaml b/.agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.i18n.yaml index 855c9b755d..13ae111421 100644 --- a/.agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.i18n.yaml @@ -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-08-11-background-first-continuable-delegation.md -2026-08-11-background-first-continuable-delegation.md: 3eab4ba36d8e6b2a1984450149021450741f3b89 -2026-08-11-background-first-continuable-delegation.zh.md: b8f14e0c40c341fc91b2fd0dc8c3f00fc2e14b66 +2026-08-11-background-first-continuable-delegation.md: 59232ae8821ef4a093fd610ecbbb39690316ce6c +2026-08-11-background-first-continuable-delegation.zh.md: 928d1aef31babe853cb48f6df114c79a188da345 diff --git a/.agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.md b/.agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.md index 3eab4ba36d..59232ae882 100644 --- a/.agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.md +++ b/.agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.md @@ -20,7 +20,7 @@ The model-facing text divides responsibility by location: - the `run_in_background` parameter states the lifecycle-specific default and when to override it; - a `tool:` system-prompt section tells the model to start independent delegations together, continue useful work while they run, and choose foreground only when the next action depends on the result. The section renders only when that tool remains visible in the assembly scope, so a child tool restriction removes the schema and its guidance together. -The [continuable child report obligation](2026-08-06-continuable-child-report-obligation.md) remains unchanged: the child prompt requires one self-contained final report and earlier reports for findings that change the parent's next action. Manager-owned settlement remains unconditional and does not inspect whether a report arrived. The two messages may repeat final content, but they retain distinct authors and purposes: `report` is the child's explicit handoff, while settlement records how the run ended and preserves terminal output when the child cannot cooperate. `reportDelivery` remains deployment scheduling policy with `wakeup` as its default. +The [continuable child report obligation](2026-08-06-continuable-child-report-obligation.md) remains unchanged: the child prompt requires one self-contained final report and earlier reports for findings that change the parent's next action. Manager-owned settlement remains unconditional and does not inspect whether a report arrived. The two messages may repeat final content, but they retain distinct authors and purposes: `report` is the child's explicit handoff, while settlement records how the run ended and preserves terminal output when the child cannot cooperate. `reportDelivery` remains deployment scheduling policy with `next-step` as its default, preserving report-before-settlement order through the parent inbox. The keyless headless `subagent-settlement` scenario omits `run_in_background`, receives the immediate child id, and reaches the final parent answer through the manager-authored settlement notice even though its fixture deliberately does not call `report`. Package tests separately pin explicit `false` as foreground, the parent scheduling text, and the child's mandatory-report prompt. diff --git a/.agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.zh.md b/.agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.zh.md index b8f14e0c40..928d1aef31 100644 --- a/.agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.zh.md +++ b/.agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.zh.md @@ -20,7 +20,7 @@ child 作用域的 `report` 提示词要求发送自包含的最终报告,而[ - `run_in_background` 参数说明具体生命周期的默认值以及何时覆盖; - `tool:` 系统提示词 section 会告诉模型同时启动相互独立的委派、在它们运行时继续有用工作,并且仅当下一步动作依赖结果时选择前台。只有当该工具在组装作用域中仍可见时才会渲染这个 section,因此子级工具限制会同时移除 schema 与对应指引。 -[可继续 child 上报义务](2026-08-06-continuable-child-report-obligation.md)保持不变:child 提示词要求发送一份自包含的最终报告,并在发现会改变 parent 下一步动作的信息时提前报告。由管理器负责的结算仍然无条件执行,不检查报告是否已经到达。这两条消息可能重复最终内容,但作者和用途不同:`report` 是 child 的显式交接,结算则记录本次运行如何结束,并在 child 无法配合时保留终止输出。`reportDelivery` 仍是部署调度策略,默认值仍为 `wakeup`。 +[可继续 child 上报义务](2026-08-06-continuable-child-report-obligation.md)保持不变:child 提示词要求发送一份自包含的最终报告,并在发现会改变 parent 下一步动作的信息时提前报告。由管理器负责的结算仍然无条件执行,不检查报告是否已经到达。这两条消息可能重复最终内容,但作者和用途不同:`report` 是 child 的显式交接,结算则记录本次运行如何结束,并在 child 无法配合时保留终止输出。`reportDelivery` 仍是部署调度策略,默认值为 `next-step`,通过 parent inbox 保持报告先于结算的顺序。 无密钥 headless `subagent-settlement` 场景省略 `run_in_background`,收到立即返回的 child id;尽管 fixture(测试前置数据)有意不调用 `report`,它仍通过管理器生成的结算通知到达 parent 最终答案。包测试另行固定了显式 `false` 的前台语义、parent 调度文本以及 child 的强制报告提示词。 diff --git a/.agents/notes/implemented/feature/2026-08-11-background-job-completion-wakes-an-idle-owner.i18n.yaml b/.agents/notes/implemented/feature/2026-08-11-background-job-completion-wakes-an-idle-owner.i18n.yaml index 779e692e5f..5b1413fa51 100644 --- a/.agents/notes/implemented/feature/2026-08-11-background-job-completion-wakes-an-idle-owner.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-11-background-job-completion-wakes-an-idle-owner.i18n.yaml @@ -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-08-11-background-job-completion-wakes-an-idle-owner.md -2026-08-11-background-job-completion-wakes-an-idle-owner.md: 5193fda633dac78f06e6eb5e7b97be6aaa0ea94c -2026-08-11-background-job-completion-wakes-an-idle-owner.zh.md: fa338f06b2950ad9b1d88450ddac9f13a643cd15 +2026-08-11-background-job-completion-wakes-an-idle-owner.md: 15ff0fbdc173f6cadaa2f75e265effe61e512f15 +2026-08-11-background-job-completion-wakes-an-idle-owner.zh.md: b7b528ce9294fde39ca82c293edf099b8b9fddef diff --git a/.agents/notes/implemented/feature/2026-08-11-background-job-completion-wakes-an-idle-owner.md b/.agents/notes/implemented/feature/2026-08-11-background-job-completion-wakes-an-idle-owner.md index 5193fda633..15ff0fbdc1 100644 --- a/.agents/notes/implemented/feature/2026-08-11-background-job-completion-wakes-an-idle-owner.md +++ b/.agents/notes/implemented/feature/2026-08-11-background-job-completion-wakes-an-idle-owner.md @@ -32,7 +32,7 @@ Injection is correct there. A cancelled turn is a user pressing stop, and reopen The bound exists because this chain is self-exciting in a way subagent settlement is not. Settlement is bounded by how many children the model spawned; a woken turn can start the background job whose completion wakes it again, with nobody watching. `dsh run` needs no separate policy: its one user message is claimed in the first turn and never repeats, so the budget is spent monotonically and the process terminates. -`completionDelivery: quiet` restores the old lane for idle owners. It exists for deterministic transcripts, and mirrors the `reportDelivery` switch on `tool-subagent-report` in name, values, and default. +`completionDelivery: quiet` restores the old lane for idle owners. It exists for deterministic transcripts; job completion independently retains `quiet | wakeup` because its bounded owner-turn policy differs from next-step subagent reports. ### Teardown claims the report diff --git a/.agents/notes/implemented/feature/2026-08-11-background-job-completion-wakes-an-idle-owner.zh.md b/.agents/notes/implemented/feature/2026-08-11-background-job-completion-wakes-an-idle-owner.zh.md index fa338f06b2..b7b528ce92 100644 --- a/.agents/notes/implemented/feature/2026-08-11-background-job-completion-wakes-an-idle-owner.zh.md +++ b/.agents/notes/implemented/feature/2026-08-11-background-job-completion-wakes-an-idle-owner.zh.md @@ -32,7 +32,7 @@ Status: implemented 设界是因为这条链会自激,而 subagent 结算不会。结算受限于模型派生了多少子 agent;被唤醒的一轮却可能启动某个后台任务,而它的完成又会唤醒同一个所有者,且无人旁观。`dsh run` 不需要单独策略:它唯一的用户消息在第一轮就被领取且不会重复,因此预算单调消耗,进程必然终止。 -`completionDelivery: quiet` 为空闲所有者恢复旧通道。它的存在是为了确定性 transcript,并在名称、取值与默认值上都对齐 `tool-subagent-report` 的 `reportDelivery` 开关。 +`completionDelivery: quiet` 为空闲所有者恢复旧通道。它的存在是为了确定性 transcript;后台任务完成会独立保留 `quiet | wakeup`,因为其有界的所有者轮次策略不同于 next-step subagent 报告。 ### 销毁自行认领报告 diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index b1b6ec9deb..658a54a3a7 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -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 docs/config-catalog.md -config-catalog.md: 469291801851e73837965992555468e659282ab2 -config-catalog.zh.md: 06207bf8a3475c20a14d2fb1509ec5d21f36733b +config-catalog.md: 6759d4b8e953c2a147a0441be21a25e0920cc1df +config-catalog.zh.md: e59ad1ac07f46b7cb044a83012aa97d1a78f769b diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 4692918018..6759d4b8e9 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -2666,9 +2666,9 @@ Requires: `subagents` · `tools` · `systemPrompt` /** Config: how accepted reports are scheduled on the parent. */ export interface Config { /** - * Parent scheduling (default `wakeup`). `wakeup` creates one ordinary later - * parent turn; `quiet` adds context without waking, so a parked parent learns - * of the report only when something else wakes it. + * Parent scheduling (default `next-step`). `next-step` wakes the parent and + * enters at its nearest step boundary; `quiet` adds the same context without + * waking, so a parked parent waits for another waking input. */ reportDelivery?: SubagentReportDelivery } diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 06207bf8a3..e59ad1ac07 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -2668,9 +2668,9 @@ export interface Config { /** Config: how accepted reports are scheduled on the parent. */ export interface Config { /** - * Parent scheduling (default `wakeup`). `wakeup` creates one ordinary later - * parent turn; `quiet` adds context without waking, so a parked parent learns - * of the report only when something else wakes it. + * Parent scheduling (default `next-step`). `next-step` wakes the parent and + * enters at its nearest step boundary; `quiet` adds the same context without + * waking, so a parked parent waits for another waking input. */ reportDelivery?: SubagentReportDelivery } diff --git a/docs/subsystems/subagent.i18n.yaml b/docs/subsystems/subagent.i18n.yaml index a86dc8de4a..b99b907ff9 100644 --- a/docs/subsystems/subagent.i18n.yaml +++ b/docs/subsystems/subagent.i18n.yaml @@ -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 docs/subsystems/subagent.md -subagent.md: a683a679e6017351540ee4b73adc74375ef0a1d6 -subagent.zh.md: 61391cd297c0eb14f4c0d8eac4539b551cb60bda +subagent.md: 00fa8faf2f6609d469f87f10aa7cbaa6e30b7eee +subagent.zh.md: 8dc578d1a3b4456edac4888bdc6dd670b59f5cb8 diff --git a/docs/subsystems/subagent.md b/docs/subsystems/subagent.md index a683a679e6..00fa8faf2f 100644 --- a/docs/subsystems/subagent.md +++ b/docs/subsystems/subagent.md @@ -191,7 +191,7 @@ interface ContinuableStart { An optional continuable-child setup contribution can install scope-local capabilities after base child composition and before Activation publication. The registry is ordered and transactional: a failed or revoked setup rolls back the unpublished Activation, child-scope disposal releases every installation, new registrations affect the next Activation, and registration removal revokes every resident installation immediately. -`SubagentRuntime.reportFrom()` uses that extension point without adding a second queue or a result-bearing child wrapper. The exact live child Agent authorizes the call; callers cannot name a recipient. The manager derives the only recipient from the child's durable `parentSession`, requires that parent Agent to be live, frames the selected content as one `subagent-report` user message, and returns the message's stable `MessageId`. Quiet delivery uses `Agent.inject()` and creates no inbox occurrence or parent turn; waking delivery uses `Agent.followup()` and creates one ordinary later parent turn. Neither mode concludes the child's turn, and no final answer reports implicitly. +`SubagentRuntime.reportFrom()` uses that extension point without adding a second queue or a result-bearing child wrapper. The exact live child Agent authorizes the call; callers cannot name a recipient. The manager derives the only recipient from the child's durable `parentSession`, requires that parent Agent to be live, frames the selected content as one `subagent-report` user message, and returns the message's stable `MessageId`. Quiet delivery uses `Agent.inject()` and does not wake the parent; next-step delivery uses `Agent.steer()`, waking an idle parent or joining a running parent's nearest step boundary. Neither mode concludes the child's turn, and no final answer reports implicitly. ```ts type-equiv /** Durable attribution for a continuable child's explicit parent report. */ @@ -206,7 +206,7 @@ interface SubagentReportMessageSource { ```ts type-equiv /** Deployment scheduling policy for accepted child reports. */ -type SubagentReportDelivery = 'quiet' | 'wakeup' +type SubagentReportDelivery = 'quiet' | 'next-step' ``` Reporting is the child's own choice, so the manager keeps a separate account of its own: when a resident Activation settles, it delivers one notice to the child's durable direct parent describing how that epoch ended and carrying its final assistant content. That delivery is unconditional for every child whose id a caller received, happens before the ownership release that would let the parent be judged settled, and reaches a resident parent through the same waking-admission accounting as a report. A parent whose own lineage is already tearing down receives it without a wake, because waking a quiescent Agent starts a turn rather than queueing work. Its provenance is a distinct kind so a transcript never presents a runtime account as something the child wrote. diff --git a/docs/subsystems/subagent.zh.md b/docs/subsystems/subagent.zh.md index 61391cd297..8dc578d1a3 100644 --- a/docs/subsystems/subagent.zh.md +++ b/docs/subsystems/subagent.zh.md @@ -191,7 +191,7 @@ interface ContinuableStart { 可选的可继续 child 设置贡献可以在 child 基础组合完成后、Activation 发布前安装限定在作用域内的能力。该注册表按顺序执行且具有事务性:设置失败或被撤销时会回滚未发布的 Activation;child 作用域 dispose 时会释放所有安装;新注册项在下一个 Activation 生效;移除注册项时则会立即撤销每个驻留中的安装。 -`SubagentRuntime.reportFrom()` 通过该扩展点实现报告,无需新增第二条队列或承载结果的 child 包装层。调用由确切的在线 child Agent 授权,调用方不能指定接收方。管理器从 child 的持久化 `parentSession` 中推导唯一接收方,要求该 parent Agent 必须在线,将选中内容封装为一条 `subagent-report` 用户消息,并返回该消息的稳定 `MessageId`。静默投递使用 `Agent.inject()`,不产生 inbox 条目实例或 parent 轮次;唤醒投递使用 `Agent.followup()`,会产生一个普通的后续 parent 轮次。两种模式都不会结束 child 轮次,最终回答也不会隐式报告。 +`SubagentRuntime.reportFrom()` 通过该扩展点实现报告,无需新增第二条队列或承载结果的 child 包装层。调用由确切的在线 child Agent 授权,调用方不能指定接收方。管理器从 child 的持久化 `parentSession` 中推导唯一接收方,要求该 parent Agent 必须在线,将选中内容封装为一条 `subagent-report` 用户消息,并返回该消息的稳定 `MessageId`。静默投递使用 `Agent.inject()`,不会唤醒 parent;next-step 投递使用 `Agent.steer()`,会唤醒空闲 parent,或加入运行中 parent 最近的 step 边界。两种模式都不会结束 child 轮次,最终回答也不会隐式报告。 ```ts type-equiv /** Durable attribution for a continuable child's explicit parent report. */ @@ -206,7 +206,7 @@ interface SubagentReportMessageSource { ```ts type-equiv /** Deployment scheduling policy for accepted child reports. */ -type SubagentReportDelivery = 'quiet' | 'wakeup' +type SubagentReportDelivery = 'quiet' | 'next-step' ``` 上报是 child 自己的选择,因此管理器还保有一份属于自己的记账:当驻留 Activation 结算时,它会向该 child 持久化的直接 parent 投递一条通知,说明该 epoch 如何结束,并携带其最终 assistant 内容。对每个调用方拿到过 id 的 child,这条投递都是无条件的;它发生在会让 parent 被判定为已结算的所有权释放之前,并通过与上报相同的唤醒准入记账到达驻留 parent。若 parent 自身所在的谱系已在拆卸中,这条通知会以不唤醒的方式送达,因为唤醒一个静息 Agent 是开启一个轮次,而不是排队等待工作。其来源信息使用一个独立的 kind,因此 transcript(文本记录)绝不会把运行时的记账呈现为 child 自己写下的内容。 diff --git a/examples/acp-agent/subagent-report-quiet.cordis.yml b/examples/acp-agent/subagent-report-quiet.cordis.yml deleted file mode 100644 index cb1c2b6d34..0000000000 --- a/examples/acp-agent/subagent-report-quiet.cordis.yml +++ /dev/null @@ -1,17 +0,0 @@ -# Snapshot-only overlay pinning quiet report delivery. The shipped default wakes -# the parent on every accepted report, and the runtime's settlement notice wakes -# it again when the child's Activation ends; two independent wakes have no single -# authored order. Quiet delivery leaves settlement as the only wake, while the -# fixture below holds the child until the parent's spawn turn has closed. -- id: base - name: '@deepseek-ai/cordis-plugin-include' - config: - path: ./cordis.yml - patches: - - id: tool-subagent-report - name: '@deepseek-ai/dsh-tool-subagent-report' - config: - reportDelivery: quiet - -- id: report-fence - name: './tests/fixtures/subagent-report-fence.ts' diff --git a/examples/acp-agent/subagent-report-quiet.cordis.snapshot.yml b/examples/acp-agent/subagent-report.cordis.snapshot.yml similarity index 81% rename from examples/acp-agent/subagent-report-quiet.cordis.snapshot.yml rename to examples/acp-agent/subagent-report.cordis.snapshot.yml index 4d28b399f0..c18d0f3bac 100644 --- a/examples/acp-agent/subagent-report-quiet.cordis.snapshot.yml +++ b/examples/acp-agent/subagent-report.cordis.snapshot.yml @@ -1,6 +1,5 @@ -# Keyless counterpart to subagent-report-quiet.cordis.yml: replace the live -# adapter with replay, keep report delivery quiet, and fence the child behind -# the end of its parent's spawn turn so settlement opens the next turn. +# Keyless counterpart to subagent-report.cordis.yml: replace the live adapter +# with replay and preserve its child and parent scheduling fence. - id: base name: '@deepseek-ai/cordis-plugin-include' config: @@ -32,10 +31,6 @@ - passthrough-runner runnerFailureSignatures: - 'passthrough-runner: profile rejected' - - id: tool-subagent-report - name: '@deepseek-ai/dsh-tool-subagent-report' - config: - reportDelivery: quiet - insert: - id: llm-replay name: '@deepseek-ai/dsh-llm-replay' diff --git a/examples/acp-agent/subagent-report.cordis.yml b/examples/acp-agent/subagent-report.cordis.yml new file mode 100644 index 0000000000..038e598e65 --- /dev/null +++ b/examples/acp-agent/subagent-report.cordis.yml @@ -0,0 +1,10 @@ +# Snapshot-only overlay fencing the child behind its parent's spawn turn and +# holding the parent in maintenance until settlement follows the default +# next-step report. The resumed parent claims both notices in causal order. +- id: base + name: '@deepseek-ai/cordis-plugin-include' + config: + path: ./cordis.yml + +- id: report-fence + name: './tests/fixtures/subagent-report-fence.ts' diff --git a/examples/acp-agent/tests/acp.snapshot.ts b/examples/acp-agent/tests/acp.snapshot.ts index 3d40e1f196..c5bf81fc1f 100644 --- a/examples/acp-agent/tests/acp.snapshot.ts +++ b/examples/acp-agent/tests/acp.snapshot.ts @@ -47,8 +47,8 @@ const CHILD_QUESTION_CONFIG = fileURLToPath(new URL('../child-question.cordis.ym const SESSION_SANDBOX_ROOT_CONFIG = fileURLToPath(new URL('../session-sandbox-root.cordis.yml', import.meta.url)) const RETRY_CONFIG = fileURLToPath(new URL('../retry.cordis.yml', import.meta.url)) const SESSION_TITLE_CONFIG = fileURLToPath(new URL('../session-title.cordis.yml', import.meta.url)) -const SUBAGENT_REPORT_QUIET_CONFIG = fileURLToPath( - new URL('../subagent-report-quiet.cordis.yml', import.meta.url), +const SUBAGENT_REPORT_CONFIG = fileURLToPath( + new URL('../subagent-report.cordis.yml', import.meta.url), ) const SUBAGENT_DURABILITY_FAILURE_CONFIG = fileURLToPath( new URL('../subagent-durability-failure.cordis.yml', import.meta.url), @@ -467,16 +467,15 @@ const SCENARIOS: Scenario[] = [ configPath: SUBAGENT_DURABILITY_FAILURE_CONFIG, }, // Authored child-to-parent transcript: the child calls its scope-local - // `report`, and the runtime's unconditional settlement notice then wakes the - // parked parent into one ordinary turn that claims both. The overlay pins - // quiet report delivery because two independent wakes have no orderable - // transcript; the shipped waking default is covered by package tests. + // `report` through the shipped next-step policy. A maintenance fence holds + // the parent until the runtime's unconditional settlement notice follows; + // the resumed parent then claims both messages in causal order. { name: 'subagent-report', hasModelTurn: true, recorded: false, overridden: false, - configPath: SUBAGENT_REPORT_QUIET_CONFIG, + configPath: SUBAGENT_REPORT_CONFIG, pinsChildToolSchemas: [1], pinsChildSystemPrompts: [1], }, diff --git a/examples/acp-agent/tests/fixtures/subagent-report-fence.ts b/examples/acp-agent/tests/fixtures/subagent-report-fence.ts index 754343e4ec..b7aaeaddb8 100644 --- a/examples/acp-agent/tests/fixtures/subagent-report-fence.ts +++ b/examples/acp-agent/tests/fixtures/subagent-report-fence.ts @@ -1,5 +1,6 @@ /** - * Loader fixture that holds the report child until its parent's spawn turn ends. + * Loader fixture that holds the report child until its parent's spawn turn ends, + * then parks the parent until child settlement follows the report. * @module subagent-report-fence */ @@ -10,13 +11,16 @@ import type {} from '@deepseek-ai/dsh-agent-loop' export const name = 'subagent-report-fence' /** - * Keep replay scheduling from folding settlement into the parent's first turn. + * Keep replay scheduling from folding settlement into the parent's first turn + * or starting a second parent request between report and settlement. * @param ctx - assembled ACP-agent context. */ export function apply(ctx: Context): void { const childReady = Promise.withResolvers() const parentStopped = Promise.withResolvers() + const childSettled = Promise.withResolvers() let hasStopped = false + let parentMaintenance: Promise | undefined ctx.effect(() => { const disposeSession = ctx.root.on('session/event', (session, event) => { @@ -24,6 +28,26 @@ export function apply(ctx: Context): void { hasStopped = true parentStopped.resolve(undefined) }) + const disposeStatus = ctx.root.on('agent/status', ({ agent, status }) => { + if ( + agent.session.header.parentSession === undefined && + status === 'idle' && + hasStopped && + parentMaintenance === undefined + ) { + parentMaintenance = agent.runMaintenance(async () => { + await childSettled.promise + }) + } + }) + const disposeInbox = ctx.root.on('agent/inbox/inserted', ({ agent, message }) => { + if ( + agent.session.header.parentSession === undefined && + message.source.kind === 'subagent-settled' + ) { + childSettled.resolve(undefined) + } + }) const disposeStep = ctx.root.on('agent/pre-step', async ({ agent, turn, step }, next) => { if (agent.session.header.parentSession !== undefined) { childReady.resolve(undefined) @@ -34,7 +58,10 @@ export function apply(ctx: Context): void { return next() }) return () => { + childSettled.resolve(undefined) disposeStep() + disposeInbox() + disposeStatus() disposeSession() } }, 'subagent-report-fence.listeners') diff --git a/examples/acp-agent/tests/snapshots/subagent-report/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/subagent-report/stdout.expected.jsonl index 785ee28128..b6de818dea 100644 --- a/examples/acp-agent/tests/snapshots/subagent-report/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-report/stdout.expected.jsonl @@ -1,7 +1,7 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} {"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}"}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"STARTED"}}}} -{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}}} +{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"CHILD_REPORT_OK"}}}} {"jsonrpc":"2.0","id":4,"result":{"stopReason":"end_turn"}} diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 553d61c881..5e27a13bdf 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -4129,7 +4129,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SubagentReportDelivery', - declaration: 'export type SubagentReportDelivery = \'quiet\' | \'wakeup\';', + declaration: 'export type SubagentReportDelivery = \'quiet\' | \'next-step\';', }, { name: 'SubagentReportOptions', diff --git a/packages/subagent/subagent/README.i18n.yaml b/packages/subagent/subagent/README.i18n.yaml index 6d443fc9ba..7cfdbec38b 100644 --- a/packages/subagent/subagent/README.i18n.yaml +++ b/packages/subagent/subagent/README.i18n.yaml @@ -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 packages/subagent/subagent/README.md -README.md: ed4a9123a2dfa5b2fa5abc67f4513547feb3d140 -README.zh.md: 3ad2ee5738a210a776d1f0b2746dcbd21d46144c +README.md: 9d60a3bd776e867ddf2f8316266970faa6454c9a +README.zh.md: 241e5e8715e7a2e7d4ae8b7385c1ee358a90e2b0 diff --git a/packages/subagent/subagent/README.md b/packages/subagent/subagent/README.md index ed4a9123a2..9d60a3bd77 100644 --- a/packages/subagent/subagent/README.md +++ b/packages/subagent/subagent/README.md @@ -19,7 +19,7 @@ The [subagent family overview](../README.md) maps implementations and model-faci | `startContinuable(spec)` | Establish one durable continuable child and deliver its initial prompt. Resolves with `{ childId, messageId }` when the child's inbox accepts that prompt, without waiting for the turn to start or for the message to reach the Session log; any earlier failure rejects with no ids and rolls the child back entirely. Requires `ctx.agents`, session persistence, and a provider with the `prepareContinuable` capability. | | `followup(parent, childId, content, { source, signal })` | Deliver one later message from the exact live direct parent as the child's next FIFO turn, matching `Agent.followup()` terminology, and return the accepted `MessageId`. A resident child's inbox accepts it directly (waking a waiting Activation); an absent one cold-resumes from its persisted Session. Requires `ctx.agents`; cold resume also requires session persistence. | | `interrupt(targetSessionId, authority)` | Interrupt one live continuable child's current turn under a human durable parent address (`{ kind: 'user', parentSessionId }`) or an exact live ancestor Agent (`{ kind: 'ancestor', agent }`). Admission is synchronous and the effect asynchronous: it issues `Agent.cancel(cause, { keepInbox: true })` and returns without waiting for the target to observe the signal. Unclaimed pending inbox work, the Activation, and published descendants are preserved; work already claimed into the interrupted turn is not requeued. An absent target is an accepted no-op; a wrong parent address or a stale, self-targeting, or non-ancestor caller rejects with `UNAUTHORIZED`. | -| `reportFrom(child, content, { delivery, signal })` | Deliver one selected message from the exact live continuable child to its exact live direct parent and return the accepted stable `MessageId`. Quiet delivery injects context; waking delivery submits one later parent turn. | +| `reportFrom(child, content, { delivery, signal })` | Deliver one selected message from the exact live continuable child to its exact live direct parent and return the accepted stable `MessageId`. Quiet delivery injects next-step context without waking; next-step delivery steers and wakes the parent. | | `registerContinuableSetup(contribution)` | Compose an optional deployment capability into each continuable child's unpublished scope, with immediate revocation from resident children. | | `drainContinuableDescendants(parents)` | Close admission below exact live host-owned parent Agents, stop only their visible continuable descendants, await materializations admitted below those roots through publication or rollback, then release the selected forests child-first. The cutoff lasts until each exact parent leaves the registry; unrelated parent forests and manager-wide admission remain live. | | `listChildren(parentSessionId, signal?)` | List direct session-backed subagents with their `one-shot`/`continuable` mode, `running`/`inactive` activity, origin-classified one-level `hasChildren` hint, and per-child diagnostics, ordered by `createdAt` then id, without loading or resuming them. Reads the live session store and optional session persistence directly (live-only enumeration when persistence is absent) and requires the mounted `sessionProjections` registry; it does not require `ctx.agents`, the continuation manager, or any query service. | @@ -146,7 +146,7 @@ Prefix-stable within a child: the statement never changes during the child's lif - **ACP children remain one-shot and are not trace-enumerable** — an ACP run has no local child session in the parent's session corpus. An ACP `prepareContinuable` requires persisting the remote session id in provider-specific descriptor data and a per-child continuation advertisement, since ACP `loadSession` support is negotiated per child rather than established by the method's presence. Remote providers also require a separate Activation ownership contract with equivalent authenticated control and child-first quiescence before they support continuable children. - **No host-user continuation** — `followup()` requires the exact live direct parent. Only `interrupt()` accepts a durable parent-address user authority, because stopping a turn is idempotent and delivers no content; a future host adapter needs a concrete authenticated interaction before the seam gains a user delivery capability. -- **No current-turn steering** — continuable messages and waking reports enqueue later turns; neither redirects an open turn. +- **Continuation messages never steer** — parent-to-child continuation messages enqueue later child turns. Child-to-parent reports are independent next-step input and may extend the parent's open turn. - **Wake gap during cancellation convergence** — a waking follow-up accepted after the interrupt signal is issued but before the active driver becomes idle remains queued until another waking send. Issue #1838 owns the agent-loop wake latch, which also affects ordinary session cancellation. - **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. - **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. diff --git a/packages/subagent/subagent/README.zh.md b/packages/subagent/subagent/README.zh.md index 3ad2ee5738..241e5e8715 100644 --- a/packages/subagent/subagent/README.zh.md +++ b/packages/subagent/subagent/README.zh.md @@ -19,7 +19,7 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委 | `startContinuable(spec)` | 建立一个持久化的可继续子 agent,并投递其初始提示词。子 agent 的 inbox 一接受该提示词,调用就会兑现为 `{ childId, messageId }`,无需等待轮次开始,也无需等待消息写入会话日志。在此之前发生的任何失败都会使调用被拒绝,不返回任何 id,并完全回滚该子 agent。要求 `ctx.agents`、会话持久化以及具备 `prepareContinuable` 能力的提供方。 | | `followup(parent, childId, content, { source, signal })` | 将来自确切在线直接父级的一条后续消息作为子 agent 的下一个 FIFO 轮次投递,术语与 `Agent.followup()` 一致,并返回被接受的 `MessageId`。驻留中的子 agent 由其 inbox 直接接受(唤醒处于 waiting 的 Activation);不驻留的则从其持久化会话冷恢复。要求 `ctx.agents`;冷恢复还要求会话持久化。 | | `interrupt(targetSessionId, authority)` | 凭人类出示的持久化父级地址 `{ kind: 'user', parentSessionId }`,或确切在线的祖先 Agent `{ kind: 'ancestor', agent }` 进行授权,中断一个在线可继续子级的当前轮次。准入判定同步完成,但取消异步生效:该操作发出 `Agent.cancel(cause, { keepInbox: true })` 后立即返回,不等待目标观察到信号。尚未领取的待处理 inbox 工作、Activation 和已发布的后代均会保留;已经领取到被中断轮次中的工作不会重新入队。目标不存在时视为已接受的空操作;错误的父级地址,或陈旧、指向自身、并非祖先的调用方,会以 `UNAUTHORIZED` 被拒绝。 | -| `reportFrom(child, content, { delivery, signal })` | 从确切在线可继续 child 向其确切在线直接 parent 投递一条选中消息,并返回已接受的稳定 `MessageId`。静默投递会注入上下文;唤醒投递会提交一个后续 parent 轮次。 | +| `reportFrom(child, content, { delivery, signal })` | 从确切在线可继续 child 向其确切在线直接 parent 投递一条选中消息,并返回已接受的稳定 `MessageId`。静默投递会注入不唤醒的 next-step 上下文;next-step 投递会 steering 并唤醒 parent。 | | `registerContinuableSetup(contribution)` | 把一项可选部署能力组合到每个可继续 child 尚未发布的作用域中,并支持从驻留 child 立即撤销。 | | `drainContinuableDescendants(parents)` | 在由 host 拥有的确切在线父级 Agent 之下关闭准入,只停止这些父级可见的可继续后代;等待已在这些根节点下获准的物化过程完成发布或回滚后,再按子级优先顺序释放所选的各棵树。该截止状态会持续到每个确切父级离开注册表;无关的父级树仍在线,管理器全局准入仍保持开放。 | | `listChildren(parentSessionId, signal?)` | 按 `createdAt`、再按 id 的顺序列出由会话支撑的直接 subagent,包括其 `one-shot`/`continuable` 模式、`running`/`inactive` 活动状态、根据 origin 分类得出的一层 `hasChildren` 提示,以及每个子级的诊断信息,且不会加载或恢复它们。该操作直接读取在线会话存储和可选的会话持久化(没有持久化时只枚举在线子级),并要求已挂载 `sessionProjections` 注册表;不要求 `ctx.agents`、继续执行管理器或任何查询服务。 | @@ -146,7 +146,7 @@ You are a delegated subagent: your permission scope was fixed when you were star - **ACP 子 agent 仍为一次性,且无法通过追踪枚举**:ACP 运行在 parent 会话语料中没有本地 child 会话。ACP 的 `prepareContinuable` 需要在提供方专用描述符数据中持久化远端会话 id,以及逐子 agent 的继续执行能力声明,因为 ACP 的 `loadSession` 支持按子 agent 协商,而不是通过方法是否存在来确定。远程提供方还需要一份独立的 Activation 所有权约定,具备等效的经认证控制和子先于父的完全停稳保证,才能支持可继续子 agent。 - **无 host-user 继续执行**:`followup()` 要求确切在线直接父级。只有 `interrupt()` 接受持久化 parent 地址形式的用户授权,因为停止一个轮次是幂等的且不投递任何内容;未来 host 适配器需要具体的经认证交互,才能让该 seam 获得用户投递能力。 -- **不对当前轮次进行 steering**:可继续消息和唤醒式 report 会排入后续轮次,均不会重定向正在进行的轮次。 +- **继续执行消息绝不 steering**:parent 到 child 的继续执行消息会排入后续 child 轮次。child 到 parent 的 report 是独立的 next-step 输入,可能延长 parent 已打开的轮次。 - **取消收敛期间存在唤醒缺口**:中断信号发出后、活动 driver 进入 idle 前被接受的唤醒型 follow-up 会保持排队,直到另一条唤醒发送到达。Issue #1838 负责 agent-loop 的唤醒锁存;普通会话取消也受此影响。 - **驻留仅限进程内**:Activation inbox 与所有权图不会在两个 harness 进程之间协调;对单个持久化存储的并发访问仍然需要持久化邮箱和跨进程租约协议。 - **不回放已接受但未记录的消息**:只有写入子 agent 会话日志的消息才能连同提供该消息的来源一起重建。崩溃可能丢失从未写入日志、已被接受的初始提示词或后续消息;此后一条经授权的消息可以冷恢复该子 agent,但丢失的消息不会自动回放。 diff --git a/packages/subagent/subagent/src/continuation.ts b/packages/subagent/subagent/src/continuation.ts index d826efbfdb..74e1bf4b0b 100644 --- a/packages/subagent/subagent/src/continuation.ts +++ b/packages/subagent/subagent/src/continuation.ts @@ -98,7 +98,7 @@ declare module '@deepseek-ai/dsh-llm' { } /** Deployment scheduling policy for accepted child reports. */ -export type SubagentReportDelivery = 'quiet' | 'wakeup' +export type SubagentReportDelivery = 'quiet' | 'next-step' /** Options for one continuable child's report to its direct parent. */ export interface SubagentReportOptions { @@ -644,7 +644,7 @@ export class SubagentContinuationManager { senderSessionId: activation.childId, }, }) - if (delivery === 'wakeup') { + if (delivery === 'next-step') { this.sendWaking(parent, message, () => { this.sendReport(parent, message, delivery) }) } else { this.sendReport(parent, message, delivery) @@ -656,7 +656,7 @@ export class SubagentContinuationManager { * Perform one waking send to a parent, accounted against that parent's own * Activation when it has one. Registering the id before the send is what * keeps a continuation-managed parent from being judged quiescent in the - * window between `followup()` and the microtask that admits it. + * window between a waking send and the microtask that admits it. * @param parent - the exact live parent receiving the waking message. * @param message - the message whose id is accounted. * @param send - the synchronous waking send to perform. @@ -681,7 +681,7 @@ export class SubagentContinuationManager { delivery: SubagentReportDelivery, ): void { try { - if (delivery === 'wakeup') parent.followup(message) + if (delivery === 'next-step') parent.steer(message) else parent.inject(message) } catch (error: unknown) { throw new SubagentError( @@ -1158,7 +1158,7 @@ export class SubagentContinuationManager { messageId: MessageId, send: () => void, ): MessageId { - // `Agent.followup()` publishes inbox events synchronously, so observers must + // Waking Agent sends publish inbox events synchronously, so observers must // see this Activation as busy before the call begins. activation.accepted.add(messageId) try { diff --git a/packages/subagent/subagent/tests/continuation.spec.ts b/packages/subagent/subagent/tests/continuation.spec.ts index 07f497628d..f69c328873 100644 --- a/packages/subagent/subagent/tests/continuation.spec.ts +++ b/packages/subagent/subagent/tests/continuation.spec.ts @@ -1524,6 +1524,42 @@ function settlementNotices(agent: Agent): { sender: string; text: string; summar }) } +describe('continuable report delivery', () => { + it('wakes an idle parent for a next-step report', async () => { + const releaseChild = Promise.withResolvers() + const adapter = new GatedAdapter([ + { chunks: textResponse('child answer'), gate: releaseChild.promise }, + { chunks: textResponse('parent report ack') }, + { chunks: textResponse('parent settlement ack') }, + ]) + const { ctx, parent } = await setupWith(adapter) + const started = await ctx.subagents.startContinuable(startSpec(parent)) + await vi.waitFor(() => { + expect(adapter.requests.filter(request => request.sessionId === started.childId)).toHaveLength(1) + }) + const child = ctx.agents.get(started.childId) + expect(child).toBeDefined() + + const messageId = await ctx.subagents.reportFrom(child!, message('an explicit report'), { + delivery: 'next-step', + signal: testSignal, + }) + + await vi.waitFor(() => { + expect(adapter.requests.filter(request => request.sessionId === parent.id)).toHaveLength(1) + }) + const report = parent.session.events.flatMap(event => event.type === 'user/message' + && event.data.source.kind === 'subagent-report' ? [event.data] : [])[0] + expect(report?.id).toBe(messageId) + + releaseChild.resolve(undefined) + await waitNoActivation(ctx, started.childId) + await vi.waitFor(() => { + expect(adapter.requests.filter(request => request.sessionId === parent.id)).toHaveLength(2) + }) + }) +}) + describe('continuable settlement delivery', () => { it('tells the parent what the child finished with, without being asked', async () => { const { ctx, parent } = await setup([textResponse('the answer'), textResponse('parent ack')]) diff --git a/packages/subagent/tool-subagent-report/README.i18n.yaml b/packages/subagent/tool-subagent-report/README.i18n.yaml index f96d1d5031..129ba8f92f 100644 --- a/packages/subagent/tool-subagent-report/README.i18n.yaml +++ b/packages/subagent/tool-subagent-report/README.i18n.yaml @@ -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 packages/subagent/tool-subagent-report/README.md -README.md: b38b6b541d9c03174de1515df7a0babe5da055b4 -README.zh.md: d8557a43bca4b9a403aac31b24812425e0586fcf +README.md: ab2bcc5e4b829b66810da4089edb7146ebc1c791 +README.zh.md: c9ae647a44cad8c04603877dddd81d3fbe5be0e3 diff --git a/packages/subagent/tool-subagent-report/README.md b/packages/subagent/tool-subagent-report/README.md index b38b6b541d..ab2bcc5e4b 100644 --- a/packages/subagent/tool-subagent-report/README.md +++ b/packages/subagent/tool-subagent-report/README.md @@ -6,7 +6,7 @@ The optional child-scoped `report` tool is a thin adapter over `ctx.subagents.re The child-scoped `tool:report` prompt section instructs the child to call `report` once before finishing, with a self-contained answer, and earlier whenever a partial finding changes what the parent should do next. The instruction is guidance, not enforcement: the mechanism still accepts zero or many calls in one turn, and no runtime path rejects a child that never reports. A successful call neither concludes the turn, settles the Activation, nor prevents later parent follow-ups, and finishing a turn never reports automatically. The tool accepts no recipient: `exec.agent` is the sender's exact live Agent and the authority credential, and the service derives the sole recipient from that child's durable `parentSession`. Success returns the stable `MessageId` of the parent-accepted message, not a read receipt, an inbox-occurrence id, a parent-log acknowledgement, a turn-completion receipt, or a persistence flush. A parent absent from the registry fails the call with `direct parent is not live; report was not delivered` — registry presence governs parent resolution, and a registered parent already in host-owned disposal still accepts while its log admits appends. The service performs no injection, parent cold resume, or offline mailbox write; the durable child transcript remains the recovery source, and a failed tool call does not prove non-delivery (a later `tools/post-execute` veto can fail a call whose report was already accepted). -`reportDelivery` selects parent scheduling for every accepted report. `wakeup` (the default) uses `parent.followup()`, creating exactly one ordinary later parent turn and waking a parked parent driver; it never steers an open turn. It is the default because a parent that already parked has no other reason to look, so quiet delivery would leave an accepted report unread until something unrelated woke it. `quiet` uses `parent.inject()`, adding model-facing context without starting a parent model request: an idle parent's append completes before the call returns, while a report reaching an admitting or running parent stages for the next safe log position. This is deployment scheduling policy, so the model-facing schema cannot select or override it per call. +`reportDelivery` selects parent scheduling for every accepted report. `next-step` (the default) uses `parent.steer()`: a running parent receives the report at its nearest safe step boundary, while an idle parent starts a turn. Reports accepted in sequence share the next-step FIFO, including the later manager-authored settlement notice, so the parent cannot observe settlement before an earlier report; reports waiting together enter one claimed batch. `quiet` uses `parent.inject()`, adding the same next-step context without waking a parked parent. This is deployment scheduling policy, so the model-facing schema cannot select or override it per call. Scope-local registration deliberately survives the child's global `toolFilter`, so a delegation allow-list cannot remove the only return channel. A deployment that requires a child with no return channel omits this package. @@ -36,7 +36,7 @@ Prefix-stable within a child; neither the schema nor the section changes at runt #### Token effect -One short acknowledgement per call in the reporting child. The reported content is additionally billed to the parent: waking delivery makes it the sole ordinary message of one new parent turn, while quiet delivery adds it to the parent's next request. +One short acknowledgement per call in the reporting child. The reported content is additionally billed to the parent: next-step delivery joins the next request in an open parent turn or starts a turn for an idle parent, while quiet delivery waits for another input to wake the parent. #### KV Cache effect @@ -54,7 +54,7 @@ The child's complete `output` plus the one-line frame, uncapped by this package. #### KV Cache effect -Append-only; the report follows the parent's reusable request prefix. Waking delivery starts an independent parent model request, while quiet delivery does not. +Append-only; the report follows the parent's reusable request prefix. Next-step delivery wakes the parent and may extend its open turn, while quiet delivery does not wake it. ## Known Limitations and Deferred Work @@ -63,4 +63,4 @@ Append-only; the report follows the parent's reusable request prefix. Waking del - **A staged quiet report is not immediately reconstructable** — acceptance returns its stable `MessageId`, but the parent Session reconstructs the framed content only after pending context reaches its ordinary log boundary. - **Granting waits for the next Activation; revocation is immediate** — installing this package after a child becomes resident grants `report` and its guidance only on that child's next Activation, while removing the package revokes both from resident children immediately. - **Nested reporting reaches exactly one edge upward** — a grandchild reports to its direct child parent, never to the top-level coordinator, which must explicitly report a derived update later. -- **No rate limiting** — the default `wakeup` mode can amplify model work when nested children report frequently; a deployment that accepts unread reports over that amplification selects `quiet`. +- **No rate limiting** — the default `next-step` mode can amplify model work when nested children report frequently, although reports waiting together share one step; a deployment that accepts unread reports over that amplification selects `quiet`. diff --git a/packages/subagent/tool-subagent-report/README.zh.md b/packages/subagent/tool-subagent-report/README.zh.md index d8557a43bc..c9ae647a44 100644 --- a/packages/subagent/tool-subagent-report/README.zh.md +++ b/packages/subagent/tool-subagent-report/README.zh.md @@ -6,7 +6,7 @@ 子级作用域的 `tool:report` 提示词 section 要求子级在结束前调用一次 `report` 并给出自足的答案,并在部分发现会改变父级下一步动作时提前上报。该指令是引导而非强制:机制本身仍接受一个轮次中调用零次或多次,也没有任何运行时路径会拒绝从不上报的子级。调用成功既不会结束轮次或结算 Activation,也不会阻止父级后续消息;轮次结束也绝不会自动上报。该工具不接受接收方参数:`exec.agent` 是发送方确切在线的 Agent,也是权限凭据;服务根据该子级持久化的 `parentSession` 推导唯一接收方。成功时返回父级已接受消息的稳定 `MessageId`,不表示已读回执、inbox 中该次出现的 id、父级日志确认、轮次完成回执或持久化刷盘。父级解析由注册表中的存在性决定:父级不在注册表时,调用失败并返回 `direct parent is not live; report was not delivered`;已开始由宿主管理的 dispose(资源释放)但仍在注册表中的父级在其日志仍接受追加时仍会接受。服务不会执行注入、父级冷恢复或离线 mailbox 写入;持久化子级 transcript(文本记录)仍是恢复依据,且工具调用失败不能证明未送达(后续 `tools/post-execute` 否决可能让报告已被接受的调用以失败结束)。 -`reportDelivery` 为每条已接受的报告选择父级调度方式。`wakeup`(默认值)使用 `parent.followup()`,恰好创建一个普通的后续父级轮次,并唤醒停驻的父级驱动;它绝不会对正在运行的轮次进行 steering(中途引导)。之所以作为默认值:已经停驻的父级没有别的理由再去查看,静默投递会让一条已被接受的报告一直无人阅读,直到别的事件把父级唤醒。`quiet` 使用 `parent.inject()`,在不启动父级模型请求的情况下添加面向模型的上下文:父级空闲时,追加操作会在调用返回前完成;报告到达正在准入或运行的父级时,则会暂存到下一个安全日志位置。这是部署调度策略,因此面向模型的 schema 不能在单次调用中选择或覆盖该策略。 +`reportDelivery` 为每条已接受的报告选择父级调度方式。`next-step`(默认值)使用 `parent.steer()`:运行中的父级会在最近的安全 step 边界收到报告,空闲父级则会启动一个轮次。按顺序接受的报告会共享 next-step FIFO,稍后由管理器撰写的结算通知也进入该队列,因此父级不会在更早的报告之前看到结算;一起等待的报告会进入同一个领取批次。`quiet` 使用 `parent.inject()`,添加相同的 next-step 上下文,但不会唤醒停驻的父级。这是部署调度策略,因此面向模型的 schema 不能在单次调用中选择或覆盖该策略。 作用域局部注册有意不受子级全局 `toolFilter` 影响,因此委派允许列表无法移除唯一的返回通道。需要子级不具备返回通道的部署应省略本包。 @@ -36,7 +36,7 @@ #### Token 影响 -每次调用都会在执行上报的子级中产生一条简短确认消息。父级还会为上报内容支付 token 成本:唤醒投递会使该内容成为一个新父级轮次中唯一的普通消息,静默投递则把内容加入父级的下一次请求。 +每次调用都会在执行上报的子级中产生一条简短确认消息。父级还会为上报内容支付 token 成本:next-step 投递会加入父级已打开轮次的下一次请求,或为空闲父级启动一个轮次;静默投递则等待其他输入唤醒父级。 #### KV Cache 影响 @@ -54,7 +54,7 @@ #### KV Cache 影响 -仅追加;报告位于父级可复用请求前缀之后。唤醒投递会启动一次独立的父级模型请求,静默投递则不会。 +仅追加;报告位于父级可复用请求前缀之后。next-step 投递会唤醒父级,并可能延长其已打开的轮次;静默投递则不会唤醒父级。 ## 已知限制与暂缓事项 @@ -63,4 +63,4 @@ - **暂存的静默报告无法立即重建**:接受时会返回其稳定 `MessageId`,但只有当待处理上下文到达普通日志边界后,父级会话才能重建带前缀的内容。 - **授权须等到下一个 Activation,撤销则立即生效**:子级驻留后再安装本包,只会在该子级的下一个 Activation 中授予 `report` 及其指引;移除本包则会立即从驻留子级撤销两者。 - **嵌套上报只向上到达一条直接边**:孙级只向作为其直接父级的子级上报,不会直接到达顶层协调器;该直接父级必须随后显式发出一条衍生更新。 -- **没有速率限制**:嵌套子级频繁上报时,默认的 `wakeup` 模式会放大模型工作量;宁可接受报告无人阅读也要避免这种放大的部署应选择 `quiet`。 +- **没有速率限制**:嵌套子级频繁上报时,默认的 `next-step` 模式会放大模型工作量,但一起等待的报告会共享一个 step;宁可接受报告无人阅读也要避免这种放大的部署应选择 `quiet`。 diff --git a/packages/subagent/tool-subagent-report/src/index.ts b/packages/subagent/tool-subagent-report/src/index.ts index 6f2a6b3c6a..85b139e641 100644 --- a/packages/subagent/tool-subagent-report/src/index.ts +++ b/packages/subagent/tool-subagent-report/src/index.ts @@ -26,15 +26,15 @@ const REPORT_SECTION_ORDER = 117 /** Config: how accepted reports are scheduled on the parent. */ export interface Config { /** - * Parent scheduling (default `wakeup`). `wakeup` creates one ordinary later - * parent turn; `quiet` adds context without waking, so a parked parent learns - * of the report only when something else wakes it. + * Parent scheduling (default `next-step`). `next-step` wakes the parent and + * enters at its nearest step boundary; `quiet` adds the same context without + * waking, so a parked parent waits for another waking input. */ reportDelivery?: SubagentReportDelivery } export const Config: z = z.object({ - reportDelivery: z.union(['quiet', 'wakeup'] as const).default('wakeup'), + reportDelivery: z.union(['quiet', 'next-step'] as const).default('next-step'), }) /** diff --git a/packages/subagent/tool-subagent-report/tests/tool-subagent-report.spec.ts b/packages/subagent/tool-subagent-report/tests/tool-subagent-report.spec.ts index b71e8365db..28d6068757 100644 --- a/packages/subagent/tool-subagent-report/tests/tool-subagent-report.spec.ts +++ b/packages/subagent/tool-subagent-report/tests/tool-subagent-report.spec.ts @@ -7,7 +7,7 @@ import type { Agent } from '@deepseek-ai/dsh-agent' import { assembleContextFor } from '@deepseek-ai/dsh-agent' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' -import { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm' +import { CallId, LlmAdapter, createUserMessage } from '@deepseek-ai/dsh-llm' import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' import { SessionId } from '@deepseek-ai/dsh-session' import type { SessionEvent } from '@deepseek-ai/dsh-session' @@ -20,22 +20,39 @@ import * as tool from '../src/index.ts' const testSignal = new AbortController().signal -/** Adapter that keeps child Activations resident until released. */ +/** Adapter that keeps selected Agent requests open until released. */ class HeldAdapter extends LlmAdapter { readonly requests: GenerateOptions[] = [] - private readonly gate = Promise.withResolvers() + private readonly gates = new Map>() + private readonly releasedSessions = new Set() + private released = false async * stream(options: GenerateOptions): AsyncIterable { this.requests.push(options) - await this.gate.promise + if (!this.released && !this.releasedSessions.has(options.sessionId)) { + let gate = this.gates.get(options.sessionId) + if (gate === undefined) { + gate = Promise.withResolvers() + this.gates.set(options.sessionId, gate) + } + await gate.promise + } for (const chunk of textResponse('held answer')) { if (options.signal?.aborted) throw new Error('aborted') yield chunk } } - release(): void { - this.gate.resolve(undefined) + release(sessionId?: SessionId): void { + if (sessionId !== undefined) { + this.releasedSessions.add(sessionId) + this.gates.get(sessionId)?.resolve(undefined) + this.gates.delete(sessionId) + return + } + this.released = true + for (const gate of this.gates.values()) gate.resolve(undefined) + this.gates.clear() } } @@ -86,6 +103,17 @@ async function startChild(ctx: Context, parent: Agent, prompt = 'child task') { return { started, child } } +/** Start one parent request that remains open in the held adapter. */ +async function startHeldParentTurn(parent: Agent, adapter: HeldAdapter): Promise { + parent.followup(createUserMessage({ + content: [{ type: 'text', text: 'parent work' }], + source: { kind: 'user' }, + })) + await vi.waitFor(() => { + expect(adapter.requests.some(request => request.sessionId === parent.id)).toBe(true) + }) +} + let calls = 0 function callReport(ctx: Context, child: Agent, output: string, signal = testSignal) { return ctx.tools.execute({ @@ -204,8 +232,8 @@ describe('dsh-tool-subagent-report', () => { expect(adapter.requests.filter(request => request.sessionId === parent.id)).toHaveLength(parentRequests) }) - it('queues wakeup reports as one later parent turn', async () => { - const { ctx, parent, adapter } = await setup({ config: { reportDelivery: 'wakeup' } }) + it('delivers next-step reports through waking steering', async () => { + const { ctx, parent, adapter } = await setup({ config: { reportDelivery: 'next-step' } }) const { child } = await startChild(ctx, parent) const enqueues: string[] = [] ctx.on('agent/inbox/inserted', ({ agent, message }) => { @@ -216,19 +244,37 @@ describe('dsh-tool-subagent-report', () => { const result = await callReport(ctx, child, 'WAKE_UP') expect(result.isError).toBe(false) - expect(enqueues).toEqual(['queued']) + expect(enqueues).toEqual(['steering']) await vi.waitFor(() => { expect(adapter.requests.some(request => request.sessionId === parent.id)).toBe(true) }) }) - it('preserves accepted order across repeated reports', async () => { - const { ctx, parent } = await setup() + it('batches repeated next-step reports in accepted order', async () => { + const { ctx, parent, adapter } = await setup({ config: { reportDelivery: 'next-step' } }) + await startHeldParentTurn(parent, adapter) const { child } = await startChild(ctx, parent) expect((await callReport(ctx, child, 'FIRST')).isError).toBe(false) expect((await callReport(ctx, child, 'SECOND')).isError).toBe(false) expect(reports(parent).map(report => report.text.split('\n').at(-1))).toEqual(['FIRST', 'SECOND']) + expect(parent.inbox.nextStep).toHaveLength(2) + }) + + it('keeps a report before the child settlement in one busy-parent batch', async () => { + const { ctx, parent, adapter } = await setup({ config: { reportDelivery: 'next-step' } }) + await startHeldParentTurn(parent, adapter) + const { started, child } = await startChild(ctx, parent) + + expect((await callReport(ctx, child, 'ORDERED_REPORT')).isError).toBe(false) + adapter.release(started.childId) + await vi.waitFor(() => { expect(ctx.agents.get(started.childId)).toBeUndefined() }) + + expect(parent.inbox.nextStep.map(message => message.source.kind)).toEqual([ + 'subagent-report', + 'subagent-settled', + ]) + expect(parent.inbox.nextTurn).toHaveLength(0) }) it('keeps an accepted report after the child settles', async () => { @@ -261,8 +307,8 @@ describe('dsh-tool-subagent-report', () => { expect(reports(child)[0]?.text).toContain('FROM_GRANDCHILD') }) - it('accounts wakeup reports delivered to a resident continuable parent', async () => { - const { ctx, parent, adapter } = await setup({ config: { reportDelivery: 'wakeup' } }) + it('accounts next-step reports delivered to a resident continuable parent', async () => { + const { ctx, parent, adapter } = await setup({ config: { reportDelivery: 'next-step' } }) const { child } = await startChild(ctx, parent, 'outer task') const { started: grandchildStart, child: grandchild } = await startChild(ctx, child, 'inner task') @@ -509,9 +555,10 @@ describe('dsh-tool-subagent-report', () => { expect('default' in tool).toBe(false) expect(tool.name).toBe('tool-subagent-report') expect(tool.inject).toEqual(['subagents', 'tools', 'systemPrompt']) - // Waking is the default because a report that never wakes its parent - // cannot deliver a result to an agent that already parked. - expect(tool.Config({}).reportDelivery).toBe('wakeup') + // Next-step delivery wakes a parked parent and lets a running parent act at + // its nearest safe boundary. + expect(tool.Config({}).reportDelivery).toBe('next-step') + expect(() => tool.Config({ reportDelivery: 'wakeup' } as never)).toThrow() expect(() => tool.Config({ reportDelivery: 'shout' } as never)).toThrow() }) @@ -526,7 +573,7 @@ describe('dsh-tool-subagent-report', () => { }) expect((await callReport(ctx, child, 'DEFAULT_WAKES')).isError).toBe(false) - expect(enqueues).toEqual(['queued']) + expect(enqueues).toEqual(['steering']) await vi.waitFor(() => { expect(adapter.requests.some(request => request.sessionId === parent.id)).toBe(true) })