refactor(plan): remove generic mode abstraction

This commit is contained in:
Tianyi Cui
2026-07-22 16:57:23 +08:00
parent 92da23270d
commit f4185122dc
61 changed files with 990 additions and 1161 deletions

View File

@@ -2,6 +2,8 @@
Status: implemented
> **Superseded vocabulary (2026-07-22):** [Collapse named session modes into plan mode](../simplification/2026-07-22-plan-specific-collaboration-state.md) replaces this note's generic `dsh-mode`, `mode/set`, definition map, and `ctx.modes` design with the current plan-specific `dsh-plan-mode`, `plan/mode`, `{ section }`, and `ctx.planMode` contract. The review, boundary, reconstructability, and sandbox-orthogonality decisions below remain in force; generic API examples are retained as the historical design this simplification removed.
## Problem
Before this change, the harness had no durable way to put one agent into a distinct working stance. Plan mode needs the agent to explore and design under planning guidance, produce a reviewable artifact, cross an explicit approval boundary, and restore that state across resume and fork without making the model-visible request diverge from the session log.

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-22-plan-specific-collaboration-state.md: 8a7caf9b1150cb6d3ea2c8ed52e42751f30c773c
2026-07-22-plan-specific-collaboration-state.zh.md: c4d2528cc06a74ce8c152199bc2503daff315dbf

View File

@@ -0,0 +1,47 @@
# Agent Note: Collapse named session modes into plan mode
Status: implemented
English | [中文](2026-07-22-plan-specific-collaboration-state.zh.md)
## Problem
The first plan-mode implementation introduced a generic named-mode registry even though the product shipped only `plan`. `ModeConfig.modes`, definition-name validation, `ctx.modes.list()`, retired-definition fallback, and a synthetic `review` mode in tests existed only to support hypothetical future collaboration modes. The production-specific behavior—plan guidance, `/plan`, and `exit_plan_mode`—still lived in the same package, so the generic API did not isolate a reusable mechanism from plan policy.
The word “mode” also spans unrelated domains. Sandbox mode is an enforcing policy owned by `ctx.sandboxPolicy` and logged as `sandbox/mode`; plan mode is a collaboration stance that contributes guidance and a reviewed exit. Treating both as instances of one named-mode abstraction would obscure their independent ownership. ACP's protocol happens to expose a generic mode picker, but that is an adapter vocabulary rather than evidence that the harness needs a generic mode domain.
## Decision
Plan mode owns a plan-specific product package: `@deepseek-ai/dsh-plan-mode` at `packages/plan/plan-mode/`. The durable fact is `plan/mode: { active: boolean }`, folded by `foldPlanMode(events)` with `false` as the empty-log value. `ctx.planMode.get(agent)` returns `{ active, pending? }`, and `set(agent, active)` records the boundary-applied selection. The existing prompt-submit, continuation, retry, append-failure, and disposal fences remain unchanged in meaning.
Configuration is exactly `{ section: string }`. The package registers the fixed `plan:policy` section, `/plan [message]`, and `exit_plan_mode` itself. Bare `/plan` selects the state; a non-empty argument selects it first and then sends the trimmed text through `agent.steer()`, making the text an ordinary logged user message in the affected step. The exit tool remains registered while plan mode is inactive so the request tool catalog stays stable.
ACP keeps its protocol-level `default` and `plan` ids. The bridge maps those two ids to the boolean service, advertises only that fixed pair, rejects every other id at the adapter boundary, and maps committed `plan/mode` events back to `current_mode_update`. The protocol remains generic without forcing genericity into the product domain.
Sandbox mode and approval policy remain separate enforcement axes. Plan mode neither reads nor writes them, and the simplification introduces no shared base type, registry, or preset abstraction across those concepts.
## Deleted surface
- The arbitrary definition map, mode-name regular expression, reserved-name rules, and per-definition command loop.
- `ModeDefinition`, the resolved definition map, `ctx.modes.list()`, string-valued get/set state, and unknown or retired mode handling.
- Test-only `review` mode cases and claims that additional modes can be added through configuration.
- Generic `mode/set` and `mode:policy` names; the plan package now owns `plan/mode` and `plan:policy`.
## Alternatives considered
**Keep a private generic registry and expose only plan today.** Rejected because the unused name/config machinery would still be maintained and tested without a second production consumer. A future collaboration state can establish the right shared seam from two concrete cases.
**Fold sandbox mode into the same service.** Rejected because collaboration guidance and execution confinement have different owners, lifecycle semantics, and consumers. Their shared English noun is not a domain relationship.
**Let ACP own plan state.** Rejected because TUI, resume, fork, prompt assembly, and the exit tool need the same logged fact independently of ACP. ACP owns only the wire projection.
## Verification
- Package tests retain boundary ordering, retry, append-failure, HMR disposal, prompt assembly, stable native and Code Mode schemas, review outcomes, and invariant coverage through the boolean service.
- Command tests cover bare `/plan`, `/plan <message>`, absence of `/mode` and `/review`, and effect-scoped removal.
- ACP tests cover fixed advertisement, both ids, unknown-id rejection, optimistic updates, committed exits, and load replay.
- The keyless TUI scenario enters through `/plan <message>` and proves `plan/mode` precedes the first request header and that the message is logged under plan guidance.
## Consequences
The implementation has one vocabulary for one shipped feature. Adding another collaboration stance is now an explicit design decision instead of a config entry, while ACP clients continue to see their standard mode picker. The migration intentionally rejects old `mode/set` logs and old `modes.plan.section` configuration under the repository's pre-release format policy.

View File

@@ -0,0 +1,47 @@
# Agent Note: 将具名会话模式收敛为 plan mode
Status: implemented
[English](2026-07-22-plan-specific-collaboration-state.md) | 中文
## 问题
产品只交付了 `plan`,首个 plan mode 实现却引入了通用的具名模式注册表。`ModeConfig.modes`、定义名称校验、`ctx.modes.list()`、已退役定义的回退逻辑,以及测试中合成的 `review` 模式都只为支持假想中的未来协作模式而存在。plan 引导、`/plan``exit_plan_mode` 这些生产专用行为仍位于同一个包package因此通用 API 并未将可复用机制与 plan 策略隔离开来。
「mode」一词还横跨互不相关的领域。沙箱模式是由 `ctx.sandboxPolicy` 拥有、以 `sandbox/mode` 记录日志的强制执行策略plan mode 则是一种协作方式会贡献引导内容和经评审的退出路径。若把两者都视为同一个具名模式抽象的实例就会掩盖二者各自独立的归属关系。ACPAgent Client Protocol协议恰好暴露了通用模式选择器但这只是适配器词汇并不能证明 harness 需要通用模式领域。
## 决策
Plan mode 拥有一个 plan 专用产品包:位于 `packages/plan/plan-mode/``@deepseek-ai/dsh-plan-mode`。持久化事实为 `plan/mode: { active: boolean }`,由 `foldPlanMode(events)` 折叠,空日志值为 `false``ctx.planMode.get(agent)` 返回 `{ active, pending? }``set(agent, active)` 则记录在边界生效的选择。现有的提示词提交、continuation、重试、追加失败和 dispose资源释放栅栏在语义上保持不变。
配置严格为 `{ section: string }`。该包自行注册固定的 `plan:policy` 段、`/plan [message]``exit_plan_mode`。不带参数的 `/plan` 选择该状态;非空参数则先选择该状态,再通过 `agent.steer()` 发送去除首尾空白后的文本,使该文本在受影响的步骤中成为一条记录到日志的普通用户消息。即使 plan mode 未激活,退出工具仍保持注册,以确保请求工具目录稳定。
ACP 保留协议层的 `default``plan` id。桥接层把这两个 id 映射到布尔服务,只公布这组固定选项,在适配器边界拒绝其他所有 id并把已提交的 `plan/mode` 事件映射回 `current_mode_update`。协议仍保持通用性,但不会迫使产品领域也采用通用抽象。
沙箱模式与审批策略仍是彼此独立的强制约束轴。Plan mode 既不读取也不写入二者;此次简化也没有为这些概念引入共享基类型、注册表或预设抽象。
## 删除的接口
- 任意定义映射、模式名正则表达式、保留名称规则以及逐定义命令循环。
- `ModeDefinition`、解析后的定义映射、`ctx.modes.list()`、字符串值的 get/set 状态,以及未知或已退役模式处理。
- 仅用于测试的 `review` 模式用例,以及可通过配置添加其他模式的表述。
- 通用的 `mode/set``mode:policy` 名称plan 包拥有 `plan/mode``plan:policy`
## 考虑过的替代方案
**保留私有的通用注册表,目前只暴露 plan。** 不予采纳,因为没有第二个生产消费方时,仍需维护和测试未使用的名称与配置机制。未来若出现另一种协作状态,可以从两个具体案例出发建立合适的共享 seam。
**将沙箱模式折叠进同一服务。** 不予采纳因为协作引导与执行约束有不同的归属方、生命周期语义和消费方。二者的英文名称都含「mode」不代表存在领域关系。
**让 ACP 拥有 plan 状态。** 不予采纳,因为 TUI、恢复、fork、提示词组装和退出工具都需要在 ACP 之外独立使用同一项已记录事实。ACP 只拥有协议投影。
## 验证
- 包测试通过布尔服务继续覆盖边界顺序、重试、追加失败、HMR热模块替换资源释放、提示词组装、稳定的原生 schema 与 Code Mode schema、评审结果和不变式。
- 命令测试覆盖不带参数的 `/plan``/plan <message>`、不存在 `/mode``/review`,以及随 effect 作用域移除。
- ACP 测试覆盖固定模式列表公布、两个 id、未知 id 拒绝、乐观更新、已提交退出和加载回放。
- 无密钥 TUI 场景通过 `/plan <message>` 进入,证明 `plan/mode` 先于首个请求头,且消息在 plan 引导下记录到日志。
## 后果
该实现只用一套词汇描述一项已交付功能。若要添加另一种协作方式必须显式作出设计决策而不能只增加配置项ACP 客户端仍可看到标准模式选择器。根据仓库的预发布格式策略,本次迁移有意拒绝旧的 `mode/set` 日志与 `modes.plan.section` 配置。