Codex goal 机制深度分析
Codex /goal 机制深度分析
调研日期:2026-05-19
来源:OpenAI 官方文档 + 开源仓库(openai/codex, tag rust-v0.128)+ 社区技术分析
一、概述:/goal 不是什么
/goal 不是”更强力的 prompt”。它不是让 Codex “更自主”的模糊开关。
从 OpenAI 官方定义来看:
Goal 是线程内持久化的完成状态(durable completion target),可被检查、暂停、恢复,并基于证据评估完成条件。
普通 prompt 的控制模型是:
ask → work → result → wait
Goal 的控制模型是:
work → check → continue or complete
差异在于:Goal 完成后,Codex 会问一个不同的问题——“objective 真的满足了吗?”如果答案是否定的,并且 Goal 仍在活跃且未超预算,Codex 根据最新证据自动继续。
二、五层架构
/goal 的实现跨越 5 个抽象层:
2.1 持久层(SQLite)
migration 0029_thread_goals.sql:
CREATE TABLE thread_goals (
thread_id TEXT PRIMARY KEY NOT NULL REFERENCES threads(id) ON DELETE CASCADE,
goal_id TEXT NOT NULL,
objective TEXT NOT NULL,
status TEXT NOT NULL CHECK(status IN ('active','paused','budget_limited','complete')),
token_budget INTEGER,
tokens_used INTEGER NOT NULL DEFAULT 0,
time_used_seconds INTEGER NOT NULL DEFAULT 0,
created_at_ms INTEGER NOT NULL,
updated_at_ms INTEGER NOT NULL
);
四种状态机:
- active — 进行中,runtime 记录用量
- paused — 被用户或系统暂停,用量停止追踪
- budget_limited — token 预算耗尽(终态,但在途用量仍被记录)
- complete — objective 达成(终态)
关键设计: 每次替换会生成新的 goal_id(UUID)。写入时要求调用者传入 expected_goal_id,若不匹配则写入被静默忽略。这防止了在途的旧 accounting 调用覆写刚替换的新 goal。
预算自动执行: 设置 token_budget 时若 tokens_used 已超过限制,SQL CASE 语句立即将状态转为 budget_limited——没有 check-and-set 的竞态窗口。
2.2 协议层(JSON-RPC App-Server)
三个实验性方法:
| Method | 用途 |
|---|---|
thread/goal/set |
创建/替换/更新 goal。新 objective = 替换(重置用量);相同 non-terminal objective = 更新(保留用量) |
thread/goal/get |
获取当前 goal。返回 goal: null 表示无 |
thread/goal/clear |
删除 goal。返回 cleared: bool |
两个服务端通知:
-
thread/goal/updated— 任何 goal 变化;包含完整 ThreadGoal + optional turnId -
thread/goal/cleared— goal 被移除;包含 threadId
2.3 模型可见层(Tool Interface)
模型只能看到 3 个工具(刻意不对称设计):
| Tool | 参数 | 行为 |
|---|---|---|
create_goal |
{objective, token_budget?} |
若 goal 已存在则失败;创建新 active goal |
update_goal |
{status: "complete"} |
只能标记完成;pause/resume/budget-limit 由系统控制 |
get_goal |
无 | 返回当前 goal 或 null |
设计原则: “Create a goal only when explicitly requested by the user or system/developer instructions; do not infer goals from ordinary tasks.”
Goal complete 的 tool response 附带 completion_budget_report:
{
"remainingTokens": 6750,
"completionBudgetReport": "Goal achieved. Report final budget usage to the user: tokens used: 3250 of 10000; time used: 75 seconds."
}
2.4 运行时事件总线层(Runtime Event Bus)
核心位于 core/src/goals.rs,GoalRuntimeEvent 枚举定义了系统事件:
| 事件 | 行为 |
|---|---|
TurnStarted |
捕获活跃 goal_id + token 用量基线。Plan 模式跳过 |
ToolCompleted |
记录 token + wall-clock delta。可能注入 budget-limit steering |
ToolCompletedGoal |
同上,但抑制 budget-limit steering(避免重复报告) |
TurnFinished |
最终记账,无工具延续的抑制逻辑 |
TaskAborted(Interrupted) |
暂停活跃 goal |
ThreadResumed |
恢复暂停的 goal(paused → active) |
MaybeContinueIfIdle |
启动自动延续轮次 |
ExternalMutationStarting |
在外部 set/clear 之前尽力记账 |
ExternalSet {status} |
应用外部状态 |
ExternalClear |
清除运行时记账状态 |
记账模型(并发安全):
每个线程持有两个快照:
-
GoalTurnAccountingSnapshot— 最近一次记账的 token 用量 -
GoalWallClockAccountingSnapshot— 已流逝的真实时间
Delta = current - last_accounted,然后原子推送到 SQLite。Semaphore(1) 序列化记账更新。
自动延续的保守设计:
- 如果一次延续轮次产生零次工具调用(纯聊天),runtime 设置
continuation_suppressed = true,后续MaybeContinueIfIdle事件跳过 - 用户操作、工具调用或外部 mutation 会重置抑制状态
-
continuation_lock 使用Semaphore(1),若已有延续在途则 bail
停止条件:
- Goal 达到终态(complete / budget_limited)
- 无工具延续抑制
- Plan 模式略过 goals
- 非空闲状态
2.5 表示层(TUI)
- 在 thread 状态栏渲染 goal objective + status
- 处理
thread/goal/updated 和thread/goal/cleared通知 - 显示已用时长和 token 用量
-
/goal slash 命令:支持 inline args,available_during_task() = true
三、Goal 如何加载到上下文(重点)
这是本调研的核心问题。Goal 从 SQLite 持久化到模型 prompt 的完整路径:
Step 1: 线程恢复时的注入
当线程被 resume:
- 发出 goal 快照通知(
thread/goal/updated) - 应用 goal resume 运行时效果:paused → active
- 发送 resume response + replay
- 如果存在活跃 goal 且空闲,触发 MaybeContinueIfIdle
Step 2: 延续 prompt 的构造
Runtime 使用模板文件 templates/goals/continuation.md:
Continue working toward your goal: {objective}
Current status: {status}
Tokens used: {tokensUsed} of {tokenBudget}
Time used: {timeUsedSeconds}s
Use create_goal, get_goal, or update_goal(complete) to manage this goal.
这个 prompt 被注入到下一次 Responses API 调用的 input 数组中,作为 developer role 或 user role 的消息内容。
Step 3: 上下文中的位置
回顾 Codex agent loop 的 prompt 构造:
[system message — 由 Responses API 服务端控制]
↓
[tools — 由客户端提供的工具定义]
↓
[instructions — developer/system 指令]
↓
[input — 列表形式]
input 的结构(依次):
1. [developer] sandbox 描述(内嵌模板:workspace_write.md, on_request.md)
2. [developer] config.toml 中的 developer_instructions(可选)
3. [user] user instructions(AGENTS.md + skills 定义等)
→ 限制 32 KiB,逐文件夹向上搜索
4. 实际用户消息
延续 prompt(含 goal objective)被追加在已有对话历史之后,作为新一轮输入的开头部分。每次 Responses API 调用都会重新发送完整历史(包括 goal 文本),利用 prompt caching 的 prefix 匹配优化。
Step 4: Budget-limit steering 的注入
当 accounting 检测到 token 用量越过预算,runtime 使用 templates/goals/budget_limit.md:
You're approaching the token budget for your goal: {objective}
Current usage: {tokensUsed}/{tokenBudget}
Consider completing soon or the goal will be marked budget_limited.
这个 steering 文本被注入到模型的 response stream 中(作为 output_item),模型感知到后可以自行决定是否调用 update_goal(complete)。
完整数据流:
用户输入 /goal ...
↓
TUI 解析 slash command
↓
JSON-RPC: thread/goal/set(→ 写入 SQLite)
↓
通知 thread/goal/updated
↓
Runtime: TurnStarted → 捕获 goal 状态 + 基线
↓
Responses API 请求(input 中包含延续 prompt + goal objective)
↓
模型生成:推断 → 工具调用 → 工具完成 → 记账(自动)
↓
TurnFinished → 最终记账
↓
MaybeContinueIfIdle(检查:非终态 + 未抑制 + 非 plan 模式)
↓
注入 continuity prompt → 新一轮 Responses API 请求
↓
[循环直至 complete/budget_limited/抑制]
四、Claude Code 的 /goal 对比
Anthropic 在 Claude Code 中也推出了类似的 goal 命令(大概在相同时间窗口),但目前公开信息有限。从社区讨论来看:
- Claude Code 的 goal 是在单次 session 内的,没有 SQLite 持久化支持跨 session 的 pause/resume
- Codex 作为 Rust 实现的独立二进制,有完全的线程生命周期管理(fork/rollback/archive),Claude Code 作为 Node.js 进程缺乏这个基础设施
- 两者都用了”模型不可 pause/resume”的不对称设计原则
不过 Claude Code 在 Codex 开源 /goal 实现之后的快速跟进,表明 goal 正在成为编程 agent 的标准原语——类似于 “task” 原语(spawn_agent / Claude Code Task)之后的第二轮共识收敛。
五、总结:Goal 机制的本质
/goal 不是一个 prompt 增强器。它是一个分布式记账 + 状态机 + 自动延续引擎:
- 持久化:SQLite 保存的目标在进程重启后存活
- 量化的完成标准:token_budget + tokens_used + time_used_seconds 三轴追踪
- 不对称控制:模型只能开始和完成,不能 pause/resume——这是防止自我循环的关键安全边界
- 原子预算执行:SQL CASE 语句确保预算用尽立即停,无竞态条件
- 保守的自动延续:无工具循环时自动抑制,防止无限 loop
- Stale update 保护:goal_id UUID 版本控制防止旧记账覆写新目标
这本质上是把”长程任务管理”从模型的能力缺陷(无法可靠追踪自己的进度)转移到了编译时确定的运行时系统——类似实时操作系统的调度器负责任务切换,而不是让每个应用程序自己实现抢占。
附录:用户动线与上下文机制(Mermaid 图版)
从用户视角看到的完整路径,每一步对应什么系统动作、上下文如何变化。
1. 全景用户动线(时序图)
sequenceDiagram
actor 用户
participant TUI as TUI终端
participant SQL as SQLite
participant Runtime as 运行时
participant Model as 模型
Note over 用户,Model: 阶段A:澄清
用户->>TUI: /goal 迁移支付到Paddle
TUI->>Runtime: 普通prompt推理
Runtime->>Model: POST /v1/responses
Model-->>用户: 追问六要素(Outcome/Verification/...)
用户->>Model: 确认选A,API兼容,只改后端
Note over 用户,Model: 阶段B:创建Goal
Model->>Runtime: create_goal({objective: "...", token_budget: 100000})
Runtime->>SQL: INSERT thread_goals (active)
SQL-->>Runtime: goal_id: a1b2c3d4
Runtime-->>TUI: thread/goal/updated 通知
TUI-->>用户: 状态栏显示 🎯 active
Note over 用户,Model: 阶段C:执行与自动延续
Model->>Runtime: 第一轮推理(工具调用:读代码、写适配器)
Runtime->>SQL: ToolCompleted → UPDATE tokens_used
Runtime->>Runtime: TurnFinished → 检查自动延续条件
loop 自动延续(检查四个条件)
Runtime->>SQL: SELECT objective, status, tokens_used
SQL-->>Runtime: active, 4520/100000
Runtime->>Runtime: 填充 continuation.md 模板
Runtime->>Model: POST /v1/responses(含goal prompt)
Model->>Runtime: 工具调用 → 跑测试
Runtime->>SQL: ToolCompleted → 记账
Runtime->>Runtime: TurnFinished → 再检查延续条件
end
Note over 用户,Model: 阶段D:完成
Model->>Runtime: update_goal({status: "complete"})
Runtime->>SQL: UPDATE status='complete'
Runtime-->>TUI: goal/cleared 通知
TUI-->>用户: 状态栏消失,显示完成报告
2. 自动延续循环(流程图)
graph TD
TF[TurnFinished] --> Q1{goal.status?}
Q1 -->|complete| STOP_OK[✅ 停]
Q1 -->|paused| WAIT[⏸ 等用户 resume]
WAIT --> RESUME[ThreadResumed → active]
RESUME --> Q2
Q1 -->|budget_limited| STOP_BUDGET[⛔ 停 预算强制]
Q1 -->|active| Q2{continuation_suppressed?}
Q2 -->|true 零工具轮| SKIP1[跳过]
Q2 -->|false| Q3{Plan 模式?}
Q3 -->|yes| SKIP2[跳过]
Q3 -->|no| Q4{有 token_budget?}
Q4 -->|超了| SET_BL[设 budget_limited → 停]
Q4 -->|null 或没超| Q5{线程空闲?}
Q5 -->|不空闲| SKIP3[跳过]
Q5 -->|空闲| CONTINUE[MaybeContinueIfIdle]
CONTINUE --> SQL_READ[读 SQLite SELECT goal 状态]
SQL_READ --> FILL[填充 continuation.md 模板]
FILL --> APPEND[追加到 input 数组]
APPEND --> POST[POST /v1/responses]
POST --> LOOP[新一轮推理循环]
LOOP --> TF
3. 上下文注入时间点(时序图)
sequenceDiagram
participant SQL as SQLite
participant Runtime as 运行时
participant Model as 模型
Note over SQL,Model: 阶段A:用户输入 /goal 之前
Runtime->>Model: POST input=[sandbox][AGENTS.md][用户消息]
Note over Model: ❌ 无 goal 内容
Note over SQL,Model: 阶段B:create_goal 之后的第一次推理
Model->>Runtime: create_goal(...)
Runtime->>SQL: INSERT thread_goals
Runtime->>Model: POST input=[sandbox][AGENTS.md][对话历史(含create_goal工具调用)]
Note over Model: ⚡ 模型知道goal存在,因为自己调的create_goal
Note over Model: ❌ 但 continuation.md 尚未注入
Model->>Runtime: 第一轮工具调用
Runtime->>SQL: ToolCompleted → 记账
Runtime->>Runtime: TurnFinished → 检查延续条件 ✓
Note over SQL,Model: 阶段C:自动延续后(关键变化)
Runtime->>SQL: SELECT objective, status, tokens_used
SQL-->>Runtime: "迁移支付...", active, 4520/100000
Runtime->>Runtime: 填充 continuation.md 模板
Runtime->>Model: POST input=[...对话历史][🎯continuation.md][续轮消息]
Note over Model: ✅ goal 文本第一次以结构化方式出现!
关键发现:continuation.md 不是在 create_goal 后立即注入的,而是在第一轮推理结束、触发自动延续后才注入。 第一轮模型靠自己执行的 create_goal 工具调用来感知 goal 的存在,从第二轮开始才有模板注入。这意味着第一轮之后,goal 在上下文中有两个来源:
- 对话历史中 create_goal/get_goal/update_goal 的 tool call 记录
- continuation.md 模板注入的结构化 reminder
4. 用户可感知的反馈
用户在终端能看到什么?三处反馈:
TUI 状态栏(thread 底部):
╔═══════════════════════════════════════╗
║ 🎯 迁移支付模块到 Paddle █ active ║
║ 37,250 / 100,000 tokens · 847s ║
╚═══════════════════════════════════════╝
Slash 命令:
/goal → 查看当前 goal 状态
/goal pause → 暂停(系统设 paused)
/goal resume → 恢复(系统设 active)
/goal clear → 删除 goal 记录
进度输出(模型每轮报告的摘要):
第1轮 ✅ 读取 Stripe 配置
第2轮 ✅ 创建 Paddle 适配器骨架
第3轮 ❌ 测试失败: 签名不兼容
第4轮 ✅ 修复签名逻辑
第5轮 ✅ 32/32 测试全部通过
5. 责任分界表
| 环节 | 责任人 | 具体内容 | 不可推卸的原因 |
|---|---|---|---|
| Outcome(完成标准) | 用户确认 | 做成什么样算完(定量或定性标准) | 业务决策,模型无权替用户决定”什么算够好” |
| Constraints(约束) | 用户确认 | 什么不能动(API兼容、数据不迁移等) | 涉及业务风险和用户资产,模型不能越权决定 |
| Boundaries(范围) | 用户确认 | 改哪些不改哪些 | 项目范围决策属于用户职责 |
| Verification(验证方法) | 模型写 | 跑什么测试、读什么benchmark | 技术执行层面,模型知道怎么验证 |
| Iteration Policy(迭代策略) | 模型写 | 每轮干完怎么决定下一步 | 执行策略由模型根据环境状态决定 |
| Blocked Stop(阻塞条件) | 模型判断 + 用户复核 | 卡住时怎么停、报什么信息 | 模型可判断”卡住了”,但下一步决策需要用户 |
| 持久化 goal 状态 | 系统(Runtime + SQLite) | 存 objective、status、用量 | 基础设施层责任 |
| 模板注入 context | 系统(Runtime) | 读 SQLite → 填 continuation.md → 追加 input | 自动延续管道 |
| 自动延续调度 | 系统(Runtime) | 检查四条件 → 决定下一轮 | 事件总线生命周期 |
| 预算强制停止 | 系统(SQLite) | SQL CASE tokens_used ≥ token_budget | 不可绕过的上限 |
| 工具执行与验证 | 模型 | 跑测试、改代码、读结果 | 核心执行能力 |
| 标记完成 | 模型 | update_goal(complete) + budget report | 自判断完成时主动报告 |
来源:
- developers.openai.com/codex/use-cases/follow-goals
- openai.com/index/unrolling-the-codex-agent-loop
- openai/codex GitHub 仓库 tag rust-v0.128 (core/src/goals.rs, migration 0029)
- xaicontrol.com, zylos.ai
- Using Goals in Codex (OpenAI Cookbook)