KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

01 · 定位与七种运行宿主 — keel 龙骨

Hermes Agent 0.20.4 出自 Nous Research,是一个纯 Python 的 agent 内核,外面套着 CLI、消息网关、HTTP API、编辑器协议、定时器、批处理与桌面 GUI 共七张壳。它的自我定位写得很直白——「self-improving AI agent」,卖点不是「能调工具」,而是「会自己长出技能」。

Hermes Agent 0.20.4 出自 Nous Research,是一个纯 Python 的 agent 内核,外面套着 CLI、消息网关、HTTP API、编辑器协议、定时器、批处理与桌面 GUI 共七张壳。它的自我定位写得很直白——「self-improving AI agent」,卖点不是「能调工具」,而是「会自己长出技能」。

这一章先把「它自称是什么」和「它实际长什么样」对齐,然后回答三个问题:

  1. 顶层目录被切成了哪几层,每一层的职责边界在哪;
  2. hermes 这一个命令如何扇出到 50+ 子命令与七种宿主;
  3. 容器镜像里为什么不是「一个进程」,而是一棵 s6-overlay 监督树。

阅读约定:本章所有路径都相对于仓库根。注意本机这份源码的实际根目录是 hermes-agent/hermes-agent-new/(外层 hermes-agent/ 只是一个不完整的父目录),下文一律省略该前缀。

一、它自称是什么

pyproject.toml:6 的 description 是完整的一句话定位:

version = "0.20.4"
description = "The self-improving AI agent — creates skills from experience, improves them during use, and runs anywhere"
authors = [{ name = "Nous Research" }]

README.md:19 把这句拆成五个可验证的能力声明:内置学习闭环(built-in learning loop)、使用中自我改进、主动 nudge 自己沉淀知识、检索自己过去的会话、跨会话建立对你的模型。同一段还给了部署姿态:$5 VPS、GPU 集群,或者闲置时几乎不花钱的 serverless;「不绑在你的笔记本上——它在云 VM 上干活时你可以从 Telegram 跟它对话」。

README.md:26 的表格行是这几条声明对应的实现清单:FTS5 会话检索 + LLM 摘要、Honcho 式辩证法(dialectic)用户建模、兼容 agentskills.io 开放标准。这三项在第 04 章展开。

需要当心一个易混点:README.md:29 列的「Seven terminal backends」(local / Docker / SSH / Singularity / Modal / Daytona / Vercel Sandbox)说的是终端后端,不是本章要讲的运行宿主。前者是「shell 命令在哪执行」,后者是「整个 agent 挂在什么进程模型上」。二者数量都常被凑成「七」,但所指完全不同。

二、顶层目录的三层切分

顶层目录可以按「内核 / 宿主 / 扩展」三层理解:

关键点:扩展不是可选的装饰,而是主路径。36 个模型供应商、22 个渠道全部以插件形式存在(第 05 章),内核只提供协议与注册表。AGENTS.md:73 把这条讲成一句能力梯度:CLI command + skill → service-gated tool(check_fn)→ plugin → MCP server。

三个根级文件需要单独认位置,因为它们各自被不同层引用:

三、入口:hermes 与 50+ 子命令

pyproject.toml:372 声明了三个 console script:

[project.scripts]
hermes = "hermes_cli.main:main"
hermes-agent = "run_agent:main"
hermes-acp = "acp_adapter.entry:main"

hermes 是主入口。hermes_cli/main.py:11433 的 _BUILTIN_SUBCOMMANDS 是一个 frozenset,一次性列出全部内置子命令:acp / approvals / auth / backup / chat / config / console / cron / curator / dashboard / serve / debug / doctor / egress / gateway / gui / hooks / kanban / mcp / memory / model / plugins / profile / project / proxy / resume / send / sessions / setup / skills / status / sync / tools / update / webhook / whatsapp-cloud / secrets / security / verify …共 70 余项。

这份集合有一个非显然的用途:_plugin_cli_discovery_needed()(hermes_cli/main.py:11519)用它做快速路径。若首个位置参数命中内置集合,就跳过插件发现——省掉约 500–650 ms 的 argparse 期开销;注释写明了代价:「插件命令不出现在顶层 --help 里,是可以接受的交换」。

子命令的解析器实现集中在 hermes_cli/subcommands/,共 47 个 .py(45 个子命令模块,另有 __init__.py 与 _shared.py),命名统一为 build_<group>_parser,例如 hermes_cli/subcommands/skills.py:12: def build_skills_parser(subparsers, *, cmd_skills)。

四、七种宿主

同一个 AIAgent 被七种不同的进程模型承载。按「谁拥有主循环、谁来喂 user message」区分:

  1. 交互 CLI / TUI。hermes_cli.main:main()。是否进 TUI 由 _wants_tui_early()(hermes_cli/main.py:311)在 argparse 之前判断,main.py:352 据此分流。会话续跑 hermes -c/--continue 由 _resolve_continue_arg()(hermes_cli/main.py:1823)解析。
  2. 消息网关。gateway/run.py:30576: def main() 启动,核心是 gateway/run.py:6522: class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, GatewaySlashCommandsMixin)——多继承已经把「鉴权 / 看板观察 / 斜杠命令」三块拆开。
  3. HTTP API。hermes serve 起一个 OpenAI 兼容服务,实现是 gateway/platforms/api_server.py:1352: class APIServerAdapter(BasePlatformAdapter);文件头注释写「OpenAI-compatible API server platform adapter」,可接 Open WebUI / LobeChat / LibreChat,鉴权用 API_SERVER_KEY。
  4. ACP 适配器。acp_adapter/entry.py:220: main();它强制「stdout 专供 JSON-RPC、日志走 stderr」(_setup_logging() 把 root handler 挂到 sys.stderr 并套 RedactingFormatter),最终交给 acp_adapter/server.py:566: class HermesACPAgent(acp.Agent)。
  5. cron。cron/scheduler.py,配合 cron/jobs.py / cron/executions.py / cron/monitor.py 构成一套带执行记录与生命周期守卫的调度器。
  6. 批处理。batch_runner.py:1156: def main() + batch_runner.py:529: class BatchRunner,用于轨迹生成与离线评测。
  7. dashboard / kanban dispatcher。hermes dashboard 子命令(hermes_cli/subcommands/dashboard.py)+ gateway/kanban_watchers.py,供 GUI 与多 agent 看板协作使用。这一项与其它六种的「独立宿主」性质略有差别——它更接近「网关的一个附加角色」(推断)。

五、容器与 s6 多进程监督

docker-compose.yml 定义两个 service,都跑同一个镜像 hermes-agent:

services:
  gateway:
    image: hermes-agent
    network_mode: host
    volumes:
      - ~/.hermes:/opt/data
    command: ["gateway", "run"]
  dashboard:
    image: hermes-agent
    network_mode: host
    command: ["dashboard", "--host", "127.0.0.1", "--no-open"]

三个细节值得记:network_mode: host(不是端口映射,容器直接共享宿主网络栈);~/.hermes 挂到 /opt/data(宿主配置与容器内数据是同一份);dashboard 默认只绑 127.0.0.1,注释明确写了「它存 API key,无鉴权暴露到 LAN 是不安全的,请走 SSH 隧道或加反代,不要 --insecure --host 0.0.0.0」。

多进程监督来自 s6-overlay 3.2.3.0(Dockerfile:108)。Dockerfile:456 的 ENTRYPOINT [ "/opt/hermes/docker/entrypoint-dispatch.sh" ] 是一个小派发器,/init(s6-overlay 的 PID 1 = s6-svscan)才是真正的监督根。服务定义在 docker/s6-rc.d/:main-hermes 与 dashboard 两个服务目录,各自带 run / type / dependencies.d/base。docker-compose.yml:9 的注释解释了 stage2 hook 的职责:按 HERMES_UID / HERMES_GID 用 usermod/groupmod 重映射内部 hermes 用户,随后每个受监督服务用 s6-setuidgid 掉权运行。

六、TUI 为什么拆成前后端

TUI 宿主是唯一一个被拆成两个顶层目录的:tui_gateway/(Python 侧)与 ui-tui/(前端侧)。tui_gateway/ 有 26 个模块,形状是一个 RPC 服务:server.py 是入口,methods_prompt.py / methods_session.py / methods_tools.py / methods_complete.py / methods_images.py / methods_config.py / methods_profiles.py 是按域切分的方法实现,transport.py 与 ws.py 负责传输,event_publisher.py 负责把内核回调转成前端事件,slash_worker.py / slash_fuzzy.py 处理斜杠命令与其模糊匹配。

ui-tui/ 侧是独立的 npm 包(ui-tui/package.json),并带一个自研渲染包 ui-tui/packages/hermes-ink/package.json——「ink」这个名字表明它是终端 React 风格渲染器。

拆开的原因在 AGENTS.md:537 里有一段旁证:tui_gateway/server.py 同时提供 commands.catalog(空查询时的命令清单)与 complete.slash(输入时的补全),两者都包含内置命令、用户 quick_commands、以及技能派生的命令(scan_skill_commands() / get_skill_commands());注释明确写「The desktop app does not need a new RPC to see skills」——也就是说这套 RPC 同时服务 TUI 与桌面 app,把「命令从哪来」收敛到后端一处。

代码地图

机制 位置 要点
自我定位 pyproject.toml: description 第 6 行;"self-improving AI agent — creates skills from experience, improves them during use, and runs anywhere"
版本与作者 pyproject.toml: version 0.20.4;Nous Research;requires-python = ">=3.11,<3.14"
学习闭环卖点 README.md 第 19 / 26 行 跨会话用户建模、FTS5 会话检索、Honcho dialectic、agentskills.io 兼容
内核入口类 run_agent.py: AIAgent 第 412 行;主循环与工具执行都在此类的实例上
主入口 pyproject.toml: [project.scripts] hermes = hermes_cli.main:main;另有 hermes-agent、hermes-acp
子命令全集 hermes_cli/main.py: _BUILTIN_SUBCOMMANDS 第 11433 行;70 余项 frozenset,main.py:11530 用它跳过插件发现
子命令解析器 hermes_cli/subcommands/skills.py: build_skills_parser 第 12 行;subcommands/ 共 47 个 .py,统一 build_<group>_parser 命名
TUI 早分流 hermes_cli/main.py: _wants_tui_early 第 311 行;argparse 之前判断是否走 TUI
会话续跑参数 hermes_cli/main.py: _resolve_continue_arg 第 1823 行;-c / --continue 支持可选会话名
网关宿主 gateway/run.py: GatewayRunner 第 6522 行;三 mixin 组合鉴权 / 看板 / 斜杠命令;main() 在第 30576 行
HTTP 宿主 gateway/platforms/api_server.py: APIServerAdapter 第 1352 行;OpenAI 兼容,API_SERVER_KEY 鉴权
ACP 宿主 acp_adapter/entry.py: main 第 220 行;stdout 留给 JSON-RPC,日志强制走 stderr
ACP Agent acp_adapter/server.py: HermesACPAgent 第 566 行;继承 acp.Agent
批处理宿主 batch_runner.py: BatchRunner 第 529 行;main() 在第 1156 行
定时宿主 cron/scheduler.py 配合 jobs.py / executions.py / monitor.py / lifecycle_guard.py
工具清单组装 model_tools.py: get_tool_definitions 第 323 行;调用分发 handle_function_call 在同文件 :1192
TUI 后端 RPC tui_gateway/server.py commands.catalog 与 complete.slash 两个方法(见 AGENTS.md:537),同时服务 TUI 与桌面 app
TUI 前端 ui-tui/package.json 独立 npm 包,含渲染包 ui-tui/packages/hermes-ink/package.json
容器编排 docker-compose.yml: services gateway 与 dashboard 两 service;network_mode: host;~/.hermes:/opt/data
多进程监督 Dockerfile: ENTRYPOINT 第 456 行;派发到 /init(s6-overlay PID 1),服务定义见 docker/s6-rc.d/

关键取舍

把 provider 与 channel 全部外置成插件,代价是启动期要做发现与注册。
内核只认识协议(ProviderProfile、BasePlatformAdapter)和注册表(_REGISTRY、PlatformRegistry),36 个 provider / 22 个渠道都是插件。收益是新增一家供应商不需要改内核;代价是「有哪些能力可用」在静态阅读时看不出来,必须跑一次发现流程,且 CLI 启动为此引入了可观开销——这正是 _plugin_cli_discovery_needed() 需要存在的原因。

用 frozenset 硬编码内置子命令来换启动速度,代价是「插件命令不出现在 --help」。
_BUILTIN_SUBCOMMANDS 是手写清单,与 subcommands/ 目录存在耦合:新增内置子命令若忘了登记,最坏后果是每次调用都多做一次插件发现(注释明确说这是「correctness-safe」方向)。这是一个用可维护性换冷启动延迟的典型取舍。

容器用 host 网络而不是端口映射,代价是隔离面变小。
network_mode: host 让容器与宿主共享网络命名空间——省掉了端口映射与 NAT,也让 gateway 能直接监听平台回调端口;但容器内的网络策略此时约等于宿主策略,无法靠 Docker 网络规则约束 egress。文件里对 dashboard 的反复告警(只绑 127.0.0.1、走 SSH 隧道)正是对这个松弛面的补偿。

镜像用 s6-overlay 而不是「一个 entrypoint + 一个进程」,代价是启动链变长。
/init → stage2 hook(改 UID/GID、reconcile profile、决定 dashboard 是否启用)→ s6-svscan → 各服务 run 脚本。任何绕过 /init 的 entrypoint 覆盖都会跳过全部 cont-init 准备,gateway 直接不可用——docker-compose.yml:18 把这条写成了硬约束。换来的是 gateway、dashboard、per-profile gateway 能在同一个容器里被独立重启与监督。

外层目录与仓库根分离,代价是路径引用必须小心。
本机这份源码真实根是 hermes-agent/hermes-agent-new/,而 hermes-agent/ 自身含有 gateway/、cron/、hermes_state.py 等条目但缺少 agent/ 与 run_agent.py。所有「路径:行号」引用都以 hermes-agent-new/ 为基准解析,否则会指向不存在或错位的文件。

自测题

  1. README.md 说的「七个终端后端」和本章说的「七种运行宿主」为什么不是同一回事?请各举两个例子,并说明如果一个 agent 同时有多个宿主,终端后端应当属于宿主层还是内核层。
  2. _wants_tui_early() 在 argparse 之前运行,而 _plugin_cli_discovery_needed() 在 argparse 期运行。请说明为什么这两件事不能合并成一次判断,以及「插件命令不进 --help」这个代价具体体现在哪个调用路径上。
  3. docker-compose.yml 里 gateway 用 command: ["gateway","run"] 而 dashboard 用 ["dashboard","--host","127.0.0.1","--no-open"]。如果不小心把 ENTRYPOINT 覆盖成 ["gateway","run"],容器会怎样失败?请从 /init 的职责推断。
  4. 本章列出的第 7 种宿主(dashboard / kanban dispatcher)为什么被标注为「推断」?请从代码结构上找一个理由说明它和其它六种的差异。
  5. 内核把 provider 与渠道外置成插件。请指出这种设计在「排查『某个能力为什么没生效』」时带来的具体困难,并说明 check_fn / requires_env 这类声明在这个问题上的作用。

进入 keel 阅读