> ## Documentation Index
> Fetch the complete documentation index at: https://docs.openkova.site/llms.txt
> Use this file to discover all available pages before exploring further.

# 目标模式

> goal 档：显式状态机、两条停机阀、过期护栏，以及为什么没有 token 预算。

目标模式（goal）是会话模式的第四档。切到这一档之后，你说的**第一句话就是目标**，
模型跨轮自治地把它做完，中途不需要你点「继续」，直到停机阀叫停为止。

```
agent  = baseTools + subagentTools + plan_enter
plan   = 只读子集 + plan_write + plan_exit
ask    = 纯只读子集（无 bash）+ ask_needs_work
goal   = baseTools + subagentTools + goal_complete + goal_blocked
```

`goal` 拿的是**完整工具集**（含 write/edit/bash），不含 plan 三件套。
理由很直接：目标模式的价值是「放手做完」，给它只读工具就退化成 plan 了。

## 目标是一个显式状态机

goal 不是「跑到底」的布尔标志。终态和暂停态必须可区分，
全部迁移收敛在 `transitionGoal` 里，非法迁移返回「没生效」而不是抛异常——
绝不让一次状态竞争毁掉整个回合。

```text theme={null}
(建目标) → active ──goal_complete──→ complete ◄──goal_blocked── blocked
              │  ↑
              ↓  │ resume
           paused ──────────────────┘
```

| 状态 | 含义 |
| - | - |
| `active` | 自治循环中。**只有这一档配上「有 run 在飞」才真的在跑** |
| `paused` | 停机阀触发 / 你输入接管 / 离开 goal 档 / 重启恢复。点「继续」回到 active |
| `blocked` | 模型主动报告死锁。`resumeGoal` 回到 active |
| `complete` | 终态，出口是空集 |

## 两条停机阀 + 你输入即接管

判定收敛在一个纯函数里，逐条短路，**顺序即优先级**：

<Steps>
  <Step title="provider 报错或被中止">
    立即暂停。
  </Step>

  <Step title="轮次上限">
    超过这条目标的 <code>maxAutoTurns</code>（默认 300，0 = 不限）就暂停。
    上限挂在**每条目标**上而不是全局设置——「补个 README」和
    「把整个鉴权重构完」差两个数量级，全局值注定对一半任务是错的。
  </Step>

  <Step title="无进展停滞">
    连续 3 轮零实质工具调用、且输出指纹不变 → 暂停。
    指纹是「本轮全部文本 → NFKC 归一 → 去空白 → sha256」，
    模型卡住时的典型表现正是把同一句话换几种标点再吐一遍。
  </Step>
</Steps>

**轮次只在结算时 +1**：上下文溢出时同一轮会用原文本重跑，
在注入时就计数会让轮次凭空多算。

**你输入即接管**：goal 档里有 active 目标时，你发的任何消息都会把它暂停
并重置安全轮次计数——你中途发消息通常是在纠偏，这条消息本身已经占掉一轮上下文，
模型拿到的是全新起点。

## 过期护栏（stale-turn guard）

两个出口工具 `goal_complete` / `goal_blocked` 的 `goal_id` 是**必填参数**，
执行时与当前目标比对，不一致就拒绝并把当前目标原文回给模型。

这不是防御性编程，是真实竞态：模型的 `goal_complete` 在工具调用排队期间，
你可能已经发了新消息或换了目标。没有 id 比对就会出现
「旧目标的完成把新目标标成完成」——而这个 bug 的症状是**静默的**，
日志里什么都看不出来。

## token 账是现累的，不是从转录重算的

`Goal.tokensUsed` 单调递增：每个轮边界把 run 上攒的增量折进来，只加不减。
早期实现是「会话累计 − 建目标时的基线」，有三个毛病，换掉的理由分别是：

<AccordionGroup>
  <Accordion title="看不见子代理">
    子代理是独立 Agent、独立上下文，用量从不写进父会话转录。
    而 goal 档的工具表带 Task 组，也就是鼓励委派——
    「目标靠子代理干完了大半活」时读数会严重少报。
    现在子代理自己记账，结算时汇给父 run，重复结算被状态守卫挡住。
  </Accordion>

  <Accordion title="把暂停期间的消耗算进来">
    基线只在建目标时取一次，resume 不重取——你在别的模式里干的活
    全被记在这条目标账上。现累的增量只在「目标是 active 且这一轮正在跑」时累加。
  </Accordion>

  <Accordion title="是 O(n²)">
    旧实现每个轮边界都全量重读转录文件，300 轮的目标就是 300 次全长重读，
    文件还在变长。现累是四个加法。
  </Accordion>
</AccordionGroup>

<Note>
  口径是四项相加（input + output + cacheRead + cacheWrite），与用量统计页同源。
  含 cacheRead 意味着长循环里这个数会比直觉大一个数量级——
  这是**全局口径**，不是目标模式特有的偏差，单改一处会让两个页面数字对不上。
</Note>

## 为什么没有 token 预算

曾经有第四道阀：目标可带 token 预算，超限转入收尾。它被**整体删除**了，
因为设计上就说不清：

* **你无法预判**。一个「把测试跑通」的目标可能花 20k 也可能 2M，
  预算填多少都是猜。填小了目标被腰斩，填大了等于没设。
* **它按累计量截断，不看进度**。同一目标第 4 轮和第 24 轮花掉的 token
  可能一样多，预算并不知道该在哪停。
* **收尾轮本身还要再烧一次模型请求**去写完成说明，而此时你已经看不到
  「还剩多少没做完」这个事实。

所以 `tokensUsed` 降级为纯展示字段（常驻条上一个计数，悬停注明
「仅供参考，不参与停机判定」）。想省配额的正确反应是让人看见消耗再自己决定，
而不是让程序在你没定的数字上自动刹车。

## 停下之后，提示词必须跟着改口

一个很隐蔽的坑：模式段 `GOAL_MODE_PROMPT` 是给 `active` 状态写的，
通篇是「别收手、别问、系统会自动续下一轮」，而它**不随状态变化**。

如果目标暂停后提示词不改口，模型会同时读到
「你正在自治循环里，别停下来问」和「目标已暂停」——
而后者如果不存在，模型就继续埋头干目标，而界面上明明写着「已暂停」。
你看到的是「上面说暂停了，对话还在进行中」。

修复前正是如此。现在 `goalPromptBlock` 是状态敏感的：

| 状态 | 注入什么 |
| - | - |
| `active` | 目标原文 + `goal_id` + 轮次 + 自治纪律段 |
| `paused` / `blocked` / `complete` | 目标原文 + 一段显式撤销：「模式段那套纪律此刻不适用」 |

这也是为什么「你输入即接管」这条阀看起来生效了（状态确实变了）却没真的接管：
**状态机停了，提示词没停**。

## 状态是 active ≠ 循环在跑

这是本模块最容易踩的一条，一次踩出过三个 bug
（「继续」按钮点了没用、切档后卡死、重启后谎报进行中），
共同症状都是「条上说在跑，实际什么都没跑」。

唯一的循环驱动源是：**有一个 run 正在跑，并且它走到了回合边界**。
所以把状态改成 active 不会启动任何东西；一个已停的目标不会被任何定时器唤醒。
凡是「让目标重新跑起来」的入口，都必须自己补起那一轮。

<Card title="下一步" icon="arrow-right" href="/features/subagents">
  目标模式鼓励委派——看看子代理是怎么设计的。
</Card>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.