定位: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:架构全景 — 四个核心角色及其关系。
如果只看官方文档,可以把整个流程理解成下面这样:
四个核心角色
| 角色 | 源码位置 | 一句话职责 |
| Gateway | gateway/run.py | 宿主机进程,承载 Dispatcher 和 Notifier |
| Dispatcher | kanban_db.py + gateway/kanban_watchers.py | 定期扫描 Board,回收过期任务、孵化 Worker |
| Kanban Board | kanban_db.py / ~/.hermes/kanban.db | SQLite 持久化状态,CAS 原子操作保证并发安全 |
| Worker | cli.py → _default_spawn() 子进程 | 通过 kanban_* 工具集与 Board 直接通信 |
关键:Gateway 不参与 Worker 的执行。 Dispatcher 只负责"把 Worker 孵出来"和"检测 Worker 是否还活着"。Worker 独立运行,通过共享 SQLite Board 与系统通信。Gateway 宕机不影响正在执行的 Worker。
状态机 — 9 种有效状态。
创建时只有两种初始状态:
- `running`:Task 创建后立即等待调度(Agent 自我创建的 Sub Task 常用)
- `blocked`:外部依赖未满足,先阻塞
注意:不是所有 Task 都经历 `todo → ready → running` 这一完整链条。Agent 创建的 Sub Task 通常直接 `running`,等待 Dispatcher 在下一个 Tick 领取。
Step 1: 创建 —— 写入 Board,与 Worker 解耦
创建时不指定执行者、执行时间、Profile。Task 只写入 SQLite 的 `tasks` 表,进入 `running` 或 `blocked`。
---💡 为什么? 创建者不需要知道谁会执行——这是"Task 与 Worker 解耦"的核心设计思想。后续所有调度逻辑由 Dispatcher 负责。
Step 2: 晋升 —— 从 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 的完整生命周期:
整个过程,Gateway 不参与 Worker 执行、Worker 通过共享 SQLite 与系统通信、Dispatcher 永不接触 LLM。
夜雨聆风
