Просмотр исходного кода

feat(subagent): unify adjacent agent delivery on steer

Dudu-0223 2 недель назад
Родитель
Сommit
ec493c2db8
94 измененных файлов с 846 добавлено и 829 удалено
  1. 6 0
      .agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.i18n.yaml
  2. 64 0
      .agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.md
  3. 64 0
      .agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md
  4. 2 2
      .agents/notes/implemented/bug-fix/2026-08-17-subagent-report-settlement-ordering.i18n.yaml
  5. 2 0
      .agents/notes/implemented/bug-fix/2026-08-17-subagent-report-settlement-ordering.md
  6. 2 0
      .agents/notes/implemented/bug-fix/2026-08-17-subagent-report-settlement-ordering.zh.md
  7. 2 2
      .agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.i18n.yaml
  8. 2 0
      .agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.md
  9. 2 0
      .agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.zh.md
  10. 2 2
      .agents/notes/implemented/simplification/2026-07-27-intent-named-subagent-continuation-operations.i18n.yaml
  11. 1 1
      .agents/notes/implemented/simplification/2026-07-27-intent-named-subagent-continuation-operations.md
  12. 1 1
      .agents/notes/implemented/simplification/2026-07-27-intent-named-subagent-continuation-operations.zh.md
  13. 15 12
      apps/web/tests/subagent-interrupt-ui.e2e.ts
  14. 2 2
      docs/config-catalog.i18n.yaml
  15. 1 22
      docs/config-catalog.md
  16. 1 22
      docs/config-catalog.zh.md
  17. 2 2
      docs/event-producer-consumer.i18n.yaml
  18. 4 4
      docs/event-producer-consumer.md
  19. 4 4
      docs/event-producer-consumer.zh.md
  20. 2 2
      docs/subsystems/subagent.i18n.yaml
  21. 28 70
      docs/subsystems/subagent.md
  22. 28 70
      docs/subsystems/subagent.zh.md
  23. 2 2
      docs/tool-catalog.i18n.yaml
  24. 2 2
      docs/tool-catalog.md
  25. 2 2
      docs/tool-catalog.zh.md
  26. 4 4
      packages/client/ui-chat/tests/chat-branch-tails.client.spec.tsx
  27. 2 1
      packages/experimental/agent-team/src/mailbox.ts
  28. 26 22
      packages/experimental/agent-team/tests/team.spec.ts
  29. 9 24
      packages/extensions/tool-cordis/src/api-catalog.ts
  30. 2 2
      packages/subagent/subagent/README.i18n.yaml
  31. 6 5
      packages/subagent/subagent/README.md
  32. 6 5
      packages/subagent/subagent/README.zh.md
  33. 4 0
      packages/subagent/subagent/package.json
  34. 130 142
      packages/subagent/subagent/src/continuation.ts
  35. 51 45
      packages/subagent/subagent/src/index.ts
  36. 55 0
      packages/subagent/subagent/src/internal.ts
  37. 19 4
      packages/subagent/subagent/tests/continuation-inheritance.spec.ts
  38. 75 59
      packages/subagent/subagent/tests/continuation.spec.ts
  39. 24 20
      packages/subagent/subagent/tests/control.spec.ts
  40. 2 2
      packages/subagent/subagent/tests/service.spec.ts
  41. 2 2
      packages/subagent/tool-subagent-control/README.i18n.yaml
  42. 4 4
      packages/subagent/tool-subagent-control/README.md
  43. 4 4
      packages/subagent/tool-subagent-control/README.zh.md
  44. 11 16
      packages/subagent/tool-subagent-control/src/index.ts
  45. 23 11
      packages/subagent/tool-subagent-control/tests/tool-subagent-control.spec.ts
  46. 2 2
      packages/subagent/tool-subagent-report/README.i18n.yaml
  47. 12 18
      packages/subagent/tool-subagent-report/README.md
  48. 12 18
      packages/subagent/tool-subagent-report/README.zh.md
  49. 0 3
      packages/subagent/tool-subagent-report/package.json
  50. 10 31
      packages/subagent/tool-subagent-report/src/index.ts
  51. 27 54
      packages/subagent/tool-subagent-report/tests/tool-subagent-report.spec.ts
  52. 3 3
      packages/test-support/session-snapshot/tests/fixtures/subagent-durability-failure.ts
  53. 0 4
      pnpm-lock.yaml
  54. 2 5
      scripts/gen-cordis-catalog.ts
  55. 2 3
      scripts/gen-tool-catalog.ts
  56. 2 17
      scripts/type-equiv.manifest.json
  57. 1 1
      snapshots/sdk/bash-tool/tool-schemas.expected.json
  58. 2 2
      snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json
  59. 14 18
      snapshots/sdk/subagent-continuable/session.1.jsonl
  60. 10 10
      snapshots/sdk/subagent-continuable/session.jsonl
  61. 2 2
      snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json
  62. 1 1
      snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json
  63. 1 1
      snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json
  64. 1 1
      snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json
  65. 2 2
      snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json
  66. 2 2
      snapshots/sdk/subagent-report/session.1.jsonl
  67. 6 6
      snapshots/sdk/subagent-report/session.jsonl
  68. 2 2
      snapshots/sdk/subagent-report/tool-schemas.1.expected.json
  69. 1 1
      snapshots/sdk/text-turn/tool-schemas.expected.json
  70. 2 2
      snapshots/session/agent-instructions/tool-schemas.expected.json
  71. 1 1
      snapshots/session/both-mode-turn/system-prompt.expected.md
  72. 1 1
      snapshots/session/both-mode-turn/tool-schemas.expected.json
  73. 1 1
      snapshots/session/code-mode-read-image/system-prompt.expected.md
  74. 1 1
      snapshots/session/code-mode-turn/system-prompt.expected.md
  75. 2 2
      snapshots/session/compaction-recovery/tool-schemas.expected.json
  76. 1 1
      snapshots/session/cordis-inspect-jsdoc/system-prompt.expected.md
  77. 1 1
      snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json
  78. 1 1
      snapshots/session/fs-glob-sampling/tool-schemas.expected.json
  79. 1 1
      snapshots/session/lsp-definition/tool-schemas.expected.json
  80. 1 1
      snapshots/session/product-subagent-both/tool-schemas.expected.json
  81. 1 1
      snapshots/session/product-subagent-codex/tool-schemas.expected.json
  82. 1 1
      snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json
  83. 1 1
      snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json
  84. 1 1
      snapshots/session/ralph-loop/tool-schemas.1.expected.json
  85. 1 1
      snapshots/session/ralph-loop/tool-schemas.2.expected.json
  86. 1 1
      snapshots/session/session-query-spill/tool-schemas.expected.json
  87. 1 1
      snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json
  88. 1 1
      snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json
  89. 1 1
      snapshots/session/text-turn/tool-schemas.expected.json
  90. 1 1
      snapshots/session/web-fetch/tool-schemas.expected.json
  91. 1 1
      snapshots/web/code-mode-round/system-prompt.expected.md
  92. 1 1
      snapshots/web/cordis-tool-round/tool-schemas.expected.json
  93. 1 1
      snapshots/web/fresh-round-trip/tool-schemas.expected.json
  94. 1 0
      tsconfig.base.json

+ 6 - 0
.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.md
+2026-08-27-adjacent-agent-steer-messaging.md: 2d540a40847c61c55c0a0e1cc995e01cc924829a
+2026-08-27-adjacent-agent-steer-messaging.zh.md: f04b560b8db15ffd9f6095214670ac5f99a392cb

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

@@ -0,0 +1,64 @@
+# Agent Note: Adjacent Agents share one Steer messaging operation
+
+Status: implemented
+
+English | [中文](2026-08-27-adjacent-agent-steer-messaging.zh.md)
+
+## Problem
+
+Continuable Agents had direction-specific public operations and message sources. A parent used `followup(parent, childId, content, { source, signal })`, while a child used `reportFrom(child, content, { delivery, signal })`. The first created a later FIFO turn and accepted caller-supplied provenance; the second selected quiet injection or next-step steering through deployment configuration and derived its recipient internally.
+
+Those differences described the tools that first consumed the service, not two lifecycle capabilities. Both directions deliver model-authored content across one parent/child edge, require the continuation manager to authorize the exact live Agents, and depend on the same residency and cold-resume ownership. Direction-specific sources also made equivalent messages reconstruct differently.
+
+[Issue #3220](https://github.com/deepseek-harness/deepseek-harness/issues/3220) requires one foundation before the model-facing tools are unified.
+
+## 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:
+
+```ts
+type SessionId = string
+
+interface AgentMessageSource {
+  readonly kind: 'agent-message'
+  readonly form: 'relay'
+  readonly senderSessionId: SessionId
+}
+```
+
+The service derives `senderSessionId` from the authorized Agent and frames the model-visible content as `Agent <sender-id> sent a message:`. Attribution therefore cannot diverge from authority. The runtime-owned `subagent-settled` notice remains separate because its words are the manager's account, not content selected by an Agent.
+
+Human browser prompts are not model-authored Agent messages. The existing remote prompt path keeps a private Queue delivery so each human prompt remains a distinct turn. Interrupt behavior and settlement delivery are unchanged.
+
+The child-scoped `report` tool temporarily derives its parent id and adapts to `sendMessage()`. Its `reportDelivery` configuration is removed: accepted reports now use the same fixed Steer scheduling and `agent-message` provenance as parent-to-child content. A later change may unify the model-facing tools without changing this service decision.
+
+## 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 operation name for a direction-neutral capability.
+
+**Keep separate `followup` and `reportFrom` methods over one implementation.** Two public methods still permit different options, provenance, and error behavior to reappear. Tool-specific adapters belong in Consumer packages, not the Service Definition.
+
+**Let callers supply `MessageSource`.** The sender Agent is already the authorization credential. Accepting independent attribution allows a caller to record a different author from the one the manager authorized.
+
+**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 batching at a running target's step boundary.
+
+**Use `Agent.followup()` for idle targets and `Agent.steer()` for running targets.** `Agent.steer()` already defines both cases. Selecting from a pre-send status read would add a race and two inbox targets without changing the intended idle behavior.
+
+## Consequences
+
+- Service consumers have one direction-neutral model messaging operation and one provenance vocabulary.
+- 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. Several messages waiting together share next-step FIFO ordering.
+- Caller cancellation owns work only until inbox acceptance; it does not retract an accepted message or dispose the target.
+- Human prompts, settlement notices, QueueDock, and continuable fork enablement remain separate decisions.
+
+This decision supersedes the `followup` naming choice in [Intent-named subagent continuation operations](../simplification/2026-07-27-intent-named-subagent-continuation-operations.md), the public `reportFrom` and configurable delivery portions of [Continuable subagent report tool](../feature/2026-07-30-continuable-subagent-report-tool.md), and the report-specific delivery choice in [Subagent reports precede their settlement notices](../bug-fix/2026-08-17-subagent-report-settlement-ordering.md). Their provider, setup-contribution, prompt-guidance, durability, and settlement-ordering rationale remains applicable where not replaced here.

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

@@ -0,0 +1,64 @@
+# Agent Note:相邻 Agent 共享一个 Steer 消息操作
+
+状态:已实现
+
+[English](2026-08-27-adjacent-agent-steer-messaging.md) | 中文
+
+## 问题
+
+可继续 Agent 曾使用按方向划分的公开操作与消息来源。parent 使用 `followup(parent, childId, content, { source, signal })`,child 使用 `reportFrom(child, content, { delivery, signal })`。前者创建后续 FIFO 轮次并接受调用方提供的来源信息;后者通过部署配置选择静默注入或 next-step steering,并在内部推导接收方。
+
+这些差异描述的是最初消费服务的工具,而不是两种生命周期能力。两个方向都跨一条 parent/child 边投递模型编写的内容,都要求继续执行管理器授权确切在线 Agent,也都依赖相同的驻留与冷恢复所有权。按方向划分来源还会让等价消息以不同方式重建。
+
+[Issue #3220](https://github.com/deepseek-harness/deepseek-harness/issues/3220) 要求先统一这层基础,再统一面向模型的工具。
+
+## 决策
+
+`SubagentRuntime.sendMessage(sender, targetId, content, { signal })` 是唯一公开的模型编写消息操作。继续执行管理器只接受确切在线 sender,以及位于一条相邻边上的目标:
+
+- parent 到直接可继续 child,由 child 持久化的 `SessionHeader.parentSession` 授权;
+- 驻留的可继续 child 到其确切在线直接 parent,由 child 的 Activation 授权。
+
+sibling、self-target、相隔多于一条边的 ancestor、陈旧 Agent 对象、未知目标与一次性 child 都不是备用路由。该操作不接受调用方提供的 source、投递模式、离线 parent mailbox 或提供方分发。
+
+每条被接受的消息都使用 `Agent.steer()`。运行中的目标在最近的 step 边界接收消息;空闲目标启动一个轮次。不存在 Activation 的直接 child 会先经既有继续执行生命周期完成冷恢复,再接受相同的 Steer 投递。管理器保留唤醒发送记账,避免受继续执行管理的目标在同步 inbox 插入与 driver 准入之间结算。
+
+两个方向都使用同一个持久化来源:
+
+```ts
+type SessionId = string
+
+interface AgentMessageSource {
+  readonly kind: 'agent-message'
+  readonly form: 'relay'
+  readonly senderSessionId: SessionId
+}
+```
+
+服务从已授权 Agent 推导 `senderSessionId`,并把模型可见内容设为 `Agent <sender-id> sent a message:` 前缀。来源信息因此无法偏离权限。由 runtime 生成的 `subagent-settled` 通知保持独立,因为其中的文字是管理器的记账,而不是 Agent 选择的内容。
+
+浏览器中的人类提示不是由模型编写的 Agent 消息。既有远程提示路径保留私有 Queue 投递,使每条人类提示继续形成独立轮次。中断行为与结算投递不变。
+
+child 作用域的 `report` 工具暂时推导 parent id,并适配到 `sendMessage()`。其 `reportDelivery` 配置被移除:被接受的报告现在与 parent 到 child 的内容使用相同的固定 Steer 调度与 `agent-message` 来源。后续变更可以统一面向模型的工具,而无需改变该服务决策。
+
+## 考虑过的替代方案
+
+**保留 `followup` 并添加 child 到 parent 路由。** 该名称承诺后续轮次,并继承 `Agent.followup()` 语义。它会掩盖已选择的最近 step 行为,也会为方向中立的能力保留以 parent 为中心的操作名称。
+
+**在同一实现上保留独立的 `followup` 与 `reportFrom` 方法。** 两个公开方法仍允许不同的 options、来源信息与错误行为重新出现。工具专属适配器应归 Consumer 包所有,而不是归 Service Definition 所有。
+
+**允许调用方提供 `MessageSource`。** sender Agent 已是权限凭据。接受独立来源信息会允许调用方记录一个不同于管理器已授权 Agent 的作者。
+
+**保留静默投递作为部署策略。** 静默的模型编写消息可能已被接受,但空闲目标永远不会读取。固定 Steer 为两个方向提供同一种投递含义,并保留运行中目标 step 边界的批处理。
+
+**对空闲目标使用 `Agent.followup()`,对运行中目标使用 `Agent.steer()`。** `Agent.steer()` 已定义这两种情况。根据发送前读取的状态选择方法会增加竞态与两个 inbox 目标,却不会改变预期的空闲行为。
+
+## 后果
+
+- 服务 Consumer 只有一个方向中立的模型消息操作与一种来源词汇。
+- 继续执行管理器仍是相邻关系授权、驻留、冷恢复、唤醒准入与拆卸竞态的唯一所有者。
+- 被接受的消息可能延长运行中目标的当前轮次。多条共同等待的消息共享 next-step FIFO 顺序。
+- 调用方取消只在 inbox 接受前掌管工作;它不会撤回已接受消息,也不会 dispose 目标。
+- 人类提示、结算通知、QueueDock 与启用可继续 fork 仍是独立决策。
+
+本决策取代[按意图命名的 subagent 继续执行操作](../simplification/2026-07-27-intent-named-subagent-continuation-operations.zh.md)中的 `followup` 命名选择、[可继续 subagent report 工具](../feature/2026-07-30-continuable-subagent-report-tool.zh.md)中的公开 `reportFrom` 与可配置投递部分,以及[Subagent 报告先于其结算通知](../bug-fix/2026-08-17-subagent-report-settlement-ordering.zh.md)中的 report 专属投递选择。它们关于提供方、设置贡献、提示词指导、持久性与结算顺序的理由,在未被本记录取代之处仍然适用。

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-08-17-subagent-report-settlement-ordering.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-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
+2026-08-17-subagent-report-settlement-ordering.md: 8d8ef6ac1708eb66d7e9b80235cc563f2ab38989
+2026-08-17-subagent-report-settlement-ordering.zh.md: 6666baff72c3ee4ee6079bb73318f944b646d3bb

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

@@ -4,6 +4,8 @@ Status: implemented
 
 English | [中文](2026-08-17-subagent-report-settlement-ordering.zh.md)
 
+[Adjacent Agents share one Steer messaging operation](../architecture/2026-08-27-adjacent-agent-steer-messaging.md) makes Steer mandatory for every model-authored adjacent-Agent message and removes quiet delivery. This record still owns why an Agent message accepted before a settlement notice must precede that notice in the shared next-step FIFO.
+
 ## 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.

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

@@ -4,6 +4,8 @@ Status: implemented
 
 [English](2026-08-17-subagent-report-settlement-ordering.md) | 中文
 
+[相邻 Agent 共享一个 Steer 消息操作](../architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md)要求所有模型编写的相邻 Agent 消息使用 Steer,并移除静默投递。本记录仍负责解释:先于结算通知被接受的 Agent 消息必须在共享 next-step FIFO 中先于该通知。
+
 ## 问题
 
 可继续 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)记录了该缺陷。

+ 2 - 2
.agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-30-continuable-subagent-report-tool.md
-2026-07-30-continuable-subagent-report-tool.md: 07d17f18f318a86070d9b8612512fa3c2a3815e2
-2026-07-30-continuable-subagent-report-tool.zh.md: 9cbf76a9f3f50d9b1a84b573621cd6fc2a1611c4
+2026-07-30-continuable-subagent-report-tool.md: deb124c8d8e71d4dd0574e877580b8ad73b64f80
+2026-07-30-continuable-subagent-report-tool.zh.md: cc0d2e59454be0a070aca383993bb235ac9038aa

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

@@ -4,6 +4,8 @@ Status: implemented
 
 English | [中文](2026-07-30-continuable-subagent-report-tool.zh.md)
 
+The child-scoped setup and prompt-guidance decisions remain current while the adapter exists. [Adjacent Agents share one Steer messaging operation](../architecture/2026-08-27-adjacent-agent-steer-messaging.md) supersedes this record's public `reportFrom`, direction-specific provenance, and configurable delivery decisions.
+
 ## 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.

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

@@ -4,6 +4,8 @@ Status: implemented
 
 [English](2026-07-30-continuable-subagent-report-tool.md) | 中文
 
+只要该适配器仍然存在,child 作用域设置与提示词指导决策就仍然有效。[相邻 Agent 共享一个 Steer 消息操作](../architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md)取代了本记录的公开 `reportFrom`、按方向划分来源与可配置投递决策。
+
 ## 问题
 
 可继续的进程内 subagent 能够接收 parent 后续发来的消息、保留后代、结算并冷恢复,但基础生命周期无法让它们将选中内容发送给直接 parent。child 的完整输出已可从持久化会话中重建,因此缺失的能力是显式投递,而非结果存储。

+ 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: b25ca03a7427ac7b4b7de87f52e51a8ef970dfbd
+2026-07-27-intent-named-subagent-continuation-operations.zh.md: a9c737a8a216c3eaa824080383baaacf8ba43c15

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

@@ -4,7 +4,7 @@ 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
 

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

@@ -4,7 +4,7 @@ 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 })`。
 
 ## 问题
 

+ 15 - 12
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,
@@ -220,12 +221,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 +241,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 {

+ 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: 4331c0a5153f32f0e6af5b6ec6fd182ee9b335b4
-config-catalog.zh.md: 54f6ddde10053d422af2c5ebfd88a367597adc89
+config-catalog.md: a9cc4be949441c4ca546684f2d109de9cfff787d
+config-catalog.zh.md: 7b9d63fa189bdaecfff21cbebd2811b47a22a2d3

+ 1 - 22
docs/config-catalog.md

@@ -3001,28 +3001,6 @@ Depends on: [`AgentOptions`](subsystems/core.md)
 
 Source: [`packages/subagent/tool-subagent/src/index.ts:49`](../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:27`](../packages/subagent/tool-subagent-report/src/index.ts)
-
 <a id="deepseek-aidsh-tool-terminal"></a>
 
 ## `@deepseek-ai/dsh-tool-terminal`
@@ -3470,6 +3448,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
 - `@deepseek-ai/dsh-tool-call-timeout-policy` — requires `tools` ([`packages/guard/timeout-policy/src/index.ts`](../packages/guard/timeout-policy/src/index.ts))
 - `@deepseek-ai/dsh-tool-cordis` — requires `tools` · `systemPrompt` · `dynamicCordisRunner` · `cordisInspect` ([`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts))
 - `@deepseek-ai/dsh-tool-subagent-control` — requires `tools` · `subagents` ([`packages/subagent/tool-subagent-control/src/index.ts`](../packages/subagent/tool-subagent-control/src/index.ts))
+- `@deepseek-ai/dsh-tool-subagent-report` — requires `subagents` · `tools` · `systemPrompt` ([`packages/subagent/tool-subagent-report/src/index.ts`](../packages/subagent/tool-subagent-report/src/index.ts))
 - `@deepseek-ai/dsh-user-questions` ([`packages/interaction/user-questions/src/index.ts`](../packages/interaction/user-questions/src/index.ts))
 - `@deepseek-ai/dsh-webhook` — requires `agents` · `agentDefaultModel` · `agentPresets` · `permissionPresets` · `sessionTitle` · `workspaceRegistry` ([`packages/webhook/webhook/src/index.ts`](../packages/webhook/webhook/src/index.ts))
 - `@deepseek-ai/dsh-workspace` — requires `storageDomain` · `sessionPersistence` ([`packages/workspace/workspace/src/index.ts`](../packages/workspace/workspace/src/index.ts))

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

@@ -3003,28 +3003,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:27`](../packages/subagent/tool-subagent-report/src/index.ts)
-
 <a id="deepseek-aidsh-tool-terminal"></a>
 
 ## `@deepseek-ai/dsh-tool-terminal`
@@ -3472,6 +3450,7 @@ export interface Config {
 - `@deepseek-ai/dsh-tool-call-timeout-policy` — 需要 `tools`([`packages/guard/timeout-policy/src/index.ts`](../packages/guard/timeout-policy/src/index.ts))
 - `@deepseek-ai/dsh-tool-cordis` — 需要 `tools` · `systemPrompt` · `dynamicCordisRunner` · `cordisInspect`([`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts))
 - `@deepseek-ai/dsh-tool-subagent-control` — 需要 `tools` · `subagents`([`packages/subagent/tool-subagent-control/src/index.ts`](../packages/subagent/tool-subagent-control/src/index.ts))
+- `@deepseek-ai/dsh-tool-subagent-report` — 需要 `subagents` · `tools` · `systemPrompt`([`packages/subagent/tool-subagent-report/src/index.ts`](../packages/subagent/tool-subagent-report/src/index.ts))
 - `@deepseek-ai/dsh-user-questions`([`packages/interaction/user-questions/src/index.ts`](../packages/interaction/user-questions/src/index.ts))
 - `@deepseek-ai/dsh-webhook` — 需要 `agents` · `agentDefaultModel` · `agentPresets` · `permissionPresets` · `sessionTitle` · `workspaceRegistry`([`packages/webhook/webhook/src/index.ts`](../packages/webhook/webhook/src/index.ts))
 - `@deepseek-ai/dsh-workspace` — 需要 `storageDomain` · `sessionPersistence`([`packages/workspace/workspace/src/index.ts`](../packages/workspace/workspace/src/index.ts))

+ 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: e849cb84265c0781e4a8680d0bb247e9955b5c2e
-event-producer-consumer.zh.md: b5835c56b26f3a75fd792d3a71c3ab2dc688ea42
+event-producer-consumer.md: 529c8b0f95f9ae473e18413478f4fbe9c93a3dbc
+event-producer-consumer.zh.md: 76d2b4aef326d3ea9fb4026c6155c2009a9b4c38

+ 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:297`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
-| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:178`](../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:152`](../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:158`](../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:169`](../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:175`](../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:149`](../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:155`](../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:166`](../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:207`](../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:297`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
-| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:178`](../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:152`](../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:158`](../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:169`](../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:175`](../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:149`](../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:155`](../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:166`](../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:207`](../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/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: 9206672b89ac31e30bee176f536b3057eb37f3ee
-subagent.zh.md: 30e4e09a145a4b3d51044d85c602ce81fd44c3b3
+subagent.md: adfd51a37a25dbbf999211fd5f91b8a8ec7f003e
+subagent.zh.md: bd85a1faf0594e0122384d93811f8776ea62ce9b

+ 28 - 70
docs/subsystems/subagent.md

@@ -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
 }
@@ -199,23 +197,7 @@ interface ContinuableStart {
 
 An optional continuable-child setup contribution can install scope-local capabilities after base child composition and before Activation publication. The registry is ordered and transactional: a failed or revoked setup rolls back the unpublished Activation, child-scope disposal releases every installation, new registrations affect the next Activation, and registration removal revokes every resident installation immediately.
 
-`SubagentRuntime.reportFrom()` uses that extension point without adding a second queue or a result-bearing child wrapper. The exact live child Agent authorizes the call; callers cannot name a recipient. The manager derives the only recipient from the child's durable `parentSession`, requires that parent Agent to be live, frames the selected content as one `subagent-report` user message, and returns the message's stable `MessageId`. Quiet delivery uses `Agent.inject()` and 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'
-```
+The optional child-scoped `report` tool uses that extension point as a compatibility adapter over `sendMessage()`. It derives the target from the child's durable `parentSession`; accepted content therefore uses the same fixed Steer scheduling, framing, provenance, adjacency checks, and stable `MessageId` as parent-to-child messages. Reporting does not conclude the child's turn, and no final answer reports implicitly.
 
 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.
 
@@ -223,7 +205,7 @@ Reporting is the child's own choice, so the manager keeps a separate account of
 /**
  * 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 +220,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
-}
-```
-
 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 +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
@@ -565,19 +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

+ 28 - 70
docs/subsystems/subagent.zh.md

@@ -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
 }
@@ -199,23 +197,7 @@ 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 作用域 `report` 工具通过该扩展点实现,并作为 `sendMessage()` 的兼容适配器。它从 child 的持久化 `parentSession` 推导目标;已接受内容因此与 parent 到 child 的消息使用相同的固定 Steer 调度、前缀、来源信息、相邻关系校验与稳定 `MessageId`。报告不会结束 child 轮次,最终回答也不会隐式报告。
 
 上报是 child 自己的选择,因此管理器还保有一份属于自己的记账:当驻留 Activation 结算时,它会向该 child 持久化的直接 parent 投递一条通知,说明该 epoch 如何结束,并携带其最终 assistant 内容。对每个调用方拿到过 id 的 child,这条投递都是无条件的;它发生在会让 parent 被判定为已结算的所有权释放之前,并通过与上报相同的唤醒准入记账到达驻留 parent。若 parent 自身所在的谱系已在拆卸中,这条通知会以不唤醒的方式送达,因为唤醒一个静息 Agent 是开启一个轮次,而不是排队等待工作。其来源信息使用一个独立的 kind,因此 transcript(文本记录)绝不会把运行时的记账呈现为 child 自己写下的内容。
 
@@ -223,7 +205,7 @@ type SubagentReportDelivery = 'quiet' | 'next-step'
 /**
  * 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 +220,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 +508,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,19 +540,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

+ 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: 16142c2f7d98cf1037b034d2836c742b62f26594
-tool-catalog.zh.md: 533caacc7a923b5ea36f527292f420b610f1c488
+tool-catalog.md: d04c8bd9c54e8110f36c8d9967c420ad16397d5a
+tool-catalog.zh.md: 70bcea7e10200174ef42fb199ac81fa95263b257

+ 2 - 2
docs/tool-catalog.md

@@ -1640,7 +1640,7 @@ 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 background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.
 
 ```json
 {
@@ -1672,7 +1672,7 @@ The globally named control tools over continuable background subagents: provider
 
 ### `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.
+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. If that agent is working, the report steers its nearest step; otherwise it starts a turn. A failed call may still have arrived if a later tool-result hook failed, so do not blindly repeat it.
 
 ```json
 {

+ 2 - 2
docs/tool-catalog.zh.md

@@ -1646,7 +1646,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
 
 ### `send_message`
 
-根据 subagent id 向后台 subagent 发送消息,继续同一段对话。该消息会成为 subagent 的下一轮次:如果它仍在工作,消息会等待当前轮次结束,因此无法改变已经开始的工作方向。此调用不会返回 subagent 的答案,只会确认消息已投递,因此请用它分派更多工作。调用失败表示消息**未**投递。
+根据 subagent id 向后台 subagent 发送消息,继续同一段对话。如果它仍在工作,消息会 steer 其最近的 step;如果它处于 idle,消息会启动一个轮次。此调用不会返回 subagent 的答案,只会确认消息已投递。调用失败表示消息**未**投递。
 
 ```json
 {
@@ -1678,7 +1678,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
 
 ### `report`
 
-向启动你的 agent 报告选定内容。在你结束前调用一次,给出自包含的最终结果;当进度或发现会改变该 agent 接下来的行动时,也可以更早调用。该 agent 与你共享工作区,但不会自动收到你的 transcript(文本记录)、工具输出或推理,因此完成你的工作本身并不等于交出结果。报告不会结束你的轮次或完成你的工作,且只有直接父级会收到。失败的调用仍可能已经送达,因此不要盲目重复。
+向启动你的 agent 报告选定内容。在你结束前调用一次,给出自包含的最终结果;当进度或发现会改变该 agent 接下来的行动时,也可以更早调用。该 agent 与你共享工作区,但不会自动收到你的 transcript(文本记录)、工具输出或推理,因此完成你的工作本身并不等于交出结果。报告不会结束你的轮次或完成你的工作,且只有直接 parent 会收到。如果该 agent 正在工作,报告会 steer 其最近的 step;否则会启动一个轮次。如果后续工具结果 hook 失败,失败的调用仍可能已经送达,因此不要盲目重复。
 
 ```json
 {

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

@@ -701,7 +701,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) {
@@ -741,13 +741,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')
   })

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

@@ -7,6 +7,7 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm'
 import type { ContentBlock } from '@deepseek-ai/dsh-llm'
 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 +261,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

@@ -10,6 +10,7 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm'
 import { SessionId, type Session } from '@deepseek-ai/dsh-session'
 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'
@@ -1104,16 +1105,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,
@@ -1226,7 +1228,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,
     })
@@ -1532,18 +1535,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',

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

@@ -2123,11 +2123,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',
@@ -2135,13 +2135,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.',
@@ -5279,10 +5272,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};',
@@ -5311,14 +5300,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}',
@@ -5341,7 +5322,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    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 | undefined;\n    list(): string[];\n    async start(name: string, request: SubagentStartRequest): Promise<Subag /* …truncated — full shape in source */',
+  },
+  {
+    name: 'SubagentSendMessageOptions',
+    declaration: 'export interface SubagentSendMessageOptions {\n    readonly signal: AbortSignal;\n}',
   },
   {
     name: 'SubagentStartRequest',

+ 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: 76df70351d711d580d3ab1a89d0f929b85eab172
-README.zh.md: c4deb001c47435d29a5ca18cc0f8c0d26e48941e
+README.md: 9015a24e662494c308d3492f8165531cbb92320d
+README.zh.md: 3e50c6aa50d3bf80abdc5aa0168b79bb8b3e8d2c

+ 6 - 5
packages/subagent/subagent/README.md

@@ -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 and its direct parent or direct continuable child.
 - **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,7 +125,7 @@ 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, model controls, and the temporary child-scoped `report` adapter belong to the Consumer packages.
 
 #### Token effect
 
@@ -162,7 +163,7 @@ 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.
+- **Adjacent model messaging only** — `sendMessage()` requires an exact live sender and a direct parent or direct continuable child; browser prompts use the separate Queue control path.
 - **Continuation messages never steer** — parent-to-child follow-ups enqueue later turns; they never redirect the child's current turn.
 - **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.

+ 6 - 5
packages/subagent/subagent/README.zh.md

@@ -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,以及其直接 parent 或直接可继续 child
 - **描述符仅进日志**——它是会话事件,不进入模型历史,并跨压缩(compaction)保留;可继续描述符会显式记录解析后的子级提供方、模型与推理等级,用于冷恢复。
 
 </details>
@@ -124,7 +125,7 @@ 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、模型控制工具与临时的 child 作用域 `report` 适配器归 Consumer 包所有。
 
 #### Token 影响
 
@@ -162,7 +163,7 @@ You are a delegated subagent: your permission scope was fixed when you were star
 这些限制说明该 seam 何时不合适,或何时需要特别的运维注意。它们是当前包约束,不是通用委派对比或任务积压。
 
 - **ACP 子级仍为一次性,且无法通过追踪枚举**——ACP 运行在父级会话语料中没有本地子会话,远程提供方需要 Activation 所有权约定才能支持可继续子级。
-- **无 host-user 继续执行**——`followup()` 要求确切在线直接父级;只有 `interrupt()` 接受持久化的人类父级地址
+- **仅允许相邻模型消息**——`sendMessage()` 要求确切在线 sender,以及直接 parent 或直接可继续 child;浏览器提示使用独立的 Queue 控制路径
 - **继续执行消息绝不 steering(中途引导)**——父到子的后续消息排入后续轮次;它们绝不会重定向子级当前轮次。
 - **取消收敛期间存在唤醒缺口**——中断信号发出后、driver 进入 idle 前被接受的后续消息会保持排队,直到另一条唤醒发送到达。
 - **驻留仅限进程内**——Activation inbox 与所有权图不会在两个 harness 进程之间协调;对单个持久化存储的并发访问需要持久化邮箱与跨进程租约协议。

+ 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"

+ 130 - 142
packages/subagent/subagent/src/continuation.ts

@@ -55,28 +55,19 @@ import type { ActivationObserver, ActivationTerminal } from './lifecycle.ts'
 import { SubagentError } from './error.ts'
 import type SubagentActivationSetupRegistry from './activation-setup-registry.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 +83,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 +126,22 @@ 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
 }
 
+/** Private scheduling choice for one direct-child delivery. */
+type ChildDelivery = 'queue' | 'steer'
+
+/** Inputs shared by model steering and the human Queue adapter. */
+interface ChildDeliveryOptions {
+  readonly source: MessageSource
+  readonly signal: AbortSignal
+  readonly delivery: ChildDelivery
+}
+
 /**
  * The residency state of one continuable child, derived from Agent quiescence
  * and the owned-child set rather than a second state machine:
@@ -232,7 +219,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 +275,26 @@ 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),
+  })
+}
+
 /**
  * One line telling a parent that a background child is finished and why, in
  * the parent's own task vocabulary.
@@ -471,9 +478,8 @@ export class SubagentContinuationManager {
       return this.submitMaterialized(
         activation,
         request.prompt,
-        { kind: 'user' },
+        { source: { kind: 'user' }, signal: spec.signal, delivery: 'queue' },
         parent,
-        spec.signal,
       )
     })
     return { childId, messageId }
@@ -487,27 +493,70 @@ 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 parent authority, availability, or admission rejects the delivery.
+   * @throws when adjacency, availability, or admission rejects delivery.
    */
-  async followup(
+  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)
+    }
+    return this.deliverToChild(sender, targetId, content, {
+      source: agentMessageSource(sender),
+      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.
+   */
+  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) {
@@ -523,7 +572,7 @@ export class SubagentContinuationManager {
         if (activation.disposal !== undefined) {
           return activation.disposal.then(() => undefined, () => 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. */
@@ -597,88 +646,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
   }
 
@@ -704,18 +694,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 },
       )
@@ -950,7 +938,7 @@ export class SubagentContinuationManager {
     parent: Agent,
     childId: SessionId,
     content: ContentBlock[],
-    options: SubagentFollowupOptions,
+    options: ChildDeliveryOptions,
   ): Promise<MessageId> {
     const query = this.requireSessionQuery()
     let observation: SessionObservation
@@ -1001,27 +989,25 @@ 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 {
-      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. */
@@ -1203,15 +1189,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.source.kind === 'agent-message'
+      ? 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.
@@ -1254,11 +1243,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. */
@@ -1273,7 +1261,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)
   }
 
   /**

+ 51 - 45
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
@@ -33,7 +33,7 @@ import { Context } from '@deepseek-ai/cordis'
 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 { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
@@ -67,9 +67,8 @@ 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'
@@ -77,6 +76,7 @@ import { listChildren as listSubagentChildren, listDescendants as listSubagentDe
 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'
@@ -122,14 +122,11 @@ 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'
@@ -239,27 +236,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)
   }
 
   /**
@@ -281,25 +298,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
@@ -455,7 +453,15 @@ export class SubagentRuntime extends TypertRemoteService {
     }
     const content: ContentBlock[] = [...request.content]
     try {
-      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)
     }

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

@@ -0,0 +1,55 @@
+/**
+ * Host-only continuation 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 SubagentRuntime from './index.ts'
+
+/**
+ * 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,
+  )
+}

+ 19 - 4
packages/subagent/subagent/tests/continuation-inheritance.spec.ts

@@ -15,6 +15,7 @@ import type { Agent } from '@deepseek-ai/dsh-agent'
 import AgentLoop from '@deepseek-ai/dsh-agent-loop'
 import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
 import { createUserMessage } from '@deepseek-ai/dsh-llm'
+import type { ContentBlock } from '@deepseek-ai/dsh-llm'
 import SandboxPolicyService, { effectiveSandboxMode, setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy'
 import { SessionId } from '@deepseek-ai/dsh-session'
 import type { SessionEvent } from '@deepseek-ai/dsh-session'
@@ -191,10 +192,24 @@ describe('continuable policy inheritance', () => {
     // The parent widens AFTER the child was created; the resumed child keeps
     // the delegation-time snapshot from its own log.
     setSandboxMode(parent.session, 'danger-full-access')
-    await ctx.subagents.followup(parent, started.childId, [{ type: 'text', text: 'continue please' }], {
-      source: { kind: 'user' },
-      signal: new AbortController().signal,
-    })
+    const manager = (ctx.subagents as unknown as {
+      continuations: {
+        queuePrompt(
+          parent: Agent,
+          childId: SessionId,
+          content: ContentBlock[],
+          source: { kind: 'user' },
+          signal: AbortSignal,
+        ): Promise<string>
+      }
+    }).continuations
+    await manager.queuePrompt(
+      parent,
+      started.childId,
+      [{ type: 'text', text: 'continue please' }],
+      { kind: 'user' },
+      new AbortController().signal,
+    )
     await waitNoActivation(ctx, started.childId)
 
     const loaded = await ctx.sessionPersistence.load(started.childId)

+ 75 - 59
packages/subagent/subagent/tests/continuation.spec.ts

@@ -127,17 +127,26 @@ function userTexts(events: readonly SessionEvent[]): string[] {
     : [])
 }
 
-function followup(
+function queuePrompt(
   ctx: Context,
   parent: Agent,
   childId: SessionId,
   content: ReturnType<typeof message>,
   signal: AbortSignal = testSignal,
 ) {
-  return ctx.subagents.followup(parent, childId, content, {
-    source: { kind: 'user' },
-    signal,
-  })
+  const manager = (ctx.subagents as unknown as {
+    continuations?: {
+      queuePrompt(
+        parent: Agent,
+        childId: SessionId,
+        content: ReturnType<typeof message>,
+        source: { kind: 'user' },
+        signal: AbortSignal,
+      ): Promise<string>
+    }
+  }).continuations
+  if (manager === undefined) throw new Error('expected a bound continuation manager')
+  return manager.queuePrompt(parent, childId, content, { kind: 'user' }, signal)
 }
 
 /**
@@ -317,7 +326,7 @@ describe('SubagentRuntime.startContinuable', () => {
     expect(loaded.events.find(event => event.type === 'subagent/descriptor')?.data)
       .toMatchObject({ agentReasoningEffort: 'max' })
 
-    await followup(ctx, parent, started.childId, message('resume selected reasoning'))
+    await queuePrompt(ctx, parent, started.childId, message('resume selected reasoning'))
     await waitNoActivation(ctx, started.childId)
     expect(childEfforts).toEqual(['max', 'max'])
   })
@@ -464,7 +473,7 @@ describe('SubagentRuntime.startContinuable', () => {
     await fresh.plugin(SubagentRuntime)
     await fresh.plugin(SubagentSpawn, { providerName: 'spawn' })
     const freshParent = fresh.agentLoop.create(SessionId('routeless-resume'), {})
-    await followup(fresh, freshParent, started.childId, message('resume routeless'))
+    await queuePrompt(fresh, freshParent, started.childId, message('resume routeless'))
 
     const resumed = await vi.waitFor(() => {
       const found = fresh.agents.get(started.childId)
@@ -516,21 +525,21 @@ describe('SubagentRuntime.startContinuable', () => {
     expect(descriptor?.data).toMatchObject({ persona: 'You are scoped.' })
 
     // Cold resume reconstructs the declared composition from that descriptor.
-    await followup(ctx, parent, started.childId, message('resume it'))
+    await queuePrompt(ctx, parent, started.childId, message('resume it'))
     await waitNoActivation(ctx, started.childId)
     const resumed = await ctx.sessionPersistence.load(started.childId)
     expect(hasUserText(resumed.events, 'resume it')).toBe(true)
   })
 })
 
-describe('SubagentRuntime.followup residency routing', () => {
+describe('direct-child Queue residency routing', () => {
   it('fails a cold follow-up when Session query is unavailable', async () => {
     const { ctx, parent } = await setupWith(new MockAdapter([]), {
       persistence: false,
       sessionQuery: false,
     })
 
-    await expect(followup(ctx, parent, SessionId('cold-without-query'), message('continue')))
+    await expect(queuePrompt(ctx, parent, SessionId('cold-without-query'), message('continue')))
       .rejects.toMatchObject({ code: 'CONTINUATION_UNAVAILABLE' })
   })
 
@@ -548,8 +557,8 @@ describe('SubagentRuntime.followup residency routing', () => {
     expect(child?.status).toBe('running')
 
     // Both messages queue behind the open turn, in call order.
-    const firstMessage = await followup(ctx, parent, started.childId, message('first follow-up'))
-    const secondMessage = await followup(ctx, parent, started.childId, message('second follow-up'))
+    const firstMessage = await queuePrompt(ctx, parent, started.childId, message('first follow-up'))
+    const secondMessage = await queuePrompt(ctx, parent, started.childId, message('second follow-up'))
     expect(firstMessage).not.toBe(secondMessage)
     // Still the same Activation: no second child Agent was created.
     expect(ctx.agents.get(started.childId)).toBe(child)
@@ -565,7 +574,7 @@ describe('SubagentRuntime.followup residency routing', () => {
     const started = await ctx.subagents.startContinuable(startSpec(parent))
     await waitNoActivation(ctx, started.childId)
 
-    const messageId = await followup(ctx, parent, started.childId, message('continue please'))
+    const messageId = await queuePrompt(ctx, parent, started.childId, message('continue please'))
     expect(messageId).toBeTypeOf('string')
     await waitNoActivation(ctx, started.childId)
 
@@ -596,7 +605,7 @@ describe('SubagentRuntime.followup residency routing', () => {
     disposeProvider()
     expect(ctx.subagents.getProvider('retired')).toBeUndefined()
 
-    await expect(followup(ctx, parent, started.childId, message('continue without provider')))
+    await expect(queuePrompt(ctx, parent, started.childId, message('continue without provider')))
       .resolves.toBeTypeOf('string')
     await waitNoActivation(ctx, started.childId)
     await vi.waitFor(() => { expect(ends).toHaveLength(2) })
@@ -632,7 +641,7 @@ describe('SubagentRuntime.followup residency routing', () => {
     // Waiting retains the handle: the same Agent is still live.
     expect(ctx.agents.get(started.childId)).toBe(child)
 
-    await followup(ctx, parent, started.childId, message('while waiting'))
+    await queuePrompt(ctx, parent, started.childId, message('while waiting'))
     // Woken back to running on the SAME Activation.
     expect(ctx.agents.get(started.childId)).toBe(child)
 
@@ -652,7 +661,7 @@ describe('SubagentRuntime.followup residency routing', () => {
     await waitNoActivation(ctx, started.childId)
     const stranger = ctx.agentLoop.create(SessionId('stranger'), { provider: 'mock', model: 'mock' })
 
-    await expect(followup(ctx, stranger, started.childId, message('mine now')))
+    await expect(queuePrompt(ctx, stranger, started.childId, message('mine now')))
       .rejects.toThrow(/belongs to another parent session/)
   })
 
@@ -670,13 +679,13 @@ describe('SubagentRuntime.followup residency routing', () => {
     const oneShotId = run.id
     await run.dispose()
 
-    await expect(followup(ctx, parent, oneShotId, message('continue')))
+    await expect(queuePrompt(ctx, parent, oneShotId, message('continue')))
       .rejects.toThrow(/no supported continuation state/)
   })
 
   it('reports an unknown child id as unavailable', async () => {
     const { ctx, parent } = await setup([])
-    await expect(followup(ctx, parent, SessionId('missing'), message('hello')))
+    await expect(queuePrompt(ctx, parent, SessionId('missing'), message('hello')))
       .rejects.toMatchObject({ code: 'NOT_RESUMABLE' })
   })
 
@@ -701,7 +710,7 @@ describe('SubagentRuntime.followup residency routing', () => {
     const reason = new Error('cold inspection cancelled')
 
     try {
-      const delivery = followup(ctx, parent, started.childId, message('cancel me'), controller.signal)
+      const delivery = queuePrompt(ctx, parent, started.childId, message('cancel me'), controller.signal)
       await inspectStarted.promise
       controller.abort(reason)
       await expect(delivery).rejects.toBe(reason)
@@ -717,7 +726,7 @@ describe('SubagentRuntime.followup residency routing', () => {
     const failure = new SubagentError('materialization denied', 'UNAUTHORIZED')
     ctx.agents.resume = () => Promise.reject(failure)
 
-    await expect(followup(ctx, parent, started.childId, message('continue')))
+    await expect(queuePrompt(ctx, parent, started.childId, message('continue')))
       .rejects.toBe(failure)
   })
 
@@ -733,7 +742,7 @@ describe('SubagentRuntime.followup residency routing', () => {
     // exactly one side wins the cutoff. A delivery that loses awaits release and
     // cold-resumes rather than reaching a handle being torn down.
     const delivery = child.whenIdle().then(() =>
-      followup(ctx, parent, started.childId, message('raced')))
+      queuePrompt(ctx, parent, started.childId, message('raced')))
 
     await expect(delivery).resolves.toBeTypeOf('string')
     await waitNoActivation(ctx, started.childId)
@@ -916,11 +925,11 @@ describe('continuable durability and teardown', () => {
     expect(ctx.agents.get(target.childId)).toBe(targetChild)
     expect(ctx.agents.get(grandchild.childId)).toBeDefined()
     expect(ctx.agents.get(sibling.childId)).toBe(siblingChild)
-    await expect(followup(ctx, siblingParent, sibling.childId, message('still live')))
+    await expect(queuePrompt(ctx, siblingParent, sibling.childId, message('still live')))
       .resolves.toBeTypeOf('string')
     await expect(ctx.subagents.startContinuable(startSpec(parent)))
       .rejects.toMatchObject({ code: 'DRAINING' })
-    await expect(followup(ctx, parent, target.childId, message('too late')))
+    await expect(queuePrompt(ctx, parent, target.childId, message('too late')))
       .rejects.toMatchObject({ code: 'DRAINING' })
 
     releaseTarget.resolve(undefined)
@@ -1167,7 +1176,7 @@ describe('continuable durability and teardown', () => {
 
     await expect(ctx.subagents.startContinuable(startSpec(parent)))
       .rejects.toMatchObject({ code: 'DRAINING' })
-    await expect(followup(ctx, parent, started.childId, message('too late')))
+    await expect(queuePrompt(ctx, parent, started.childId, message('too late')))
       .rejects.toMatchObject({ code: 'DRAINING' })
   })
 
@@ -1224,7 +1233,7 @@ describe('continuable durability and teardown', () => {
     })
     observeCancel(child, () => { order.push('cancel') })
 
-    const delivery = followup(ctx, parent, started.childId, message('before drain'))
+    const delivery = queuePrompt(ctx, parent, started.childId, message('before drain'))
     // Let the child-lock operation reach the live admission cutoff. Admission
     // and inbox submission must then complete in one synchronous span.
     await Promise.resolve()
@@ -1243,7 +1252,7 @@ describe('continuable durability and teardown', () => {
     const started = await ctx.subagents.startContinuable(startSpec(parent))
     await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
     // Accepted into the inbox, but this queued turn never opens.
-    await followup(ctx, parent, started.childId, message('never logged'))
+    await queuePrompt(ctx, parent, started.childId, message('never logged'))
 
     const drained = drainManager(ctx)
     hold.resolve(undefined)
@@ -1281,7 +1290,7 @@ describe('continuable review regressions', () => {
       return handle
     })
 
-    const delivery = followup(
+    const delivery = queuePrompt(
       ctx,
       originalParent.agent,
       started.childId,
@@ -1321,7 +1330,7 @@ describe('continuable review regressions', () => {
       throw new Error('synthetic inbox failure')
     }
 
-    await expect(followup(ctx, parent, started.childId, message('throws')))
+    await expect(queuePrompt(ctx, parent, started.childId, message('throws')))
       .rejects.toThrow(/synthetic inbox failure/)
     expect(activation.accepted.size).toBe(0)
 
@@ -1361,7 +1370,7 @@ describe('continuable review regressions', () => {
 
     const controller = new AbortController()
     controller.abort('caller gave up')
-    await expect(followup(ctx, parent, started.childId, message('cancelled'), controller.signal))
+    await expect(queuePrompt(ctx, parent, started.childId, message('cancelled'), controller.signal))
       .rejects.toThrow()
 
     // Nothing was enqueued, so no later turn can carry it.
@@ -1387,7 +1396,7 @@ describe('continuable review regressions', () => {
 
     // A cold resume is a new epoch: it must report its OWN answer, never the
     // previous epoch's, which the replayed transcript still contains.
-    await followup(ctx, parent, started.childId, message('again'))
+    await queuePrompt(ctx, parent, started.childId, message('again'))
     await waitNoActivation(ctx, started.childId)
     await vi.waitFor(() => { expect(ends).toHaveLength(2) })
     expect(ends[1]!.lastAssistantMessage).toEqual([{ type: 'text', text: 'second answer' }])
@@ -1443,7 +1452,7 @@ describe('continuable review regressions', () => {
       if (subject === parent) return next()
       return { kind: 'reject' }
     })
-    await followup(ctx, parent, started.childId, message('again'))
+    await queuePrompt(ctx, parent, started.childId, message('again'))
     await waitNoActivation(ctx, started.childId)
 
     await vi.waitFor(() => { expect(ends).toHaveLength(1) })
@@ -1567,7 +1576,7 @@ describe('continuable review regressions', () => {
     await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
     // Queue a turn, then cancel so it is discarded rather than dequeued. The
     // Activation must still reach settlement instead of waiting on that id.
-    await followup(ctx, parent, started.childId, message('discarded'))
+    await queuePrompt(ctx, parent, started.childId, message('discarded'))
 
     const drained = drainManager(ctx)
     hold.resolve(undefined)
@@ -1587,13 +1596,13 @@ describe('continuable review regressions', () => {
     const child = ctx.agents.get(started.childId)!
 
     // Cancel from the synchronous enqueue observer: the discard fires after the
-    // id is recorded but before `followup()` returns.
+    // id is recorded but before `queuePrompt()` returns.
     const off = child.ctx.on('agent/inbox/inserted', ({ message }) => {
       if (message.content.some(block => block.type === 'text' && block.text === 'doomed')) {
         child.cancel({ kind: 'user' })
       }
     })
-    await followup(ctx, parent, started.childId, message('doomed'))
+    await queuePrompt(ctx, parent, started.childId, message('doomed'))
     off()
 
     releaseFirst.resolve(undefined)
@@ -1618,14 +1627,14 @@ describe('continuable review regressions', () => {
     }).continuations
     const activation = manager.activations.get(started.childId)!
 
-    await followup(ctx, parent, started.childId, message('queued'))
+    await queuePrompt(ctx, parent, started.childId, message('queued'))
     expect(activation.accepted.size).toBe(1)
     const off = child.ctx.on('agent/inbox/inserted', ({ message }) => {
       if (message.content.some(block => block.type === 'text' && block.text === 'doomed')) {
         child.cancel({ kind: 'user' })
       }
     })
-    await followup(ctx, parent, started.childId, message('doomed'))
+    await queuePrompt(ctx, parent, started.childId, message('doomed'))
     off()
 
     expect(activation.accepted.size).toBe(0)
@@ -1673,7 +1682,7 @@ describe('continuable review regressions', () => {
     const started = await ctx.subagents.startContinuable(startSpec(parent))
     await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
     const child = ctx.agents.get(started.childId)
-    await followup(ctx, parent, started.childId, message('queued'))
+    await queuePrompt(ctx, parent, started.childId, message('queued'))
 
     expect(registeredAtEnqueue.length).toBeGreaterThan(0)
     expect(registeredAtEnqueue).not.toContain(false)
@@ -1701,8 +1710,8 @@ function settlementNotices(agent: Agent): { sender: string; text: string; summar
   })
 }
 
-describe('continuable report delivery', () => {
-  it('wakes an idle parent for a next-step report', async () => {
+describe('continuable adjacent-Agent delivery', () => {
+  it('steers an idle direct parent and preserves sender attribution', async () => {
     const releaseChild = Promise.withResolvers<undefined>()
     const adapter = new GatedAdapter([
       { chunks: textResponse('child answer'), gate: releaseChild.promise },
@@ -1717,17 +1726,25 @@ describe('continuable report delivery', () => {
     const child = ctx.agents.get(started.childId)
     expect(child).toBeDefined()
 
-    const messageId = await ctx.subagents.reportFrom(child!, message('an explicit report'), {
-      delivery: 'next-step',
+    const messageId = await ctx.subagents.sendMessage(child!, parent.id, message('an explicit message'), {
       signal: testSignal,
     })
 
     await vi.waitFor(() => {
       expect(adapter.requests.filter(request => request.sessionId === parent.id)).toHaveLength(1)
     })
-    const report = parent.session.events.flatMap(event => event.type === 'user/message'
-      && event.data.source.kind === 'subagent-report' ? [event.data] : [])[0]
-    expect(report?.id).toBe(messageId)
+    const delivered = parent.session.events.flatMap(event => event.type === 'user/message'
+      && event.data.source.kind === 'agent-message' ? [event.data] : [])[0]
+    expect(delivered?.id).toBe(messageId)
+    expect(delivered?.source).toMatchObject({
+      kind: 'agent-message',
+      form: 'relay',
+      senderSessionId: started.childId,
+    })
+    expect(delivered?.content).toEqual([
+      { type: 'text', text: `Agent ${started.childId} sent a message:` },
+      { type: 'text', text: 'an explicit message' },
+    ])
 
     releaseChild.resolve(undefined)
     await waitNoActivation(ctx, started.childId)
@@ -1756,7 +1773,7 @@ describe('continuable settlement delivery', () => {
     )
   })
 
-  it('delivers even when the child already reported for itself', async () => {
+  it('delivers settlement even when the child already sent a message', async () => {
     const { ctx, parent } = await setup([textResponse('the answer'), textResponse('parent ack')])
     const started = await ctx.subagents.startContinuable(startSpec(parent))
     const child = await vi.waitFor(() => {
@@ -1764,8 +1781,7 @@ describe('continuable settlement delivery', () => {
       expect(live).toBeDefined()
       return live!
     })
-    await ctx.subagents.reportFrom(child, message('an explicit report'), {
-      delivery: 'quiet',
+    await ctx.subagents.sendMessage(child, parent.id, message('an explicit message'), {
       signal: testSignal,
     })
     await waitNoActivation(ctx, started.childId)
@@ -1823,7 +1839,7 @@ describe('continuable settlement delivery', () => {
     })
 
     const started = await ctx.subagents.startContinuable(startSpec(parent))
-    await followup(ctx, parent, started.childId, message('second task'))
+    await queuePrompt(ctx, parent, started.childId, message('second task'))
     releaseFirst.resolve(undefined)
     await waitNoActivation(ctx, started.childId)
 
@@ -1857,7 +1873,7 @@ describe('continuable settlement delivery', () => {
     const started = await ctx.subagents.startContinuable(startSpec(parent))
     // Queued while turn 1 still runs, so turn 2 opens and claims it without a
     // second model call: the Activation is mid-turn when the drain cancels it.
-    await followup(ctx, parent, started.childId, message('second task'))
+    await queuePrompt(ctx, parent, started.childId, message('second task'))
     releaseFirst.resolve(undefined)
     await atCheckpoint.promise
     const drained = drainManager(ctx)
@@ -1953,7 +1969,7 @@ describe('continuable settlement delivery', () => {
     // Context maintenance folds into `idle` and defers waking work, so this
     // delivery is accepted with no turn to claim it.
     const maintaining = child.runMaintenance(async () => { await releaseMaintenance.promise })
-    await followup(ctx, parent, started.childId, message('never runs'))
+    await queuePrompt(ctx, parent, started.childId, message('never runs'))
     const drained = drainManager(ctx)
     releaseMaintenance.resolve(undefined)
     releaseGrandchild.resolve(undefined)
@@ -2258,7 +2274,7 @@ describe('continuable lifecycle observation', () => {
     await vi.waitFor(() => { expect(ends).toHaveLength(1) })
 
     // A cold resume is a NEW epoch with its own pair.
-    await followup(ctx, parent, started.childId, message('again'))
+    await queuePrompt(ctx, parent, started.childId, message('again'))
     await waitNoActivation(ctx, started.childId)
     await vi.waitFor(() => { expect(ends).toHaveLength(2) })
 
@@ -2314,7 +2330,7 @@ describe('continuable public API', () => {
 
     const controller = new AbortController()
     controller.abort('caller gave up')
-    await expect(followup(ctx, parent, started.childId, message('aborted'), controller.signal))
+    await expect(queuePrompt(ctx, parent, started.childId, message('aborted'), controller.signal))
       .rejects.toThrow()
 
     const loaded = await ctx.sessionPersistence.load(started.childId)
@@ -2332,7 +2348,7 @@ describe('continuable public API', () => {
     await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
 
     const controller = new AbortController()
-    await followup(ctx, parent, started.childId, message('survives'), controller.signal)
+    await queuePrompt(ctx, parent, started.childId, message('survives'), controller.signal)
     // After acceptance the manager owns the Activation independently.
     controller.abort('caller gave up')
 
@@ -2361,7 +2377,7 @@ describe('continuable errors', () => {
     }).continuations
     manager.activations.delete(started.childId)
 
-    await expect(followup(ctx, parent, started.childId, message('hello')))
+    await expect(queuePrompt(ctx, parent, started.childId, message('hello')))
       .rejects.toThrow(SubagentError)
     expect(ctx.agents.get(started.childId)).toBe(child)
     hold.resolve(undefined)
@@ -2378,7 +2394,7 @@ describe('continuable errors', () => {
     // A stale parent reference: same id, not the exact live entry.
     const stale = { ...parent, id: parent.id } as unknown as Agent
 
-    await expect(followup(ctx, stale, started.childId, message('stale')))
+    await expect(queuePrompt(ctx, stale, started.childId, message('stale')))
       .rejects.toMatchObject({ code: 'UNAUTHORIZED' })
     void child
   })
@@ -2493,7 +2509,7 @@ describe('continuable errors', () => {
       })
 
     // The resumed Activation runs on the declared route, not the parent's.
-    await followup(ctx, parent, started.childId, message('again'))
+    await queuePrompt(ctx, parent, started.childId, message('again'))
     await vi.waitFor(() => {
       expect(ctx.agents.get(started.childId)?.options).toMatchObject({
         model: 'child-model',
@@ -2547,8 +2563,8 @@ describe('SubagentRuntime.interrupt', () => {
     const started = await ctx.subagents.startContinuable(startSpec(parent))
     await vi.waitFor(() => { expect(adapter.requests).toHaveLength(1) })
     const child = ctx.agents.get(started.childId)!
-    await followup(ctx, parent, started.childId, message('parked B'))
-    await followup(ctx, parent, started.childId, message('parked C'))
+    await queuePrompt(ctx, parent, started.childId, message('parked B'))
+    await queuePrompt(ctx, parent, started.childId, message('parked C'))
     const cancelSpy = vi.spyOn(child, 'cancel')
 
     ctx.subagents.interrupt(started.childId, { kind: 'user', parentSessionId: parent.id })
@@ -2567,7 +2583,7 @@ describe('SubagentRuntime.interrupt', () => {
 
     // Only an explicit waking send restores the driver; the parked items then
     // run before it in the existing FIFO order.
-    await followup(ctx, parent, started.childId, message('waking D'))
+    await queuePrompt(ctx, parent, started.childId, message('waking D'))
     await waitNoActivation(ctx, started.childId)
     const loaded = await ctx.sessionPersistence.load(started.childId)
     expect(userTexts(loaded.events)).toEqual(['child task', 'parked B', 'parked C', 'waking D'])

+ 24 - 20
packages/subagent/subagent/tests/control.spec.ts

@@ -12,6 +12,7 @@ import SubagentRuntime, {
   type SubagentListEntry,
   type SubagentPromptRequestId,
 } from '@deepseek-ai/dsh-subagent'
+import { queueSubagentPrompt, type HostPromptQueue } from '@deepseek-ai/dsh-subagent/internal'
 
 const PARENT = SessionId('parent')
 const CHILD = SessionId('child')
@@ -30,6 +31,11 @@ async function bench(live?: Record<string, { status: 'running' | 'idle' }>) {
   return { ctx, subagents: ctx.subagents }
 }
 
+/** Spy on the private human-Queue adapter without widening the public service. */
+function promptDelivery(subagents: SubagentRuntime) {
+  return vi.spyOn(subagents as unknown as HostPromptQueue, queueSubagentPrompt)
+}
+
 function childRow(id: SessionId, activity: 'running' | 'inactive'): SubagentListEntry {
   return { kind: 'child', id, mode: 'continuable', label: 'worker', activity, hasChildren: false }
 }
@@ -142,7 +148,7 @@ describe('subagent catalog Remote', () => {
 describe('subagent prompt Remote', () => {
   it('rejects empty parent and child ids before delivery', async () => {
     const { subagents } = await bench({ [PARENT]: { status: 'idle' } })
-    const followup = vi.spyOn(subagents, 'followup')
+    const delivery = promptDelivery(subagents)
 
     const cases: readonly {
       readonly field: 'parentSessionId' | 'childSessionId'
@@ -155,47 +161,45 @@ describe('subagent prompt Remote', () => {
       await expect(subagents.prompt(request, signal))
         .rejects.toMatchObject({ failure: emptyIdFailure('subagent.prompt', field) })
     }
-    expect(followup).not.toHaveBeenCalled()
+    expect(delivery).not.toHaveBeenCalled()
   })
 
   it('forwards non-text content blocks without narrowing them', async () => {
     const { subagents } = await bench({ [PARENT]: { status: 'idle' } })
-    const followup = vi.spyOn(subagents, 'followup').mockResolvedValue('m-content' as MessageId)
+    const delivery = promptDelivery(subagents).mockResolvedValue('m-content' as MessageId)
     const content = [{ type: 'reasoning' as const, text: 'retain this block' }]
 
     await expect(subagents.prompt({ ...promptRequest(), content }, signal))
       .resolves.toEqual({ messageId: 'm-content' })
-    expect(followup.mock.calls[0]?.[2]).toEqual(content)
+    expect(delivery.mock.calls[0]?.[2]).toEqual(content)
   })
 
   it('delivers the content under the caller-minted identity and canonical browser zone', async () => {
     const { subagents } = await bench({ [PARENT]: { status: 'idle' } })
-    const followup = vi.spyOn(subagents, 'followup').mockResolvedValue('m-1' as MessageId)
+    const delivery = promptDelivery(subagents).mockResolvedValue('m-1' as MessageId)
 
     await expect(subagents.prompt(promptRequest('Asia/Shanghai'), signal))
       .resolves.toEqual({ messageId: 'm-1' })
-    expect(followup).toHaveBeenCalledWith(
+    expect(delivery).toHaveBeenCalledWith(
       { status: 'idle' },
       CHILD,
       [{ type: 'text', text: 'continue' }],
-      {
-        source: { kind: 'user', rpcId: REQUEST_ID, clientTimeZone: 'Asia/Shanghai' },
-        signal,
-      },
+      { kind: 'user', rpcId: REQUEST_ID, clientTimeZone: 'Asia/Shanghai' },
+      signal,
     )
   })
 
   it('omits the zone from the durable source when the browser reported none', async () => {
     const { subagents } = await bench({ [PARENT]: { status: 'idle' } })
-    const followup = vi.spyOn(subagents, 'followup').mockResolvedValue('m-2' as MessageId)
+    const delivery = promptDelivery(subagents).mockResolvedValue('m-2' as MessageId)
 
     await expect(subagents.prompt(promptRequest(), signal)).resolves.toEqual({ messageId: 'm-2' })
-    expect(followup.mock.calls[0]?.[3].source).toEqual({ kind: 'user', rpcId: REQUEST_ID })
+    expect(delivery.mock.calls[0]?.[3]).toEqual({ kind: 'user', rpcId: REQUEST_ID })
   })
 
   it('accepts UTC and rejects an empty, untrimmed, malformed, or unknown zone', async () => {
     const { subagents } = await bench({ [PARENT]: { status: 'idle' } })
-    vi.spyOn(subagents, 'followup').mockResolvedValue('m-3' as MessageId)
+    promptDelivery(subagents).mockResolvedValue('m-3' as MessageId)
 
     await expect(subagents.prompt(promptRequest('UTC'), signal)).resolves.toEqual({ messageId: 'm-3' })
     for (const zone of ['', ' UTC', 'Shanghai', 'Nowhere/Nowhere']) {
@@ -207,17 +211,17 @@ describe('subagent prompt Remote', () => {
 
   it('refuses delivery when the exact parent Agent is not live', async () => {
     const { subagents } = await bench()
-    const followup = vi.spyOn(subagents, 'followup')
+    const delivery = promptDelivery(subagents)
 
     await expect(subagents.prompt(promptRequest(), signal)).rejects.toMatchObject({
       failure: { code: 'subagent-parent-unavailable', details: { parentSessionId: PARENT } },
     })
-    expect(followup).not.toHaveBeenCalled()
+    expect(delivery).not.toHaveBeenCalled()
   })
 
   it('maps each admission failure onto its stable code and hides the rest', async () => {
     const { subagents } = await bench({ [PARENT]: { status: 'idle' } })
-    const followup = vi.spyOn(subagents, 'followup')
+    const delivery = promptDelivery(subagents)
     const cases: readonly [string, string][] = [
       ['NOT_RESUMABLE', 'subagent-not-resumable'],
       ['UNAUTHORIZED', 'subagent-unauthorized'],
@@ -226,12 +230,12 @@ describe('subagent prompt Remote', () => {
       ['NO_PROVIDER', 'internal'],
     ]
     for (const [thrown, code] of cases) {
-      followup.mockRejectedValue(new SubagentError('refused', thrown))
+      delivery.mockRejectedValue(new SubagentError('refused', thrown))
       await expect(subagents.prompt(promptRequest(), signal))
         .rejects.toMatchObject({ failure: { code } })
     }
 
-    followup.mockRejectedValue(new Error('inbox exploded'))
+    delivery.mockRejectedValue(new Error('inbox exploded'))
     await expect(subagents.prompt(promptRequest(), signal))
       .rejects.toMatchObject({ failure: { code: 'internal', message: 'subagent prompt failed' } })
   })
@@ -239,7 +243,7 @@ describe('subagent prompt Remote', () => {
   it('answers a caller-cancelled delivery as cancelled rather than a failure', async () => {
     const { subagents } = await bench({ [PARENT]: { status: 'idle' } })
     const aborted = new AbortController()
-    vi.spyOn(subagents, 'followup').mockImplementation(() => {
+    promptDelivery(subagents).mockImplementation(() => {
       aborted.abort()
       return Promise.reject(new SubagentError('gone', 'NOT_RESUMABLE'))
     })
@@ -250,7 +254,7 @@ describe('subagent prompt Remote', () => {
 
   it('preserves a cancellation reported by the continuation operation', async () => {
     const { subagents } = await bench({ [PARENT]: { status: 'idle' } })
-    vi.spyOn(subagents, 'followup')
+    promptDelivery(subagents)
       .mockRejectedValue(new SubagentError('stopped', 'CANCELLED'))
 
     await expect(subagents.prompt(promptRequest(), signal))

+ 2 - 2
packages/subagent/subagent/tests/service.spec.ts

@@ -153,11 +153,11 @@ describe('SubagentRuntime', () => {
       request: baseRequest(),
       signal: new AbortController().signal,
     })).rejects.toMatchObject({ code: 'CONTINUATION_UNAVAILABLE' })
-    await expect(subagents.followup(
+    await expect(subagents.sendMessage(
       fakeParent(),
       SessionId('child'),
       [{ type: 'text', text: 'hello' }],
-      { source: { kind: 'user' }, signal: new AbortController().signal },
+      { signal: new AbortController().signal },
     )).rejects.toMatchObject({ code: 'CONTINUATION_UNAVAILABLE' })
   })
 

+ 2 - 2
packages/subagent/tool-subagent-control/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/subagent/tool-subagent-control/README.md
-README.md: 18cdd9d22be3ce30057bcaa518f3795fcc89f48d
-README.zh.md: 46f31d7dcba3d4e05ddf212dda087fd3a671b69e
+README.md: a1a4f45e982db5d936ff6d6d79e3c7deffc414b1
+README.zh.md: da763aafcfae0a345adb412c945a348d74eb764c

+ 4 - 4
packages/subagent/tool-subagent-control/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`dsh-tool-subagent-control` adds the global control tools for continuable children: `send_message` delivers a follow-up message that becomes the child's next turn, `interrupt_agent` stops a child's current turn while keeping its queue and descendants intact, and `list_agents` (from the separately loadable `list-agents` plugin) lists continuable children by durable id and label. The tools are global, so any number of delegation tools never duplicates them. These tools cover only the parent-to-child direction; the child-to-parent direction belongs to the independently installed `dsh-tool-subagent-report`. No tool's presence decides whether a delegation tool starts continuable work.
+`dsh-tool-subagent-control` adds the global control tools for continuable children: `send_message` steers a direct child at its nearest step, `interrupt_agent` stops a child's current turn while keeping its inbox and descendants intact, and `list_agents` (from the separately loadable `list-agents` plugin) lists continuable children by durable id and label. The tools are global, so any number of delegation tools never duplicates them. The temporary child-scoped `report` tool uses the same service operation in the reverse direction. No tool's presence decides whether a delegation tool starts continuable work.
 
 ## Table of Contents
 
@@ -68,11 +68,11 @@ This section explains what the tools delegate to the subagent service; the obser
 
 ### Design concept
 
-Thin adapters over `ctx.subagents.followup()`, `interrupt()`, and the list projections; the tools perform no lifecycle routing. Residency, cold resume, and interrupt authorization belong to the service, and the tools pass the exact live calling agent (`exec.agent`) as the authority the service verifies against the target's recorded lineage.
+Thin adapters over `ctx.subagents.sendMessage()`, `interrupt()`, and the list projections; the tools perform no lifecycle routing. Residency, cold resume, and authorization belong to the service, and the tools pass the exact live calling agent (`exec.agent`) as both sender and authority.
 
 ### Delivery and signal ownership
 
-The tool forwards its execution signal, which owns admission only until inbox acceptance. Once the child accepts a message, the accepted turn cannot be cancelled through this tool. Every message is recorded with the coordinator source `{ kind: 'coordinator', senderSessionId: parent.id }`, which the service retains but never treats as authority.
+The tool forwards its execution signal, which owns admission only until inbox acceptance. Once the child accepts a message, it cannot be cancelled through this tool. Every message is framed as `Agent <sender-id> sent a message:` and recorded with `{ kind: 'agent-message', form: 'relay', senderSessionId: parent.id }`; the service derives that attribution and never treats it as authority.
 
 ### Listing projection
 
@@ -97,7 +97,7 @@ Read these pages when the package-level contract is not enough; they move from t
 
 - [Subagent subsystem](../../../docs/subsystems/subagent.md) — continuable children, activations, inbox, interrupt, and follow-up authority.
 - [dsh-tool-subagent](../tool-subagent/README.md) — the delegation tool that starts continuable children.
-- [dsh-tool-subagent-report](../tool-subagent-report/README.md) — the child-to-parent report channel.
+- [dsh-tool-subagent-report](../tool-subagent-report/README.md) — the temporary child-to-parent adapter over the same service operation.
 - [Generated tool catalog](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent-control) — the three tool schemas.
 
 -----

+ 4 - 4
packages/subagent/tool-subagent-control/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-`dsh-tool-subagent-control` 为可继续子级添加全局控制工具:`send_message` 投递一条成为子级下一轮次的后续消息,`interrupt_agent` 停止子级当前轮次但保留其队列与后代,`list_agents`(来自可单独加载的 `list-agents` 插件)按持久化 id 与标签列出可继续子级。这些工具是全局的,因此任意数量的委派工具都不会产生重复。这些工具只覆盖父到子方向;子到父方向属于独立安装的 `dsh-tool-subagent-report`。是否加载这些工具不会决定委派工具是否启动可继续工作。
+`dsh-tool-subagent-control` 为可继续子级添加全局控制工具:`send_message` 在直接 child 最近的 step 进行 steer,`interrupt_agent` 停止 child 当前轮次但保留其 inbox 与后代,`list_agents`(来自可单独加载的 `list-agents` 插件)按持久化 id 与标签列出可继续 child。这些工具是全局的,因此任意数量的委派工具都不会产生重复。临时的 child 作用域 `report` 工具反向使用同一个服务操作。是否加载这些工具不会决定委派工具是否启动可继续工作。
 
 ## 目录
 
@@ -68,11 +68,11 @@ kind: "package-reference"
 
 ### 设计理念
 
-`ctx.subagents.followup()`、`interrupt()` 与列表投影之上的轻量适配器;工具不执行任何生命周期路由。驻留、冷恢复与中断授权归服务所有,工具把确切在线的调用 agent(`exec.agent`)作为服务对照目标已记录 lineage 校验的权限凭据传入。
+`ctx.subagents.sendMessage()`、`interrupt()` 与列表投影之上的轻量适配器;工具不执行任何生命周期路由。驻留、冷恢复与授权归服务所有,工具把确切在线的调用 Agent(`exec.agent`)同时作为 sender 与权限凭据传入。
 
 ### 投递与信号所有权
 
-工具转发其执行信号,该信号只在 inbox 接受之前掌管准入。子级一旦接受消息,已接受的轮次便无法再通过本工具取消。每条消息都记录协调者来源 `{ kind: 'coordinator', senderSessionId: parent.id }`;服务会保留该来源,但绝不将其视为权限。
+工具转发其执行信号,该信号只在 inbox 接受之前掌管准入。child 一旦接受消息,该消息便无法再通过本工具取消。每条消息都以 `Agent <sender-id> sent a message:` 作为前缀,并记录 `{ kind: 'agent-message', form: 'relay', senderSessionId: parent.id }`;该来源信息由服务推导,且绝不被视为权限。
 
 ### 列表投影
 
@@ -97,7 +97,7 @@ kind: "package-reference"
 
 - [Subagent 子系统](../../../docs/subsystems/subagent.zh.md)——可继续子级、Activation、inbox、中断与后续消息权限。
 - [dsh-tool-subagent](../tool-subagent/README.zh.md)——启动可继续子级的委派工具。
-- [dsh-tool-subagent-report](../tool-subagent-report/README.zh.md)——子到父的上报通道
+- [dsh-tool-subagent-report](../tool-subagent-report/README.zh.md)——同一服务操作之上的临时 child 到 parent 适配器
 - [生成工具目录](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-subagent-control)——三个工具的 schema。
 
 -----

+ 11 - 16
packages/subagent/tool-subagent-control/src/index.ts

@@ -1,6 +1,6 @@
 /**
  * The globally named `send_message` and `interrupt_agent` tools: thin
- * model-facing adapters over `ctx.subagents.followup()` and
+ * model-facing adapters over `ctx.subagents.sendMessage()` and
  * `ctx.subagents.interrupt()`. They perform no lifecycle routing of their own —
  * residency, cold resume, and interrupt authorization belong to the subagent
  * service — and they live apart from the provider-bound
@@ -26,11 +26,10 @@ export function apply(ctx: Context): void {
   ctx.tools.register(defineTool({
     name: 'send_message',
     description:
-      '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 background subagent by its subagent id, continuing the same conversation. If it '
+      + '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 subagent — only confirmation that the message was delivered. '
+      + 'A failure means the message was NOT delivered.',
     parameters: {
       subagent_id: {
         type: 'string',
@@ -53,24 +52,20 @@ export function apply(ctx: Context): void {
       },
       render: (args, _value) => [{
         type: 'text',
-        text: `message queued as the next turn for subagent ${args.subagent_id}`,
+        text: `message delivered to subagent ${args.subagent_id}`,
       }],
     },
     async execute(args, exec) {
-      const parent = exec.agent
-      if (!parent) {
-        // Parent authority requires an exact live calling agent.
+      const sender = exec.agent
+      if (!sender) {
         throw new Error('send_message requires a calling agent (exec.agent was undefined)')
       }
       const message: ContentBlock[] = [{ type: 'text', text: args.message }]
-      const messageId = await ctx.subagents.followup(
-        parent,
+      const messageId = await ctx.subagents.sendMessage(
+        sender,
         SessionId(args.subagent_id),
         message,
-        {
-          source: { kind: 'coordinator', form: 'relay', senderSessionId: parent.id },
-          signal: exec.signal,
-        },
+        { signal: exec.signal },
       )
       return { messageId }
     },

+ 23 - 11
packages/subagent/tool-subagent-control/tests/tool-subagent-control.spec.ts

@@ -111,11 +111,10 @@ describe('dsh-tool-subagent-control', () => {
     // The continuable path has no Task, so the schema must not promise one.
     expect(schemas[0]!.description).not.toContain('job_output')
     expect(schemas[0]!.description).not.toContain('job id')
-    // Follow-up ordering is model-visible: it cannot redirect the open turn.
-    expect(schemas[0]!.description).toContain('next turn')
+    expect(schemas[0]!.description).toContain('nearest step')
   })
 
-  it('cold-resumes a settled child and reports the queued next turn', async () => {
+  it('cold-resumes a settled child and reports delivery', async () => {
     const { ctx, parent } = await setup([textResponse('first answer'), textResponse('second answer')])
     const started = await ctx.subagents.startContinuable({
       provider: 'spawn',
@@ -131,20 +130,24 @@ describe('dsh-tool-subagent-control', () => {
     }, parent)
 
     expect(result.isError).toBe(false)
-    expect(text(result)).toBe(`message queued as the next turn for subagent ${started.childId}`)
+    expect(text(result)).toBe(`message delivered to subagent ${started.childId}`)
     await waitNoActivation(ctx, started.childId)
 
     const loaded = await ctx.sessionPersistence.load(started.childId)
     const followUp = loaded.events.findLast(event => event.type === 'user/message')
     // The durable message source records the calling agent without granting authority.
     expect(followUp?.type === 'user/message' && followUp.data.source).toEqual({
-      kind: 'coordinator',
+      kind: 'agent-message',
       form: 'relay',
       senderSessionId: parent.id,
     })
+    expect(followUp?.type === 'user/message' && followUp.data.content).toEqual([
+      { type: 'text', text: `Agent ${parent.id} sent a message:` },
+      { type: 'text', text: 'and then?' },
+    ])
   })
 
-  it('queues behind an open turn instead of joining it', async () => {
+  it('steers the nearest step of an open turn', async () => {
     const { ctx, parent, adapter } = await setup([textResponse('first'), textResponse('second')])
     const started = await ctx.subagents.startContinuable({
       provider: 'spawn',
@@ -165,8 +168,11 @@ describe('dsh-tool-subagent-control', () => {
     const prompts = loaded.events.flatMap(event => event.type === 'user/message' && event.data.source.kind !== 'plugin'
       ? event.data.content.flatMap(block => block.type === 'text' ? [block.text] : [])
       : [])
-    // A follow-up is its own later turn, never steering inside the first one.
-    expect(prompts).toEqual(['long work', 'also consider Y'])
+    expect(prompts).toEqual([
+      'long work',
+      `Agent ${parent.id} sent a message:`,
+      'also consider Y',
+    ])
   })
 
   it('reports a delivery failure as an errored, not-delivered result', async () => {
@@ -267,9 +273,9 @@ describe('dsh-tool-subagent-control interrupt_agent', () => {
     expect(cancelSpy).toHaveBeenCalledExactlyOnceWith({ kind: 'parent' }, { keepInbox: true })
     releaseFirst.resolve(undefined)
     await child.whenIdle()
-    // Parked, not resumed: the queued follow-up waits for a waking send.
+    // Parked, not resumed: the steering waits for another waking send.
     expect(adapter.requests).toHaveLength(1)
-    expect(child.inbox.nextTurn).toHaveLength(1)
+    expect(child.inbox.nextStep).toHaveLength(1)
 
     const waking = await callTool(ctx, 'send_message', {
       subagent_id: started.childId,
@@ -281,7 +287,13 @@ describe('dsh-tool-subagent-control interrupt_agent', () => {
     const prompts = loaded.events.flatMap(event => event.type === 'user/message' && event.data.source.kind !== 'plugin'
       ? event.data.content.flatMap(block => block.type === 'text' ? [block.text] : [])
       : [])
-    expect(prompts).toEqual(['long work', 'parked follow-up', 'wake up'])
+    expect(prompts).toEqual([
+      'long work',
+      `Agent ${parent.id} sent a message:`,
+      'parked follow-up',
+      `Agent ${parent.id} sent a message:`,
+      'wake up',
+    ])
   })
 
   it('lets a deep live ancestor interrupt a descendant it did not directly create', async () => {

+ 2 - 2
packages/subagent/tool-subagent-report/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/subagent/tool-subagent-report/README.md
-README.md: 24dac02c11925de9d99264a7ff672f556701faf2
-README.zh.md: 089919d96677528946a000453ff9abdad174e631
+README.md: 19fc7202da4d36b5e32c622b526c9366d498abfd
+README.zh.md: e43357cfca05f0603a373f2f36a26a8a6859f5f3

+ 12 - 18
packages/subagent/tool-subagent-report/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`dsh-tool-subagent-report` gives every continuable in-process child a return channel to the agent that started it: it installs a child-scoped `report` tool plus the prompt guidance that tells the child to use it. The tool and its guidance exist only inside those children — roots, one-shot subagents, remote providers, and sibling scopes never see them. Accepted reports reach the parent as ordinary parent messages, framed as `Background subagent <child-id> reported:`. Continuable mode depends on neither this package nor the control package; this one owns only the child-to-parent direction.
+`dsh-tool-subagent-report` gives every continuable in-process child a temporary child-scoped adapter over the adjacent-Agent messaging service: it installs a `report` tool plus prompt guidance telling the child to use it. Roots, one-shot subagents, remote providers, and sibling scopes never see either registration. Accepted reports reach the direct parent through fixed Steer scheduling and the same framing and provenance as parent-to-child messages. Continuable mode depends on neither this package nor the control package.
 
 ## Table of Contents
 
@@ -41,11 +41,7 @@ Load the subagent service, a backend, the delegation tool in `continuable` mode,
 - name: '@deepseek-ai/dsh-tool-subagent-report'
 ```
 
-| Field | Default | Meaning |
-|---|---|---|
-| `reportDelivery` | `next-step` | Parent scheduling for accepted reports: `next-step` wakes the parent at its nearest step boundary; `quiet` adds the same context without waking it |
-
-The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-tool-subagent-report) is the exhaustive source for every accepted field and its JSDoc.
+The package takes no configuration.
 
 ### What the child gets
 
@@ -53,7 +49,7 @@ Each continuable child gets a `report` tool whose only parameter is `output` —
 
 ### What the parent sees
 
-An accepted report becomes one user-role parent message framed as `Background subagent <child-id> reported:` followed by the child's exact output, with a durable source naming the child. `next-step` delivery wakes an idle parent or joins a running parent's nearest step boundary; `quiet` delivery adds the same context without waking the parent. The tool takes no recipient: the service derives the sole recipient from the child's durable `parentSession`.
+An accepted report becomes one user-role parent message framed as `Agent <child-id> sent a message:` followed by the child's exact output, with a durable `agent-message` source naming the child. Fixed Steer scheduling starts a turn for an idle parent or joins a running parent's nearest step boundary. The tool takes no recipient: it derives the sole recipient from the child's durable `parentSession` and delegates authorization and delivery to `ctx.subagents.sendMessage()`.
 
 ### Scope and direction
 
@@ -75,17 +71,17 @@ The package registers a continuable-child setup contribution rather than a globa
 
 ### Delivery scheduling
 
-`next-step` uses `parent.steer()`: a running parent receives the report at its nearest safe step boundary, an idle parent starts a turn, and reports accepted in sequence share the next-step FIFO. `quiet` uses `parent.inject()`, adding the same next-step context without waking a parked parent. Both are deployment policy: the model-facing schema cannot select or override delivery per call.
+The service always uses `parent.steer()`: a running parent receives the report at its nearest step boundary, an idle parent starts a turn, and reports accepted in sequence share the next-step FIFO. The model-facing schema cannot select or override scheduling.
 
 ### Exported contribution
 
-`installReportTool(childCtx, ctx, delivery)` installs the tool and guidance into a minted child scope and returns one disposer revoking both. The generated tool catalog uses this path because the global registry cannot expose a scope-local schema; production composition still enters through `apply()`.
+`installReportTool(childCtx, ctx)` installs the tool and guidance into a minted child scope and returns one disposer revoking both. The generated tool catalog uses this path because the global registry cannot expose a scope-local schema; production composition still enters through `apply()`.
 
 ### Source map
 
 | File | Role |
 |---|---|
-| [`src/index.ts`](src/index.ts) | Continuable-child setup: `installReportTool`, `Config`, delivery resolution |
+| [`src/index.ts`](src/index.ts) | Continuable-child setup and `installReportTool` adapter |
 | [`src/invariant.ts`](src/invariant.ts) | Invariant companion |
 
 </details>
@@ -97,11 +93,10 @@ The package registers a continuable-child setup contribution rather than a globa
 
 Read these pages when the package-level contract is not enough; they move from the report channel to the continuation service behind it and the parent-facing tools.
 
-- [Subagent subsystem](../../../docs/subsystems/subagent.md) — continuable children, activations, and the `reportFrom`/`reportDelivery` contract.
+- [Subagent subsystem](../../../docs/subsystems/subagent.md) — continuable children, activations, and the `sendMessage` contract.
 - [dsh-tool-subagent-control](../tool-subagent-control/README.md) — the parent-to-child control tools.
 - [dsh-tool-subagent](../tool-subagent/README.md) — the delegation tool that starts continuable children.
 - [Generated tool catalog](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent-report) — the `report` schema.
-- [Generated configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-tool-subagent-report) — every accepted config field.
 
 -----
 
@@ -126,11 +121,11 @@ Prefix-stable within a child; neither the schema nor the section changes at runt
 
 #### What the model sees
 
-`report accepted by the agent that started you as message <messageId>` on acceptance; the canonical output carries the stable `messageId`. A failure from an unauthorized sender, an unavailable parent, or a closing lifecycle is an errored result. The description says a failed call may still have arrived, because a later `tools/post-execute` failure can replace the result after `reportFrom()` accepted the message.
+`report accepted by the agent that started you as message <messageId>` on acceptance; the canonical output carries the stable `messageId`. A failure from an unauthorized sender, an unavailable parent, or a closing lifecycle is an errored result. Delivery acceptance still precedes later tool-result hooks, which are outside this package.
 
 #### Token effect
 
-One short acknowledgement per call in the reporting child. The reported content is additionally billed to the parent: next-step delivery joins the next request in an open parent turn or starts a turn for an idle parent, while quiet delivery waits for another input to wake the parent.
+One short acknowledgement per call in the reporting child. The reported content is additionally billed to the parent: delivery joins the next request in an open parent turn or starts a turn for an idle parent.
 
 #### KV Cache effect
 
@@ -140,7 +135,7 @@ Append-only in the child. In the parent, the framed report follows existing hist
 
 #### What the model sees
 
-One user-role parent message framed as `Background subagent <child-id> reported:` followed by the child's exact `output`, with a durable source `{ kind: 'subagent-report', senderSessionId: <child-id> }` that names the child.
+One user-role parent message framed as `Agent <child-id> sent a message:` followed by the child's exact `output`, with a durable source `{ kind: 'agent-message', form: 'relay', senderSessionId: <child-id> }` that names the child.
 
 #### Token effect
 
@@ -148,7 +143,7 @@ The child's complete `output` plus the one-line frame, uncapped by this package.
 
 #### KV Cache effect
 
-Append-only; the report follows the parent's reusable request prefix. Next-step delivery wakes the parent and may extend its open turn, while quiet delivery does not wake it.
+Append-only; the report follows the parent's reusable request prefix. Steer wakes an idle parent and may extend an open turn.
 
 ## Known Limitations and Deferred Work
 
@@ -159,10 +154,9 @@ These limits define what an accepted report does and does not guarantee; they ar
 
 - **A parent whose host-owned disposal already started can still accept** — `AgentHandle.dispose()` cancels, awaits quiescence, and only then unwinds the scope and leaves the registry; it exposes no signal for "disposal started." A report accepted in that window is appended to the parent's transcript, but that parent will not act on it in this process. A continuation-manager-owned parent rejects forest teardown through the manager's admission boundary.
 - **Acceptance is weaker than durable delivery** — there is no durable mailbox, idempotency key, delivery receipt, retry protocol, or exactly-once claim. A process failure after one side recorded acceptance leaves the outcome ambiguous, and an external retry may duplicate the report.
-- **A staged quiet report is not immediately reconstructable** — acceptance returns its stable `MessageId`, but the parent Session reconstructs the framed content only after pending context reaches its ordinary log boundary.
 - **Granting waits for the next Activation; revocation is immediate** — installing this package after a child becomes resident grants `report` and its guidance only on that child's next Activation, while removing the package revokes both from resident children immediately.
 - **Nested reporting reaches exactly one edge upward** — a grandchild reports to its direct child parent, never to the top-level coordinator, which must explicitly report a derived update later.
-- **No rate limiting** — the default `next-step` mode can amplify model work when nested children report frequently, although reports waiting together share one step; a deployment that accepts unread reports over that amplification selects `quiet`.
+- **No rate limiting** — frequent nested reports can amplify model work, although reports waiting together share one step.
 
 <a id="dev-note"></a>
 ### Dev Note

+ 12 - 18
packages/subagent/tool-subagent-report/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-`dsh-tool-subagent-report` 为每个可继续的进程内子级提供一条返回通道,指向启动它的 agent(智能体):它安装子级作用域的 `report` 工具,以及指示子级使用该工具的提示词指导。工具及其指导只存在于这些子级内部——根 agent、一次性 subagent、远程提供方与同级作用域永远看不到它们。被接受的报告会以普通父级消息到达父级,前缀为 `Background subagent <child-id> reported:`。可继续模式不依赖本包,也不依赖控制包;本包只负责子到父方向
+`dsh-tool-subagent-report` 为每个可继续的进程内 child 提供一个临时的 child 作用域适配器,连接相邻 Agent 消息服务:它安装 `report` 工具及指示 child 使用该工具的提示词指导。根 Agent、一次性 subagent、远程提供方与 sibling 作用域永远看不到这两个注册项。被接受的报告通过固定 Steer 调度到达直接 parent,并与 parent 到 child 的消息使用相同前缀和来源信息。可继续模式不依赖本包,也不依赖控制包。
 
 ## 目录
 
@@ -41,11 +41,7 @@ kind: "package-reference"
 - name: '@deepseek-ai/dsh-tool-subagent-report'
 ```
 
-| 字段 | 默认值 | 含义 |
-|---|---|---|
-| `reportDelivery` | `next-step` | 已接受报告的父级调度:`next-step` 在最近 step 边界唤醒父级;`quiet` 添加相同上下文但不唤醒 |
-
-生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-tool-subagent-report)是每个受支持字段及其 JSDoc 的穷尽式真源。
+本包不接收任何配置。
 
 ### 子级获得什么
 
@@ -53,7 +49,7 @@ kind: "package-reference"
 
 ### 父级看到什么
 
-被接受的报告会成为一条用户角色的父级消息,以 `Background subagent <child-id> reported:` 开头,后接子级未经改动的输出,并带有指明子级的持久化来源。`next-step` 投递会唤醒空闲父级,或加入运行中父级最近的 step 边界;`quiet` 投递添加相同上下文但不唤醒父级。工具不接受接收方参数:服务根据子级持久化的 `parentSession` 推导唯一接收方。
+被接受的报告会成为一条用户角色的 parent 消息,以 `Agent <child-id> sent a message:` 开头,后接 child 未经改动的输出,并带有指明 child 的持久化 `agent-message` 来源。固定 Steer 调度会为空闲 parent 启动一个轮次,或加入运行中 parent 最近的 step 边界。工具不接受接收方参数:它根据 child 持久化的 `parentSession` 推导唯一接收方,并把授权与投递交给 `ctx.subagents.sendMessage()`
 
 ### 作用域与方向
 
@@ -75,17 +71,17 @@ kind: "package-reference"
 
 ### 投递调度
 
-`next-step` 使用 `parent.steer()`:运行中的父级在最近的安全 step 边界接收报告,空闲父级启动一个轮次,按顺序接受的报告共享 next-step FIFO。`quiet` 使用 `parent.inject()`,添加相同的 next-step 上下文但不唤醒停驻的父级。两者都是部署策略:面向模型的 schema 不能在单次调用中选择或覆盖投递方式。
+服务始终使用 `parent.steer()`:运行中的 parent 在最近的 step 边界接收报告,空闲 parent 启动一个轮次,按顺序接受的报告共享 next-step FIFO。面向模型的 schema 不能选择或覆盖调度方式。
 
 ### 导出的贡献
 
-`installReportTool(childCtx, ctx, delivery)` 把工具及其指导安装到新创建的子级作用域中,并返回同时撤销两者的唯一 disposer。生成工具目录使用这条路径,因为全局注册表无法公开作用域局部 schema;生产组合仍通过 `apply()` 进入。
+`installReportTool(childCtx, ctx)` 把工具及其指导安装到新创建的 child 作用域中,并返回同时撤销两者的唯一 disposer。生成工具目录使用这条路径,因为全局注册表无法公开作用域局部 schema;生产组合仍通过 `apply()` 进入。
 
 ### 源码地图
 
 | 文件 | 职责 |
 |---|---|
-| [`src/index.ts`](src/index.ts) | 可继续子级设置:`installReportTool`、`Config`、投递解析 |
+| [`src/index.ts`](src/index.ts) | 可继续 child 设置与 `installReportTool` 适配器 |
 | [`src/invariant.ts`](src/invariant.ts) | 不变式伴生插件 |
 
 </details>
@@ -97,11 +93,10 @@ kind: "package-reference"
 
 当包级约定不够用时阅读以下页面;它们从上报通道进入其背后的继续执行服务与面向父级的工具。
 
-- [Subagent 子系统](../../../docs/subsystems/subagent.zh.md)——可继续子级、Activation 与 `reportFrom`/`reportDelivery` 约定。
+- [Subagent 子系统](../../../docs/subsystems/subagent.zh.md)——可继续 child、Activation 与 `sendMessage` 约定。
 - [dsh-tool-subagent-control](../tool-subagent-control/README.zh.md)——父到子的控制工具。
 - [dsh-tool-subagent](../tool-subagent/README.zh.md)——启动可继续子级的委派工具。
 - [生成工具目录](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-subagent-report)——`report` 的 schema。
-- [生成配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-tool-subagent-report)——每个受支持配置字段。
 
 -----
 
@@ -126,11 +121,11 @@ kind: "package-reference"
 
 #### 模型看到什么
 
-接受时返回 `report accepted by the agent that started you as message <messageId>`;规范输出携带稳定的 `messageId`。发送方未授权、父级不可用或生命周期正在关闭时,会返回出错结果。描述中会说明,失败的调用仍可能已经送达,因为 `reportFrom()` 接受消息后,后续 `tools/post-execute` 失败可能替换工具结果
+接受时返回 `report accepted by the agent that started you as message <messageId>`;规范输出携带稳定的 `messageId`。sender 未授权、parent 不可用或生命周期正在关闭时,会返回出错结果。投递接受之后仍可能运行后续工具结果 hook,它们不归本包所有
 
 #### Token 影响
 
-每次调用都会在执行上报的子级中产生一条简短确认消息。父级还会为上报内容支付 token 成本:next-step 投递会加入父级已打开轮次的下一次请求,或为空闲父级启动一个轮次;静默投递则等待其他输入唤醒父级
+每次调用都会在执行上报的 child 中产生一条简短确认消息。parent 还会为上报内容支付 token 成本:投递会加入 parent 已打开轮次的下一次请求,或为空闲 parent 启动一个轮次
 
 #### KV Cache 影响
 
@@ -140,7 +135,7 @@ kind: "package-reference"
 
 #### 模型看到什么
 
-一条用户角色的父级消息,以 `Background subagent <child-id> reported:` 开头,后接子级未经改动的 `output`,并带有指明该子级的持久化来源 `{ kind: 'subagent-report', senderSessionId: <child-id> }`。
+一条用户角色的 parent 消息,以 `Agent <child-id> sent a message:` 开头,后接 child 未经改动的 `output`,并带有指明该 child 的持久化来源 `{ kind: 'agent-message', form: 'relay', senderSessionId: <child-id> }`。
 
 #### Token 影响
 
@@ -148,7 +143,7 @@ kind: "package-reference"
 
 #### KV Cache 影响
 
-仅追加;报告位于父级可复用请求前缀之后。next-step 投递会唤醒父级,并可能延长其已打开的轮次;静默投递则不会唤醒父级
+仅追加;报告位于 parent 可复用请求前缀之后。Steer 会唤醒空闲 parent,并可能延长已打开的轮次
 
 ## 已知限制与延期工作
 
@@ -159,10 +154,9 @@ kind: "package-reference"
 
 - **父级可能在宿主启动 dispose 后继续接受报告**——`AgentHandle.dispose()` 会先取消并等待完全停稳,然后才撤销作用域并离开注册表;它不公开「dispose 已开始」信号。在该窗口内接受的报告会追加到父级 transcript(文本记录),但该父级不会在本进程中处理它。对于由继续执行管理器拥有的父级,管理器的准入边界会在整片森林拆卸期间拒绝该上报。
 - **接受弱于持久投递**——没有持久化 mailbox、幂等键、投递回执、重试协议,也不保证恰好一次。任一侧记录接受后若进程失败,结果都不明确;外部重试可能产生重复上报。
-- **暂存的静默报告无法立即重建**——接受时会返回其稳定 `MessageId`,但只有当待处理上下文到达普通日志边界后,父级会话才能重建带前缀的内容。
 - **授权须等到下一个 Activation,撤销则立即生效**——子级驻留后再安装本包,只会在该子级的下一个 Activation 中授予 `report` 及其指导;移除本包则会立即从驻留子级撤销两者。
 - **嵌套上报只向上到达一条直接边**——孙级只向作为其直接父级的子级上报,不会直接到达顶层协调器;该直接父级必须随后显式发出一条衍生更新。
-- **没有速率限制**——嵌套子级频繁上报时,默认的 `next-step` 模式会放大模型工作量,但一起等待的报告会共享一个 step;宁可接受报告无人阅读也要避免这种放大的部署应选择 `quiet`
+- **没有速率限制**——嵌套 child 频繁上报会放大模型工作量,但一起等待的报告会共享一个 step。
 
 <a id="dev-note"></a>
 ### 开发备注

+ 0 - 3
packages/subagent/tool-subagent-report/package.json

@@ -39,9 +39,6 @@
     "@deepseek-ai/dsh-tools": "workspace:^",
     "@deepseek-ai/cordis": "workspace:^"
   },
-  "dependencies": {
-    "@deepseek-ai/schemastery": "workspace:^"
-  },
   "devDependencies": {
     "@deepseek-ai/dsh-agent": "workspace:^",
     "@deepseek-ai/dsh-agent-loop": "workspace:^",

+ 10 - 31
packages/subagent/tool-subagent-report/src/index.ts

@@ -7,10 +7,9 @@
  */
 
 import type { Context } from '@deepseek-ai/cordis'
-import z from '@deepseek-ai/schemastery'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import type { ContentBlock } from '@deepseek-ai/dsh-llm'
-import type { SubagentReportDelivery } from '@deepseek-ai/dsh-subagent'
+import type {} from '@deepseek-ai/dsh-subagent'
 import { FIRST_PARTY_SECTION_ORDER } from '@deepseek-ai/dsh-system-prompt'
 import { defineTool } from '@deepseek-ai/dsh-tools'
 
@@ -23,33 +22,17 @@ export const inject = ['subagents', 'tools', 'systemPrompt']
 /** Guidance order after every per-tool section a continuable child can carry. */
 const REPORT_SECTION_ORDER = FIRST_PARTY_SECTION_ORDER.TOOL_REPORT
 
-/** 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
-}
-
-export const Config: z<Config> = z.object({
-  reportDelivery: z.union(['quiet', 'next-step'] as const).default('next-step'),
-})
-
 /**
  * Install `report` and its usage guidance into one continuable child's scope.
  * Both registrations are owned by that scope and are therefore invisible to the
  * child's parent and siblings.
  * @param childCtx - child-scoped context receiving the tool and the guidance.
  * @param ctx - service context used for delivery.
- * @param delivery - resolved deployment scheduling policy.
  * @returns disposer that attempts both child registrations before reporting cleanup failures.
  */
 export function installReportTool(
   childCtx: Context,
   ctx: Context,
-  delivery: SubagentReportDelivery,
 ): () => void {
   const disposeSection = childCtx.systemPrompt.section({
     name: 'tool:report',
@@ -69,8 +52,9 @@ export function installReportTool(
         + '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.',
+        + 'turn or finish your work, and only your direct parent receives it. If that agent is working, the '
+        + 'report steers its nearest step; otherwise it starts a turn. A failed call may still have arrived '
+        + 'if a later tool-result hook failed, so do not blindly repeat it.',
       parameters: {
         output: {
           type: 'string',
@@ -95,10 +79,10 @@ export function installReportTool(
         const content: ContentBlock[] = [{ type: 'text', text: args.output }]
         // Scope-local resolution guarantees an Agent. The service still verifies
         // its exact live Activation identity at the authority boundary.
-        const messageId = await ctx.subagents.reportFrom(exec.agent as Agent, content, {
-          delivery,
-          signal: exec.signal,
-        })
+        const child = exec.agent as Agent
+        const parentId = child.session.header.parentSession
+        if (parentId === undefined) throw new Error('report requires a direct parent Agent')
+        const messageId = await ctx.subagents.sendMessage(child, parentId, content, { signal: exec.signal })
         return { messageId }
       },
     }))
@@ -131,12 +115,7 @@ export function installReportTool(
 /**
  * Register the continuable-child contribution.
  * @param ctx - context carrying tools, the system prompt, and the subagent service.
- * @param config - deployment scheduling policy.
  */
-export function apply(ctx: Context, config: Config = {}): void {
-  // Config() applies the schema default at runtime; the schemastery return
-  // type keeps the input's optional shape, so assert the resolved one.
-  const { reportDelivery } = Config(config) as { reportDelivery: SubagentReportDelivery }
-  ctx.subagents.registerContinuableSetup(childCtx =>
-    installReportTool(childCtx, ctx, reportDelivery))
+export function apply(ctx: Context): void {
+  ctx.subagents.registerContinuableSetup(childCtx => installReportTool(childCtx, ctx))
 }

+ 27 - 54
packages/subagent/tool-subagent-report/tests/tool-subagent-report.spec.ts

@@ -62,7 +62,7 @@ afterEach(async () => {
 })
 
 /** Boot the real continuation graph with optional report installation. */
-async function setup(options: { load?: boolean; config?: tool.Config } = {}) {
+async function setup(options: { load?: boolean } = {}) {
   const ctx = new Context()
   await mountAgentLoopTestDependencies(ctx)
   const root = mkdtempSync(join(tmpdir(), 'dsh-tool-subagent-report-'))
@@ -72,7 +72,7 @@ async function setup(options: { load?: boolean; config?: tool.Config } = {}) {
   await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
   const fiber = options.load === false
     ? undefined
-    : await ctx.plugin(tool, options.config ?? { reportDelivery: 'quiet' })
+    : await ctx.plugin(tool)
   const adapter = new HeldAdapter()
   ctx.llm.registerAdapter(['mock'], adapter)
   const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
@@ -140,7 +140,7 @@ function registerReportConflict(child: Agent): () => void {
 function reports(agent: Agent): { id: string; text: string; sender: string }[] {
   const visible = agent.session.events.flatMap(event => event.type === 'user/message' ? [event.data] : [])
   return [...visible, ...agent.inbox.nextStep].flatMap((message) => {
-    if (message.source.kind !== 'subagent-report') return []
+    if (message.source.kind !== 'agent-message') return []
     return [{
       id: message.id,
       text: message.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('\n'),
@@ -205,10 +205,9 @@ describe('dsh-tool-subagent-report', () => {
     expect(names).not.toContain('send_message')
   })
 
-  it('delivers quiet reports with stable message and sender identities without waking', async () => {
+  it('delivers reports with stable message and sender identities through Steer', async () => {
     const { ctx, parent, adapter } = await setup()
     const { started, child } = await startChild(ctx, parent)
-    const parentRequests = adapter.requests.filter(request => request.sessionId === parent.id).length
     const enqueues: string[] = []
     ctx.on('agent/inbox/inserted', ({ agent, message }) => {
       if (agent === parent) {
@@ -224,34 +223,17 @@ describe('dsh-tool-subagent-report', () => {
     expect(renderedText(result)).toContain(messageId)
     expect(reports(parent)).toEqual([{
       id: messageId,
-      text: `Background subagent ${started.childId} reported:\nCHILD_FINDING`,
+      text: `Agent ${started.childId} sent a message:\nCHILD_FINDING`,
       sender: started.childId,
     }])
     expect(enqueues).toEqual(['steering'])
-    expect(parent.status).toBe('idle')
-    expect(adapter.requests.filter(request => request.sessionId === parent.id)).toHaveLength(parentRequests)
-  })
-
-  it('delivers next-step reports through waking steering', async () => {
-    const { ctx, parent, adapter } = await setup({ config: { reportDelivery: 'next-step' } })
-    const { child } = await startChild(ctx, parent)
-    const enqueues: string[] = []
-    ctx.on('agent/inbox/inserted', ({ agent, message }) => {
-      if (agent === parent) {
-        enqueues.push(agent.inbox.nextTurn.some(queued => queued.id === message.id) ? 'queued' : 'steering')
-      }
-    })
-
-    const result = await callReport(ctx, child, 'WAKE_UP')
-    expect(result.isError).toBe(false)
-    expect(enqueues).toEqual(['steering'])
     await vi.waitFor(() => {
       expect(adapter.requests.some(request => request.sessionId === parent.id)).toBe(true)
     })
   })
 
-  it('batches repeated next-step reports in accepted order', async () => {
-    const { ctx, parent, adapter } = await setup({ config: { reportDelivery: 'next-step' } })
+  it('batches repeated reports in accepted order at a running parent step', async () => {
+    const { ctx, parent, adapter } = await setup()
     await startHeldParentTurn(parent, adapter)
     const { child } = await startChild(ctx, parent)
 
@@ -262,7 +244,7 @@ describe('dsh-tool-subagent-report', () => {
   })
 
   it('keeps a report before the child settlement in one busy-parent batch', async () => {
-    const { ctx, parent, adapter } = await setup({ config: { reportDelivery: 'next-step' } })
+    const { ctx, parent, adapter } = await setup()
     await startHeldParentTurn(parent, adapter)
     const { started, child } = await startChild(ctx, parent)
 
@@ -271,7 +253,7 @@ describe('dsh-tool-subagent-report', () => {
     await vi.waitFor(() => { expect(ctx.agents.get(started.childId)).toBeUndefined() })
 
     expect(parent.inbox.nextStep.map(message => message.source.kind)).toEqual([
-      'subagent-report',
+      'agent-message',
       'subagent-settled',
     ])
     expect(parent.inbox.nextTurn).toHaveLength(0)
@@ -287,7 +269,7 @@ describe('dsh-tool-subagent-report', () => {
       expect(ctx.agents.get(started.childId) === undefined).toBe(true)
     }, { timeout: 5_000 })
     expect(reports(parent).map(report => report.text)).toEqual([
-      `Background subagent ${started.childId} reported:\nDURABLE_SELECTION`,
+      `Agent ${started.childId} sent a message:\nDURABLE_SELECTION`,
     ])
   })
 
@@ -298,8 +280,8 @@ describe('dsh-tool-subagent-report', () => {
 
     expect((await callReport(ctx, grandchild, 'FROM_GRANDCHILD')).isError).toBe(false)
     expect(reports(parent)).toEqual([])
-    // The intermediate parent's turn is open, so quiet context is pending in
-    // its inbox until that turn reaches its next safe log boundary.
+    // The intermediate parent's turn is open, so steering is pending in its
+    // inbox until that turn reaches its next step boundary.
     expect(reports(child)).toHaveLength(1)
     adapter.release()
     await vi.waitFor(() => { expect(reports(child)).toHaveLength(1) })
@@ -307,8 +289,8 @@ describe('dsh-tool-subagent-report', () => {
     expect(reports(child)[0]?.text).toContain('FROM_GRANDCHILD')
   })
 
-  it('accounts next-step reports delivered to a resident continuable parent', async () => {
-    const { ctx, parent, adapter } = await setup({ config: { reportDelivery: 'next-step' } })
+  it('accounts reports delivered to a resident continuable parent', async () => {
+    const { ctx, parent, adapter } = await setup()
     const { child } = await startChild(ctx, parent, 'outer task')
     const { started: grandchildStart, child: grandchild } = await startChild(ctx, child, 'inner task')
 
@@ -324,12 +306,11 @@ describe('dsh-tool-subagent-report', () => {
   it('normalizes a direct parent send rejection', async () => {
     const { ctx, parent } = await setup()
     const { child } = await startChild(ctx, parent)
-    vi.spyOn(parent, 'inject').mockImplementationOnce(() => {
+    vi.spyOn(parent, 'steer').mockImplementationOnce(() => {
       throw new Error('parent closed during delivery')
     })
 
-    await expect(ctx.subagents.reportFrom(child, [{ type: 'text', text: 'rejected' }], {
-      delivery: 'quiet',
+    await expect(ctx.subagents.sendMessage(child, parent.id, [{ type: 'text', text: 'rejected' }], {
       signal: testSignal,
     })).rejects.toMatchObject({ code: 'PARENT_UNAVAILABLE' })
     expect(reports(parent)).toEqual([])
@@ -337,10 +318,9 @@ describe('dsh-tool-subagent-report', () => {
 
   it('rejects roots, forged same-id senders, absent parents, cancellation, and drain', async () => {
     const { ctx, parent, adapter } = await setup()
-    await expect(ctx.subagents.reportFrom(parent, [{ type: 'text', text: 'root' }], {
-      delivery: 'quiet',
+    await expect(ctx.subagents.sendMessage(parent, SessionId('not-a-child'), [{ type: 'text', text: 'root' }], {
       signal: testSignal,
-    })).rejects.toMatchObject({ code: 'UNAUTHORIZED' })
+    })).rejects.toMatchObject({ code: 'CONTINUATION_UNAVAILABLE' })
 
     const disposable = await ctx.agents.create({
       sessionId: SessionId('disposable-parent'),
@@ -348,8 +328,7 @@ describe('dsh-tool-subagent-report', () => {
     })
     const { child } = await startChild(ctx, disposable.agent)
     const forged = { ...child } as Agent
-    await expect(ctx.subagents.reportFrom(forged, [{ type: 'text', text: 'forged' }], {
-      delivery: 'quiet',
+    await expect(ctx.subagents.sendMessage(forged, disposable.agent.id, [{ type: 'text', text: 'forged' }], {
       signal: testSignal,
     })).rejects.toMatchObject({ code: 'UNAUTHORIZED' })
 
@@ -362,8 +341,7 @@ describe('dsh-tool-subagent-report', () => {
 
     adapter.release()
     const draining = ctx.subagents.drainContinuableDescendants([child])
-    await expect(ctx.subagents.reportFrom(child, [{ type: 'text', text: 'draining' }], {
-      delivery: 'quiet',
+    await expect(ctx.subagents.sendMessage(child, disposable.agent.id, [{ type: 'text', text: 'draining' }], {
       signal: testSignal,
     })).rejects.toMatchObject({ code: 'DRAINING' })
     await draining
@@ -380,7 +358,7 @@ describe('dsh-tool-subagent-report', () => {
     expect(await sectionNames(ctx, child)).not.toContain('tool:report')
     expect((await callReport(ctx, child, 'revoked')).isError).toBe(true)
 
-    const late = await ctx.plugin(tool, { reportDelivery: 'quiet' })
+    const late = await ctx.plugin(tool)
     expect(ctx.tools.schemas(child).map(schema => schema.name)).not.toContain('report')
     expect(await sectionNames(ctx, child)).not.toContain('tool:report')
     await late.dispose()
@@ -391,7 +369,7 @@ describe('dsh-tool-subagent-report', () => {
     const { child } = await startChild(ctx, parent)
     const disposeConflict = registerReportConflict(child)
 
-    expect(() => tool.installReportTool(child.ctx, ctx, 'quiet')).toThrow(/already registered in this scope/)
+    expect(() => tool.installReportTool(child.ctx, ctx)).toThrow(/already registered in this scope/)
     expect(await sectionNames(ctx, child)).not.toContain('tool:report')
     disposeConflict()
   })
@@ -409,7 +387,7 @@ describe('dsh-tool-subagent-report', () => {
 
     let failure: unknown
     try {
-      tool.installReportTool(child.ctx, ctx, 'quiet')
+      tool.installReportTool(child.ctx, ctx)
     } catch (error: unknown) {
       failure = error
     }
@@ -427,7 +405,7 @@ describe('dsh-tool-subagent-report', () => {
   it('attempts both revocations and aggregates change-listener failures', async () => {
     const { ctx, parent } = await setup({ load: false })
     const { child } = await startChild(ctx, parent)
-    const dispose = tool.installReportTool(child.ctx, ctx, 'quiet')
+    const dispose = tool.installReportTool(child.ctx, ctx)
     const toolFailure = new Error('tool removal listener failed')
     const promptFailure = new Error('prompt removal listener failed')
     const offTool = ctx.on('tools/change', () => { throw toolFailure })
@@ -551,19 +529,14 @@ describe('dsh-tool-subagent-report', () => {
     expect((await callReport(ctx, child, 'after-close')).isError).toBe(true)
   })
 
-  it('keeps the namespace plugin shape and validates its default', () => {
+  it('keeps the namespace plugin shape', () => {
     expect('default' in tool).toBe(false)
     expect(tool.name).toBe('tool-subagent-report')
     expect(tool.inject).toEqual(['subagents', 'tools', 'systemPrompt'])
-    // Next-step delivery wakes a parked parent and lets a running parent act at
-    // its nearest safe boundary.
-    expect(tool.Config({}).reportDelivery).toBe('next-step')
-    expect(() => tool.Config({ reportDelivery: 'wakeup' } as never)).toThrow()
-    expect(() => tool.Config({ reportDelivery: 'shout' } as never)).toThrow()
   })
 
-  it('wakes the parent under the default configuration', async () => {
-    const { ctx, parent, adapter } = await setup({ config: {} })
+  it('wakes an idle parent', async () => {
+    const { ctx, parent, adapter } = await setup()
     const { child } = await startChild(ctx, parent)
     const enqueues: string[] = []
     ctx.on('agent/inbox/inserted', ({ agent, message }) => {

+ 3 - 3
packages/test-support/session-snapshot/tests/fixtures/subagent-durability-failure.ts

@@ -86,10 +86,10 @@ export function apply(ctx: Context): void {
   // never reach the live inbox.
   let realChildId: string | undefined
   const subagents = ctx.subagents as unknown as {
-    followup: (authority: unknown, childId: SessionId, content: unknown, options: unknown) => Promise<unknown>
+    sendMessage: (authority: unknown, childId: SessionId, content: unknown, options: unknown) => Promise<unknown>
   }
-  const deliver = subagents.followup.bind(subagents)
-  subagents.followup = (authority, childId, content, options) => {
+  const deliver = subagents.sendMessage.bind(subagents)
+  subagents.sendMessage = (authority, childId, content, options) => {
     const mapped = childId === PLACEHOLDER_CHILD_ID && realChildId !== undefined
       ? SessionId(realChildId)
       : childId

+ 0 - 4
pnpm-lock.yaml

@@ -8879,10 +8879,6 @@ importers:
         version: link:../../core/tools
 
   packages/subagent/tool-subagent-report:
-    dependencies:
-      '@deepseek-ai/schemastery':
-        specifier: link:../../../vendor/schemastery
-        version: link:../../../vendor/schemastery
     devDependencies:
       '@deepseek-ai/cordis':
         specifier: workspace:^

+ 2 - 5
scripts/gen-cordis-catalog.ts

@@ -471,19 +471,16 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
   ContinuableSetupContribution: 'subagent.md',
   ContinuableStart: 'subagent.md',
   ContinuableStartSpec: 'subagent.md',
-  CoordinatorMessageSource: 'subagent.md',
+  AgentMessageSource: 'subagent.md',
   SubagentCatalog: 'subagent.md',
   SubagentDescendantListEntry: 'subagent.md',
-  SubagentFollowupOptions: 'subagent.md',
+  SubagentSendMessageOptions: 'subagent.md',
   SubagentInterruptAuthority: 'subagent.md',
   SubagentInterruptReceipt: 'subagent.md',
   SubagentListEntry: 'subagent.md',
   SubagentPromptReceipt: 'subagent.md',
   SubagentPromptRequest: 'subagent.md',
   SubagentProvider: 'subagent.md',
-  SubagentReportDelivery: 'subagent.md',
-  SubagentReportMessageSource: 'subagent.md',
-  SubagentReportOptions: 'subagent.md',
   SubagentRun: 'subagent.md',
   SubagentRuntime: 'subagent.md',
   SubagentStartRequest: 'subagent.md',

+ 2 - 3
scripts/gen-tool-catalog.ts

@@ -33,7 +33,7 @@ import WebRuntime from '@deepseek-ai/dsh-web'
 import * as WebSearchExa from '@deepseek-ai/dsh-web-search-exa'
 import * as WebFetchLocal from '@deepseek-ai/dsh-web-fetch-http'
 import SubagentRuntime from '@deepseek-ai/dsh-subagent'
-import type { SubagentProvider, SubagentReportDelivery } from '@deepseek-ai/dsh-subagent'
+import type { SubagentProvider } from '@deepseek-ai/dsh-subagent'
 import * as ToolSubagentControl from '@deepseek-ai/dsh-tool-subagent-control'
 import * as ToolSubagentListAgents from '@deepseek-ai/dsh-tool-subagent-control/list-agents'
 import * as ToolSubagentReport from '@deepseek-ai/dsh-tool-subagent-report'
@@ -505,9 +505,8 @@ const TOOL_PACKAGES: ToolPackage[] = [
     async mount(ctx) {
       await ctx.plugin(AgentRegistry)
       await ctx.plugin(SubagentRuntime)
-      const { reportDelivery } = ToolSubagentReport.Config({}) as { reportDelivery: SubagentReportDelivery }
       await mountCatalogChildScope(ctx, (childCtx) => {
-        ToolSubagentReport.installReportTool(childCtx, ctx, reportDelivery)
+        ToolSubagentReport.installReportTool(childCtx, ctx)
       })
     },
     scope: ctx => catalogChildScopes.get(ctx) as Agent,

+ 2 - 17
scripts/type-equiv.manifest.json

@@ -1303,17 +1303,7 @@
     },
     {
       "doc": "docs/subsystems/subagent.md",
-      "symbol": "CoordinatorMessageSource",
-      "source": "packages/subagent/subagent/src/continuation.ts"
-    },
-    {
-      "doc": "docs/subsystems/subagent.md",
-      "symbol": "SubagentReportMessageSource",
-      "source": "packages/subagent/subagent/src/continuation.ts"
-    },
-    {
-      "doc": "docs/subsystems/subagent.md",
-      "symbol": "SubagentReportDelivery",
+      "symbol": "AgentMessageSource",
       "source": "packages/subagent/subagent/src/continuation.ts"
     },
     {
@@ -1323,12 +1313,7 @@
     },
     {
       "doc": "docs/subsystems/subagent.md",
-      "symbol": "SubagentReportOptions",
-      "source": "packages/subagent/subagent/src/continuation.ts"
-    },
-    {
-      "doc": "docs/subsystems/subagent.md",
-      "symbol": "SubagentFollowupOptions",
+      "symbol": "SubagentSendMessageOptions",
       "source": "packages/subagent/subagent/src/continuation.ts"
     },
     {

+ 1 - 1
snapshots/sdk/bash-tool/tool-schemas.expected.json

@@ -322,7 +322,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {

+ 2 - 2
snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json

@@ -322,7 +322,7 @@
     },
     {
       "name": "report",
-      "description": "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.",
+      "description": "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. If that agent is working, the report steers its nearest step; otherwise it starts a turn. A failed call may still have arrived if a later tool-result hook failed, so do not blindly repeat it.",
       "parameters": {
         "type": "object",
         "properties": {
@@ -338,7 +338,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {

+ 14 - 18
snapshots/sdk/subagent-continuable/session.1.jsonl

@@ -7,8 +7,8 @@
 {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{message:14}}"}]}}
 {"type":"turn/start","data":{"turn":1}}
 {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
-{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Now reply with exactly SECOND_OK."}],"source":{"kind":"coordinator","form":"relay","senderSessionId":"{{session:1}}"},"role":"user","id":"{{message:15}}"}]}}
-{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":1,"inserted":[{"content":[{"type":"text","text":"Now reply with exactly THIRD_OK."}],"source":{"kind":"coordinator","form":"relay","senderSessionId":"{{session:1}}"},"role":"user","id":"{{message:16}}"}]}}
+{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Agent {{session:1}} sent a message:"},{"type":"text","text":"Now reply with exactly SECOND_OK."}],"source":{"kind":"agent-message","form":"relay","senderSessionId":"{{session:1}}"},"role":"user","id":"{{message:15}}"}]}}
+{"type":"agent/inbox/spliced","data":{"target":"next-step","start":1,"inserted":[{"content":[{"type":"text","text":"Agent {{session:1}} sent a message:"},{"type":"text","text":"Now reply with exactly THIRD_OK."}],"source":{"kind":"agent-message","form":"relay","senderSessionId":"{{session:1}}"},"role":"user","id":"{{message:16}}"}]}}
 {"type":"step/start","data":{"turn":1,"step":1}}
 {"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{message:14}}"},"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: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\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: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"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":"{{message:17}}"},"surfaceOp":"append"}
@@ -20,21 +20,17 @@
 {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CHILD_OK"}}}}
 {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
 {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:18}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[16,17,18,19,20],"surfaceOp":"append"}
+{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:18}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[[16,20]],"surfaceOp":"append"}
 {"type":"step/end","data":{"turn":1,"step":1}}
+{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":2,"inserted":[]}}
+{"type":"step/start","data":{"turn":1,"step":2}}
+{"type":"user/message","data":{"content":[{"type":"text","text":"Agent {{session:1}} sent a message:"},{"type":"text","text":"Now reply with exactly SECOND_OK."}],"source":{"kind":"agent-message","form":"relay","senderSessionId":"{{session:1}}"},"role":"user","id":"{{message:15}}"},"surfaceOp":"append"}
+{"type":"user/message","data":{"content":[{"type":"text","text":"Agent {{session:1}} sent a message:"},{"type":"text","text":"Now reply with exactly THIRD_OK."}],"source":{"kind":"agent-message","form":"relay","senderSessionId":"{{session:1}}"},"role":"user","id":"{{message:16}}"},"surfaceOp":"append"}
+{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"SECOND_OK"}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SECOND_OK"}}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"SECOND_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:19}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[[27,31]],"surfaceOp":"append"}
+{"type":"step/end","data":{"turn":1,"step":2}}
 {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
-{"type":"turn/start","data":{"turn":2}}
-{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
-{"type":"step/start","data":{"turn":2,"step":1}}
-{"type":"user/message","data":{"content":[{"type":"text","text":"Now reply with exactly SECOND_OK."}],"source":{"kind":"coordinator","form":"relay","senderSessionId":"{{session:1}}"},"role":"user","id":"{{message:15}}"},"surfaceOp":"append"}
-{"type":"assistant/chunk","data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
-{"type":"assistant/chunk","data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":0,"text":"SECOND_OK"}}}
-{"type":"assistant/chunk","data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SECOND_OK"}}}}
-{"type":"assistant/chunk","data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
-{"type":"assistant/chunk","data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SECOND_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:19}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[28,29,30,31,32],"surfaceOp":"append"}
-{"type":"step/end","data":{"turn":2,"step":1}}
-{"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}}
-{"type":"turn/start","data":{"turn":3}}
-{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
-{"type":"turn/end","data":{"turn":3,"reason":{"kind":"error","error":{"message":"snapshot disk full","code":"UNKNOWN"}}}}

+ 10 - 10
snapshots/sdk/subagent-continuable/session.jsonl

@@ -16,7 +16,7 @@
 {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}}}
 {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
 {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[12,13,14,15,16],"surfaceOp":"append"}
+{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[[12,16]],"surfaceOp":"append"}
 {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\", \"run_in_background\": true}"}}
 {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[18],"surfaceOp":"append"}
 {"type":"step/end","data":{"turn":1,"step":1}}
@@ -26,9 +26,9 @@
 {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_1","name":"send_message","arguments":"{\"subagent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}}}}
 {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
 {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_1","name":"send_message","arguments":"{\"subagent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[22,23,24,25,26],"surfaceOp":"append"}
+{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_1","name":"send_message","arguments":"{\"subagent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[[22,26]],"surfaceOp":"append"}
 {"type":"tool/call","data":{"turn":1,"step":2,"callId":"call_followup_1","name":"send_message","arguments":"{\"subagent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly SECOND_OK.\"}"}}
-{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_followup_1"},"content":[{"type":"tool-result","toolCallId":"call_followup_1","content":[{"type":"text","text":"message queued as the next turn for subagent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:6}}"}},"sourceEventSeqs":[28],"surfaceOp":"append"}
+{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_followup_1"},"content":[{"type":"tool-result","toolCallId":"call_followup_1","content":[{"type":"text","text":"message delivered to subagent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:6}}"}},"sourceEventSeqs":[28],"surfaceOp":"append"}
 {"type":"step/end","data":{"turn":1,"step":2}}
 {"type":"step/start","data":{"turn":1,"step":3}}
 {"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
@@ -36,9 +36,9 @@
 {"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_2","name":"send_message","arguments":"{\"subagent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}}}}
 {"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
 {"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_2","name":"send_message","arguments":"{\"subagent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[32,33,34,35,36],"surfaceOp":"append"}
+{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_2","name":"send_message","arguments":"{\"subagent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[[32,36]],"surfaceOp":"append"}
 {"type":"tool/call","data":{"turn":1,"step":3,"callId":"call_followup_2","name":"send_message","arguments":"{\"subagent_id\": \"{{session:2}}\", \"message\": \"Now reply with exactly THIRD_OK.\"}"}}
-{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"call_followup_2"},"content":[{"type":"tool-result","toolCallId":"call_followup_2","content":[{"type":"text","text":"message queued as the next turn for subagent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:8}}"}},"sourceEventSeqs":[38],"surfaceOp":"append"}
+{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"call_followup_2"},"content":[{"type":"tool-result","toolCallId":"call_followup_2","content":[{"type":"text","text":"message delivered to subagent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:8}}"}},"sourceEventSeqs":[38],"surfaceOp":"append"}
 {"type":"step/end","data":{"turn":1,"step":3}}
 {"type":"step/start","data":{"turn":1,"step":4}}
 {"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
@@ -46,7 +46,7 @@
 {"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_followup_unknown","name":"send_message","arguments":"{\"subagent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}}}}
 {"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
 {"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_unknown","name":"send_message","arguments":"{\"subagent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[42,43,44,45,46],"surfaceOp":"append"}
+{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_followup_unknown","name":"send_message","arguments":"{\"subagent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[[42,46]],"surfaceOp":"append"}
 {"type":"tool/call","data":{"turn":1,"step":4,"callId":"call_followup_unknown","name":"send_message","arguments":"{\"subagent_id\": \"22222222-2222-4222-8222-222222222222\", \"message\": \"Please continue.\"}"}}
 {"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"call_followup_unknown"},"content":[{"type":"tool-result","toolCallId":"call_followup_unknown","content":[{"type":"text","text":"Error: subagent \"22222222-2222-4222-8222-222222222222\" is unavailable"}],"isError":true}],"role":"user","id":"{{message:10}}"},"error":{"name":"SubagentError","code":"NOT_RESUMABLE"}},"sourceEventSeqs":[48],"surfaceOp":"append"}
 {"type":"step/end","data":{"turn":1,"step":4}}
@@ -56,19 +56,19 @@
 {"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}}
 {"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
 {"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[52,53,54,55,56],"surfaceOp":"append"}
+{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[[52,56]],"surfaceOp":"append"}
 {"type":"step/end","data":{"turn":1,"step":5}}
 {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
-{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{session:2}} failed before it finished."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"SECOND_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} failed before it finished.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:12}}"}]}}
+{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"SECOND_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:12}}"}]}}
 {"type":"turn/start","data":{"turn":2}}
 {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
 {"type":"step/start","data":{"turn":2,"step":1}}
-{"type":"user/message","data":{"content":[{"type":"text","text":"Background subagent {{session:2}} failed before it finished."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"SECOND_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} failed before it finished.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:12}}"},"surfaceOp":"append"}
+{"type":"user/message","data":{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"SECOND_OK"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:12}}"},"surfaceOp":"append"}
 {"type":"assistant/chunk","data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
 {"type":"assistant/chunk","data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":0,"text":"SUBAGENT_SETTLED_NOTED"}}}
 {"type":"assistant/chunk","data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}}}
 {"type":"assistant/chunk","data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
 {"type":"assistant/chunk","data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:13}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[65,66,67,68,69],"surfaceOp":"append"}
+{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:13}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[[65,69]],"surfaceOp":"append"}
 {"type":"step/end","data":{"turn":2,"step":1}}
 {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}}

+ 2 - 2
snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json

@@ -322,7 +322,7 @@
     },
     {
       "name": "report",
-      "description": "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.",
+      "description": "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. If that agent is working, the report steers its nearest step; otherwise it starts a turn. A failed call may still have arrived if a later tool-result hook failed, so do not blindly repeat it.",
       "parameters": {
         "type": "object",
         "properties": {
@@ -338,7 +338,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json

@@ -322,7 +322,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json

@@ -322,7 +322,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json

@@ -339,7 +339,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {

+ 2 - 2
snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json

@@ -322,7 +322,7 @@
     },
     {
       "name": "report",
-      "description": "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.",
+      "description": "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. If that agent is working, the report steers its nearest step; otherwise it starts a turn. A failed call may still have arrived if a later tool-result hook failed, so do not blindly repeat it.",
       "parameters": {
         "type": "object",
         "properties": {
@@ -338,7 +338,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {

+ 2 - 2
snapshots/sdk/subagent-report/session.1.jsonl

@@ -18,7 +18,7 @@
 {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_report_1","name":"report","arguments":"{\"output\": \"CHILD_REPORT_OK\"}"}}}}
 {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
 {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_report_1","name":"report","arguments":"{\"output\": \"CHILD_REPORT_OK\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:13}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[14,15,16,17,18],"surfaceOp":"append"}
+{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_report_1","name":"report","arguments":"{\"output\": \"CHILD_REPORT_OK\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:13}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[[14,18]],"surfaceOp":"append"}
 {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_report_1","name":"report","arguments":"{\"output\": \"CHILD_REPORT_OK\"}"}}
 {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_report_1"},"content":[{"type":"tool-result","toolCallId":"call_report_1","content":[{"type":"text","text":"report accepted by the agent that started you as message {{message:6}}"}],"isError":false}],"role":"user","id":"{{message:14}}"}},"sourceEventSeqs":[20],"surfaceOp":"append"}
 {"type":"step/end","data":{"turn":1,"step":1}}
@@ -28,6 +28,6 @@
 {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Reported."}}}}
 {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
 {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"Reported."}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:15}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[24,25,26,27,28],"surfaceOp":"append"}
+{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"Reported."}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:15}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[[24,28]],"surfaceOp":"append"}
 {"type":"step/end","data":{"turn":1,"step":2}}
 {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}

+ 6 - 6
snapshots/sdk/subagent-report/session.jsonl

@@ -16,7 +16,7 @@
 {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Report a finding\", \"prompt\": \"Call the report tool once with output exactly CHILD_REPORT_OK, then stop.\", \"run_in_background\": true}"}}}}
 {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
 {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Report a finding\", \"prompt\": \"Call the report tool once with output exactly CHILD_REPORT_OK, then stop.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[12,13,14,15,16],"surfaceOp":"append"}
+{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Report a finding\", \"prompt\": \"Call the report tool once with output exactly CHILD_REPORT_OK, then stop.\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[[12,16]],"surfaceOp":"append"}
 {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_bg_start","name":"subagent","arguments":"{\"description\": \"Report a finding\", \"prompt\": \"Call the report tool once with output exactly CHILD_REPORT_OK, then stop.\", \"run_in_background\": true}"}}
 {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_bg_start"},"content":[{"type":"tool-result","toolCallId":"call_bg_start","content":[{"type":"text","text":"started subagent {{session:2}}"}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[18],"surfaceOp":"append"}
 {"type":"step/end","data":{"turn":1,"step":1}}
@@ -26,23 +26,23 @@
 {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"STARTED"}}}}
 {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
 {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[22,23,24,25,26],"surfaceOp":"append"}
+{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[[22,26]],"surfaceOp":"append"}
 {"type":"step/end","data":{"turn":1,"step":2}}
 {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
-{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{session:2}} reported:"},{"type":"text","text":"CHILD_REPORT_OK"}],"source":{"kind":"subagent-report","form":"relay","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:6}}"}]}}
+{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Agent {{session:2}} sent a message:"},{"type":"text","text":"CHILD_REPORT_OK"}],"source":{"kind":"agent-message","form":"relay","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:6}}"}]}}
 {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"Reported."}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:7}}"}]}}
 {"type":"turn/start","data":{"turn":2}}
 {"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
 {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
 {"type":"step/start","data":{"turn":2,"step":1}}
-{"type":"user/message","data":{"content":[{"type":"text","text":"Background subagent {{session:2}} reported:"},{"type":"text","text":"CHILD_REPORT_OK"}],"source":{"kind":"subagent-report","form":"relay","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:6}}"},"surfaceOp":"append"}
+{"type":"user/message","data":{"content":[{"type":"text","text":"Agent {{session:2}} sent a message:"},{"type":"text","text":"CHILD_REPORT_OK"}],"source":{"kind":"agent-message","form":"relay","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:6}}"},"surfaceOp":"append"}
 {"type":"user/message","data":{"content":[{"type":"text","text":"Background subagent {{session:2}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"Reported."}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{session:2}} finished and will do no further work unless you send it more.","senderSessionId":"{{session:2}}"},"role":"user","id":"{{message:7}}"},"surfaceOp":"append"}
 {"type":"assistant/chunk","data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
 {"type":"assistant/chunk","data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":0,"text":"SUBAGENT_SETTLED_NOTED"}}}
 {"type":"assistant/chunk","data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}}}}
 {"type":"assistant/chunk","data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
 {"type":"assistant/chunk","data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[38,39,40,41,42],"surfaceOp":"append"}
+{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SUBAGENT_SETTLED_NOTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[[38,42]],"surfaceOp":"append"}
 {"type":"step/end","data":{"turn":2,"step":1}}
 {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}}
 {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Repeat back, verbatim, the exact output the background subagent reported to you. Reply with only that text. Do not use any tools."}],"source":{"kind":"user"},"role":"user","id":"{{message:9}}"}]}}
@@ -55,6 +55,6 @@
 {"type":"assistant/chunk","data":{"turn":3,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CHILD_REPORT_OK"}}}}
 {"type":"assistant/chunk","data":{"turn":3,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
 {"type":"assistant/chunk","data":{"turn":3,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","data":{"turn":3,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"CHILD_REPORT_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[51,52,53,54,55],"surfaceOp":"append"}
+{"type":"assistant/message","data":{"turn":3,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"CHILD_REPORT_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[[51,55]],"surfaceOp":"append"}
 {"type":"step/end","data":{"turn":3,"step":1}}
 {"type":"turn/end","data":{"turn":3,"reason":{"kind":"completed"}}}

+ 2 - 2
snapshots/sdk/subagent-report/tool-schemas.1.expected.json

@@ -322,7 +322,7 @@
     },
     {
       "name": "report",
-      "description": "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.",
+      "description": "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. If that agent is working, the report steers its nearest step; otherwise it starts a turn. A failed call may still have arrived if a later tool-result hook failed, so do not blindly repeat it.",
       "parameters": {
         "type": "object",
         "properties": {
@@ -338,7 +338,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/sdk/text-turn/tool-schemas.expected.json

@@ -322,7 +322,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {

+ 2 - 2
snapshots/session/agent-instructions/tool-schemas.expected.json

@@ -322,7 +322,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {
@@ -1023,7 +1023,7 @@
       },
       {
         "name": "send_message",
-        "description": "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.",
+        "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
         "parameters": {
           "type": "object",
           "properties": {

+ 1 - 1
snapshots/session/both-mode-turn/system-prompt.expected.md

@@ -159,7 +159,7 @@ interface ToolArgsMap {
     /** Path to the image file, resolved by the filesystem backend. */
     file_path: string;
   } & Record<string, JsonValue>;
-  /** 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 background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered. */
   send_message: {
     /** The subagent id returned when the background subagent was started. */
     subagent_id: string;

+ 1 - 1
snapshots/session/both-mode-turn/tool-schemas.expected.json

@@ -343,7 +343,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/session/code-mode-read-image/system-prompt.expected.md

@@ -161,7 +161,7 @@ interface ToolArgsMap {
     /** Path to the image file, resolved by the filesystem backend. */
     file_path: string;
   } & Record<string, JsonValue>;
-  /** 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 background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered. */
   send_message: {
     /** The subagent id returned when the background subagent was started. */
     subagent_id: string;

+ 1 - 1
snapshots/session/code-mode-turn/system-prompt.expected.md

@@ -161,7 +161,7 @@ interface ToolArgsMap {
     /** Path to the image file, resolved by the filesystem backend. */
     file_path: string;
   } & Record<string, JsonValue>;
-  /** 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 background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered. */
   send_message: {
     /** The subagent id returned when the background subagent was started. */
     subagent_id: string;

+ 2 - 2
snapshots/session/compaction-recovery/tool-schemas.expected.json

@@ -322,7 +322,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {
@@ -1023,7 +1023,7 @@
       },
       {
         "name": "send_message",
-        "description": "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.",
+        "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
         "parameters": {
           "type": "object",
           "properties": {

+ 1 - 1
snapshots/session/cordis-inspect-jsdoc/system-prompt.expected.md

@@ -326,7 +326,7 @@ interface ToolArgsMap {
     /** Path to the image file, resolved by the filesystem backend. */
     file_path: string;
   } & Record<string, JsonValue>;
-  /** 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 background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered. */
   send_message: {
     /** The subagent id returned when the background subagent was started. */
     subagent_id: string;

+ 1 - 1
snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json

@@ -540,7 +540,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/session/fs-glob-sampling/tool-schemas.expected.json

@@ -242,7 +242,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/session/lsp-definition/tool-schemas.expected.json

@@ -359,7 +359,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/session/product-subagent-both/tool-schemas.expected.json

@@ -322,7 +322,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/session/product-subagent-codex/tool-schemas.expected.json

@@ -322,7 +322,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json

@@ -322,7 +322,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json

@@ -322,7 +322,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/session/ralph-loop/tool-schemas.1.expected.json

@@ -322,7 +322,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/session/ralph-loop/tool-schemas.2.expected.json

@@ -322,7 +322,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/session/session-query-spill/tool-schemas.expected.json

@@ -322,7 +322,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json

@@ -322,7 +322,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json

@@ -385,7 +385,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/session/text-turn/tool-schemas.expected.json

@@ -322,7 +322,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/session/web-fetch/tool-schemas.expected.json

@@ -322,7 +322,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/web/code-mode-round/system-prompt.expected.md

@@ -189,7 +189,7 @@ interface ToolArgsMap {
     /** Path to the image file, resolved by the filesystem backend. */
     file_path: string;
   } & Record<string, JsonValue>;
-  /** 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 background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered. */
   send_message: {
     /** The subagent id returned when the background subagent was started. */
     subagent_id: string;

+ 1 - 1
snapshots/web/cordis-tool-round/tool-schemas.expected.json

@@ -582,7 +582,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 1
snapshots/web/fresh-round-trip/tool-schemas.expected.json

@@ -385,7 +385,7 @@
     },
     {
       "name": "send_message",
-      "description": "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.",
+      "description": "Send a message to a background subagent by its subagent id, continuing the same conversation. If it 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 subagent — only confirmation that the message was delivered. A failure means the message was NOT delivered.",
       "parameters": {
         "type": "object",
         "properties": {

+ 1 - 0
tsconfig.base.json

@@ -75,6 +75,7 @@
       "@deepseek-ai/dsh-session-title/types": ["./packages/session/session-title/src/types.ts"],
       "@deepseek-ai/dsh-session-title/client": ["./packages/session/session-title/src/client.ts"],
       "@deepseek-ai/dsh-subagent/client": ["./packages/subagent/subagent/src/client.ts"],
+      "@deepseek-ai/dsh-subagent/internal": ["./packages/subagent/subagent/src/internal.ts"],
       "@deepseek-ai/dsh-workspace/types": ["./packages/workspace/workspace/src/types.ts"],
       "@deepseek-ai/dsh-util-workspace-path": ["./packages/util/workspace-path/src/index.ts"],
       "@deepseek-ai/dsh-session-stats/types": ["./packages/session/session-stats/src/types.ts"],