什么是计划模式
计划模式(plan mode)是一种被记录的每-agent 协作状态,它改变模型收到的语言指引并节制一个特定的退出工具——它是软指引,不是强制机制。唯一包是 packages/plan/plan-mode 下的 @deepseek-ai/dsh-plan-mode,UI 半壁在 packages/client/ui-plan。沙箱模式与审批策略独立地执行限制,且不读写计划状态。
计划模式给模型一段部署撰写的系统提示词段,告诉它先探索与设计,并注册 exit_plan_mode,从而在真正干活前把完整计划呈现给人类评审。它不限制工具,本身也不要求每步审批。
激活:一个按会话持久化的设置
计划模式存储为 plan/mode ——一个仅日志、全值替换的 SessionEventMap 成员,承载 { active: boolean }。因为被记录,foldPlanMode(events) 直接从容器的会话日志恢复计划状态,因此 resume、fork 与压缩都会自动保留它。UI 通过 session/event 观察已提交的翻转。
服务表面(ctx.planMode):
| 成员 | 语义 |
|---|---|
set(agent, active) | 空闲时立即追加独立的 plan/mode 事件(下个提示词前没有轮中 pre-step);运行时则持有待决选择备下次接受的轮中 pre-step。返回 committed/queued、cancelled 反转或 noop。 |
get(agent) | 返回 { active, pending? },把被记录状态(用于组装当前步骤)与用户的轮中选择分开。 |
当最后一次被记录的请求头描述了另一状态时,改变的用户选择会贡献一条插件来源的 user/message 通知。Fork 的 agent 继承被记录的计划状态;新产生的 agent 初始为 inactive。
与普通模式的行为差异
- 激活时,插件把配置的
section渲染为名为plan:policy、提示顺序 50 的系统提示词段。inactive 模式不贡献文本,因此没有 token 成本,且除非发生转换否则不会使其 KV 失效。 exit_plan_mode始终注册(两种状态),保持工具模式跨转换稳定。其执行路径只接受激活的计划模式,并且只有经ctx.userQuestions获得精确用户审批后才离开计划模式。- 进入或离开会从顺序 50 起改变系统提示词,从而移动 KV 缓存的可复用边界。
这与基于工具与守卫的强制形成鲜明对比:计划模式从不禁用任何工具。需要强制限制的部署必须独立配置沙箱与审批控制。
评审 / 退出流程
exit_plan_mode 是经评审的退出。当模型提交其计划时,工具发起一个问题,携带 plan-review 呈现意图:
const REVIEW_ID = 'plan-review'
intent: { kind: 'plan-review', approve: APPROVE_LABEL }有能力 UI(Web 客户端)把计划呈现为决策而非通用问题,命名 approve 标签;不识别该标签的 UI 则渲染通用选项列表。两种方式工具都读到相同的答案。获批的评审返回规范值 { approved: true };被拒的评审是携带评审反馈的失败调用;被忽略的评审(用户关闭请求去说话)会如此报告给模型,告诉它留在计划模式并等待消息。
EXIT_PLAN_MODE = 'exit_plan_mode' 从 packages/plan/plan-mode/src/index.ts 导出;其 schema 钉在 docs/tool-catalog.md 的 @deepseek-ai/dsh-plan-mode 条目。
/plan 命令与退出条件
当组合了 ctx.commands 时,该包注册 /plan 并保留精确参数 off 用于直接退出:
| 输入 | 行为 |
|---|---|
/plan | 选择计划模式(激活) |
/plan <消息> | 先选择计划模式,再通过 agent.steer() 提交 <消息>,使之在计划指引下成为下一步骤的普通已记录用户消息 |
/plan off | 不发模型输入地选择 inactive;也取消待决的进入 |
退出条件因此是两个:/plan off(人类直接退出),或模型提交计划后经获批的 exit_plan_mode 评审。不存在基于计时器或工具计数的退出。
退出工具 schema 与评审交换
exit_plan_mode 在两种状态下都保持可用,从而让工具 schema 跨转换保持稳定;无论哪种方式,调用方都按 ToolRuntime 模式支付 schema 成本。执行行为随状态而异:
| 状态 | exit_plan_mode 行为 |
|---|---|
| 激活计划模式 | 打开 plan-review 用户问题;获批 → 返回 { approved: true } 并离开计划模式;被拒 → 带评审反馈的失败调用;被忽略 → 命名用户接管权的失败调用 |
| inactive | 调用失败(该工具并无可退出的东西) |
评审问题声明 plan-review 呈现意图,命名 Approve 为批准标签。因为意图只改呈现,不识别该标签的 UI 会渲染通用选项列表却返回相同标签,所以工具任一种方式都读到相同的 { approved: boolean } 值。plan 参数、评审结果与叙述式活跃退出都会正常扩展会话历史,且不改工具目录。
会话投影单元
当组合挂载 ctx.sessionProjections(@deepseek-ai/dsh-session-projection)时,此包注册 plan 投影单元。它折叠两类事件:一条名为 plan、带已记录 args 的 command/run 记录设定想要目标(off → inactive,其他 → active),而 plan/mode 提交被记录状态并清除它。view 派生 { active, pending },其中 pending 是纯重放量,可单独从日志恢复。它通过 ./types 服务宿主消费者、通过 ./client 服务客户端聚合。未组合注册表的配置不受影响。
持久存储语义
计划状态是全值替换事件,而非增量:每次 plan/mode 写入都记录整个 { active } 值,因此 foldPlanMode(events) 无需累积即可恢复当前状态——最后一次被记录的值(或 false)胜出。这带来具体后果:
- Resume 与 fork 纯粹从日志重建计划模式;没有需要重新发明的内存标志。
- 压缩会连同其他一切遮蔽较早的
plan/mode事件;因为只有最后的值有意义,遮蔽中间的翻转不改变任何东西。 - agent 空闲时的
set()会立即追加,因为下个提示词前没有轮中 pre-step。运行时的set()则把选择排队给下一次接受的轮中 pre-step;初始与续作 pre-step 都应用待决选择。同一步骤的请求恢复重试会复用其冻结的组装,并让选择保持待决以备下次 pre-step。
active(被记录、持久)与 pending(尚未提交的轮中选择)之间的区分,正是 get(agent) 返回 { active, pending? } 的原因。
模型体验:针对性摘要
- 激活时,
plan:policy段出现在提示顺序 50;inactive 不贡献任何内容。进入或离开会从顺序 50 起改变系统提示词并移动 KV 缓存的可复用边界——这是计划模式唯一的缓存效果。 - 可选的
/plan <消息>后缀经agent.steer()变成一个修剪过的用户文本块,其历史 token 成本与单独提交该文本相同;裸/plan与/plan off不增加任何 token。 - 获批的
exit_plan_mode评审在plan/review交换之后离开计划模式;被忽略的评审告诉模型留在计划模式并等待用户的消息。
源码布局与公共表面
packages/plan/plan-mode/src/ 很紧凑——四个文件:
| 文件 | 关注点 |
|---|---|
index.ts | 插件 apply():注册 plan:policy 段、/plan 命令、exit_plan_mode、plan/mode 事件与 ctx.planMode;导出 EXIT_PLAN_MODE 与 SectionConfig 类型 |
types.ts | plan/mode 事件载荷与可合并扩展的 SessionEventMap 声明 |
client.ts | Web 会话尾部使用的客户端安全 plan 投影值与 SessionProjectionMap 键 |
invariant.ts | 为重放不变量伴生件校验 plan/mode 事件与投影折叠 |
该插件本身就是整个 @deepseek-ai/dsh-plan-mode 包——不像压缩家族那样有独立的 Service Definition / Provider 切分。计划模式是一个产品插件,通过三个接缝组合:系统提示词 section 注册表(plan:policy)、ctx.commands(/plan)与 ctx.userQuestions(exit_plan_mode 评审)。当部署省略 ctx.commands 时,/plan 命令便不被注册;服务与 exit_plan_mode 仍然可用。
配置
- id: plan-mode
name: '@deepseek-ai/dsh-plan-mode'
config:
section: |
You are in plan mode. Explore and design before presenting the complete
plan through exit_plan_mode.section 必填且非空;未知键在加载时失败。该包刻意不接受任意命名模式、工具过滤、沙箱设置或审批策略——这是记录在计划专属协作状态 Agent Note 中的有界显式设计。
UI 侧(简要)
packages/client/ui-plan 是 Web 渲染器。它消费插件拥有的 /plan 命令,并提供专用 plan-review 渲染器,把 exit_plan_mode 的 detail 呈现为带 approve 动作的计划评审。只有 Web UI 才有这个专用渲染器;其他交互 provider 通过其通用选项流呈现同一个问题。UI 驱动 ctx.planMode.set() 并通过 session/event 观察 plan/mode 翻转。
值得记录的一个注意点:若进程在另一次接受的轮中 pre-step 之前退出,在轮的最后一次已接受 pre-step 之后做出的选择会丢失,因此 UI 必须重新应用它。
计划模式与沙箱、审批的不同
一个常见误解是期望计划模式去限制行为。它并不——它改语言指引并节制一个退出工具。三者独立组合:
| 机制 | 包 | 它约束什么 |
|---|---|---|
| 计划模式 | dsh-plan-mode | 软指引(系统提示词段)+ 经评审的 exit_plan_mode;无工具限制 |
| 沙箱模式 | dsh-sandbox / native | 工具执行的文件系统与操作系统隔离 |
| 审批策略 | dsh-user-approval | 工具执行前的一次性 ask/never 门禁 |
想要既先计划协作又强制安全的部署需三者都配置;计划模式既不读也不写沙箱或审批旋钮的状态。这种分离是刻意的,便于每个维度独立演进。
已知限制与设计边界
上游 README 明确说明了计划模式不是什么:
- 指引,而非强制。 需要强制限制的部署必须独立配置沙箱与审批控制。
- 没有创建期计划选项。 Fork 的 agent 继承被记录的计划状态,而新产生的 agent 初始为 inactive。
- 没有工具限制、计时器或审批策略属于此包——这是有界、刻意的设计。
- 活跃子节点评审边界。 被另一 agent 拥有的活跃子节点无法打开
exit_plan_mode评审;失败的调用会告诉子节点把未决决策纳入其最终结果。单是持久 fork 血统并不会阻止被当作运行时根恢复的会话打开评审。 - 单一专用渲染器。 只有 Web UI 有
plan-review渲染器;另一个交互 provider 通过其通用选项流呈现相同请求。
进一步阅读
- 交互:命令、提问与审批 —
/plan与exit_plan_mode所依赖的问题接缝与命令平面。 - 系统提示词组装 —
plan:policy段如何在顺序 50 处注册。 - 权限与审批 — 为何计划模式只是指引而强制是分开的。
packages/plan/plan-mode/README.md— 完整行为契约(持久化状态、投影、模型体验)。.agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.md— 设计决策记录。