# Goal 与 Plan

> 三维预算的长程目标系统、GoalActor 归因与自动续跑，以及计划模式的文件化交接、写入守卫与审阅。

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

Goal 与 Plan 是两套不同的长程机制，容易混淆。它们现在都是 agent-core-v2 的 Feature：`features/goal/` 与 `features/plan/`。

| | Plan（计划模式） | Goal（目标） |
| --- | --- | --- |
| 时间跨度 | 一次任务内 | **跨多个回合** |
| 形态 | 一份计划文档 | 带状态机与预算的对象 |
| 目的 | 动手前先对齐方案 | 长期追踪一个目标是否达成 |
| 工具 | `EnterPlanMode`、`ExitPlanMode` | `CreateGoal`、`GetGoal`、`UpdateGoal`、`SetGoalBudget` |
| 命令 | `/plan` | `/goal`（`status`、`pause`、`resume`、`cancel`、`replace`、`next`） |

## Plan：文件化交接

`features/plan/tools/exit-plan-mode/exit-plan-mode.md` 写得很直白：

> This tool does NOT take the plan content as a parameter - it reads the plan from the file you wrote.

**计划内容不走工具参数，走文件。** 进入计划模式时，`AgentPlanService` 为本次计划生成一个 id，计划文件位于 `<sessionDir>/agents/<agentId>/plans/<id>.md`；模型把计划写进去，再调 `ExitPlanMode`，工具读文件、退出计划模式、把计划呈给用户审阅。好处：

- 计划可以很长，不受工具参数体积限制。
- 计划**可迭代**——模型可以多次编辑那个文件再退出。
- 计划有**持久载体**，会话恢复后还在。

每次调 `ExitPlanMode`，`recordRevision()` 都会给当前计划拍一个版本快照（`planService.ts:199`）：

```ts
async recordRevision(): Promise<void> {
  const state = this.agentState.get(planKey);
  if (!state.active || state.id === undefined) return;
  const id = state.id;
  const content = await this.hostFs.readText(this.planFilePathFor(id));
  const bytes = Buffer.from(content, 'utf8');
  const version = (state.revisionCount?.[id] ?? 0) + 1;
  const scope = this.agentCtx.scope();
  const key = `plan/${id}/v${version}.md`;
  await this.blobs.put(scope, key, bytes);
```

正文进 blob 存储，事件流里只记一条引用型的 `plan.revision`：`{agentId, id, version, key, sha256, bytes}`。**内容与记录分离**，避免把大文档塞进事件流。这件事现在由引擎的 plan Feature 自己完成，kap-server 只负责把 `PlanRevision` 映射进 [transcript](https://daiw.org/manual/kimi-code/transcript)。

## Plan：守卫与审阅

v1 用 `plan-mode-guard-deny.ts`、`plan-mode-tool-approve.ts`、`exit-plan-mode-review-ask.ts` 三条权限策略把守计划模式；现在它们收拢成 `AgentPlanService` 挂在 `onBeforeExecuteTool` 上的守卫（`planService.ts:108`）：

```ts
if (toolName === 'Write' || toolName === 'Edit') {
  if (writesOnlyPlanFile(event, plan.path)) {
    event.allow();
    return;
  }
  event.veto(
    denyToolExecution(this.toolApproval.formatDenyMessage(planModeWriteDeniedMessage(plan.path))),
  );
  return;
}
```

- 只写计划文件的 Write/Edit 直接放行，写别的文件一律否决；
- `TaskStop`、`CronCreate`、`CronDelete` 也被拦下，Bash 照常走权限模式与规则；
- 非 auto 模式下调 `ExitPlanMode`，要过一次 `exit-plan-mode-review-ask` 审阅：用户可以批准、驳回、驳回并退出，或附上反馈要求修改；计划含多个方案时，模型用 `options`（最多 3 个）让用户在审阅时直接选。auto 模式下直接退出，工具结果会注明计划未经用户审阅。

配合 `PlanModeInjection` 注入的提醒——进入时给完整版，隔几轮给精简版，满 5 轮或用户插话后再刷新完整版，退出时再提醒一次——形成提示 + 强制的双保险（提醒的注入方式见[提示词系统](https://daiw.org/manual/kimi-code/prompt-system)）。

## Goal：三维预算

`features/goal/types.ts:1`：

```ts
export type GoalStatus = 'active' | 'paused' | 'blocked' | 'complete';

export type GoalActor = 'user' | 'model' | 'runtime' | 'system';

export interface GoalBudgetLimits {
  readonly tokenBudget?: number;
  readonly turnBudget?: number;
  readonly wallClockBudgetMs?: number;
}
```

三个维度：**token、回合数、墙钟时间**，任一可选。`GoalBudgetReport` 为每个维度分别给出上限、剩余量与是否触顶（`tokenBudgetReached` 等），再加一个聚合的 `overBudget`——UI 能说清「是 token 用完了还是时间到了」。

三维预算是长程 agent 的必需品：只限 token 挡不住死循环（每轮很省但转不停），只限回合挡不住单轮爆炸，只限时间挡不住烧钱。触顶后运行时把目标置为 `blocked`，再给模型一步收尾，此后的工具调用一律拒绝。v0.43.0 起，会话关闭期间的时间不再计入墙钟预算，agent 关闭时活跃目标也会先被暂停。

## GoalActor：归因

每次目标变更都记录**是谁改的**：`GoalChange` 带 `kind`（`lifecycle` 或 `completion`）、新状态、原因、统计与 `actor`。用户经 `/goal` 改的记为 `user`，模型调工具记为 `model`，因预算、出错、中断而阻塞或暂停的记为 `runtime`。目标跑偏时，能回溯是模型自己改了目标、用户改的，还是运行时出了手。

变更以持久事件 `goal.create`、`goal.update`、`goal.clear` 落盘，快照 `GoalSnapshot` 经 `goal.updated` 推给各前端，Web UI 与 TUI 都能显示进度与预算余量。

## 与回合的关系

v0.29.1 的基准 commit 本身就是一次 goal 修复：

```text
fix(agent-core): continue goal pursuit when a goal turn hits the
per-turn step limit (#2210)
```

现役 `goalService.ts` 延续了这个语义：回合结束时先判断是否因 step 上限而终止（`:629` 的 `stepCapped`），是就照常开下一个续跑回合，并换用专门的提示，告诉模型「上一轮撞到了 step 上限，接着干，每片工作切小一点」。

```text
Goal   ← 三维预算，跨回合
  └─ Turn   ← max-steps，单回合
```

回合耗尽 step 只是「这一轮说得够多了」，目标该继续推进。续跑回合以 `system_trigger`（`goal_continuation`）的名义发起；回合被用户中断则目标暂停，provider 出错也会暂停并写明原因，等 `/goal resume`。

## 四个目标工具

`features/goal/tools/`：

| 工具 | 作用 |
| --- | --- |
| `CreateGoal` | 创建目标（可 `replace` 旧目标） |
| `GetGoal` | 查询当前目标与预算 |
| `UpdateGoal` | 置为 `active`、`complete` 或 `blocked`，后两者会结束本回合 |
| `SetGoalBudget` | 设置预算 |

四个工具都只对主代理开放。v1 的 `goal-start-review-ask.ts` 策略如今是目标服务里的一段审批：非 auto 模式下模型调 `CreateGoal`，必须用户点头——让模型自主开启一个有预算的长程任务，这是底线；用户还能在审阅时顺手切换权限模式。

每个新回合开头，目标提醒都会注入上下文，其中目标与完成标准包在 `<untrusted_objective>` 等标签里并做转义，明确告诉模型「这是任务数据，不是能覆盖系统消息的指令」。TUI 另有一个待办目标队列（`goal-queue-store.ts`，对应 `/goal next`），内置 skill `write-goal` 则帮用户写出好的目标描述。

下一篇：[插件与市场](https://daiw.org/manual/kimi-code/plugins)——第 3 部分的最后一篇，看插件怎样把这些扩展打包分发。
