# Plan 模式与目标

> Shift+Tab 与 /plan 怎样让会话进入只读规划、计划如何审批落地；/goal 设定的线程目标如何记账、判定完成，并驱动回合自动续跑。

- 作者：David（道雾轩）
- 专栏：MiniMax Code 源码解读（https://daiw.net/manual/minimax-code.md）
- 最后更新：2026-09-19
- 原文：https://daiw.net/manual/minimax-code/plan-and-goal
- 转载与引用：请注明出处并附原文链接（https://daiw.net/about/copyright）

有两类任务不适合“一问一答”：一类要先想清楚、经用户批准再动手，另一类要跨很多回合持续推进，直到能证明做完。MiniMax Code 分别用 Plan 模式和线程目标（Thread Goal）处理。两者都记在会话上，由 local-runtime-v2 的会话与回合系统承载；模型通过扩展贡献的提醒与工具感知它们。

## Plan 模式：怎么用

- 在输入框按 `Shift+Tab`（`packages/tui/src/tui/shell/keybindings.ts:406`），或者用 `/plan on`、`/plan off`、`/plan status`、`/plan view`（`packages/tui/src/tui/commands/catalog.ts:169`）。按键不会立刻切换，只是给下一条消息挂上意图，提示栏显示 “Plan Mode · next message enters Plan”（`packages/tui/src/tui/controller/interaction/plan-mode-flow.ts:226`），再按一次就撤销。
- 意图随消息以 `plan-entry` 或 `plan-exit` 提交，在消息准入的数据库事务里生效（`packages/local-runtime-v2/src/service/plan/questionnaire/admission.ts:99`）：退出直接把会话切回默认模式；进入时若还有待答的问卷就拒绝，否则先准备好新的计划草稿再切到 Plan 模式（`admission.ts:171`）。
- 模型也能主动申请进入，但工具 `EnterPlanMode` 只在 `beta.desktopPlanModeAgentEntry` 为 `true` 时出现，而它默认关闭（`packages/config/src/config.ts:628`、`packages/local-runtime-v2/src/service/plan/plan-entry-policy.ts:11`）。`beta.desktopPlanMode: false` 是新进入的急停开关，已经在 Plan 里的会话仍可退出。
- 审批计划时，TUI 给三个选项：同意并开始实施、暂不处理、补充意见后修订（`packages/tui/src/tui/features/interaction/plan-review-panel.ts:17`）。

## 进入之后：能做什么、不能做什么

会话的 `interactionMode` 为 `plan` 时，回合预检会给装配上下文加一个 `plan` 字段，带上规范计划文件的路径（`packages/local-runtime-v2/src/service/turn-system/agent-host/preparation/turn-preflight.ts:106`）。这个文件固定在会话目录下的 `artifacts/plan.md`（`packages/local-runtime-v2/src/service/session-system/sessions/support/plan-document.ts:12`、`plan-document.ts:90`）；每次新进入 Plan 模式，旧文件先挪成 `.plan.md.plan-entry-backup`，进入失败就还原（`plan-document.ts:14`）。

约束分软硬两层。软的一层是 `planModeExtension`（`packages/agent-extension/src/plan-mode.ts:105`）贡献的每回合提醒，优先级 1000（`plan-mode.ts:126`）。提醒允许一切调查手段——读仓库、只读的 Shell 命令、外部检索、浏览器、技能、委派出去的研究任务，跑已有的构建和测试也算调查——但禁止创建、修改、删除、发布、发送任何东西，唯一例外是那个计划文件（`plan-mode.ts:184`）：

```text
The sole mutation exception is writing or editing the canonical Plan file at
exactly this path:
${canonicalPath}
No other file may be created or modified, and Git state must not change.

If a tool is denied, switch to another read or investigation path. When two
distinct approaches to the same question have been blocked, stop pursuing
that question: converge on the evidence already available and record the
remaining uncertainty in the Plan; do not keep looping.
```

提醒还规定计划文件的正文要以非空的 `## Summary` 开头，内容按需覆盖目标与范围、现状与受影响的文件、方案与端到端流程、状态与失败恢复、测试与验收、未决问题；只在对话里写计划不算数，必须先落盘再调用 `ExitPlanMode`。

硬的一层是工具守卫（`packages/local-runtime-v2/src/service/plan/tool-guard.ts:47`）：`write`、`edit`、`append`、`apply_patch`、`multiedit`、`notebook_edit` 一律拦下，只放行目标恰好是规范计划文件的 `write` 与 `edit`（`tool-guard.ts:5`）；Plan 模式里再调 `EnterPlanMode` 也会被拒（`tool-guard.ts:62`）。守卫和目标预算守卫组合后挂进回合执行器（`packages/local-runtime-v2/src/services.ts:973`）。注意这份清单里没有 `bash`：从代码看，“Shell 只许读”只靠提醒约束，再加上常规的权限审批（见[权限模式与规则](https://daiw.net/manual/minimax-code/permission)）。

两个生命周期工具按场合出现：`EnterPlanMode` 只在默认模式、且允许模型主动进入时注册，`ExitPlanMode` 只在 Plan 模式的回合里注册（`plan-mode.ts:124`）。二者都在结果里带上 `terminate: true`，结束当前回合等用户决定（`plan-mode.ts:256`）。默认模式的系统提示词里还会追加一段 `PLAN_MODE_GUIDANCE`（`plan-mode.ts:10`），教模型判断什么时候该申请 Plan：要交付实施方案的软件任务，或用户要求先审批再执行的计划；同样只在允许主动进入时才加（`plan-mode.ts:113`）。

## 计划怎样落地与退出

模型调用 `ExitPlanMode` 后，`exitFromAgent`（`packages/local-runtime-v2/src/service/plan/application.ts:256`）要求会话确实在 Plan 模式、队列已清空（`application.ts:270`），然后读取计划文件的冻结快照（超过 512 KiB 截断并注明，`application.ts:19`），连同路径一起放进一份 Plan 审批问卷。用户的选择由问卷处理器分派（`packages/local-runtime-v2/src/service/plan/questionnaire/questionnaire-action-handler.ts:73`）：

| 用户选择 | 结果 |
| --- | --- |
| 同意并开始实施 | `approvePlan`（`application.ts:375`）把一条实施消息插到队首，来源记为 `questionnaire`，用审批请求 ID 派生幂等键（`application.ts:471`）；会话切回默认模式 |
| 暂不处理 | 什么也不改，会话留在 Plan 模式 |
| 补充意见 | 意见作为回复送回，接着开一个续跑回合，仍在 Plan 模式里修订 |
| 问卷被撤销 | 回到默认模式，不实施 |

实施消息的开头是一段固定说明，后面紧跟审批时冻结的计划正文（`application.ts:20`）：

```ts
const APPROVED_PLAN_MARKDOWN_INSTRUCTION = `Implement the approved plan below.
Treat the Markdown after the "Approved plan:" line as the immutable plan snapshot for this implementation turn; it is authoritative.
Do not read the canonical Plan file and do not re-derive the plan from anywhere else.

If implementation reveals the plan is infeasible or materially wrong — a premise fails, a referenced component does not exist, or the work is fundamentally riskier than planned — stop and report the mismatch to the user instead of redesigning on your own.
Mechanical adjustments that keep the plan's decisions intact do not require stopping.

Approved plan:
`;
```

实施回合看到的是审批那一刻的快照，而不是磁盘上的文件，审批之后对文件的改动不会混进来。模型主动进入的路径与此对称：`EnterPlanMode` 打开一份进入确认问卷（`application.ts:208`），用户确认后 `confirmEntry` 准备草稿并切到 Plan 模式（`application.ts:316`），拒绝则留在默认模式，两种情况都会接着开一个续跑回合（`questionnaire-action-handler.ts:42`）。用户任何时候都能用 `Shift+Tab` 直接退出，这条路不触发实施。

## 目标：怎么用

`/goal`（`catalog.ts:154`）的用法写在帮助文本里（`packages/tui/src/application/thread-goal-command.ts:19`）：

| 命令 | 作用 |
| --- | --- |
| `/goal` | 查看当前目标及其状态 |
| `/goal <objective>` | 设定一个跨回合持续推进的目标 |
| `/goal <objective> budget=50K` | 同时设定 token 预算 |
| `/goal budget=50K`、`/goal budget=clear` | 修改或取消预算 |
| `/goal edit`、`/goal pause`、`/goal resume`、`/goal clear` | 编辑目标、暂停续跑、恢复、删除 |

一个会话同时只有一个目标。功能总开关是 `beta.threadGoal`：注释说外部正式版默认关闭（`config.ts:481`），但特性表把它的默认可见范围定为 `online`（`config.ts:641`），按 `resolveBetaFeature` 的规则，所有渠道都默认开启，以代码为准。`config.yaml` 里的 `goal` 一段（`packages/config/src/goal-config.ts:62`）可以调：`verification`（验证方式，没有默认值）、`budget.defaultTokens`、`budget.defaultMainTurns`、`budget.defaultActiveSeconds`（可选的全局上限）、`budget.graceSteps`（预算用尽后给几次“立刻总结”的提示，默认 1，最多 3）、`breaker.repeatedReplyLimit`（默认 3）、`verifier.repeatedNotMetLimit`（默认 5）等。

模型一侧有三个工具（`packages/agent-modules/goal/src/tool-defs.ts:22`、`tool-defs.ts:48`、`tool-defs.ts:153`）：`create_goal` 只在用户明确要求时使用；`get_goal` 读取当前目标；`update_goal` 有两种模式，`status` 提议 `complete` 或 `blocked`，`token_budget` 修改预算。后者只能出现在用户亲自发起的回合里，自动续跑、定时任务、子 Agent 调用都会被守卫拦下（`packages/local-runtime-v2/src/application/agent/goal-budget-tool-policy.ts:6`）。

## 目标的状态

```mermaid
stateDiagram-v2
  [*] --> active: 用户设定或 create_goal
  active --> complete: 验证通过或模型提议完成
  active --> blocked: 模型报告受阻
  active --> paused: 用户暂停、熔断或验证出错
  active --> budget_limited: 预算用尽
  active --> usage_limited: 配额或限流
  paused --> active: 用户恢复
  blocked --> active: 用户恢复
  usage_limited --> active: 用户恢复
  budget_limited --> active: 用户调高 token 预算
  complete --> [*]
```

六种状态定义在 `THREAD_GOAL_STATUSES`（`packages/agent-modules/goal/src/types.ts:40`），只有 `active` 会继续（`types.ts:177`）；`complete` 是终局，因 token 超限进入的 `budget_limited` 只能靠用户显式调高或取消 token 预算重新激活，回合数和时长超限则不行（`types.ts:222`）。每次转换都带一个原因，取自封闭的原因表（`types.ts:85`），例如 `complete(verifier_met)`、`paused(no_progress)`、`budget_limited(main_turn)`。用户在回合进行中按停止，活跃的目标会被记为 `paused(user_requested)`（`packages/local-runtime-v2/src/application/conversation/conversation-application.ts:356`）。

## 何时判定达成

模型觉得做完了，就调用 `update_goal`，以 `status` 模式提议 `complete`。工具只把提议记成回合内的信号，持久状态一个字都不改，并在结果里请求结束回合（`packages/agent-modules/goal/src/tool-impls.ts:216`、`tool-impls.ts:281`）。真正的裁决发生在回合结算：`GoalSettlement` 的十段决策流水线（`packages/local-runtime/src/thread-goal/settlement.ts:209`）依次做用量记账（token、回合数、活跃秒数，超预算即转 `budget_limited`）、陈旧检查（目标在回合期间被改过就作废）、异常分类（出错、被安全审查撤回、被中止）、预算收尾、依赖检查（有待答问卷、待批权限、进行中的后台任务或自动化归属冲突就先挂起）、验证、熔断、提交提议，最后安排续跑。

验证方式由 `resolveMode` 决定（`packages/local-runtime/src/thread-goal/verification-dispatch.ts:156`）：配置里写了 `goal.verification` 就用它，否则看这一回合实际走的模型路由（`packages/agent-modules/goal/src/verification/verification-policy.ts:18`）。注释给了理由：

> BYOK routes (`custom_provider`, `configured_provider`) spend the user's own quota, so we accept the worker's completion proposal instead of silently doubling their bill.

- `none`（自带 key 的路由）：直接接受提议，记为 `complete(worker_proposal)`（`verification-dispatch.ts:349`）。
- `subagent`（MiniMax 计费的 `managed_token_plan`、`minimax_api_key` 路由）：派一个只读的验证子 Agent，给它目标、证据摘要和模型的完成声明，要求回一行 `VERDICT:` 加 `PASS`、`FAIL` 或 `PARTIAL`（`packages/agent-modules/goal/src/verification/subagent.ts:187`）。`PASS` 记为 `complete(verifier_met)`；`FAIL` 附带缺口清单，目标保持 `active`，清单写进下一轮续跑提示（`packages/agent-modules/goal/src/continuation.ts:200`）；`PARTIAL` 与各种验证故障都转 `paused`。同一份缺口清单连续出现 5 次，目标以 `paused(no_progress)` 停下（`packages/local-runtime/src/thread-goal/store-verification.ts:32`）。验证子 Agent 花的 token 也计入目标预算。
- 另有一个 `evaluator` 后端（同路由上的小模型一次性评审），只能通过配置选用。

模型提议 `blocked` 则不经验证，直接记为 `blocked(worker_reported)`。

## 达成与否怎样影响回合继续

结算走到最后一段，`enqueueActive`（`packages/local-runtime/src/thread-goal/continuation.ts:283`）往会话队列里放一条宿主生成的续跑消息，正文包在 `<archon_internal_context source="goal">` 里（`packages/agent-modules/goal/src/internal-context-fragment.ts:46`）。完整的目标契约只在第一轮给一次（`DEFAULT_GOAL_CONTINUATION_TEMPLATE`，`goal/src/continuation.ts:12`），之后每轮只附一句短提示（`goal/src/continuation.ts:75`），让历史前缀保持稳定、便于缓存。挑哪种提示由 `selectPrompt` 决定（`thread-goal/continuation.ts:424`）：目标刚被编辑就换成“目标已更新”，被撤回后恢复时要求先 `get_goal` 核对，每满 5 轮做一次状态审计（`packages/local-runtime/src/thread-goal/reminder-policy.ts:3`），熔断计数不为零时附一段“不要原地踏步”的催促。

续跑消息出队前还要过 `finalRecheck`（`packages/local-runtime/src/thread-goal/gate.ts:96`）：目标变了就取消；预算已尽就转 `budget_limited`；有待答问卷、待批权限、未决的 Plan、进行中的后台任务或自动化归属冲突就等着；用户排了自己的消息，就取消这条续跑，让用户的消息先走（`gate.ts:138`）。

回合进行中，每次工具调用前都会查一次预算（`packages/local-runtime/src/thread-goal/admission-budget.ts:29`），由 `local-v1-goal-budget-guard` 扩展在 `before_tool_call` 上执行（`packages/local-runtime-v2/src/compat/v1/agent-host.ts:299`）。一旦用尽，这次调用被拦下，拒绝理由要求模型别再调工具、立刻给出简短总结；这样的提示最多给 `graceSteps` 次，之后的调用只会收到“工具已禁用”（`admission-budget.ts:79`）。目标因预算停下后，宿主再安排一个专门的收尾回合，由 `local-goal-budget-summary` 扩展注入提醒，要求模型总结已完成与未完成的部分、说明可以调高预算，并且本回合不许调工具（`packages/local-runtime-v2/src/application/agent/goal-budget-summary-reminder.ts:10`）。

最后一道保险是熔断（`packages/local-runtime/src/thread-goal/breaker.ts:73`）：最终回复与之前一模一样，或者整回合一次工具都没用，各自计数；第 2 次在续跑提示里催促，达到上限（默认 3）就以 `paused(no_progress)` 停下，如果重复就发生在一次完成声明之后，原因记为 `paused(no_progress_after_completion_claim)`（`packages/local-runtime/src/thread-goal/store-breaker.ts:20`）。用户亲自发一条消息会清零熔断计数，并负责把续跑重新接上。

`packages/local-runtime/src/thread-goal/orchestrator.ts` 里还有一个“跑完一轮读一次状态、`active` 就接着跑”的循环，注释说明它是旧路径，只观察持久状态，提议的结算归 v2 负责（`orchestrator.ts:36`）；公开代码里没有调用它的地方。

下一篇：[子 Agent 与后台任务](https://daiw.net/manual/minimax-code/subagents)——任务怎样派给子 Agent 或放到后台执行，结果又怎样回到父会话。
