Selaa lähdekoodia

Merge remote-tracking branch 'origin/master' into fix/model-discovery-profile-headers

Yichen Jiang 3 viikkoa sitten
vanhempi
sitoutus
8fa464890c
100 muutettua tiedostoa jossa 911 lisäystä ja 1661 poistoa
  1. 2 2
      .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.i18n.yaml
  2. 16 24
      .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md
  3. 19 27
      .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-08-25-sparse-first-party-prompt-section-orders.i18n.yaml
  5. 1 1
      .agents/notes/implemented/architecture/2026-08-25-sparse-first-party-prompt-section-orders.md
  6. 1 1
      .agents/notes/implemented/architecture/2026-08-25-sparse-first-party-prompt-section-orders.zh.md
  7. 3 3
      .agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.i18n.yaml
  8. 82 0
      .agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.md
  9. 82 0
      .agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md
  10. 2 2
      .agents/notes/implemented/bug-fix/2026-08-10-child-agents-join-their-parent-preset.i18n.yaml
  11. 3 3
      .agents/notes/implemented/bug-fix/2026-08-10-child-agents-join-their-parent-preset.md
  12. 3 3
      .agents/notes/implemented/bug-fix/2026-08-10-child-agents-join-their-parent-preset.zh.md
  13. 3 3
      .agents/notes/implemented/bug-fix/2026-08-17-subagent-message-settlement-ordering.i18n.yaml
  14. 44 0
      .agents/notes/implemented/bug-fix/2026-08-17-subagent-message-settlement-ordering.md
  15. 44 0
      .agents/notes/implemented/bug-fix/2026-08-17-subagent-message-settlement-ordering.zh.md
  16. 0 44
      .agents/notes/implemented/bug-fix/2026-08-17-subagent-report-settlement-ordering.md
  17. 0 44
      .agents/notes/implemented/bug-fix/2026-08-17-subagent-report-settlement-ordering.zh.md
  18. 2 2
      .agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.i18n.yaml
  19. 1 1
      .agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md
  20. 1 1
      .agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.zh.md
  21. 2 2
      .agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.i18n.yaml
  22. 5 7
      .agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md
  23. 5 7
      .agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.zh.md
  24. 0 118
      .agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.md
  25. 0 118
      .agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.zh.md
  26. 0 6
      .agents/notes/implemented/feature/2026-08-06-continuable-child-report-obligation.i18n.yaml
  27. 0 60
      .agents/notes/implemented/feature/2026-08-06-continuable-child-report-obligation.md
  28. 0 60
      .agents/notes/implemented/feature/2026-08-06-continuable-child-report-obligation.zh.md
  29. 2 2
      .agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.i18n.yaml
  30. 9 9
      .agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.md
  31. 9 9
      .agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.zh.md
  32. 2 2
      .agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.i18n.yaml
  33. 2 2
      .agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.md
  34. 2 2
      .agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.zh.md
  35. 2 2
      .agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.i18n.yaml
  36. 6 6
      .agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.md
  37. 6 6
      .agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.zh.md
  38. 2 2
      .agents/notes/implemented/simplification/2026-07-27-intent-named-subagent-continuation-operations.i18n.yaml
  39. 6 6
      .agents/notes/implemented/simplification/2026-07-27-intent-named-subagent-continuation-operations.md
  40. 6 6
      .agents/notes/implemented/simplification/2026-07-27-intent-named-subagent-continuation-operations.zh.md
  41. 0 3
      apps/cli/composition.md
  42. 0 1
      apps/cli/package.json
  43. 2 2
      apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/child.expected.jsonl
  44. 2 2
      apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/parent.override.json
  45. 4 4
      apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl
  46. 0 3
      apps/web/tests/agent-team-panel.overlay.yml
  47. 2 0
      apps/web/tests/goal-command-presentation.e2e.ts
  48. 13 0
      apps/web/tests/scaffold.ts
  49. 2 1
      apps/web/tests/subagent-conversation.e2e.ts
  50. 20 14
      apps/web/tests/subagent-interrupt-ui.e2e.ts
  51. 3 1
      apps/web/tests/subagent-interrupt.e2e.ts
  52. 2 2
      docs/config-catalog.i18n.yaml
  53. 1 23
      docs/config-catalog.md
  54. 0 22
      docs/config-catalog.zh.md
  55. 2 2
      docs/event-producer-consumer.i18n.yaml
  56. 4 4
      docs/event-producer-consumer.md
  57. 4 4
      docs/event-producer-consumer.zh.md
  58. 2 2
      docs/module-graph.i18n.yaml
  59. 0 6
      docs/module-graph.md
  60. 0 6
      docs/module-graph.zh.md
  61. 2 2
      docs/subsystems/subagent.i18n.yaml
  62. 30 86
      docs/subsystems/subagent.md
  63. 29 85
      docs/subsystems/subagent.zh.md
  64. 2 2
      docs/tool-catalog.i18n.yaml
  65. 6 34
      docs/tool-catalog.md
  66. 6 34
      docs/tool-catalog.zh.md
  67. 3 7
      packages/bundle/base/cordis.patch.yml
  68. 0 1
      packages/bundle/base/package.json
  69. 0 6
      packages/bundle/web-app/cordis.patch.yml
  70. 4 4
      packages/client/ui-chat/tests/chat-branch-tails.client.spec.tsx
  71. 3 3
      packages/core/tools/src/index.ts
  72. 1 1
      packages/core/tools/tests/gen-tool-catalog.spec.ts
  73. 2 2
      packages/experimental/agent-team-profile/README.i18n.yaml
  74. 1 1
      packages/experimental/agent-team-profile/README.md
  75. 1 1
      packages/experimental/agent-team-profile/README.zh.md
  76. 0 3
      packages/experimental/agent-team-profile/cordis.patch.yml
  77. 0 1
      packages/experimental/agent-team-profile/tests/profile.spec.ts
  78. 4 2
      packages/experimental/agent-team/src/mailbox.ts
  79. 26 22
      packages/experimental/agent-team/tests/team.spec.ts
  80. 6 1
      packages/experimental/tool-agent-team/tests/tool-team.spec.ts
  81. 9 34
      packages/extensions/tool-cordis/src/api-catalog.ts
  82. 2 8
      packages/preset/agent-presets/presets/cordis/agent.cordis.yml
  83. 2 2
      packages/preset/agent-presets/presets/ptc/agent.cordis.yml
  84. 2 8
      packages/preset/agent-presets/presets/standard/agent.cordis.yml
  85. 2 2
      packages/subagent/README.i18n.yaml
  86. 2 3
      packages/subagent/README.md
  87. 2 3
      packages/subagent/README.zh.md
  88. 2 2
      packages/subagent/subagent-fork-in-process/README.i18n.yaml
  89. 4 4
      packages/subagent/subagent-fork-in-process/README.md
  90. 4 4
      packages/subagent/subagent-fork-in-process/README.zh.md
  91. 0 6
      packages/subagent/subagent-fork-in-process/src/index.ts
  92. 2 2
      packages/subagent/subagent/README.i18n.yaml
  93. 12 11
      packages/subagent/subagent/README.md
  94. 12 11
      packages/subagent/subagent/README.zh.md
  95. 4 0
      packages/subagent/subagent/package.json
  96. 0 185
      packages/subagent/subagent/src/activation-setup-registry.ts
  97. 153 151
      packages/subagent/subagent/src/continuation.ts
  98. 52 67
      packages/subagent/subagent/src/index.ts
  99. 79 0
      packages/subagent/subagent/src/internal.ts
  100. 0 163
      packages/subagent/subagent/tests/activation-setup-registry.spec.ts

+ 2 - 2
.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: ba1e99c4d78d14230a2199cd7fb3c3eb9d3ad754
-2026-08-10-fork-children-stay-one-shot.zh.md: 0d4e214be952f5a4d96c296f38a5b0dbfbd85c86
+2026-08-10-fork-children-stay-one-shot.md: 0edf6ca9347dcf57313365e570daed22589f69fd
+2026-08-10-fork-children-stay-one-shot.zh.md: e832846d4018cf328e02c693bf21fe47a401d320

+ 16 - 24
.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md

@@ -1,4 +1,4 @@
-# Agent Note: Forked children stay one-shot
+# Agent Note: Forked children preserve the parent request prefix
 
 Status: implemented
 
@@ -6,44 +6,36 @@ English | [中文](2026-08-10-fork-children-stay-one-shot.zh.md)
 
 ## Problem
 
-Fork's only difference from spawn is that the child Session is seeded with the parent's completed-turn prefix ([subagent-fork-in-process](../../../../packages/subagent/subagent-fork-in-process/README.md)). That seed costs real tokens — the inherited history is re-sent in every child request — and its one concrete payoff is provider-side prefix reuse: under the same provider and model, a child request whose leading bytes are identical to the parent's re-prefills none of the shared span. Anything a child scope adds *ahead* of the inherited history spends that payoff, because reuse stops at the first differing byte.
+Fork differs from spawn by seeding the child Session with the parent's completed-turn prefix. That seed costs tokens, and its intended payoff is provider-side prefix reuse: under the same provider and model, a child request whose leading bytes match the parent's does not prefill the shared span again. A child-only system-prompt section or tool schema ahead of the inherited history defeats that payoff.
 
-The child-scoped `report` return channel is now the largest such addition, and since [the report obligation](../feature/2026-08-06-continuable-child-report-obligation.md) it is two deltas rather than one: the `report` tool schema and the `tool:report` system-prompt section. Both live in the request head — the system block and the tool block precede every message — so a continuable forked child invalidates reuse before the first inherited turn and re-prefills the whole transcript it was forked to reuse. That composition pays fork's duplication cost and collects none of its benefit, while the parent still holds a reusable prefix the child could have shared.
+The earlier shipped composition avoided this mismatch by keeping forked children one-shot. That restriction was a consequence of the former child-only return tool, not an intrinsic property of continuable fork.
 
 ## Decision
 
-Every shipped composition inherits the fork delegation tool's `backgroundMode: one-shot` from the [base bundle](../../../../packages/bundle/base/cordis.patch.yml). The base bundle leaves `run_in_background` available because it also mounts the task service needed to settle background work.
+The model-facing `send_message` tool is registered globally for every Agent in a composition. A continuable forked child therefore receives the same tool name, description, schema, and ordering as its parent. Its initial task is appended after the inherited Session seed, and the task includes the direct parent id plus guidance to return results with `send_message({ agent_id, message })` when that tool is visible to the child.
 
-One-shot children — foreground and background alike — are created through `SubagentRuntime.start()`, which never enters the continuable activation-setup registry, so neither `report` nor its prompt section is installed. A forked one-shot child's system prompt and tool schemas therefore equal its parent's, apart from the `persona` and `toolFilter` deltas a deployment opts into per delegation tool.
+The base and headless compositions retain one-shot fork as their conservative lifecycle policy. The `cordis`, `standard`, and `ptc` CLI presets may bind fork to the continuable lifecycle because that binding no longer inserts child-only request-head fields. `ForkInProcessProvider.prepareContinuable()` and `ctx.subagents.startContinuable()` remain the implementation seam for those presets.
 
-`spawn` keeps `backgroundMode: continuable`. Continuable children and the report obligation ship unchanged for the provider whose child starts with no inherited prefix to protect, so this decision costs the report channel nothing.
-
-### The restriction is composition, not code
-
-`ForkInProcessProvider.prepareContinuable` stays implemented and `ctx.subagents.startContinuable()` still accepts `fork`; only the shipped `cordis.yml` rows changed. `tool-subagent` knows both the provider's `inheritsParentContext` and its own `backgroundMode` at mount, so a load-time rejection of the pair was available and is deliberately not added: the pair is not wrong in general. It is wrong only while a child-scope delta precedes inherited history, and the package that creates that delta — [`dsh-tool-subagent-report`](../../../../packages/subagent/tool-subagent-report/README.md) — is separately installable and, by its own design, invisible to `tool-subagent`. A deployment that omits the report package can run continuable forked children with the prefix intact. Encoding one roster's consequence as a delegation-tool invariant would make the tool assert something it cannot observe.
-
-The reintroduction condition is recorded as a `TODO(fork-continuable-prefix-reuse)` marker on `prepareContinuable` itself, the one method the shipped compositions do not call, and tracked as issue #2124: continuable fork reopens when a child's system prompt and tool schemas can match its parent's byte for byte.
+Byte-identical prefix reuse is qualified by explicit deployment choices. A fork delegation that applies a child persona or `toolFilter` may still change the request head. In particular, filtering out `send_message` removes both the schema and the return guidance from the child; the runtime does not bypass an explicit allow-list.
 
 ## Alternatives considered
 
-**Reject `inheritsParentContext` + `continuable` at mount.** A loud load-time failure would prevent silent reintroduction, which is what the configuration change cannot do. Rejected because the delegation tool cannot see the report package and the combination is legitimate without it; the invariant would be false for a deployment that never installs a child-scope delta, and `tool-subagent` would be asserting a fact owned by the roster.
-
-**Stop mounting the fork provider at all.** This was the broader form of the restriction. Rejected because foreground fork *is* the prefix-reusing case and is untouched by the report channel, so a full ban gives up the capability without buying anything the one-shot binding does not already buy — and would leave no shipped composition exercising session seeding.
+**Keep every fork one-shot.** This preserves the prefix but unnecessarily gives up durable, multi-turn forked children after the child-only schema difference is gone.
 
-**Ship continuable forked children and accept the loss.** Rejected because the loss is total rather than marginal: reuse breaks ahead of the inherited history, so the child pays full prefill on a transcript it duplicated for the sole purpose of not paying it. A deployment that wants a long-lived child with no inherited context already has `spawn`.
+**Install a child-only return alias.** A recipient-free alias would make child calls shorter, but it would recreate a tool-schema and prompt delta before inherited history and duplicate the adjacent-Agent operation.
 
-**Make `report` visible to every Agent.** A global registration would restore byte-identical prefixes by giving parent and child the same schema and section. Rejected because roots, one-shot children, remote children, and agentless callers would advertise a tool with no derivable recipient, and execution-time rejection would make schema visibility disagree with authority — the scope-local decision the [report tool Agent Note](../feature/2026-07-30-continuable-subagent-report-tool.md) already settled.
+**Add the return instruction to the system prompt.** This would place child-only bytes ahead of inherited messages. Appending it to the initial user task preserves the inherited prefix and keeps the parent id next to the task that needs it.
 
-**Install the child-scope deltas after the inherited history.** Rejected as unrepresentable: the system prompt and the tool schemas are request-head structures in every provider's wire format, so no ordering within them can place a child-only addition behind the message list.
+**Ignore an explicit child `toolFilter`.** Structural return tools previously bypassed the child allow-list. Rejected because a declared tool restriction must determine both schema visibility and guidance; hidden authority would make the model-facing roster inaccurate.
 
 ## Consequences
 
-- No shipped composition creates a continuable forked child; `subagent_fork` returns a result to its caller's turn, and `send_message` addresses only spawned children.
-- 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 `next-step` scheduling, authority model, and coverage remain independent of fork composition.
+- Parent and continuable-fork child expose byte-identical ordered tool schemas when the delegation does not request a persona or tool filter.
+- The inherited Session seed precedes the child's initial task and return guidance.
+- The base and headless profiles keep one-shot fork, while selected CLI presets exercise continuable fork without a child-only request-head addition.
+- A child sends zero or more messages to its direct parent explicitly; its final answer is not implicitly copied. The manager-owned settlement notice remains unconditional and separate.
+- Keyless snapshots and package tests pin schema equality, inherited-history ordering, parent-id guidance, and child-to-parent delivery through the same `send_message` operation used in the other direction.
 
 ### Accepted risks
 
-The constraint lives in three configuration files and a code comment, not in a gate. A future bundle row or profile patch can set `backgroundMode: continuable` on a fork tool and silently reintroduce the prefix loss; nothing fails loud. That is the accepted cost of not encoding one roster's consequence into `tool-subagent`.
+Provider-side prefix reuse still depends on the selected provider and model and on the absence of explicit persona or tool-filter differences. The harness proves equality of its assembled request-head inputs, not a provider's cache behavior.

+ 19 - 27
.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md

@@ -1,49 +1,41 @@
-# Agent Note: fork 出的 child 保持 one-shot
+# Agent Note:Fork child 保留 parent 请求前缀
 
-Status: implemented
+状态:已实现
 
 [English](2026-08-10-fork-children-stay-one-shot.md) | 中文
 
 ## 问题
 
-fork 与 spawn 的唯一区别是 child 的 Session 会以 parent 已完成轮次的前缀作为初始内容(见 [subagent-fork-in-process](../../../../packages/subagent/subagent-fork-in-process/README.zh.md))。这份初始内容有实打实的 token 成本——继承的历史会在 child 的每次请求中重新发送——而它唯一确定的回报是提供方侧的前缀复用:在提供方与模型相同的前提下,起始字节与 parent 逐字节相同的 child 请求,无需为这段共享区间重新预填充。任何由 child 作用域添加在继承历史*之前*的内容都会消耗掉这份回报,因为复用在第一个不同字节处即告停止
+fork 与 spawn 的差异在于:fork 会用 parent 已完成轮次的前缀作为 child Session 的种子。该种子会消耗 token,其预期收益是提供方侧的前缀复用:使用相同提供方和模型时,如果 child 请求的开头字节与 parent 相同,共享区段就无需再次预填充。任何位于继承历史之前、仅属于 child 的系统提示词 section 或工具 schema 都会破坏这项收益
 
-作用域局部的 `report` 返回通道现在是此类添加中最大的一项,而自[report 义务](../feature/2026-08-06-continuable-child-report-obligation.zh.md)起它是两项而非一项增量:`report` 工具 schema,以及 `tool:report` 系统提示词 section。两者都位于请求头部——系统块与工具块先于所有消息——因此一个可继续的 fork child 会在第一条继承轮次之前就使复用失效,并重新预填充它当初 fork 就是为了复用的整份 transcript(文本记录)。这种组合付出了 fork 的复制成本却收不到它的收益,而 parent 手上仍握着一份 child 本可共享的可复用前缀
+先前的随附组合通过把 fork child 保持为 one-shot 来避开这种不匹配。该限制源于原来的 child-only 返回工具,并非可继续 fork 的固有属性
 
 ## 决策
 
-所有交付组合都从 [base bundle](../../../../packages/bundle/base/cordis.patch.yml)继承 fork 委派工具的 `backgroundMode: one-shot`。base bundle 保留 `run_in_background`,因为它也挂载了结算后台工作所需的 task 服务
+面向模型的 `send_message` 工具在组合中的每个 Agent 上全局注册。因此,可继续 fork child 获得与 parent 相同的工具名称、描述、schema 和顺序。其初始任务追加在继承的 Session 种子之后;当 child 可以看到该工具时,任务还会包含直接 parent id,以及使用 `send_message({ agent_id, message })` 返回结果的指引
 
-one-shot child——前台与后台皆然——经由 `SubagentRuntime.start()` 创建,该路径从不进入可继续的 activation setup 注册表,因此 `report` 与它的提示词 section 都不会被安装。于是一个 fork 出的 one-shot child 的系统提示词与工具 schema 与其 parent 相同,只差部署逐个委派工具主动选择的 `persona` 与 `toolFilter` 增量
+base 与 headless 组合保留 one-shot fork 作为其保守生命周期策略。`cordis`、`standard` 和 `ptc` CLI preset 可以把 fork 绑定为可继续生命周期,因为该绑定不再插入 child-only 请求头字段。`ForkInProcessProvider.prepareContinuable()` 与 `ctx.subagents.startContinuable()` 仍是这些 preset 使用的实现 seam
 
-`spawn` 保持 `backgroundMode: continuable`。对于 child 起步时本就没有继承前缀需要保护的那个提供方,可继续 child 与 report 义务随附行为不变,因此本决策没有让 report 通道付出任何代价
+逐字节相同的前缀复用受显式部署选择约束。配置 child persona 或 `toolFilter` 的 fork 委派仍可能改变请求头。尤其是过滤掉 `send_message` 时,child 会同时失去该 schema 与返回指引;运行时不会绕过显式 allow-list
 
-### 该限制在于组合,不在于代码
+## 考虑过的替代方案
 
-`ForkInProcessProvider.prepareContinuable` 仍然实现完好,`ctx.subagents.startContinuable()` 也仍接受 `fork`;改动的只有随附的 `cordis.yml` 行。`tool-subagent` 在挂载时同时知道提供方的 `inheritsParentContext` 与自身的 `backgroundMode`,因此一个加载期拒绝该组合的检查是可行的,而这里刻意不加:该组合并非普遍错误。它只在某个 child 作用域增量位于继承历史之前时才是错的,而产生该增量的包——[`dsh-tool-subagent-report`](../../../../packages/subagent/tool-subagent-report/README.zh.md)——是独立安装的,并且按其自身设计对 `tool-subagent` 不可见。一个不安装 report 包的部署可以在前缀完好的前提下运行可继续的 fork child。把某一份插件清单的后果写成委派工具的不变量,会让该工具断言它无法观察到的事实
+**让所有 fork 保持 one-shot。** 这能保留前缀,但 child-only schema 差异消失后,继续放弃持久且多轮的 fork child 已无必要
 
-重新开放的条件记录为 `prepareContinuable` 方法上的 `TODO(fork-continuable-prefix-reuse)` 标记——随附组合不调用这个方法——并由 issue #2124 跟踪:当 child 的系统提示词与工具 schema 能与其 parent 逐字节一致时,可继续 fork 即可重新开放
+**安装 child-only 返回别名。** 无需填写接收方的别名可以缩短 child 调用,但会在继承历史之前重新产生工具 schema 与提示词差异,并重复相邻 Agent 操作
 
-## 备选方案
+**把返回指令放进系统提示词。** 这会在继承消息之前加入 child-only 字节。将其追加到初始用户任务,可以保留继承前缀,并让 parent id 紧邻需要它的任务。
 
-**在挂载时拒绝 `inheritsParentContext` 与 `continuable` 的组合。** 一次响亮的加载期失败可以阻止悄然的重新引入,而配置改动做不到这一点。否决的原因是委派工具看不到 report 包,且在没有它时该组合是合法的;对于从不安装任何 child 作用域增量的部署,这个不变量是假的,而 `tool-subagent` 会去断言一件由插件清单拥有的事实。
-
-**干脆不挂载 fork 提供方。** 这是该限制更彻底的形式。否决的原因是前台 fork *正是*复用前缀的那种情形,且不受 report 通道影响,因此全面禁用会在不换来任何 one-shot 绑定尚未换来的东西的同时放弃该能力——并且随附组合将没有任何一个演练 session 初始内容。
-
-**照常随附可继续的 fork child 并接受这份损失。** 否决的原因是这份损失是全额而非边际的:复用在继承历史之前就已中断,于是 child 为一份自己复制过来、目的恰恰是不必付费的 transcript 付了全额预填充。想要一个没有继承上下文的长期 child 的部署,本来就有 `spawn`。
-
-**让 `report` 对每个 Agent 可见。** 全局注册会通过让 parent 与 child 拥有相同的 schema 与 section 来恢复逐字节相同的前缀。否决的原因是根 agent、one-shot child、远端 child 与无 agent 调用方都会宣告一件推导不出收件方的工具,而执行期拒绝会让 schema 可见性与权限彼此矛盾——这正是[report 工具 Agent Note](../feature/2026-07-30-continuable-subagent-report-tool.zh.md)已经定下的作用域局部决策。
-
-**把 child 作用域增量安装到继承历史之后。** 否决的原因是它无法表达:在每个提供方的协议格式中,系统提示词与工具 schema 都是请求头部结构,因此它们内部的任何排序都无法把仅属于 child 的添加放到消息列表之后。
+**忽略显式 child `toolFilter`。** 结构性返回工具过去会绕过 child allow-list。否决该方案,因为声明的工具限制必须同时决定 schema 可见性与指引;隐藏权限会让面向模型的工具清单失真。
 
 ## 后果
 
-- 没有任何随附组合会创建可继续的 fork child;`subagent_fork` 把结果返回给调用方的轮次,而 `send_message` 只寻址 spawn 出的 child
-- 除非部署在 fork 委派工具上配置了 `persona` 或 `toolFilter`,fork child 的请求前缀与其 parent 逐字节相同,因此初始内容的 token 成本重新换来了提供方侧的复用
-- fork 提供方的可继续路径没有生产调用方,也没有整体组装层面的覆盖。它保留自己的包内测试,seam 也仍然接受它,因此某个组合包或 `--patch` 覆盖层可以无需改动代码、也不会有任何警告地把它重新引入
-- `subagent_fork` 面向模型的 schema 发生变化:base 组合包中可继续的后台措辞被 one-shot 的 task 措辞取代,在两个示例中则完全消失。受影响的无密钥快照工具 schema 伴随文件在同一次改动中重新记录
-- 在随附部署中,report 义务的覆盖范围收窄到 spawn 出的 child。它的 `next-step` 默认调度、权限模型与覆盖仍独立于 fork 组合
+- 未请求 persona 或工具过滤时,parent 与可继续 fork child 暴露逐字节相同且顺序一致的工具 schema。
+- 继承的 Session 种子位于 child 初始任务及其返回指引之前。
+- base 与 headless profile 保持 one-shot fork;选定的 CLI preset 会在没有 child-only 请求头增量的前提下使用可继续 fork。
+- child 显式向直接 parent 发送零条或多条消息;最终回答不会被隐式复制。管理器负责的结算通知仍然无条件执行,并且与 Agent 消息分离。
+- keyless snapshot 与包测试固定 schema 相等性、继承历史顺序、parent-id 指引,以及通过同一个 `send_message` 操作完成的 child-to-parent 投递。
 
-### 已接受风险
+### 已接受风险
 
-该限制存在于三个配置文件与一处代码注释中,而不在门禁里。未来某个组合包行或 profile 补丁可以在 fork 工具上设置 `backgroundMode: continuable`,从而悄然重新引入前缀损失;没有任何东西会失败得很响亮。这就是不把某一份插件清单的后果写入 `tool-subagent` 所接受的代价
+提供方侧前缀复用仍取决于选定的提供方和模型,以及是否不存在显式 persona 或工具过滤差异。harness 证明的是自己组装出的请求头输入相等,而不是提供方的缓存行为

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-25-sparse-first-party-prompt-section-orders.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-25-sparse-first-party-prompt-section-orders.md
-2026-08-25-sparse-first-party-prompt-section-orders.md: ffa2e6a4f602178007a6702938dfd71a2f85cbaa
-2026-08-25-sparse-first-party-prompt-section-orders.zh.md: d26ac18b075a2f0ebccccdbd072c7c066c2fe1b0
+2026-08-25-sparse-first-party-prompt-section-orders.md: 3cfdb58af3c576b3e46449a763db1c811073517e
+2026-08-25-sparse-first-party-prompt-section-orders.zh.md: 31cbb98eab9eb786ac9d854b76df4c18efb88563

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-25-sparse-first-party-prompt-section-orders.md

@@ -24,7 +24,7 @@ The allocation preserves the established first-party sequence except for two del
 | Work modes | `plan:policy` 500, `team:policy` 600 |
 | Invocation prelude | `tools:ptc-only` 800, `context:file-reference` 900 |
 | Local tools | `tool:bash` 1000, `tool:pwsh` 1010, `tool:read` 1100, `tool:write` 1200, `tool:edit` 1300, `tool:glob` 1400, `tool:grep` 1500, `tool:jobs` 1600, `tool:pty` 1700 |
-| Higher-level tools | `tool:web_search` 2000, `tool:web_fetch` 2100, `tool:lsp` 2200, `tool:session-query` 2300, `tool:goal` 2400, `tool:cordis` 2500, `tool:workflow` 2600, `tool:ralph` 2700, continuable-subagent guidance 2800, `tool:report` 2900 |
+| Higher-level tools | `tool:web_search` 2000, `tool:web_fetch` 2100, `tool:lsp` 2200, `tool:session-query` 2300, `tool:goal` 2400, `tool:cordis` 2500, `tool:workflow` 2600, `tool:ralph` 2700, continuable-subagent guidance 2800 |
 | Generated protocol | `tools:sdk` 5000 |
 | Final-output obligations | deliverable file references 9000, `tool:structured_output` 9900 |
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-25-sparse-first-party-prompt-section-orders.zh.md

@@ -24,7 +24,7 @@ Status: implemented
 | 工作模式 | `plan:policy` 500、`team:policy` 600 |
 | 调用前置说明 | `tools:ptc-only` 800、`context:file-reference` 900 |
 | 本地工具 | `tool:bash` 1000、`tool:pwsh` 1010、`tool:read` 1100、`tool:write` 1200、`tool:edit` 1300、`tool:glob` 1400、`tool:grep` 1500、`tool:jobs` 1600、`tool:pty` 1700 |
-| 高层工具 | `tool:web_search` 2000、`tool:web_fetch` 2100、`tool:lsp` 2200、`tool:session-query` 2300、`tool:goal` 2400、`tool:cordis` 2500、`tool:workflow` 2600、`tool:ralph` 2700、可继续运行的 subagent 指导 2800、`tool:report` 2900 |
+| 高层工具 | `tool:web_search` 2000、`tool:web_fetch` 2100、`tool:lsp` 2200、`tool:session-query` 2300、`tool:goal` 2400、`tool:cordis` 2500、`tool:workflow` 2600、`tool:ralph` 2700、可继续运行的 subagent 指导 2800 |
 | 生成协议 | `tools:sdk` 5000 |
 | 最终输出义务 | 可交付文件引用 9000、`tool:structured_output` 9900 |
 

+ 3 - 3
.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.i18n.yaml → .agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.i18n.yaml

@@ -1,6 +1,6 @@
 # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.md
-2026-07-30-continuable-subagent-report-tool.md: 07d17f18f318a86070d9b8612512fa3c2a3815e2
-2026-07-30-continuable-subagent-report-tool.zh.md: 9cbf76a9f3f50d9b1a84b573621cd6fc2a1611c4
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.md
+2026-08-27-adjacent-agent-steer-messaging.md: 31c5a4fc8c0e807ff8ab869561d0acd32eccc060
+2026-08-27-adjacent-agent-steer-messaging.zh.md: c49da882a5beb7a7e8379d15a0628fb5c617e040

+ 82 - 0
.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.md

@@ -0,0 +1,82 @@
+# Agent Note: Adjacent Agents share one Steer send_message operation
+
+Status: implemented
+
+English | [中文](2026-08-27-adjacent-agent-steer-messaging.zh.md)
+
+## Problem
+
+Continuable Agents originally used direction-specific model controls. A parent called `send_message({ subagent_id, message })`, which delegated to a FIFO `followup` service operation. A child instead received a child-scoped `report({ output })` tool, a `tool:report` system-prompt section, and deployment-selected quiet or waking delivery. The tools described one adjacent-Agent operation through different schemas, service paths, provenance, and scheduling.
+
+A continuable child owns its own Session, so its parent does not automatically receive the child's transcript, tool output, or reasoning. The return path must therefore remain explicit and repeatable: a child may send progress before it finishes, remain available after sending, or fail before it can cooperate. Turning every final assistant message into an implicit result would conflate turn completion with model-selected communication and would not cover abnormal endings.
+
+The child-only tool and system-prompt section also preceded every inherited fork turn. They made a continuable fork child's request head differ from its parent's before the history that fork exists to reuse, forcing the provider to prefill the entire copied transcript again.
+
+## Decision
+
+`SubagentRuntime.sendMessage(sender, targetId, content, { signal })` is the only public model-authored message operation. The continuation manager accepts only the exact live sender and a target on one adjacent edge:
+
+- parent to direct continuable child, authorized by the child's durable `SessionHeader.parentSession`;
+- resident continuable child to its exact live direct parent, authorized by the child's Activation.
+
+Siblings, self-targets, ancestors beyond one edge, stale Agent objects, unknown targets, and one-shot children are not alternate routes. The operation has no caller-supplied source, delivery mode, offline parent mailbox, or provider dispatch.
+
+Every accepted message uses `Agent.steer()`. A running target receives it at the nearest step boundary; an idle target starts a turn. An absent direct child is cold-resumed through the existing continuation lifecycle before the same Steer delivery. The manager retains waking-send accounting so a continuation-managed target cannot settle between synchronous inbox insertion and driver admission.
+
+Every direction uses one durable source. The service derives `senderSessionId` from the authorized Agent and frames the model-visible content as `Agent <sender-id> sent a message:`, so attribution cannot diverge from authority.
+
+```ts
+import type { SessionId } from '@deepseek-ai/dsh-session'
+
+interface AgentMessageSource {
+  readonly kind: 'agent-message'
+  readonly form: 'relay'
+  readonly senderSessionId: SessionId
+}
+```
+
+### One model tool and one return instruction
+
+The globally registered model tool is direction-neutral and has one fixed schema:
+
+```ts
+interface SendMessageInput {
+  readonly agent_id: string
+  readonly message: string
+}
+```
+
+Parents and children inherit the same definition in the same registry order. The standard definition carries a process-stable internal identity that a scoped same-name tool does not satisfy. A child `toolFilter` may explicitly remove the inherited tool, and a scoped replacement may provide different semantics; neither case receives the standard call instruction. When the standard tool remains visible, the continuation manager appends the JSON-encoded direct parent id and the instruction to send one self-contained result before finishing, plus earlier actionable findings, to the child's initial user task. For a fork child this task follows the inherited completed-turn prefix; no child-only system-prompt section or tool schema precedes that prefix.
+
+The instruction is guidance, not settlement enforcement. Sending does not end the child's turn, zero or several calls remain mechanically valid, and the runtime never rejects a child for staying silent. The manager-owned `subagent-settled` notice remains unconditional and separately attributed because it records how an Activation ended and preserves terminal output when the child cannot cooperate.
+
+Human browser prompts are not model-authored Agent messages. The remote prompt path keeps a private Queue delivery so each human prompt remains a distinct turn. Interrupt behavior and settlement delivery remain independent.
+
+### Complete removal and reintroduction condition
+
+The standalone `@deepseek-ai/dsh-tool-subagent-report` package, `report` schema, `tool:report` prompt section, `reportDelivery` configuration, report-specific message source, catalog entries, composition rows, and supported-behavior snapshots are absent. The unified tool gives up the recipient-free child shortcut and the old ability for a structural return tool to survive an explicit child allow-list. Those capabilities return only if a concrete use case requires semantics that an adjacent `agent_id` and fixed Steer cannot express; reintroducing them requires a distinct model operation and prefix-cost evidence, not an alias over `sendMessage()`.
+
+## Alternatives considered
+
+**Keep `followup` and add child-to-parent routing.** The name promises a later turn and inherits `Agent.followup()` semantics. It would obscure the chosen nearest-step behavior and preserve a parent-centric name for a direction-neutral capability.
+
+**Keep a recipient-free `report` wrapper over `sendMessage()`.** This preserves a convenient child shortcut and lets a scope-local registration survive global tool filtering. It loses because the separate schema and prompt duplicate one operation, make parent and child request heads differ, and let equivalent directions drift again.
+
+**Make `report` global.** Roots, one-shot children, remote children, and agentless callers cannot derive a report recipient. Advertising it globally would make schema visibility disagree with authority, while `send_message` already makes the recipient explicit.
+
+**Turn every child final message into an implicit send.** A long-lived child may have nothing useful to send in one turn and several findings in another. Automatic delivery would merge model-authored communication with the runtime's settlement account and could not replace the unconditional notice on errors, cancellation, or token exhaustion.
+
+**Rely only on the tool description.** A tool description helps after the model considers that tool; the failure mode is a child that believes it is finished without considering any return call. Initial-task guidance reaches that decision without changing the inherited system or tool prefix.
+
+**Keep quiet delivery as deployment policy.** A quiet model-authored message can be accepted while an idle target never reads it. Fixed Steer gives both directions one delivery meaning and preserves accepted order with later settlement notices.
+
+## Consequences
+
+- Model consumers expose one `send_message({ agent_id, message })` definition to parents and children, with no model-selected Queue versus Steer parameter.
+- The continuation manager remains the sole owner of adjacency authorization, residency, cold resume, waking admission, and teardown races.
+- Accepted messages may extend a running target's current turn; messages waiting together share next-step FIFO ordering.
+- Caller cancellation owns work only until inbox acceptance and does not retract an accepted message or dispose the target.
+- The initial task carries JSON-encoded dynamic parent addressing after a fork prefix, while the request-head system prompt and tool ordering remain reusable.
+- Human prompts, settlement notices, QueueDock, and the base bundle's one-shot fork policy remain separate decisions.
+
+This decision consolidates and removes the fully superseded report-tool and child-report-obligation records. It supersedes the `followup` naming choice in [Intent-named subagent continuation operations](../simplification/2026-07-27-intent-named-subagent-continuation-operations.md) and retains the accepted-order guarantee in [Child Agent messages precede their settlement notices](../bug-fix/2026-08-17-subagent-message-settlement-ordering.md).

+ 82 - 0
.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md

@@ -0,0 +1,82 @@
+# Agent Note: 相邻 Agent 共享一个 Steer send_message 操作
+
+Status: implemented
+
+[English](2026-08-27-adjacent-agent-steer-messaging.md) | 中文
+
+## 问题
+
+可继续 Agent 最初使用方向专属的模型控制。parent 调用 `send_message({ subagent_id, message })`,委托给 FIFO `followup` 服务操作。child 则获得 child 作用域的 `report({ output })` 工具、`tool:report` 系统提示词 section,以及由部署选择的静默或唤醒投递。两个工具用不同 schema、服务路径、来源与调度描述同一个相邻 Agent 操作。
+
+可继续 child 拥有自己的 Session,因此 parent 不会自动收到 child 的 transcript(文本记录)、工具输出或推理。返回路径必须保持显式且可重复:child 可以在结束前发送进度、发送后仍保持可用,也可能在来得及配合前失败。把每条最终 assistant 消息变成隐式结果会混淆轮次完成与模型选择的通信,而且无法覆盖异常结束。
+
+child 专属工具与系统提示词 section 还位于每个继承 fork 轮次之前。它们让可继续 fork child 的请求头在 fork 旨在复用的历史之前就与 parent 不同,迫使提供方重新预填充整份复制 transcript。
+
+## 决策
+
+`SubagentRuntime.sendMessage(sender, targetId, content, { signal })` 是唯一公开的模型编写消息操作。继续执行管理器只接受确切在线 sender 与一条相邻边上的目标:
+
+- parent 到直接可继续 child,由 child 的持久化 `SessionHeader.parentSession` 授权;
+- 驻留的可继续 child 到其确切在线直接 parent,由 child 的 Activation 授权。
+
+sibling、自身目标、超过一条边的 ancestor、陈旧 Agent 对象、未知目标与一次性 child 都不是替代路由。该操作没有调用方提供的 source、投递模式、离线 parent mailbox 或提供方分发。
+
+每条被接受的消息都使用 `Agent.steer()`。运行中目标在最近 step 边界接收消息;空闲目标启动轮次。缺失的直接 child 会先通过现有继续执行生命周期冷恢复,再接受同一 Steer 投递。管理器保留唤醒发送记账,因此受继续执行管理的目标不会在同步 inbox 插入与 driver 准入之间结算。
+
+两个方向使用同一种持久来源。服务从已授权 Agent 推导 `senderSessionId`,并把模型可见内容组装为 `Agent <sender-id> sent a message:`,因此来源信息不会偏离权限。
+
+```ts
+import type { SessionId } from '@deepseek-ai/dsh-session'
+
+interface AgentMessageSource {
+  readonly kind: 'agent-message'
+  readonly form: 'relay'
+  readonly senderSessionId: SessionId
+}
+```
+
+### 一个模型工具与一条返回指令
+
+全局注册的模型工具与方向无关,并使用一个固定 schema:
+
+```ts
+interface SendMessageInput {
+  readonly agent_id: string
+  readonly message: string
+}
+```
+
+parent 与 child 以相同注册表顺序继承相同定义。标准定义携带进程稳定的内部身份,同名的作用域工具不满足该身份。child `toolFilter` 可以显式移除继承的工具,作用域替代工具也可以提供不同语义;两种情况都不会收到标准调用指令。当标准工具仍可见时,继续执行管理器会把经过 JSON 编码的直接 parent id、结束前发送一份自包含结果的指令,以及更早发送可操作发现的指令追加到 child 初始用户任务。对 fork child 而言,该任务位于继承的已完成轮次前缀之后;没有 child 专属系统提示词 section 或工具 schema 位于此前缀之前。
+
+该指令是指导,不是结算强制。发送不会结束 child 轮次,机制仍允许零次或多次调用,runtime 绝不会因 child 保持沉默而拒绝它。由管理器负责的 `subagent-settled` 通知仍无条件发送并采用独立来源,因为它记录 Activation 如何结束,并在 child 无法配合时保留终态输出。
+
+浏览器中的人类提示不是模型编写的 Agent 消息。远程提示路径保留私有 Queue 投递,使每条人类提示保持为独立轮次。中断行为与结算投递保持独立。
+
+### 完整移除与重新引入条件
+
+独立的 `@deepseek-ai/dsh-tool-subagent-report` 包、`report` schema、`tool:report` 提示词 section、`reportDelivery` 配置、report 专属消息来源、目录项、组合行和受支持行为快照均已不存在。统一工具放弃了无需接收方的 child 快捷方式,也放弃了让结构性返回工具绕过显式 child allow-list 的旧能力。只有具体用例需要相邻 `agent_id` 与固定 Steer 无法表达的语义时,这些能力才会重新出现;重新引入需要独立的模型操作与前缀成本证据,而非 `sendMessage()` 之上的别名。
+
+## 考虑过的替代方案
+
+**保留 `followup` 并添加 child 到 parent 路由。** 该名称承诺后续轮次并继承 `Agent.followup()` 语义。它会掩盖选定的最近 step 行为,并为方向无关能力保留以 parent 为中心的名称。
+
+**保留 `sendMessage()` 之上无需接收方的 `report` 包装层。** 这会保留便利的 child 快捷方式,并让作用域局部注册绕过全局工具过滤。它落选是因为独立 schema 与提示词重复一个操作、使 parent 与 child 请求头不同,并允许等价方向再次漂移。
+
+**让 `report` 全局可见。** 根 Agent、一次性 child、远程 child 与无 Agent 调用方无法推导 report 接收方。全局宣传它会让 schema 可见性与权限不一致,而 `send_message` 已显式给出接收方。
+
+**把每条 child 最终消息变成隐式发送。** 长期运行的 child 可能在某个轮次没有值得发送的内容,在另一个轮次却有多条发现。自动投递会混合模型编写通信与 runtime 结算说明,而且无法替代错误、取消或 token 耗尽时的无条件通知。
+
+**只依赖工具描述。** 工具描述会在模型考虑该工具后提供帮助;失败模式是 child 认为自己已经完成而根本没有考虑返回调用。初始任务指导能触及该决策,又不会改变继承的系统或工具前缀。
+
+**保留静默投递作为部署策略。** 静默的模型编写消息可能被接受,但空闲目标永远不会读取它。固定 Steer 为两个方向提供一种投递含义,并保持与后续结算通知的接受顺序。
+
+## 后果
+
+- 模型 Consumer 向 parent 与 child 公开一个 `send_message({ agent_id, message })` 定义,不提供模型选择的 Queue 与 Steer 参数。
+- 继续执行管理器仍是相邻关系授权、驻留、冷恢复、唤醒准入与拆卸竞态的唯一所有者。
+- 被接受的消息可以延长运行中目标的当前轮次;一起等待的消息共享 next-step FIFO 顺序。
+- 调用方取消只在 inbox 接受前掌管工作,不会撤回已接受消息或 dispose(资源释放)目标。
+- 初始任务在 fork 前缀之后携带经过 JSON 编码的动态 parent 地址,而请求头系统提示词与工具顺序保持可复用。
+- 人类提示、结算通知、QueueDock 与 base bundle 的一次性 fork 策略仍是独立决策。
+
+本决策合并并删除了已完全被取代的 report 工具与 child report 义务记录。它取代[按意图命名的 subagent 继续执行操作](../simplification/2026-07-27-intent-named-subagent-continuation-operations.zh.md)中的 `followup` 命名选择,并保留[Child Agent 消息先于其结算通知](../bug-fix/2026-08-17-subagent-message-settlement-ordering.zh.md)中的接受顺序保证。

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-08-10-child-agents-join-their-parent-preset.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/bug-fix/2026-08-10-child-agents-join-their-parent-preset.md
-2026-08-10-child-agents-join-their-parent-preset.md: 5a3d0d1c5e2b492b10a0274204c10e244c37196f
-2026-08-10-child-agents-join-their-parent-preset.zh.md: ce5d69404d4f8b6a9879668d2ed0cb1c9d133a40
+2026-08-10-child-agents-join-their-parent-preset.md: 5321d99bad35cf26ebc0e7842dc68c2775bc384b
+2026-08-10-child-agents-join-their-parent-preset.zh.md: f0d5b59c3787ef69c2c983da0c8ec0ff4a43e6e7

+ 3 - 3
.agents/notes/implemented/bug-fix/2026-08-10-child-agents-join-their-parent-preset.md

@@ -22,7 +22,7 @@ This is a bind, not a mount, and both differences are load-bearing. The child ge
 
 `dsh-subagent` reaches the roster through `ctx.get('agentPresets')` with a type-only import and an optional peer dependency — the documented opportunistic-consumption pattern it already uses for `sandboxPolicy` and `approval`.
 
-Giving the child its parent's tools exposed a second defect the same agent-plane move introduced: `ToolRuntime` exempted SCOPED registrations from a restriction and filtered only the global layer, so once every model-facing row became an ancestor contribution, a child's `toolFilter` stopped constraining anything — and, with the global layer empty, `restrict()` rejected every name it was given as unknown, failing the child outright. The exempt set is the tools a scope registers ITSELF, not the tools that happen to live in the global layer; reading it the second way held only while those two sets coincided. `view()` now filters everything a scope inherits — the global layer and every ancestor layer — and exempts only its own. The own-layer exemption is load-bearing rather than incidental: the delegation runtime registers a child's `report` and structured-output tools into the child's own layer, and a filter naming the capabilities the child may use must not strip the machinery it answers through.
+Giving the child its parent's tools exposed a second defect the same agent-plane move introduced: `ToolRuntime` exempted SCOPED registrations from a restriction and filtered only the global layer, so once every model-facing row became an ancestor contribution, a child's `toolFilter` stopped constraining anything — and, with the global layer empty, `restrict()` rejected every name it was given as unknown, failing the child outright. The exempt set is the tools a scope registers ITSELF, not the tools that happen to live in the global layer; reading it the second way held only while those two sets coincided. `view()` now filters everything a scope inherits — the global layer and every ancestor layer — and exempts only its own. The own-layer exemption is load-bearing rather than incidental: the delegation runtime registers a child's structured-output tool into the child's own layer, and a filter naming the capabilities the child may use must not strip the machinery it answers through.
 
 ## Alternatives considered
 
@@ -30,11 +30,11 @@ Giving the child its parent's tools exposed a second defect the same agent-plane
 
 **Bind the child's key to the PARENT's key rather than to the standing mount.** Rejected because it changes what a child inherits: the parent's own scope layer carries its per-agent restrictions, which would then intersect into every descendant, and a child outliving its parent would hang off a disposed agent's key. Joining the standing mount gives the child its parent's composition and nothing else.
 
-**Extend the continuable activation setup registry to cover one-shot children.** Rejected because that registry's contribution type is synchronous `(childCtx) => () => void` with per-installation revocation, modelling deployment capabilities that come and go, while a preset join is a one-time bind with no revocation of its own. Widening it would have made the omission possible again for any driver that skipped the registry.
+**Introduce one shared child-setup registry for both drivers.** Rejected because a synchronous, revocable contribution models deployment capabilities that come and go, while a preset join is a one-time bind with no revocation of its own. Routing composition through an optional registry would make the omission possible again for any driver that skipped it.
 
 **Let `dsh-subagent` import `resolveSessionPreset` and mount by the resolved id.** Rejected because it makes the preset roster a hard module edge for a package that must work without one, and it lands back on the remount semantics above.
 
-**Filter every layer on the chain, including the scope's own.** Rejected because it makes a per-child capability filter delete that child's reporting and structured-output tools, which the delegation runtime registers into the child's own layer — an `allow` naming the capabilities a child may use would leave it unable to answer at all.
+**Filter every layer on the chain, including the scope's own.** Rejected because it makes a per-child capability filter delete that child's structured-output tool, which the delegation runtime registers into the child's own layer — an `allow` naming the capabilities a child may use would leave it unable to produce the requested result.
 
 **Leave the durable header alone and fix only the live join.** Rejected because the live child and the same child read cold would then disagree about which composition produced its history — the same class of defect, moved rather than fixed.
 

+ 3 - 3
.agents/notes/implemented/bug-fix/2026-08-10-child-agents-join-their-parent-preset.zh.md

@@ -22,7 +22,7 @@ Status: implemented
 
 `dsh-subagent` 以类型级导入加可选 peer 依赖的方式,通过 `ctx.get('agentPresets')` 触达 roster——这正是它对 `sandboxPolicy` 与 `approval` 已在使用的、有明确文档的机会性消费模式。
 
-把父方的工具交给子 agent 之后,暴露出同一次 agent 平面搬迁引入的第二个缺陷:`ToolRuntime` 把**作用域级**注册排除在限制之外、只过滤全局层,因此当所有面向模型的行都变成祖先贡献之后,子 agent 的 `toolFilter` 就不再约束任何东西——而且全局层为空时,`restrict()` 会把收到的每个名字都判为未知并直接让子 agent 创建失败。豁免集合应当是作用域**自己注册**的工具,而不是恰好位于全局层的工具;后一种读法只在这两个集合重合时才成立。`view()` 现在过滤作用域继承来的一切——全局层与每个祖先层——只豁免它自己那层。这条自身层豁免是承重的而非顺带的:委派运行时把子 agent 的 `report` 与结构化输出工具注册进子 agent 自己那层,而一个只点名子 agent 可用能力的过滤器绝不能把它回报所依赖的机制一并剥掉。
+把父方的工具交给子 agent 之后,暴露出同一次 agent 平面搬迁引入的第二个缺陷:`ToolRuntime` 把**作用域级**注册排除在限制之外、只过滤全局层,因此当所有面向模型的行都变成祖先贡献之后,子 agent 的 `toolFilter` 就不再约束任何东西——而且全局层为空时,`restrict()` 会把收到的每个名字都判为未知并直接让子 agent 创建失败。豁免集合应当是作用域**自己注册**的工具,而不是恰好位于全局层的工具;后一种读法只在这两个集合重合时才成立。`view()` 现在过滤作用域继承来的一切——全局层与每个祖先层——只豁免它自己那层。这条自身层豁免是承重的而非顺带的:委派运行时把子 agent 的结构化输出工具注册进子 agent 自己那层,而一个只点名子 agent 可用能力的过滤器绝不能把它产出请求结果所依赖的机制一并剥掉。
 
 ## 考虑过的替代方案
 
@@ -30,11 +30,11 @@ Status: implemented
 
 **把子 agent 的 key 绑到**父方的** key 而不是常驻挂载上。** 否决,因为这改变了子 agent 继承的内容:父方自己的 scope 层携带其逐 agent 限制,那些限制会就此与每个后代求交,而活得比父方久的子 agent 会挂在一个已 dispose 的 agent key 上。加入常驻挂载给到子 agent 的是父方的组装,仅此而已。
 
-**扩展可继续 activation setup 注册表以覆盖一次性子 agent。** 否决,因为该注册表的贡献类型是同步的 `(childCtx) => () => void` 并带有逐次安装的撤销,建模的是会来会走的部署能力,而 preset 加入是一次性认父、自身没有撤销可言。扩展它反而会让任何绕过该注册表的驱动重新具备遗漏的可能。
+**为两个驱动引入一份共享 child setup 注册表。** 否决,因为同步且可撤销的贡献建模的是会来会走的部署能力,而 preset 加入是一次性认父、自身没有撤销可言。让组合经由可选注册表完成,反而会让任何绕过它的驱动重新具备遗漏的可能。
 
 **让 `dsh-subagent` 导入 `resolveSessionPreset` 并按解析出的 id 挂载。** 否决,因为这会给一个必须在没有 roster 时也能工作的包引入硬模块边,而且最终仍落回上述的重新挂载语义。
 
-**过滤链上的每一层,包括作用域自身那层。** 否决,因为那会让逐子 agent 的能力过滤器把该子 agent 的回报与结构化输出工具一并删掉——它们由委派运行时注册进子 agent 自己那层——于是一个点名"子 agent 可用哪些能力"的 `allow` 会让它彻底无法回报
+**过滤链上的每一层,包括作用域自身那层。** 否决,因为那会让逐子 agent 的能力过滤器删掉该子 agent 的结构化输出工具——它由委派运行时注册进子 agent 自己那层——于是一个点名“子 agent 可用哪些能力”的 `allow` 会让它无法产出所请求的结果
 
 **只修活着的加入,不动持久化 header。** 否决,因为那样活着的子 agent 与冷读同一个子 agent 会对"哪份组装产出了这段历史"给出不同答案——同一类缺陷,只是被搬了个地方而不是被修掉。
 

+ 3 - 3
.agents/notes/implemented/bug-fix/2026-08-17-subagent-report-settlement-ordering.i18n.yaml → .agents/notes/implemented/bug-fix/2026-08-17-subagent-message-settlement-ordering.i18n.yaml

@@ -1,6 +1,6 @@
 # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write .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
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-17-subagent-message-settlement-ordering.md
+2026-08-17-subagent-message-settlement-ordering.md: cdc996643c84c5f50a3bd1836e82645660dc8c57
+2026-08-17-subagent-message-settlement-ordering.zh.md: 1143da1560e4969dcc4f6a0c6d5ca18060b56191

+ 44 - 0
.agents/notes/implemented/bug-fix/2026-08-17-subagent-message-settlement-ordering.md

@@ -0,0 +1,44 @@
+# Agent Note: Child Agent messages precede their settlement notices
+
+Status: implemented
+
+English | [中文](2026-08-17-subagent-message-settlement-ordering.zh.md)
+
+## Problem
+
+A continuable child can send selected content and later produce an unconditional manager-authored settlement notice. If those two messages enter queues with different claim priority, the later settlement notice can reach the parent model before the earlier child message. The first step of a turn claims the complete `next-step` batch before one `next-turn` message, so mixing a FIFO later-turn send with a next-step settlement reverses causal order. [Issue #2600](https://github.com/deepseek-harness/deepseek-harness/issues/2600) records the defect.
+
+The child instruction says to send a finding whenever it changes what the parent should do next. Deferring that message to a later turn contradicts its scheduling meaning and separates causally ordered messages across queues with different claim priority.
+
+## Decision
+
+Every model-authored adjacent-Agent message uses fixed Steer delivery through `SubagentRuntime.sendMessage()`. A running parent reads the child message at its nearest safe step boundary and an idle parent starts a turn. There is no quiet or next-turn model delivery option.
+
+The continuation manager retains `sendWaking()` and `admitWaking()` around messages delivered to resident continuable parents. Their purpose is waking-send admission accounting: 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 child message 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 child message; settlement may then open a later turn without reversing observed order.
+
+During parent maintenance, the child message 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 follows the core Agent's cancellation convergence rather than bypassing it.
+
+### Verification
+
+The control-tool suite holds a parent inside an active model request, submits child messages, settles the child, and verifies sender identity, Steer admission, FIFO batching, and preservation after settlement. Continuation coverage pins waking admission accounting for a resident continuable parent and keeps the runtime-owned settlement source distinct from `agent-message`.
+
+The keyless continuable-subagent snapshot uses the shipped fixed delivery. Its child-visible tool schema is the same as the parent's, and the accepted child message precedes the later settlement notice without a scheduling overlay.
+
+## Alternatives considered
+
+**Offer quiet delivery.** A quiet message can remain unread after an idle parent parks. It also gives equivalent model-authored messages different liveness semantics and reopens deployment-dependent ordering.
+
+**Offer next-turn delivery.** A later next-step settlement notice can still overtake it. Preserving message-before-settlement would require a cross-queue ordering barrier, and no current model operation requires later-turn isolation strongly enough to own that mechanism.
+
+**Move settlement notices to `next-turn`.** Settlement batching 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 an unnecessary message scheduling mode.
+
+## Consequences
+
+- A child message 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.
+- Messages accepted together share one next-step batch, preserving FIFO order and limiting turn amplification.
+- Model callers cannot choose a delivery mode, so ordering and wake behavior do not vary by deployment or call.
+- A child-to-parent send still requires the direct parent to remain live; the service provides no durable parent mailbox.

+ 44 - 0
.agents/notes/implemented/bug-fix/2026-08-17-subagent-message-settlement-ordering.zh.md

@@ -0,0 +1,44 @@
+# Agent Note: Child Agent 消息先于其结算通知
+
+Status: implemented
+
+[English](2026-08-17-subagent-message-settlement-ordering.md) | 中文
+
+## 问题
+
+可继续 child 可以发送选中内容,之后还会产生一条由管理器编写且无条件投递的结算通知。如果这两条消息进入领取优先级不同的队列,较晚的结算通知可能先于较早的 child 消息到达 parent 模型。一个轮次的第一个 step 会先领取完整 `next-step` 批次,再领取一条 `next-turn` 消息,因此混用 FIFO 后续轮次发送与 next-step 结算会颠倒因果顺序。[Issue #2600](https://github.com/deepseek-harness/deepseek-harness/issues/2600)记录了该缺陷。
+
+child 指令要求在发现会改变 parent 下一步动作时发送该发现。把这条消息推迟到后续轮次既违背其调度含义,也会把具有因果顺序的消息拆到领取优先级不同的队列。
+
+## 决策
+
+每条模型编写的相邻 Agent 消息都通过 `SubagentRuntime.sendMessage()` 使用固定 Steer 投递。运行中的 parent 在最近安全 step 边界读取 child 消息,空闲 parent 则启动一个轮次。模型没有静默或 next-turn 投递选项。
+
+继续执行管理器在投递到驻留可继续 parent 的消息周围保留 `sendWaking()` 与 `admitWaking()`。它们负责唤醒发送准入记账:接收方 Activation 会在同步 inbox 插入与观察到唤醒的微任务之间保持在线。
+
+### 不同 parent 状态下的顺序
+
+运行中的 parent 在同一条 `next-step` FIFO 中接收已接受的 child 消息与该 child 随后的结算通知。如果 parent 在结算到达前变为空闲,它已经领取 child 消息;结算随后可以开启后续轮次,而不会颠倒观察顺序。
+
+parent 处于 maintenance 时,child 消息占用 `next-step` 并锁存一次唤醒,而结算可能因 maintenance 报告空闲状态而占用 `next-turn`。初始领取仍会先取 next-step 输入,再取排队轮次。取消后提交的唤醒输入遵循核心 Agent 的取消收敛,而不会绕过它。
+
+### 验证
+
+控制工具测试套件让 parent 保持在活跃模型请求中,提交 child 消息、结算 child,并验证 sender 身份、Steer 准入、FIFO 批处理与结算后保留。继续执行覆盖固定驻留可继续 parent 的唤醒准入记账,并让 runtime 所有的结算来源与 `agent-message` 保持不同。
+
+无密钥可继续 subagent 快照使用随附的固定投递。其 child 可见工具 schema 与 parent 相同,且已接受的 child 消息先于之后的结算通知,无需调度 overlay。
+
+## 考虑过的替代方案
+
+**提供静默投递。** 空闲 parent 停驻后可能永远不读取静默消息。它还会让等价的模型编写消息具有不同存活语义,并重新引入依赖部署的顺序。
+
+**提供 next-turn 投递。** 较晚的 next-step 结算通知仍可能越过它。保持消息先于结算需要跨队列顺序屏障,而当前没有模型操作对后续轮次隔离的需求强到足以承担该机制。
+
+**把结算通知移到 `next-turn`。** 结算批处理使用 next-step 队列,使多个 child 同时结束只消耗 parent 的一个 step,而不是每个 child 一个轮次。移动结算会为了保留不必要的消息调度模式而增加延迟与模型工作。
+
+## 后果
+
+- child 消息可以延长开放的 parent 轮次。它绝不会中断活跃模型请求或工具执行;agent loop 只在 step 边界接纳它。
+- 一起被接受的消息共享一个 next-step 批次,保持 FIFO 顺序并限制轮次放大。
+- 模型调用方不能选择投递模式,因此顺序与唤醒行为不会随部署或调用而变化。
+- child 到 parent 的发送仍要求直接 parent 保持在线;服务不提供持久 parent mailbox。

+ 0 - 44
.agents/notes/implemented/bug-fix/2026-08-17-subagent-report-settlement-ordering.md

@@ -1,44 +0,0 @@
-# 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.

+ 0 - 44
.agents/notes/implemented/bug-fix/2026-08-17-subagent-report-settlement-ordering.zh.md

@@ -1,44 +0,0 @@
-# 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` 仍是部署退路,同时保留既有风险:在另一条唤醒输入到达之前,没有模型会读取这些报告。

+ 2 - 2
.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.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-21-continuable-background-subagents.md
-2026-07-21-continuable-background-subagents.md: b2a3a8c53db5ae2860ed5cc6edccadfcd417e7fa
-2026-07-21-continuable-background-subagents.zh.md: 24cc09e621731cbb54f4d081d232f0598220d418
+2026-07-21-continuable-background-subagents.md: e1adf8efa19cce8e0632170886770b8d12bc9e3d
+2026-07-21-continuable-background-subagents.zh.md: f62c314320a1dace43f3028b68a1bb7d65203e84

+ 1 - 1
.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md

@@ -57,7 +57,7 @@ The continuation manager does not serialize two callers that race a stopped chil
 
 ### Model-facing `send_message`
 
-The model receives one `send_message(subagent_id, message)` tool backed by `SubagentRuntime.followup()`, matching the intent verb on `Agent`. The service operation owns steer-or-resume orchestration and is distinct from the run's `SubagentRun.steer?()`, which only delivers to an already active run. The tool performs no lifecycle routing of its own. It attributes the follow-up as `{ kind: 'coordinator', senderSessionId: parent.id }` and forwards `{ source, signal }`; the service requires both facts in one options object. The source crosses both live steering and cold resume, while cancellation owns only a pending live-delivery wait because a cold-resume Task returns immediately and owns its later cancellation. The child model still receives ordinary user-role content, while the durable source prevents model-generated follow-ups from being classified as direct human input. A human adapter instead supplies `{ kind: 'user' }` and its interaction signal. The tool lives in the separately loaded `@deepseek-ai/dsh-tool-subagent-control` package so provider-bound `@deepseek-ai/dsh-tool-subagent` instances can continue registering distinct delegation tools for spawn, fork, or ACP without registering duplicate global control tools.
+The model receives one global `send_message(agent_id, message)` tool backed by `SubagentRuntime.sendMessage()`. The exact live sender may name only its direct parent or direct continuable child; the service owns adjacency checks, cold resume, fixed Steer scheduling, and durable `{ kind: 'agent-message', senderSessionId }` attribution. Cancellation owns work only until inbox acceptance. A human adapter remains separate because browser-authored input carries user provenance and request identity rather than Agent authority. The tool lives in the separately loaded `@deepseek-ai/dsh-tool-subagent-control` package so provider-bound `@deepseek-ai/dsh-tool-subagent` instances can continue registering distinct delegation tools for spawn, fork, or ACP without registering duplicate global control tools.
 
 - If the child has a running Task and live-steering capability, the service calls `run.steer(message, source)` and returns the existing Job id; it creates no Task of its own.
 - If the child has no running Task, `send_message` creates a fresh Task, cold-resumes the durable session with the message, and returns the new Job id.

+ 1 - 1
.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.zh.md

@@ -57,7 +57,7 @@ durable child Session
 
 ### 面向模型的 `send_message`
 
-模型获得一个由 `SubagentRuntime.followup()` 支撑的 `send_message(subagent_id, message)` 工具,与 `Agent` 上的意图动词一致。该服务操作负责在 steering 与恢复之间编排;它不同于 run 的 `SubagentRun.steer?()`,后者只能向已活跃的 run 发送消息。工具本身不执行生命周期路由。该工具将后续消息的来源标记为 `{ kind: 'coordinator', senderSessionId: parent.id }`,并转发 `{ source, signal }`;服务要求在一个选项对象中同时提供这两项信息。来源会贯穿在线 steering 和 cold resume 两条路径,而取消只控制尚未完成的在线投递等待,因为 cold resume Task 会立即返回,并自行负责后续取消。child 模型收到的仍是普通的 user role 内容,而持久化的来源信息可防止模型生成的后续消息被归类为直接用户输入。用户适配器则提供 `{ kind: 'user' }` 及其交互信号。该工具位于单独加载的 `@deepseek-ai/dsh-tool-subagent-control` 包中,因此按提供方绑定的 `@deepseek-ai/dsh-tool-subagent` 实例可以继续为 spawn、fork 或 ACP 注册不同的委派工具,而不会重复注册全局控制工具。
+模型获得一个由 `SubagentRuntime.sendMessage()` 支撑的全局 `send_message(agent_id, message)` 工具。确切在线 sender 只能指定其直接 parent 或直接可继续 child;服务负责相邻关系检查、冷恢复、固定 Steer 调度,以及持久化 `{ kind: 'agent-message', senderSessionId }` 来源信息。取消只负责 inbox 接受前的工作。用户适配器保持分离,因为浏览器编写的输入携带用户来源信息与请求身份,而不是 Agent 权限。该工具位于单独加载的 `@deepseek-ai/dsh-tool-subagent-control` 包中,因此按提供方绑定的 `@deepseek-ai/dsh-tool-subagent` 实例可以继续为 spawn、fork 或 ACP 注册不同的委派工具,而不会重复注册全局控制工具。
 
 - 如果 child 存在运行中的 Task 并支持在线消息,服务会调用 `run.steer(message, source)` 并返回现有 job id;它不会创建新 Task。
 - 如果 child 没有运行中的 Task,`send_message` 会创建新 Task,使用该消息从持久化存储恢复会话,并返回新的 job id。

+ 2 - 2
.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: f456bacbf775bf914b47051e19639811e2385f65
-2026-07-28-continuable-subagent-conversations.zh.md: ea6df53026b6a76d8e100fa4fef0aebcd8fcd7cf
+2026-07-28-continuable-subagent-conversations.md: 1efb18ab69cfe779a7de85f288a6e3aa7caaca83
+2026-07-28-continuable-subagent-conversations.zh.md: 7cb1b4a4cff1fc7f6fc41662d05693917084b63a

+ 5 - 7
.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md

@@ -44,7 +44,7 @@ Cold resume does not dispatch through a subagent provider. The continuation mana
 
 `SubagentProvider.start()` and `SubagentRun` remain exclusively on the unchanged one-shot path. A continuable Activation directly owns its `AgentHandle` and never creates, wraps, or retains a `SubagentRun`; `SubagentRun.steer?()` is therefore absent.
 
-`ctx.subagents.followup(parent, childId, content, { source, signal })` remains the sole parent-to-child continuation-message operation. The exact live parent Agent authorizes delivery; cold resume checks that authority before reconstruction and every path checks it again in the final no-await inbox-admission span, so a parent unregistered or replaced during materialization cannot authorize delivery. `source` records who supplied the admitted message and grants no authority. The model-facing `send_message` tool keeps only its stable `subagent_id` and `message` fields and always submits a follow-up turn. Both start and follow-up return the accepted `MessageId`, and neither reports how the manager materialized the Activation.
+`ctx.subagents.sendMessage(sender, targetId, content, { signal })` is the sole model-authored continuation-message operation. The exact live sender authorizes delivery to its direct parent or direct continuable child; cold resume checks direct-child authority before reconstruction and every path checks again in the final no-await inbox-admission span, so an Agent unregistered or replaced during materialization cannot authorize delivery. The service derives durable `agent-message` provenance from that sender. The model-facing `send_message` tool keeps only `agent_id` and `message` and uses fixed Steer scheduling. Both start and send return the accepted `MessageId`, and neither reports how the manager materialized the Activation.
 
 For start and follow-up, the caller signal owns lookup, materialization, and admission only until inbox acceptance. After the operation returns its `MessageId`, the manager owns the Activation independently; later caller cancellation does not cancel the accepted turn or dispose the child.
 
@@ -111,15 +111,13 @@ Top-level teardown is host-owned rather than represented as another Activation.
 
 The activation-owner scope exists because ordinary Cordis owner effects unwind in reverse registration order, which cannot express the dynamic child graph. Manager initialization registers the private scope's structural disposer first and its drain disposer afterward, so reverse unwind invokes the drain before releasing that scope; merely registering a cleanup effect on the same scope as later Agent handles would allow structural handle disposal to bypass child-first ordering. Each materialization registers its barrier participant and snapshots its exact live ancestry before starting the inner transaction, then remains tracked until it installs an Activation or fully rolls back. The Activation retains weak membership of that ancestry, so an intermediate Agent may leave the registry without hiding a still-live descendant from its host root. Each Activation installs one memoized disposal promise before cancellation or recursive callbacks, allowing scoped host shutdown, global manager unload, child release, and normal settlement to converge without double release. Cancellation propagates top-down before slow descendant cleanup; handle release remains child-first. Sibling branches drain independently; one disposal failure is recorded but does not prevent the manager from attempting the remaining selected handles, and the aggregate drain reports failure after all selected branches settle. Durable child Sessions survive this process-local teardown.
 
-### Report delivery extension
+### Adjacent-Agent messaging
 
-The optional child-scoped `report(output)` tool was added later without changing Activation residency or adding another queue. It can be called zero or multiple times per turn, derives the live direct parent rather than accepting a recipient, and selects quiet injection or a waking parent follow-up through deployment config. The [report-tool Agent Note](2026-07-30-continuable-subagent-report-tool.md) owns its authority, acknowledgement, setup-contribution, and delivery contracts.
+The shared `sendMessage(sender, targetId, content, options)` service operation adds no second queue. It accepts an exact live sender, permits only its direct parent or direct continuable child, and uses fixed Steer scheduling through the Agent inbox. The global `send_message({ agent_id, message })` tool exposes that same operation in both directions; the child's initial task identifies its direct parent when the tool is visible. The [adjacent-Agent messaging Agent Note](../architecture/2026-08-27-adjacent-agent-steer-messaging.md) owns its schema, authority, attribution, and prompt placement.
 
-### Deferred steering
+### Fixed Steer scheduling
 
-This version exposes no subagent steering operation. Parent continuation messages always open later FIFO turns, so the continuation layer stores no current-turn controller and adds no controller-aware Agent admission contract.
-
-A later host UI may expose separate **Steer** and **Follow up** actions. Host steering would be strict and live-only: it may call the existing Agent steering path only while the Activation accepts a next step, must reject otherwise, and must never fall back to queueing or cold resume. Exposing parent steering to a model-facing tool remains a separate design.
+Every accepted Agent message uses `Agent.steer()`. A running target claims it at the nearest step boundary; an idle or cold-resumed target starts a turn. The continuation layer does not expose a caller-selectable quiet, next-turn, or follow-up mode.
 
 ### Authority and recorded sender identity
 

+ 5 - 7
.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.zh.md

@@ -44,7 +44,7 @@ inbox 接受消息前发生任何失败,操作都会在不返回任何 id 的
 
 `SubagentProvider.start()` 和 `SubagentRun` 只保留在不变的 one-shot 路径上。可继续激活直接持有自身的 `AgentHandle`,绝不创建、包装或保留 `SubagentRun`;因此,`SubagentRun.steer?()` 不存在。
 
-`ctx.subagents.followup(parent, childId, content, { source, signal })` 仍是唯一的从 parent 到 child 的继续执行消息操作。确切的在线 parent Agent 授权投递;冷恢复会在重建前检查该权限,每条路径还会在最终无 await 的 inbox 准入区间再次检查,因此在物化期间被注销或替换的 parent 无法授权投递。`source` 记录谁提供了获准消息,不赋予任何权限。面向模型的 `send_message` 工具只保留稳定的 `subagent_id` 和 `message` 字段,并始终提交一个 follow-up 轮次。start 和 follow-up 都返回已接受的 `MessageId`,两者都不报告管理器如何物化激活
+`ctx.subagents.sendMessage(sender, targetId, content, { signal })` 是唯一由模型编写的继续执行消息操作。确切在线 sender 授权向其直接 parent 或直接可继续 child 投递;冷恢复会在重建前检查直接 child 权限,每条路径还会在最终无 await 的 inbox 准入区间再次检查,因此在物化期间被注销或替换的 Agent 无法授权投递。服务从该 sender 推导持久化 `agent-message` 来源信息。面向模型的 `send_message` 工具只保留 `agent_id` 和 `message`,并使用固定 Steer 调度。start 与 send 都返回已接受的 `MessageId`,两者都不报告管理器如何物化 Activation
 
 对于 start 和 follow-up,调用方 signal 只在 inbox 接受消息前持有查找、物化和准入。操作返回 `MessageId` 后,管理器会独立持有该激活;调用方之后的取消不会取消已接受的轮次,也不会 dispose child。
 
@@ -111,15 +111,13 @@ Agent inbox 是唯一队列。每条继续执行消息都使用 `Agent.followup(
 
 activation-owner 作用域之所以存在,是因为普通 Cordis owner effect 按注册逆序撤销,无法表达动态 child 图。管理器初始化时先注册私有作用域的结构化 disposer,再注册自身的 drain disposer,使逆序撤销先执行 drain、再释放该作用域;如果只在与后续 Agent handle 相同的作用域上注册 cleanup effect,结构化 handle dispose 就可能绕过 child-first 顺序。每个物化过程都会在启动内部事务前注册其屏障参与项,并对其确切的在线祖先建立快照,然后保持跟踪,直到安装 Activation 或完全回滚。Activation 会保留其在这组祖先中的弱成员关系,因此中间 Agent 即使离开注册表,也不会让仍在线的后代脱离宿主根节点的可见范围。每个 Activation 都会在取消或递归回调前安装一个记忆化的 dispose promise,使限定作用域的宿主关闭、全局管理器卸载、child 释放和正常结算能够汇合,而不会重复释放。取消会在等待缓慢的后代清理之前自顶向下传播;handle 释放仍是 child-first。同级分支独立 drain;系统会记录单次 dispose 失败,但仍会尝试其余选中 handle,聚合 drain 则在所有选中分支结算后报告失败。这次进程内拆卸不会销毁持久化 child 会话。
 
-### 报告投递扩展
+### 相邻 Agent 消息
 
-后来添加的可选 child 作用域 `report(output)` 工具不会改变 Activation 驻留状态,也不会增加另一条队列。它每轮可调用零次或多次,不允许指定接收方,而是推导在线的直接 parent;投递采用静默注入还是唤醒 parent follow-up,由部署配置选择。[report 工具 Agent Note](2026-07-30-continuable-subagent-report-tool.zh.md)规定其权限、确认、设置贡献和投递约定
+共享的 `sendMessage(sender, targetId, content, options)` 服务操作不会增加第二条队列。它接收确切在线 sender,只允许其直接 parent 或直接可继续 child,并通过 Agent inbox 使用固定 Steer 调度。全局 `send_message({ agent_id, message })` 工具在两个方向暴露同一个操作;当 child 可以看到该工具时,其初始任务会标明直接 parent。[相邻 Agent 消息 Agent Note](../architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md)规定其 schema、权限、来源信息与提示词位置
 
-### 延后的 steering(中途引导)
+### 固定 Steer 调度
 
-本版本不暴露 subagent steering 操作。parent 的继续执行消息始终开启后续 FIFO 轮次,因此继续执行层不存储当前轮次控制方,也不新增能够感知控制方的 Agent 准入约定。
-
-后续宿主 UI 可以分别暴露 **Steer** 和 **Follow up** 操作。宿主 steering 必须严格且仅限在线使用:只有当激活接受下一步骤时,它才能调用现有的 Agent steering 路径;其他情况必须拒绝,而且绝不能转为排队或冷恢复。是否通过面向模型的工具暴露 parent steering 仍需单独设计。
+每条已接受的 Agent 消息都使用 `Agent.steer()`。运行中的目标会在最近的 step 边界领取消息;空闲或冷恢复的目标会启动一个轮次。继续执行层不暴露由调用方选择的 quiet、next-turn 或 follow-up 模式。
 
 ### 权限与已记录的发送方身份
 

+ 0 - 118
.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.md

@@ -1,118 +0,0 @@
-# Agent Note: Continuable subagent report tool
-
-Status: implemented
-
-English | [中文](2026-07-30-continuable-subagent-report-tool.zh.md)
-
-## Problem
-
-Continuable in-process subagents can receive later parent messages, retain descendants, settle, and cold-resume, but the base lifecycle gives them no way to send selected content back to their direct parent. Their complete output already remains reconstructable from the durable child Session, so the missing capability is explicit delivery rather than result storage.
-
-Treating every final assistant message as an implicit result would conflate turn completion with reporting. A long-lived child may have nothing useful to report in one turn, may report progress several times in another, and must remain available after reporting. Recipient authority, quiet versus next-step delivery, acknowledgement, durability, and retry behavior therefore need one explicit contract.
-
-## Decision
-
-Add the independently installed `@deepseek-ai/dsh-tool-subagent-report` package. It contributes an ordinary model-facing `report` tool to each continuable in-process child Activation. The mechanism accepts zero or multiple calls in a turn; the child is separately instructed to call it once before finishing ([the report obligation](2026-08-06-continuable-child-report-obligation.md)). Success neither concludes the turn, settles the Activation, nor prevents later parent follow-ups, and finishing a turn never reports automatically.
-
-The feature is a collaboration control, not a result-bearing execution wrapper. It adds no Task, `SubagentRun`, result promise, Activation state, delivery queue, or replay path.
-
-### Model-facing contract
-
-`report` accepts exactly `{ output: string }` and returns exactly `{ messageId: string }`. It accepts no child id, recipient id, or delivery mode. `exec.agent` binds the tool call to the reporting child, the service derives the sole recipient from durable `parentSession`, and deployment config owns scheduling.
-
-`messageId` is the stable `MessageId` of the user-role message accepted 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.
-
-The tool uses generic rendering with no locations. Its acknowledgement includes `messageId`. Scope-local registration keeps presentation and execution aligned: roots, one-shot children, remote providers, sibling scopes, and agentless execution neither see nor execute `report`. It installs after the child's global `toolFilter`, so a delegation allow-list cannot accidentally remove the structural return channel; deployments that require no return channel omit the package.
-
-### Service authority
-
-The subagent seam exposes `ctx.subagents.reportFrom(child, content, { delivery, signal }): Promise<MessageId>`. The exact live child Agent is the sender credential. The continuation manager accepts only an Activation whose `handle.agent === child`, derives its direct parent from the child's durable header, and requires that id to resolve to a live parent Agent in the final synchronous authorization-and-send span. The API accepts no caller-selected recipient, ancestor, or sender fields.
-
-Roots, one-shot children, forged objects, stale Agents, and same-id replacements fail with `UNAUTHORIZED`. A closing child Activation fails with `ACTIVATION_CLOSING`; manager drain and pre-acceptance cancellation retain their existing lifecycle errors. A missing or send-rejecting direct parent fails with `PARENT_UNAVAILABLE` and `direct parent is not live; report was not delivered`. Failure returns no id, cold-resumes no parent, writes no offline mailbox, and mutates no absent-parent Session.
-
-Nested reporting crosses exactly one edge. A grandchild reports to its direct child parent, never to the top-level coordinator. That intermediate child may explicitly report a derived update later.
-
-### Delivery policy
-
-The package validates `reportDelivery: 'quiet' | '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 next-step context without waking an idle parent; a running parent stages the report for the next safe log position.
-
-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 <child-id> 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. 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.
-
-### Composition and lifecycle
-
-The subagent seam adds `registerContinuableSetup(contribution): () => void`, backed by `SubagentActivationSetupRegistry`. Each synchronous contribution receives the unpublished child context and returns the disposer for its installation. The continuation manager first applies base child composition, then current contributions in registration order through the same setup closure used for fresh creation and cold resume.
-
-The registry owns registration, per-child installation records, setup rollback, child-scope cleanup, and immediate revocation. Applying a batch returns the Agent setup commit that revalidates provisioning after every setup await and immediately before Agent publication. A throwing or concurrently revoked contribution therefore rejects before either Agent or Session publication and rolls back the batch. New registrations affect a resident child only on its next Activation; removing a registration first closes it to new setup and then revokes every provisioning or resident installation immediately. Registration disposal and child-context disposal are idempotent and attempt every release before aggregating failures.
-
-This seam keeps the continuation manager unaware of tool names. The report package installs only `report` and its child-scoped guidance section; `@deepseek-ai/dsh-tool-subagent-control` independently installs parent-side `send_message` and `list_agents`. A deployment can install either direction, both, or neither. Providers remain data-only, durable descriptors do not snapshot report availability or delivery mode, and cold resume uses the deployment's current contributions and policy.
-
-### Snapshot coverage
-
-The ACP snapshot harness adds `waitForSubagentTurnEnd`, selecting the Nth harvested child by the same order as `session.N.jsonl`. It waits for a closed child turn containing a request header so a continuable child's earlier descriptor-seed turn cannot satisfy the boundary. This lets the assembled 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, 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
-
-### Automatically deliver every final answer
-
-Automatic delivery cannot represent zero reports, progress reports, or several selected updates. It also couples reporting to settlement and can duplicate content already reported explicitly.
-
-### Always wake the parent
-
-Waking on every report creates unsolicited turns and can cascade through nested subagents. Quiet delivery was chosen as the default on the assumption that the parent had another reason to read its context. [The report obligation](2026-08-06-continuable-child-report-obligation.md) supersedes that choice: a parked background coordinator has no such reason, so waking is the default and this paragraph now records why `quiet` still exists.
-
-### 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 wakes the parent.
-
-### Register a global tool
-
-A global `report` would advertise an unusable capability to roots, one-shot children, remote children, and agentless callers. Execution-time rejection would make schema visibility disagree with authority.
-
-### Combine both directions in the control package
-
-`send_message` and `report` have different audiences, scopes, configuration, and lifecycle. Independent packages let deployments grant either direction without implying the other.
-
-### Persist an offline parent mailbox
-
-Mutating or cold-resuming an absent parent requires a new durable addressing, authorization, conflict, acknowledgement, and replay protocol. Requiring a live direct parent keeps the first version on the existing Agent send path.
-
-### Reintroduce a Task or result promise
-
-A result-bearing wrapper makes one report or one turn appear terminal and recreates the lifetime mismatch that continuable Activations removed. Explicit repeatable sends need no intermediate execution object.
-
-### Validate setup after Agent creation
-
-A post-creation revocation check can reject the Activation only after the Agent and Session have been published. Disposing the returned handle removes the live objects but cannot delete persistence through the current seam, leaving a resumable child that the continuation manager said was never established. Returning an `AgentSetupCommit` instead lets the Agent factory perform the same mutable-state check synchronously at its publication boundary.
-
-## 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`; 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.
-- 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, 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.
-
-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.

+ 0 - 118
.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.zh.md

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

+ 0 - 6
.agents/notes/implemented/feature/2026-08-06-continuable-child-report-obligation.i18n.yaml

@@ -1,6 +0,0 @@
-# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
-# side as of the last confirmed-consistent state. Both languages carry equal authority;
-# after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-06-continuable-child-report-obligation.md
-2026-08-06-continuable-child-report-obligation.md: 422d3ab74389e084becb75a145a82139dda38ac7
-2026-08-06-continuable-child-report-obligation.zh.md: ec7e19885204ddc99b2640407c605288f1e9c045

+ 0 - 60
.agents/notes/implemented/feature/2026-08-06-continuable-child-report-obligation.md

@@ -1,60 +0,0 @@
-# Agent Note: The continuable child return channel is an obligation
-
-Status: implemented
-
-English | [中文](2026-08-06-continuable-child-report-obligation.zh.md)
-
-## Problem
-
-A continuable background child owns its own Session, so nothing it writes there reaches the agent that started it. [The report tool](2026-07-30-continuable-subagent-report-tool.md) gave that child a return channel and then presented it as one option among several: the schema said "call this zero or more times", nothing in the child's prompt asked it to call the tool at all, and the accepted default scheduling (`quiet`) added the report to a parked parent's next request without waking it.
-
-Each of those choices is defensible alone. Together they made the return channel unusable as a delegation contract. A child that finished its work, wrote its answer into its own transcript, and stopped left the parent with nothing; a child that did report reached a parent that had already parked and would not read the report until something unrelated woke it. External reports of parents busy-polling `list_agents`, re-sending messages to settled children, and abandoning `subagent` for `workflow` all reduce to the same missing guarantee.
-
-## Decision
-
-The return channel is an instruction the child receives, not a capability it may discover. The report package installs two scope-local registrations into every continuable in-process child, and one disposer revokes both:
-
-- 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 first-party order 2900 carrying the same obligation in the child's own voice, so a child that never reads tool descriptions closely still receives it.
-
-`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
-
-They address different failure modes. The tool description is read when the model is already considering `report`; the prompt section is read when it is deciding whether it is finished. The obligation belongs at both points because the failure this fixes — a child that simply stops — happens at the second one.
-
-The section is registered on the child's own scope, the same mechanism [child composition](../../../../packages/subagent/subagent/src/child-agent.ts) already uses for a shadowing persona, so the parent and every sibling see neither the tool nor the guidance. `installReportTool` rolls the section back if tool registration fails, and its returned disposer attempts both revocations before surfacing cleanup failures.
-
-### Instruction, not enforcement
-
-Nothing rejects a child that never reports. No runtime path inspects whether a report was sent, and `report` still accepts zero or many calls per turn. The change is model-facing wording plus a scheduling default; the service authority, acknowledgement, and recovery contracts are unchanged.
-
-That boundary is deliberate: prompt text can only reach a child that is still running its own loop. A child stopped by an error, a token ceiling, cancellation, or teardown never gets the chance to comply, which is why the runtime keeps its own account of settlement rather than trusting this instruction ([manager-owned settlement delivery](2026-08-06-manager-owned-subagent-settlement-delivery.md)).
-
-### Snapshot coverage
-
-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.<n>.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
-
-**Keep `quiet` as the default and rely on the prompt alone.** This was the shipped position, and it supersedes nothing on its own: a report the parent never reads is indistinguishable from a report never sent. The [report-tool note's](2026-07-30-continuable-subagent-report-tool.md) rejection of always-waking assumed the parent had another reason to look at its context; a parked background coordinator does not. Turn amplification is the real cost, and it is now the reason `quiet` still exists rather than the reason it is the default.
-
-**Let the child choose the delivery mode per call.** Unchanged from the original rejection: the model would own scheduler pressure, and behavior would vary per call rather than per deployment.
-
-**Put the obligation only in the tool description.** A description is read while choosing among tools. The child this change targets is not choosing a tool; it believes it is done. Prompt guidance is the surface that reaches that decision.
-
-**Enforce the obligation at settlement by rejecting a silent child.** There is nothing to reject: by the time settlement is observable the child's loop is over, and failing its teardown would destroy work rather than deliver it. Delivering the terminal facts unconditionally from the runtime is the answer to that case, and it belongs to the continuation manager, not to this package.
-
-## 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 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
-
-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.

+ 0 - 60
.agents/notes/implemented/feature/2026-08-06-continuable-child-report-obligation.zh.md

@@ -1,60 +0,0 @@
-# Agent Note: 可继续 child 的返回通道是一项义务
-
-Status: implemented
-
-[English](2026-08-06-continuable-child-report-obligation.md) | 中文
-
-## 问题
-
-可继续后台 child 拥有自己的 Session,因此它写在那里的任何内容都不会到达启动它的 agent。[report 工具](2026-07-30-continuable-subagent-report-tool.zh.md)为该 child 提供了一条返回通道,却把它呈现为若干选项之一:schema 里写着「可调用零次或多次」,child 的提示词中没有任何地方要求它调用该工具,而已采纳的默认调度(`quiet`)会把报告加入已停驻 parent 的下一次请求,却不唤醒它。
-
-这些选择单独看都站得住脚。合在一起,它们让这条返回通道无法作为委派契约使用。一个完成工作、把答案写进自己 transcript(文本记录)随后停止的 child,会让 parent 一无所获;而确实上报了的 child,面对的是一个已经停驻、要等到别的事件把它唤醒才会读到报告的 parent。外部反馈中的 parent 忙轮询 `list_agents`、反复向已结算 child 发送消息、以及放弃 `subagent` 改用 `workflow`,都可归结为同一处缺失的保证。
-
-## 决策
-
-返回通道是 child 收到的一条指令,而不是它需要自行发现的能力。report 包会向每个可继续进程内 child 安装两项作用域局部注册,并由同一个 disposer 撤销两者:
-
-- `report` 工具,其描述现在说明 child 要在结束前调用一次并给出自足的最终结果,并在部分进展会改变 parent 下一步动作时提前调用;
-- 一个 first-party order 为 2900 的 `tool:report` 系统提示词 section,用 child 自己的语气承载同一条义务,使从不细读工具描述的 child 仍能收到它。
-
-`reportDelivery` 的默认值为 `next-step`。一条被接受的报告会唤醒停驻的 parent driver,或加入运行中 parent 最近的 step 边界,与发现会改变 parent 下一步动作时上报的指令一致。对于宁可让报告无人阅读也要避免模型工作量放大的部署,`quiet` 依旧可用。[报告与结算顺序决策](../bug-fix/2026-08-17-subagent-report-settlement-ordering.zh.md)负责调度理由。
-
-### 为什么 section 与描述同时存在
-
-两者针对不同的失效模式。工具描述是在模型已经在考虑 `report` 时被读到的;提示词 section 是在它判断自己是否已经完成时被读到的。这条义务必须同时出现在两处,因为本次修复的失效——child 直接停下——发生在第二处。
-
-该 section 注册在 child 自己的作用域上,与[child 组合](../../../../packages/subagent/subagent/src/child-agent.ts)为遮蔽式 persona 已经使用的机制相同,因此 parent 与所有同级都看不到该工具与该指引。工具注册失败时,`installReportTool` 会回滚该 section;它返回的 disposer 会先尝试撤销两项注册,再抛出清理失败。
-
-### 是指令,不是强制
-
-没有任何东西会拒绝一个从不上报的 child。没有任何运行时路径会检查是否发送过报告,`report` 仍接受一个轮次中调用零次或多次。本次改动是面向模型的措辞加上一个调度默认值;服务权限、确认与恢复契约都保持不变。
-
-这条边界是刻意划定的:提示词文本只能到达仍在运行自身循环的 child。被错误、token 上限、取消或拆卸终止的 child 根本没有机会遵守,因此运行时会自己记录结算这件事,而不是信任这条指令(见[由管理器负责的结算投递](2026-08-06-manager-owned-subagent-settlement-delivery.zh.md))。
-
-### 快照覆盖
-
-整体组装的 ACP `subagent-report` 场景演练随附的默认行为:child 在 parent 处于 maintenance 时上报,稍后的结算通知排在其后,而恢复的 parent 会先领取 next-step 报告、再领取 next-turn 结算。由于该 child 的作用域组合出类别 pin 无法描述的提示词,快照 harness 提供 `pinsChildSystemPrompts`,它与 `pinsChildToolSchemas` 完全对称:把一个 child fixture 的提示词移入 `system-prompt.<n>.expected.md`,其余请求 header 字段仍归类别 pin 所有,要求 sidecar 恰好在声明时存在,并拒绝与该类别 pin 完全相同的 sidecar,使冗余副本无法悄悄漂移。
-
-## 备选方案
-
-**保留 `quiet` 作为默认值,只依赖提示词。** 这曾是随附的立场,而它本身什么也没有解决:一条 parent 从不阅读的报告,与一条从未发送的报告无法区分。[report 工具 Agent Note](2026-07-30-continuable-subagent-report-tool.zh.md)对「始终唤醒」的否决,前提是 parent 还有别的理由去查看自己的上下文;已停驻的后台协调者并没有。轮次放大才是真正的代价,而它现在是 `quiet` 仍然保留的理由,而不是它作为默认值的理由。
-
-**让 child 按调用选择投递模式。** 与最初的否决相同:模型将掌握调度压力,行为也会随调用而非随部署变化。
-
-**只把义务写在工具描述里。** 描述是在从多个工具中选择时被读到的。本次改动针对的 child 并不在选择工具,它认为自己已经做完了。提示词指引才是能触及该判断的界面。
-
-**在结算时拒绝沉默的 child,以此强制该义务。** 没有什么可以拒绝:当结算可被观察时 child 的循环已经结束,让它的拆卸失败只会毁掉工作而不会送达结果。由运行时无条件投递终止事实才是这一情形的答案,而它属于继续执行管理器,不属于本包。
-
-## 后果
-
-- 加载本包后,每个可继续进程内 child 的每次请求都会多出一个提示词 section 和一段更长的 `report` 描述;其他任何 Agent 的请求都不变。
-- 默认部署会为每条被接受的报告唤醒 parent 一次。频繁上报的嵌套树会消耗额外的 parent 请求,而一起等待的报告会共享一个 step;`quiet` 是有文档记载的退路。
-- `installReportTool` 需要 child 作用域中的 `ctx.systemPrompt`,因此本包在 `inject` 中声明 `systemPrompt`,从而在加载时失败,而不是等到下一次 child 物化时。
-- 单元覆盖固定了新默认值、两处关键指令措辞、该 section 相对 parent 与同级均仅限 child 的作用域,以及两项注册在安装回滚或撤销时的清理。
-- 三个带可继续 child 的整体组装 ACP 场景通过新的 sidecar 逐字固定完整的 child 提示词;今后任何对 child 作用域 section 的改动都会让这些场景失败,而不是悄悄通过。
-
-### 已接受的风险
-
-默认 next-step 投递会在深层树中放大模型工作量。部署通过 `reportDelivery` 掌握该取舍;一起等待的报告会共享一个 step,且每条被接受的报告至多产生一次唤醒。
-
-child 仍可能不上报就结束,本次改动无法检测这一点。只有运行时自己的[结算记账](2026-08-06-manager-owned-subagent-settlement-delivery.zh.md)才能补上这一情形。

+ 2 - 2
.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: 27daa6d5150950efb50bf23dea945498651d2c09
-2026-08-06-manager-owned-subagent-settlement-delivery.zh.md: 1e3b866204d54e26b87061153fe97ffaed369141
+2026-08-06-manager-owned-subagent-settlement-delivery.md: 7dfa05c247ee0efb71963e057731c7ff6a5a1989
+2026-08-06-manager-owned-subagent-settlement-delivery.zh.md: 6bd8836d966c85ee0ef08ec19bbf96a4f72b0dd4

+ 9 - 9
.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.md

@@ -8,7 +8,7 @@ English | [中文](2026-08-06-manager-owned-subagent-settlement-delivery.zh.md)
 
 Continuable background delegation was the one asynchronous operation a model could start but could not reach the end of. Every other shape has a retrieval primitive or a return value: a background bash command and a one-shot background subagent both settle through a Task that `job_output(wait: true)` can block on, a workflow and a foreground subagent return their result to the caller. A continuable background child returned only its durable id, and nothing existed that a parent could wait on or would be handed.
 
-[The report obligation](2026-08-06-continuable-child-report-obligation.md) closed the cooperative half of that gap by instructing the child to report before it finishes. Instruction cannot close the rest. A child stopped by a token ceiling, a model failure, cancellation, or teardown never reaches the point where it could comply — not rarely, but never — and those are precisely the endings a waiting parent most needs to hear about. The observable downstream symptoms were parents busy-polling `list_agents`, re-sending messages to children that had already settled, and deployments abandoning `subagent` for `workflow` because a workflow at least returns something.
+Child-authored messages close the cooperative half of that gap: a child can send progress and a final handoff to its direct parent. Model choice cannot close the rest. A child stopped by a token ceiling, a model failure, cancellation, or teardown may never send such a message, and those are precisely the endings a waiting parent most needs to hear about. The observable downstream symptoms were parents busy-polling `list_agents`, re-sending messages to children that had already settled, and deployments abandoning `subagent` for `workflow` because a workflow at least returns something.
 
 The signal already existed. `subagent/end` has carried `stopReason` and `lastAssistantMessage` since continuable Activations shipped. What was missing was any consumer that turned it into context the parent's model could see.
 
@@ -20,7 +20,7 @@ When a resident Activation settles, `notifySettlement()` resolves the child's du
 
 ### Provenance
 
-The notice carries `{ kind: 'subagent-settled', form: 'notice', summary, senderSessionId }`. It is deliberately not the existing `subagent-report` kind. A report is content the child chose; this is the runtime stating what became of the child. Merging them would credit the child with words it never wrote, and would make a durable log unable to distinguish "the child said it was done" from "the harness observed that it stopped". The `notice` form also gives a UI the collapsed one-line presentation this message wants, where `relay` would present it as correspondence.
+The notice carries `{ kind: 'subagent-settled', form: 'notice', summary, senderSessionId }`. It is deliberately not the `agent-message` kind used by `send_message`. An Agent message is content the child chose; this is the runtime stating what became of the child. Merging them would credit the child with words it never wrote, and would make a durable log unable to distinguish "the child said it was done" from "the harness observed that it stopped". The `notice` form also gives a UI the collapsed one-line presentation this message wants, where `relay` presents Agent correspondence.
 
 ### Two ordering rules, and why the manager owns them
 
@@ -56,13 +56,13 @@ Both matter past the notice: `subagent/end` carries `stopReason` to the jsonrpc
 
 ### Snapshot coverage
 
-Three assembled ACP scenarios cover the notice: a child that never reports, a child that reports first, and a child driven through several follow-up turns. All three needed an explicit fence. The notice arrives once the child's teardown finishes, which races whatever the parent is already doing, so each scenario holds the child behind the parent's spawn turn and then waits for the parent turn the notice opens (`waitForTurnStart` at that turn, then `waitForTurnEnd`) before the script continues. Waiting for a turn the run is not fenced to produce is not coverage: it is a timeout when the notice lands in the turn already running instead.
+Three assembled ACP scenarios cover the notice: a child that sends no message, a child that sends a message first, and a child driven through several Agent-message turns. All three need an explicit fence. The notice arrives once the child's teardown finishes, which races whatever the parent is already doing, so each scenario holds the child behind the parent's spawn turn and then waits for the parent turn the notice opens (`waitForTurnStart` at that turn, then `waitForTurnEnd`) before the script continues. Waiting for a turn the run is not fenced to produce is not coverage: it is a timeout when the notice lands in the turn already running instead.
 
 `subagent-continuable` is the one that pins a failure. Its child's last turn dies on the forced durability checkpoint without entering a step, so that transcript is where the stop-reason rule above is visible end to end: the notice says the child *failed*, carries the earlier `SECOND_OK` as its last content rather than as a result, and the parent's own acknowledgement turn reaches the ACP client.
 
-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.
+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 sends no Agent message, so the transcript depends only on the runtime notice. 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.
 
-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 `subagent-send-message` scenario holds the child until the parent's spawn turn ends, then holds the parent in maintenance until settlement follows the child-authored message. The resumed parent claims the next-step Agent message before the queued next-turn settlement. The [message/settlement ordering decision](../bug-fix/2026-08-17-subagent-message-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.
 
@@ -72,7 +72,7 @@ The refusal and interruption wordings are pinned verbatim in unit tests rather t
 
 **Attach an external `subagent/end` listener.** Rejected on three counts above — no parent in the payload, a disposed child handle, and an ordering the listener cannot influence. A listener would also have to be strictly synchronous to beat the release, and nothing at that seam enforces it, so the correct version would be correct only by accident.
 
-**Deliver only when the child did not report.** This was the first design. It needs per-Activation bookkeeping, still misses the child that reported progress and then died before its result, and — decisively — makes the parent-facing promise conditional. "Usually you are told" is not a contract a tool description can state, and a model that cannot rely on the notice will poll anyway.
+**Deliver only when the child sent no message.** This was the first design. It needs per-Activation bookkeeping, still misses the child that sent progress and then died before its result, and — decisively — makes the parent-facing promise conditional. "Usually you are told" is not a contract a tool description can state, and a model that cannot rely on the notice will poll anyway.
 
 **Make delivery configurable.** A deployment switch would return the model-facing text to "usually", which is the failure this change exists to remove. Protocol constants and safety invariants stay fixed; this is one of them.
 
@@ -87,8 +87,8 @@ 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` 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.
+- Three ACP scenarios use an explicit settlement fence, and `subagent-send-message` pins the Agent-message-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-authored message.
 
 ### 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.
 
-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.
+Agent messages 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.

+ 9 - 9
.agents/notes/implemented/feature/2026-08-06-manager-owned-subagent-settlement-delivery.zh.md

@@ -8,7 +8,7 @@ Status: implemented
 
 可继续后台委派是模型唯一一种能够发起、却无法抵达终点的异步操作。其他每一种形态都有取回原语或返回值:后台 bash 命令与一次性后台 subagent 都通过 Task 结算,`job_output(wait: true)` 可以阻塞等待;workflow 与前台 subagent 会把结果返回给调用方。可继续后台 child 只返回它持久化的 id,而父级既没有可等待的对象,也不会被交付任何东西。
 
-[报告义务](2026-08-06-continuable-child-report-obligation.zh.md)通过要求 child 在结束前上报,补上了这一缺口中协作的那一半。指令无法补上其余部分。被 token 上限、模型失败、取消或拆卸终止的 child 永远走不到能够遵守的那一步——不是很少,而是从不——而这些恰恰是等待中的父级最需要被告知的结束方式。可观察到的下游症状包括:父级忙轮询 `list_agents`、向已经结算的 child 反复发送消息,以及部署放弃 `subagent` 转用 `workflow`,因为 workflow 至少会返回点什么。
+由 child 编写的消息补上了这一缺口中协作的那一半:child 可以向直接 parent 发送进度与最终交接。模型选择无法补上其余部分。被 token 上限、模型失败、取消或拆卸终止的 child 可能永远不会发送这种消息,而这些恰恰是等待中的父级最需要被告知的结束方式。可观察到的下游症状包括:父级忙轮询 `list_agents`、向已经结算的 child 反复发送消息,以及部署放弃 `subagent` 转用 `workflow`,因为 workflow 至少会返回点什么。
 
 信号本身早就存在。自可继续 Activation 发布以来,`subagent/end` 就一直携带 `stopReason` 与 `lastAssistantMessage`。缺的是把它变成父级模型能看到的上下文的那个消费者。
 
@@ -20,7 +20,7 @@ Status: implemented
 
 ### 来源信息
 
-该通知携带 `{ kind: 'subagent-settled', form: 'notice', summary, senderSessionId }`,刻意不复用既有的 `subagent-report` kind。上报是 child 选择的内容;这条消息则是运行时在陈述这个 child 后来怎样了。把两者合并会把 child 从未写过的话算到它头上,也会让持久化日志无法区分「child 说它做完了」和「harness 观察到它停下了」。`notice` 形态还为 UI 提供了这条消息想要的折叠单行呈现,而 `relay` 会把它呈现为往来信件
+该通知携带 `{ kind: 'subagent-settled', form: 'notice', summary, senderSessionId }`,刻意不复用 `send_message` 使用的 `agent-message` kind。Agent 消息是 child 选择的内容;这条消息则是运行时在陈述这个 child 后来怎样了。把两者合并会把 child 从未写过的话算到它头上,也会让持久化日志无法区分「child 说它做完了」和「harness 观察到它停下了」。`notice` 形态还为 UI 提供了这条消息想要的折叠单行呈现,而 `relay` 会把 Agent 往来消息呈现为通信
 
 ### 两条顺序规则,以及为什么归管理器所有
 
@@ -56,13 +56,13 @@ Status: implemented
 
 ### 快照覆盖
 
-三个整体组装的 ACP 场景覆盖该通知:一个从不上报的 child、一个先上报的 child,以及一个被多轮 follow-up 驱动的 child。三者都需要显式栅栏。通知在 child 拆卸完成后才到达,会与父级当时正在做的事竞争,因此每个场景都会把 child 保持到父级启动轮次结束,再等待该通知开启的那个父级轮次(先 `waitForTurnStart` 到该轮次,再 `waitForTurnEnd`),然后脚本才继续。等待一个运行并未被栅栏保证会产生的轮次不算覆盖:一旦通知落进已经在跑的那个轮次,它就是一次超时。
+三个整体组装的 ACP 场景覆盖该通知:一个不发送消息的 child、一个先发送消息的 child,以及一个被多轮 Agent 消息驱动的 child。三者都需要显式栅栏。通知在 child 拆卸完成后才到达,会与父级当时正在做的事竞争,因此每个场景都会把 child 保持到父级启动轮次结束,再等待该通知开启的那个父级轮次(先 `waitForTurnStart` 到该轮次,再 `waitForTurnEnd`),然后脚本才继续。等待一个运行并未被栅栏保证会产生的轮次不算覆盖:一旦通知落进已经在跑的那个轮次,它就是一次超时。
 
 `subagent-continuable` 是其中固定失败结局的那个。它的 child 最后一个轮次在被强制的持久化检查点上死亡,且未进入任何 step,因此该 transcript 正是上面那条终止原因规则的端到端可见之处:通知说该 child **失败**,把此前的 `SECOND_OK` 作为它最后产出的内容而非结果携带,而父级自己的确认轮次会到达 ACP 客户端。
 
-另有一个无密钥的 headless Loader 快照端到端覆盖用户可见路径。其重放父级省略 `run_in_background` 以覆盖可继续后台默认路径,从不调用 `list_agents`、`send_message` 或 Task 工具,消费管理器写入的 `subagent-settled` 通知,并给出最终答案。child 从不调用 `report`,因此该 transcript 不可能经由协作式上报路径通过。一个仅用于测试的 Loader 栅栏会把父级启动后的请求保持到真实管理器通知进入其 inbox 为止,从 transcript 中排除平台调度差异,但不会伪造该通知。
+另有一个无密钥的 headless Loader 快照端到端覆盖用户可见路径。其重放父级省略 `run_in_background` 以覆盖可继续后台默认路径,从不调用 `list_agents`、`send_message` 或 Task 工具,消费管理器写入的 `subagent-settled` 通知,并给出最终答案。child 不发送 Agent 消息,因此该 transcript 只依赖运行时通知。一个仅用于测试的 Loader 栅栏会把父级启动后的请求保持到真实管理器通知进入其 inbox 为止,从 transcript 中排除平台调度差异,但不会伪造该通知。
 
-`subagent-report` 场景使用默认 next-step 报告投递。一个仅用于快照的围栏会让 child 等到 parent 的派生轮次结束,随后让 parent 保持 maintenance,直至结算跟在报告之后到达。恢复的 parent 会先领取 next-step 报告、再领取排队的 next-turn 结算。[报告与结算顺序决策](../bug-fix/2026-08-17-subagent-report-settlement-ordering.zh.md)负责说明这种跨状态顺序。
+`subagent-send-message` 场景会让 child 等到 parent 的派生轮次结束,随后让 parent 保持 maintenance,直至结算跟在 child 编写的消息之后到达。恢复的 parent 会先领取 next-step Agent 消息、再领取排队的 next-turn 结算。[消息与结算顺序决策](../bug-fix/2026-08-17-subagent-message-settlement-ordering.zh.md)负责说明这种跨状态顺序。
 
 拒绝与中断两种措辞在单元测试中逐字钉死,而不进入重放 transcript:触发它们需要一个会拒绝的策略插件、或一次在 step 边界被栅栏卡住的取消,而无密钥组装本身并不携带这些;通知通路本身已由整体组装场景端到端钉住。
 
@@ -72,7 +72,7 @@ Status: implemented
 
 **挂一个外部 `subagent/end` listener。** 因上文三点被否决——payload 里没有父级、child handle 已被 dispose,以及 listener 无法影响的顺序。listener 还必须严格同步才能抢在释放之前,而该 seam 上没有任何东西强制这一点,因此正确的版本只能靠碰巧正确。
 
-**仅在 child 没有上报时投递。** 这是最初的设计。它需要按 Activation 记账,仍会漏掉「报了进度、随后在给出结果前死掉」的 child,而且最关键的是:它让面向父级的承诺变成有条件的。「通常你会被告知」不是工具描述能陈述的契约,而无法依赖该通知的模型无论如何都会去轮询。
+**仅在 child 没有发送消息时投递。** 这是最初的设计。它需要按 Activation 记账,仍会漏掉「发送了进度、随后在给出结果前死掉」的 child,而且最关键的是:它让面向父级的承诺变成有条件的。「通常你会被告知」不是工具描述能陈述的契约,而无法依赖该通知的模型无论如何都会去轮询。
 
 **把投递做成可配置。** 部署开关会把面向模型的文本重新变回「通常」,而这正是本次改动要消除的失效。协议常量与安全不变量保持固定;这就是其中之一。
 
@@ -87,8 +87,8 @@ Status: implemented
 - `Activation` 携带 `parentSession` 与 `announced`。前者存在是因为 child handle 在投递前已被 dispose;后者让被回滚的物化保持静默。
 - `foldConsumedWork()` 取代 `dsh-session` 的 `findLastMessageTurnEnd()`,并迁移到 `dsh-agent`——它拥有该 fold 所读取的 inbox 标记;一次性 in-process 路径折叠同一个答案,不会把被中途切断的一次性 child 归类为 `completed`。
 - 单元覆盖固定了无条件约定、每种终止原因、空闲与繁忙两种调度、批量语义、维护期回归、释放前顺序、父级已消失,以及一次不得让拆卸失败的发送被拒。
-- 三个 ACP 场景使用显式的结算围栏,`subagent-report` 固定默认的报告先于结算的 next-step 顺序。
-- 一个无密钥的 headless Loader 快照固定了「后台启动 → 管理器写入的结算通知 → 父级最终答案」路径,其中没有轮询,也没有 child `report` 调用
+- 三个 ACP 场景使用显式的结算围栏,`subagent-send-message` 固定 Agent 消息先于结算的 next-step 顺序。
+- 一个无密钥的 headless Loader 快照固定了「后台启动 → 管理器写入的结算通知 → 父级最终答案」路径,其中没有轮询,也没有 child 编写的消息
 
 ### 已接受的风险
 
@@ -100,4 +100,4 @@ Status: implemented
 
 对于深或宽的树,轮次放大是真实存在的,而且按设计不可配置。step 边界的批量语义只能约束同时结算的情形,无法约束分散结算的 child。
 
-报告与其稍后的结算通知通过 parent 的 next-step FIFO 排序。来自同级 child 的独立结算保留其实际投递顺序,不会虚构同级间的顺序。
+Agent 消息与其稍后的结算通知通过 parent 的 next-step FIFO 排序。来自同级 child 的独立结算保留其实际投递顺序,不会虚构同级间的顺序。

+ 2 - 2
.agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.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-10-continuable-subagent-policy-inheritance.md
-2026-08-10-continuable-subagent-policy-inheritance.md: a33a211747cf19cd465c56b8bda6ebfe1d7b6e06
-2026-08-10-continuable-subagent-policy-inheritance.zh.md: af43bbdc10c926b5255e8e5af98f7ea950b60091
+2026-08-10-continuable-subagent-policy-inheritance.md: e567c0557be45ba9e0e0515d742bd6f99a5f10e7
+2026-08-10-continuable-subagent-policy-inheritance.zh.md: 5c9cc677e9154d5b14214979283c00fd28a4c2df

+ 2 - 2
.agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.md

@@ -6,7 +6,7 @@ English | [中文](2026-08-10-continuable-subagent-policy-inheritance.zh.md)
 
 ## Problem
 
-The one-shot in-process driver has seeded parent sandbox/approval overrides into its children since the [in-process policy-inheritance decision](2026-07-25-subagent-policy-inheritance.md), but the continuable path never did: `SubagentContinuationManager` materialization applied only child composition and the activation setup registry. The default bundle wires both delegation tools as `backgroundMode: continuable`, so in a default deployment every background child silently fell back to deployment defaults — a parent switched to `danger-full-access` produced children stuck at `workspace-write` whose every out-of-workspace operation raised an approval prompt, and a parent's unattended `'never'` approval stance reverted to prompting ([dsh-external/issues#334](https://github.com/dsh-external/issues/issues/334)).
+The one-shot in-process driver has seeded parent sandbox/approval overrides into its children since the [in-process policy-inheritance decision](2026-07-25-subagent-policy-inheritance.md), but the continuable path never did: `SubagentContinuationManager` materialization applied only child composition. The default bundle wires both delegation tools as `backgroundMode: continuable`, so in a default deployment every background child silently fell back to deployment defaults — a parent switched to `danger-full-access` produced children stuck at `workspace-write` whose every out-of-workspace operation raised an approval prompt, and a parent's unattended `'never'` approval stance reverted to prompting ([dsh-external/issues#334](https://github.com/dsh-external/issues/issues/334)).
 
 ## Decision
 
@@ -16,7 +16,7 @@ The capture/append pair moved from the one-shot driver into the seam's shared ch
 
 ## Alternatives considered
 
-- **An activation-setup-registry contribution** (`registerContinuableSetup`) — rejected: a contribution receives only the child context, so it cannot capture the parent's overrides at the delegation boundary; the registry applies on cold resume as well as fresh creation, which would re-append or re-capture; and nothing ties a contribution's capture to the start call's synchronous prefix, so the pre-await capture guarantee would be lost.
+- **A generic child-setup contribution** — rejected: a contribution receives only the child context, so it cannot capture the parent's overrides at the delegation boundary; applying it on cold resume as well as fresh creation would re-append or re-capture; and nothing ties its capture to the start call's synchronous prefix, so the pre-await capture guarantee would be lost.
 - **Re-capturing the parent's overrides at cold resume** — rejected: a resumed child would silently change policy with the parent's later switches, breaking the snapshot-at-delegation semantic and making effective policy depend on resume timing instead of the child's own log. A parent that wants a resumed child under new policy re-delegates.
 - **Importing the one-shot driver's inline logic from the continuation manager** — rejected: the Service Definition package cannot depend on its own provider package, and duplicating the capture/append pair in `continuation.ts` invites drift; `child-agent.ts` already holds every other shared composition step.
 - **Seeding the events into the descriptor seed turn** — rejected: the capture value is not known when the seed is assembled for every caller, and the one-shot precedent already establishes unpublished-setup appends as the ordering that places inherited facts after fork history with `firstLiveSeq` intact.

+ 2 - 2
.agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.zh.md

@@ -6,7 +6,7 @@ Status: implemented
 
 ## 问题
 
-自[进程内策略继承决策](2026-07-25-subagent-policy-inheritance.zh.md)以来,一次性进程内驱动器一直会把父级的沙箱/审批覆盖项注入其子级,但可继续路径从未这样做:`SubagentContinuationManager` 的物化只应用子级组合与 Activation(激活)设置注册表。默认组合包把两个委派工具都配置为 `backgroundMode: continuable`,因此在默认部署中,每个后台子 agent(智能体)都静默回退到部署默认值:切换到 `danger-full-access` 的父级产出的子 agent 卡在 `workspace-write`,每次工作区外操作都会触发审批提示;父级无人值守的 `'never'` 审批立场也退回为发起提示的行为([dsh-external/issues#334](https://github.com/dsh-external/issues/issues/334))。
+自[进程内策略继承决策](2026-07-25-subagent-policy-inheritance.zh.md)以来,一次性进程内驱动器一直会把父级的沙箱/审批覆盖项注入其子级,但可继续路径从未这样做:`SubagentContinuationManager` 的物化只应用子级组合。默认组合包把两个委派工具都配置为 `backgroundMode: continuable`,因此在默认部署中,每个后台子 agent(智能体)都静默回退到部署默认值:切换到 `danger-full-access` 的父级产出的子 agent 卡在 `workspace-write`,每次工作区外操作都会触发审批提示;父级无人值守的 `'never'` 审批立场也退回为发起提示的行为([dsh-external/issues#334](https://github.com/dsh-external/issues/issues/334))。
 
 ## 决策
 
@@ -16,7 +16,7 @@ Status: implemented
 
 ## 考虑过的替代方案
 
-- **一项 Activation 设置注册表贡献**(`registerContinuableSetup`):不予采纳。贡献只接收子级上下文,因此无法在委派边界捕获父级的覆盖项;该注册表在冷恢复与全新创建时都会应用,会导致重复追加或重复捕获;而且没有任何机制把贡献的捕获绑定到 start 调用的同步前缀,await 前捕获的保证会因此丢失。
+- **一项通用 child 设置贡献**:不予采纳。贡献只接收子级上下文,因此无法在委派边界捕获父级的覆盖项;在冷恢复与全新创建时都应用它会导致重复追加或重复捕获;而且没有任何机制把它的捕获绑定到 start 调用的同步前缀,await 前捕获的保证会因此丢失。
 - **在冷恢复时重新捕获父级覆盖项**:不予采纳。恢复的子 agent 会随父级后续切换静默改变策略,这会破坏委派时快照的语义,并让生效策略取决于恢复时机而非子级自身的日志。希望恢复的子 agent 采用新策略的父级应重新委派。
 - **让继续执行管理器导入一次性驱动器的内联逻辑**:不予采纳。Service Definition 包不能依赖自己的提供方包,而在 `continuation.ts` 中复制捕获/追加这对函数会招致偏差;`child-agent.ts` 已经承载其余每个共享组合步骤。
 - **把这些事件写入描述符种子轮次**:不予采纳。种子为每个调用方组装时,捕获值尚不可知;而且一次性路径的先例已经确立:在未发布的设置阶段追加,才是把继承事实排在 fork 历史之后、同时保持 `firstLiveSeq` 不变的顺序。

+ 2 - 2
.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: 59232ae8821ef4a093fd610ecbbb39690316ce6c
-2026-08-11-background-first-continuable-delegation.zh.md: d39f129e0ceb4aa75ba91c6860fb017c69f0fbe6
+2026-08-11-background-first-continuable-delegation.md: a3dcd75c820742eda622b04ce6077b444f66846b
+2026-08-11-background-first-continuable-delegation.zh.md: f0cbdca1c1cba7400e6f9926453fb640bde575bb

+ 6 - 6
.agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.md

@@ -8,7 +8,7 @@ English | [中文](2026-08-11-background-first-continuable-delegation.zh.md)
 
 A continuable child already has a durable id, independent turns, follow-up messaging, and a manager-owned settlement notice. Treating an omitted `run_in_background` as foreground makes that lifecycle depend on the model restating `true` on every call. It also obscures the useful scheduling test: the parent should wait only when its next action requires the child's result.
 
-The child-scoped `report` prompt requires a self-contained final report, while [manager-owned settlement delivery](2026-08-06-manager-owned-subagent-settlement-delivery.md) independently sends the run outcome and closing message. A completed child can therefore wake its parent with a final report and again with settlement. Background-first scheduling must preserve both deliveries: the child-authored handoff remains mandatory guidance, while the manager-authored notice covers every terminal path regardless of model compliance.
+The child's initial task tells it how to address its direct parent with the shared `send_message` tool, while [manager-owned settlement delivery](2026-08-06-manager-owned-subagent-settlement-delivery.md) independently sends the run outcome and closing message. A child may send progress or a final handoff before settlement. Background-first scheduling preserves both: Agent-authored messages remain explicit model choices, while the manager-authored notice covers every terminal path regardless of model compliance.
 
 ## Decision
 
@@ -20,9 +20,9 @@ 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:<toolName>` 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 `next-step` as its default, preserving report-before-settlement order through the parent inbox.
+The child receives its direct parent id and return guidance in the initial task after any inherited fork seed. It may call `send_message` zero or more times, including for findings that change the parent's next action and for a self-contained final handoff. Manager-owned settlement remains unconditional and does not inspect whether an Agent message arrived. The two messages may repeat final content, but they retain distinct authors and purposes: `send_message` carries content the child chose, while settlement records how the run ended and preserves terminal output when the child cannot cooperate. Both use the Agent inbox and fixed Steer scheduling; the accepted child message precedes the later settlement notice.
 
-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.
+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 sends no child-authored message. Package tests separately pin explicit `false` as foreground, the parent scheduling text, and the child's parent-id return guidance.
 
 ## Alternatives considered
 
@@ -32,14 +32,14 @@ The keyless headless `subagent-settlement` scenario omits `run_in_background`, r
 
 **Change only the prompt.** Prompt preference without runtime resolution still turns an omitted argument into foreground. The model must be able to rely on the advertised default rather than reproduce it perfectly on every tool call.
 
-**Suppress settlement after a final report arrives.** Conditional settlement reintroduces per-Activation bookkeeping and loses the unconditional runtime guarantee when a child reports progress and then fails. Settlement remains unconditional even when the resulting message overlaps a final report.
+**Suppress settlement after a final Agent message arrives.** Conditional settlement reintroduces per-Activation bookkeeping and loses the unconditional runtime guarantee when a child sends progress and then fails. Settlement remains unconditional even when the resulting message overlaps a final handoff.
 
-**Use `report` only for progress before settlement.** This removes duplicate final content but also removes the explicit child-authored handoff from the child prompt. The final-report obligation remains, and runtime settlement remains its independent fallback and terminal record.
+**Reserve `send_message` for progress before settlement.** This removes duplicate final content but makes the shared adjacent-Agent operation depend on message purpose. The child may explicitly hand off a final result, while runtime settlement remains its independent fallback and terminal record.
 
 ## Consequences
 
 - An ordinary continuable call is non-blocking without spelling `run_in_background: true`; serialized delegation is an explicit `false` choice.
 - Independent subagent calls in one assistant message overlap under the tool loop's concurrency-safe dispatch, while dependent foreground calls can still be issued one at a time.
 - Parent guidance, tool schema, runtime resolution, and settlement delivery state the same default.
-- A compliant child reports one self-contained final result and may report important findings earlier. Every Activation also produces an unconditional settlement notice, so a completed run may deliver overlapping final content twice.
+- A child may send one self-contained final result and important findings earlier. Every Activation also produces an unconditional settlement notice, so a completed run may deliver overlapping final content twice.
 - One-shot background Jobs and disabled-background tool instances retain their existing behavior.

+ 6 - 6
.agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.zh.md

@@ -8,7 +8,7 @@ Status: implemented
 
 可继续 child 已经具备持久化 id、独立轮次、后续消息以及由管理器负责的结算通知。如果把省略的 `run_in_background` 视为前台,模型就必须在每次调用时重复写出 `true`,才能得到这套生命周期。这样也会掩盖真正有用的调度判断:只有当 parent 的下一步动作需要 child 结果时,parent 才应等待。
 
-child 作用域的 `report` 提示词要求发送自包含的最终报告,而[由管理器负责的结算投递](2026-08-06-manager-owned-subagent-settlement-delivery.zh.md)会独立发送本次运行的结束结果与收尾消息。已完成的 child 因而可能先用最终报告唤醒 parent,再用结算通知唤醒一次。后台优先调度会保留两次投递:由 child 编写的交接仍是强制提示词指引,由管理器生成的通知则不依赖模型是否遵循指令,覆盖每种终止路径。
+child 的初始任务会告诉它如何使用共享的 `send_message` 工具向直接 parent 发送消息,而[由管理器负责的结算投递](2026-08-06-manager-owned-subagent-settlement-delivery.zh.md)会独立发送本次运行的结束结果与收尾消息。child 可以在结算前发送进度或最终交接。后台优先调度会保留两者:由 Agent 编写的消息仍是模型的显式选择,由管理器生成的通知则不依赖模型是否遵循指令,覆盖每种终止路径。
 
 ## 决策
 
@@ -20,9 +20,9 @@ child 作用域的 `report` 提示词要求发送自包含的最终报告,而[
 - `run_in_background` 参数说明具体生命周期的默认值以及何时覆盖;
 - `tool:<toolName>` 系统提示词 section 会告诉模型同时启动相互独立的委派、在它们运行时继续有用工作,并且仅当下一步动作依赖结果时选择前台。只有当该工具在组装作用域中仍可见时才会渲染这个 section,因此子级工具限制会同时移除 schema 与对应指引。
 
-[可继续 child 上报义务](2026-08-06-continuable-child-report-obligation.zh.md)保持不变:child 提示词要求发送一份自包含的最终报告,并在发现会改变 parent 下一步动作的信息时提前报告。由管理器负责的结算仍然无条件执行,不检查报告是否已经到达。这两条消息可能重复最终内容,但作者和用途不同:`report` 是 child 的显式交接,结算则记录本次运行如何结束,并在 child 无法配合时保留终止输出。`reportDelivery` 仍是部署调度策略,默认值为 `next-step`,通过 parent inbox 保持报告先于结算的顺序
+child 会在继承的 fork 种子之后,从初始任务获得直接 parent id 与返回指引。它可以调用零次或多次 `send_message`,包括发送会改变 parent 下一步动作的发现,以及自包含的最终交接。由管理器负责的结算仍然无条件执行,不检查 Agent 消息是否已经到达。这两条消息可能重复最终内容,但作者和用途不同:`send_message` 携带 child 自己选择的内容,结算则记录本次运行如何结束,并在 child 无法配合时保留终止输出。两者都使用 Agent inbox 与固定 Steer 调度;已接受的 child 消息先于后续结算通知
 
-无密钥 headless `subagent-settlement` 场景省略 `run_in_background`,收到立即返回的 child id;尽管 fixture(测试前置数据)有意不调用 `report`,它仍通过管理器生成的结算通知到达 parent 最终答案。包测试另行固定了显式 `false` 的前台语义、parent 调度文本以及 child 的强制报告提示词
+无密钥 headless `subagent-settlement` 场景省略 `run_in_background`,收到立即返回的 child id;尽管 fixture(测试前置数据)有意不发送 child 编写的消息,它仍通过管理器生成的结算通知到达 parent 最终答案。包测试另行固定了显式 `false` 的前台语义、parent 调度文本以及 child 的 parent-id 返回指引
 
 ## 考虑过的替代方案
 
@@ -32,14 +32,14 @@ child 作用域的 `report` 提示词要求发送自包含的最终报告,而[
 
 **只修改提示词。** 如果运行时解析不变,提示词偏好仍会让省略参数的调用进入前台。模型必须能够依赖公布的默认值,而不是在每次工具调用中完美复述它。
 
-**最终报告到达后抑制结算通知。** 条件结算会重新引入每次 Activation 的记账,并且当 child 先报告进度、随后失败时丢掉无条件运行时保证。即使生成的消息与最终报告重叠,结算仍然无条件执行。
+**最终 Agent 消息到达后抑制结算通知。** 条件结算会重新引入每次 Activation 的记账,并且当 child 先发送进度、随后失败时丢掉无条件运行时保证。即使生成的消息与最终交接重叠,结算仍然无条件执行。
 
-**只用 `report` 发送结算前的进度。** 这样可以消除重复的最终内容,但也会从 child 提示词中移除由 child 编写的显式交接。最终报告义务保持不变,运行时结算则继续作为它的独立后备和终止记录。
+**只用 `send_message` 发送结算前的进度。** 这样可以消除重复的最终内容,但会让共享的相邻 Agent 操作依赖消息用途。child 可以显式交接最终结果,运行时结算则继续作为它的独立后备和终止记录。
 
 ## 后果
 
 - 普通可继续调用无需写出 `run_in_background: true` 即为非阻塞;串行委派需要显式选择 `false`。
 - 同一条 assistant 消息中的独立 subagent 调用会在工具循环的并发安全分发下重叠执行;有依赖的前台调用仍可逐个发出。
 - parent 指引、工具 schema、运行时解析和结算投递陈述同一个默认值。
-- 遵循指令的 child 会发送一份自包含的最终结果,也可以更早报告重要发现。每次 Activation 还会产生无条件结算通知,因此已完成的运行可能两次投递相互重叠的最终内容。
+- child 可以发送一份自包含的最终结果,也可以更早发送重要发现。每次 Activation 还会产生无条件结算通知,因此已完成的运行可能两次投递相互重叠的最终内容。
 - 一次性后台 Task 与禁用后台的工具实例保留现有行为。

+ 2 - 2
.agents/notes/implemented/simplification/2026-07-27-intent-named-subagent-continuation-operations.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/simplification/2026-07-27-intent-named-subagent-continuation-operations.md
-2026-07-27-intent-named-subagent-continuation-operations.md: 72fb042978cdb8faa238d7483c17eb7c7fce80bd
-2026-07-27-intent-named-subagent-continuation-operations.zh.md: 8734c439fd4f3baf384d6fa1fc2ae197e2ed5b64
+2026-07-27-intent-named-subagent-continuation-operations.md: cb0827e63ed6bf88f7f661cf071d15aaa2278bde
+2026-07-27-intent-named-subagent-continuation-operations.zh.md: fba07eac66af8e27d1f548a68f2ac060d39d10e7

+ 6 - 6
.agents/notes/implemented/simplification/2026-07-27-intent-named-subagent-continuation-operations.md

@@ -4,17 +4,17 @@ Status: implemented
 
 English | [中文](2026-07-27-intent-named-subagent-continuation-operations.zh.md)
 
-The current activation-based realization is owned by [Continuable subagents](../feature/2026-07-28-continuable-subagent-conversations.md). It retains the `followup` operation this record names, returns the accepted `MessageId`, uses the bare `Agent` parameter as exact live-direct-parent authority, and limits provider participation in continuable children to `prepareContinuable`.
+The provider-request and session-flush decisions remain current. [Adjacent Agents share one Steer messaging operation](../architecture/2026-08-27-adjacent-agent-steer-messaging.md) supersedes this record's `followup` naming and options: the public operation is now `sendMessage(sender, targetId, content, { signal })` for either adjacent direction.
 
 ## Problem
 
-Merging continuable-child orchestration into `ctx.subagents` left provider dispatch and caller intent on the same public service. `resume(name, request)` accepted a descriptor, authorized parent, durable child id, and activation signal that only the internal continuation manager could resolve correctly. `sendMessage(...)` exposed transport wording rather than the `followup` intent already used by `Agent`, and its separate source and signal parameters widened an operation every caller had to use atomically.
+Merging continuable-child orchestration into `ctx.subagents` left provider dispatch and caller intent on the same public service. `resume(name, request)` accepted a descriptor, authorized parent, durable child id, and activation signal that only the internal continuation manager could resolve correctly. Direction-specific `followup` and `reportFrom` operations would also encode routing and scheduling differences for one adjacent-Agent capability.
 
 The durability boundary also exposed both `SessionStore.flush()` and `flushRequired()`. They performed the same scoped parallel dispatch and differed only in whether an empty listener snapshot was accepted, so the session interface encoded one consumer's policy as a second operation.
 
 ## Decision
 
-`SubagentRuntime` separates four execution intents: `start(name, request)` returns an ordinary holder-owned one-shot run; `startContinuable(spec)` establishes a durable child and returns its id plus the accepted initial `MessageId`; `followup(parent, childId, content, { source, signal })` sends later parent content; and `reportFrom(child, content, { delivery, signal })` sends selected child content to its direct parent. `followup` matches `Agent.followup()`, while `SubagentRun.steer()` remains the narrower confirmed live-run capability. The model-facing tools keep their stable `send_message` and `report` names and delegate routing to the corresponding intent methods.
+`SubagentRuntime` separates three caller intents: `start(name, request)` returns an ordinary holder-owned one-shot run; `startContinuable(spec)` establishes a durable child and returns its id plus the accepted initial `MessageId`; and `sendMessage(sender, targetId, content, { signal })` sends model-authored content across one direct parent-child edge. The message operation derives attribution from the exact live sender and owns adjacency checks, cold resume, and fixed Steer scheduling. The single model-facing `send_message({ agent_id, message })` tool delegates to that operation in either direction.
 
 Caller and provider requests are distinct. `SubagentStartRequest` contains caller-supplied one-shot data; `ResolvedSubagentStartRequest` adds the service-resolved descriptor before `SubagentProvider.start()`. For continuable creation, the manager passes a `ContinuableCreateRequest` to optional `SubagentProvider.prepareContinuable()` and receives detached creation data only. `SubagentRuntime.resume()` and provider resume dispatch are absent: the continuation manager loads the descriptor, authorizes the parent, and owns Agent materialization, prompt delivery, cold resume, and teardown.
 
@@ -24,7 +24,7 @@ Caller and provider requests are distinct. `SubagentStartRequest` contains calle
 
 **Keep public provider resume dispatch.** No production caller outside the continuation manager owns descriptor lookup, direct-parent authorization, Agent materialization, Activation ownership, and child-first teardown. A public method would expose resolved implementation data without a valid independent intent; providers instead contribute detached first-creation data through `prepareContinuable` and never participate in cold resume.
 
-**Keep `sendMessage` on the service.** The model tool sends a message, but the service operation represents a follow-up that may steer or cold-resume. `followup` aligns with the structural `Agent` interface and does not promise a particular route.
+**Keep direction-specific `followup` and `reportFrom` operations.** They would preserve shorter recipient-free child calls, but duplicate authority, attribution, and delivery behavior while making the service vocabulary depend on direction. One `sendMessage` operation names the shared intent and keeps the target explicit.
 
 **Keep `flushRequired()`.** A second method hides only an empty-listener check. Returning participation from the existing barrier keeps dispatch in one implementation and lets each caller state whether absence is acceptable.
 
@@ -33,6 +33,6 @@ Caller and provider requests are distinct. `SubagentStartRequest` contains calle
 ## Consequences
 
 - The Cordis service catalog contains only caller operations; a provider can opt into continuable first creation through `SubagentProvider.prepareContinuable?()` without receiving Agent lifecycle authority or a public resume operation.
-- Follow-up source and cancellation travel in one options object, matching the intent-helper shape on `Agent` while retaining the existing live-delivery and cold-resume semantics.
+- Sender authority is an exact live `Agent`; cancellation travels in one options object and owns work only until inbox acceptance.
 - Session durability has one barrier operation. Its participation result remains observable, but no continuable-child path treats arbitrary listener participation as proof that a persistence backend stored the state.
-- The `send_message` and `report` schemas, accepted message identities, `AgentHandle` ownership, durable event vocabulary, and model-visible transcript follow the activation-based realization linked above.
+- The single `send_message` schema, accepted message identities, `AgentHandle` ownership, durable event vocabulary, and model-visible transcript follow the activation-based realization linked above.

+ 6 - 6
.agents/notes/implemented/simplification/2026-07-27-intent-named-subagent-continuation-operations.zh.md

@@ -4,17 +4,17 @@ Status: implemented
 
 [English](2026-07-27-intent-named-subagent-continuation-operations.md) | 中文
 
-当前基于 Activation 的实现由[可继续的 subagent](../feature/2026-07-28-continuable-subagent-conversations.zh.md)负责。它保留本记录命名的 `followup` 操作,返回已接受的 `MessageId`,使用裸 `Agent` 参数作为确切的在线直属父级权限,并将提供方对可继续 child 的参与限制为 `prepareContinuable`。
+提供方请求与会话 flush 决策仍然有效。[相邻 Agent 共享一个 Steer 消息操作](../architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md)取代了本记录的 `followup` 命名与 options:公开操作现在是可用于任一相邻方向的 `sendMessage(sender, targetId, content, { signal })`。
 
 ## 问题
 
-将可继续 child 的编排合并到 `ctx.subagents` 后,提供方分发与调用方意图共存于同一个公开服务中。`resume(name, request)` 接受描述符、已鉴权的 parent、持久化 child id 与激活信号,而只有内部继续执行管理器才能正确解析这些数据。`sendMessage(...)` 暴露的是传输层措辞,而不是 `Agent` 已采用的 `followup` 意图;它还将来源与信号拆成独立参数,扩大了操作接口,而每个调用方都必须以原子方式同时使用二者
+将可继续 child 的编排合并到 `ctx.subagents` 后,提供方分发与调用方意图共存于同一个公开服务中。`resume(name, request)` 接受描述符、已鉴权的 parent、持久化 child id 与激活信号,而只有内部继续执行管理器才能正确解析这些数据。方向专属的 `followup` 与 `reportFrom` 操作还会为一项相邻 Agent 能力编码不同的路由与调度
 
 持久性边界还同时公开了 `SessionStore.flush()` 与 `flushRequired()`。二者执行相同的作用域内并行分发,唯一差别是是否接受空的监听器快照,因此会话接口将一个消费方的策略编码为第二项操作。
 
 ## 决策
 
-`SubagentRuntime` 分离四种执行意图:`start(name, request)` 返回普通的、由持有方负责的 one-shot run;`startContinuable(spec)` 建立持久化 child,并返回其 id 与已接受的初始 `MessageId`;`followup(parent, childId, content, { source, signal })` 发送后续 parent 内容;`reportFrom(child, content, { delivery, signal })` 将选定的 child 内容发送给其直接 parent。`followup` 与 `Agent.followup()` 一致,而 `SubagentRun.steer()` 仍是范围更窄的能力,仅向已确认仍在运行的 run 提供 steering(中途引导)。面向模型的工具保留稳定的 `send_message` 与 `report` 名称,并将路由委托给对应的意图方法
+`SubagentRuntime` 分离三种调用方意图:`start(name, request)` 返回普通的、由持有方负责的 one-shot run;`startContinuable(spec)` 建立持久化 child,并返回其 id 与已接受的初始 `MessageId`;`sendMessage(sender, targetId, content, { signal })` 则跨一条直接 parent-child 边发送由模型编写的内容。消息操作从确切在线 sender 推导来源信息,并负责相邻关系检查、冷恢复与固定 Steer 调度。唯一面向模型的 `send_message({ agent_id, message })` 工具会在两个方向委托给该操作
 
 调用方请求与提供方请求相互分离。`SubagentStartRequest` 包含调用方提供的 one-shot 数据;`ResolvedSubagentStartRequest` 会在调用 `SubagentProvider.start()` 前加入由服务解析的描述符。创建可继续 child 时,管理器将 `ContinuableCreateRequest` 传给可选的 `SubagentProvider.prepareContinuable()`,且只接收分离的创建数据。`SubagentRuntime.resume()` 与提供方恢复分发均不存在:继续执行管理器加载描述符、对 parent 进行鉴权,并负责 Agent 实体化、提示词投递、冷恢复与 teardown。
 
@@ -24,7 +24,7 @@ Status: implemented
 
 **保留公开的提供方恢复分发。** 继续执行管理器之外,没有任何生产调用方同时负责安全调用所需的描述符查找、直接 parent 鉴权、Agent 实体化、Activation 所有权与 child-first teardown。公开方法会暴露已解析的实现数据,却没有合理的独立调用意图;提供方改为通过 `prepareContinuable` 贡献分离的首次创建数据,且从不参与冷恢复。
 
-**在服务上保留 `sendMessage`。** 面向模型的工具发送消息,但服务操作表达的是后续操作,既可能对运行中的激活执行 steering,也可能从持久化存储恢复。`followup` 与结构化 `Agent` 接口保持一致,也不承诺特定路由
+**保留方向专属的 `followup` 与 `reportFrom` 操作。** 这样可以保留 child 无需填写接收方的短调用,但会重复权限、来源信息与投递行为,并让服务词汇取决于方向。一个 `sendMessage` 操作可以命名共享意图,并让目标保持显式
 
 **保留 `flushRequired()`。** 第二个方法只封装了空监听器检查。由现有屏障返回是否有监听器参与,可以让分发只保留一套实现,并让每个调用方自行判定缺少监听器是否可接受。
 
@@ -33,6 +33,6 @@ Status: implemented
 ## 影响
 
 - Cordis 服务目录只包含调用方操作;提供方可以通过 `SubagentProvider.prepareContinuable?()` 选择参与可继续 child 的首次创建,但不会获得 Agent 生命周期权限或公开恢复操作。
-- 后续操作的来源与取消信号通过同一个选项对象传递,与 `Agent` 上按意图命名的辅助方法形态一致,同时保留在线投递与从持久化存储恢复的语义
+- sender 权限来自确切在线 `Agent`;取消信号通过一个选项对象传递,并且只负责 inbox 接受前的工作
 - 会话持久性只有一个屏障操作。参与结果仍可观测,但任何可继续 child 路径都不会将任意监听器参与视为持久化后端已存储状态的证明。
-- `send_message` 与 `report` schema、已接受的消息标识、`AgentHandle` 所有权、持久化事件词汇与模型可见的 transcript(文本记录)遵循上文链接的基于 Activation 的实现。
+- 单一 `send_message` schema、已接受的消息标识、`AgentHandle` 所有权、持久化事件词汇与模型可见的 transcript(文本记录)遵循上文链接的基于 Activation 的实现。

+ 0 - 3
apps/cli/composition.md

@@ -136,8 +136,6 @@ flowchart LR
   cfg --> plugin_dsh_base_tool_subagent
   plugin_dsh_base_tool_subagent_fork["tool-subagent-fork<br/>@deepseek-ai/dsh-tool-subagent"]
   cfg --> plugin_dsh_base_tool_subagent_fork
-  plugin_dsh_base_tool_subagent_report["tool-subagent-report<br/>@deepseek-ai/dsh-tool-subagent-report"]
-  cfg --> plugin_dsh_base_tool_subagent_report
   plugin_dsh_base_workflow_worker_thread["workflow-worker-thread<br/>@deepseek-ai/dsh-workflow-worker-thread"]
   cfg --> plugin_dsh_base_workflow_worker_thread
   plugin_dsh_base_tool_workflow["tool-workflow<br/>@deepseek-ai/dsh-tool-workflow"]
@@ -248,7 +246,6 @@ flowchart LR
 | `tool-subagent-list-agents` | `@deepseek-ai/dsh-tool-subagent-control/list-agents` |
 | `tool-subagent` | `@deepseek-ai/dsh-tool-subagent` |
 | `tool-subagent-fork` | `@deepseek-ai/dsh-tool-subagent` |
-| `tool-subagent-report` | `@deepseek-ai/dsh-tool-subagent-report` |
 | `workflow-worker-thread` | `@deepseek-ai/dsh-workflow-worker-thread` |
 | `tool-workflow` | `@deepseek-ai/dsh-tool-workflow` |
 | `timeout-policy` | `@deepseek-ai/dsh-tool-call-timeout-policy` |

+ 0 - 1
apps/cli/package.json

@@ -137,7 +137,6 @@
     "@deepseek-ai/dsh-subagent-spawn-in-process": "workspace:^",
     "@deepseek-ai/dsh-subprocess-local": "workspace:^",
     "@deepseek-ai/dsh-system-prompt": "workspace:^",
-    "@deepseek-ai/dsh-tool-subagent-report": "workspace:^",
     "@deepseek-ai/dsh-tools": "workspace:^",
     "@deepseek-ai/dsh-user-approval": "workspace:^",
     "@types/js-yaml": "^4.0.9",

+ 2 - 2
apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/child.expected.jsonl

@@ -3,11 +3,11 @@
 {"type":"session/end-seed","data":{}}
 {"type":"sandbox/mode","data":{"mode":"workspace-write","source":"delegation"}}
 {"type":"approval/policy","data":{"policy":"never","source":"delegation"}}
-{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly CHILD_RESULT and nothing else. Do not call report."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}}
+{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message."},{"type":"text","text":"Your parent agent id is \"{{sessionId}}\". Before you finish, send your result to that agent with send_message({ agent_id: \"{{sessionId}}\", message: \"<self-contained result>\" }). The parent shares your workspace but does not automatically receive your transcript, tool output, or reasoning. Send earlier messages as well when a finding changes what the parent should do next; sending a message does not end your turn."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}}
 {"type":"turn/start","data":{"turn":1}}
 {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
 {"type":"step/start","data":{"turn":1,"step":1}}
-{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly CHILD_RESULT and nothing else. Do not call report."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
+{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message."},{"type":"text","text":"Your parent agent id is \"{{sessionId}}\". Before you finish, send your result to that agent with send_message({ agent_id: \"{{sessionId}}\", message: \"<self-contained result>\" }). The parent shares your workspace but does not automatically receive your transcript, tool output, or reasoning. Send earlier messages as well when a finding changes what the parent should do next; sending a message does not end your turn."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
 {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
 {"type":"session/title","data":{"title":"Reply with exactly CHILD_RESULT and","messageSeqs":[8],"source":{"kind":"fallback"}}}
 {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}

+ 2 - 2
apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/parent.override.json

@@ -3,8 +3,8 @@
     "kind": "chunks",
     "chunks": [
       { "type": "block-start", "index": 0, "blockType": "tool-call" },
-      { "type": "tool-call-delta", "index": 0, "id": "start-child", "name": "subagent", "argumentsDelta": "{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call report.\"}" },
-      { "type": "block-end", "index": 0, "block": { "type": "tool-call", "id": "start-child", "name": "subagent", "arguments": "{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call report.\"}" } },
+      { "type": "tool-call-delta", "index": 0, "id": "start-child", "name": "subagent", "argumentsDelta": "{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}" },
+      { "type": "block-end", "index": 0, "block": { "type": "tool-call", "id": "start-child", "name": "subagent", "arguments": "{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}" } },
       { "type": "usage", "usage": { "inputTokens": 10, "outputTokens": 5 } },
       { "type": "finish", "reason": { "kind": "tool-calls" } }
     ]

+ 4 - 4
apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl

@@ -9,12 +9,12 @@
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title-llm-request","seq":12,"time":0,"data":{"titleProvider":"session-title-first-prompt-llm","messageSeqs":[7],"route":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"Create a concise title for an AI coding-assistant session from the supplied human messages.\nReturn only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.\nUse the language of the messages.\nAim for about 5 words in non-CJK languages or 10 CJK characters.","messages":[{"content":[{"type":"text","text":"Generate the session title from this JSON array of human messages:\n[{\"seq\":7,\"text\":\"Start one continuable background subagent and answer from its completion notice. Do not call list_agents, send_message, job_output, or job_list.\"}]"}],"source":{"kind":"plugin","plugin":"dsh-session-title-llm"},"role":"user","id":"{{sessionId}}"}],"maxTokens":64}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":13,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":14,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"start-child","name":"subagent","argumentsDelta":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call report.\"}"}}}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":15,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call report.\"}"}}}}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":14,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"start-child","name":"subagent","argumentsDelta":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}}}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":15,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}}}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":16,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":17,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":18,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call report.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[13,14,15,16,17],"surfaceOp":"append"}}
-{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":19,"time":0,"data":{"turn":1,"step":1,"callId":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call report.\"}"}}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":18,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[13,14,15,16,17],"surfaceOp":"append"}}
+{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":19,"time":0,"data":{"turn":1,"step":1,"callId":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":20,"time":0,"data":{"title":"Subagent settlement","messageSeqs":[7],"source":{"kind":"provider","provider":"session-title-first-prompt-llm","model":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":21,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"start-child"},"content":[{"type":"tool-result","toolCallId":"start-child","content":[{"type":"text","text":"started subagent {{sessionId}}"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[19],"surfaceOp":"append"}}
 {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":22,"time":0,"data":{"turn":1,"step":1}}}

+ 0 - 3
apps/web/tests/agent-team-panel.overlay.yml

@@ -6,9 +6,6 @@
 - id: tool-subagent-list-agents
   disabled: true
 
-- id: tool-subagent-report
-  disabled: true
-
 - id: tool-subagent
   config:
     provider: spawn

+ 2 - 0
apps/web/tests/goal-command-presentation.e2e.ts

@@ -95,6 +95,8 @@ describe('web e2e: /goal human transcript presentation', () => {
     expect(events.some(event => event.type === 'step/start')).toBe(false)
     expect(events.some(event => event.type === 'request/header')).toBe(false)
 
+    // The command result can arrive before Lexical clears the submitted claim.
+    await expect.poll(() => input.textContent(), { timeout: 10_000 }).toBe('')
     const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
     await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
   }, 60_000)

+ 13 - 0
apps/web/tests/scaffold.ts

@@ -1174,12 +1174,14 @@ export async function captureStableAria(
  * @param page - the page under test.
  * @param selector - the region locator selector.
  * @param workspaceCwd - normalization input.
+ * @param options - optional user-visible state to establish before capture.
  * @returns the stable normalized expanded snapshot.
  */
 export async function captureExpandedTurnProcessAria(
   page: Page,
   selector: string,
   workspaceCwd: string,
+  options: { scrollToBottom?: boolean } = {},
 ): Promise<string> {
   const controls = page.locator('[data-turn-process]')
   const count = await controls.count()
@@ -1192,6 +1194,17 @@ export async function captureExpandedTurnProcessAria(
     opened.push(index)
   }
   try {
+    if (options.scrollToBottom === true) {
+      const backToBottom = page.getByRole('button', { name: 'Back to bottom', exact: true })
+      const scroll = page.locator('[data-conversation-scroll]')
+      await expect.poll(async () => {
+        const distanceFromBottom = await scroll.evaluate((host) => {
+          host.scrollTop = host.scrollHeight
+          return host.scrollHeight - host.clientHeight - host.scrollTop
+        })
+        return Math.abs(distanceFromBottom) <= 1 && await backToBottom.count() === 0
+      }, { timeout: 10_000 }).toBe(true)
+    }
     return await captureStableAria(page, selector, workspaceCwd)
   } finally {
     for (const index of opened.reverse()) {

+ 2 - 1
apps/web/tests/subagent-conversation.e2e.ts

@@ -359,7 +359,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
     await page.getByRole('button', { name: '3 subagents' }).hover()
     await page.getByRole('treeitem', { name: new RegExp(LABEL) }).click()
     await expect.poll(
-      () => page.getByText(INITIAL_PROMPT, { exact: true }).count(),
+      () => page.getByText(/^Explain event sourcing in one sentence\.Your parent agent id is /).count(),
       { timeout: 15_000 },
     ).toBe(1)
     if (scaffold.ctx.agents.get(childId) !== undefined) {
@@ -442,6 +442,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
       page,
       '[class*="centerCol"]',
       scaffold.workspaceCwd,
+      { scrollToBottom: true },
     )
     await compareOrRefreshGolden(AVAILABLE_CHILD_EXPANDED_EXPECTED, expanded, MODE)
     expect(tripwire.pageErrors).toEqual([])

+ 20 - 14
apps/web/tests/subagent-interrupt-ui.e2e.ts

@@ -20,6 +20,7 @@ import { chromium } from 'playwright'
 import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
 import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
 import type { Agent } from '@deepseek-ai/dsh-agent'
+import type { SubagentPromptRequestId } from '@deepseek-ai/dsh-subagent'
 import {
   acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
   launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
@@ -203,7 +204,8 @@ describe.skipIf(MODE === 'record')('web e2e: composer interrupt for a running co
         name: 'Parent session offline; sending is unavailable but you can still stop the run',
       })
       await input.waitFor({ timeout: 15_000 })
-      await page.getByText(INITIAL, { exact: true }).waitFor({ timeout: 15_000 })
+      await page.getByText(/^Explain event sourcing in one sentence\.Your parent agent id is /)
+        .waitFor({ timeout: 15_000 })
       expect(await input.isDisabled()).toBe(true)
       const stop = page.getByRole('button', { name: 'Stop generating' })
       expect(await stop.count()).toBe(1)
@@ -220,12 +222,13 @@ describe.skipIf(MODE === 'record')('web e2e: composer interrupt for a running co
       // Keep the continuable Activation resident after this first abort. The
       // direct setup queue does not change the parent-offline UI contract: its
       // input and Send remain disabled throughout the exercised browser path.
-      await scaffold.ctx.subagents.followup(
-        parent,
-        childId,
-        [{ type: 'text', text: REARM }],
-        { source: { kind: 'user' }, signal: new AbortController().signal },
-      )
+      await scaffold.ctx.subagents.prompt({
+        requestId: 'interrupt-ui-rearm' as SubagentPromptRequestId,
+        parentSessionId: parent.id,
+        childSessionId: childId,
+        mode: 'continuable',
+        content: [{ type: 'text', text: REARM }],
+      }, new AbortController().signal)
       const aborted = waitForAbortedTurn(scaffold, childId)
       const interruptResponse = page.waitForResponse(response =>
         new URL(response.url()).pathname === '/api/subagents/interruptByParent')
@@ -239,12 +242,13 @@ describe.skipIf(MODE === 'record')('web e2e: composer interrupt for a running co
 
       // Wake the parked setup message only after cancellation converges. A
       // second hang keeps the parent-available case independent from this stop.
-      await scaffold.ctx.subagents.followup(
-        parent,
-        childId,
-        [{ type: 'text', text: REARM_WAKE }],
-        { source: { kind: 'user' }, signal: new AbortController().signal },
-      )
+      await scaffold.ctx.subagents.prompt({
+        requestId: 'interrupt-ui-rearm-wake' as SubagentPromptRequestId,
+        parentSessionId: parent.id,
+        childSessionId: childId,
+        mode: 'continuable',
+        content: [{ type: 'text', text: REARM_WAKE }],
+      }, new AbortController().signal)
       await waitFor(() => existsSync(rearmedReadyFile), 'the re-armed child turn to open')
       expect(scaffold.ctx.agents.get(childId)?.status).toBe('running')
     } finally {
@@ -306,7 +310,9 @@ describe.skipIf(MODE === 'record')('web e2e: composer interrupt for a running co
       && event.data.source.kind === 'user'
       ? event.data.content.flatMap(block => block.type === 'text' ? [block.text] : [])
       : [])
-    expect(userTexts).toEqual([INITIAL, REARM, REARM_WAKE, FOLLOWUP, WAKING])
+    expect(userTexts[0]).toBe(INITIAL)
+    expect(userTexts[1]).toMatch(/^Your parent agent id is .+send_message\(\{ agent_id: /)
+    expect(userTexts.slice(2)).toEqual([REARM, REARM_WAKE, FOLLOWUP, WAKING])
     const turnEndKinds = loaded.events
       .filter(event => event.type === 'turn/end')
       .map(event => event.data.reason.kind)

+ 3 - 1
apps/web/tests/subagent-interrupt.e2e.ts

@@ -183,7 +183,9 @@ describe.skipIf(MODE === 'record')('web e2e: subagents/interruptByParent over th
       && event.data.source.kind === 'user'
       ? event.data.content.flatMap(block => block.type === 'text' ? [block.text] : [])
       : [])
-    expect(userTexts).toEqual([INITIAL, FOLLOWUP, WAKING])
+    expect(userTexts[0]).toBe(INITIAL)
+    expect(userTexts[1]).toMatch(/^Your parent agent id is .+send_message\(\{ agent_id: /)
+    expect(userTexts.slice(2)).toEqual([FOLLOWUP, WAKING])
     const turnEndKinds = loaded.events
       .filter(event => event.type === 'turn/end')
       .map(event => (event).data.reason.kind)

+ 2 - 2
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: b4e79c1b3895c199c03cb79b54ee3cc25a50c517
-config-catalog.zh.md: 7cf425cd09fb5a2d40ea6af39fa12e352f235929
+config-catalog.md: f9eb7ae0bc634e655462412e7dd5d341b7a449a2
+config-catalog.zh.md: 827f39d068de067aefa4dfc77fc53c3db74b5336

+ 1 - 23
docs/config-catalog.md

@@ -2959,29 +2959,7 @@ export interface Config {
 
 Depends on: [`AgentOptions`](subsystems/core.md)
 
-Source: [`packages/subagent/tool-subagent/src/index.ts:48`](../packages/subagent/tool-subagent/src/index.ts)
-
-<a id="deepseek-aidsh-tool-subagent-report"></a>
-
-## `@deepseek-ai/dsh-tool-subagent-report`
-
-Requires: `subagents` · `tools` · `systemPrompt`
-
-```ts config-catalog
-/** Config: how accepted reports are scheduled on the parent. */
-export interface Config {
-  /**
-   * 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
-}
-```
-
-Depends on: [`SubagentReportDelivery`](subsystems/subagent.md)
-
-Source: [`packages/subagent/tool-subagent-report/src/index.ts:25`](../packages/subagent/tool-subagent-report/src/index.ts)
+Source: [`packages/subagent/tool-subagent/src/index.ts:46`](../packages/subagent/tool-subagent/src/index.ts)
 
 <a id="deepseek-aidsh-tool-terminal"></a>
 

+ 0 - 22
docs/config-catalog.zh.md

@@ -2963,28 +2963,6 @@ export interface Config {
 
 来源:[`packages/subagent/tool-subagent/src/index.ts:48`](../packages/subagent/tool-subagent/src/index.ts)
 
-<a id="deepseek-aidsh-tool-subagent-report"></a>
-
-## `@deepseek-ai/dsh-tool-subagent-report`
-
-需要:`subagents` · `tools` · `systemPrompt`
-
-```ts config-catalog
-/** Config: how accepted reports are scheduled on the parent. */
-export interface Config {
-  /**
-   * 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
-}
-```
-
-依赖:[`SubagentReportDelivery`](subsystems/subagent.zh.md)
-
-来源:[`packages/subagent/tool-subagent-report/src/index.ts:25`](../packages/subagent/tool-subagent-report/src/index.ts)
-
 <a id="deepseek-aidsh-tool-terminal"></a>
 
 ## `@deepseek-ai/dsh-tool-terminal`

+ 2 - 2
docs/event-producer-consumer.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/event-producer-consumer.md
-event-producer-consumer.md: 0f330c2dea35ab5cf1c822dbfa9c024a86e879cc
-event-producer-consumer.zh.md: a25269c9d559c399c1472580482f54eda2c4ae1c
+event-producer-consumer.md: f9606dcf399519f0c62b6403f300b66e80e47d36
+event-producer-consumer.zh.md: f9e4d35e6ed213046957b5e7fc1e03a334de9992

+ 4 - 4
docs/event-producer-consumer.md

@@ -52,10 +52,10 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 | `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:105`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `remotes` |
 | `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:92`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
 | `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:298`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
-| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:179`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), `server`, [`subagent`](../packages/subagent/subagent) |
-| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:153`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:159`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:170`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`subagent`](../packages/subagent/subagent) |
+| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:173`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), `server`, [`subagent`](../packages/subagent/subagent) |
+| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:147`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:153`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:164`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`subagent`](../packages/subagent/subagent) |
 | `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`system-prompt`](../packages/core/system-prompt) |
 | `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
 | `tools/change` | `emit` | [`packages/core/tools/src/index.ts:199`](../packages/core/tools/src/index.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`), [`tools`](../packages/core/tools) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) |

+ 4 - 4
docs/event-producer-consumer.zh.md

@@ -54,10 +54,10 @@
 | `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:105`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `remotes` |
 | `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:92`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
 | `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:298`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
-| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:179`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), `server`, [`subagent`](../packages/subagent/subagent) |
-| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:153`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:159`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:170`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`subagent`](../packages/subagent/subagent) |
+| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:173`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), `server`, [`subagent`](../packages/subagent/subagent) |
+| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:147`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:153`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:164`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`subagent`](../packages/subagent/subagent) |
 | `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`system-prompt`](../packages/core/system-prompt) |
 | `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
 | `tools/change` | `emit` | [`packages/core/tools/src/index.ts:199`](../packages/core/tools/src/index.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`), [`tools`](../packages/core/tools) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) |

+ 2 - 2
docs/module-graph.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/module-graph.md
-module-graph.md: 9ec4b6de7d333a4c5ba0c408384f696d262007f3
-module-graph.zh.md: 3f831d06149c4a536391080d5053c78c593de638
+module-graph.md: a028d1efd12924f0592b1dba954071cebd9d3dbe
+module-graph.zh.md: c4efd116e5c79aa9c0210d5ff3692d7a39cd1c46

+ 0 - 6
docs/module-graph.md

@@ -72,7 +72,6 @@ flowchart TD
     pkg_subagent_spawn_in_process["subagent-spawn-in-process"]
     pkg_tool_subagent["tool-subagent"]
     pkg_tool_subagent_control["tool-subagent-control"]
-    pkg_tool_subagent_report["tool-subagent-report"]
   end
   subgraph group_web["packages/web"]
     pkg_tool_web["tool-web"]
@@ -1029,10 +1028,6 @@ flowchart TD
   pkg_tool_subagent_control --> pkg_session
   pkg_tool_subagent_control --> pkg_subagent
   pkg_tool_subagent_control --> pkg_tools
-  pkg_tool_subagent_report --> pkg_llm
-  pkg_tool_subagent_report --> pkg_subagent
-  pkg_tool_subagent_report --> pkg_system_prompt
-  pkg_tool_subagent_report --> pkg_tools
   pkg_hooks_claude_code --> pkg_agent
   pkg_hooks_claude_code --> pkg_hook_protocol
   pkg_hooks_claude_code --> pkg_llm
@@ -1380,7 +1375,6 @@ flowchart TD
 | [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
-| [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`hooks-claude-code`](../packages/hooks/hooks-claude-code) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
 | [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`file-reference`](../packages/context/file-reference), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-time`](../packages/util/time), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) |
 | [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) |

+ 0 - 6
docs/module-graph.zh.md

@@ -74,7 +74,6 @@ flowchart TD
     pkg_subagent_spawn_in_process["subagent-spawn-in-process"]
     pkg_tool_subagent["tool-subagent"]
     pkg_tool_subagent_control["tool-subagent-control"]
-    pkg_tool_subagent_report["tool-subagent-report"]
   end
   subgraph group_web["packages/web"]
     pkg_tool_web["tool-web"]
@@ -1031,10 +1030,6 @@ flowchart TD
   pkg_tool_subagent_control --> pkg_session
   pkg_tool_subagent_control --> pkg_subagent
   pkg_tool_subagent_control --> pkg_tools
-  pkg_tool_subagent_report --> pkg_llm
-  pkg_tool_subagent_report --> pkg_subagent
-  pkg_tool_subagent_report --> pkg_system_prompt
-  pkg_tool_subagent_report --> pkg_tools
   pkg_hooks_claude_code --> pkg_agent
   pkg_hooks_claude_code --> pkg_hook_protocol
   pkg_hooks_claude_code --> pkg_llm
@@ -1382,7 +1377,6 @@ flowchart TD
 | [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
-| [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`hooks-claude-code`](../packages/hooks/hooks-claude-code) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
 | [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`file-reference`](../packages/context/file-reference), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-time`](../packages/util/time), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) |
 | [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) |

+ 2 - 2
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: 095d73f43e75add0fab68aee97a6e6c0d27dbed2
-subagent.zh.md: e3df4c982cec86cf1701bb807715a20e18978677
+subagent.md: 109f8569879c0578f804569d1105c03250ad3bf5
+subagent.zh.md: 649a3796f9f199306e45ea9653bb38df8683a518

+ 30 - 86
docs/subsystems/subagent.md

@@ -4,7 +4,7 @@ English | [中文](subagent.zh.md)
 
 The subagent seam lets an agent delegate work to a child agent. Like [bash](shell.md), it is **one optional capability**, not part of the agent loop, so its types live here rather than in [core.md](core.md). It differs from the other capability seams because **multiple provider implementations coexist** in one context, registered by name (`ctx.subagents`), while bash allows only one executor. Its registry follows the [LLM adapter registry](llm-streaming.md), not the single-service bash executor.
 
-Service Definition: [dsh-subagent](../../packages/subagent/subagent) (`ctx.subagents` + the vocabulary below). Service Providers are sibling packages (`dsh-subagent-spawn-in-process`, `dsh-subagent-fork-in-process`, `dsh-subagent-acp`, `dsh-subagent-codex`, `dsh-subagent-claude-code`, `dsh-subagent-dsh-sdk`); the model-facing Consumers are [dsh-tool-subagent](../../packages/subagent/tool-subagent) (per-provider delegation), [dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control) (the optional global `send_message`, `interrupt_agent`, and `list_agents` controls), and [dsh-tool-subagent-report](../../packages/subagent/tool-subagent-report) (the optional child-scoped `report` return channel). The same `ctx.subagents` service owns continuable-child orchestration through an internal activation manager and read-only child and descendant discovery straight from the session store and optional session persistence. Product-provider rationale lives in [the Codex and Claude Code Agent Note](../../.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md); common-seam rationale lives in [the subagent Agent Note](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md), [the continuable subagents Agent Note](../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md), [the report-tool Agent Note](../../.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.md), [the durable catalog Agent Note](../../.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md), [the list-identity-projection Agent Note](../../.agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.md), and [the merged-service Agent Note](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md).
+Service Definition: [dsh-subagent](../../packages/subagent/subagent) (`ctx.subagents` + the vocabulary below). Service Providers are sibling packages (`dsh-subagent-spawn-in-process`, `dsh-subagent-fork-in-process`, `dsh-subagent-acp`, `dsh-subagent-codex`, `dsh-subagent-claude-code`, `dsh-subagent-dsh-sdk`); the model-facing Consumers are [dsh-tool-subagent](../../packages/subagent/tool-subagent) (per-provider delegation) and [dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control) (the optional global `send_message`, `interrupt_agent`, and `list_agents` controls). The same `ctx.subagents` service owns continuable-child orchestration through an internal activation manager and read-only child and descendant discovery straight from the session store and optional session persistence. Product-provider rationale lives in [the Codex and Claude Code Agent Note](../../.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md); common-seam rationale lives in [the subagent Agent Note](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md), [the continuable subagents Agent Note](../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md), [the adjacent-Agent messaging Agent Note](../../.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.md), [the durable catalog Agent Note](../../.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md), [the list-identity-projection Agent Note](../../.agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.md), and [the merged-service Agent Note](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md).
 
 Sources: [`packages/subagent/subagent/src/types.ts`](../../packages/subagent/subagent/src/types.ts), [`packages/subagent/subagent/src/index.ts`](../../packages/subagent/subagent/src/index.ts), and [`packages/subagent/subagent/src/continuation.ts`](../../packages/subagent/subagent/src/continuation.ts)
 
@@ -133,21 +133,21 @@ persisted Session
 
 `SubagentRuntime.startContinuable()` reserves the stable child id, snapshots the versioned `subagent/descriptor` payload, asks the named provider for its detached `ContinuableCreateSpec`, creates the child Agent through a private activation-owner scope, establishes any continuable-parent ownership, and submits the initial prompt. It resolves with `{ childId, messageId }` when inbox acceptance yields the message id — without waiting for the turn to start or for the message to enter the Session log. Every failure before that acceptance rejects with neither id, disposing any created handle and rolling back the Activation and parent ownership.
 
-`SubagentRuntime.followup()` is the sole continuation-message operation, and routing depends only on Activation residency:
+`SubagentRuntime.sendMessage()` is the sole model-authored message operation. It accepts the exact live sender plus a target id, permits only a direct parent or direct continuable child, derives sender attribution itself, and routes a direct-child target by Activation residency:
 
-| Activation state | `followup` |
+| Target Activation state | `sendMessage` |
 |---|---|
-| `running` | enqueue in the same Activation |
-| `waiting` | wake the same Activation |
-| no Activation | cold-resume a new Activation |
+| `running` | steer the nearest step in the same Activation |
+| `waiting` | wake and steer the same Activation |
+| no Activation | cold-resume a new Activation, then steer it |
 
 `running` means the Agent has an active admission or turn, or waking inbox work; `waiting` means it is quiescent but still owns at least one child Activation that has not completed disposal; `settled` means quiescent with every owned child disposed, at which point the manager disposes the [`AgentHandle`](core.md#creation-and-ownership) and removes the Activation. The manager derives these internal conditions from Agent quiescence and the owned-child set rather than maintaining a second execution state machine.
 
-The Agent inbox is the only queue. Every continuation message becomes one `Agent.followup()` FIFO turn, so accepted messages have one observable order and a follow-up cannot redirect a turn already underway. Successful delivery returns the accepted `MessageId`; the existing `agent/inbox/inserted`, `agent/inbox/claimed`, and `agent/inbox/discarded` events remain the message-lifecycle observations, and the continuation layer defines no subagent-specific delivery route.
+The Agent inbox is the only queue. Every Agent message uses `Agent.steer()`: an idle target starts a turn, while a running target claims it at the nearest step boundary. Successful delivery returns the accepted `MessageId`; the existing `agent/inbox/inserted`, `agent/inbox/claimed`, and `agent/inbox/discarded` events remain the message-lifecycle observations, and the continuation layer defines no subagent-specific delivery route.
 
-Follow-up authority comes from an exact live Agent tool context. The authenticated Agent must be the durable child's direct parent recorded in `SessionHeader.parentSession`. `MessageSource` and `senderSessionId` record who supplied an admitted message but grant no authority; the optional model-facing tool uses `CoordinatorMessageSource`.
+Authority comes from the exact live sender. Parent-to-child delivery requires the target's `SessionHeader.parentSession` to name the sender; child-to-parent delivery requires the sender's resident Activation to name the target. Siblings, ancestors beyond one edge, self-targets, stale Agent objects, and one-shot children are rejected. Each accepted message is framed as `Agent <sender-id> sent a message:` and records `AgentMessageSource`; provenance records the sender but grants no authority.
 
-For both operations the caller signal owns lookup, materialization, and admission only until inbox acceptance. Afterwards the manager owns the Activation independently: later caller cancellation neither cancels the accepted turn nor disposes the child, and the seam exposes no steering operation.
+For `startContinuable()` and `sendMessage()`, the caller signal owns lookup, materialization, and admission only until inbox acceptance. Afterwards the manager owns the Activation independently: later caller cancellation neither cancels the accepted turn nor disposes the child. Human browser prompts remain a separate private Queue adapter and therefore still produce distinct FIFO turns.
 
 `SubagentRuntime.interrupt(targetSessionId, authority)` is the one public stop: it authorizes synchronously, issues `Agent.cancel(cause, { keepInbox: true })` on the live target, and returns without awaiting quiescence. The Activation, its unclaimed pending inbox work, and published descendants are untouched; work already claimed into the interrupted turn is not requeued. Once the interrupted driver is idle, a waking send resumes the parked FIFO queue. An absent target — unknown, one-shot, or already settled — and a manager-less composition are accepted no-ops. For a live target, a mismatched parent address or caller outside its live ancestry rejects with `UNAUTHORIZED`; stale ancestor objects and self-targeting ancestor requests reject before target lookup.
 
@@ -167,21 +167,19 @@ Every Activation owns its `AgentHandle` and an `ownedChildren: Set<SessionId>`;
 Final settlement awaits `ctx.sessions.flush(session)` but ignores its participation boolean because an arbitrary listener cannot prove that a persistence backend stored the state. Rejection is logged without failing the Activation, and the manager still disposes the handle and releases ownership; the persisted child state may then be missing or stale on a later resume. Manager unload invokes an internal manager-wide drain that closes admission and disposes every live forest; `drainContinuableDescendants(parents)` closes admission only below exact live host-owned Agents and disposes their continuable descendants while unrelated forests remain live. Both await already-admitted materializations in their scope, propagate cancellation top-down, release handles child-first, and await every selected branch despite individual failures. Durable child Sessions survive that process-local teardown.
 
 ```ts type-equiv
-/** Attribution for a model coordinator's follow-up to one of its children. */
-interface CoordinatorMessageSource {
-  readonly kind: 'coordinator'
+/** Durable attribution for one model-authored message between adjacent Agents. */
+interface AgentMessageSource {
+  readonly kind: 'agent-message'
   /** A message another agent addressed to this one (`relay` context form). */
   readonly form: 'relay'
-  /** Session id of the agent whose tool call produced the follow-up. */
+  /** Session id of the Agent whose tool call produced the message. */
   readonly senderSessionId: SessionId
 }
 ```
 
 ```ts type-equiv
-/** Options for following up with one continuable child. */
-interface SubagentFollowupOptions {
-  /** Durable attribution retained on the delivered message; it grants no authority. */
-  readonly source: MessageSource
+/** Options for one model-authored message between adjacent Agents. */
+interface SubagentSendMessageOptions {
   /** Caller cancellation, owning the operation only until inbox acceptance. */
   readonly signal: AbortSignal
 }
@@ -197,33 +195,13 @@ 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 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. */
-interface SubagentReportMessageSource {
-  readonly kind: 'subagent-report'
-  /** A message another agent addressed to this one (`relay` context form). */
-  readonly form: 'relay'
-  /** Session id of the reporting child. */
-  readonly senderSessionId: SessionId
-}
-```
-
-```ts type-equiv
-/** Deployment scheduling policy for accepted child reports. */
-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.
+When a resident Activation settles, the manager 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 an Agent message. 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.
 
 ```ts type-equiv
 /**
  * Durable attribution for the runtime's own account of a continuable child
  * settling. Deliberately a different kind from
- * {@link SubagentReportMessageSource}: a report is content the child chose,
+ * {@link AgentMessageSource}: an Agent message is content the sender chose,
  * while this message is the manager stating what became of the child, and a
  * transcript that merged them would credit the child with words it never wrote.
  */
@@ -238,17 +216,7 @@ interface SubagentSettledMessageSource {
 }
 ```
 
-```ts type-equiv
-/** Options for one continuable child's report to its direct parent. */
-interface SubagentReportOptions {
-  /** Already-resolved parent scheduling policy. */
-  readonly delivery: SubagentReportDelivery
-  /** Caller cancellation, owning authorization and admission until acceptance. */
-  readonly signal: AbortSignal
-}
-```
-
-The provider participates only in preparing the initial creation spec, where `spawn` and `fork` differ. Its returned spec carries only detached provider-specific creation inputs — today the optional parent-history seed — and no Agent, `AgentHandle`, prompt delivery, result, disposal, or resume operation. Cold resume does not dispatch through a provider at all: the manager folds the generic descriptor, calls `ctx.agents.resume()` through the same activation-owner scope, and submits the waiting turn.
+The provider participates only in preparing the initial creation spec, where `spawn` and `fork` differ. Its returned spec carries only detached provider-specific creation inputs — the optional parent-history seed — and no Agent, `AgentHandle`, prompt delivery, result, disposal, or resume operation. Cold resume does not dispatch through a provider at all: the manager folds the generic descriptor, calls `ctx.agents.resume()` through the same activation-owner scope, and submits the waiting turn.
 
 ```ts type-equiv
 /**
@@ -532,21 +500,20 @@ Named provider registry with one-shot runs, durable discovery, and continuable-c
 async startContinuable(spec: ContinuableStartSpec): Promise<ContinuableStart>
 
 /**
- * Deliver one later message to a continuable child as its next FIFO turn. A
- * resident child's Agent inbox accepts it directly (waking a `waiting`
- * Activation), while an absent one is cold-resumed from its persisted
- * Session. The Agent inbox is the only queue, so every accepted message has
- * one observable order.
- * @param parent - the exact live direct parent authorizing this delivery.
- * @param childId - durable child session id.
- * @param content - user-role content to deliver.
- * @param options - the message source fields and caller cancellation, which stops the
- *   operation only before inbox acceptance.
+ * Steer one model-authored message to the sender's direct parent or direct
+ * continuable child. A running target admits it at the nearest step boundary;
+ * an idle target starts a turn, and an absent direct child cold-resumes from
+ * persistence. The service derives durable sender attribution from the exact
+ * live sender. Caller cancellation stops only pre-acceptance work.
+ * @param sender - exact live Agent authorizing and originating the message.
+ * @param targetId - durable direct-parent or direct-child session id.
+ * @param content - model-authored content to deliver.
+ * @param options - caller cancellation before inbox acceptance.
  * @returns the accepted message's inbox id.
- * @throws when continuation services are unavailable, parent authority is
- *   rejected, or the message was not admitted.
+ * @throws when continuation services are unavailable, adjacency is rejected,
+ *   or the message was not admitted.
  */
-async followup( parent: Agent, childId: SessionId, content: ContentBlock[], options: SubagentFollowupOptions, ): Promise<MessageId>
+async sendMessage( sender: Agent, targetId: SessionId, content: ContentBlock[], options: SubagentSendMessageOptions, ): Promise<MessageId>
 
 /**
  * Interrupt one live continuable child's current turn under a human parent
@@ -565,29 +532,6 @@ async followup( parent: Agent, childId: SessionId, content: ContentBlock[], opti
  */
 interrupt(targetSessionId: SessionId, authority: SubagentInterruptAuthority): void
 
-/**
- * Deliver selected content from one live continuable child to its durable
- * direct parent. The child is the authority credential; callers cannot name a
- * recipient. Reporting does not conclude the child's turn or Activation.
- * @param child - exact live reporting child.
- * @param content - selected model-facing content.
- * @param options - parent scheduling and pre-acceptance cancellation.
- * @returns the stable identity of the parent-accepted message.
- * @throws when continuation services are unavailable, sender authorization
- *   fails, or the direct parent is not live.
- */
-async reportFrom( child: Agent, content: ContentBlock[], options: SubagentReportOptions, ): Promise<MessageId>
-
-/**
- * Compose one deployment capability into every continuable child's
- * unpublished creation context on fresh creation and cold resume. Grants wait
- * for the next Activation; removing the contribution revokes every resident
- * installation immediately.
- * @param contribution - synchronous child-scope installer.
- * @returns the exact Cordis effect disposer.
- */
-registerContinuableSetup(contribution: ContinuableSetupContribution): () => void
-
 /**
  * Close continuable admission below exact live parent Agents, stop only their
  * visible descendant Activations synchronously, then await admitted scoped

+ 29 - 85
docs/subsystems/subagent.zh.md

@@ -4,7 +4,7 @@
 
 subagent seam 让一个 agent(智能体)将工作委派给子 agent。与 [bash](shell.zh.md) 一样,它是**一项可选能力**,不属于 agent loop(智能体循环),因此其类型定义在此而非 [core.md](core.zh.md) 中。它不同于其他能力 seam,因为**同一上下文中可共存多个提供方实现**,并按名称注册(`ctx.subagents`),而 bash 只允许一个执行器。该注册表遵循 [LLM(大语言模型)适配器注册表](llm-streaming.zh.md),而非单服务的 bash 执行器。
 
-Service Definition:[dsh-subagent](../../packages/subagent/subagent)(`ctx.subagents` + 下文词汇)。Service Provider 是六个兄弟包:`dsh-subagent-spawn-in-process`、`dsh-subagent-fork-in-process`、`dsh-subagent-acp`、`dsh-subagent-codex`、`dsh-subagent-claude-code`、`dsh-subagent-dsh-sdk`;面向模型的 Consumer 包括 [dsh-tool-subagent](../../packages/subagent/tool-subagent)(按提供方委派)[dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control)(可选的全局 `send_message`、`interrupt_agent` 与 `list_agents` 控制工具)和 [dsh-tool-subagent-report](../../packages/subagent/tool-subagent-report)(可选的 child 作用域 `report` 返回通道)。同一个 `ctx.subagents` 服务通过内部激活管理器负责可继续子 agent 编排,并直接基于会话存储和可选的会话持久化提供只读的 child 与后代发现。产品提供方设计理由见 [Codex 与 Claude Code Agent Note](../../.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md);通用 seam 的设计理由见 [subagent Agent Note](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.zh.md)、[可继续 subagent Agent Note](../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.zh.md)、[report 工具 Agent Note](../../.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.zh.md)、[持久化目录 Agent Note](../../.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.zh.md)、[列表身份投影 Agent Note](../../.agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.zh.md)和[服务合并 Agent Note](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.zh.md)。
+Service Definition:[dsh-subagent](../../packages/subagent/subagent)(`ctx.subagents` + 下文词汇)。Service Provider 是六个兄弟包:`dsh-subagent-spawn-in-process`、`dsh-subagent-fork-in-process`、`dsh-subagent-acp`、`dsh-subagent-codex`、`dsh-subagent-claude-code`、`dsh-subagent-dsh-sdk`;面向模型的 Consumer 包括 [dsh-tool-subagent](../../packages/subagent/tool-subagent)(按提供方委派)[dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control)(可选的全局 `send_message`、`interrupt_agent` 与 `list_agents` 控制工具)。同一个 `ctx.subagents` 服务通过内部激活管理器负责可继续子 agent 编排,并直接基于会话存储和可选的会话持久化提供只读的 child 与后代发现。产品提供方设计理由见 [Codex 与 Claude Code Agent Note](../../.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md);通用 seam 的设计理由见 [subagent Agent Note](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.zh.md)、[可继续 subagent Agent Note](../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.zh.md)、[相邻 Agent 消息 Agent Note](../../.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md)、[持久化目录 Agent Note](../../.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.zh.md)、[列表身份投影 Agent Note](../../.agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.zh.md)和[服务合并 Agent Note](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.zh.md)。
 
 源码:[`packages/subagent/subagent/src/types.ts`](../../packages/subagent/subagent/src/types.ts)、[`packages/subagent/subagent/src/index.ts`](../../packages/subagent/subagent/src/index.ts)和 [`packages/subagent/subagent/src/continuation.ts`](../../packages/subagent/subagent/src/continuation.ts)
 
@@ -133,21 +133,21 @@ persisted Session
 
 `SubagentRuntime.startContinuable()` 会预留稳定的子 agent id,对版本化的 `subagent/descriptor` payload 建立快照,向指定提供方索取其分离的 `ContinuableCreateSpec`,通过私有的 activation-owner 作用域创建子 Agent,建立任何可继续父级的所有权,并提交初始提示词。当收件箱(inbox)准入产出消息 id 时,它以 `{ childId, messageId }` resolve——无需等待轮次开始,也无需等待消息进入会话日志。在该准入之前的任何失败都会以两个 id 都不返回的方式 reject,并 dispose(资源释放)任何已创建的 handle,回滚 Activation 与父级所有权。
 
-`SubagentRuntime.followup()` 是唯一的继续执行消息操作,其路由仅取决于 Activation 的驻留状态
+`SubagentRuntime.sendMessage()` 是唯一由模型编写消息的操作。它接收确切在线 sender 与目标 id,只允许直接 parent 或直接可继续 child,自行推导 sender 来源信息,并根据目标 child 的 Activation 驻留状态路由
 
-| Activation 状态 | `followup` |
+| 目标 Activation 状态 | `sendMessage` |
 |---|---|
-| `running` | 在同一 Activation 中入队 |
-| `waiting` | 唤醒同一 Activation |
-| 无 Activation | 冷恢复一个新的 Activation |
+| `running` | 在同一 Activation 中 steer 最近的 step |
+| `waiting` | 唤醒并 steer 同一 Activation |
+| 无 Activation | 冷恢复新的 Activation,然后 steer |
 
 `running` 表示 Agent 拥有活跃的准入或轮次,或正在唤醒收件箱工作;`waiting` 表示它已完全停稳,但仍拥有至少一个尚未完成 dispose 的子 Activation;`settled` 表示已完全停稳且其拥有的每个子级都已 dispose,此时管理器会 dispose [`AgentHandle`](core.zh.md#creation-and-ownership) 并移除该 Activation。管理器根据 Agent 的完全停稳状态与其拥有的子级集合推导这些内部条件,而非维护第二套执行状态机。
 
-Agent 收件箱是唯一的队列。每条继续执行消息都会成为一个 `Agent.followup()` FIFO 轮次,因此已接受的消息共享同一个可观测顺序,且后续消息无法改变已在进行中的轮次。投递成功会返回被接受的 `MessageId`;既有的 `agent/inbox/inserted`、`agent/inbox/claimed` 与 `agent/inbox/discarded` 事件仍是消息生命周期的观测点,继续执行层不定义任何 subagent 专属的投递路由。
+Agent 收件箱是唯一队列。每条 Agent 消息都使用 `Agent.steer()`:空闲目标会启动一个轮次,运行中目标则在最近的 step 边界领取消息。投递成功会返回被接受的 `MessageId`;既有的 `agent/inbox/inserted`、`agent/inbox/claimed` 与 `agent/inbox/discarded` 事件仍是消息生命周期的观测点,继续执行层不定义任何 subagent 专属的投递路由。
 
-后续操作的权限来自确切的在线 Agent 工具上下文。已认证的 Agent 必须是持久化子 agent 在 `SessionHeader.parentSession` 中记录的直接父级。`MessageSource` 与 `senderSessionId` 记录谁提供了已准入的消息,但不授予任何权限;可选的面向模型工具使用 `CoordinatorMessageSource`
+权限来自确切在线 sender。parent 到 child 的投递要求目标的 `SessionHeader.parentSession` 指向 sender;child 到 parent 的投递要求 sender 的驻留 Activation 指向目标。sibling、相隔多于一条边的 ancestor、self-target、陈旧 Agent 对象与一次性 child 都会被拒绝。每条已接受消息都以 `Agent <sender-id> sent a message:` 作为前缀,并记录 `AgentMessageSource`;来源信息记录 sender,但不授予权限
 
-对于这两种操作,调用方 signal 仅在收件箱接受之前掌管查找、物化与准入。此后管理器独立掌管该 Activation:之后的调用方取消既不会取消已接受的轮次,也不会 dispose 子 agent,并且该 seam 不对外暴露任何 steering(中途引导)操作
+对于 `startContinuable()` 与 `sendMessage()`,调用方 signal 仅在收件箱接受之前掌管查找、物化与准入。此后管理器独立掌管该 Activation:之后的调用方取消既不会取消已接受的轮次,也不会 dispose 子 agent。浏览器中的人类提示仍由私有 Queue 适配器处理,因此继续产生独立 FIFO 轮次
 
 `SubagentRuntime.interrupt(targetSessionId, authority)` 是唯一的公开停止操作:它同步完成鉴权,对在线目标发出 `Agent.cancel(cause, { keepInbox: true })`,然后不等待完全停稳即返回。Activation、其尚未领取的待处理 inbox 工作与已发布的后代均不受影响;已被领取进入中断轮次的工作不会重新入队。被中断的 driver 进入 idle 后,一次唤醒发送会恢复被暂停的 FIFO 队列。不存在的目标——未知、一次性或已结算——以及未绑定管理器的组合是被接受的 no-op。对在线目标,错误的 parent 地址或不在其在线祖先链中的调用方会以 `UNAUTHORIZED` 拒绝;陈旧的 ancestor 对象和指向自身的 ancestor 请求会在查找目标前拒绝。
 
@@ -167,21 +167,19 @@ type SubagentInterruptAuthority =
 最终结算会等待 `ctx.sessions.flush(session)`,但会忽略其参与布尔值,因为任意 listener 都无法证明某个持久化后端已存储该状态。rejection 会被记录,但不会使 Activation 失败;管理器仍会 dispose 该 handle 并释放所有权,此后持久化的子 agent 状态在后续恢复时可能缺失或陈旧。管理器卸载会调用内部的管理器全局 drain,关闭准入并 dispose 每片在线森林;`drainContinuableDescendants(parents)` 只关闭由 host 确切拥有的在线 Agent 之下的准入,并 dispose 其可继续后代,而无关森林保持在线。两者都会等待各自作用域内已获准的物化过程,自顶向下传播取消,按 child-first 顺序释放 handle,并且即使个别分支失败也会等待所有选中分支。持久化子会话不受该进程内拆卸的影响。
 
 ```ts type-equiv
-/** Attribution for a model coordinator's follow-up to one of its children. */
-interface CoordinatorMessageSource {
-  readonly kind: 'coordinator'
+/** Durable attribution for one model-authored message between adjacent Agents. */
+interface AgentMessageSource {
+  readonly kind: 'agent-message'
   /** A message another agent addressed to this one (`relay` context form). */
   readonly form: 'relay'
-  /** Session id of the agent whose tool call produced the follow-up. */
+  /** Session id of the Agent whose tool call produced the message. */
   readonly senderSessionId: SessionId
 }
 ```
 
 ```ts type-equiv
-/** Options for following up with one continuable child. */
-interface SubagentFollowupOptions {
-  /** Durable attribution retained on the delivered message; it grants no authority. */
-  readonly source: MessageSource
+/** Options for one model-authored message between adjacent Agents. */
+interface SubagentSendMessageOptions {
   /** Caller cancellation, owning the operation only until inbox acceptance. */
   readonly signal: AbortSignal
 }
@@ -197,33 +195,13 @@ interface ContinuableStart {
 }
 ```
 
-可选的可继续 child 设置贡献可以在 child 基础组合完成后、Activation 发布前安装限定在作用域内的能力。该注册表按顺序执行且具有事务性:设置失败或被撤销时会回滚未发布的 Activation;child 作用域 dispose 时会释放所有安装;新注册项在下一个 Activation 生效;移除注册项时则会立即撤销每个驻留中的安装。
-
-`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. */
-interface SubagentReportMessageSource {
-  readonly kind: 'subagent-report'
-  /** A message another agent addressed to this one (`relay` context form). */
-  readonly form: 'relay'
-  /** Session id of the reporting child. */
-  readonly senderSessionId: SessionId
-}
-```
-
-```ts type-equiv
-/** Deployment scheduling policy for accepted child reports. */
-type SubagentReportDelivery = 'quiet' | 'next-step'
-```
-
-上报是 child 自己的选择,因此管理器还保有一份属于自己的记账:当驻留 Activation 结算时,它会向该 child 持久化的直接 parent 投递一条通知,说明该 epoch 如何结束,并携带其最终 assistant 内容。对每个调用方拿到过 id 的 child,这条投递都是无条件的;它发生在会让 parent 被判定为已结算的所有权释放之前,并通过与上报相同的唤醒准入记账到达驻留 parent。若 parent 自身所在的谱系已在拆卸中,这条通知会以不唤醒的方式送达,因为唤醒一个静息 Agent 是开启一个轮次,而不是排队等待工作。其来源信息使用一个独立的 kind,因此 transcript(文本记录)绝不会把运行时的记账呈现为 child 自己写下的内容。
+当驻留 Activation 结算时,管理器会向该 child 持久化的直接 parent 投递一条通知,说明该 epoch 如何结束,并携带其最终 assistant 内容。对每个调用方拿到过 id 的 child,这条投递都是无条件的;它发生在会让 parent 被判定为已结算的所有权释放之前,并通过与 Agent 消息相同的唤醒准入记账到达驻留 parent。若 parent 自身所在的谱系已在拆卸中,这条通知会以不唤醒的方式送达,因为唤醒一个静息 Agent 是开启一个轮次,而不是排队等待工作。其来源信息使用一个独立的 kind,因此 transcript(文本记录)绝不会把运行时的记账呈现为 child 自己写下的内容。
 
 ```ts type-equiv
 /**
  * Durable attribution for the runtime's own account of a continuable child
  * settling. Deliberately a different kind from
- * {@link SubagentReportMessageSource}: a report is content the child chose,
+ * {@link AgentMessageSource}: an Agent message is content the sender chose,
  * while this message is the manager stating what became of the child, and a
  * transcript that merged them would credit the child with words it never wrote.
  */
@@ -238,16 +216,6 @@ interface SubagentSettledMessageSource {
 }
 ```
 
-```ts type-equiv
-/** Options for one continuable child's report to its direct parent. */
-interface SubagentReportOptions {
-  /** Already-resolved parent scheduling policy. */
-  readonly delivery: SubagentReportDelivery
-  /** Caller cancellation, owning authorization and admission until acceptance. */
-  readonly signal: AbortSignal
-}
-```
-
 提供方只参与准备初始创建 spec,`spawn` 与 `fork` 在此有所不同。其返回的 spec 只携带分离的、提供方专属的创建输入——即可选的父级历史种子——不含 Agent、`AgentHandle`、提示词投递、结果、dispose 或恢复操作。冷恢复根本不经由提供方分发:管理器折叠通用描述符,通过同一个 activation-owner 作用域调用 `ctx.agents.resume()`,并提交等待中的轮次。
 
 ```ts type-equiv
@@ -536,21 +504,20 @@ Named provider registry with one-shot runs, durable discovery, and continuable-c
 async startContinuable(spec: ContinuableStartSpec): Promise<ContinuableStart>
 
 /**
- * Deliver one later message to a continuable child as its next FIFO turn. A
- * resident child's Agent inbox accepts it directly (waking a `waiting`
- * Activation), while an absent one is cold-resumed from its persisted
- * Session. The Agent inbox is the only queue, so every accepted message has
- * one observable order.
- * @param parent - the exact live direct parent authorizing this delivery.
- * @param childId - durable child session id.
- * @param content - user-role content to deliver.
- * @param options - the message source fields and caller cancellation, which stops the
- *   operation only before inbox acceptance.
+ * Steer one model-authored message to the sender's direct parent or direct
+ * continuable child. A running target admits it at the nearest step boundary;
+ * an idle target starts a turn, and an absent direct child cold-resumes from
+ * persistence. The service derives durable sender attribution from the exact
+ * live sender. Caller cancellation stops only pre-acceptance work.
+ * @param sender - exact live Agent authorizing and originating the message.
+ * @param targetId - durable direct-parent or direct-child session id.
+ * @param content - model-authored content to deliver.
+ * @param options - caller cancellation before inbox acceptance.
  * @returns the accepted message's inbox id.
- * @throws when continuation services are unavailable, parent authority is
- *   rejected, or the message was not admitted.
+ * @throws when continuation services are unavailable, adjacency is rejected,
+ *   or the message was not admitted.
  */
-async followup( parent: Agent, childId: SessionId, content: ContentBlock[], options: SubagentFollowupOptions, ): Promise<MessageId>
+async sendMessage( sender: Agent, targetId: SessionId, content: ContentBlock[], options: SubagentSendMessageOptions, ): Promise<MessageId>
 
 /**
  * Interrupt one live continuable child's current turn under a human parent
@@ -569,29 +536,6 @@ async followup( parent: Agent, childId: SessionId, content: ContentBlock[], opti
  */
 interrupt(targetSessionId: SessionId, authority: SubagentInterruptAuthority): void
 
-/**
- * Deliver selected content from one live continuable child to its durable
- * direct parent. The child is the authority credential; callers cannot name a
- * recipient. Reporting does not conclude the child's turn or Activation.
- * @param child - exact live reporting child.
- * @param content - selected model-facing content.
- * @param options - parent scheduling and pre-acceptance cancellation.
- * @returns the stable identity of the parent-accepted message.
- * @throws when continuation services are unavailable, sender authorization
- *   fails, or the direct parent is not live.
- */
-async reportFrom( child: Agent, content: ContentBlock[], options: SubagentReportOptions, ): Promise<MessageId>
-
-/**
- * Compose one deployment capability into every continuable child's
- * unpublished creation context on fresh creation and cold resume. Grants wait
- * for the next Activation; removing the contribution revokes every resident
- * installation immediately.
- * @param contribution - synchronous child-scope installer.
- * @returns the exact Cordis effect disposer.
- */
-registerContinuableSetup(contribution: ContinuableSetupContribution): () => void
-
 /**
  * Close continuable admission below exact live parent Agents, stop only their
  * visible descendant Activations synchronously, then await admitted scoped

+ 2 - 2
docs/tool-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/tool-catalog.md
-tool-catalog.md: f69f6e3650dccc06a5cbe243c26f0411befdc9e2
-tool-catalog.zh.md: 5ac0948c3beb50b9c361ca575029a9a2763716fa
+tool-catalog.md: f71c5dea97ca92fd816c45c6fbd816ee05309d63
+tool-catalog.zh.md: 3286da7015cfa793820f6107a96af1fc73472ddb

+ 6 - 34
docs/tool-catalog.md

@@ -35,7 +35,6 @@ This table connects model-visible tool names to the plugin package and service s
 | `@deepseek-ai/dsh-tool-session-query` | `session_event_read`, `session_event_search`, `session_event_trace`, `session_search`, `session_trace` | `ctx.tools`, `ctx.systemPrompt`, `ctx.sessionQuery`, `a calling Agent for workspace authority` | `tool/call`, `tool/result` | - | The five read-only tools hide provider cursors and authorize every result from the immutable calling agent session. The package is opt-in; compositions that need enforced deadlines or bounded inline output also mount the generic timeout or spill policies. |
 | `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`, `subagent` | `ctx.tools`, `ctx.subagents`, `ctx.systemPrompt`, `ctx.llm for model discovery and selected-route validation` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion available in an enabled Session. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` remains fixed-route. Each instance independently controls whether it reads model-selection settings and its background behavior through `modelSelectionSettings`, `backgroundMode`, and `enableRunInBackground`. |
 | `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`, `list_agents`, `send_message` | `ctx.tools`, `ctx.subagents`, `ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`, `tool/result`, `child session events through ctx.subagents` | - | The globally named control tools over continuable background subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` and `interrupt_agent` once, plus `list_agents` from its separately loaded `/list-agents` plugin (whose catalog rows use the sessionProjections and live Agent registries). |
-| `@deepseek-ai/dsh-tool-subagent-report` | `report` | `ctx.subagents`, `ctx.systemPrompt`, `a live continuable in-process child Agent` | `tool/call`, `tool/result`, `a user-role message in the direct parent session` | - | Registered per continuable in-process child rather than globally, so this schema is visible only inside such a child and survives its global `toolFilter`. The same contribution installs the child-scoped `tool:report` prompt section, which this catalog does not render. The parent-facing `send_message` tool is installed independently. |
 | `@deepseek-ai/dsh-tool-jobs` | `job_kill`, `job_list`, `job_output` | `ctx.tools`, `ctx.jobs`, `ctx.systemPrompt` | `tool/call`, `tool/result`, `user/message via agent.inject() for background completion notices` | - | The kind-agnostic background-job controller: background bash commands, PTY sends, and subagents are read, listed, and killed through the same three tools. Loading the plugin attaches the controller that arms producers' `ctx.jobs.start()`. |
 | `@deepseek-ai/dsh-experimental-tool-agent-team` | `followup_task`, `interrupt_agent`, `list_agents`, `send_message`, `spawn_teammate`, `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update`, `wait_agent` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `an exact live Team member Agent` | `tool/call`, `team/member`, `team/message/queued`, `team/message/delivered`, `team/task`, `tool/result` | - | All ten tools are scoped to implicit Team Leads and durable teammates. The shipped dsh-base bundle keeps the package disabled; the documented Agent Teams profile patch enables it while disabling the legacy continuable-child control names. |
 | `@deepseek-ai/dsh-tool-todo` | `todo_write` | `ctx.tools`, `owning Agent session` | `tool/call`, `todo/write`, `tool/result` | - | todo_write is session-owned state; UIs render the latest todo/write event as a checklist. `allowParallelInProgress` is required with no default, so the catalog states its choice: `true`, whose description invites several `in_progress` items. A deployment choosing `false` receives the same tool with a description asking for exactly one active task. |
@@ -1618,7 +1617,7 @@ Source: [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/sub
 
 ### `list_agents`
 
-List your continuable background subagents by durable id and label. Use it to recall which ones you started, not to poll for completion — you are told when one finishes. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and ready means it exists only in storage — resumable, not terminal, and not a result waiting to be collected; a `send_message` starts a new turn on the same conversation, and a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.
+List your continuable background subagents by durable id and label. Use it to recall which ones you started, not to poll for completion — you are told when one finishes. Status comes from the live registry: running means the agent is working right now, idle means it is loaded but between turns (it may be waiting on agents it started), and ready means it exists only in storage — resumable, not terminal, and not a result waiting to be collected; a `send_message` steers a running child at its nearest step boundary or starts a turn for an idle or ready child, and a direct child remains a `send_message` candidate in every status. The snapshot is not a delivery promise — `send_message` performs the authoritative check and may still fail. Children that could not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are candidates for `interrupt_agent` only.
 
 ```json
 {
@@ -1640,23 +1639,23 @@ Source: [`packages/subagent/tool-subagent-control/src/list-agents.ts`](../packag
 
 ### `send_message`
 
-Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered.
+Send a message to a direct continuable child by its agent id. If you are a resident continuable child, you may also target your direct parent. If the target is still working, the message steers its nearest step; if it is idle, the message starts a turn. This call returns no answer from the agent — only confirmation that the message was delivered. A failure means the message was NOT delivered.
 
 ```json
 {
   "type": "object",
   "properties": {
-    "subagent_id": {
+    "agent_id": {
       "type": "string",
-      "description": "The subagent id returned when the background subagent was started."
+      "description": "The agent id of your direct continuable child, or your direct parent when you are a resident continuable child."
     },
     "message": {
       "type": "string",
-      "description": "The message to deliver to the subagent."
+      "description": "The message to deliver to the agent."
     }
   },
   "required": [
-    "subagent_id",
+    "agent_id",
     "message"
   ]
 }
@@ -1666,33 +1665,6 @@ Source: [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/sub
 
 The globally named control tools over continuable background subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` and `interrupt_agent` once, plus `list_agents` from its separately loaded `/list-agents` plugin (whose catalog rows use the sessionProjections and live Agent registries).
 
-<a id="deepseek-aidsh-tool-subagent-report"></a>
-
-## `@deepseek-ai/dsh-tool-subagent-report`
-
-### `report`
-
-Report selected content to the agent that started you. Call this once before you finish, with a self-contained final result, and earlier for progress or findings that change what that agent does next. That agent shares your workspace but does not automatically receive your transcript, tool output, or reasoning, so finishing your work is not itself a result. Reporting does not end your turn or finish your work, and only your direct parent receives it. A failed call may still have arrived, so do not blindly repeat it.
-
-```json
-{
-  "type": "object",
-  "properties": {
-    "output": {
-      "type": "string",
-      "description": "Actionable content for your parent; summarize conclusions and reference relevant shared paths."
-    }
-  },
-  "required": [
-    "output"
-  ]
-}
-```
-
-Source: [`packages/subagent/tool-subagent-report/src/index.ts`](../packages/subagent/tool-subagent-report/src/index.ts)
-
-Registered per continuable in-process child rather than globally, so this schema is visible only inside such a child and survives its global `toolFilter`. The same contribution installs the child-scoped `tool:report` prompt section, which this catalog does not render. The parent-facing `send_message` tool is installed independently.
-
 <a id="deepseek-aidsh-tool-jobs"></a>
 
 ## `@deepseek-ai/dsh-tool-jobs`

+ 6 - 34
docs/tool-catalog.zh.md

@@ -39,7 +39,6 @@
 | `@deepseek-ai/dsh-tool-session-query` | `session_event_read`、`session_event_search`、`session_event_trace`、`session_search`、`session_trace` | `ctx.tools`、`ctx.systemPrompt`、`ctx.sessionQuery`、`a calling Agent for workspace authority` | `tool/call`、`tool/result` | - | 这 5 个只读工具会隐藏提供方游标,并根据不可变的调用 agent 会话为每个结果授权。该包需要选择启用;需要强制截止时间或限制行内输出的组合还会挂载通用超时或 spill 策略。 |
 | `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述默认 schema 关闭模型选择,而发现 schema 则展示为已启用 Session 中可用的固定配套工具。Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;`subagent_fork` 始终使用固定路由。每个实例通过 `modelSelectionSettings`、`backgroundMode` 与 `enableRunInBackground` 独立控制是否读取模型选择设置及其后台行为。 |
 | `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`、`list_agents`、`send_message` | `ctx.tools`、`ctx.subagents`、`ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`、`tool/result`、`child session events through ctx.subagents` | - | 这些是控制可继续后台 subagent 的全局命名工具:绑定提供方的 `tool-subagent` 实例注册不同的委派工具;本包注册一次 `send_message` 和 `interrupt_agent`,另由 `list_agents` 通过单独加载的 `/list-agents` 插件提供,其目录行使用 sessionProjections 和实时 Agent 注册表。 |
-| `@deepseek-ai/dsh-tool-subagent-report` | `report` | `ctx.subagents`、`ctx.systemPrompt`、`a live continuable in-process child Agent` | `tool/call`、`tool/result`、`a user-role message in the direct parent session` | - | 按可继续的进程内子级注册,而非全局注册,因此该 schema 仅在这种子级内部可见,并且不受其全局 `toolFilter` 影响。同一份贡献还会安装子级作用域的 `tool:report` 系统提示词 section,本目录不渲染该 section。面向父级的 `send_message` 工具单独安装。 |
 | `@deepseek-ai/dsh-tool-jobs` | `job_kill`、`job_list`、`job_output` | `ctx.tools`、`ctx.jobs`、`ctx.systemPrompt` | `tool/call`、`tool/result`、`user/message via agent.inject() for background completion notices` | - | 与任务种类无关的后台任务控制器:后台 bash 命令、PTY 发送和 subagent 都通过相同的 3 个工具读取、列出和终止。加载该插件会挂接控制器,从而启用生产方的 `ctx.jobs.start()`。 |
 | `@deepseek-ai/dsh-experimental-tool-agent-team` | `followup_task`、`interrupt_agent`、`list_agents`、`send_message`、`spawn_teammate`、`team_task_create`、`team_task_get`、`team_task_list`、`team_task_update`、`wait_agent` | `ctx.tools`、`ctx.systemPrompt`、`ctx.agentTeams`、`an exact live Team member Agent` | `tool/call`、`team/member`、`team/message/queued`、`team/message/delivered`、`team/task`、`tool/result` | - | 这 10 个工具限定于隐式 Team Lead 与持久 teammate 作用域。随产品发布的 dsh-base bundle 默认禁用该包;文档中的 Agent Teams profile patch 会启用它,并禁用旧 continuable child 的同名控制工具。 |
 | `@deepseek-ai/dsh-tool-todo` | `todo_write` | `ctx.tools`、`owning Agent session` | `tool/call`、`todo/write`、`tool/result` | - | todo_write 是会话所有的状态;UI 将最新的 todo/write 事件渲染为检查清单。`allowParallelInProgress` 是没有默认值的必填项,因此本目录明确选择 `true`,对应描述允许同时存在多个 `in_progress` 项。选择 `false` 的部署会获得同一工具,但描述会要求只能有 1 个活动任务。 |
@@ -1624,7 +1623,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
 
 ### `list_agents`
 
-按持久 id 和标签列出你的可继续后台 subagent。用它回忆你启动过哪些 subagent,而不是轮询完成情况——subagent 完成时你会被告知。状态来自实时注册表:running 表示 agent 此刻正在工作;idle 表示已加载但处于轮次之间,可能正在等待它启动的 agent;ready 表示它只存在于存储中——可恢复而非终态,也不表示有结果等待收集;`send_message` 会在同一对话上开启新的轮次,且无论处于哪种状态,直接子级都仍可作为 `send_message` 的目标。该快照并非投递承诺;`send_message` 会执行权威检查,仍可能失败。无法读取的子级会作为诊断信息报告,而不会被静默丢弃。`descendants` 作用域会按稳定的前序顺序遍历你下方的整棵树,并为每个条目标注其持久的直接父会话 id 和深度。只有深度为 1 的条目可以使用 `send_message`;更深的条目只能作为 `interrupt_agent` 的候选目标。
+按持久 id 和标签列出你的可继续后台 subagent。用它回忆你启动过哪些 subagent,而不是轮询完成情况——subagent 完成时你会被告知。状态来自实时注册表:running 表示 agent 此刻正在工作;idle 表示已加载但处于轮次之间,可能正在等待它启动的 agent;ready 表示它只存在于存储中——可恢复而非终态,也不表示有结果等待收集;`send_message` 会在运行中 child 的最近 step 边界 steer 消息,或为 idle、ready child 启动轮次,且无论处于哪种状态,直接子级都仍可作为 `send_message` 的目标。该快照并非投递承诺;`send_message` 会执行权威检查,仍可能失败。无法读取的子级会作为诊断信息报告,而不会被静默丢弃。`descendants` 作用域会按稳定的前序顺序遍历你下方的整棵树,并为每个条目标注其持久的直接父会话 id 和深度。只有深度为 1 的条目可以使用 `send_message`;更深的条目只能作为 `interrupt_agent` 的候选目标。
 
 ```json
 {
@@ -1646,23 +1645,23 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
 
 ### `send_message`
 
-根据 subagent id 向后台 subagent 发送消息,继续同一段对话。该消息会成为 subagent 的下一轮次:如果它仍在工作,消息会等待当前轮次结束,因此无法改变已经开始的工作方向。此调用不会返回 subagent 的答案,只会确认消息已投递,因此请用它分派更多工作。调用失败表示消息**未**投递。
+根据 agent id 向直接可继续 child 发送消息。如果你是驻留的可继续 child,也可以把自己的直接 parent 作为目标。如果目标仍在工作,消息会 steer 其最近的 step;如果目标处于 idle,消息会启动一个轮次。此调用不会返回该 agent 的答案,只会确认消息已投递。调用失败表示消息**未**投递。
 
 ```json
 {
   "type": "object",
   "properties": {
-    "subagent_id": {
+    "agent_id": {
       "type": "string",
-      "description": "The subagent id returned when the background subagent was started."
+      "description": "The agent id of your direct continuable child, or your direct parent when you are a resident continuable child."
     },
     "message": {
       "type": "string",
-      "description": "The message to deliver to the subagent."
+      "description": "The message to deliver to the agent."
     }
   },
   "required": [
-    "subagent_id",
+    "agent_id",
     "message"
   ]
 }
@@ -1672,33 +1671,6 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
 
 这些是控制可继续后台 subagent 的全局命名工具:绑定提供方的 `tool-subagent` 实例注册不同的委派工具;本包注册一次 `send_message` 和 `interrupt_agent`,另由 `list_agents` 通过单独加载的 `/list-agents` 插件提供,其目录行使用 sessionProjections 和实时 Agent 注册表。
 
-<a id="deepseek-aidsh-tool-subagent-report"></a>
-
-## `@deepseek-ai/dsh-tool-subagent-report`
-
-### `report`
-
-向启动你的 agent 报告选定内容。在你结束前调用一次,给出自包含的最终结果;当进度或发现会改变该 agent 接下来的行动时,也可以更早调用。该 agent 与你共享工作区,但不会自动收到你的 transcript(文本记录)、工具输出或推理,因此完成你的工作本身并不等于交出结果。报告不会结束你的轮次或完成你的工作,且只有直接父级会收到。失败的调用仍可能已经送达,因此不要盲目重复。
-
-```json
-{
-  "type": "object",
-  "properties": {
-    "output": {
-      "type": "string",
-      "description": "Actionable content for your parent; summarize conclusions and reference relevant shared paths."
-    }
-  },
-  "required": [
-    "output"
-  ]
-}
-```
-
-来源:[`packages/subagent/tool-subagent-report/src/index.ts`](../packages/subagent/tool-subagent-report/src/index.ts)
-
-按可继续的进程内子级注册,而非全局注册,因此该 schema 仅在这种子级内部可见,并且不受其全局 `toolFilter` 影响。同一份贡献还会安装子级作用域的 `tool:report` 系统提示词 section,本目录不渲染该 section。面向父级的 `send_message` 工具单独安装。
-
 <a id="deepseek-aidsh-tool-jobs"></a>
 
 ## `@deepseek-ai/dsh-tool-jobs`

+ 3 - 7
packages/bundle/base/cordis.patch.yml

@@ -360,9 +360,9 @@
         backgroundMode: continuable
 
     # Fork omits model selection so provider/model stay equal to the parent and
-    # the inherited history remains eligible for KV Cache reuse. It stays one-shot
-    # because a continuable child's `report` tool and prompt section precede that
-    # history and invalidate the same prefix.
+    # the inherited history remains eligible for KV Cache reuse. This base row
+    # stays one-shot; preset layers may select continuable mode without adding a
+    # child-only system-prompt section or tool schema ahead of that history.
     # See .agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.md
     # and .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md.
     - id: tool-subagent-fork
@@ -372,10 +372,6 @@
         toolName: subagent_fork
         backgroundMode: one-shot
 
-    # Optional direct-child return channel; absent from roots and one-shot agents.
-    - id: tool-subagent-report
-      name: '@deepseek-ai/dsh-tool-subagent-report'
-
     - id: workflow-worker-thread
       name: '@deepseek-ai/dsh-workflow-worker-thread'
       config:

+ 0 - 1
packages/bundle/base/package.json

@@ -104,7 +104,6 @@
     "@deepseek-ai/dsh-tool-str-replace-editor": "workspace:^",
     "@deepseek-ai/dsh-tool-subagent": "workspace:^",
     "@deepseek-ai/dsh-tool-subagent-control": "workspace:^",
-    "@deepseek-ai/dsh-tool-subagent-report": "workspace:^",
     "@deepseek-ai/dsh-tool-jobs": "workspace:^",
     "@deepseek-ai/dsh-tool-todo": "workspace:^",
     "@deepseek-ai/dsh-tool-web": "workspace:^",

+ 0 - 6
packages/bundle/web-app/cordis.patch.yml

@@ -412,12 +412,6 @@
 - id: tool-subagent-fork
   disabled: true
 
-# `tool-subagent-report` is host-plane for the same reason as the registry, not
-# because a preset may not want it: it registers a CONTINUABLE SETUP on that
-# singleton rather than a tool this agent calls, and the setup list is not
-# scope-aware — one copy per mounted preset means every child gets `report`
-# registered once per live session, which throws on the second.
-
 - id: workflow-worker-thread
   disabled: true
 

+ 4 - 4
packages/client/ui-chat/tests/chat-branch-tails.client.spec.tsx

@@ -706,7 +706,7 @@ describe('MessageItem arms', () => {
     // reach it, and the row marker must not claim a form that did not render.
     const cases = [
       { form: 'snapshot', source: { kind: 'plugin', form: 'snapshot', sections: 'not-a-list' }, label: 'plugin' },
-      { form: 'relay', source: { kind: 'subagent-report', form: 'relay' }, label: 'subagent-report' },
+      { form: 'relay', source: { kind: 'agent-message', form: 'relay' }, label: 'agent-message' },
       { form: 'recall', source: { kind: 'session-reference', form: 'recall', references: [{ label: 'x' }] }, label: 'session-reference' },
     ] as const
     for (const { form, source, label } of cases) {
@@ -746,13 +746,13 @@ describe('MessageItem arms', () => {
         kind: 'context',
         seq: 3,
         content: [{ type: 'text', text: 'child report body' }],
-        source: { kind: 'subagent-report', form: 'relay', senderSessionId: 'child-7' },
-        provenance: { role: 'inject', label: 'subagent-report' },
+        source: { kind: 'agent-message', form: 'relay', senderSessionId: 'child-7' },
+        provenance: { role: 'inject', label: 'agent-message' },
         form: 'relay',
       } as never}
       />,
     )
-    fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*subagent-report$/ }))
+    fireEvent.click(view.getByRole('button', { name: /^上下文注入\s*agent-message$/ }))
     expect(view.container.querySelector('[data-context-relay-sender]')?.textContent).toBe('来自会话 child-7')
     expect(view.container.querySelector('[data-context-text]')?.textContent).toBe('child report body')
   })

+ 3 - 3
packages/core/tools/src/index.ts

@@ -1128,9 +1128,9 @@ export class ToolRuntime extends Service {
    * A restriction filters what a scope inherits — the global layer and every
    * ancestor layer on its chain — and never what its OWN layer registers.
    * That exemption is what a per-child capability filter has to keep intact:
-   * the delegation runtime registers a child's reporting and structured-output
-   * tools into the child's own layer, and a filter naming the capabilities the
-   * child may use must not strip the machinery it answers through.
+   * the delegation runtime registers a child's structured-output tool into the
+   * child's own layer, and a filter naming the capabilities the child may use
+   * must not strip the machinery it answers through.
    *
    * Reading the exempt set as "the global layer" instead of "not mine" held
    * only while every model-facing tool sat in the host composition. Once

+ 1 - 1
packages/core/tools/tests/gen-tool-catalog.spec.ts

@@ -31,7 +31,7 @@ describe('gen-tool-catalog collectToolCatalog', () => {
       'cordis_undefine', 'create_goal', 'edit', 'exit_plan_mode', 'followup_task', 'get_goal', 'glob', 'grep',
       'interrupt_agent', 'interrupt_agent', 'job_kill', 'job_list', 'job_output',
       'list_agents', 'list_agents', 'list_subagent_models', 'lsp', 'pwsh', 'pwsh', 'ralph',
-      'read', 'read_image', 'report', 'run_code', 'schedule_create', 'schedule_delete',
+      'read', 'read_image', 'run_code', 'schedule_create', 'schedule_delete',
       'schedule_list', 'send_message', 'send_message', 'session_event_read', 'session_event_search',
       'session_event_trace', 'session_search', 'session_trace', 'skill', 'spawn_teammate',
       'str_replace_editor', 'subagent', 'team_task_create',

+ 2 - 2
packages/experimental/agent-team-profile/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/experimental/agent-team-profile/README.md
-README.md: 0c0d0cd24b0c4fca52421e8908e5ae196429265d
-README.zh.md: 83c8a8f55e4d9d1bfcbc6ee85f8ce3e5c695fa82
+README.md: 3a3ce9ba9d1298dc525533c2fddcb968f6823dae
+README.zh.md: e33e47f99ea018d5a8a58156d41d23e5a324cf94

+ 1 - 1
packages/experimental/agent-team-profile/README.md

@@ -48,7 +48,7 @@ The layer adds the Agent Teams domain and its scoped creation, roster, messaging
 <details>
 <summary>Implementation internals — click to expand</summary>
 
-The package's runtime content is [`cordis.patch.yml`](cordis.patch.yml). Applied after `dsh-base`, the patch disables `tool-subagent-control`, `tool-subagent-list-agents`, and `tool-subagent-report`; sets the fresh and fork Subagent rows to `one-shot`; and inserts the Team service and tool rows with explicit providers and limits.
+The package's runtime content is [`cordis.patch.yml`](cordis.patch.yml). Applied after `dsh-base`, the patch disables `tool-subagent-control` and `tool-subagent-list-agents`; sets the fresh and fork Subagent rows to `one-shot`; and inserts the Team service and tool rows with explicit providers and limits.
 
 | File | Role |
 |---|---|

+ 1 - 1
packages/experimental/agent-team-profile/README.zh.md

@@ -48,7 +48,7 @@ profile 必须已经包含 `@deepseek-ai/dsh-base`,本层会使用其中的 Su
 <details>
 <summary>实现细节——点击展开</summary>
 
-本包的运行时内容是 [`cordis.patch.yml`](cordis.patch.yml)。在 `dsh-base` 之后应用时,patch 会禁用 `tool-subagent-control`、`tool-subagent-list-agents` 与 `tool-subagent-report`,把 fresh 与 fork Subagent 行设置为 `one-shot`,并以显式 provider 和限制插入 Team service 与工具行。
+本包的运行时内容是 [`cordis.patch.yml`](cordis.patch.yml)。在 `dsh-base` 之后应用时,patch 会禁用 `tool-subagent-control` 与 `tool-subagent-list-agents`,把 fresh 与 fork Subagent 行设置为 `one-shot`,并以显式 provider 和限制插入 Team service 与工具行。
 
 | 文件 | 职责 |
 |---|---|

+ 0 - 3
packages/experimental/agent-team-profile/cordis.patch.yml

@@ -8,9 +8,6 @@
 - id: tool-subagent-list-agents
   disabled: true
 
-- id: tool-subagent-report
-  disabled: true
-
 - id: tool-subagent
   config:
     provider: spawn

+ 0 - 1
packages/experimental/agent-team-profile/tests/profile.spec.ts

@@ -37,7 +37,6 @@ describe('Agent Teams profile bundle', () => {
     }[]
     expect(patches.find(patch => patch.id === 'tool-subagent-control')).toMatchObject({ disabled: true })
     expect(patches.find(patch => patch.id === 'tool-subagent-list-agents')).toMatchObject({ disabled: true })
-    expect(patches.find(patch => patch.id === 'tool-subagent-report')).toMatchObject({ disabled: true })
     expect(patches.find(patch => patch.id === 'tool-subagent')?.config).toMatchObject({ backgroundMode: 'one-shot' })
     expect(patches.find(patch => patch.id === 'tool-subagent-fork')?.config).toMatchObject({ backgroundMode: 'one-shot' })
     const inserted = patches.flatMap(patch => patch.insert ?? [])

+ 4 - 2
packages/experimental/agent-team/src/mailbox.ts

@@ -6,7 +6,9 @@ import { brandString } from '@deepseek-ai/dsh-brand'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import { createUserMessage } from '@deepseek-ai/dsh-llm'
 import type { ContentBlock } from '@deepseek-ai/dsh-llm'
-import type { Session, SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
+import { SessionId } from '@deepseek-ai/dsh-session'
+import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
+import { queueHostSubagentPrompt } from '@deepseek-ai/dsh-subagent/internal'
 import { errorMessage, TeamError } from './error.ts'
 import type { TeamJournal } from './journal.ts'
 import type { TeamRuntimeLifecycle } from './lifecycle.ts'
@@ -260,7 +262,7 @@ export class TeamMailbox {
           return true
         }
       }
-      await this.ctx.subagents.followup(root, message.targetId, content, { source, signal })
+      await queueHostSubagentPrompt(this.ctx.subagents, root, message.targetId, content, source, signal)
       return target === undefined
         ? true
         : await this.checkpointDelivered(root, target.session, message.id)

+ 26 - 22
packages/experimental/agent-team/tests/team.spec.ts

@@ -11,6 +11,7 @@ import { SessionId, type Session } from '@deepseek-ai/dsh-session'
 import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
 import SubagentService from '@deepseek-ai/dsh-subagent'
+import { queueSubagentPrompt, type HostPromptQueue } from '@deepseek-ai/dsh-subagent/internal'
 import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork-in-process'
 import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
 import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
@@ -1126,16 +1127,17 @@ describe('Team mailbox and waiting', () => {
     const entered = Promise.withResolvers<undefined>()
     const release = Promise.withResolvers<undefined>()
     const admitted: string[] = []
-    vi.spyOn(ctx.subagents, 'followup').mockImplementation(async (_parent, _childId, blocks) => {
-      const last = blocks.at(-1)
-      const text = last?.type === 'text' ? last.text : ''
-      admitted.push(text)
-      if (text === 'first waking') {
-        entered.resolve(undefined)
-        await release.promise
-      }
-      return createUserMessage({ content: blocks, source: { kind: 'user' } }).id
-    })
+    vi.spyOn(ctx.subagents as unknown as HostPromptQueue, queueSubagentPrompt)
+      .mockImplementation(async (_parent, _childId, blocks) => {
+        const last = blocks.at(-1)
+        const text = last?.type === 'text' ? last.text : ''
+        admitted.push(text)
+        if (text === 'first waking') {
+          entered.resolve(undefined)
+          await release.promise
+        }
+        return createUserMessage({ content: blocks, source: { kind: 'user' } }).id
+      })
 
     const first = ctx.agentTeams.sendMessage(lead, {
       target: 'ordered-target', content: content('first waking'), delivery: 'wakeup', signal: SIGNAL,
@@ -1248,7 +1250,8 @@ describe('Team mailbox and waiting', () => {
     expect(uncertain.status).toBe('queued')
     inspect.mockRestore()
 
-    vi.spyOn(ctx.subagents, 'followup').mockRejectedValueOnce(new Error('delivery unavailable'))
+    vi.spyOn(ctx.subagents as unknown as HostPromptQueue, queueSubagentPrompt)
+      .mockRejectedValueOnce(new Error('delivery unavailable'))
     const failed = await ctx.agentTeams.sendMessage(lead, {
       target: 'inactive-target', content: content('delivery failure'), delivery: 'wakeup', signal: SIGNAL,
     })
@@ -1555,18 +1558,19 @@ describe('Team mailbox and waiting', () => {
     const entered = Promise.withResolvers<undefined>()
     const aborted = Promise.withResolvers<undefined>()
     const release = Promise.withResolvers<undefined>()
-    vi.spyOn(ctx.subagents, 'followup').mockImplementation(async (_parent, _childId, _content, options) => {
-      entered.resolve(undefined)
-      return await new Promise<never>((_resolve, reject) => {
-        options.signal.addEventListener('abort', () => {
-          aborted.resolve(undefined)
-          void release.promise.then(() => {
-            const reason: unknown = options.signal.reason
-            reject(reason instanceof Error ? reason : new Error(String(reason)))
-          })
-        }, { once: true })
+    vi.spyOn(ctx.subagents as unknown as HostPromptQueue, queueSubagentPrompt)
+      .mockImplementation(async (_parent, _childId, _content, _source, signal) => {
+        entered.resolve(undefined)
+        return await new Promise<never>((_resolve, reject) => {
+          signal.addEventListener('abort', () => {
+            aborted.resolve(undefined)
+            void release.promise.then(() => {
+              const reason: unknown = signal.reason
+              reject(reason instanceof Error ? reason : new Error(String(reason)))
+            })
+          }, { once: true })
+        })
       })
-    })
 
     const sending = ctx.agentTeams.sendMessage(lead, {
       target: 'mailbox-worker',

+ 6 - 1
packages/experimental/tool-agent-team/tests/tool-team.spec.ts

@@ -151,6 +151,11 @@ describe('dsh-tool-team', () => {
     expect(childAssembly.tools.map(schema => schema.name).filter(name => TOOL_NAMES.includes(name)).sort())
       .toEqual(TOOL_NAMES)
     expect(renderPrompt(childAssembly)).toContain('Your Team role is teammate; your Team name is tool-worker')
+    const initialPrompt = child.session.snapshotEvents().find(event => event.type === 'user/message'
+      && event.data.source.kind === 'user')
+    expect(initialPrompt?.type === 'user/message'
+      ? initialPrompt.data.content.flatMap(block => block.type === 'text' ? [block.text] : [])
+      : []).toEqual(['stay available'])
 
     const denied = await execute(ctx, child, 'spawn_teammate', {
       name: 'nested', description: 'not allowed', prompt: 'no',
@@ -374,7 +379,7 @@ describe('dsh-tool-team', () => {
 
     await fiber.dispose()
     const legacySchema = (await assembly(ctx, lead)).tools.find(schema => schema.name === 'send_message')
-    expect(JSON.stringify(legacySchema)).toContain('subagent_id')
+    expect(JSON.stringify(legacySchema)).toContain('agent_id')
   })
 
   it('rolls back partial scoped installation after a same-scope collision', async () => {

+ 9 - 34
packages/extensions/tool-cordis/src/api-catalog.ts

@@ -2195,11 +2195,11 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
         throws: ['when continuation services are unavailable or materialization fails.'],
       },
       {
-        signature: 'async followup( parent: Agent, childId: SessionId, content: ContentBlock[], options: SubagentFollowupOptions, ): Promise<MessageId>',
-        description: 'Deliver one later message to a continuable child as its next FIFO turn. A resident child\'s Agent inbox accepts it directly (waking a `waiting` Activation), while an absent one is cold-resumed from its persisted Session. The Agent inbox is the only queue, so every accepted message has one observable order.',
-        parameters: [{ name: 'parent', description: 'the exact live direct parent authorizing this delivery.' }, { name: 'childId', description: 'durable child session id.' }, { name: 'content', description: 'user-role content to deliver.' }, { name: 'options', description: 'the message source fields and caller cancellation, which stops the operation only before inbox acceptance.' }],
+        signature: 'async sendMessage( sender: Agent, targetId: SessionId, content: ContentBlock[], options: SubagentSendMessageOptions, ): Promise<MessageId>',
+        description: 'Steer one model-authored message to the sender\'s direct parent or direct continuable child. A running target admits it at the nearest step boundary; an idle target starts a turn, and an absent direct child cold-resumes from persistence. The service derives durable sender attribution from the exact live sender. Caller cancellation stops only pre-acceptance work.',
+        parameters: [{ name: 'sender', description: 'exact live Agent authorizing and originating the message.' }, { name: 'targetId', description: 'durable direct-parent or direct-child session id.' }, { name: 'content', description: 'model-authored content to deliver.' }, { name: 'options', description: 'caller cancellation before inbox acceptance.' }],
         returns: 'the accepted message\'s inbox id.',
-        throws: ['when continuation services are unavailable, parent authority is rejected, or the message was not admitted.'],
+        throws: ['when continuation services are unavailable, adjacency is rejected, or the message was not admitted.'],
       },
       {
         signature: 'interrupt(targetSessionId: SessionId, authority: SubagentInterruptAuthority): void',
@@ -2207,19 +2207,6 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
         parameters: [{ name: 'targetSessionId', description: 'the durable child session id to interrupt.' }, { name: 'authority', description: 'the human parent address or exact live ancestor Agent.' }],
         throws: ['{SubagentError} `UNAUTHORIZED` when the authority does not own the live target.'],
       },
-      {
-        signature: 'async reportFrom( child: Agent, content: ContentBlock[], options: SubagentReportOptions, ): Promise<MessageId>',
-        description: 'Deliver selected content from one live continuable child to its durable direct parent. The child is the authority credential; callers cannot name a recipient. Reporting does not conclude the child\'s turn or Activation.',
-        parameters: [{ name: 'child', description: 'exact live reporting child.' }, { name: 'content', description: 'selected model-facing content.' }, { name: 'options', description: 'parent scheduling and pre-acceptance cancellation.' }],
-        returns: 'the stable identity of the parent-accepted message.',
-        throws: ['when continuation services are unavailable, sender authorization fails, or the direct parent is not live.'],
-      },
-      {
-        signature: 'registerContinuableSetup(contribution: ContinuableSetupContribution): () => void',
-        description: 'Compose one deployment capability into every continuable child\'s unpublished creation context on fresh creation and cold resume. Grants wait for the next Activation; removing the contribution revokes every resident installation immediately.',
-        parameters: [{ name: 'contribution', description: 'synchronous child-scope installer.' }],
-        returns: 'the exact Cordis effect disposer.',
-      },
       {
         signature: 'async drainContinuableDescendants(parents: readonly Agent[]): Promise<void>',
         description: 'Close continuable admission below exact live parent Agents, stop only their visible descendant Activations synchronously, then await admitted scoped materializations and release those forests child-first. The scoped cutoff lasts until each exact parent leaves the registry; unrelated parent trees remain live.',
@@ -3751,10 +3738,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [
     name: 'ContinuableCreateSpec',
     declaration: 'export interface ContinuableCreateSpec {\n    readonly seed?: readonly SessionEvent[];\n}',
   },
-  {
-    name: 'ContinuableSetupContribution',
-    declaration: 'export type ContinuableSetupContribution = (childCtx: Context) => () => void;',
-  },
   {
     name: 'ContinuableStart',
     declaration: 'export interface ContinuableStart {\n    readonly childId: SessionId;\n    readonly messageId: MessageId;\n}',
@@ -5419,10 +5402,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [
     name: 'SubagentDescriptorData',
     declaration: 'export type SubagentDescriptorData = OneShotSubagentDescriptorData | ContinuableSubagentDescriptorData;',
   },
-  {
-    name: 'SubagentFollowupOptions',
-    declaration: 'export interface SubagentFollowupOptions {\n    readonly source: MessageSource;\n    readonly signal: AbortSignal;\n}',
-  },
   {
     name: 'SubagentInterruptAuthority',
     declaration: 'export type SubagentInterruptAuthority = {\n    readonly kind: \'user\';\n    readonly parentSessionId: SessionId;\n} | {\n    readonly kind: \'ancestor\';\n    readonly agent: Agent;\n};',
@@ -5451,14 +5430,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [
     name: 'SubagentProvider',
     declaration: 'export interface SubagentProvider {\n    readonly name: string;\n    readonly capabilities: SubagentCapabilities;\n    readonly inheritsParentContext: boolean;\n    readonly agentRouteDefaults?: Readonly<{\n        provider: string;\n        model: string;\n    }>;\n    start(request: ResolvedSubagentStartRequest): Promise<SubagentRun>;\n    prepareContinuable?(request: ContinuableCreateRequest): Promise<ContinuableCreateSpec>;\n}',
   },
-  {
-    name: 'SubagentReportDelivery',
-    declaration: 'export type SubagentReportDelivery = \'quiet\' | \'next-step\';',
-  },
-  {
-    name: 'SubagentReportOptions',
-    declaration: 'export interface SubagentReportOptions {\n    readonly delivery: SubagentReportDelivery;\n    readonly signal: AbortSignal;\n}',
-  },
   {
     name: 'SubagentResult',
     declaration: 'export interface SubagentResult {\n    readonly output: ContentBlock[];\n    readonly structured?: unknown;\n    readonly diagnostic?: string;\n    readonly stopReason: SubagentStopReason;\n}',
@@ -5481,7 +5452,11 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   {
     name: 'SubagentRuntime',
-    declaration: 'export class SubagentRuntime extends TypertRemoteService {\n    constructor(ctx: Context);\n    async startContinuable(spec: ContinuableStartSpec): Promise<ContinuableStart>;\n    async followup(parent: Agent, childId: SessionId, content: ContentBlock[], options: SubagentFollowupOptions): Promise<MessageId>;\n    interrupt(targetSessionId: SessionId, authority: SubagentInterruptAuthority): void;\n    async reportFrom(child: Agent, content: ContentBlock[], options: SubagentReportOptions): Promise<MessageId>;\n    registerContinuableSetup(contribution: ContinuableSetupContribution): () => void;\n    async drainContinuableDescendants(parents: readonly Agent[]): Promise<void>;\n    async drainContinuableChildren(parent: Agent, childIds: readonly SessionId[]): Promise<void>;\n    listChildren(parentSessionId: SessionId, signal?: AbortSignal): Promise<SubagentListEntry[]>;\n    listDescendants(rootSessionId: SessionId, signal?: AbortSignal): Promise<SubagentDescendantListEntry[]>;\n    @Remote(\'list\')\n    async remoteExportList(parentSessionId: SessionId, signal: AbortSignal): Promise<SubagentCatalog>;\n    @Remote(\'prompt\')\n    async prompt(request: SubagentPromptRequest, signal: AbortSignal): Promise<SubagentPromptReceipt>;\n    @Remote(\'interruptByParent\')\n    interruptByParent(childSessionId: SessionId, parentSessionId: SessionId, mode: \'continuable\'): SubagentInterruptReceipt;\n    registerProvider(provider: SubagentProvider): () => void;\n    getProvider(name: string): SubagentProvider | un /* …truncated — full shape in source */',
+    declaration: 'export class SubagentRuntime extends TypertRemoteService {\n    constructor(ctx: Context);\n    async startContinuable(spec: ContinuableStartSpec): Promise<ContinuableStart>;\n    async sendMessage(sender: Agent, targetId: SessionId, content: ContentBlock[], options: SubagentSendMessageOptions): Promise<MessageId>;\n    interrupt(targetSessionId: SessionId, authority: SubagentInterruptAuthority): void;\n    async drainContinuableDescendants(parents: readonly Agent[]): Promise<void>;\n    async drainContinuableChildren(parent: Agent, childIds: readonly SessionId[]): Promise<void>;\n    listChildren(parentSessionId: SessionId, signal?: AbortSignal): Promise<SubagentListEntry[]>;\n    listDescendants(rootSessionId: SessionId, signal?: AbortSignal): Promise<SubagentDescendantListEntry[]>;\n    @Remote(\'list\')\n    async remoteExportList(parentSessionId: SessionId, signal: AbortSignal): Promise<SubagentCatalog>;\n    @Remote(\'prompt\')\n    async prompt(request: SubagentPromptRequest, signal: AbortSignal): Promise<SubagentPromptReceipt>;\n    @Remote(\'interruptByParent\')\n    interruptByParent(childSessionId: SessionId, parentSessionId: SessionId, mode: \'continuable\'): SubagentInterruptReceipt;\n    registerProvider(provider: SubagentProvider): () => void;\n    getProvider(name: string): SubagentProvider | undefined;\n    list(): string[];\n    async start(name: string, request: SubagentStartRequest): Promise<SubagentRun>;\n}',
+  },
+  {
+    name: 'SubagentSendMessageOptions',
+    declaration: 'export interface SubagentSendMessageOptions {\n    readonly signal: AbortSignal;\n}',
   },
   {
     name: 'SubagentStartRequest',

+ 2 - 8
packages/preset/agent-presets/presets/cordis/agent.cordis.yml

@@ -153,12 +153,6 @@
 # `workflows` is different — nothing outside an agent reads it — so every row
 # that reaches it shares one entry-local realm here, and a consumer left
 # outside would resolve a host registry this preset does not populate.
-#
-# `tool-subagent-report` is host-plane for the same reason as the registry,
-# not because a preset may not want it: it registers a CONTINUABLE SETUP on
-# that singleton rather than a tool this agent calls, and the setup list is
-# not scope-aware — one copy per mounted preset means every child gets
-# `report` registered once per live session, which throws on the second.
 - id: delegation
   name: cordis:group
   group: true
@@ -181,8 +175,8 @@
 
     # Fork omits model selection so provider/model stay equal to the parent and
     # the inherited history remains eligible for KV Cache reuse. This preset
-    # keeps fork continuable and accepts its child-scoped `report` additions invalidating
-    # that prefix; issue #2124 tracks cache-preserving continuable fork.
+    # keeps fork continuable; parent and child inherit the same messaging tool,
+    # while the parent id and return guidance follow the inherited history.
     - id: tool-subagent-fork
       name: '@deepseek-ai/dsh-tool-subagent'
       config:

+ 2 - 2
packages/preset/agent-presets/presets/ptc/agent.cordis.yml

@@ -194,8 +194,8 @@
 
     # Fork omits model selection so provider/model stay equal to the parent and
     # the inherited history remains eligible for KV Cache reuse. This preset
-    # keeps fork continuable and accepts its child-scoped `report` additions invalidating
-    # that prefix; issue #2124 tracks cache-preserving continuable fork.
+    # keeps fork continuable; parent and child inherit the same messaging tool,
+    # while the parent id and return guidance follow the inherited history.
     - id: tool-subagent-fork
       name: '@deepseek-ai/dsh-tool-subagent'
       config:

+ 2 - 8
packages/preset/agent-presets/presets/standard/agent.cordis.yml

@@ -165,12 +165,6 @@
 # `workflows` is different — nothing outside an agent reads it — so every row
 # that reaches it shares one entry-local realm here, and a consumer left
 # outside would resolve a host registry this preset does not populate.
-#
-# `tool-subagent-report` is host-plane for the same reason as the registry,
-# not because a preset may not want it: it registers a CONTINUABLE SETUP on
-# that singleton rather than a tool this agent calls, and the setup list is
-# not scope-aware — one copy per mounted preset means every child gets
-# `report` registered once per live session, which throws on the second.
 - id: delegation
   name: cordis:group
   group: true
@@ -193,8 +187,8 @@
 
     # Fork omits model selection so provider/model stay equal to the parent and
     # the inherited history remains eligible for KV Cache reuse. This preset
-    # keeps fork continuable and accepts its child-scoped `report` additions invalidating
-    # that prefix; issue #2124 tracks cache-preserving continuable fork.
+    # keeps fork continuable; parent and child inherit the same messaging tool,
+    # while the parent id and return guidance follow the inherited history.
     - id: tool-subagent-fork
       name: '@deepseek-ai/dsh-tool-subagent'
       config:

+ 2 - 2
packages/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/README.md
-README.md: 06d123c900cb81ec8896167ed2e505df3b913fdc
-README.zh.md: 3c2e25f005ee85665e3fda5e1fa3a5b7409d7872
+README.md: 0773bf83c87a43ed0697db413939b84668948d06
+README.zh.md: cf3de50990e810c0c671628c95026e1567edac94

+ 2 - 3
packages/subagent/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-The subagent group is the delegation family: it lets an agent hand a task to a child agent, wait for or continue the child's work, and keep every child discoverable. One contract (`ctx.subagents`) serves any number of named providers, so a single composition can mix in-process children (fresh, or forked from the parent's completed history) with out-of-process children — an ACP agent, a real Codex or Claude Code installation, or a complete Harness runtime over the SDK. The model-facing tools expose delegation, follow-up, and listing to agents, and a parent can always see which children exist and whether they are live or stored. This page maps the group; each package README owns its package contract.
+The subagent group is the delegation family: it lets an agent hand a task to a child agent, wait for or continue the child's work, and keep every child discoverable. One contract (`ctx.subagents`) serves any number of named providers, so a single composition can mix in-process children (fresh, or forked from the parent's completed history) with out-of-process children — an ACP agent, a real Codex or Claude Code installation, or a complete Harness runtime over the SDK. The model-facing tools expose delegation, adjacent-Agent messaging, and listing to agents, and a parent can always see which children exist and whether they are live or stored. This page maps the group; each package README owns its package contract.
 
 ## Table of Contents
 
@@ -33,8 +33,7 @@ The subagent group is the delegation family: it lets an agent hand a task to a c
 | [`subagent-claude-code/`](subagent-claude-code/README.md) | Runs a real Claude Code child through the official Agent SDK | registers on `ctx.subagents` |
 | [`subagent-dsh-sdk/`](subagent-dsh-sdk/README.md) | Runs an out-of-process Harness child through the TypeScript SDK | registers on `ctx.subagents` |
 | [`tool-subagent/`](tool-subagent/README.md) | Exposes delegation to the model | registers on `ctx.tools` |
-| [`tool-subagent-control/`](tool-subagent-control/README.md) | Exposes follow-up, interrupt, and listing to the model | registers on `ctx.tools` |
-| [`tool-subagent-report/`](tool-subagent-report/README.md) | Provides the child-to-parent report channel | registers in child scopes |
+| [`tool-subagent-control/`](tool-subagent-control/README.md) | Exposes adjacent-Agent messaging, interrupt, and listing to the model | registers on `ctx.tools` |
 
 -----
 

+ 2 - 3
packages/subagent/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-group"
 
 ## 概述
 
-subagent 组是委派能力家族:它让 agent(智能体)把任务交给子 agent,等待或继续子 agent 的工作,并让每个子 agent 随时可被发现。一个约定(`ctx.subagents`)服务任意数量的具名提供方,因此单个组合可以混合进程内子 agent(全新启动,或从父级已完成历史派生)与进程外子 agent——ACP agent、真实 Codex 或 Claude Code 安装,或经 SDK 运行的完整 Harness 运行时。面向模型的工具向 agent 公开委派、后续消息与列举,父级总能看到存在哪些子级、它们在线还是仅存于存储。本页是组的映射;各包 README 负责各自的包约定。
+subagent 组是委派能力家族:它让 agent(智能体)把任务交给子 agent,等待或继续子 agent 的工作,并让每个子 agent 随时可被发现。一个约定(`ctx.subagents`)服务任意数量的具名提供方,因此单个组合可以混合进程内子 agent(全新启动,或从父级已完成历史派生)与进程外子 agent——ACP agent、真实 Codex 或 Claude Code 安装,或经 SDK 运行的完整 Harness 运行时。面向模型的工具向 agent 公开委派、相邻 Agent 消息与列举,父级总能看到存在哪些子级、它们在线还是仅存于存储。本页是组的映射;各包 README 负责各自的包约定。
 
 ## 目录
 
@@ -33,8 +33,7 @@ subagent 组是委派能力家族:它让 agent(智能体)把任务交给
 | [`subagent-claude-code/`](subagent-claude-code/README.zh.md) | 经官方 Agent SDK 运行真实 Claude Code 子 agent | 注册到 `ctx.subagents` |
 | [`subagent-dsh-sdk/`](subagent-dsh-sdk/README.zh.md) | 经 TypeScript SDK 运行进程外 Harness 子 agent | 注册到 `ctx.subagents` |
 | [`tool-subagent/`](tool-subagent/README.zh.md) | 向模型公开委派 | 注册到 `ctx.tools` |
-| [`tool-subagent-control/`](tool-subagent-control/README.zh.md) | 向模型公开后续消息、中断与列举 | 注册到 `ctx.tools` |
-| [`tool-subagent-report/`](tool-subagent-report/README.zh.md) | 提供从子级到父级的报告通道 | 注册到子级作用域 |
+| [`tool-subagent-control/`](tool-subagent-control/README.zh.md) | 向模型公开相邻 Agent 消息、中断与列举 | 注册到 `ctx.tools` |
 
 -----
 

+ 2 - 2
packages/subagent/subagent-fork-in-process/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-fork-in-process/README.md
-README.md: dd5fba5fce1f1883912a4ec4ef27a66140743280
-README.zh.md: 84eabe803d7ddbd464d83e04eb502382ed0c1bbf
+README.md: 0c8ff5372717b26a4e5ee556655094a74c9d6dda
+README.zh.md: 113b0ef9657173c7da8bf89d1a2f27d05f81ff3d

+ 4 - 4
packages/subagent/subagent-fork-in-process/README.md

@@ -82,9 +82,9 @@ One difference from spawn, expressed as data: the backend computes the balanced
 
 On `start`, the prefix is sliced from the parent's event log up to and including the last `turn/end`; the shared driver then creates the child with that seed, applies the same persona, tool-filter, and structured-output setup, drives one task, reads the child's own final output, and disposes quiescently. The provider advertises `agentOptions` plus the same output, depth, filter, and persona capabilities as spawn. `prepareContinuable` captures the prefix once, at creation, because it becomes part of the child's own durable transcript.
 
-### One-shot binding
+### Lifecycle binding
 
-The base bundle and ACP/headless examples bind this provider to `backgroundMode: one-shot`: a continuable fork child carries the child-scoped `report` tool and its prompt section before the inherited history, defeating byte-identical prefix reuse. The CLI presets retain `continuable` fork and accept that prefix loss ([cache-preserving fork Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md)).
+The base bundle and ACP/headless examples bind this provider to `backgroundMode: one-shot`, while the CLI presets select `continuable`. Both preserve the inherited request prefix: parent and child receive the same messaging tool definition and ordering, and the continuable child's parent id and return guidance live in its initial user task after inherited history ([cache-preserving fork Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md)).
 
 </details>
 
@@ -119,7 +119,7 @@ Forking duplicates retained completed history into the child's request, which th
 
 #### KV Cache effect
 
-The child may reuse the inherited byte-identical prefix under the same provider and model. Persona, tool-filter, generated-SDK, or route changes may invalidate reuse before inherited history; later child history is append-only. The base bundle and ACP/headless examples use one-shot fork to preserve this prefix. The CLI presets retain continuable fork and accept that the child-scoped `report` tool and its prompt section invalidate it ([cache-preserving fork Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md)).
+The child may reuse the inherited byte-identical prefix under the same provider and model. Persona, tool-filter, generated-SDK, or route changes may invalidate reuse before inherited history; later child history is append-only. Continuable messaging adds no child-only system-prompt section or tool schema; the parent id and return guidance follow inherited history in the initial user task ([cache-preserving fork Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md)).
 
 ### Parent tool result, indirectly
 
@@ -143,7 +143,7 @@ Append-only; newly visible content follows the reusable request prefix and does
 These limits define when the backend is the wrong choice; they are current package constraints.
 
 - **The seed is a one-time snapshot** — the child sees the parent's completed turns as of the fork and nothing the parent logs afterwards; there is no live context sharing.
-- **Fork lifecycle policy differs by composition** — the base bundle and ACP/headless examples use one-shot fork to preserve prefix reuse, while the CLI presets use continuable fork and accept the child-scoped [`report` return channel](../tool-subagent-report/README.md) invalidating that prefix. Making continuable fork cache-preserving requires the child system prompt and tool schemas to match the parent's byte for byte. Rationale and the reintroduction condition: the [cache-preserving fork Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md).
+- **Fork lifecycle policy differs by composition** — the base bundle and ACP/headless examples use one-shot fork, while the CLI presets use continuable fork. Both keep the inherited prefix eligible for reuse because parent and child messaging definitions match byte for byte; explicit persona, tool filtering, generated-SDK, or route changes can still break equality. Rationale: the [cache-preserving fork Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md).
 - **Shipped fork tools do not expose child LLM route selection** — they inherit the parent's provider and model so the copied history remains eligible for KV Cache reuse. Route selection stays disabled until a change can preserve reuse or expose a bounded recomputation cost; the [model-selected route Agent Note](../../../.agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.md) owns that restriction.
 
 <a id="dev-note"></a>

+ 4 - 4
packages/subagent/subagent-fork-in-process/README.zh.md

@@ -82,9 +82,9 @@ kind: "package-reference"
 
 `start` 时,从父级事件日志中截取截至最后一个 `turn/end` 的前缀;共享驱动器随后以该初始内容创建子 agent,应用相同的 persona、工具过滤器与结构化输出设置,驱动一项任务,读取子 agent 自身的最终输出,并完全停稳地 dispose。该提供方声明 `agentOptions`,以及与 spawn 相同的输出、深度、过滤与 persona 能力。`prepareContinuable` 在创建时只捕获一次前缀,因为它会成为子 agent 自身持久 transcript(文本记录)的一部分。
 
-### 一次性绑定
+### 生命周期绑定
 
-base bundle 与 ACP/headless 示例在委派工具上把本提供方绑定为 `backgroundMode: one-shot`:可继续 fork 子 agent 会在继承历史之前携带子级作用域的 `report` 工具及其提示词 section,从而破坏逐字节前缀复用。CLI preset 保留可继续 fork,并接受该前缀损失(见[保持 fork 缓存的 Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md))。
+base bundle 与 ACP/headless 示例在委派工具上把本提供方绑定为 `backgroundMode: one-shot`,CLI preset 则选择 `continuable`。两者都保留继承的请求前缀:parent 与 child 获得定义和顺序相同的消息工具,可继续 child 的 parent id 与返回指导位于继承历史之后的初始用户任务中(见[保持 fork 缓存的 Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md))。
 
 </details>
 
@@ -119,7 +119,7 @@ fork 会把保留的已完成历史复制到子 agent 的请求中,子 agent 
 
 #### KV Cache 影响
 
-在提供方与模型相同的前提下,子 agent 可以复用继承的逐字节相同前缀。persona、工具过滤、生成 SDK 或路由变化可能在继承历史之前使复用失效;后续子 agent 历史仅追加。base bundle 与 ACP/headless 示例使用一次性 fork 来保留此前缀。CLI preset 保留可继续 fork,并接受子级作用域的 `report` 工具及其提示词 section 使此前缀失效(见[保持 fork 缓存的 Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md))。
+在提供方与模型相同的前提下,子 agent 可以复用继承的逐字节相同前缀。persona、工具过滤、生成 SDK 或路由变化可能在继承历史之前使复用失效;后续子 agent 历史仅追加。可继续消息不增加 child 专属系统提示词 section 或工具 schema;parent id 与返回指导在初始用户任务中位于继承历史之后(见[保持 fork 缓存的 Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md))。
 
 ### 父级工具结果(间接)
 
@@ -143,7 +143,7 @@ fork 会把保留的已完成历史复制到子 agent 的请求中,子 agent 
 这些限制说明何时选择该后端是错误的;它们是当前包约束。
 
 - **初始内容是一次性快照**——子 agent 只能看到 fork 时父级已完成的轮次,看不到父级此后记录的任何内容;不会实时共享上下文。
-- **fork 生命周期策略因组合而异**——base bundle 与 ACP/headless 示例使用一次性 fork 来保留前缀复用;CLI preset 使用可继续 fork,并接受子级作用域的 [`report` 返回通道](../tool-subagent-report/README.zh.md)使此前缀失效。要让可继续 fork 保留缓存,子 agent 的系统提示词与工具 schema 必须和父级逐字节一致。理由与重新开放条件见[保持 fork 缓存的 Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md)。
+- **fork 生命周期策略因组合而异**——base bundle 与 ACP/headless 示例使用一次性 fork,CLI preset 使用可继续 fork。两者都因 parent 与 child 消息定义逐字节相同而让继承前缀保持可复用;显式 persona、工具过滤、生成 SDK 或路由变化仍可破坏相等性。理由见[保持 fork 缓存的 Agent Note](../../../.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md)。
 - **随附 fork 工具不公开子级 LLM 路由选择**——它们继承父级提供方与模型,使复制的历史仍有资格复用 KV Cache。在某项改动能保留复用或公开有界重算成本前,路由选择保持禁用;[模型选择路由 Agent Note](../../../.agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.zh.md)说明这项限制。
 
 <a id="dev-note"></a>

+ 0 - 6
packages/subagent/subagent-fork-in-process/src/index.ts

@@ -81,12 +81,6 @@ class ForkInProcessProvider implements SubagentProvider {
     })
   }
 
-  // TODO(fork-continuable-prefix-reuse): CLI presets call this and accept that
-  // a continuable child's `report` tool and prompt section precede the inherited
-  // history, defeating the prefix reuse a fork exists for. Cache-preserving
-  // continuable fork needs byte-identical child system prompt and tool schemas;
-  // see issue #2124 and
-  // .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md.
   prepareContinuable(request: ContinuableCreateRequest): Promise<ContinuableCreateSpec> {
     // The fork prefix is captured ONCE, at creation: it becomes part of the
     // child's own durable transcript, so a later cold resume replays that

+ 2 - 2
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: f13de661015f3aa59b776273376f07653f6bbba5
-README.zh.md: 42923504ed11b8eff7c0bdcd9d0c9505d741f42e
+README.md: 257a76a4ef84583d3337f697bc8024d72219c4d6
+README.zh.md: 135086b982df38089cdef0acf85535d35354a9d7

+ 12 - 11
packages/subagent/subagent/README.md

@@ -44,11 +44,11 @@ An agent that calls the tool gets the child's final answer as the tool result. M
 
 ### One-shot and continuable children
 
-One-shot children run once and settle with a single result, plus an optional structured output and a safe diagnostic on failure. A start request may override the child Agent's provider, model, reasoning effort, and output-token limit through `agentOptions`; every requested option requires the provider's matching capability. Continuable children keep a durable session and accept later messages in order: the caller receives a stable child id, sends follow-ups, and can interrupt the current turn without destroying the child. The tool row's `backgroundMode` picks the shape (`one-shot` by default, or `continuable` on providers that support it).
+One-shot children run once and settle with a single result, plus an optional structured output and a safe diagnostic on failure. A start request may override the child Agent's provider, model, reasoning effort, and output-token limit through `agentOptions`; every requested option requires the provider's matching capability. Continuable children keep a durable session and accept later messages in order: the caller receives a stable child id, sends adjacent-Agent messages, and can interrupt the current turn without destroying the child. The tool row's `backgroundMode` picks the shape (`one-shot` by default, or `continuable` on providers that support it).
 
-### Following up, interrupting, and discovering
+### Messaging, interrupting, and discovering
 
-Continuable children answer follow-up messages as their next turns, and the parent can interrupt a running turn or list its children at any time. A browser continuation prompt may carry image parts: the Host admits and persists each image batch through the attachment store before the child inbox accepts the message, and refuses delivery when the child's declared model does not accept image input. Discovery covers both shapes: the service lists direct children and the full descendant tree — mode, activity, and lineage — reading live session state and optional persistence, without loading any child.
+Every exact live Agent can use `sendMessage()` with a direct continuable child; a resident continuable child can also use it with its direct parent. A working target receives the message through Steer at its nearest step; an idle target starts a turn, and only a direct child can be cold-resumed. The parent can also interrupt a running descendant or list its children at any time. A browser continuation prompt may carry image parts: the Host admits and persists each image batch through the attachment store before the child inbox accepts the message, and refuses delivery when the child's declared model does not accept image input. Discovery covers both shapes: the service lists direct children and the full descendant tree — mode, activity, and lineage — reading live session state and optional persistence, without loading any child.
 
 ### Failure and recovery
 
@@ -76,7 +76,8 @@ This section explains how the service is built and where the observable behavior
 | File | Role |
 |---|---|
 | [`src/index.ts`](src/index.ts) | Service entry: provider registry, start and continuation API, lifecycle events |
-| [`src/continuation.ts`](src/continuation.ts) | Continuable children: identity reservation, Activation residency, follow-up, interrupt, settlement |
+| [`src/continuation.ts`](src/continuation.ts) | Continuable children: identity reservation, Activation residency, adjacent messaging, interrupt, settlement |
+| [`src/internal.ts`](src/internal.ts) | Host-only Queue adapter for browser and Team message protocols |
 | [`src/types.ts`](src/types.ts) | Public request, result, and provider contracts |
 | [`src/descriptor.ts`](src/descriptor.ts) | Versioned `subagent/descriptor` session-event vocabulary |
 | [`src/child-agent.ts`](src/child-agent.ts) | Child composition, delegated policy, depth helpers |
@@ -90,13 +91,13 @@ A request is validated against the provider's advertised capabilities, a durable
 
 ### Continuable flow
 
-The manager reserves a child identity, resolves the durable descriptor, creates (or cold-resumes) the child Agent, installs it in an Activation, and submits the prompt. Later messages become FIFO turns through the child's own inbox; an absent Activation cold-resumes from the persisted session. When a resident Activation settles, the manager tells the child's direct parent in the parent's own turn stream.
+The manager reserves a child identity, resolves the durable descriptor, creates (or cold-resumes) the child Agent, installs it in an Activation, and submits the prompt. Model-authored messages cross one parent/child edge through fixed Steer scheduling; host protocols retain an internal Queue adapter for distinct turns. An absent direct-child Activation cold-resumes from the persisted session. When a resident Activation settles, the manager tells the child's direct parent in the parent's own turn stream.
 
 ### Ownership and invariants
 
 - **Publication is the boundary** — before it the provider owns the setup and must roll back on failure; after it the caller owns the run and must dispose it.
 - **Registration is effect-scoped** — removing a provider blocks new starts but never revokes accepted runs.
-- **Continuation authority is exact identity** — follow-ups require the exact live direct parent; reports require the exact live child.
+- **Agent-message authority is exact adjacency** — `sendMessage()` requires the exact live sender; every sender may target a direct continuable child, while only a sender with a resident continuable Activation may target its direct parent.
 - **The descriptor is log-only** — a session event absent from model history and retained across compaction; a continuable descriptor records the resolved child provider, model, and reasoning effort explicitly for cold resume.
 
 </details>
@@ -124,11 +125,11 @@ Read these pages when the package-level contract is not enough. They move from t
 
 #### What the model sees
 
-One user-role parent message opening with the outcome — `Background subagent <child-id> finished and will do no further work unless you send it more.`, or the matching line for a child that was stopped, ran out of room, declined, or failed — followed by `Its closing message:` and the child's final assistant content, or `It left no closing message.` when it produced none. This is the service's only direct parent-side contribution; delegation schemas, parent continuation and discovery, and the child-scoped `report` belong to `dsh-tool-subagent`, `dsh-tool-subagent-control`, and `dsh-tool-subagent-report`.
+One user-role parent message opening with the outcome — `Background subagent <child-id> finished and will do no further work unless you send it more.`, or the matching line for a child that was stopped, ran out of room, declined, or failed — followed by `Its closing message:` and the child's final assistant content, or `It left no closing message.` when it produced none. This runtime-owned notice is distinct from model-authored parent/child messages, which use `sendMessage()` and `AgentMessageSource`; delegation schemas and model controls belong to the Consumer packages.
 
 #### Token effect
 
-One notice per settled Activation in the parent's request, sized by the child's final message. A child that both reports and settles costs the parent both.
+One notice per settled Activation in the parent's request, sized by the child's final message. A child that sends its own message and then settles costs the parent both.
 
 #### KV Cache effect
 
@@ -162,12 +163,12 @@ Prefix-stable within a child: the statement never changes during the child's lif
 These limits define when the seam is a poor fit or needs special operational care. They are current package constraints, not a general delegation comparison or a task backlog.
 
 - **ACP children remain one-shot and are not trace-enumerable** — an ACP run has no local child session in the parent's session corpus, and remote providers need an Activation ownership contract before they can support continuable children.
-- **No host-user continuation** — `followup()` requires the exact live direct parent; only `interrupt()` accepts a durable human parent address.
-- **Continuation messages never steer** — parent-to-child follow-ups enqueue later turns; they never redirect the child's current turn.
+- **Adjacent model messaging only** — `sendMessage()` requires an exact live sender; every sender may target a direct continuable child, while only a sender with a resident continuable Activation may target its direct parent. Browser prompts use the separate Queue control path.
+- **A direct parent must remain live for child-to-parent delivery** — the service has no durable parent mailbox; a missing parent rejects the message instead of accepting work it cannot wake.
 - **Wake gap during cancellation convergence** — a follow-up accepted after an interrupt signal but before the driver becomes idle stays queued until another waking send.
 - **Process-local residency** — the Activation inbox and ownership graph do not coordinate two harness processes; concurrent access to one persistence store needs a durable mailbox and cross-process lease protocol.
 - **No replay of accepted-but-unlogged messages** — a crash can lose an accepted prompt that never reached the child's session log; the lost message is not replayed automatically.
-- **No durable report mailbox** — reports require a live direct parent and provide acceptance identity rather than exactly-once delivery.
+- **No durable parent mailbox** — child-to-parent messages require a resident continuable child and live direct parent, and provide acceptance identity rather than exactly-once delivery.
 - **Lifecycle events are observe-only** — a run-affecting `subagent/end` continuation or decision API waits for a concrete consumer.
 
 <a id="dev-note"></a>

+ 12 - 11
packages/subagent/subagent/README.zh.md

@@ -44,11 +44,11 @@ kind: "package-reference"
 
 ### 一次性与可继续子级
 
-一次性子 agent 只运行一次,并以单个结果结算,可附带可选的结构化输出与失败时的安全诊断。启动请求可以通过 `agentOptions` 覆盖子 Agent 的提供方、模型、推理等级与输出 token 上限;每个请求的选项都要求提供方声明对应能力。可继续子 agent 保留持久会话并按顺序接受后续消息:调用方收到稳定的子 agent id、发送后续消息,并可中断当前轮次而不销毁子 agent。工具行的 `backgroundMode` 选择形态(默认 `one-shot`,或在支持的提供方上使用 `continuable`)。
+一次性子 agent 只运行一次,并以单个结果结算,可附带可选的结构化输出与失败时的安全诊断。启动请求可以通过 `agentOptions` 覆盖子 Agent 的提供方、模型、推理等级与输出 token 上限;每个请求的选项都要求提供方声明对应能力。可继续子 agent 保留持久会话并按顺序接受后续消息:调用方收到稳定的子 agent id、发送相邻 Agent 消息,并可中断当前轮次而不销毁子 agent。工具行的 `backgroundMode` 选择形态(默认 `one-shot`,或在支持的提供方上使用 `continuable`)。
 
-### 后续消息、中断与发现
+### 消息、中断与发现
 
-可继续子 agent 把后续消息作为下一个轮次回答,父级随时可以中断运行中的轮次或列举自己的子级。浏览器发出的继续执行 prompt 可以携带图片部分:Host 先通过附件存储完成整批图片的准入与持久化,子级 inbox 才接受这条消息;当子级声明的模型不接受图片输入时拒绝投递。发现覆盖两种形态:服务列举直接子级与完整后代树——模式、活动状态与血缘——直接读取在线会话状态与可选持久化,不加载任何子 agent。
+每个确切在线 Agent 都可以对直接可继续 child 使用 `sendMessage()`;驻留的可继续 child 还可以对自己的直接 parent 使用它。正在工作的目标通过 Steer 在最近 step 接收消息;空闲目标启动轮次,且只有直接 child 可以冷恢复。parent 也可以随时中断正在运行的后代或列举自己的子级。浏览器发出的继续执行 prompt 可以携带图片部分:Host 先通过附件存储完成整批图片的准入与持久化,子级 inbox 才接受这条消息;当子级声明的模型不接受图片输入时拒绝投递。发现覆盖两种形态:服务列举直接子级与完整后代树——模式、活动状态与血缘——直接读取在线会话状态与可选持久化,不加载任何子 agent。
 
 ### 失败与恢复
 
@@ -76,7 +76,8 @@ kind: "package-reference"
 | 文件 | 职责 |
 |---|---|
 | [`src/index.ts`](src/index.ts) | 服务入口:提供方注册表、启动与继续 API、生命周期事件 |
-| [`src/continuation.ts`](src/continuation.ts) | 可继续子级:身份预留、Activation 驻留、后续消息、中断、结算 |
+| [`src/continuation.ts`](src/continuation.ts) | 可继续子级:身份预留、Activation 驻留、相邻消息、中断、结算 |
+| [`src/internal.ts`](src/internal.ts) | 供浏览器与 Team 消息协议使用的 host-only Queue 适配器 |
 | [`src/types.ts`](src/types.ts) | 公开的请求、结果与提供方约定 |
 | [`src/descriptor.ts`](src/descriptor.ts) | 版本化的 `subagent/descriptor` 会话事件词汇 |
 | [`src/child-agent.ts`](src/child-agent.ts) | 子级组装、委派策略、深度辅助函数 |
@@ -90,13 +91,13 @@ kind: "package-reference"
 
 ### 可继续流程
 
-管理器预留子 agent 身份、解析持久化描述符、创建(或冷恢复)子 agent、把它安装进 Activation 并提交提示词。后续消息经子 agent 自己的 inbox 成为 FIFO 轮次;没有 Activation 时从持久化会话冷恢复。当驻留 Activation 结算时,管理器会在父级自身的轮次流中告知该子级的直接父级
+管理器预留 child 身份、解析持久化描述符、创建(或冷恢复)child、把它安装进 Activation 并提交提示词。模型编写的消息通过固定 Steer 调度跨一条 parent/child 边;host 协议保留内部 Queue 适配器以创建独立轮次。直接 child 不存在 Activation 时会从持久化会话冷恢复。当驻留 Activation 结算时,管理器会在 parent 自身的轮次流中告知该 child 的直接 parent
 
 ### 所有权与不变式
 
 - **发布即边界**——发布前提供方拥有设置并须在失败时回滚;发布后调用方拥有运行并须 dispose(资源释放)它。
 - **注册受 effect 作用域约束**——移除提供方会阻止新启动,但绝不撤销已接受的运行。
-- **继续执行权限基于确切身份**——后续消息要求确切在线直接父级;上报要求确切在线子级
+- **Agent 消息权限基于确切相邻关系**——`sendMessage()` 要求确切在线 sender;每个 sender 都可以指定直接可继续 child,只有具备驻留可继续 Activation 的 sender 可以指定自己的直接 parent
 - **描述符仅进日志**——它是会话事件,不进入模型历史,并跨压缩(compaction)保留;可继续描述符会显式记录解析后的子级提供方、模型与推理等级,用于冷恢复。
 
 </details>
@@ -124,11 +125,11 @@ kind: "package-reference"
 
 #### 模型看到什么
 
-一条用户角色的父级消息,开头是结果本身——`Background subagent <child-id> finished and will do no further work unless you send it more.`,或子级被停止、耗尽额度、拒绝任务或失败时的对应句子——随后是 `Its closing message:` 与子级的最终 assistant 内容;若子级没有产出内容,则是 `It left no closing message.`。这是本服务面向父级的唯一直接贡献;委派 schema、父级延续与发现以及子级作用域的 `report` 分别归 `dsh-tool-subagent`、`dsh-tool-subagent-control` 和 `dsh-tool-subagent-report` 所有。
+一条用户角色的父级消息,开头是结果本身——`Background subagent <child-id> finished and will do no further work unless you send it more.`,或子级被停止、耗尽额度、拒绝任务或失败时的对应句子——随后是 `Its closing message:` 与子级的最终 assistant 内容;若子级没有产出内容,则是 `It left no closing message.`。这条由 runtime 生成的通知与模型编写的父子消息相互独立;后者使用 `sendMessage()` 与 `AgentMessageSource`。委派 schema 与模型控制工具归 Consumer 包所有。
 
 #### Token 影响
 
-父级请求中,每个已结算的 Activation 一条通知,长度取决于子级的最终消息。如果子级既上报又结算,父级请求会同时承担两者。
+父级请求中,每个已结算的 Activation 一条通知,长度取决于子级的最终消息。如果子级先发送自己的消息再结算,父级请求会同时承担两者。
 
 #### KV Cache 影响
 
@@ -162,12 +163,12 @@ You are a delegated subagent: your permission scope was fixed when you were star
 这些限制说明该 seam 何时不合适,或何时需要特别的运维注意。它们是当前包约束,不是通用委派对比或任务积压。
 
 - **ACP 子级仍为一次性,且无法通过追踪枚举**——ACP 运行在父级会话语料中没有本地子会话,远程提供方需要 Activation 所有权约定才能支持可继续子级。
-- **无 host-user 继续执行**——`followup()` 要求确切在线直接父级;只有 `interrupt()` 接受持久化的人类父级地址
-- **继续执行消息绝不 steering(中途引导)**——父到子的后续消息排入后续轮次;它们绝不会重定向子级当前轮次
+- **仅允许相邻模型消息**——`sendMessage()` 要求确切在线 sender;每个 sender 都可以指定直接可继续 child,只有具备驻留可继续 Activation 的 sender 可以指定自己的直接 parent。浏览器提示使用独立的 Queue 控制路径
+- **child 到 parent 的投递要求直接 parent 保持在线**——服务没有持久 parent mailbox;parent 缺失时会拒绝消息,而非接受无法唤醒的工作
 - **取消收敛期间存在唤醒缺口**——中断信号发出后、driver 进入 idle 前被接受的后续消息会保持排队,直到另一条唤醒发送到达。
 - **驻留仅限进程内**——Activation inbox 与所有权图不会在两个 harness 进程之间协调;对单个持久化存储的并发访问需要持久化邮箱与跨进程租约协议。
 - **不回放已接受但未记录的消息**——崩溃可能丢失从未写入子会话日志、已被接受的提示词;丢失的消息不会自动回放。
-- **没有持久化的上报 mailbox**——上报需要在线直接父级,提供的是接受标识,不保证恰好一次投递。
+- **没有持久化 parent mailbox**——child 到 parent 的消息要求驻留的可继续 child 与在线直接 parent,提供的是接受标识,不保证恰好一次投递。
 - **生命周期事件只供观察**——影响运行的 `subagent/end` 延续或决策接口仍需等待具体消费方。
 
 <a id="dev-note"></a>

+ 4 - 0
packages/subagent/subagent/package.json

@@ -18,6 +18,10 @@
       "types": "./lib/types/index.d.ts",
       "default": "./lib/index.js"
     },
+    "./internal": {
+      "types": "./lib/types/internal.d.ts",
+      "default": "./lib/types/internal.js"
+    },
     "./invariant": {
       "types": "./lib/types/invariant.d.ts",
       "default": "./lib/invariant.js"

+ 0 - 185
packages/subagent/subagent/src/activation-setup-registry.ts

@@ -1,185 +0,0 @@
-/**
- * Internal registry of deployment capabilities composed into every continuable
- * child's unpublished creation context.
- *
- * A contribution grants a child-scoped capability without teaching the
- * continuation manager which capabilities exist. The manager owns residency;
- * this registry owns the join between plugin lifetime, unpublished setup, and
- * Activation disposal, so no installation outlives either owner and no removed
- * contribution can be installed after revocation reports completion.
- *
- * @module @deepseek-ai/dsh-subagent/activation-setup-registry
- */
-
-import type { Context } from '@deepseek-ai/cordis'
-import type { AgentSetupCommit } from '@deepseek-ai/dsh-agent'
-import { errorChain } from '@deepseek-ai/dsh-llm'
-import { SubagentError } from './error.ts'
-
-/**
- * One deployment capability installed into a continuable child's unpublished
- * creation context. It composes synchronously before publication and returns
- * the disposer for exactly that installation.
- * @param childCtx - the child's unpublished scoped context.
- * @returns the disposer revoking this installation.
- */
-export type ContinuableSetupContribution = (childCtx: Context) => () => void
-
-/** One contribution's live registration. */
-interface Registration {
-  readonly contribution: ContinuableSetupContribution
-  removed: boolean
-  readonly installations: Set<Installation>
-}
-
-/** One contribution installed into one child context. */
-interface Installation {
-  readonly registration: Registration
-  readonly childCtx: Context
-  readonly dispose: () => void
-  released: boolean
-  /** Present until the child reaches residency. */
-  transaction: TransactionState | undefined
-}
-
-/** One child's provisioning batch. */
-interface TransactionState {
-  readonly installations: Installation[]
-  invalidated: boolean
-}
-
-/** Re-read mutable removal state after a contribution may have revoked itself. */
-function isRemoved(registration: Registration): boolean {
-  return registration.removed
-}
-
-/**
- * Owns continuable-child setup registrations, installations, rollback, child
- * cleanup, and immediate live revocation.
- */
-export class SubagentActivationSetupRegistry {
-  /** Live contributions in installation order. */
-  private readonly registrations = new Set<Registration>()
-  /** Child context to its live installations. */
-  private readonly byChild = new Map<Context, Set<Installation>>()
-
-  /**
-   * Register one contribution.
-   * @param contribution - synchronous child-scope installer.
-   * @returns an idempotent registration undo.
-   * @throws after attempting every installation when any disposer fails.
-   */
-  register(contribution: ContinuableSetupContribution): () => void {
-    const registration: Registration = { contribution, removed: false, installations: new Set() }
-    this.registrations.add(registration)
-    return () => {
-      if (registration.removed) return
-      // Close before disposal so a snapshotted apply() cannot install after
-      // revocation reports completion.
-      registration.removed = true
-      this.registrations.delete(registration)
-      this.releaseAll([...registration.installations], 'contribution removal')
-    }
-  }
-
-  /**
-   * Install every live contribution into one unpublished child context.
-   * @param childCtx - the child's unpublished scoped context.
-   * @returns the provisioning commit consumed at Agent publication.
-   */
-  apply(childCtx: Context): AgentSetupCommit {
-    const state: TransactionState = { installations: [], invalidated: false }
-    try {
-      for (const registration of [...this.registrations]) {
-        /* v8 ignore next -- only a synchronous re-entrant revocation of an
-         * already-snapshotted registration reaches this guard. */
-        if (registration.removed) continue
-        const installation: Installation = {
-          registration,
-          childCtx,
-          dispose: registration.contribution(childCtx),
-          released: false,
-          transaction: state,
-        }
-        registration.installations.add(installation)
-        state.installations.push(installation)
-        let indexed = this.byChild.get(childCtx)
-        if (indexed === undefined) {
-          indexed = new Set()
-          this.byChild.set(childCtx, indexed)
-        }
-        indexed.add(installation)
-        // An installer may revoke itself before its installation record exists.
-        // Dispose that escaped record and invalidate the provisioning batch.
-        if (isRemoved(registration)) this.release(installation)
-      }
-    } catch (error: unknown) {
-      // Keep the installer failure authoritative, but attempt every rollback.
-      try {
-        this.releaseAll([...state.installations], 'setup rollback')
-      } catch (releaseFailure: unknown) {
-        /* v8 ignore next -- requires independent installer and rollback faults. */
-        void releaseFailure
-      }
-      throw error
-    }
-    childCtx.effect(() => () => { this.releaseChild(childCtx) }, 'subagents.activationSetup()')
-    return {
-      commit: () => {
-        if (state.invalidated) {
-          throw new SubagentError(
-            'a continuable-subagent setup contribution was revoked while this child was being built; '
-            + 'the child was not established',
-            'ACTIVATION_SETUP_REVOKED',
-          )
-        }
-        for (const installation of state.installations) installation.transaction = undefined
-      },
-    }
-  }
-
-  /** Release every remaining installation owned by one disposed child scope. */
-  private releaseChild(childCtx: Context): void {
-    const indexed = this.byChild.get(childCtx) ?? []
-    this.releaseAll([...indexed], 'child scope disposal')
-  }
-
-  /**
-   * Release a batch completely before reporting disposer failures.
-   * @param installations - records to release.
-   * @param during - operation name for diagnostics.
-   */
-  private releaseAll(installations: readonly Installation[], during: string): void {
-    const failures: unknown[] = []
-    for (const installation of installations) {
-      try {
-        this.release(installation)
-      } catch (error: unknown) {
-        failures.push(error)
-      }
-    }
-    if (failures.length === 0) return
-    throw new SubagentError(
-      `continuable-subagent setup ${during} failed to release ${failures.length} installation(s): `
-      + failures.map(failure => errorChain(failure)).join('; '),
-      'ACTIVATION_SETUP_RELEASE_FAILED',
-    )
-  }
-
-  /** Drop one installation from both indices and dispose it exactly once. */
-  private release(installation: Installation): void {
-    if (installation.released) return
-    installation.released = true
-    installation.registration.installations.delete(installation)
-    const indexed = this.byChild.get(installation.childCtx)
-    /* v8 ignore next 4 -- every live installation is indexed until this method removes it. */
-    if (indexed !== undefined) {
-      indexed.delete(installation)
-      if (indexed.size === 0) this.byChild.delete(installation.childCtx)
-    }
-    if (installation.transaction !== undefined) installation.transaction.invalidated = true
-    installation.dispose()
-  }
-}
-
-export default SubagentActivationSetupRegistry

+ 153 - 151
packages/subagent/subagent/src/continuation.ts

@@ -28,7 +28,6 @@ import type {
   Agent,
   AgentHandle,
   AgentOptions,
-  AgentSetupCommit,
   CreateAgentOptions,
 } from '@deepseek-ai/dsh-agent'
 import { ReasoningEffortId, boundContextSummary, contentHasImage, createUserMessage, errorChain } from '@deepseek-ai/dsh-llm'
@@ -53,30 +52,21 @@ import { seedDescriptorTurn } from './descriptor-seed.ts'
 import type { ContinuableCreateRequest, ContinuableCreateSpec, SubagentResult, SubagentStartRequest } from './types.ts'
 import type { ActivationObserver, ActivationTerminal } from './lifecycle.ts'
 import { SubagentError } from './error.ts'
-import type SubagentActivationSetupRegistry from './activation-setup-registry.ts'
+import { isAdjacentAgentSendMessageTool } from './internal.ts'
 
-/** Attribution for a model coordinator's follow-up to one of its children. */
-export interface CoordinatorMessageSource {
-  readonly kind: 'coordinator'
+/** Durable attribution for one model-authored message between adjacent Agents. */
+export interface AgentMessageSource {
+  readonly kind: 'agent-message'
   /** A message another agent addressed to this one (`relay` context form). */
   readonly form: 'relay'
-  /** Session id of the agent whose tool call produced the follow-up. */
-  readonly senderSessionId: SessionId
-}
-
-/** Durable attribution for a continuable child's explicit parent report. */
-export interface SubagentReportMessageSource {
-  readonly kind: 'subagent-report'
-  /** A message another agent addressed to this one (`relay` context form). */
-  readonly form: 'relay'
-  /** Session id of the reporting child. */
+  /** Session id of the Agent whose tool call produced the message. */
   readonly senderSessionId: SessionId
 }
 
 /**
  * Durable attribution for the runtime's own account of a continuable child
  * settling. Deliberately a different kind from
- * {@link SubagentReportMessageSource}: a report is content the child chose,
+ * {@link AgentMessageSource}: an Agent message is content the sender chose,
  * while this message is the manager stating what became of the child, and a
  * transcript that merged them would credit the child with words it never wrote.
  */
@@ -92,23 +82,11 @@ export interface SubagentSettledMessageSource {
 
 declare module '@deepseek-ai/dsh-llm' {
   interface MessageSourceMap {
-    coordinator: CoordinatorMessageSource
-    'subagent-report': SubagentReportMessageSource
+    'agent-message': AgentMessageSource
     'subagent-settled': SubagentSettledMessageSource
   }
 }
 
-/** Deployment scheduling policy for accepted child reports. */
-export type SubagentReportDelivery = 'quiet' | 'next-step'
-
-/** Options for one continuable child's report to its direct parent. */
-export interface SubagentReportOptions {
-  /** Already-resolved parent scheduling policy. */
-  readonly delivery: SubagentReportDelivery
-  /** Caller cancellation, owning authorization and admission until acceptance. */
-  readonly signal: AbortSignal
-}
-
 /** What a caller asks for when starting a continuable background child. */
 export interface ContinuableStartSpec {
   /** The `ctx.subagents` provider whose continuable-creation capability establishes the child. */
@@ -147,14 +125,17 @@ export type SubagentInterruptAuthority =
   | { readonly kind: 'user'; readonly parentSessionId: SessionId }
   | { readonly kind: 'ancestor'; readonly agent: Agent }
 
-/** Options for following up with one continuable child. */
-export interface SubagentFollowupOptions {
-  /** Durable attribution retained on the delivered message; it grants no authority. */
-  readonly source: MessageSource
+/** Options for one model-authored message between adjacent Agents. */
+export interface SubagentSendMessageOptions {
   /** Caller cancellation, owning the operation only until inbox acceptance. */
   readonly signal: AbortSignal
 }
 
+/** Inputs shared by model steering and the human Queue adapter. */
+type ChildDeliveryOptions =
+  | { readonly delivery: 'steer'; readonly signal: AbortSignal }
+  | { readonly delivery: 'queue'; readonly source: MessageSource; readonly signal: AbortSignal }
+
 /**
  * The residency state of one continuable child, derived from Agent quiescence
  * and the owned-child set rather than a second state machine:
@@ -232,7 +213,7 @@ interface Activation {
   disposal: Promise<void> | undefined
   /**
    * Accepted waking message ids this manager has not yet seen leave the inbox.
-   * `Agent.status` is still `idle` in the window between `followup()` and the
+   * `Agent.status` is still `idle` in the window between a waking send and the
    * microtask that admits it, so settlement must not treat that gap as quiet.
    */
   readonly accepted: Set<MessageId>
@@ -288,6 +269,42 @@ function disposalOf(activation: Activation): Promise<void> | undefined {
   return activation.disposal
 }
 
+/** Build durable attribution for one adjacent-Agent message. */
+function agentMessageSource(sender: Agent): AgentMessageSource {
+  return {
+    kind: 'agent-message',
+    form: 'relay',
+    senderSessionId: sender.id,
+  }
+}
+
+/** Build the model-visible and durable representation of one adjacent-Agent message. */
+function agentMessage(sender: Agent, content: ContentBlock[]) {
+  return createUserMessage({
+    content: [
+      { type: 'text' as const, text: `Agent ${sender.id} sent a message:` },
+      ...content,
+    ],
+    source: agentMessageSource(sender),
+  })
+}
+
+/** Append adjacent-Agent return guidance to a continuable child's initial task. */
+function continuableInitialPrompt(parentId: SessionId, prompt: ContentBlock[]): ContentBlock[] {
+  const encodedParentId = JSON.stringify(parentId)
+  return [
+    ...prompt,
+    {
+      type: 'text',
+      text: `Your parent agent id is ${encodedParentId}. Before you finish, send your result to that agent with `
+        + `send_message({ agent_id: ${encodedParentId}, message: "<self-contained result>" }). The parent shares `
+        + 'your workspace but does not automatically receive your transcript, tool output, or reasoning. Send '
+        + 'earlier messages as well when a finding changes what the parent should do next; sending a message '
+        + 'does not end your turn.',
+    },
+  ]
+}
+
 /**
  * One line telling a parent that a background child is finished and why, in
  * the parent's own task vocabulary.
@@ -373,7 +390,6 @@ export class SubagentContinuationManager {
   constructor(
     private readonly ctx: Context,
     private readonly host: ContinuationHost,
-    private readonly setupRegistry: SubagentActivationSetupRegistry,
   ) {
     // Ordinary Cordis owner effects unwind in reverse registration order, which
     // cannot express the dynamic child graph. Register the private scope's
@@ -470,10 +486,11 @@ export class SubagentContinuationManager {
       })
       return this.submitMaterialized(
         activation,
-        request.prompt,
-        { kind: 'user' },
+        isAdjacentAgentSendMessageTool(this.ctx.get('tools')?.get('send_message', activation.handle.agent))
+          ? continuableInitialPrompt(parent.id, request.prompt)
+          : request.prompt,
+        { source: { kind: 'user' }, signal: spec.signal, delivery: 'queue' },
         parent,
-        spec.signal,
       )
     })
     return { childId, messageId }
@@ -487,27 +504,75 @@ export class SubagentContinuationManager {
   }
 
   /**
-   * Deliver one later message to a known continuable child as its next FIFO
-   * turn. Routing depends only on Activation residency: a `running` Activation
-   * enqueues, a `waiting` one wakes the same Agent, and an absent one
-   * cold-resumes a new Activation from the persisted Session. The Agent inbox
-   * is the only queue, so every accepted message has one observable order.
-   *
-   * The caller signal owns lookup, materialization, and admission only until
-   * inbox acceptance; afterwards the accepted turn cannot be cancelled through
-   * this service.
-   * @param parent - the exact live direct parent authorizing this delivery.
-   * @param childId - the durable child session id.
-   * @param content - the user-role content to deliver.
-   * @param options - the message source fields and caller cancellation.
+   * Deliver one model-authored message to a direct continuable child or to the
+   * sender's direct parent. Both directions use Steer: a running target admits
+   * the message at its nearest step boundary, while an idle target starts a
+   * turn. A missing direct child cold-resumes through the ordinary continuation
+   * lifecycle. The caller signal owns the operation only until inbox acceptance.
+   * @param sender - exact live Agent authorizing and originating the message.
+   * @param targetId - durable direct-parent or direct-child session id.
+   * @param content - model-authored content to deliver.
+   * @param options - caller cancellation before acceptance.
+   * @returns the accepted message's inbox id.
+   * @throws when adjacency, availability, or admission rejects delivery.
+   */
+  async sendMessage(
+    sender: Agent,
+    targetId: SessionId,
+    content: ContentBlock[],
+    options: SubagentSendMessageOptions,
+  ): Promise<MessageId> {
+    if (this.ctx.agents.get(sender.id) !== sender) {
+      throw new SubagentError(
+        'message delivery requires the exact live sender agent',
+        'UNAUTHORIZED',
+      )
+    }
+    this.assertAdmitting(sender)
+    const senderActivation = this.activations.get(sender.id)
+    if (senderActivation !== undefined
+      && senderActivation.handle.agent === sender
+      && senderActivation.parentSession === targetId) {
+      options.signal.throwIfAborted()
+      return this.sendToParent(senderActivation, sender, content)
+    }
+    if (sender.session.header.parentSession === targetId) {
+      throw new SubagentError(
+        `agent "${sender.id}" is not a resident continuable child and cannot send to parent "${targetId}"`,
+        'UNAUTHORIZED',
+      )
+    }
+    return this.deliverToChild(sender, targetId, content, {
+      signal: options.signal,
+      delivery: 'steer',
+    })
+  }
+
+  /**
+   * Queue one human-authored prompt as a distinct direct-child turn.
+   * @param parent - exact live direct parent authorizing delivery.
+   * @param childId - durable direct-child session id.
+   * @param content - human-authored content to deliver.
+   * @param source - durable host-protocol provenance.
+   * @param signal - caller cancellation before inbox acceptance.
    * @returns the accepted message's inbox id.
-   * @throws when parent authority, availability, or admission rejects the delivery.
    */
-  async followup(
+  async queuePrompt(
     parent: Agent,
     childId: SessionId,
     content: ContentBlock[],
-    options: SubagentFollowupOptions,
+    source: MessageSource,
+    signal: AbortSignal,
+  ): Promise<MessageId> {
+    return this.deliverToChild(parent, childId, content, { source, signal, delivery: 'queue' })
+  }
+
+  /** Route one parent-originated delivery through residency and cold resume. */
+  private async deliverToChild(
+    parent: Agent,
+    childId: SessionId,
+    content: ContentBlock[],
+    options: ChildDeliveryOptions,
   ): Promise<MessageId> {
     this.assertAdmitting(parent)
     while (true) {
@@ -536,7 +601,7 @@ export class SubagentContinuationManager {
             return undefined
           }
         }
-        return this.submitAdmitted(activation, content, options.source, parent, options.signal)
+        return this.submitAdmitted(activation, content, options, parent)
       })
       /* v8 ignore start -- only the lost-cutoff arm above returns undefined, so only that
        * race reaches the retry below, which then cold-resumes a new Activation. */
@@ -610,88 +675,29 @@ export class SubagentContinuationManager {
     )
   }
 
-  /**
-   * Deliver explicitly selected content from one resident continuable child to
-   * its durable direct parent. Sender authorization, parent resolution, and
-   * send acceptance share one no-await span. Reporting neither concludes the
-   * child's turn nor changes its Activation lifetime.
-   * @param child - exact live reporting child; this is the authority credential.
-   * @param content - selected model-facing content.
-   * @param options - scheduling policy and pre-acceptance cancellation.
-   * @returns the stable identity of the message accepted by the parent.
-   * @throws {SubagentError} when the sender is unauthorized, the parent is not
-   *   live, or continuation admission is closing.
-   */
-  // oxlint-disable-next-line typescript/require-await -- keep rejection semantics without yielding during admission
-  async reportFrom(
-    child: Agent,
+  /** Deliver one resident continuable child's message to its live direct parent. */
+  private sendToParent(
+    activation: Activation,
+    sender: Agent,
     content: ContentBlock[],
-    options: SubagentReportOptions,
-  ): Promise<MessageId> {
-    options.signal.throwIfAborted()
-    this.assertAdmitting(child)
-    const activation = this.authorizeReporter(child)
-    const parent = this.resolveReportParent(child)
-    return this.deliverReport(activation, parent, content, options.delivery)
-  }
-
-  /** Authorize only the exact Agent of one resident Activation. */
-  private authorizeReporter(child: Agent): Activation {
-    const activation = this.activations.get(child.id)
-    if (activation === undefined || activation.handle.agent !== child) {
-      throw new SubagentError(
-        `agent "${child.id}" is not a live continuable subagent and cannot report`,
-        'UNAUTHORIZED',
-      )
-    }
-    /* v8 ignore next 6 -- only a synchronous re-entrant disposer can open this
-     * transaction between exact-agent authorization and this no-await cutoff. */
+  ): MessageId {
+    /* v8 ignore next 6 -- only synchronous re-entrant teardown can open this
+     * transaction between exact-agent authorization and this no-await span. */
     if (activation.disposal !== undefined) {
       throw new SubagentError(
-        `subagent "${child.id}" activation is being disposed; the report was not delivered`,
+        `subagent "${sender.id}" activation is being disposed; the message was not delivered`,
         'ACTIVATION_CLOSING',
       )
     }
-    return activation
-  }
-
-  /** Resolve the reporting child's live direct parent from durable lineage. */
-  private resolveReportParent(child: Agent): Agent {
-    const parentId = child.session.header.parentSession
-    /* v8 ignore next -- every continuation-managed child has direct-parent metadata. */
-    const parent = parentId === undefined ? undefined : this.ctx.agents.get(parentId)
+    const parent = this.ctx.agents.get(activation.parentSession)
     if (parent === undefined) {
       throw new SubagentError(
-        'direct parent is not live; report was not delivered',
+        'direct parent is not live; the message was not delivered',
         'PARENT_UNAVAILABLE',
       )
     }
-    return parent
-  }
-
-  /** Deliver one framed report through the selected parent scheduling preset. */
-  private deliverReport(
-    activation: Activation,
-    parent: Agent,
-    content: ContentBlock[],
-    delivery: SubagentReportDelivery,
-  ): MessageId {
-    const message = createUserMessage({
-      content: [
-        { type: 'text' as const, text: `Background subagent ${activation.childId} reported:` },
-        ...content,
-      ],
-      source: {
-        kind: 'subagent-report' as const,
-        form: 'relay' as const,
-        senderSessionId: activation.childId,
-      },
-    })
-    if (delivery === 'next-step') {
-      this.sendWaking(parent, message, () => { this.sendReport(parent, message, delivery) })
-    } else {
-      this.sendReport(parent, message, delivery)
-    }
+    const message = agentMessage(sender, content)
+    this.sendWaking(parent, message, () => { this.sendAgentMessage(parent, message) })
     return message.id
   }
 
@@ -717,18 +723,16 @@ export class SubagentContinuationManager {
     }
   }
 
-  /** Send one report while translating only the parent's own rejection. */
-  private sendReport(
+  /** Send one Agent message while translating only the target's own rejection. */
+  private sendAgentMessage(
     parent: Agent,
     message: ReturnType<typeof createUserMessage>,
-    delivery: SubagentReportDelivery,
   ): void {
     try {
-      if (delivery === 'next-step') parent.steer(message)
-      else parent.inject(message)
+      parent.steer(message)
     } catch (error: unknown) {
       throw new SubagentError(
-        'direct parent is not live; report was not delivered',
+        'direct parent is not live; the message was not delivered',
         'PARENT_UNAVAILABLE',
         { cause: error },
       )
@@ -963,7 +967,7 @@ export class SubagentContinuationManager {
     parent: Agent,
     childId: SessionId,
     content: ContentBlock[],
-    options: SubagentFollowupOptions,
+    options: ChildDeliveryOptions,
   ): Promise<MessageId> {
     const query = this.requireSessionQuery()
     let observation: SessionObservation
@@ -988,8 +992,7 @@ export class SubagentContinuationManager {
     )
     if (descriptor === undefined || descriptor.mode !== 'continuable') {
       throw new SubagentError(
-        `subagent "${childId}" has no supported continuation state and cannot be resumed; `
-        + 'do not retry send_message with this id',
+        `subagent "${childId}" has no supported continuation state and cannot be resumed; choose a different target`,
         'NOT_RESUMABLE',
       )
     }
@@ -1014,36 +1017,34 @@ export class SubagentContinuationManager {
       if (error instanceof SubagentError) throw error
       throw new SubagentError(`subagent "${childId}" is unavailable`, 'NOT_RESUMABLE', { cause: error })
     }
-    return await this.submitMaterialized(activation, content, options.source, parent, options.signal)
+    return await this.submitMaterialized(activation, content, options, parent)
   }
 
   /**
    * Submit to a freshly materialized Activation or roll it back completely.
    * @param activation - the just-published Activation to admit or release.
    * @param content - the initial or resumed message content.
-   * @param source - durable fields naming who supplied the accepted message.
+   * @param options - durable source, scheduling, and pre-acceptance cancellation.
    * @param parent - the live direct parent authorizing admission.
-   * @param signal - caller cancellation owning admission until acceptance.
    * @returns the accepted inbox message id.
    */
   private async submitMaterialized(
     activation: Activation,
     content: ContentBlock[],
-    source: MessageSource,
+    options: ChildDeliveryOptions,
     parent: Agent,
-    signal: AbortSignal,
   ): Promise<MessageId> {
     try {
       if (contentHasImage(content)) {
         // The capability read awaits with the activation already published, so
         // the disposal cutoff is re-checked before the submit; a drain that
         // began during the read turns into a clean closing rejection.
-        await this.assertImageCapable(activation.handle.agent, signal)
+        await this.assertImageCapable(activation.handle.agent, options.signal)
         if (activation.disposal !== undefined) {
           throw new SubagentError(`subagent "${activation.childId}" is closing`, 'ACTIVATION_CLOSING')
         }
       }
-      return this.submitAdmitted(activation, content, source, parent, signal)
+      return this.submitAdmitted(activation, content, options, parent)
     } catch (error: unknown) {
       /* v8 ignore next -- rollback disposal failures must not mask the
        * pre-acceptance signal, drain, or lifecycle failure. */
@@ -1119,7 +1120,7 @@ export class SubagentContinuationManager {
     // `AgentRegistry.enter()` is the authoritative collision boundary for an id
     // some other owner holds — a duplicate would reject there with rollback.
     inputs.signal.throwIfAborted()
-    const setup = (childCtx: Context): AgentSetupCommit => {
+    const setup = (childCtx: Context): void => {
       // Only fresh creation seeds the delegation policy onto the child's own
       // log (after any fork seed, so fresh policy wins stale seed state); a
       // cold resume replays those persisted events instead.
@@ -1127,7 +1128,6 @@ export class SubagentContinuationManager {
         appendDelegatedPolicyOverrides((childCtx.agent as Agent).session, create.delegatedPolicies)
       }
       applyChildComposition(childCtx, parent, inputs.composition)
-      return this.setupRegistry.apply(childCtx)
     }
     const observer = this.host.observeActivation(provider, childId, parent)
     // Agent creation owns rollback before handle transfer. A rejection leaves
@@ -1256,15 +1256,18 @@ export class SubagentContinuationManager {
   private submit(
     activation: Activation,
     content: ContentBlock[],
-    source: MessageSource,
+    options: ChildDeliveryOptions,
     parent: Agent,
   ): MessageId {
     // Parent-originated delivery keeps the parent live through ownership, so
     // establish it before the message can enter the child's inbox.
     this.acquireOwnership(parent, activation.childId)
-    const message = createUserMessage({ content, source })
+    const message = options.delivery === 'steer'
+      ? agentMessage(parent, content)
+      : createUserMessage({ content, source: options.source })
     const accepted = this.admitWaking(activation, message.id, () => {
-      activation.handle.agent.followup(message)
+      if (options.delivery === 'steer') activation.handle.agent.steer(message)
+      else activation.handle.agent.followup(message)
     })
     // Past this point the caller has an id for this child, so its eventual
     // settlement is something the parent is owed an account of.
@@ -1307,11 +1310,10 @@ export class SubagentContinuationManager {
   private submitAdmitted(
     activation: Activation,
     content: ContentBlock[],
-    source: MessageSource,
+    options: ChildDeliveryOptions,
     parent: Agent,
-    signal: AbortSignal,
   ): MessageId {
-    signal.throwIfAborted()
+    options.signal.throwIfAborted()
     this.assertAdmitting(parent)
     /* v8 ignore next 6 -- only a synchronous re-entrant disposer can change
      * this field between the caller's live check and this no-await boundary. */
@@ -1326,7 +1328,7 @@ export class SubagentContinuationManager {
       activation.childId,
       activation.handle.agent.session.header.parentSession,
     )
-    return this.submit(activation, content, source, parent)
+    return this.submit(activation, content, options, parent)
   }
 
   /**

+ 52 - 67
packages/subagent/subagent/src/index.ts

@@ -13,8 +13,8 @@
  *
  * Public operations express caller intent: `start` returns one published owned
  * one-shot run, `startContinuable` establishes a durable continuable child, and
- * `followup` delivers later content without exposing whether the child is
- * resident. Continuable children never become a {@link SubagentRun}: the
+ * `sendMessage` steers between adjacent Agents without exposing whether a child
+ * is resident. Continuable children never become a {@link SubagentRun}: the
  * continuation manager holds their `AgentHandle` directly and orders every turn
  * through the child's own inbox, so providers contribute only the detached
  * creation spec and see no handle, turn, or teardown. Child and descendant
@@ -34,7 +34,7 @@ import { admitPromptContent } from '@deepseek-ai/dsh-attachment'
 import { scopeTarget } from '@deepseek-ai/dsh-scope'
 import type { Scoped } from '@deepseek-ai/dsh-scope'
 import { assertObjectJsonSchema } from '@deepseek-ai/dsh-tools'
-import type { ContentBlock, MessageId } from '@deepseek-ai/dsh-llm'
+import type { ContentBlock, MessageId, MessageSource } from '@deepseek-ai/dsh-llm'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import type { SessionId } from '@deepseek-ai/dsh-session'
 import { canonicalClientTimeZone } from '@deepseek-ai/dsh-util-time'
@@ -68,16 +68,14 @@ import SubagentContinuationManager from './continuation.ts'
 import type {
   ContinuableStart,
   ContinuableStartSpec,
-  SubagentFollowupOptions,
   SubagentInterruptAuthority,
-  SubagentReportOptions,
+  SubagentSendMessageOptions,
 } from './continuation.ts'
-import SubagentActivationSetupRegistry from './activation-setup-registry.ts'
-import type { ContinuableSetupContribution } from './activation-setup-registry.ts'
 import { listChildren as listSubagentChildren, listDescendants as listSubagentDescendants } from './list-children.ts'
 import type { SubagentDescendantListEntry, SubagentListEntry } from './list-children.ts'
 import { snapshotSubagentDescriptor } from './descriptor.ts'
 import { subagentIdentityProjectionDefinition, subagentTimingProjectionDefinition } from './projection.ts'
+import { queueSubagentPrompt } from './internal.ts'
 
 export * from './out-of-process.ts'
 export { AssistantOutputFold, finalAssistantOutput } from './assistant-output.ts'
@@ -123,17 +121,13 @@ export {
 } from './child-agent.ts'
 export type { ChildComposition, DelegatedPolicyOverrides } from './child-agent.ts'
 export type {
+  AgentMessageSource,
   ContinuableStart,
   ContinuableStartSpec,
-  CoordinatorMessageSource,
-  SubagentFollowupOptions,
   SubagentInterruptAuthority,
-  SubagentReportDelivery,
-  SubagentReportMessageSource,
-  SubagentReportOptions,
+  SubagentSendMessageOptions,
   SubagentSettledMessageSource,
 } from './continuation.ts'
-export type { ContinuableSetupContribution } from './activation-setup-registry.ts'
 export type * from './control-types.ts'
 export type { SubagentDescendantListEntry } from './list-children.ts'
 export type { SubagentRunEndInfo, SubagentRunInfo } from './types.ts'
@@ -197,8 +191,6 @@ interface BrowserPromptSource {
 export class SubagentRuntime extends TypertRemoteService {
   private providers = new Map<string, SubagentProvider>()
   private continuations: SubagentContinuationManager | undefined
-  /** Deployment contributions composed into unpublished continuable children. */
-  private readonly setupRegistry = new SubagentActivationSetupRegistry()
   /**
    * The contained lifecycle-edge publisher. Built here because scoped dispatch
    * keys its carrier by this exact service instance, whose own context filter
@@ -213,7 +205,7 @@ export class SubagentRuntime extends TypertRemoteService {
       const manager = new SubagentContinuationManager(childCtx, {
         prepareContinuable: (name, request) => this.prepareContinuable(name, request),
         observeActivation: (provider, childId, parent) => this.observeActivation(provider, childId, parent),
-      }, this.setupRegistry)
+      })
       this.continuations = manager
       childCtx.effect(() => () => {
         /* v8 ignore else -- one injected binding owns the slot until its fiber disposes. */
@@ -240,27 +232,47 @@ export class SubagentRuntime extends TypertRemoteService {
   }
 
   /**
-   * Deliver one later message to a continuable child as its next FIFO turn. A
-   * resident child's Agent inbox accepts it directly (waking a `waiting`
-   * Activation), while an absent one is cold-resumed from its persisted
-   * Session. The Agent inbox is the only queue, so every accepted message has
-   * one observable order.
-   * @param parent - the exact live direct parent authorizing this delivery.
-   * @param childId - durable child session id.
-   * @param content - user-role content to deliver.
-   * @param options - the message source fields and caller cancellation, which stops the
-   *   operation only before inbox acceptance.
+   * Steer one model-authored message to the sender's direct parent or direct
+   * continuable child. A running target admits it at the nearest step boundary;
+   * an idle target starts a turn, and an absent direct child cold-resumes from
+   * persistence. The service derives durable sender attribution from the exact
+   * live sender. Caller cancellation stops only pre-acceptance work.
+   * @param sender - exact live Agent authorizing and originating the message.
+   * @param targetId - durable direct-parent or direct-child session id.
+   * @param content - model-authored content to deliver.
+   * @param options - caller cancellation before inbox acceptance.
    * @returns the accepted message's inbox id.
-   * @throws when continuation services are unavailable, parent authority is
-   *   rejected, or the message was not admitted.
+   * @throws when continuation services are unavailable, adjacency is rejected,
+   *   or the message was not admitted.
    */
-  async followup(
+  async sendMessage(
+    sender: Agent,
+    targetId: SessionId,
+    content: ContentBlock[],
+    options: SubagentSendMessageOptions,
+  ): Promise<MessageId> {
+    return this.requireContinuations().sendMessage(sender, targetId, content, options)
+  }
+
+  /**
+   * Queue one host-protocol message as a distinct direct-child turn.
+   * Symbol-keyed so host adapters can preserve their own provenance without
+   * widening the public Service Definition or impersonating an Agent sender.
+   * @param parent - exact live direct parent authorizing delivery.
+   * @param childId - durable direct-child session id.
+   * @param content - host-authored content to deliver.
+   * @param source - durable host-protocol provenance.
+   * @param signal - caller cancellation before inbox acceptance.
+   * @returns the accepted message's inbox id.
+   */
+  private [queueSubagentPrompt](
     parent: Agent,
     childId: SessionId,
     content: ContentBlock[],
-    options: SubagentFollowupOptions,
+    source: MessageSource,
+    signal: AbortSignal,
   ): Promise<MessageId> {
-    return this.requireContinuations().followup(parent, childId, content, options)
+    return this.requireContinuations().queuePrompt(parent, childId, content, source, signal)
   }
 
   /**
@@ -282,41 +294,6 @@ export class SubagentRuntime extends TypertRemoteService {
     this.continuations?.interrupt(targetSessionId, authority)
   }
 
-  /**
-   * Deliver selected content from one live continuable child to its durable
-   * direct parent. The child is the authority credential; callers cannot name a
-   * recipient. Reporting does not conclude the child's turn or Activation.
-   * @param child - exact live reporting child.
-   * @param content - selected model-facing content.
-   * @param options - parent scheduling and pre-acceptance cancellation.
-   * @returns the stable identity of the parent-accepted message.
-   * @throws when continuation services are unavailable, sender authorization
-   *   fails, or the direct parent is not live.
-   */
-  async reportFrom(
-    child: Agent,
-    content: ContentBlock[],
-    options: SubagentReportOptions,
-  ): Promise<MessageId> {
-    return this.requireContinuations().reportFrom(child, content, options)
-  }
-
-  /**
-   * Compose one deployment capability into every continuable child's
-   * unpublished creation context on fresh creation and cold resume. Grants wait
-   * for the next Activation; removing the contribution revokes every resident
-   * installation immediately.
-   * @param contribution - synchronous child-scope installer.
-   * @returns the exact Cordis effect disposer.
-   */
-  registerContinuableSetup(contribution: ContinuableSetupContribution): () => void {
-    // oxlint-disable-next-line typescript/no-misused-promises -- synchronous disposer
-    return this.ctx.effect(
-      () => this.setupRegistry.register(contribution),
-      'subagents.registerContinuableSetup()',
-    )
-  }
-
   /**
    * Close continuable admission below exact live parent Agents, stop only their
    * visible descendant Activations synchronously, then await admitted scoped
@@ -467,7 +444,15 @@ export class SubagentRuntime extends TypertRemoteService {
         if (attachments === undefined) throw new Error('subagent image prompt requires an attachment store')
         content = await admitPromptContent(attachments, request.content)
       }
-      return { messageId: await this.followup(parent, childSessionId, content, { source, signal }) }
+      return {
+        messageId: await this[queueSubagentPrompt](
+          parent,
+          childSessionId,
+          content,
+          source,
+          signal,
+        ),
+      }
     } catch (error: unknown) {
       return rejectPrompt(error, childSessionId, signal)
     }

+ 79 - 0
packages/subagent/subagent/src/internal.ts

@@ -0,0 +1,79 @@
+/**
+ * Continuation integration markers and host adapters outside the public
+ * Service Definition and model-facing Agent messaging contract.
+ * @module @deepseek-ai/dsh-subagent/internal
+ */
+
+import type { Agent } from '@deepseek-ai/dsh-agent'
+import type { ContentBlock, MessageId, MessageSource } from '@deepseek-ai/dsh-llm'
+import type { SessionId } from '@deepseek-ai/dsh-session'
+import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
+import type SubagentRuntime from './index.ts'
+
+/** Process-stable identity carried only by the standard adjacent-Agent messaging tool. */
+export const adjacentAgentSendMessageTool = Symbol.for('dsh.subagent.adjacentAgentSendMessageTool')
+
+/**
+ * Mark the standard adjacent-Agent messaging tool without changing its model-visible schema.
+ * @param definition - the standard `send_message` definition.
+ * @returns the same definition with its internal identity installed.
+ */
+export function markAdjacentAgentSendMessageTool(definition: ToolDefinition): ToolDefinition {
+  Object.defineProperty(definition, adjacentAgentSendMessageTool, { value: true })
+  return definition
+}
+
+/**
+ * Test whether one visible definition is the standard adjacent-Agent messaging tool.
+ * @param definition - the scope-resolved `send_message` candidate.
+ * @returns whether the definition carries the internal standard-tool identity.
+ */
+export function isAdjacentAgentSendMessageTool(definition: ToolDefinition | undefined): boolean {
+  return definition !== undefined
+    && (definition as ToolDefinition & { [adjacentAgentSendMessageTool]?: true })[adjacentAgentSendMessageTool] === true
+}
+
+/**
+ * Process-stable symbol-keyed Queue delivery shared by the bundled runtime
+ * entry and this unbundled internal subpath.
+ * @internal
+ */
+export const queueSubagentPrompt = Symbol.for('dsh.subagent.queuePrompt')
+
+/** Runtime face required by the host-only Queue adapter. */
+export interface HostPromptQueue {
+  [queueSubagentPrompt](
+    parent: Agent,
+    childId: SessionId,
+    content: ContentBlock[],
+    source: MessageSource,
+    signal: AbortSignal,
+  ): Promise<MessageId>
+}
+
+/**
+ * Queue one host-protocol message without exposing another Service operation.
+ * @param runtime - subagent runtime owning continuation residency.
+ * @param parent - exact live direct parent authorizing delivery.
+ * @param childId - durable direct-child session id.
+ * @param content - host-authored content to deliver.
+ * @param source - durable host-protocol provenance.
+ * @param signal - caller cancellation before inbox acceptance.
+ * @returns the accepted message's inbox id.
+ */
+export function queueHostSubagentPrompt(
+  runtime: SubagentRuntime,
+  parent: Agent,
+  childId: SessionId,
+  content: ContentBlock[],
+  source: MessageSource,
+  signal: AbortSignal,
+): Promise<MessageId> {
+  return (runtime as unknown as HostPromptQueue)[queueSubagentPrompt](
+    parent,
+    childId,
+    content,
+    source,
+    signal,
+  )
+}

+ 0 - 163
packages/subagent/subagent/tests/activation-setup-registry.spec.ts

@@ -1,163 +0,0 @@
-import { describe, expect, it } from 'vitest'
-import { Context } from '@deepseek-ai/cordis'
-import SubagentActivationSetupRegistry from '../src/activation-setup-registry.ts'
-
-/** A child-like scoped context with observable disposal. */
-function childContext(): { ctx: Context; close: () => Promise<void> } {
-  const root = new Context()
-  const scope = root.plugin(function child() {})
-  return { ctx: scope.ctx, close: async () => { await scope.dispose() } }
-}
-
-describe('SubagentActivationSetupRegistry', () => {
-  it('installs contributions in registration order and commits them', () => {
-    const registry = new SubagentActivationSetupRegistry()
-    const order: string[] = []
-    registry.register(() => { order.push('first'); return () => order.push('undo-first') })
-    registry.register(() => { order.push('second'); return () => order.push('undo-second') })
-    const child = childContext()
-
-    const transaction = registry.apply(child.ctx)
-    expect(order).toEqual(['first', 'second'])
-    expect(() => { transaction.commit() }).not.toThrow()
-    expect(order).toEqual(['first', 'second'])
-  })
-
-  it('makes repeated removal and converging ownership idempotent', async () => {
-    const registry = new SubagentActivationSetupRegistry()
-    let disposals = 0
-    const remove = registry.register(() => () => { disposals += 1 })
-    const child = childContext()
-    registry.apply(child.ctx).commit()
-
-    remove()
-    remove()
-    await child.close()
-    expect(disposals).toBe(1)
-  })
-
-  it('makes the opposite ownership convergence idempotent', async () => {
-    const registry = new SubagentActivationSetupRegistry()
-    let disposals = 0
-    const remove = registry.register(() => () => { disposals += 1 })
-    const child = childContext()
-    registry.apply(child.ctx).commit()
-
-    await child.close()
-    remove()
-    expect(disposals).toBe(1)
-  })
-
-  it('skips a contribution removed before a child is applied', () => {
-    const registry = new SubagentActivationSetupRegistry()
-    const installed: string[] = []
-    const remove = registry.register(() => { installed.push('gone'); return () => {} })
-    registry.register(() => { installed.push('kept'); return () => {} })
-    remove()
-
-    registry.apply(childContext().ctx).commit()
-    expect(installed).toEqual(['kept'])
-  })
-
-  it('invalidates a provisioning batch revoked before commit', () => {
-    const registry = new SubagentActivationSetupRegistry()
-    let disposals = 0
-    const remove = registry.register(() => () => { disposals += 1 })
-    const transaction = registry.apply(childContext().ctx)
-
-    remove()
-    expect(disposals).toBe(1)
-    expect(() => { transaction.commit() }).toThrow(/revoked while this child was being built/)
-  })
-
-  it('catches a contribution revoked inside its own installer', () => {
-    const registry = new SubagentActivationSetupRegistry()
-    let disposals = 0
-    const self: { remove?: () => void } = {}
-    self.remove = registry.register(() => {
-      self.remove?.()
-      return () => { disposals += 1 }
-    })
-
-    const transaction = registry.apply(childContext().ctx)
-    expect(disposals).toBe(1)
-    expect(() => { transaction.commit() }).toThrow(/revoked/)
-  })
-
-  it('attempts every contribution-removal disposer before reporting failures', () => {
-    const registry = new SubagentActivationSetupRegistry()
-    const released: string[] = []
-    let seq = 0
-    const remove = registry.register(() => {
-      const id = `child-${++seq}`
-      return () => {
-        released.push(id)
-        if (id === 'child-1') throw new Error('disposer exploded')
-      }
-    })
-    for (const child of [childContext(), childContext(), childContext()]) {
-      registry.apply(child.ctx).commit()
-    }
-
-    expect(() => { remove() }).toThrow(/failed to release 1 installation\(s\)/)
-    expect(released).toEqual(['child-1', 'child-2', 'child-3'])
-  })
-
-  it('attempts every child-scope disposer before reporting failures', async () => {
-    const registry = new SubagentActivationSetupRegistry()
-    const released: string[] = []
-    registry.register(() => () => {
-      released.push('a')
-      throw new Error('first disposer exploded')
-    })
-    registry.register(() => () => { released.push('b') })
-    const child = childContext()
-    registry.apply(child.ctx).commit()
-
-    await child.close().catch(() => undefined)
-    expect(released).toEqual(['a', 'b'])
-  })
-
-  it('rolls back earlier installations when a later contribution throws', () => {
-    const registry = new SubagentActivationSetupRegistry()
-    const undone: string[] = []
-    registry.register(() => () => undone.push('first'))
-    registry.register(() => { throw new Error('boom') })
-    registry.register(() => () => undone.push('third'))
-
-    expect(() => registry.apply(childContext().ctx)).toThrow(/boom/)
-    expect(undone).toEqual(['first'])
-  })
-
-  it('does not dispose twice when revocation precedes setup rollback', () => {
-    const registry = new SubagentActivationSetupRegistry()
-    const disposals: string[] = []
-    const removeFirst = registry.register(() => () => { disposals.push('first') })
-    registry.register(() => {
-      removeFirst()
-      throw new Error('second failed after revoking the first')
-    })
-
-    expect(() => registry.apply(childContext().ctx)).toThrow(/second failed/)
-    expect(disposals).toEqual(['first'])
-  })
-
-  it('does not cross-release independent child scopes', async () => {
-    const registry = new SubagentActivationSetupRegistry()
-    const disposed: string[] = []
-    let seq = 0
-    registry.register(() => {
-      const id = `child-${++seq}`
-      return () => disposed.push(id)
-    })
-    const first = childContext()
-    const second = childContext()
-    registry.apply(first.ctx).commit()
-    registry.apply(second.ctx).commit()
-
-    await first.close()
-    expect(disposed).toEqual(['child-1'])
-    await second.close()
-    expect(disposed).toEqual(['child-1', 'child-2'])
-  })
-})

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä