Hermes Agent Goal

Hermes Agent /goal 的原理机制与实现方式 #

基于 Hermes Agent 开源仓库源码深度分析。核心实现位于 hermes_cli/goals.py(~535 行),CLI 集成在 cli.py(~6500-7200 行),Gateway 集成在 gateway/run.py(~7479-7672 行)。

一、整体定位 #

Hermes Agent 的 /goal 是一个跨轮次持久化目标系统——它给 Agent 设定一个"站着的目标"(standing goal),然后让 Agent 在多个对话轮次中自动持续工作,直到目标达成、预算耗尽、或用户手动暂停/清除。

文档中的原话:

It’s our take on the Ralph loop, directly inspired by Codex CLI 0.128.0’s /goal by Eric Traut (OpenAI). The core idea — keep a goal alive across turns and don’t stop until it’s achieved — is theirs. The implementation here is independent and adapted to Hermes’ architecture.

一句话概括:用户设定目标 → 每轮结束后调用辅助模型(judge)判断是否完成 → 未完成则自动注入 continuation prompt 触发下一轮 → 循环直到 done / budget exhausted / user intervention。

二、整体架构 #

┌──────────────────────────────────────────────────────────────────┐
│                        User Interface                             │
│  CLI: _handle_goal_command() + _pending_input.put()              │
│  Gateway: _handle_goal_command() + _enqueue_fifo()               │
└──────────────────────┬───────────────────────────────────────────┘
                       │
            ┌──────────▼──────────┐
            │    GoalManager       │
            │  (hermes_cli/goals.py)│
            │                      │
            │  set/pause/resume/   │
            │  clear/status/       │
            │  evaluate_after_turn │
            │  next_continuation   │
            └──┬──────┬──────┬─────┘
               │      │      │
     ┌─────────▼┐ ┌───▼────┐ │
     │SessionDB │ │ Judge  │ │
     │state_meta│ │(aux    │ │
     │goal:<sid>│ │ model) │ │
     └──────────┘ └────────┘ │
                             │
              ┌──────────────▼──────────────┐
              │  Continuation Prompt → FIFO  │
              │  (_pending_input / adapter)  │
              └─────────────────────────────┘

三、核心模块拆解 #

3.1 GoalState — 数据模型 #

定义在 hermes_cli/goals.py,是一个可序列化的 dataclass:

@dataclass
class GoalState:
    goal: str                        # 目标文本
    status: str = "active"           # active | paused | done | cleared
    turns_used: int = 0              # 已消耗轮次
    max_turns: int = 20              # 轮次预算上限(可配置)
    created_at: float = 0.0          # 创建时间戳
    last_turn_at: float = 0.0        # 最后一轮时间戳
    last_verdict: Optional[str] = None  # "done" | "continue" | "skipped"
    last_reason: Optional[str] = None   # judge 的判据理由
    paused_reason: Optional[str] = None # 暂停原因

状态机

              set()                  pause()
  [none] ──────────► active ──────────► paused
                         │                │
                         │                │ resume(reset_budget=True)
                         │                │
                         │                ▼
                    evaluate_after_turn  active
                         │
            ┌────────────┼────────────┐
            │            │            │
        done=true   turns>=max   user clears
            │            │            │
            ▼            ▼            ▼
          done        paused       cleared

3.2 持久化层 — SessionDB #

Goal 状态通过 SessionDB.state_meta 表持久化,key 为 goal:<session_id>

def _meta_key(session_id: str) -> str:
    return f"goal:{session_id}"

def save_goal(session_id: str, state: GoalState) -> None:
    db = _get_session_db()
    db.set_meta(_meta_key(session_id), state.to_json())

def load_goal(session_id: str) -> Optional[GoalState]:
    db = _get_session_db()
    raw = db.get_meta(_meta_key(session_id))
    return GoalState.from_json(raw) if raw else None

关键设计:DB 实例按 HERMES_HOME 路径缓存(_DB_CACHE),避免每次调用都重新打开 SQLite 文件。

持久化的意义/resume 能恢复之前设置的目标——即使关闭终端、第二天回来,goal 状态(active/paused/done)依然完好。

3.3 Judge 机制 — 核心判断引擎 #

这是整个 /goal灵魂。定义在 hermes_cli/goals.pyjudge_goal() 函数:

def judge_goal(goal: str, last_response: str, *, timeout=30.0) -> Tuple[str, str]:
    """Ask the auxiliary model whether the goal is satisfied.
    
    Returns (verdict, reason) where verdict is "done", "continue", or "skipped".
    
    Deliberately fail-open: any error returns ("continue", "...")
    so a broken judge doesn't wedge progress — the turn budget is the backstop.
    """

Judge 的 System Prompt(硬编码在源码中):

You are a strict judge evaluating whether an autonomous agent has 
achieved a user's stated goal. You receive the goal text and the 
agent's most recent response. Your only job is to decide whether 
the goal is fully satisfied based on that response.

A goal is DONE only when:
- The response explicitly confirms the goal was completed, OR
- The response clearly shows the final deliverable was produced, OR
- The response explains the goal is unachievable / blocked / needs 
  user input (treat this as DONE with reason describing the block).

Otherwise the goal is NOT done — CONTINUE.

Reply ONLY with a single JSON object on one line:
{"done": <true|false>, "reason": "<one-sentence rationale>"}

Judge 的 User Prompt(模板):

Goal:
{goal}

Agent's most recent response:
{response}

Is the goal satisfied?

调用方式

client, model = get_text_auxiliary_client("goal_judge")
resp = client.chat.completions.create(
    model=model,
    messages=[
        {"role": "system", "content": JUDGE_SYSTEM_PROMPT},
        {"role": "user", "content": prompt},
    ],
    temperature=0,
    max_tokens=200,
    timeout=timeout,
)

关键设计决策

设计点 说明
Fail-open 语义 Judge 出错(网络故障、JSON 解析失败、模型不可用)时返回 ("continue", "..."),不会卡住进度。真正的安全网是 turn budget。
保守判断 只有明确完成或确认受阻才标记 done,其他情况一律 continue。这使得 false positive(误判完成)比 false negative(误判未完成)更罕见。
辅助模型 使用独立的 goal_judge auxiliary task,可配置为便宜快速的模型(如 gemini-3-flash),每次调用仅 ~200 输出 token。
截断保护 goal 文本截断到 2000 字符,response 截断到 4000 字符,避免上下文爆炸。
JSON 解析容错 先尝试整体解析,失败后用正则提取第一个 JSON 对象,支持 markdown code fence 包裹。

3.4 GoalManager — 编排引擎 #

GoalManager 是 CLI 和 Gateway 共同使用的核心编排类:

class GoalManager:
    def __init__(self, session_id: str, *, default_max_turns=20):
        self.session_id = session_id
        self.default_max_turns = default_max_turns
        self._state = load_goal(session_id)

核心方法

方法 作用
set(goal) 设置新目标,创建 GoalState,status=active,turns_used=0
pause(reason) 暂停,status=paused,记录原因
resume(reset_budget=True) 恢复,status=active,重置 turns_used=0
clear() 清除,status=cleared
mark_done(reason) 标记完成
status_line() 生成状态行(用于 CLI/Gateway 显示)
evaluate_after_turn(last_response) 核心入口:调用 judge、更新状态、返回决策
next_continuation_prompt() 生成 continuation prompt 文本

evaluate_after_turn 的决策逻辑(最关键的函数):

def evaluate_after_turn(self, last_response, *, user_initiated=True):
    # 1. 检查是否有 active goal
    if state is None or state.status != "active":
        return {"status": None, "should_continue": False, ...}
    
    # 2. 计轮
    state.turns_used += 1
    state.last_turn_at = time.time()
    
    # 3. 调用 judge
    verdict, reason = judge_goal(state.goal, last_response)
    state.last_verdict = verdict
    state.last_reason = reason
    
    # 4. 分支决策
    if verdict == "done":
        # → 目标达成!
        state.status = "done"
        return {"should_continue": False, "verdict": "done", 
                "message": f"✓ Goal achieved: {reason}"}
    
    if state.turns_used >= state.max_turns:
        # → 预算耗尽!
        state.status = "paused"
        state.paused_reason = f"turn budget exhausted ({state.turns_used}/{state.max_turns})"
        return {"should_continue": False, "verdict": "continue",
                "message": f"⏸ Goal paused — {state.turns_used}/{state.max_turns} turns used."}
    
    # → 继续!
    return {"should_continue": True, "verdict": "continue",
            "continuation_prompt": self.next_continuation_prompt(),
            "message": f"↻ Continuing toward goal ({state.turns_used}/{state.max_turns}): {reason}"}

3.5 Continuation Prompt — 自动续跑注入 #

当 judge 判断需要继续时,系统自动生成一个 continuation prompt 并注入到对话队列中:

CONTINUATION_PROMPT_TEMPLATE = (
    "[Continuing toward your standing goal]\n"
    "Goal: {goal}\n\n"
    "Continue working toward this goal. Take the next concrete step. "
    "If you believe the goal is complete, state so explicitly and stop. "
    "If you are blocked and need input from the user, say so clearly and stop."
)

关键设计

  • continuation prompt 只是一个普通的 user-role 消息
  • 不修改 system prompt,不切换 toolset,不触碰对话上下文
  • 因此 prompt caching 完全不受影响——20 轮 goal 运行的 cache 成本等同于 20 轮普通对话

3.6 CLI 集成 #

CLI 端的 Goal 生命周期:

# 1. 初始化(懒加载,缓存)
def _get_goal_manager(self):
    sid = self.session_id
    existing = getattr(self, "_goal_manager", None)
    if existing and existing.session_id == sid:
        return existing
    cfg = load_config()
    max_turns = cfg.get("goals", {}).get("max_turns", 20)
    mgr = GoalManager(session_id=sid, default_max_turns=max_turns)
    self._goal_manager = mgr
    return mgr

# 2. 处理 /goal 命令
def _handle_goal_command(self, cmd):
    parts = cmd.strip().split(None, 1)
    arg = parts[1].strip() if len(parts) > 1 else ""
    mgr = self._get_goal_manager()
    
    # /goal 或 /goal status → 显示状态
    if not arg or arg.lower() == "status":
        _cprint(mgr.status_line())
        return
    
    # /goal pause → 暂停
    if arg.lower() == "pause":
        mgr.pause(reason="user-paused")
        return
    
    # /goal resume → 恢复(重置计数器)
    if arg.lower() == "resume":
        mgr.resume()
        return
    
    # /goal clear/stop/done → 清除
    if arg.lower() in ("clear", "stop", "done"):
        mgr.clear()
        return
    
    # /goal <text> → 设置新目标,并立即触发第一轮
    state = mgr.set(arg)
    self._pending_input.put(state.goal)  # 注入队列,立即开始工作

# 3. 每轮结束后调用的 Hook
def _maybe_continue_goal_after_turn(self):
    mgr = self._get_goal_manager()
    if not mgr or not mgr.is_active():
        return
    
    # 如果用户已输入新消息,跳过判断(用户消息优先)
    if self._pending_input and not self._pending_input.empty():
        return
    
    # 提取 assistant 的最终回复
    last_response = extract_last_assistant_response(self.conversation_history)
    
    # 调用 judge
    decision = mgr.evaluate_after_turn(last_response, user_initiated=True)
    _cprint(decision.get("message", ""))
    
    # 如果需要继续,将 continuation prompt 注入队列
    if decision.get("should_continue"):
        self._pending_input.put(decision["continuation_prompt"])

3.7 Gateway 集成 #

Gateway 端(支持 Telegram/Discord/Slack/Matrix/Signal/WhatsApp 等所有平台)的实现逻辑相同,但使用了 adapter-FIFO 队列机制:

async def _handle_goal_command(self, event):
    # ... 解析子命令 ...
    
    # 设置新目标时,构造一个 MessageEvent 并放入 adapter FIFO
    kickoff_event = MessageEvent(
        text=state.goal,
        message_type=MessageType.TEXT,
        source=event.source,
    )
    self._enqueue_fifo(_quick_key, kickoff_event, adapter)

def _post_turn_goal_continuation(self, *, session_entry, source, final_response):
    """Turn 边界后的 Hook — 调用 judge 并可能注入 continuation。"""
    mgr = GoalManager(session_id=sid, default_max_turns=max_turns)
    if not mgr.is_active():
        return
    
    decision = mgr.evaluate_after_turn(final_response, user_initiated=True)
    
    # 将 judge 结果发送给用户
    if decision.get("message"):
        adapter.send_message(source, decision["message"])
    
    # 如果需要继续,通过 adapter FIFO 注入 continuation
    if decision.get("should_continue"):
        cont_event = MessageEvent(text=decision["continuation_prompt"], ...)
        self._enqueue_fifo(_quick_key, cont_event, adapter)

Gateway 的安全设计

  • Agent 运行中,/goal status/goal pause/goal clear 是安全的(只修改控制面状态)
  • /goal <new text> 在 Agent 运行中被拒绝,提示先 /stop,避免旧 continuation 与新目标竞争

3.8 用户消息优先机制 #

这是确保系统不会失控的关键设计:

CLI 端

# 如果 _pending_input 队列中已有用户消息,跳过判断
if self._pending_input and not self._pending_input.empty():
    return  # 用户消息优先

Gateway 端

# 通过 adapter FIFO 队列,用户消息天然排在 continuation 前面
self._enqueue_fifo(_quick_key, cont_event, adapter)

效果:任何用户发送的真实消息都会自动抢占 continuation loop,用户消息执行完后 judge 会再次运行——如果用户消息碰巧完成了目标,judge 会识别并停止循环。

四、完整生命周期流程图 #

用户: /goal "修复 tests/hermes_cli/ 所有失败测试"

  ⊙ Goal set (20-turn budget): 修复 tests/hermes_cli/ 所有失败测试
  
  [CLI/Gateway 将 goal 文本注入 _pending_input/FIFO]

  ┌─ Turn 1 ────────────────────────────────────────────────────┐
  │ Hermes: 运行测试...发现 5 个失败,修复了 2 个               │
  │ [轮次结束 Hook 触发]                                         │
  │ judge_goal(goal, last_response) → {"done": false, "reason":  │
  │   "only 2 of 5 failures fixed; 3 remain"}                   │
  │ → decision: should_continue=True                             │
  │ ↻ Continuing toward goal (1/20): only 2 of 5 fixed          │
  │ [continuation prompt 注入队列]                               │
  └──────────────────────────────────────────────────────────────┘

  ┌─ Turn 2 (continuation prompt 触发) ──────────────────────────┐
  │ Hermes: [Continuing toward your standing goal]...            │
  │ 修复了剩余 3 个测试                                           │
  │ [轮次结束 Hook 触发]                                         │
  │ judge_goal(goal, last_response) → {"done": true, "reason":   │
  │   "All 5 tests fixed, response confirms completion"}         │
  │ → decision: should_continue=False, verdict=done              │
  │ ✓ Goal achieved: All 5 tests fixed...                        │
  └──────────────────────────────────────────────────────────────┘

  用户: _ (空闲,等待新输入)

其他终止路径

路径 B(预算耗尽):
  ↻ Continuing (18/20) → ↻ (19/20) → ⏸ Goal paused — 20/20 turns used
  用户: /goal resume → 计数器重置为 0 → 继续

路径 C(用户干预):
  ↻ Continuing (3/20) → 用户发送 "等一下,换个思路"
  → 用户消息抢占,continuation 被丢弃
  → 新轮次结束后 judge 再次运行

路径 D(受阻确认):
  Hermes: "这个测试需要修改上游 API,我无法访问"
  judge → {"done": true, "reason": "goal is blocked, needs user input"}
  ✓ Goal achieved: goal is blocked, needs user input

五、配置方式 #

~/.hermes/config.yaml 中:

goals:
  # 最大续跑轮次,默认 20。调低用于 tighter loops,调高用于长期重构
  max_turns: 20

auxiliary:
  goal_judge:
    # 可选:将 judge 路由到便宜快速的模型
    provider: openrouter
    model: google/gemini-3-flash-preview

六、设计亮点与权衡 #

设计点 说明
Fail-open 语义 Judge 失败 → continue,而非 block。broken judge 不会卡住进度,turn budget 是最终安全网
零侵入性 continuation 只是普通 user 消息,不改 system prompt、不切 toolset、不碰 prompt cache
持久化 SessionDB.state_meta 持久化,/resume 可跨会话恢复
用户消息优先 真实用户消息天然抢占 continuation loop,不会丢失用户输入
保守判断策略 只有明确完成或确认受阻才 done,使 false positive 比 false negative 更罕见
双端统一 CLI(_pending_input)和 Gateway(adapter-FIFO)共享同一 GoalManager,逻辑一致
独立辅助模型 Judge 用独立 goal_judge auxiliary task,可配便宜模型,每次 ~200 token
JSON 解析容错 多层 fallback:直接解析 → 正则提取 JSON 对象 → 剥离 markdown fence

七、与 Codex /goal 的对比 #

维度 Codex /goal Hermes /goal
灵感来源 原创(Eric Traut, OpenAI) 受 Codex 启发的 Ralph loop 变体
Judge 机制 Agent 自身通过 update_goal tool 自我判断 独立的辅助模型(auxiliary client)做判断
续跑注入 Steering prompt(隐藏上下文,InternalModelContextFragment 普通 user 消息_pending_input / FIFO 队列)
持久化 SQLite thread_goals 表(独立表) SessionDB.state_meta KV 存储(goal:<session_id>
预算维度 Token budget + 挂钟时间 Turn budget(轮次计数)
完成审计 严格 completion audit + blocked audit(需 3 轮相同阻塞) Judge 单次判断(保守策略,blocked 也视为 done)
架构集成 Extension API(ThreadLifecycle/TurnLifecycle/ToolContributor) GoalManager 类 + CLI/Gateway 双端 Hook
语言 Rust Python

本质区别:Codex 把 goal 判断内嵌在 Agent 的工具调用中(Agent 自己调 update_goal),而 Hermes 用独立的外部 Judge 模型在每轮结束后做判断。这使得 Hermes 的实现更轻量——不需要修改 Agent 的工具集或 system prompt,但代价是 Judge 可能误判(通过保守策略和 turn budget 兜底)。

八、总结 #

Hermes Agent 的 /goal 是一个精心设计的自主持续工作模式,核心思想可以概括为:

设定目标 → 工作 → Judge 判断 → 未完成则自动续跑 → 循环直到 done/budget/intervention

它的巧妙之处在于:

  1. 极简实现——仅 ~535 行核心代码,零侵入性(不改 system prompt / toolset)
  2. 独立 Judge——用辅助模型做判断,避免 Agent 自我评估的 bias
  3. Fail-open——Judge 坏了也不卡进度,turn budget 兜底
  4. 用户优先——真实消息天然抢占 continuation loop
  5. 跨平台统一——CLI 和 Gateway 共享同一 GoalManager

它把一个"一次性问答"变成了"有明确目标、有轮次预算、有自动判断、能自主续跑的持久任务"。