知识
← 返回

Codex goal 机制深度分析

2026-05-19 技术
#技术

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

停止条件:

  1. Goal 达到终态(complete / budget_limited)
  2. 无工具延续抑制
  3. Plan 模式略过 goals
  4. 非空闲状态

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:

  1. 发出 goal 快照通知thread/goal/updated
  2. 应用 goal resume 运行时效果:paused → active
  3. 发送 resume response + replay
  4. 如果存在活跃 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 命令(大概在相同时间窗口),但目前公开信息有限。从社区讨论来看:

  1. Claude Code 的 goal 是在单次 session 内的,没有 SQLite 持久化支持跨 session 的 pause/resume
  2. Codex 作为 Rust 实现的独立二进制,有完全的线程生命周期管理(fork/rollback/archive),Claude Code 作为 Node.js 进程缺乏这个基础设施
  3. 两者都用了”模型不可 pause/resume”的不对称设计原则

不过 Claude Code 在 Codex 开源 /goal 实现之后的快速跟进,表明 goal 正在成为编程 agent 的标准原语——类似于 “task” 原语(spawn_agent / Claude Code Task)之后的第二轮共识收敛。


五、总结:Goal 机制的本质

/goal 不是一个 prompt 增强器。它是一个分布式记账 + 状态机 + 自动延续引擎

  1. 持久化:SQLite 保存的目标在进程重启后存活
  2. 量化的完成标准:token_budget + tokens_used + time_used_seconds 三轴追踪
  3. 不对称控制:模型只能开始和完成,不能 pause/resume——这是防止自我循环的关键安全边界
  4. 原子预算执行:SQL CASE 语句确保预算用尽立即停,无竞态条件
  5. 保守的自动延续:无工具循环时自动抑制,防止无限 loop
  6. 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: 确认选AAPI兼容,只改后端

    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: 阶段Bcreate_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 在上下文中有两个来源

  1. 对话历史中 create_goal/get_goal/update_goal 的 tool call 记录
  2. 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)