← → 翻页 · B 静态 · ESC 索引
Agent · 目标机制研究 · 技术分享
MP-26 · 2026.05.14 · 01 / 26
面向工程实现的 /goal 对比 · 2026-05

/goal
同频时刻.

01 · OPENAI

Codex

/goal 是目标控制台
02 · ANTHROPIC

Claude Code

/goal 是独立验收员
03 · NOUS RESEARCH

Hermes

/goal 是跨入口持续
三家同频 · 长任务的目标控制面
2026 · 4 月底 → 5 月中
02 / 26
同频信号 · concurrent ship dates

三家几乎前后脚把 goal 抬到运行时.

≈ APR 28

Codex 0.128

官方 /goal 落进运行时 · Brockman: "built-in Ralph loop++"
codex-cli · v0.128 · sha 4a8e2f1
≈ MAY 5

Claude Code · /goal docs

Stop hook + evaluator subagent 一键化为 slash 命令
claude-cookbook · PR #214 · main
≈ MAY 14

Hermes · persistent goals

SessionDB · 跨 gateway · goal_judge · turn budget
hermes · PR #142 · main
这不是巧合
长任务跑过几个上下文窗口之后,
目标会 / / — 三家都在补这一格.
太长不看 · TL;DR
03 / 26
TOO LONG; DIDN'T READ · 五张便签

五条带走的 判断.

TLDR · 05 · MAIN

一个能用的 Goal,
至少要交代 五件事

✓ 范围 什么算"做完" SCOPE
✓ 不变量 不能踩的红线 INVARIANT
✓ 验证证据 什么算"真完成" EVIDENCE
✓ 预算边界 多少轮 · token · 工具调用 BUDGET
✓ 收束方式 怎么交付 · 怎么 close CLOSE
少一件,长任务都接不住。下一页起逐项展开。
TLDR · 01

让目标变成对象,不是少停几次

savestatusresumeclose
TLDR · 02

值得看的是同频背后的 Harness

Codex 0.128 Claude #214 Hermes #142
TLDR · 03

三种做法 = 三种产品边界

都把"自由发挥"那一格抽走了
TLDR · 04

和 Skills 走的是同一种路

Skill .md → 工作流包
Goal  一句话 → 运行时合同
SUMMARY

"自主" ≠ "放任"

系统开始把自主性装进工程结构里。

可观察 可回滚 可接管
先看一个问题 · why goal · why now
04 / 26
CENTRAL QUESTION

长时目标,
谁有权
「完成」?

ROOT CAUSE · 病灶在哪
goal 留在 聊天里,而不是 运行时对象里。
goal_obj { scope · invariants · evidence · budget · close }
TRIGGERS · 一旦跨过这条线
跨 turn 跨上下文 跨会话
goal 就需要一个生命周期——而不是 prompt 里一句愿望。
SYMPTOM 01

目标会

"完成整个迁移" 跑了几轮就缩成 "核心路径已迁移"。局部证据顺势变成新中心。

DRIFT » "完成迁移" 核心路径 主要逻辑 "看起来 OK"
FIX » 完成定义锁进 schema,不让模型改。
SYMPTOM 02

状态会

做完的、试过的、卡在环境上的——全散在聊天里。下一个 Agent 拿到的是碎片。

✓ DONE ⟳ TRIED ⚠ ENV-FAIL ✗ REAL-FAIL  » 只剩 chat log
FIX » 把状态写进 event log(status + evidence),不写聊天里。
SYMPTOM 03

完成标准会

短任务里没事,长任务里这些话会被当成「完成」。

SOFT WORDS · 出现就 ⚠ 一下
"看起来 OK"
"应该没问题"
"主要逻辑完成"
"已经基本上…"
FIX » 验收 hook(test + subagent)给二元判定。
三家一句话 · 各自抽走了一格
05 / 26
three takes · one runtime interface

各自把哪一块
"模型自由发挥"里抽走了?

[ 目标状态 ] » Codex [ 停止判断 ] » Claude [ 跨会话目标 ] » Hermes
01 · CODEX

目标控制台

目标状态 从自由发挥里抽走。
KEY MECH
state SQLite goal_obj 一行
gates pre·post·eval·stop·resume ×5
lock  schema-lock 完成权
cap   预算 + 续跑 + audit
codex-cli 0.128 · goal_spec.rs:66
运行时状态机 · heavy
02 · CLAUDE CODE

独立验收员

停止判断 从自由发挥里抽走。
KEY MECH
cmd  slash /goal save · status
hook Stop hook 拦截 stop
judge evaluator subagent
feed transcript + reason 反喂
claude-cookbook PR #214 · .claude/hooks/stop.json
用户态原语 · light
03 · HERMES

跨入口持续目标

跨会话目标 从自由发挥里抽走。
KEY MECH
db   SessionDB.state_meta
way  CLI / API / WS / Hook 接力
judge goal_judge fail-open
cap   turn budget 兜底
hermes PR #142 · session.py · gateway/
跨入口 agent · cross-gateway
共同点 三家都装了三层——goal 对象 · 完成证据 · 预算兜底 goal 是接口契约,不是 prompt 句式
第一部分 · Codex
06 / 26
01
CHAPTER 01 · OPENAI CODEX

Goal 是
运行时状态.

CHAPTER MAP · 4 slides ahead
p07 · arch p08 · fields p09 · tool-bound p10 · gates
最"正统"的做法

把 goal 放进运行时状态里

和 thread · protocol · tools · usage · resume 都打通。好处是,代价是

这不是加一个命令,是改一条产品链路
codex-cli 0.128 · sha 4a8e2f1
src/runtime/goal.rs · protocol/goal.rs
/goal save · status · resume · close
⚙ SCOPE
per-thread · 一线一目标
⌁ GATES
5 道 · pre/post/eval/stop/resume
⚠ STATE
EXPERIMENTAL · 默认关闭
Codex · /goal 五幕分镜 · turn-cycle assembly
07 / 26
01INPUT
02STORAGE
03RUNTIME
04MODEL
05FEEDBACK ✓
SCENE 01 · STATION 1/5

键入 → 分发 → RPC

TUI · slash dispatch · app-server
用户敲 /goal … · TUI 把它解析成 6 个变体之一,再编码成 3 个 RPC之一发往 app-server。 这一层是编译时总闸 Feature::Goals 的天下——关掉就什么都不存在。
// 6 slash variants
/goal              → open menu
/goal <objective>  → set / replace
/goal clear        → delete row
/goal edit         → prefill editor
/goal pause/resume → flip status

// 3 RPCs over app-server
thread/goal/set    // write
thread/goal/get    // read
thread/goal/clear  // delete
slash_dispatch.rs:226 · thread_goal_processor.rs:92–322
SCENE 02 · STATION 2/5

SQLite · 一行一线程

mig 0029 · thread_goals · 4 态 CHECK enum
持久化的不是 goal 本身,是线程-目标对PRIMARY KEY = thread_id 保证一个线程只有一个 goal。 status 列是 4 态 CHECK enum——SQL 层守门员,写不进第五个值。
CREATE TABLE thread_goals (
  thread_id    TEXT PRIMARY KEY,
  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 DEFAULT 0,
  ...
);
state/migrations/0029_thread_goals.sql
SCENE 03 · STATION 3/5

5 道闸门 + Plan KILL

continuation governor · <goal_context> 注入
每个 turn 结束都问一遍"还续吗?"。5 道闸门全部放行才返回续命候选, Plan 模式是红 KILL 一票否决。token 触顶时本站主动把状态翻成 budget_limited。 放行后,胶囊被包进 <goal_context>,下一轮 prompt 就带着它走。
fn continuation_candidate_if_idle():
  if !Feature::Goals             return None
  if status != Active           return None
  if turn_in_progress           return None
  if queued_user_input          return None
  if mode == Plan               return None  // kill
  return Some(Candidate)

// hidden injection:
<goal_context> { continuation.md
               | budget_limit.md
               | objective_updated.md }
core/src/goals.rs:1301 · 1014 · context/goal_context.rs
SCENE 04 · STATION 4/5

3 工具 · schema 硬锁

goal_spec.rs · enum["complete"] 是单值
模型只看见 3 个工具。get_goal 只读;create_goal 守得严,几乎不应自己用; update_goal.status 的 JSON schema 是 单值 enum ["complete"]—— 模型能写的只有 complete,硬锁,不是 prompt 教育。
{"name": "get_goal",    // read
 "parameters": {}}

{"name": "create_goal", // guarded
 "parameters": {
   objective: str,
   token_budget?: int+ }}

{"name": "update_goal", // SCHEMA-LOCK
 "parameters": {
   "status": { "enum": ["complete"] }
 }}
// 单值 enum:写啥都只能是 "complete"
core/src/tools/handlers/goal_spec.rs:14, 66
SCENE 05 · FEEDBACK ROUND-TRIP

complete 的回家路

model → app-server → state_db → emit
模型按下 update_goal 不是 SQL UPDATE 直发—— 要经 app-server 校验、runtime 授权才写库,最后 emit 一个 ThreadGoalUpdatedEvent状态 LED 由这条环的回信触发,不是模型自己点亮的。这才是它"complete"。
// model side
update_goal({status: "complete"})
  ↓ tool_call
// app-server side
validate · authorize
  ↓
state_db.update_thread_goal(
  status = Complete,
  updated_at_ms = now())
  ↓
// emit
ThreadGoalUpdatedEvent
thread_goal_processor.rs · goals.rs ToolCompletedGoal
SCENE 1 / 5 INPUT /goal → dispatch → RPC codex · turn cycle $ ↳ TUI parses 6 variants · picks "set" /goal "…"/goal set → encode thread/goal/set RPC → post → app-server::handle a new turn-cycle capsule is born ↓ GOAL turn cycle ⚙ compile gate Feature::Goals = true · L1 关掉 = 整条链失效 → next station SQLite vault 写 thread_goals 一行 status = active SCENE 2 / 5 STORAGE sqlite · mig 0029 · 1 row / thread GOAL turn thread_goals migration 0029 · 一行一线程 thread_id T:thr_4a2… PRIMARY KEY objective T:"rewrite tests in pytest" status T:'active' CHECK ∈ {…} token_budget · tokens_used I: 50000 · 0 status enum · 4 态 CHECK 守门员 = SQL 层 enum lock ● ACTIVE 由 INPUT 写入 ○ PAUSED /goal pause 切换 ⚠ BUDGET_LIMITED runtime 主动翻 ✓ COMPLETE 经 update_goal SCENE 3 / 5 RUNTIME · 5-GATE continuation governor · 还续吗? GOAL turn continuation_candidate_if_idle() 5 道闸门 + Plan KILL · 全过才放行 Feature::Goals compile-time · 总闸 status == Active 非 active 一律不续 !turn_in_progress 手上正在跑的不打断 !queued_user_input 用户排了新键 = 让位 mode != Plan · KILL Plan 模式直接红 KILL 放行 → 包裹 注入 <goal_context> CTX <goal_context> ⚠ token 触顶 tokens_used > budget → status = budget_limited → 注 budget_limit.md SCENE 4 / 5 MODEL · 3 TOOLS schema-lock · enum["complete"] CTX <goal_context> 上一站包成的 context → 进 prompt model 看见的 3 个工具 get_goal read-only · returns current row 无副作用 · 谁都能调 create_goal guarded · model 几乎不该自己调 objective + token_budget? update_goal · SCHEMA-LOCK 🔒 "status" : { "enum" : [ "complete" ] } ↑ 单值 enum · 只能写 "complete" 不是 prompt 教育 — JSON schema 硬锁 model 按下 = round-trip 启动 ▶ pressed ⚠ schema-lock trap JSON schema 层硬锁 不是 prompt 工程 model 想写别的也写不出来 SCENE 5 / 5 FEEDBACK · ROUND-TRIP model → app-server → state_db → emit model 按下: update_goal({ status: "complete" }) 1 · app-server validate · authorize 2 · state_db UPDATE thread_goals · status=Complete 3 · emit event ThreadGoalUpdatedEvent FLIP! ✓ status LED · Γ[thread_id] 由 emit 触发 · 不是 model 自己点亮 active paused budget_limited complete DONE turn cycle terminated
scene 1/5 · 0.0s INPUT
Codex · thread_goals · 一行 6 列
08 / 26
migration 0029 · sqlite row schema

Goal 是一行数据,
不是 prompt 注释.

FIELD · 01

thread_id TEXT PK

PRIMARY KEY = thread_id —— 一条线程只有一个 goal

FIELD · 02

objective TEXT

用户想完成的事 —— 不再散落在聊天记录里。

★ FIELD · 03 · GUARDED

status CHECK enum

{ active · paused · budget_limited · complete }

SQL 守门员 · 写不进第五个值
FIELD · 04

token_budget INT

超了就 budget_limited · 注 budget_limit.md。

FIELD · 05

tokens_used INT

由运行时统计,不靠模型自己汇报

FIELD · 06

created_at_ms · updated_at_ms

目标自己的生命周期,不依赖聊天窗口

Codex · 模型权限边界 · 3 tools · enum["complete"]
09 / 26
模型能推进 · 但不能独占完成权

JSON schema
硬锁 上,
不是 prompt 教育.

// goal_spec.rs:66
"status": { "enum": ["complete"] }
↑ 单值 enum · 想写别的也写不出来
01 · READ

get_goal()

读当前目标是什么、做到哪一步、已经消耗多少。无副作用,谁都能调。

02 · GUARDED

create_goal(objective)

用户明确要长期推进时才用。模型不该自己立目标——看什么都自启目标就乱了。

03 · LOCKED 🔒

update_goal · SCHEMA-LOCK

模型能写的只有 "complete"。 想暂停 · 想恢复 · 想说"放弃" —— 写不出来

Codex · 续跑 · 5 道闸门 + 预算收束
10 / 26
continuation_candidate_if_idle() · 还续吗?

每轮结束都问一遍 ——
五道闸门全过才放行,
Plan 是红 KILL 一票否决.

5-GATE CHECKLIST
Feature::Goalscompile-time · 总闸
status == Active非 active 一律不续
!turn_in_progress手上正在跑的不打断
!queued_user_input用户排了新键 = 让位
mode != Plan · KILLPlan 模式直接红 KILL
⚠ TOKEN 触顶

tokens_used > token_budget

runtime 主动翻 status = budget_limited,注 budget_limit.md 让模型自己写一份收束

⤷ 放行后

注入 <goal_context>

下一轮 prompt 自带 continuation.md / budget_limit.md / objective_updated.md 之一 —— 续跑是调度,不是 prompt。

第二部分 · Claude Code
11 / 26
02
CHAPTER 02 · ANTHROPIC CLAUDE CODE

Goal 是
停止前的闸门.

轻得多的思路

把 goal 写成停止条件

挂在每轮结束前。评估器说没完成就不让这轮结束,继续干。

不是状态机,是一道停止闸门
⌁ SESSION SCOPE
⚓ Stop hook + evaluator
↻ 从会话记录恢复
Claude Code · Stop hook 五幕分镜 · 自定义闭环
12 / 26
01INSTALL
02TURN 1
03STOP-GATE
04DENY
05MET ✓
SCENE 01 · STATION 1/5

挂钩子 · activeGoal

/goal slash · register session Stop hook
和 Codex 不一样,Claude Code 把整个 goal 系统建在用户态/goal 是一个 slash command,它做两件事: 登记 activeGoal 字符串注册一个 session-scope 的 Stop hook。 从这一刻起,模型说"我说完了"会被拦下来。
// ~/.claude/commands/goal.md (一个 markdown)
allowed-tools: [Bash, Edit, Read]
description: "set the goal-until-met condition"

// 用户敲 /goal "直到 README 写完..."
session.activeGoal = "直到 README 写完 3 节 + 介绍"
session.activeGoal.iteration = 0

// stop hook 在 settings.json 里:
"hooks": { "Stop": [
  { matcher: "*",
    command: "evaluator-claude-haiku" }] }
commands/goal.md · settings.json Stop hook
SCENE 02 · STATION 2/5

Turn 1 · 干一会就喊停

worker model · edit + stop
worker(主模型) 不知道 stop hook 存在。 它正常干活——编辑 README,加了两节就觉得"差不多了", 主动 emit 一个 stop tool,准备结束 turn。 没有这层 hook,session 就此安静下来。
// turn 1 · worker
Edit  README.md
  + ## Quick Start
Edit  README.md
  + ## Installation

// worker 自评:已完成大部分内容
worker → stop
user-prompt-submit / worker turn
SCENE 03 · STATION 3/5

Stop 拦下来 · 召唤评判员

stop hook fires · evaluator reads transcript
Stop hook 在 worker 真正 stop 之前截住了它。 默认拉起一个 haiku 级 evaluator subagent, 它的任务很窄:看 transcript 和 activeGoal 的条件,判断是否真完成。 evaluator 不写代码,只投票。
// stop hook fires
stop-hook eval (default haiku)

condition: "直到 README 写完 3 节 + 介绍"
input:     transcript + file diffs

// deliberating ...
hooks/stop · evaluator subagent
SCENE 04 · STATION 4/5

unmet → reason 反喂

verdict block · prefix injected next turn
verdict 是 block。evaluator 写一条 reason 解释为啥还没完成。 这条 reason 不是日志——它会被 当作下一轮 worker 的 prefix注入, 告诉 worker 该补什么。一轮一轮地"反喂",直到 evaluator 说 MET 为止。
// evaluator → block
{ "decision": "block",
  "reason": "README 只有 2 节,
              还缺 1 节 + 介绍段落" }

→ append GS-01 unmet to transcript
→ next worker turn 自动加上 prefix:
   "上一轮被 block, 原因:
    README 只有 2 节, 还缺 1 节 + 介绍"
decision=block · reason 进 transcript 前缀
SCENE 05 · MET ROUND-TRIP

第 3 轮 met · hook 自清

verdict met · valve opens · goal cleared
就这样跑了 turn 2、turn 3。第 3 轮 evaluator 终于回 met—— 放行,session 真的能 stop 了。 activeGoal 被自动清空,Stop hook 也卸下。 整个机制完全长在用户态,核心代码一行没改
// turn 3 · worker
Edit README.md
  + Project intro paragraph
worker → stop

// evaluator
{ "decision": "met",
  "reason": "✓ 3 sections + intro" }

// session
activeGoal = null
Stop hook removed
→ session 安静结束 · 总 3 turn
claude-code/hooks/stop · activeGoal cleared
SCENE 1 / 5 INSTALL /goal · register hook + activeGoal claude code · session $ ↳ slash command resolves to ~/.claude/commands/goal.md session.activeGoal = "直到 README 写完 3 节 + 介绍" ⚓ Stop hook 挂上了 · session-scope ⚙ hook config matcher: "*" command: evaluator-haiku timeout: 60s 关掉 = 一切回到自由 → user-space only 不改 claude code 核心 命令 + 钩子 + 子代理 三个用户态原语 就能拼出 goal 系统 SCENE 2 / 5 TURN 1 · WORKER edit + edit + worker → stop worker model 不知道 stop hook 存在 Edit README.md + ## Quick Start Edit README.md + ## Installation worker → stop stop event 即将冒出来 ↓ 下一幕被截住 这一幕 worker 是"自由身" — 没有强制结构,没有特殊工具,没有 schema 限制。 — 它能直接 Edit / Bash / Read,做完了就 emit stop。 — 这种自由是 Claude Code 的核心——直到 stop 那一瞬间。 ⚠ 接下来这一瞬,被 hook 截胡。 SCENE 3 / 5 STOP-GATE · pulls transcript hook fires → evaluator subagent wakes stop event worker → stop STOP HOOK matcher: * 挡住 stop evaluator (haiku) subagent · read-only · 投票员 input: transcript + activeGoal prompt: "is the condition met?" deliberating ... 读完 transcript · 比对 activeGoal → 给出 verdict (下一幕) 关键:这一刻 worker 完全不知情 — evaluator 是另一个模型,看不到 worker 的 in-context 数据。 — 它只读 transcript + activeGoal · 输出 met / block + reason · 像第三方裁判。 SCENE 4 / 5 DENY · reason → prefix block · GS-01 unmet · feed reason back verdict · block "reason" "README 只有 2 节, 还缺 1 节 + 介绍段落" ⚠ block = stop 被拒 next worker prefix ↳ 自动加在下一轮 worker 输入头部 "上一轮被 block, 原因:README 只有 2 节, 还缺 1 节 + 介绍段落" TRANSCRIPT · goal_status attachments GS-01 unmet 2/3 sections ⚠ 这就是"反喂" evaluator 写的 reason 不是日志 而是下一轮 worker 的 input prefix SCENE 5 / 5 MET ✓ · gate opens turn 3 · valve opens · activeGoal cleared turns · cumulative turn 1 2 sections · GS-01 unmet turn 2 + API Reference · GS-02 unmet turn 3 + intro · evaluator → met STOP HOOK closed verdict · met ✓ 3 sections + intro "decision": "met" → Stop hook 放行 → activeGoal = null TRANSCRIPT · cumulative goal_status GS-01 unmet 2/3 sections GS-02 unmet missing intro GS-03 ✓ MET 3 sections + intro CLEARED activeGoal = null
scene 1/5 · 0.0s INSTALL
Claude · 用户态三原语 · 不改核心代码
13 / 26
three user-space building blocks

一个 goal 系统,
三块积木就能拼出来.

不是改 Claude Code 核心,而是把三个用户态原语组合起来用:slash command 落 activeGoal,hook 拦截 stop,subagent 当裁判。
SLASH COMMAND

~/.claude/commands/goal.md

一个 markdown 文件即一条 slash。把 activeGoal 写入 session,再注册 hook。

STOP HOOK

settings.json · matcher: "*"

每次 worker 想 stop,hook 先拦下来 —— stop event 不能直接落地

EVALUATOR SUBAGENT

haiku · read-only · 投票员

看 transcript + activeGoal,只 met / block + reason,不写代码。

Claude · evaluator 的视野 · 只看证据
14 / 26
evaluator boundary · what it can / cannot see

目标怎么写是一半,
证据怎么留在对话里是另一半.

✓ EVALUATOR CAN SEE

transcript 里写下来的

  • ✓ Edit / Bash / Read 这些工具调用的结果
  • ✓ 测试命令和它打印的 stdout / stderr
  • ✓ file diff 摘要(取决于实现)
  • ✓ 模型自己写的总结/Markdown
  • ✓ activeGoal 的字符串条件
✕ EVALUATOR CANNOT SEE

transcript 之外的世界

  • ✕ 不能自己 npm test · 不能 grep
  • ✕ 不能 fs.read 文件系统
  • ✕ 看不到 worker 心里那点想法
  • ✕ 看不到 CI、外部接口、日志服务
  • ✕ 默认只跑 ≤ 60s · 太重的判断接不住
写 goal 的人要做的 验证命令、失败输出、修复结果、最终验收都老老实实写进对话里。如果不把证据留下,evaluator 只能凭运气。
Claude · reason 反喂 · 不是日志,是下一轮 prefix
15 / 26
reason ≠ log · reason → next-turn prefix

evaluator 的 block.reason,
其实是下一轮 worker 的输入开头.

TURN 1

worker 写了 2 节

// evaluator
{ "decision": "block",
  "reason": "README 只有 2 节, 还缺 1 节 + 介绍" }
这条 reason 不消失。它会被挂到下一轮 worker 输入头部
TURN 2 · prefix injected

worker 看见的第一行 ↓

"上一轮被 block,
原因: README 只有 2 节,
还缺 1 节 + 介绍"
worker 接着补 API Reference 一节,不是再问一遍
TURN 3 · MET ✓

3 sections + intro

{ "decision": "met",
  "reason": "✓ 3 sections + intro" }
activeGoal = null · Stop hook removed · session 安静结束
第三部分 · Hermes
16 / 26
03
CHAPTER 03 · NOUS RESEARCH HERMES

Goal 是
跨入口持续目标.

把目标挂到 Agent 自己身上

不是某个终端会话的事

SessionDB.state_meta 存着,可以跨会话 · 跨 gateway 恢复。今天 CLI 起的,明天 Telegram 接着看。

重点不是无限跑,而是跨入口仍然有可见的停止边界
⌁ SESSIONDB.STATE_META
↺ CROSS-GATEWAY
⚠ TURN BUDGET 兜底
Hermes · /goal 五幕分镜 · cross-gateway continuity
17 / 26
01ARRIVE
02STATE_META
03JUDGE
04BUDGET
05HANDOFF ↺
SCENE 01 · STATION 1/5

任意 gateway · /goal 落地

CLI · Telegram · Discord · Slack · Web ...
Hermes 的目标不属于某一个会话。 用户在哪个 gateway 起 /goal,Adapter 都把它规范化成内部事件, 喂给同一个 Agent。这一刻 gateway_origin 被记下来,但目标本体属于 Agent。
// telegram bot · /goal command
gateway_event = {
  origin:   "telegram",
  user_id:  "@nik",
  text:     "ship payments module by Fri
             with tests"
}

// 同样从 cli/discord/web 也能发 ↑
hermes.set_goal(gateway_event)
gateway/* adapters · normalized goal event
SCENE 02 · STATION 2/5

SessionDB.state_meta · 不挂在会话上

agent-scoped goal · 跨入口共享一份
目标不写进某条 transcript,写进 SessionDB.state_metastate_meta 是 Agent 自己的"工作台便签",所有 gateway 共享同一份。 condition · turn_count · last_judge · origin —— 全在这里。
// SessionDB.state_meta(agent_id)
{
  goal_id:   "g_8a3f",
  condition: "ship payments module...",
  origin:    "telegram",
  turn_count: 0,
  budget:     12,
  last_judge: null
}
// 多 gateway 共同读写这一行
SessionDB · state_meta column · agent-scoped
SCENE 03 · STATION 3/5

goal_judge · 错了也别炸

每 turn 评估 met/unmet · fail-open
每个 turn 结束,goal_judge 被唤醒,读 transcript + condition, 返回 {met: bool, reason}。但它对自己要求很现实—— judge 自己挂了就 fail-open:不阻断流程,把这一轮当 unmet 处理, 真正兜底交给 turn budget。
// turn N · goal_judge
verdict = goal_judge(transcript, condition)

try:
  if verdict.met:    // ✓ stop continuation
    finalize_goal()
  else:
    continue_next_turn(reason=verdict.reason)
except JudgeError:        // fail-open
  continue_next_turn(reason="judge unreachable")
  // 不让 judge 自己挂掉 = 整段断
judges/goal_judge.py · fail-open semantics
SCENE 04 · STATION 4/5

turn budget · 可以继续,不能无限继续

continuation 兜底 · 用户可见 reason
turn_count 每轮 +1。撞到 turn budget 上限, Hermes 不再补下一轮,把状态翻成 budget_exhausted, 写一条用户能看见的 reason 回到 origin gateway。 真正的兜底从来不是 judge,是预算和可见性。
// each turn
state_meta.turn_count += 1

if turn_count >= budget:
  state_meta.status = "budget_exhausted"
  gateway.send(origin, "⚠ 跑满 12 轮还没 met,
                        暂停在 'tests 还差 1 个'
                        ——要我接着干吗?")
  // 用户看得见,可以决定续 / 改 / 停
budget controller · user-visible reason
SCENE 05 · CROSS-GATEWAY HANDOFF

切到另一个入口 · 接着干

telegram → web dashboard · state_meta 不变
用户晚上从 Telegram 出发,第二天打开 Web Dashboard, 连同一个 Agent。SessionDB 一句 get_state_meta(agent_id), 当前目标、turn_count、last_judge 都还在。不用重新讲一遍, 从上一轮 reason 接着跑。
// next morning · web dashboard
agent = hermes.connect(agent_id)
meta  = SessionDB.state_meta(agent_id)

// "tests 还差 1 个" 等在那儿
print(meta.last_judge.reason)

// 用户敲个 "go" · agent 继续
hermes.resume(agent, gateway="web")
→ turn 9 / 12 picks up
cross-gateway continuity · 同 agent 不同入口
SCENE 1 / 5 ARRIVE any gateway · /goal · normalized event telegram · @hermes_bot ↳ gateway adapter normalize origin: "telegram" user_id: "@nik" condition: "ship payments module by Fri w/ tests" gateway adapters 十几个入口 · 同一个 Agent CLI ▣ Telegram Discord Slack Matrix Signal WhatsApp SMS iMessage Webhook API Web Dashboard 所有 gateway 都把消息规范化成 GoalEvent HERMES agent_id = a_42f ⚙ goal-event normalization — Codex 收的是 TUI /goal · Claude Code 收的是 slash command — Hermes 收的是 GoalEvent —— 不绑死任何一个 gateway — 谁起的不重要 · 接得住才重要 SCENE 2 / 5 STATE_META · agent-scoped SessionDB · 不挂在会话上,挂在 Agent 上 HERMES agent_id = a_42f SessionDB.state_meta(agent_id) single row · agent-scoped goal store goal_id: "g_8a3f" condition: "ship payments module..." origin: "telegram" turn_count: 0 budget: 12 last_judge: null status: 'active' created_at: 2026-05-14T22:11 → 任意 gateway 都能读这一行 ▣ TELEGRAM origin · 现在活跃 ○ WEB DASHBOARD 明天接管 · 见 SCENE 5 ○ CLI · DISCORD ... 同一份 state_meta SCENE 3 / 5 JUDGE · fail-open goal_judge subagent · 错了也别炸 worker · turn N 编辑代码 · 跑测试 stop event ↑ ↓ goal_judge 被 awake goal_judge subagent · read transcript + cond ▣ READ transcript ▣ MATCH against condition ▣ EMIT {met, reason} 两种结局: ✓ verdict met: false reason: "tests 还差 1 个" ⚠ JudgeError judge unreachable → fail-open → 继续下一轮 → reason="judge ko" fail-open semantics · judge 不是真理,judge 也会挂 — Codex 用 JSON schema 硬锁 · 只让 model 写 complete — Claude 用 evaluator decision=block · 评估器挂掉就整个卡住 — Hermes 用 fail-open · judge 一旦异常,默认放过这一轮 = unmet — 兜底从来不是 judge,是 turn budget(下一幕) + 用户可见的 reason — 这是个产品决定:在长时 agent 里,可靠性 > 严格 — "不让评估器自己挂掉就把整个过程打断" SCENE 4 / 5 TURN BUDGET · 可以续,不能无限续 continuation 兜底 · 用户可见 reason turn counter 每轮 +1 · 写回 state_meta 0 / 12 budget running turns timeline 每个 judge 返回 unmet,就吃一格 budget 12 格都吃完 → budget_exhausted ⚠ telegram · @nik "跑满 12 轮还没 met, 暂停在 'tests 还差 1 个' ——要我接着干吗?" 三种"还能不能跑"的兜底 — Codex 看 token_budget · 触顶就翻 budget_limited — Claude Code 看 evaluator block 次数 · 没有明确预算 — Hermes 看 turn_count vs budget · 到点写 user-visible reason 回到 origin — 真正在做的事:让用户能看见 Agent 卡在哪 · 自己决定续 / 改 / 停 — 这一格是"接得住"的核心 · 不是"跑得久" SCENE 5 / 5 CROSS-GATEWAY HANDOFF telegram → web dashboard · 不变的 state_meta telegram · 昨晚 turn 8 / 12 unmet "tests 还差 1 个" user 关掉了 Telegram ↓ session 仍属 a_42f state_meta 没动 SessionDB state_meta(a_42f) · 同一行 turn_count: 8 budget: 12 last_judge: "tests 还差 1 个" status: paused web dashboard · 今早 hermes.connect(a_42f) → 同一份 state_meta user 看见 "差最后一个" 敲 "go" → 续 turn 9 不用 retell · 不丢上下文 HERMES agent_id = a_42f · 一直是它 三家"恢复"方式 — Codex 从 SQLite 那条 thread_goals 行恢复(线程级) — Claude Code 从会话记录最后一条 GS-XX 推断(会话级) — Hermes 从 state_meta 恢复 · 跨 gateway · 跨日 · 跨平台 — 三种"持久化粒度"一直在抬:thread → session → agent CONTINUITY ✓ resumed @ turn 9 / 12
scene 1/5 · 0.0s ARRIVE
Hermes · gateway map · 一台 Agent · 十几个入口
18 / 26
cross-gateway continuity · agent_id is constant

/goal 不是某个终端的事 ——
是 Agent 自己跨入口的事.

CLI

terminal

TELEGRAM

@hermes_bot

DISCORD

bot/dm

SLACK

slash/cmd

MATRIX

e2e bot

★ AGENT

hermes
agent_id

SIGNAL

link bot

WHATSAPP

business api

SMS / iMSG

phone num

WEBHOOK

POST /event

REST API

SDK / curl

WEB DASH

browser ui

1agent
命令面变小 · 入口面变大
Codex / Claude Code 在补"会话内"的命令丰富度。Hermes 把这块留薄,把入口的丰富度抬出来。
Hermes · 三件事都对"现实"很现实
19 / 26
three pragmatic choices

可靠性
> 严格.

Hermes 在 judge · budget · reason 这三件事上,都挑了"接得住"的那一边
JUDGE · FAIL-OPEN

judge 挂了就当 unmet

"不让评估器自己挂掉就把整个过程打断"。不挂死整段流程,继续下一轮。

TURN BUDGET · 兜底

真正止损的是预算,不是 judge

默认 turn budget 写得很短。可以续,但不能无限续。

USER-VISIBLE REASON

卡哪里 · 让用户看见

budget 用完不是默默退出,是把 reason 推回 origin gateway给用户:要不要续 / 改 / 停。

三家对比矩阵 · 抽走的那一格 / 持久化粒度 / 谁判完成 / 兜底
20 / 26
side-by-side · 接口稳了 · 实现各做各

同一个接口,
不同的产品决定.

CODEX
CLAUDE CODE
HERMES
抽走的格
目标状态
停止判断
跨会话目标
持久化粒度
线程级 · SQLite
thread_goals 一行
会话级
activeGoal + hook
Agent 级
SessionDB.state_meta
谁判完成
模型 + schema-lock
enum["complete"]
第三方 evaluator
haiku · 投票员
goal_judge subagent
{met,reason}
兜底机制
token_budget
→ budget_limited
evaluator 反喂 reason
到下一轮 prefix
turn budget
+ user-visible reason
入口面
CLI / IDE 内
CLI / IDE 内
CLI · Telegram · Discord · Slack · Matrix · Signal · WhatsApp · SMS / iMessage · Webhook · API · Web Dashboard
核心观点 · runtime interface
21 / 26
from a prompt line · into a runtime contract

Goal 在从
prompt 里的一句话,
慢慢变成Harness 的运行时接口.

BEFORE · prompt-level

Goal = "一句话"

  • ✕ 跨 turn 容易漂
  • ✕ 没有状态、没有预算
  • ✕ "完成"靠模型自觉
  • ✕ 上下文压缩就丢
  • ✕ 多 Agent / 多入口接不住
AFTER · runtime interface

Goal = 运行时任务合同

  • ✓ 有状态 · 有 schema
  • ✓ 有预算 · 有 budget exhaustion
  • ✓ "完成"由独立闸门判
  • ✓ 跨上下文 / 会话 / 入口恢复
  • ✓ "可以继续,但不能无限继续"
值得看的 不是表里那点功能差异,不是谁多一个字段。三家把同一个方向推到了台前:Agent 的下一步,不只是更会执行,而是更能在工程约束里执行
一个能用的 Goal · 至少要交代五件事
22 / 26
a checklist · "task contract"

把 goal 从一句愿望,
压成一份任务合同.

① · SCOPE

范围

这次到底做哪个目录 · 哪个模块 · 哪条用户路径。范围越糊,模型越容易"顺手优化"。

② · INVARIANTS

不变量

哪些 public API 不能改。 哪些测试不能为了让它过就删断言。模型很擅长用局部成功换全局风险。

③ · EVIDENCE

验证证据

不是"测试通过"四个字。要写哪条命令 · 哪段日志 · 哪个 diff · 哪个 CI 状态能把"通过"钉死。

④ · BUDGET

预算边界

最多 N 轮 · N 分钟 · N token。到点不是再硬跑,是停掉新增、把现场整理一遍。

⑤ · WRAP-UP

收束输出

哪怕没做完,也要留下已完成项 / 未完成项 / 失败命令 / 当前假设 / 下一步建议。失败也得能被接得住。

★ EXAMPLE

一句愿望 vs 一份合同

"补完整 auth 测试" → 五字段版:范围 src/auth · 不变量 不动 public · 验证 npm test test/auth · 预算 20 轮 · 收束 fail/pass 都给清单

几条公开讨论 · 都在指向同一件事
23 / 26
public discussions converging

不是突然冒出来的 ——
四条线都在逼近同一个工程问题.

@GeoffreyHuntley

Ralph Loop · self-healing

"循环本身不是目的,目标、反馈信号和验证才是。" 让 Agent 自动测试 + 修认证 + 验证。

@thetreygoff

Ralph Loop on Codex hooks

"防 early exits"。让 agent 把任务老实做完,省得人一直盯着。Codex 0.128 内置 ralph loop++。

@tobi · Shopify

context engineering > prompt

"关键能力是把任务所需的上下文,提供到 LLM 有可能把它解出来的程度。" 这上下文也包括目标、状态、预算、验证。

karpathy

autoresearch

让 Agent 在小训练 loop 里自主实验 · 5 分钟一轮 · 看 val_bpb 改没改善。自主循环装进可度量、可回滚框架。

跟 Skills 走的是同一条路
24 / 26
skills · then · /goal · now

Skill 怎么走过来的,
Goal 现在差不多就在那一步.

STAGE 01

Skills 早期

一个 Markdown 文件,写下"这类事怎么做"

→ Goal: prompt 里一句"请持续努力"
STAGE 02

Skills 中期

SKILL.md + Hooks + AGENTS.md + 子代理。"过程资产" 浮现。

→ Goal: Ralph Loop 这类外部循环
STAGE 03

Skills 现在

可分发的工作流包 · 带依赖声明 · 仓库工程化。

→ Goal: 命令 + 状态 + 评估 + 预算
★ STAGE 04 · NEXT

/goal 现在长这样

从一句指令,长成运行时任务合同:有状态、有 schema、有 budget、有恢复。

→ 这两条线最后会汇到一起
"自主" ≠ "放任" · Harness 工程化流水线
25 / 26
autonomy through scaffolding · not despite it

要让 Agent 更自主,
系统才要把护栏立起来.

SLOT 01

AGENTS.md
CLAUDE.md

仓库规则 · 持续生效的行为契约

SLOT 02

Skills

这类任务的固定流程 · 过程资产

★ SLOT 03

/goal

当前目标 + 停止条件 + 预算

SLOT 04

Hooks

关键事件上的确定性检查

SLOT 05

Memory
+ Subagents

阶段性事实 · 探索隔离

SLOT 06

CI

最终交付的外部验证

收尾
26 / 26
CLOSING · 最后一句

Goal 要进
运行时,
不要只进 记忆.

真正的 /goal,不是给提示词后面加一句"请持续努力"。它必须能调度 · 能恢复 · 能判断完成 · 能跨入口接得住。
AGENT GOAL COMMAND RESEARCH 2026 · 05 · 14
01

认接口,不认实现

Codex 状态机 · Claude evaluator · Hermes 跨入口 —— 三种实现,同一个接口。先认接口,再谈谁好谁坏。

02

把 goal 写成合同

范围 / 不变量 / 验证证据 / 预算 / 收束输出。少一件,长任务都接不住。

03

自主装进工程结构

不是 Agent 更自主,是系统开始把自主性装进可观察、可回滚、可接管的工程结构里。

如果只记一个判断
/goal 的价值不是"存一句目标",而是把长期目标变成系统能调度、能恢复、能审计、能跨入口接住的运行时能力。