乐于分享
好东西不私藏

Hermes 源码解析之一个 Kanban Task 是如何跑完整个生命周期的

Hermes 源码解析之一个 Kanban Task 是如何跑完整个生命周期的

定位:Hermes 源码解析系列 #1 本文基于 Hermes main 分支源码撰写。所有代码引用均标注文件名和行号。项目持续演进中,以 Hermes Agent 主仓库 为准。

零、为什么需要这篇文章?

很多人第一次接触 Hermes Kanban,觉得它和 Jira、Trello 很像——创建一个 Task,等 Agent 去完成。

但读完源码之后会发现,Hermes 的 Kanban 是一个无外部依赖的轻量级分布式任务调度系统。没有 Redis,没有 Celery,没有 RabbitMQ,却做到了:

  • 跨 Profile 的任务分发
  • 基于 Claim TTL 的故障恢复
  • 基于 Heartbeat 的活度检测
  • 基于 PID 的 Crash 检测
  • 基于 WAL + CAS 的原子调度

这篇文章将顺着源码,还原一个 Task 从创建到消亡的完整生命周期。我们关注的是全景,每一节的深度分析将留给系列后续文章。

一、整体架构
图 1:架构全景 — 四个核心角色及其关系。

如果只看官方文档,可以把整个流程理解成下面这样:

User
 │  创建 Task(通过 CLI / Dashboard / 其他 Agent)
 ▼
Gateway
 │  Dispatcher 每 60 秒扫描一次 Board
 ▼
Kanban Board (SQLite)
 │  Dispatcher 领取 (Claim) 并孵化 Worker
 ▼
Worker (hermes 子进程)
 │  执行任务 → 调用 LLM → 调用 Tool
 │  └── Heartbeat 持续刷新 claim_expires
 ▼
Complete → Board → Gateway Notifier(5s 轮询) → User

四个核心角色

角色源码位置一句话职责
Gatewaygateway/run.py宿主机进程,承载 Dispatcher 和 Notifier
Dispatcherkanban_db.py + gateway/kanban_watchers.py定期扫描 Board,回收过期任务、孵化 Worker
Kanban Boardkanban_db.py / ~/.hermes/kanban.dbSQLite 持久化状态,CAS 原子操作保证并发安全
Workercli.py_default_spawn() 子进程通过 kanban_* 工具集与 Board 直接通信
关键:Gateway 不参与 Worker 的执行。 Dispatcher 只负责"把 Worker 孵出来"和"检测 Worker 是否还活着"。Worker 独立运行,通过共享 SQLite Board 与系统通信。Gateway 宕机不影响正在执行的 Worker。
二、状态机:Task 的一生
状态机 — 9 种有效状态。

创建时只有两种初始状态:

- `running`:Task 创建后立即等待调度(Agent 自我创建的 Sub Task 常用)

- `blocked`:外部依赖未满足,先阻塞

注意:不是所有 Task 都经历 `todo → ready → running` 这一完整链条。Agent 创建的 Sub Task 通常直接 `running`,等待 Dispatcher 在下一个 Tick 领取。

三、Task 的旅程:8 步走完全程

Step 1: 创建 —— 写入 Board,与 Worker 解耦

创建时不指定执行者、执行时间、Profile。Task 只写入 SQLite 的 `tasks` 表,进入 `running` 或 `blocked`。

---💡 为什么? 创建者不需要知道谁会执行——这是"Task 与 Worker 解耦"的核心设计思想。后续所有调度逻辑由 Dispatcher 负责。

Step 2: 晋升 —— 从 todo 到 ready

`recompute_ready()` 扫描所有父任务已完成的 `todo` 任务,晋升为 `ready`。

--父任务依赖链条:`task_links` 表记录 parent→child 关系。只有所有父任务 `done` 或 `archived` 时才能晋升。

Step 3: 领取 —— Dispatcher 的原子 CAS

每个 Dispatcher Tick 从 `ready` 队列中取出待分配任务。

原子领取的核心——SQL UPDATE 即 Compare-And-Swap。

--💡 为什么用 SQLite CAS 而非分布式锁? Dispatcher 和 Worker 共享同一个文件系统。`UPDATE ... WHERE status='ready' AND claim_lock IS NULL` 本身就是一个原子 CAS——不需要 ZooKeeper 或 Redis。

Step 4: 孵化 —— 一条命令启动 Worker

领取成功后,Dispatcher 调用 `_default_spawn()`。

Worker 是一个纯后台子进程——无 stdin,stdout 写入 `logs/<task_id>.log`。

Worker 的环境变量注入了"身份护照"。

--💡 为什么用子进程而非线程或 RPC? 每个 Worker 需要完全隔离的 Hermes 环境——独立的 LLM 会话、独立的 Prompt Cache。子进程是操作系统提供的最轻量"真的隔离"边界,让每个 Worker 就像用户自己在命令行启动了 `hermes` 一样。

Step 5: 指导 —— KANBAN_GUIDANCE 注入

Worker 启动后,系统提示词被注入了操作手册:

```

# Kanban task execution protocol

1. Orient. Call kanban_show() first.

2. Work inside the workspace. cd $HERMES_KANBAN_WORKSPACE.

3. Heartbeat on long operations.

4. Block on genuine ambiguity.

5. Complete with structured handoff.

6. If follow-up work appears, create it; don't do it.

```

只有 Worker 进程拥有 `kanban_*` 工具集——普通 Hermes 会话看不到这些工具。

Step 6: 存活 —— Heartbeat 自动续期

Worker 长时间运行(编译、爬虫、训练)时,Dispatcher 需要知道它是否还活着。

```python

# kanban_db.py:3569-3597

def heartbeat_claim(conn, task_id, *, ttl_seconds=None, claimer=None) -> bool:

   expires = int(time.time()) + _resolve_claim_ttl_seconds(ttl_seconds)

   # UPDATE tasks SET claim_expires = ? WHERE id = ? AND status='running'

   # 续期成功返回 True,已被回收返回 False

```

默认 TTL = 15 分钟。 超过此时间无心跳,Dispatcher 会回收任务。

--🔄 重要变化(#31752): 早期版本 Worker 必须手动调用 heartbeat。最新源码中,LLM API 调用(含 Streaming)会自动刷新 `last_heartbeat_at`。大多数 Worker 不再需要显式 heartbeat,只有纯等待(编译、外部进程)才需要手动调用。

Step 7: 结束 —— Complete 触发连锁反应

Worker 完成任务后调用 `kanban_complete()`:

```python

# kanban_db.py:3978-4170

def complete_task(conn, task_id, *, result=None, summary=None, ...) -> bool:

   # 1. 状态 running → done

   # 2. 记录完成时间、summary、metadata

   # 3. 写入 completed 事件(Notifier 会读到)

   # 4. 清除连续失败计数

   # 5. 晋升子任务(如果有):recompute_ready()

   # 6. 清理工作目录

```

Enterprise 级别的细节:complete 时还会验证 Worker 声称创建的 Sub Task 是否真实存在——防止幻觉生成。虚假的卡 ID 会导致 `HallucinatedCardsError`。

Step 8: 通知 —— Notifier 5 秒轮询

Task 状态变化后,Notifier 在下一个 5 秒周期内发现事件,通过消息平台通知用户:

```

✔ Kanban t_deadbeefcafe done — Refactor auth module

⏸ Kanban t_feedcafe blocked: need API key

✖ Kanban t_badf00d worker crashed; will retry

```

四、Gateway 不参与 Worker 执行——最核心的设计

Gateway 与 Worker 的唯一交集就是 Kanban Board(SQLite DB)。Worker 不通过 Gateway 汇报结果,而是直接写入 DB。Gateway 的 Notifier 定期轮询 DB,发现变化后通知用户。

这意味着:

- Gateway 宕机 → Worker 不受影响,继续执行直到完成

- Worker 崩溃 → Dispatcher 在下一个 Tick 检测并回收

- Worker 可以在 Docker、SSH、Modal 上运行,Gateway 在本地

总结

一个 Kanban Task 的完整生命周期:

创建 Task → 写入 SQLite
 → recompute_ready (todo→ready)
 → Dispatcher 扫描 ready 队列
 → CAS claim_task (原子性领取)
 → _default_spawn (启动子进程)
 → Worker 执行:kanban_show → LLM Loop → kanban_complete
 → Notifier 发现事件 → 通知用户
整个过程,Gateway 不参与 Worker 执行、Worker 通过共享 SQLite 与系统通信、Dispatcher 永不接触 LLM。