Plan、Goal 与 Todo
三种看似相近但生命周期不同的协作原语
结论:Plan、Goal 与 Todo 不是同一种“计划状态”的三个名字
DeepSeek Harness 把三种容易混淆的协作原语放在不同生命周期里:Plan mode 改变下一次模型请求的系统指引,并把已生效的布尔状态写入 Session;Goal 保存一个可修订、可暂停、可恢复的长期完成目标,并用独立的进程内 activation 决定是否自动续跑;Todo 则是模型在当前工作中反复整表替换的轻量清单,耐久事件保留历史,但面向 UI 的“standing plan”会在下一 Turn 开始时清空。
因此,“进入 Plan”不会创建 Goal,“创建 Goal”不会自动生成 Todo,Todo 中出现多个 in_progress 也不代表 Todo tool 自己并发执行。三者可以同时存在,但没有任何一者是另外两者的隐藏数据库或调度器。
Plan 回答“下一请求应以什么协作策略思考”;Goal 回答“跨多少个自动轮次仍要完成什么”;Todo 回答“模型此刻声称哪些具体步骤待办、在做、已完成”。PLANSTATE-PLAN-STATEPLANSTATE-GOAL-TYPESPLANSTATE-TODO-TOOL
1. 三套状态机的权威事实、有效期与执行者
| 原语 | 权威事实 | 非耐久状态 | 谁推进 | 何时结束/隐藏 |
|---|---|---|---|---|
| Plan | 最后一条 plan/mode { active } | 等待 Step boundary 的 pending intent | 人类 /plan、模型经批准的 exit_plan_mode | 显式退出;不会随 Turn 自动结束 |
| Goal | goal/change 全量 snapshot 或 clear tombstone,加已接纳 goal-round message | armed / disarmed activation 与 driver reservation | GoalService mutation;可选 round driver | complete、blocked、paused 或 clear;active 也可能仅 disarm |
| Todo | 每次 todo/write 的完整清单 snapshot | 没有独立调度状态 | 调用 agent 的 todo_write | 事件永远留在日志;UI projection 在下一 turn/start 变为 null |
三者都借 SessionEvent 获得重放能力,但“事件耐久”不等于“同样长期可见”。Plan 与 Goal 的当前状态跨 Turn 折叠;Todo 的事件也耐久,产品 projection 却有意把它解释成只站立到下一 Turn 的清单。
2. Plan 的双状态:已记录模式与待生效选择
plan/mode 是 log-only、非 Surface、last-write-wins 的布尔事件;没有记录时为 false。已提交值可以随 Session resume、Fork 与完整日志重放恢复。Controller 另有一个按 Session 键控的 WeakMap,保存尚未到提交边界的目标模式及是否需要给模型叙述切换。PLANSTATE-PLAN-STATE
空闲选择
没有 open Turn 时立即 append plan/mode,因为再等下去不会有轮内 pre-step。
运行中选择
只放入 pending intent;系统提示词组装已读取这个目标值。
接受边界
pre-step listener 先等待下游决定,只有非 reject、未取消的候选 Step 才 append。
切换叙述
仅当最近 request header 告诉模型的是另一模式,才追加一条 plugin user notice。
边界 append 失败会 warn、允许当前 Step 继续,并保留 pending intent 供以后边界重试;same-Step Provider recovery 复用冻结的 assembly,不消费这个选择。真实 loop 测试要求模式只在后续 Step 生效,工具 schema 在切换前后保持相同。PLANSTATE-PLAN-BOUNDARYPLANSTATE-PLAN-LOOP-TEST
3. Plan 改的是提示词,不是权限或工具目录
Active 时,plan:policy 在 system prompt order 50 渲染部署提供的 section;inactive 时贡献空文本。发布配置要求先探索、只做非变更检查、提交完整可执行计划,并明确禁止在 Plan 阶段用 Todo 代替最终计划。PLANSTATE-PLAN-ASSEMBLYPLANSTATE-SHIPPED-PLAN-POLICY
exit_plan_mode 在两种模式都注册,其他工具也不因 Plan 而被过滤;集成测试甚至让一个写工具在 Plan 中成功执行。实际限制取决于模型服从该 section,以及独立的 sandbox、approval 和 tool policy。Plan mode 本身不读写这些权限状态。PLANSTATE-PLAN-LOOP-TEST
这个选择维持了 tool schema 与 Code Mode SDK 的前缀稳定性,但也意味着应用不能把“Plan chip 亮着”解释成“写操作在执行层必然被拒绝”。若部署需要硬只读,必须单独配置执行权限。
4. Plan 的三条控制面:命令进入、工具评审、服务直调
| 入口 | 行为 | 是否进模型 history | 授权/失败边界 |
|---|---|---|---|
/plan | 选择 active | 命令文本不进 Surface | 经通用 command plane;记录 run/done |
/plan <message> | 先选择 active,再 agent.steer() 普通 user message | 只有 message 后缀进入模型 history | 模式选择先发生;steer 后续失败不会回滚已选模式 |
/plan off | 退出,或取消尚未提交的进入 | 不增加模型输入;必要时另有 switch notice | 精确小写 off 才是控制词 |
exit_plan_mode(plan) | 把完整 Markdown 计划交给人类评审 | call/result 都是普通工具历史 | 必须已提交为 active,且评审必须明确批准 |
ctx.planMode.set() | 受信插件直接选择目标模式 | 只通过后续 policy/notice 影响模型 | 这是进程内 service,不是公开用户协议 |
退出工具要求 trim 后以一级 # 标题开头。只有唯一一条 plan-review answer、唯一选择 Approve、且没有 custom text 才算同意;“Keep planning”、自定义反馈、重复或缺失 answer 都以失败 tool result 返回,让模型继续修订。批准只写一个 silent pending exit,当前 assistant tool batch 仍处于 Plan,下一 accepted pre-step 才切换。PLANSTATE-PLAN-CONTROLS
User-question service 要求 supplied Agent 是 registry 中精确 live instance 且为当前 runtime root;被另一个 live Agent 拥有的 child 不能打开评审。持久 Fork lineage 本身不永久降权:以后独立恢复为 root 的 Session 可以评审。PLANSTATE-PLAN-REVIEW-AUTH
5. Plan 的 Web、projection 与崩溃恢复并不完全等价
plan projection 同时折叠 command/run(name="plan") 和 plan/mode:前者推导 wanted target,后者提交 active 并清空 wanted。因此它能从冷日志恢复 { active, pending },并通过 history baseline 与 session/projection frame 分发给多个浏览器。PLANSTATE-PLAN-PROJECTION
Web 只在“有效目标”为 Plan 时显示 Plan × chip;点击通过 command Remote 执行 /plan off。同一 projection 还切换输入框 placeholder。Plan review 则由专用 decision panel 呈现 Approve、Keep planning 和讨论/dismiss;不认识 intent 的 provider 仍可退化成通用 options。PLANSTATE-PLAN-UIPLANSTATE-PLAN-REVIEW-UI
没有独立的 plan.get/plan.set 业务 API 家族:Web 使用通用 command Remote 加 projection;模型使用稳定的 exit tool;host 内插件使用 service。
6. Goal 是一个带 compare-and-set revision 的完整状态机
Goal identity 在多次修订间稳定,GoalRef { id, revision } 精确标识一个版本。Snapshot 每次都带 objective、phase、maxGoalRounds;blocked phase 额外且仅额外带 { code, message }。四个 phase 是 active、paused、blocked、complete。PLANSTATE-GOAL-TYPES
create → active(r1)
active → edit → active(r+1)
active → pause → paused(r+1)
active → block → blocked(r+1)
active|paused|blocked → complete(r+1)
active|paused|blocked --resume, capacity remains→ active(r+1)
any current → clear tombstone(r+1)
complete → create a fresh id at r1
create/edit/pause/resume/complete/block 和 clear 都推进 revision;一条被接纳的正数 goal-round user/message 只把 roundsStarted 增一,不推进 revision。下一次 goal mutation 会把当前 round count 复制进新的全量 snapshot。严格 fold 会拒绝跳号 revision、非法 phase 转换、复用旧 goal id、倒退 timestamp 和非顺序 round。PLANSTATE-GOAL-FOLD
7. Durable phase 与 process-local activation 是两条轴
| 组合 | 含义 | 是否会自动续跑 |
|---|---|---|
active + armed | 目标可继续,当前进程持有自动 authority | 可选 driver 会在 idle 后调度 |
active + disarmed | 耐久目标仍未完成,但自动 authority 已撤销 | 不会,需显式 resume |
paused/blocked/complete + disarmed | 耐久生命周期已停止 | 不会 |
Activation 从不写入日志。新 cache、每次 agent/session-start、driver 热加载与不确定失败都会 disarm;resume 则写一个新的 revision 并 arm。于是 resume、Fork 和进程重启可以恢复 objective、phase、revision、round count,却不会偷偷恢复自动执行 authority。PLANSTATE-GOAL-SERVICEPLANSTATE-GOAL-COMMIT
GoalService 还要求传入的是 AgentRegistry 中精确的 live object,而不仅是相同 id;create 之后的 edit、pause、resume、complete、block 与 clear 都用当前 GoalRef 做 CAS。多标签页、模型工具和人类命令同时修改时,先提交者推进 revision,后来的旧 ref 明确失败,而不是覆盖新状态。
8. Goal continuation driver:同一 Session、一个 reservation、逐轮 durability barrier
goal/change arms or revises goal
→ driver coalesces wakeups per exact Agent
→ wait until Agent idle and no competing queued human work
→ flush pending durable prefix
→ reserve {goalId, revision, roundsStarted + 1}
→ Agent.followup(<goal_round>...)
→ pre-step validates full message + live revision before and after downstream hooks
→ admitted user/message increments roundsStarted
→ whole Agent returns idle
→ flush the settled round before reserving the next one
每个 Agent 的 driver state 只允许一个 queued/claimed/admitted attempt。Prompt 把 JSON-quoted objective、round/maxGoalRounds 和“依据当前 workspace、工具结果、耐久状态验证进展”的指令写成普通 user message;它不创建新 Agent,也不复制 Session。PLANSTATE-GOAL-DRIVER
普通人类工作在 reservation 之前或与它进入同一 batch 时,driver 标记竞争并让自动 round 让路。Claim 后 objective/revision 改变,pre-step 会把旧 round 判 stale、保留其它 claimed messages,并在新状态稳定后重排,而不会消费 round number。PLANSTATE-GOAL-DRIVER
Round cap 是 admitted-round 数量上限,不是 token、费用或时间预算。到达上限时 driver 写 durable blocked,code 为 round-limit;测试源码要求恰好接纳 1..N 且不多发一轮。PLANSTATE-GOAL-DRIVER-TEST
9. Goal 工具的 authority 比 GoalService 本身更严格
| 操作 | 模型工具 authority | 额外规则 |
|---|---|---|
get_goal | 精确 live、running、当前 initiator、open Turn | 不要求 human message |
create_goal | runtime root 当前 Turn 中有 host-attested user message | 模型判断任务是否足够长期 |
edit / pause / resume | 同样要求 direct-human root Turn | 必须先读并复制 exact id/revision |
complete / blocked | direct human,或当前 Goal 的精确 admitted round | 自动 blocked 还要达到配置的 round 下限 |
Tool authority 通过 open Turn 内实际提交的 user/message.source 判定,而不是相信模型参数。Goal-round 必须匹配当前 id、revision 与 roundsStarted;自动 round 无权 edit、pause 或 resume。PLANSTATE-GOAL-AUTHORITYPLANSTATE-GOAL-TOOLS
自动 round 成功标记 complete/blocked 后,当前实现不会调用 concludeTurn()。它用 deferContext() 增加一条 <goal_complete> 或 <goal_blocked> closing instruction,让模型再写一次面向用户、不得继续调用工具的收尾消息;direct-human mutation 不增加这条上下文。测试明确断言 concludesTurn 为 undefined。PLANSTATE-GOAL-TOOLSPLANSTATE-GOAL-WRAPUP-TEST
10. Goal 的命令、Remote/API 与 Web UI 是三种不同能力面
| 表面 | 提供的操作 | 读状态方式 | 不提供 |
|---|---|---|---|
/goal command | show、create、edit、pause、resume、clear | 直接调用 GoalService,输出文本 | complete、block、round-cap 参数 |
| Goal Remote/API | create、edit、pause、resume、complete、clear | 当前值走 goal projection,mutation 只 ack ref/cleared | get 与 block |
| Web GoalBar | edit、active→pause、paused→resume、clear | useProjection("goal") | create、complete;blocked resume 按钮;activation |
| 模型 tools | get、create、五种 update action | get_goal 返回 live activation | clear |
/goal 在 command plane 内直接执行,不开启模型 Turn;Web 还把耐久 command/run 投影成右对齐的 command-input bubble。GoalBar 每次点击都从最新 projection 读取 CAS ref,并用同步 ref + React pending 做 single-flight;clear 成功后先本地隐藏该 id,等待 null projection 收敛。PLANSTATE-GOAL-COMMANDPLANSTATE-GOAL-COMMAND-UIPLANSTATE-GOAL-COMMAND-VIEWPLANSTATE-GOAL-UIPLANSTATE-GOAL-UI-REMOTE
Host mutation façade 会复用 live Agent 或冷恢复普通 Session,并拒绝仍属于 subagent routing 的 identity。它不复用模型工具的“direct human Turn”规则——这是受信控制面的直接 domain mutation。GoalError 当前统一映射为 wire internal,稳定 domain code 放在 details.goalCode;客户端必须看 details 才能区分 stale revision 等业务拒绝。PLANSTATE-GOAL-APIPLANSTATE-GOAL-API-ROUTINGPLANSTATE-GOAL-API-ERRORS
11. Goal 的失败、取消与并发恢复策略
| 条件 | 状态后果 | 是否自动重试 |
|---|---|---|
| Goal-change 或上一 round 的 durability checkpoint 失败 | 保留当前 Session 日志中的 phase,activation disarm | 否,需人类 resume |
Agent.followup 入队失败 | 若 revision 仍精确,block 为 queue-failed | 否 |
| pre-step 下游拒绝合法 reservation | block 为 prompt-rejected;未接纳不消耗 round | 否 |
| Provider/Agent error 或 max tokens | active 可保持,但 activation disarm | driver 不做异常 auto-retry |
| 取消属于 queued/claimed/admitted goal attempt | idle checkpoint 尝试 durable pause;失败则 disarm | 否 |
| 取消只属于普通人类工作 | 不伪造 goal phase,最多 disarm | 否 |
| 插件 teardown | 关闭 admission、disarm,取消 active attempt 并等待 quiescence | 不得再开新 round |
Driver 的“串行”由每 Agent 的一个 attempt 和 coalesced run promise 保证;Goal mutation 的“并发”由 revision CAS 保证。它们解决的是不同问题。外部 LLM retry plugin 若在同一 Step 内成功恢复,原 reservation 仍可结算;driver 自己不会把未分类的异常重新排成新 round。PLANSTATE-GOAL-DRIVERPLANSTATE-GOAL-DURABILITY-TEST
12. Todo 是 whole-value replacement,不是任务数据库
todo_write({ todos }) 每次必须发送完整清单;成功后同步 append 一条 todo/write,当前值就是最后一条 snapshot。Item 只有规范化后的非空 content 与三态 status:pending、in_progress、completed。没有 id、priority、dependency、nesting、patch、delete-one 或 read-back operation。空数组是合法的显式空清单。PLANSTATE-TODO-TOOL
Tool schema 拒绝未知 item keys 和错误 enum;execute 再 trim content、拒绝空白与重复,并按部署策略检查 active count。失败发生在 append 之前,不生成 todo/write;非 Agent caller 因没有 owning Session 被拒绝。PLANSTATE-TODO-TOOLPLANSTATE-TODO-TEST
allowParallelInProgress 是必选部署决策。true 同时改变模型说明和验证,允许多个 active item;false 拒绝超过一个。发布的 base profile 选择 true。耐久 invariant 故意不按当前配置重验 active count,否则用宽松策略写出的旧日志会在以后收紧部署时无法重放。PLANSTATE-SHIPPED-TODO-POLICYPLANSTATE-TODO-INVARIANT
13. Todo 事件长期存在,standing projection 只活到下一 Turn
Turn N: todo_write(snapshot A)
todo_write(snapshot B) → projection = B
turn/end → projection still B
Turn N+1: turn/start → projection = null
... old todo/write events remain in the Session log
todos projection 在首次 write 前为 null,遇到 write 整值替换,遇到每个 turn/start 清空;它不在 turn/end 清空,因此完成清单在 Agent idle 后仍能显示,直到下一条人类或自动 goal round 真正开启新 Turn。冷读、resume 和 Fork 都按完整日志重做同一 fold。PLANSTATE-TODO-PROJECTION
Web TodoPanel 只读这个 projection:非空时显示折叠卡片、按状态计数并可展开所有项;另一个 todo_write tool row 从耐久 call arguments 生成 “done/total + 首个 active + 额外 active 数”摘要。前者显示被产品认定仍站立的清单,后者是历史工具调用,两者寿命不同。PLANSTATE-TODO-UIPLANSTATE-TODO-ROW
todo/write 本身是 log-only,不是第二条模型消息。模型看到的是自己的完整 tool-call arguments 和简短 result;后续整表替换只更新 Todo projection,不会移除旧 tool history,旧调用会继续占 Surface,直到 compaction 等机制遮蔽。没有 context plugin 把最新 Todo snapshot 自动重新注入未来请求。
14. “Todo 允许并行”与“Todo tool 并行执行”恰好相反
ToolRuntime 只有定义显式提供 isConcurrencySafe(args) 且返回严格 true 时才把调用归入 parallel group;省略、异常、隐藏或无效定义一律 exclusive。Plan exit、三个 Goal tool 与 todo_write 都没有 opt-in,因此都是 ordering barrier。PLANSTATE-TOOL-EXCLUSIVE
所以两个连续 todo_write 在同一模型 batch 中按顺序执行,第二个稳定覆盖第一个;多个 in_progress 只表示 Agent 可能通过 subagent、后台命令或 workflow 同时推进多项工作。它不是对 Todo snapshot 的并发写许可,更不是共享 swarm task registry。PLANSTATE-TOOL-SCHEDULER
正常生产写入口只有同一 Agent 的 exclusive tool pipeline,whole-value last-wins 已给出确定顺序;Goal 则同时面对命令、Remote、模型工具和调度器,必须用 revision CAS。两者的并发模型不同,不应机械统一。
15. 事件、Surface、projection 与重放矩阵
| 事实 | Session log | 直接进入 model Surface | 浏览器当前视图 | Resume/Fork |
|---|---|---|---|---|
plan/mode | 是 | 否;通过 system section 与可选 notice 生效 | plan {active,pending} | 已提交 active 恢复;Controller pending 不恢复 |
goal/change | 是 | 否;tool result 或 round prompt 才可见 | goal whole snapshot | durable state 恢复,activation 一律 disarm |
goal-round user/message | 是 | 是 | projection 目前忽略它 | strict fold 恢复 round count |
todo/write | 是 | 否;call/result 可见 | todos standing list | 按最后 write 与后续 turn/start 重折叠 |
command/run/done | 是 | 否 | Plan pending、Goal command bubble 与通用 command row | 纯日志重建 |
Compaction 改写的是 model Surface,不会删除这些 log-only domain facts。于是 Plan active 与 Goal lifecycle 不依赖摘要是否记住它们;Todo projection 也可从完整日志恢复,但它的下一-Turn清空规则仍然生效。相反,goal-round prompt、tool calls 和 results 是 Surface 内容,未来可能只通过 checkpoint 向模型保留其语义。
16. 三处已确认的文档漂移
| 文档表述 | 当前生产实现 | 判定 |
|---|---|---|
tool-goal README 称自动 complete/blocked 会 concludeTurn() | 源码用 deferContext() 请求最后一条 closing message;测试断言不 conclude | README 过时 |
ui-goal README 称 Remote mutation 写 agent/inbox/spliced 并排队 goal context | GoalService 直接 append log-only goal/change,不注入模型 context | README 仍描述旧架构 |
| command-goal README 称没有 continuous status widget | Web 已有 projection-driven GoalBar,提供 edit/pause/resume/clear | README 限制已失效 |
这些结论来自固定 commit 上的生产源码与测试,不是仅凭文档措辞推测。Plan 与 Todo 的核心 README 在本章核对范围内基本匹配当前控制流;Goal 的跨包演进更快,出现了明显同步滞后。PLANSTATE-DOC-GOAL-TOOLPLANSTATE-DOC-GOAL-UIPLANSTATE-DOC-GOAL-COMMAND
17. 已确认的限制、缺口与有意取舍
| 发现 | 分类 | 实际影响 |
|---|---|---|
| Plan 只提供 guidance | 明确设计 | 必须由独立 sandbox/approval 执行硬限制 |
| Plan durable pending view 与 process intent 可在 crash 后分离 | 恢复限制 | UI 可能显示目标模式,实际 prompt 仍按旧 committed state;需重新选择 |
| 只有 Web 有专门 plan-review 与 Plan chip | 呈现边界 | 其它 provider 使用 generic questions/commands |
| Goal projection 不处理 admitted goal-round message | 已确认 projection gap | roundsStarted 可能停在最近 mutation,直到下一 goal/change;service read 仍准确 |
| Goal projection 不含 activation | 明确设计 | GoalBar 无法区分 active-armed 与 active-disarmed,恢复后可能给出 pause 而非 resume affordance |
| GoalBar 不给 blocked goal 提供 resume 按钮 | UI 功能缺口 | 需用 /goal resume 或其它 Remote/tool 路径 |
| Goal 没有独立 evaluator 或资源预算 | 明确范围 | 完成与同一 blocker 的语义由模型判断,cap 只数 rounds |
| Todo projection 跨 Turn 主动清空 | 产品语义 | 它不是长期任务板;历史 event 仍在 |
| Todo 没有 read、patch、CAS 或共享 scope | 明确范围 | 模型必须整表重发;只属于一个 calling Agent Session |
| Todo invariant 不拒绝额外 item keys | invariant coverage gap | 工具 schema 会拒绝,但受信插件直接伪造的 extra fields 不会被该 companion 检出 |
GoalService 的严格 cache fold 会读取 goal-sourced user/message 并即时增加 roundsStarted;轻量 goal projection 的实现只在 goal/change 时替换 whole value,测试也固定了非 goal-change 事件的 same-reference fast path。因此这是浏览器 projection 新鲜度问题,不是 domain 丢失 round。PLANSTATE-GOAL-FOLDPLANSTATE-GOAL-PROJECTIONPLANSTATE-GOAL-PROJECTION-TEST
Todo tool 的 JSON schema 明确 additionalProperties: false;companion 只读取 content/status 并检查规范化、唯一性与 enum,没有比较 own keys。正常模型入口受保护,问题只涉及绕过 tool、直接写 Session 的受信 producer。PLANSTATE-TODO-TOOLPLANSTATE-TODO-INVARIANT
18. 设计评价与验证状态
| 设计选择 | 收益 | 成本 |
|---|---|---|
| Plan 用稳定 tool catalog + 可变 system section | KV-cache 形状更稳定,review 协议清晰 | 硬限制必须在另一条权限轴实现 |
| Goal 把 durable phase 与 activation 分离 | 恢复/Fork 不会意外启动自治 | UI 需要额外 live channel 才能显示真实可续跑状态 |
| Goal 用 revision CAS + 单 attempt driver | 跨控制面冲突 fail closed,自动轮次串行可审计 | 生命周期与异常矩阵明显比 Plan/Todo 更复杂 |
| Todo 用 whole-value snapshot | 重放简单、无需 item identity、模型语义直观 | 长清单重复耗 token,也不支持多人增量编辑 |
| 三者都写 log-only domain event | Compaction 不会抹掉控制状态 | 必须始终区分完整日志、模型 Surface 与 UI projection |
本章逐层核对了 PlanModeController、command/user-question/Plan UI、GoalService/strict fold/invariant、goal tools/authority/wrap-up、same-session round driver、Goal command/Remote/API/GoalBar、Todo tool/invariant/projection/UI,以及对应 unit、integration 和真实 loop 测试源码。Upstream 依赖未安装,因此没有声称测试在本机执行通过;所有 drift 与 gap 都由固定 commit 的可达生产控制流交叉验证。
我的学习体会
内容仅自动保存到当前浏览器,不上传、不进入仓库。你可以导出 Markdown 自行归档。