# Agent Note: plan mode——记录到日志的逐 agent 会话模式

Status: implemented
Archived: 2026-07-26

[English](2026-07-07-plan-mode.md) | 中文

> **已取代的词汇（2026-07-22）：**[将具名会话模式收敛为 plan mode](../simplification/2026-07-22-plan-specific-collaboration-state.md) 已将本笔记中通用的 `dsh-mode`、`mode/set`、定义 map 与 `ctx.modes` 设计，替换为当前 plan 专用的 `dsh-plan-mode`、`plan/mode`、`{ section }` 和 `ctx.planMode` 契约。下文的评审、边界、可重建性与沙箱正交性决策仍然有效；通用 API 示例则作为此次简化所移除的历史设计保留下来。

> **已取代的 ACP（Agent Client Protocol）映射：**[ACP 作为仅面向自动化的协议](../simplification/2026-07-23-acp-automation-only-protocol.md)移除了下文所述的选择器、配置选项和 elicitation 映射。面向人类的接口仍可使用 plan mode。

## 问题

此次变更之前，harness 无法持久地让某个 agent（智能体）采用独特的工作姿态。Plan mode 要求 agent 在规划指引下探索和设计，产出可供评审的产物，跨过明确的审批边界，并在恢复与 fork 后还原该状态，同时不能让模型可见请求偏离会话日志。

既有扩展 seam 已经提供了周边机制：[`system-prompt/assemble`](../../../../packages/core/system-prompt/README.md) 为每个步骤塑造指引，已发送的请求则记录在 `request/header*` 事件中（参见[可重建性](../../implemented/architecture/2026-07-05-reconstructable-requests.md)）；[`ctx.userInteraction`](../../../../packages/ui/user-interaction/README.md) 承载审批问题与纠正反馈（参见 [ask-user 先例](../../implemented/feature/2026-06-25-ask-user-question.md)）；`SessionEventMap` 承载逐 agent 的持久事实（参见 [`todo/write` 先例](../../implemented/feature/2026-06-29-todo-write-tool.md)）。缺少的是将这些 seam 连在一起的具名会话状态，同时仍让独立的沙箱轴与审批轴负责执行约束。

## 决策

交付项是 **plan mode**。它作为首个**会话模式**发布，即一个具名、记录到日志且逐 agent 生效的协作状态：模式定义是由部署配置、供模型查看的指引；对某个 agent 生效的模式则是从其日志折叠出的会话状态。模式构成一条轴，强制约束旋钮——沙箱模式与审批策略——构成其他轴；它们从不互相读写，这与 Codex 将 Plan／Default 协作预设同沙箱及审批设置分开的做法一致。新的产品包（package）`@deepseek-ai/dsh-mode` 位于 `packages/mode/mode/`，拥有事件词汇、精简的 `ctx.modes` 服务和全部监听器；循环无需改动。`plan` 是唯一的必需定义；采用模式形状的词汇，是为了以后增加第二种模式时无需重命名持久事件类型，而不是因为当前还会发布其他模式。

该状态是 `SessionEventMap` 的一个成员：**`mode/set`** 是只记录日志、不进入 surface 的事件，携带具有整值替换语义的 `{ mode: string }`；另有纯函数 `foldMode(events)` 返回生效模式，即最后一个 `mode/set`，没有该事件时则返回默认模式。由于[日志是事实通道](../../implemented/architecture/2026-06-30-event-domain-semantics.md)，恢复、fork 和压缩无需额外机制即可还原模式，UI 则从 `session/event` 读取模式切换。默认模式表示不存在模式指引，即没有段落、过滤或门禁。加载 `dsh-mode` 后，每种模式仍会贡献同一个稳定的 `exit_plan_mode` schema；这项固定成本避免了模式边界处的工具目录抖动。

模式的所有外显行为都是软约束：`mode:policy` 提示词段落渲染当前定义的指引，而 `exit_plan_mode` 在每种模式下都留在已注册的工具目录中，仅当折叠模式不是 `plan` 时才在执行阶段拒绝。因此，转换只会在下一步骤改变可归因 `request/header` 中的系统提示词部分，从而在不改变 Native schema 或 Code Mode SDK 的情况下继续满足[可重建性](../../implemented/architecture/2026-07-05-reconstructable-requests.md)。模式有意不强制执行任何约束：没有执行门禁，不过滤工具，也不触及沙箱或审批旋钮。若用户希望规划期间存在硬性的只读下限，可以在模式选择器旁切换沙箱模式选项；二者先后顺序任意，任何一条轴都不会扰动另一条轴。同样也不存在逐模式的工具允许／拒绝清单；模式允许哪些工具属于副作用问题，在工具定义能够声明自身副作用前暂缓处理（见[延期工作](#deferred)）。模式只依靠其段落指引与退出评审来约束行为。

模型通过 **`exit_plan_mode`** 工具离开 plan mode。其唯一参数是 plan 文本，因此可以从日志重建 plan；该工具自行通过用户交互 seam 完成评审：问题的辅助详情携带确切 plan，并提供选项与自由文本通道，而不是只有一项裸权限。审批通过后，记录到日志的模式切回默认模式；拒绝则成为携带用户逐字反馈的纠正错误，让模型能沿明确方向继续规划。用户可从任意接口通过 `ctx.modes.set()` 切换模式；切换会在下一个轮次边界应用（会话事件都封闭在轮次内），且只有模型可见状态确实变化时才向模型讲述一次。

## 高层 API

### 一次端到端的 plan-mode 会话

用户通过 ACP 模式选择器或终端入口中的 `/plan [message]` 将会话切换到 plan mode；从下一步骤开始，每个请求都携带已配置的 plan 指引段落。如果给出可选消息，同一命令还会把它提交到受影响的步骤中。`exit_plan_mode` schema 在默认模式下已经存在，并会保持逐字节不变。

模型进行探索与设计；段落中的指引会让它把变更推迟到 plan 中。沙箱与审批旋钮保持用户设置的值不变；希望规划期间由内核强制只读的部署方（或用户），可以把 plan mode 与独立的沙箱模式选项配合使用。

准备就绪后，模型调用 `exit_plan_mode`，并把 plan markdown 作为参数；评审问题将这段确切 markdown 作为辅助详情，用户可以批准，也可以要求继续规划并自由填写反馈。Native 调用还会渲染 plan 卡片；Code Mode 嵌套分发没有 Native 卡片，因此评审详情是共用的呈现接口。

批准后，工具把记录到日志的模式切回默认模式：下一步骤会移除 plan 段落，但保留同一个工具目录（变化后的 header 已记录到日志），此后的执行跟踪本就由 `todo_write` 负责。要求继续规划时，模型会收到携带用户反馈文本的纠正错误，随后修改并再次呈现。

### 部署配置

模式定义是经过校验的插件 Config；依照仓库约定，它可以通过 `cordis.yml` 修改，无需编辑代码。部署必须提供完整的 `plan` 段落；该包不内置任何模型指令。其他模式使用同一份配置 map：

```yaml
- id: mode
  name: '@deepseek-ai/dsh-mode'
  config:
    modes:
      plan:
        section: |
          You are in plan mode: explore and design, then present the
          plan for approval through exit_plan_mode.
```

定义的精确形状是 `{ section }`；其中有意不提供逐模式工具清单或强制约束字段（见[常见问题](#faq)）。定义名称使用小写斜杠命令子集 `/^[a-z][a-z0-9_-]*$/u`；`default` 是保留项（表示没有策略），不能用作键。名称无效或存在未知定义键——包括 `tools` 清单或 `access` 上限——会在加载时校验失败；未知模式名称则会在调用 `set()` 时大声失败。

### 在终端中

终端入口通过插件自有的命令注册表（`@deepseek-ai/dsh-commands`），为每个已配置定义获得一条进入命令：`dsh-mode` 为必需定义注册 `/plan [message]`，例如还会注册 `/review [message]`（当配置 `review` 时）。每条命令都记录其具名切换；非空的可选消息会去除首尾空白并传给 `agent.steer()`，后者会把消息放入运行中 agent 的下一步骤，或委托给 `send()` 以开启新的空闲轮次。命令名称与结果不会进入模型历史；这条显式消息则会作为所选模式下的普通用户消息记录到日志。合成的 `default` 条目不贡献命令。退出评审无需新机制即可直接在终端中提示：它是普通的用户交互问题，因此会进入组合后的用户交互提供方提示队列，与 `ask_user_question` 使用的队列相同。

### 通过 ACP

模式选择器是该包的对外接口：`session/new`／`session/load` 会通告 `availableModes`／`currentModeId`，其值来自 `ctx.modes`（通过 `ctx.get` 机会式消费，沿用 `tool-bash` 模式）；`session/set_mode` 调用 `set()` 并乐观通知 `current_mode_update`（待生效模式就是用户的选择，记录到日志的 `mode/set` 会在边界处跟进）；`session/event` 监听器则会在每次已记录切换不同于最近一次已发送值时再次通知。退出工具复用用户交互 ACP 提供方的 elicitation 流程；其 ACP 映射会携带评审 `detail`，因为 Code Mode 嵌套分发没有 Native plan 卡片，而 Native 调用还可以额外流式传输该卡片。沙箱模式、审批策略和模型等单项环境旋钮不是模式，应归入 `session/set_config_option`（见[常见问题](#faq)）。

### 面向 agent 创建方

`ctx.modes` 是完整的程序化接口：`list()` 返回已配置定义和供选择器使用的合成 `default` 条目；`get(agent)` 返回折叠模式与可能存在的待生效意图；`set(agent, mode)` 则根据 `list()` 的词汇校验名称，并记录将在边界应用的意图。`default` 始终是有效目标，因此退出模式与进入模式使用同一次调用。创建时没有模式选项；调用方在首个轮次前通过 `set()` 选择模式，随后以相同方式刷写。系统也不提供可订阅的实时 `agent/*` 镜像：UI 依照[事件领域语义](../../implemented/architecture/2026-06-30-event-domain-semantics.md)读取 `mode/set`，该事件来自 `session/event`。

## 详细设计

### 词汇

```text
'mode/set': { mode: string }        // SessionEventMap merge in dsh-mode: log-only, non-surface,
                                    // whole-value replace — the last one in the log wins
DEFAULT_MODE = 'default'            // the fold of a log with no mode/set; reserved, not definable
```

载荷不携带原因／溯源字段：工具驱动的切换在日志中紧邻其 `tool/call`，用户切换则位于轮次边界，因此原因就在日志相邻位置。这与[可重建性 Agent Note](../architecture/2026-07-05-reconstructable-requests.md)针对请求头事实所作的「叙述字段可以派生」决策相同（进行中的 `env/state` 事件之所以携带 `source`，正是因为其漂移变体在日志相邻位置没有原因；二者形成对照，并不冲突）。模式名称是配置声明的词汇，不是不透明的跨边界 id，因此仍使用裸字符串（不使用 `Branded<B>`）。

### 配置与解析步骤

```text
interface ModeDefinition { section: string }                    // prompt text — a mode's whole vocabulary
interface ModeConfig { modes: Record<string, ModeDefinition> }  // plan is required and owns its complete prompt
resolveConfig(config): ResolvedModes                            // explicit resolve (the dsh-bash template), fail-loud:
                                                                // missing plan, 'default', blank sections, and unknown keys rejected
```

单字段形状是有意采用的最简设计，并非最终词汇：逐工具策略维度会以工具定义中的副作用元数据形式回归（见[延期工作](#deferred)），在此处读取，而不是由每种模式重新声明；该维度到来时，配置形状不应需要迁移。

### 折叠、服务与刷写

`foldMode(events)` 是纯函数（导出供重建方与测试使用），直接折叠仅追加的会话日志；`mode/set` 不是 surface 节点，因此压缩无法遮蔽它。`set(agent, mode)` 根据 `list()` 的词汇校验名称，即已配置定义加上保留的 `default`；后者不能用作配置键，却始终可以作为 `set()` 目标。目标与待生效模式相同（没有待生效模式时则与当前模式相同）时，该方法丢弃无操作；其余情况会把 `{ mode, narrate }` 记录到 `WeakMap` 的待生效意图槽中。它不能立即追加，因为[每个会话事件都封闭在轮次内](../../implemented/architecture/2026-06-15-turn-enclosure-invariant.md)，而空闲 agent 没有打开的轮次。

循环拦截 seam 上经过故障隔离的监听器（参见[防御模式](../../../../docs/defensive-patterns.md)：策略插件不得阻塞提示词或轮次）会把待生效意图刷写为一条 `mode/set` 追加：`agent/prompt-submit` 在刚打开的轮次中、首次组装前触发；`agent/turn-continuation` 则在普通步骤关闭后、后续步骤开始前触发。自动请求恢复会绕过 continuation，因此，前置的 `agent/request-error` 包装器会先委托给组合后的策略和异步退避，只在 waterfall 返回循环前刷写 `retry` 决策；effect 作用域的生命周期守卫会抑制在插件资源释放后才恢复的已捕获包装器。三条路径都位于工具执行与日志发布之外（提交后的 `session/event` 观察器只负责观察），因此每个步骤都在其组装所折叠出的模式下运行。刷写模式与最后一个 `request/header` 处的折叠结果不同时，刷写会在同一帧中追加一条合并后的 `context/message` 通知（「用户已将此会话切换到 plan mode。」）；面向用户的叙述情形列在[常见问题](#faq)中。

### 软层：计算得出的段落与稳定的退出 schema

已注册的提示词段落从 `AssembleContext.agent` 读取调用 agent 的模式，并解析为当前定义的指引或 `''`。循环逐步骤渲染，并在渲染后的 header 发生变化时记录完整的 `request/header`，因此进入或离开模式均可归因。该段落在每种模式内保持静态，plan 本身则以消息和工具参数留在对话中；无需为了跨压缩保留状态，而在每个请求中重新注入独立的 plan 状态（[既有方案](#prior-art)采用的办法），徒增提示词抖动。

指引贡献为 `{ name: 'mode:policy', order: 50, text: context => … }`：排在人设（0）之后、工具指引（100–199）之前，并在默认模式或没有 agent 的组装中为空。`exit_plan_mode` 只通过 `ctx.tools` 注册一次且从不过滤，因此模式切换期间 Native schema 与 Code Mode 生成的 SDK 保持逐字节相同；未部署 `dsh-mode` 的环境则没有这项绑定。系统不注册 `tools/pre-execute` 监听器：模式不设置任何门禁，退出工具自身的折叠模式检查会拒绝 plan 之外的调用。退出评审是一个带选项和反馈的问题，不是权限，因此位于工具通过用户交互 seam 执行的过程内。

### `exit_plan_mode`

`defineTool` 有一个必填的 `plan: string` 参数。Native 执行会把它记录在普通 `tool/call` 中；Code Mode 在执行前记录外层 `run_code` 源码，并在分发结算后把规范化的嵌套参数追加到 `tool/code-dispatch`。`execute` 会拒绝没有 agent 的调用（沿用 [`todo_write` 先例](../../implemented/feature/2026-06-29-todo-write-tool.md)）和折叠模式不是 `plan` 的调用，并在询问评审人前拒绝空 plan 或不含标题的 plan；随后，它通过 `ctx.userInteraction.ask()` 发起一次单选评审，其 `detail` 是确切 plan，并开放自由文本反馈，供用户批准或要求继续规划。只有恰好选择一个 `Approve` 才表示同意，其他任何形状都按失败关闭处理。批准会记录一项将在边界生效且不叙述的意图，用于切换到 `default`，并返回简短确认。部署指引要求模型把这次调用作为回复中唯一且最后一次工具调用；如果模型违反该规则，运行时仍会让该批次剩余部分保留 plan 指引，下一步骤才记录变化后的 header，其中移除指引而工具 schema 保持不变。所有未获批准的结果都会返回纠正性的 `isError`，并让模式留在 `plan`。

其[渲染意图](../../implemented/architecture/2026-07-02-tool-render-intent-union.md)在设计之初就已确定：`presentCall` 是 `generic` 卡片，以 plan 的首个标题命名、以 plan markdown 作为内容，另配一张 `generic` 结果卡片。Native 入口会在问题之前显示该卡片；Code Mode 嵌套分发不会产生 Native 调用卡片事件，因此用户交互 `detail` 会在每个提供方上独立携带同一份 plan。系统机会式消费该 seam（`ctx.get('userInteraction')`），所以 `dsh-mode` 在没有它时仍可组合，并降级为[常见问题](#faq)中确定的手动退出方式。

### 依赖与接口

`dsh-mode` 是一个产品包，而不是由三个包组成的能力 seam（见[考虑过的替代方案](#alternatives-considered)）：它对等依赖 `cordis`、`dsh-session`、`dsh-agent`、`dsh-tools` 与 `dsh-system-prompt`，注入 `['tools', 'systemPrompt']`，并在执行时机会式读取 `ctx.userInteraction`（指向 `dsh-user-interaction` 的仅类型对等依赖边）；其仅有的 UI 侧边也是可选的仅类型对等依赖（逐定义进入命令使用 `dsh-commands`）。除 `ctx.modes` 调用接口外，所有内容都通过监听器参与，因此移除该包会平稳移除模式，而不会破坏消费方。终端入口无需模式专用代码：组合命令注册表后，`dsh-mode` 会自行注册每个定义的命令（指向 `dsh-commands` 的可选仅类型对等依赖边），退出评审则使用组合后的用户交互提供方提示队列。[高层 API](#over-acp) 已确定 ACP 协议映射；在包关系上，桥接层对 `dsh-mode` 采用仅类型对等依赖边并机会式读取服务，所以不含该插件的桥接层行为与当前完全相同。

### 已记录场景与 harness 操作

`input.json` 新增一种步骤操作 `{ "op": "setMode", "modeId": "plan" }`，通过真实的 `session/set_mode` RPC 驱动，并配有脚本化的 `elicitationAnswers` 队列。`plan-mode` 场景在第 1 个轮次前进入 plan，在独立配置的沙箱下运行真实的 `cat`，通过 `exit_plan_mode` 呈现 plan，接收脚本化审批，然后在下一步骤编辑。首个 `request/header` 包含完整、稳定的工具集和已配置模式段落；批准后变化的 header 会保留逐字节相同的工具 schema，只移除该段落。`plan-mode-reject` 固定纠正性的自由文本反馈和未变化的 plan 状态。两份记录都在 Seatbelt 或 bwrap 下回放宿主命令；后端特有的沙箱拒绝仍留在 bash 工具单元层。

### 机械收尾

系统不声明新的 Cordis 事件（`mode/set` 通过 `session/event` 传递，监听器附着到现有 waterfall），因此事件目录不变。同一变更重新生成以下内容：持久化日志目录（`mode/set`）、服务目录（`ctx.modes`，JSDoc 完整）、配置目录（`ModeConfig`）、工具目录（`exit_plan_mode`）、生产方／消费方 map 与文档图，以及模块图。仓库接线包括：根 tsconfig 的 `paths` 条目、新包组 README 和[包索引](../../../../packages/README.md)中的一行（新增顶层包组正是该表所命名的有意操作）、`architecture.md` 中经过预算检查的 `ctx.modes` 能力服务行，以及实操手册对应行的升级。

## 延期工作

以下各项都需要独立决策：通过转发的创建时模式选项实现 subagent 模式继承（由于没有消费方而移除，将随首个消费方回归）；`plan` 之外的预设模式（只读、接受编辑）；若待生效意图丢失被证明是真实问题，则引入空闲记录原语；以及最重要的**在工具定义上自行声明副作用**，即逐工具的只读／变更分类（MCP `ToolAnnotations` 词汇——`readOnlyHint`／`destructiveHint`——是自然模板，其中对不可信提示的警告意味着还需区分信任层级）。通用的逐模式工具策略正在等待这一项：本 Agent Note 最初发布过临时的逐模式名称允许清单，并在发布前移除；手工维护的清单错误地表达了副作用问题，必须跟踪部署所组合的每个工具，且会随工具增加而无声腐化。因此，按模式限制的工具可用性（以及逐工具 `ask` 策略）会作为已声明副作用的消费方回归，而首项消费需求就是其重启触发条件。

ACP 自动化组合不挂载 plan mode 或问题工具。面向人类的组合拥有 plan 选择与评审；聚焦的 plan-mode 测试和交互接口快照会固定其已记录状态、指引、评审和稳定工具 schema。

## 常见问题

以下内容澄清选定设计的行为；遭否决的设计见[考虑过的替代方案](#alternatives-considered)，已接受的代价见[后果](#consequences)。

**用户切换模式后何时生效？** 在下一个组装前边界生效：`agent/prompt-submit` 覆盖首个步骤，`agent/turn-continuation` 覆盖普通后续步骤，组合策略之后的 `agent/request-error` 重试决策覆盖自动恢复。因此，在请求或重试退避进行期间选择的模式会塑造下一次模型请求。这就是[既有方案](#prior-art)中每项产品都采用的「应用于后续请求」语义。

**何时向模型讲述模式变化？** 仅当模型可见状态确实变化时：刷写会把刚刷写的模式与最后一个 `request/header` 处的折叠结果进行比较，并合并讲述一次。净变化为零的切换序列（先进入 plan，再在边界前切回）不会产生叙述；工具驱动的退出只通过自身工具结果叙述；首个轮次前设置的模式也不叙述，因为该段落本身就是状态说明。该原则来自进行中 env-state 提案的边界叙述：如果提示词表层悄然切换，transcript（文本记录）仍会依据 header 已不再具备的状态进行论述。

**恢复时，配置已不再定义折叠出的模式会怎样？** 当前配置不再定义的折叠模式名称会在不通知的情况下表现为默认模式，因此会话既不会获得替代约束，也不会变得不可用。`set()` 的大声校验只覆盖写入路径；恢复后的日志以当时找到的配置为准。

**如果部署没有组合用户交互提供方，会怎样？** Plan mode 仍然安全，但只能手动退出：`ctx.userInteraction.ask()` 会抛出 `NO_PROVIDER`（seam 不存在时甚至无法解析到该服务），工具返回纠正性的 `isError`，退出方式降级为由用户切换模式，绝不会在未经评审时退出。模式段落会要求模型通过 `exit_plan_mode` 呈现 plan，并在失败时改用普通文本询问用户，因此模型会继续呈现，而不会停滞。

**为何没有逐模式工具允许清单？** 因为「哪些工具在规划模式下安全」是每个工具自身的属性（即副作用），不是模式的属性。逐模式名称清单会在错误的归属位置重新声明该事实，必须枚举部署所组合的每个工具（包括 MCP 服务器），且会随工具到来而无声腐化。在工具定义声明其副作用前（见[延期工作](#deferred)，其中归档了被移除的临时允许清单及其重启触发条件），模式只通过自身段落和退出评审约束行为；由此产生的暴露面属于已接受代价（见[后果](#consequences)）。

**subagent 是否继承父级模式？** fork 子级可以直接继承，因为父级的 `mode/set` 位于种子前缀中。spawn 子级从默认模式开始；创建时模式选项与 subagent 提供方的自动转发一并延期（见[延期工作](#deferred)）。

**plan mode 与沙箱只读模式有何关系？** 二者是互不接触的独立轴：模式是协作姿态（`mode/set` 折叠），沙箱模式是强制约束旋钮（`bash/sandbox-mode` 折叠，参见[沙箱 Agent Note](2026-07-06-sandbox.md)）。Plan mode 既不读取也不限制沙箱模式，与 Codex 将 Plan／Default 预设同沙箱及审批设置分开的做法完全一致。希望规划期间由内核强制只读的用户需要同时设置两者：以任意顺序切换模式选择器与沙箱模式选项；每次切换只改变自身折叠结果，因此二者互不干扰，也不存在可能崩溃的还原步骤。日志会把每条轴归因到各自事件：协作姿态对应 `mode/set`，隔离约束对应 `bash/sandbox-mode`。

**为何沙箱模式、审批策略或模型本身不属于模式？** 它们是独立于协作状态的单项环境旋钮。已退役的 ACP 映射记录在[仅面向自动化的协议决策](../simplification/2026-07-23-acp-automation-only-protocol.md)中。未来模式定义可以捆绑环境事实（在挂载处通过 `ctx.envState` 应用），让 Codex 风格的预设仍是一种模式；但把审批策略融合进模式概念本身的方案已在[考虑过的替代方案](#alternatives-considered)中遭否决。

## 既有方案

对已发布 plan mode（Claude Code、Cursor、Copilot、OpenCode、Gemini CLI、Cline、Windsurf、Codex）的调研表明，各产品都包含同样五个部分：低权限工具策略、plan 产物、审批时刻、执行状态切换，以及[问题](#problem)所依赖的持久状态。

只要产品公开模式接口，该接口就一定是清单，绝不是布尔值：Claude Code 的选择器提供 `plan`，旁边是 `acceptEdits`（另有自动进入 plan 的模式）；Codex 则把 `Plan` 与 `Default` 并列公开为协作模式预设，同时让审批和沙箱设置保持独立。ACP 传输层不公开这项面向人类的控制。

由部署拥有的示例提示词借鉴工具性行为，而非产品特有机制。它借鉴 Codex 的以下做法：即使收到祈使式实现语言也留在 plan mode；提问前先探索；区分仓库事实与由用户决定的选择；让 plan 完整覆盖 API、数据流、失败、测试和假设，从而足以作出决策。它还借鉴 Claude Code 的以下做法：禁止变更与提交；优先沿用现有模式；只针对需求或方案选择提问；通过退出工具完成规划，而不在普通文本中请求审批。它有意省略 Codex 协议标签，以及 Claude 的 plan 文件或分阶段 subagent 机制，因为这些属于各自运行时，而非本插件契约。

把模式留给约定的生态展示了应避免的失败形态。Pi 风格的模式扩展会争抢一个后写覆盖的全局活跃工具清单，只靠提示词文本强制「只读」（模型幻觉调用一个仍已注册的工具时，该调用会实际执行），并在每个请求中重新注入 plan 状态以跨过压缩。这里通过逐 agent 的折叠状态，以及压缩无法遮蔽的只记录日志、非 surface 事件，从结构上消除了有争议的全局清单和重复注入补丁。相较之下，仅靠提示词的形态被有意保留：Codex 的 Plan 正是如此实现，这也是模式轴可以与强制约束轴自由组合的原因。需要硬性下限的部署会把模式与独立的沙箱旋钮配对，而不是让模式携带自身强制约束（见[常见问题](#faq)）。

## 考虑过的替代方案

**以权限模式作为核心概念（Claude Code 的形态）。** 用一个 `permissionMode` 融合审批策略和工具策略。本设计中，它们是归不同所有者负责的两条轴：审批 seam 拥有「谁回答这个问题」，模式拥有「模型获得什么接口」。ACP 将二者建模为相关但有区别的概念（模式以后可以选择审批策略；届时是模式定义增加字段，而不是合并两者）。

**由三个包组成的能力 seam。** 接口／实现／消费方适合可替换后端；模式的可变部分是配置值，而不是实现。拆分会制造一个空实现包，与审批 seam 及 [`todo/`](../../implemented/feature/2026-06-29-todo-write-tool.md) 作出的「不要过早拆分」决策相同。

**由循环拥有模式状态。** 依据既有规则（用插件，而不修改循环）予以否决：该功能所需的每个钩子——组装、执行前处理、轮次边界、会话事件——都已经是有文档记录的 seam，修改循环只会增加耦合，别无收益。

**带默认拒绝门禁的逐模式工具允许清单（首个发布形态）。** 已在发布前移除。手工维护的名称清单会逐模式重新声明一项逐工具事实（即其副作用）：它必须枚举部署所组合的每个工具，包括 MCP 服务器和未来注册项，并会随工具增加而无声腐化（新增只读工具时，在有人编辑每种模式前都会被阻止；编写负担落在了解模式的人身上，而不是了解工具的人身上）。它还过度承诺：该清单看似安全边界，但 shell 之外的任何能力其实都不存在这种真实边界。通用维度已停放到副作用自声明（见[延期工作](#deferred)）；由此产生的后果——plan mode 只提供指引，也就是该门禁一度弥补的 Pi 缺口——是有意接受的，并计入[后果](#consequences)。

**模式上的 `access` 沙箱上限（第二个发布形态）。** 同样已在发布前移除。`ModeDefinition.access` 会把 bash seam 的逐调用沙箱解析限制在模式声明的上限内（一个 `bash/resolve-mode` waterfall 加上取阶梯最小值的监听器；守卫会在执行器无法施加约束时隐藏 bash，并在模式中途拒绝提升权限）。状态仍保持正交，因为上限从不写入沙箱旋钮；但两条轴并不正交：进入 plan 会改变沙箱实际强制执行的内容，把协作姿态与强制约束级别融合起来，违背评审最终达成的 Codex 形态分离方式（Plan／Default 预设从不触及沙箱或审批设置）。这种融合有一项用户可见症状：规划期间把沙箱选项切换为 `workspace-write` 不会产生任何效果。上限、waterfall 和 mode→bash 依赖边随后一并移除；部署可以把模式与独立沙箱模式选项配合使用，从而在规划期间由内核强制只读。以后也可以重新引入模式触发的预设（模式定义捆绑建议的旋钮值，并作为普通旋钮切换加以应用），而无需再次融合两条轴。

**仅存在于运行时的模式（只在 UI 或桥接层本地存在，不记录日志）。** 恢复与 fork 会悄然丢失模式，模式引起的 header 增量在日志中也没有可归因原因。记录到日志的状态让模式无需额外机制即可审计和还原。

**把模式切换作为 `context/message` 并通过 `agent.inject()` 写入。** 这样可以复用现有的轮次封闭路径，却会把策略状态放入模型 transcript；模型无需被告知两次（段落已经告诉它），而只记录日志的事实不应占用 surface。

**plan 文件存储（`.plans/` 目录）。** 这会为日志已经以可回放方式承载的内容创建第二个持久归属位置；需要文件的部署可以添加一个写入文件的工具。同一事实只应有一个归属位置。

**用布尔值 `planMode` 取代具名模式。** 对仓库已经跟踪的接口而言过于狭窄：ACP 通告的是模式清单，已发布的选择器也不只填入 plan（见[既有方案](#prior-art)）；以后再泛化会重命名持久事件词汇。字符串形状的机制现在不产生额外成本；只有 `plan` 作为定义发布。

**工具策略栈服务（对 Pi 批评的补救方案）。** 现在就为工具策略建立专用组合服务为时过早：本实现不执行按模式限制的工具过滤，未来的副作用策略可以通过现有带守卫的执行 seam 组合。只有已声明的工具副作用产生具体组合需求后，才应正式建立该服务。

**通过审批 seam 执行退出审批（一个 `{ kind: 'ask' }` 门禁决策）。** 最初草案提出该方案；当时审批 seam 是唯一正在落地的询问机制，所以显得自然，但它把评审放进了权限的位置。该 seam 的结果词汇有意封闭且仅供单次使用（`allowed-once`／`rejected`），因此拒绝无法携带反馈，批准也永远无法增加选项（例如「批准并接受编辑」）。退出时刻是一个问题，而非权限；用户交互 seam 为其提供选项与自由文本通道，拒绝反馈也会逐字传给模型。审批 seam 仍适合真正的权限门禁（沙箱提升权限），注册表的 `ask` 词汇也继续供希望在该处设置询问的部署使用。

**使用普通文本或 steering（中途引导）退出，而不使用工具。** 这样既没有产物，也没有审批时刻。工具参数本身就是可供评审的 plan，而评审问题会把结构化的是／否选择附着到确切转换上并交给人类。

## 后果

以下保证现在已经由单元、协议、快照和真实 API 测试层固定：

- 生效模式是会话日志的纯函数：恢复与 fork 无需额外机制即可还原它；下一变化步骤中，一条 `mode/set` 后会出现匹配的完整 `request/header`。
- 用户驱动的切换会在下一边界恰好叙述一次，净变化为零的切换序列不会产生叙述；工具驱动的退出只通过自身工具结果叙述。
- 默认模式下，插件不贡献模式段落，但会贡献稳定的 `exit_plan_mode` schema；未部署 `dsh-mode` 的环境没有这项绑定。
- 默认、plan 与自定义模式之间转换时，Native 工具 schema 与 Code Mode SDK 保持逐字节相同；只有已配置的指引段落发生变化。
- Plan mode 不改变强制约束轴上的任何内容：工具集、沙箱模式、权限提升和审批策略在 plan 与默认模式下表现完全相同；部署通过把模式与独立的沙箱／审批旋钮配合使用来强化规划。
- 模式定义可通过 `cordis.yml` 修改，无需编辑代码；其中必须提供完整 plan 指令。缺失 plan 配置、定义畸形和未知键会在加载时失败，未知模式名称会在 `set()` 时失败。
- `exit_plan_mode` 始终通告，在 plan 之外会拒绝；批准后只移除 plan 指引，并通过纠正性的 `isError` 携带继续规划反馈；每个面向人类的接口都由其用户交互提供方承载评审。
- 随功能落地一并交付的文档收尾包括：README、重新生成的目录（持久化日志、配置、Cordis 服务、工具）、包索引与架构行，以及实操手册中的对应行。

已接受的代价如下：在 agent 空闲时设置的待生效用户切换，如果进程在下一轮次前退出，就会丢失（UI 会重新应用；若实践中出现问题，空闲记录原语就是逃生口）。模式转换会改变顺序 50 处的系统提示词，因此从该处开始的缓存路径也会变化，但工具 schema 与 Code Mode SDK 不再抖动。**模式只依靠指引约束行为**：忽略该段落的模型可以在 plan 期间执行变更；评审时刻、会话日志以及独立的沙箱、审批和文件系统策略共同构成约束边界。强化规划意味着设置这些旋钮，而不是扩大模式职责；被移除的强制约束形态及其副作用声明重启触发条件仍记录在[考虑过的替代方案](#alternatives-considered)和[延期工作](#deferred)中。面向人类的接口拥有 plan 选择器与评审交互；ACP 自动化传输层两者都不承载。
