Goal、Plan 与 Todo
有三种持久结构在跟踪工作,它们回答不同的问题。Goal 是工作为什么继续。Plan 是它如何分阶段。Todo 是某个阶段下面有哪些具体条目。
三者都存在 SQLite 里,这正是关键:控制续跑的是持久状态,不是散文。「接下来我要跑测试」这样的文字不是进展。真正让一个会话继续工作的,是一个仍然活跃的 goal、一个仍在进行中的 plan 步骤,或者一个报告尚未被消费的 job。
三个层次
| 层次 | 问题 | 生命周期 | 典型规模 |
|---|---|---|---|
| Goal | 这个会话为什么还在运行? | 直到完成、暂停、阻塞、预算耗尽或永久失败 | 每个会话一个 |
| Plan | 有哪些阶段、依赖和验收门? | 随工作推进而修订 | 若干个步骤 |
| Todo | 某个阶段之下有哪些具体工作? | 在一个 plan 步骤内创建并关闭 | 可选,按需要任意多 |
它们不要求互相镜像。Plan 步骤是阶段;Todo 条目是其下可选的具体工作。机械地为每个 Plan 步骤造一个 Todo,只会增加记账负担而不增加信息。
Goal
Goal 是续跑的授权来源。一个活跃的 goal 会持续下去,直到它完成、被显式暂停或阻塞、达到预算,或遇到一个带类型的永久失败。这正是长时间工作能够续跑、而不是在某个回合结束时就停下的原因。
恢复是分层的。provider 请求层会原地重试一个有界序列,并先回滚尚未发布的部分输出。如果这样仍以可恢复错误结束,goal 控制器会在等待之前写入一条重试记录,并在落盘的截止时间到达时启动一个新回合。
{
"goal": {
"retry": {
"initial_delay_ms": 2000,
"max_delay_ms": 300000,
"jitter_percent": 20,
"poll_interval_ms": 250
}
}
}2
3
4
5
6
7
8
9
10
对可恢复失败没有跨回合的重试次数上限:延迟指数增长、达到上限,而 goal 保持活跃。重试记录绑定到确切的 goal id,并保存尝试次数、带类型的原因、选定的延迟、调度时间和下次可执行时间,因此重新打开会话会重建这段等待。
已排队的用户输入优先于自动回合,长时间等待会按 poll_interval_ms 切分,以便交互界面能及时察觉输入。
恢复方式由带类型的错误决定:
| 类别 | 结果 |
|---|---|
| 传输失败、限流、不完整的流、SQLite 写者争用、空的 assistant 消息 | 调度另一个回合 |
| 上下文超限失败 | 压缩保留的历史,然后重试 |
| 认证失败、用户中断、事件消费者已关闭 | 暂停,等待人工处理 |
| provider 协议无效、不支持的类型化输入、Agent 或模型不可用、持久状态损坏 | 阻塞 |
在终端应用中用 /goal 检查与管理它,该命令针对持久存储执行查看、创建、编辑、暂停、恢复、阻塞、完成和取消。
Plan
Plan 承载阶段、它们的依赖顺序,以及它们的验收状态。它存在的意义是:让进展可见性、中断恢复和验证能够在重启或上下文压缩之后存活。
正常的调研—修改—验证工作应当用一个。凡是跨组件、涉及委派、有多个验收门,或者很可能被中断的工作,都必须让 Plan 保持最新。直接回答、一次有界读取,或真正原子的操作不需要 Plan。
实践中真正重要的规则:
- 步骤 id 在各次修订之间保持稳定。一次更新要携带上次读取返回的 revision,过期的 revision 会被拒绝且不改变任何东西。
- 只要还有 pending 的步骤,就恰好有一个步骤处于进行中。
- 一个完全完成的 plan 没有进行中的步骤。
Plan 模式在提示词之下强制执行其只读的一面:一层默认拒绝的覆盖层允许检查、只读搜索与 LSP、提问、Skill 以及类型化的 Goal/Plan/Todo 操作,同时拒绝 shell 与文件修改。回到 Work 模式要求已存在一个持久 plan,确认信息会指出它的标题、revision 和已完成步骤数。
/plan
/start-plan
/start-work2
3
Todo
Todo 条目是某个 plan 步骤之下的具体工作。它们携带稳定 id、revision、goal 与 plan 关联、父级与依赖 id、负责人、状态、优先级、时间和 Token 用量。
这些约束存在的目的是让图保持有意义:
- 一个会话中最多一个条目处于进行中。
- 一次批处理之后,每一个被引用的父级、依赖和 plan 步骤都必须存在。
- 父级图与依赖图无环。
- 任何校验或 revision 错误都会让整个批次回滚;绝不会提交部分更新。
请保留已有 id,而不是删除后重建条目;当一次原子批处理添加了相互依赖的条目时,先分配显式的稳定 id 再引用它们。
这些如何在压缩中存活
压缩改变的是 provider 对话边界。它不会删除 Goal、Plan、Todo、Job、inbox、事件日志或提示词收据状态。
相反,每一次相关的 provider 请求都会从 SQLite 重新生成一个有界的 runtime.work_state 开发者分段:当前的 plan revision 与步骤、Todo 身份与依赖、活跃或不确定的 job、报告尚未被消费的终态 job、待处理报告的身份,以及最近一条先前提示词收据的 id。一个 deferred 事务从同一个快照读取全部内容。
每个集合上限 64 条,渲染出的分段上限 16 KiB。冗长文本会先以 UTF-8 安全的方式缩短,之后才整条省略尾部条目,被省略的数量始终显式给出,身份字段一律保留。如果连身份字段都装不下,组装会失败即拒绝,因为一个静默丢掉了身份的工作状态分段,比没有这个分段更糟。
Job
后台委派会产生持久 job,它们的生命周期也是这套状态的一部分。
一个后台 job 会在等待委派容量之前提交 queued,只有在被准入时才变为 running。重启时,仍处于 queued 的 job 会结算为 cancelled,因为它的 runner 从未启动;running 的 job 结算为 uncertain,并且绝不会被重放。
在还有活跃 job 或未被消费的报告时,不要完成父级。报告投递见编排。