refactor(subagent): merge continuation control service

This commit is contained in:
Dudu
2026-07-27 00:00:14 +08:00
committed by imccyu
parent 0a95ad8cc0
commit 88f913a9ae
71 changed files with 543 additions and 673 deletions
@@ -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/simplification/2026-07-26-merge-subagent-control-service.md
2026-07-26-merge-subagent-control-service.md: a5ce673997502ef6fbd7c66ff4e94e301d4192ba
2026-07-26-merge-subagent-control-service.zh.md: cf867ab444438b7ee62cde68a3d53e83c3d049d1
@@ -0,0 +1,37 @@
# Agent Note: Merge subagent control into the subagent service
Status: implemented
English | [中文](2026-07-26-merge-subagent-control-service.zh.md)
## Problem
Continuable-child orchestration originally lived in a separate `ctx.subagentControl` service above the raw `ctx.subagents` provider seam. That split kept provider dispatch independent of Tasks and persistence, and gave model and human adapters one orchestration contract. In practice the two services described one capability family, every continuable caller needed both, and the provider-bound delegation tool had to infer policy from `provider.resume` and inspect whether the control service and `send_message` tool happened to be loaded. This made sibling plugin presence decide execution semantics and coupled starting continuable work to an optional follow-up surface.
## Decision
`SubagentService` is the only public service. It retains raw `start(name, request)` and `resume(name, request)` for callers that own run collection, and exposes `startContinuable(spec)` and `sendMessage(...)` for durable Task-backed activations. The standalone `@deepseek-ai/dsh-subagent-control` package and `ctx.subagentControl` key are deleted; the optional `@deepseek-ai/dsh-tool-subagent-control` package injects `ctx.subagents` directly.
The continuation implementation remains an internal manager rather than expanding the provider registry's core state. `SubagentService` creates it through `ctx.inject(['tasks', 'agents'], ...)`, so the injected Cordis child fiber owns its Task completion listener and teardown effects. Loading the provider registry does not require Tasks or persistence. The manager exists only while Tasks and Agents are available, and each continuation operation resolves session persistence at the point it needs durability. Disposing that fiber cancels and settles active continuations before releasing their associations.
`startContinuable` remains distinct from raw `start` because it has a different ownership and timing contract: it allocates the durable child id, creates the Task, and returns both ids synchronously while startup continues inside the Task. Raw `start` instead awaits provider publication and transfers a holder-owned run. Folding the method onto `start` through flags or return unions would broaden the low-level contract and create more change than keeping the existing explicit entry.
Each `@deepseek-ai/dsh-tool-subagent` instance selects `backgroundMode: 'one-shot' | 'continuable'`, defaulting to `one-shot`. This configuration is policy; `provider.resume` is only the capability check for configured continuable mode. A resumable provider can therefore still run one-shot background work. The `send_message` tool is an independent adapter: loading or omitting it neither enables nor disables `startContinuable`.
## Alternatives considered
**Keep the separate service.** This preserves the strongest dependency separation, but every production continuable path composes both services and the extra public key exposes an architectural distinction callers do not need. The internal manager preserves optional Task and persistence dependencies without a second service.
**Infer continuable mode from `provider.resume`.** Method presence correctly states cold-resume capability but not deployment policy. It forced every resumable provider into continuable background semantics and made missing sibling plugins a runtime error. Explicit tool configuration separates choice from capability.
**Register continuation access or inspect the follow-up tool.** A registry could tell the delegation tool whether a continuation surface exists, but starting durable work does not require any follow-up adapter. Such a registry would encode UI composition into execution policy and recreate the sibling dependency under another name.
**Merge raw and continuable starts into one method.** A flag on `start` would return either a ready run or immediate Task and child identities, weakening a simple ownership boundary. Keeping `startContinuable` is the smaller change and preserves both contracts explicitly.
## Consequences
- The service topology has one public key and one package fewer while raw provider dispatch remains usable without Tasks or persistence.
- Continuable mode fails at provider mount when the configured provider lacks `resume`; missing Tasks, Agents, or persistence still fail at the earliest operation that requires them.
- Follow-up delivery remains optional. Deployments may start and collect continuable work through Task tools without exposing `send_message`.
- The continuation manager is still Task- and persistence-aware inside the `dsh-subagent` package, so the package declares optional peer dependencies on those services even though ordinary `start` and `resume` callers do not need them.
- Existing continuation races, authorization, durability, cancellation, and settle-then-dispose semantics are unchanged and remain pinned by the migrated `subagent` tests.
@@ -0,0 +1,37 @@
# Agent Note: 将 subagent 控制合并到 subagent 服务
Status: implemented
[English](2026-07-26-merge-subagent-control-service.md) | 中文
## 问题
可继续 child 的编排最初位于原始 `ctx.subagents` 提供方 seam 之上的独立 `ctx.subagentControl` 服务中。该拆分使提供方分发与 Task 和持久化无关,并为模型与人工适配器提供统一的编排契约。实践中,两个服务属于同一组功能,每个可继续调用方都需要二者,而绑定提供方的委派工具必须根据 `provider.resume` 推断策略,并检查控制服务与 `send_message` 工具是否碰巧已加载。如此一来,配套插件是否存在会决定执行语义,并将可继续工作的启动耦合到可选的后续操作接口。
## 决策
`SubagentService` 是唯一的公开服务。它为自行收集 run 的调用方保留底层 `start(name, request)``resume(name, request)`,并公开 `startContinuable(spec)``sendMessage(...)`,用于具备持久性、由 Task 支撑的激活。系统删除独立的 `@deepseek-ai/dsh-subagent-control` 包(package)和 `ctx.subagentControl` 键;可选的 `@deepseek-ai/dsh-tool-subagent-control` 包则直接注入 `ctx.subagents`
继续执行的实现仍是内部管理器,不会扩展提供方注册表的核心状态。`SubagentService` 通过 `ctx.inject(['tasks', 'agents'], ...)` 创建该管理器,因此注入的 Cordis child fiber 拥有自身的 Task 完成监听器和拆卸 effect。加载提供方注册表不要求 Task 或持久化。只有 Task 和 Agent 可用时,该管理器才会存在;每项继续执行操作都在需要持久性时解析会话持久化服务。dispose(资源释放)该 fiber 会先取消并结算活跃的继续执行,再释放其关联。
`startContinuable` 与底层 `start` 保持分离,因为二者的所有权与时序契约不同:前者分配持久化 child id、创建 Task,并同步返回两个 id,而启动过程继续在 Task 内运行;底层 `start` 则等待提供方发布,并移交一个由持有方负责的 run。若通过标志或返回值联合类型将该方法并入 `start`,会扩大底层契约,改动反而多于保留现有的显式入口。
每个 `@deepseek-ai/dsh-tool-subagent` 实例都会选择 `backgroundMode: 'one-shot' | 'continuable'`,默认值为 `one-shot`。这项配置表示策略;`provider.resume` 只用于检查所配置的可继续模式是否受提供方支持。因此,可恢复的提供方仍可执行一次性后台工作。`send_message` 工具是独立适配器:加载或省略该工具既不会启用也不会禁用 `startContinuable`
## 已考虑的替代方案
**保留独立服务。** 这样能保持最严格的依赖分离,但每条生产环境中的可继续路径都要组合两个服务,而额外的公开键会暴露调用方并不需要的架构差异。内部管理器无需第二个服务,也能保留可选的 Task 和持久化依赖。
**根据 `provider.resume` 推断可继续模式。** 方法是否存在可以准确表示从持久化存储恢复的功能,却不能表示部署策略。这会迫使每个可恢复的提供方都采用可继续后台语义,并使配套插件缺失成为运行时错误。显式的工具配置将选择与功能分离。
**注册继续执行访问入口,或检查后续操作工具。** 注册表可以告诉委派工具继续执行接口是否存在,但启动具备持久性的工作不需要任何后续操作适配器。这样的注册表会把 UI 组合编码进执行策略,并以另一个名称重新建立插件间依赖关系。
**将底层启动与可继续启动合并为一个方法。** `start` 上的标志会使该方法返回就绪的 run,或立即返回 Task 和 child 标识,从而削弱简单的所有权边界。保留 `startContinuable` 改动更小,也能明确保留两项契约。
## 影响
- 服务拓扑少了一个公开键和一个包,同时底层提供方分发仍可在没有 Task 或持久化时使用。
- 配置的提供方缺少 `resume` 时,可继续模式会在提供方挂载阶段失败;缺少 Task、Agent 或持久化时,仍会在需要它们的最早操作处失败。
- 后续消息投递仍为可选功能。部署可以通过 Task 工具启动并收集可继续工作,而不公开 `send_message`
- `dsh-subagent` 包内的继续执行管理器仍然感知 Task 和持久化,因此该包会将这些服务声明为可选的对等依赖(peer dependency),即使普通的 `start``resume` 调用方并不需要它们。
- 现有的继续执行竞态、授权、持久性、取消及先结算再 dispose 的语义均保持不变,并继续由迁移后的 `subagent` 测试固定。