开源自托管 Agent 项目 Hermes Agent 的仓库结构导读

2026-07-30

本文基于 hermes-agent 仓库 commit 2d40494(2026-07-29)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/NousResearch/hermes-agent 最新代码与文档为准。

NousResearch/hermes-agent(下称 Hermes Agent,一个自己装在自己机器上的开源 Agent 项目)这个仓库不要从 run_agent.py 读起,先认清一条边界:Python 进程拥有会话、工具、模型调用和斜杠命令逻辑,TypeScript 只拥有屏幕。 顶层那几十个条目看着杂,是因为它们不是按功能切的,是按「跑在哪个运行时里」切的——认出这条缝,四种技术栈各自的位置就都排得进去;认不出,你会在 cli.pyui-tui/apps/desktop/ 三处反复看到形似的「聊天界面」代码,然后开始怀疑自己读错了分支。

一、先把名字对上,再看它把什么装进了一个仓库

本文说的 Hermes 指 NousResearch/hermes-agent 这个 MIT 许可的开源 Agent 项目(LICENSE 署名 Nous Research),不是 Nous Research 同名的开源模型系列,也不是别处的同名商标与库。项目自己的开发指南 AGENTS.md 一句话定了性质:同一套 agent 内核跑在 CLI、消息网关、TUI 和一个 Electron 桌面应用上,跨会话记住东西,能派子 agent、跑定时任务、驱动真实终端和浏览器,主要通过插件和技能扩展而不是把内核养肥。

站内已有几篇相邻的文章,分工不同:pi 的仓库结构导读ECC 的仓库结构导读 拆的是另外两个项目,看懂一套遗留系统的通用方法 给的是不依赖具体项目的读码套路。本篇不重复方法论,只做一件事:把这个具体仓库的顶层目录、运行时边界和上手代价落到实处,写成你 clone 下来就能对照的东西。

值得先说清「不解决什么」。这不是安装教程,也不是功能清单——README.md 已经把安装一行命令和命令列表写完了,官方文档站还挂着完整的用户指南与架构章节,仓库里连中文 README(README.zh-CN.md)都自带。重复翻译这些没有价值。你真正会卡住的地方是:进来之后不知道谁调谁,改一处不知道会不会踩到项目自己定的红线。

二、四个运行时怎么排:一条 JSON-RPC 把界面和内核切开

package.json 里的 workspaces 字段是第一份地图:apps/*ui-tuiui-tui/packages/*webtests-js。这五项就是仓库里所有 JavaScript/TypeScript 的落脚点,其余全是 Python 侧。apps/ 下目前有 desktopsharedbootstrap-installer 三个成员。

Python 侧的入口在 pyproject.toml[project.scripts] 里写得明白:hermes 指向 hermes_cli.main:mainhermes-agent 指向 run_agent:mainhermes-acp 指向 acp_adapter.entry:main。也就是说日常敲的 hermes 走的是 hermes_cli/ 那套命令行编排,而不是直接进 agent 主循环。

真正把四个运行时缝起来的是一条换行分隔的 JSON-RPC。AGENTS.md 的进程模型写的是:hermes --tui 启动 Node 上的 Ink 界面,Ink 与 Python 的 tui_gateway 之间走 stdio 上的 JSON-RPC,请求由 Ink 发出、事件由 Python 推回,方法与事件的完整目录在 tui_gateway/server.py。这条线的分工原则是那句「TypeScript owns the screen」——会话、工具、模型调用、斜杠命令逻辑都在 Python 一侧。

桌面端是同一条协议的第二个消费者,但走的是另一条传输。apps/desktop/ 是 Electron + React 的独立聊天界面,它通过 requestGateway(method, params) 跟一个 tui_gateway 后端说话,WebSocket/JSON-RPC 的传输实现放在与框架无关的 apps/shared(包名 @hermes/shared,核心是 JsonRpcGatewayClient 和一组 WS URL 辅助函数),web/ 仪表盘也吃同一个包。桌面端并不嵌入 hermes --tui,它有自己的输入框、消息流和斜杠命令管线,所以「三处形似的聊天界面」不是错觉,是有意为之的三个独立界面。

仪表盘反过来:hermes dashboard/chat 页面嵌入的是真实的 hermes --tui,不是 React 重写版。浏览器里跑的是 xterm.js,服务端通过 hermes_cli/pty_bridge.pyhermes_cli/web_server.py 里的 /api/pty 端点把 PTY 的裸字节双向转发。项目在 AGENTS.md 里明确划线:不要在 React 里重新实现主聊天体验,围绕嵌入式 TUI 做侧栏、检查器、状态面板这类补充视图是允许的。

第四块是打包。flake.nix 用 flake-parts 组织,输入里挂着 pyproject-nixuv2nixnpm-lockfile-fix,实际的表达式拆在 nix/ 下(hermes-agent.nixpython.nixtui.nixweb.nixdesktop.nixnixosModules.nix 等)。Nix 的存在会反向约束 Python 侧的打包元数据:pyproject.toml 里那份 py-modules 顶层单文件模块清单,注释直接写了原因——没有它,uv2nix 的封闭 venv 里会缺 hermes_constantsrun_agent 这些顶层模块;gatewaypackage-data 同理,漏掉资源目录会让封闭 venv 静默丢掉网关的状态短语与配图。这类「一处漏了另一处静默降级」的耦合,是混栈仓库里最容易踩的一类。

三、顶层目录导读表

下面这张表按「你什么时候会碰到它」排,位置全部是仓库里的真实路径。

组成部分它负责什么对应仓库位置你什么时候会碰到它
会话主循环AIAgent 类,run_conversation() 里的同步循环,带中断检查与预算追踪run_agent.py改中断、迭代预算、消息序列
工具编排discover_builtin_tools()handle_function_call() 的分发层model_tools.py工具调用链出问题、插件钩子不触发
工具集清单TOOLSETS 字典与 _HERMES_CORE_TOOLS 默认捆绑toolsets.py工具注册了却没暴露给模型
工具实现每个文件顶层调 registry.register(),自动发现tools/tools/registry.py加工具、改工具行为
终端后端本地/容器/远端/沙箱等多种执行环境tools/environments/想让命令跑在别处而不是本机
会话存储SessionDB,SQLite 会话库与全文检索hermes_state.pyhermes_state_search.py会话恢复、跨会话检索
中文检索扩展CJK 双字组分词的 SQLite 扩展native/fts5_cjk/中文短词搜不到
消息网关各聊天平台适配器,单进程多平台gateway/gateway/platforms/接一个新的聊天平台
终端界面Ink(React)TUIui-tui/src/entry.tsxapp.tsxgatewayClient.ts改屏幕上的任何东西
TUI 后端JSON-RPC 方法与事件目录tui_gateway/server.py加一个 RPC 方法或事件
桌面端Electron + React 独立聊天面apps/desktop/apps/shared/改桌面端命令面板与状态
仪表盘浏览器 SPA,PTY 嵌入 TUIweb/hermes_cli/pty_bridge.py想在浏览器里用
技能默认加载的内置技能 / 显式安装的重型技能skills/optional-skills/写技能、审技能 PR
插件记忆后端、模型提供方、上下文引擎等plugins/加能力但不想动内核
MCP 目录随仓库附带的可选 MCP 接入项optional-mcps/想让 agent 连外部工具服务
定时任务任务存储与 tick 循环cron/无人值守的周期任务
编辑器接入ACP 服务端acp_adapter/从编辑器里驱动它
打包flake 与拆分表达式flake.nixnix/要可复现构建或 NixOS 部署
测试Python 与 JS 两套tests/tests-js/scripts/run_tests.sh任何改动之后
文档站文档源文件website/改用户可见的说明

几个能自己数出来的规模刻度,帮你判断该不该逐个读:skills/ 下 14 个分类目录合计 70 份 SKILL.mdoptional-skills/ 下 21 个分类目录合计 111 份,plugins/ 有 18 个顶层插件目录,optional-mcps/ 有 6 个,tests/ 里以 test_ 开头的测试文件有 2499 个。结论很直接:这四块不存在「读完」的选项,只能按需检索。真正需要通读的是 toolsets.py 这类清单文件——它短,而且决定了别的东西能不能被看见。

四、读代码的顺序,和它的「窄腰」设计取向

AGENTS.md 给了一条现成的依赖链,自下而上是:tools/registry.py(无依赖,被所有工具文件导入)→ tools/*.py(各自在导入时调 registry.register())→ model_tools.py(导入注册表并触发工具发现)→ run_agent.pycli.pybatch_runner.py 与各终端环境。

按这条链读,比按目录字母序读省事得多。registry.py 短,看完你就知道一个工具的 schema、handler、可用性检查函数是怎么绑在一起的;接着挑一两个 tools/ 下的具体工具对照,注册的形状立刻具体;再看 model_tools.py 怎么把它们收成一份发给模型的 schema 列表;最后才进 run_agent.py。反过来先啃主循环,你会在一堆参数和回调里失去坐标——AGENTS.md 自己都注明了 AIAgent.__init__ 的真实签名有约六十个参数,文档里只列了最小子集。

比读码顺序更值得先吸收的是这个项目的取向,因为它决定了你的改动会不会被接受。AGENTS.md 把两条性质摊在最前面:一是每个会话的提示缓存被当成不可侵犯的东西,任何在会话中途改动历史上下文、换工具集、重建系统提示的做法都会让缓存失效并直接放大用户成本,唯一的例外是上下文压缩;二是内核是一条窄腰,能力应该长在边缘——每加一个模型工具,它的 schema 就要在每一次 API 调用里被发送一遍,所以新增内核工具的门槛被刻意抬得很高。

围绕第二条,仓库给了一份自上而下六级的取舍阶梯,要求选能正确解决问题的最省足迹那一级:扩展已有代码 → CLI 子命令加一份技能 → 带前置条件门控的工具(只在配置齐备时才出现)→ 插件 → 进目录的 MCP 服务器 → 新的内核工具(最后手段)。这份阶梯不是文档辞令,plugins/ 下那些记忆后端、模型提供方、上下文引擎的目录,就是它的物化结果:每一类都是一个抽象基类加一个编排器,再加上每家一个目录。

理解这一点,你读仓库时的问题就会从「这个功能在哪」变成「这个功能被放在哪一级」。这两个问题的答案往往不在同一个目录里。

五、边界与代价:它放弃了什么,什么它明确不管

缓存优先换掉了「热改配置」的直觉。 会话中途不重载记忆、不重建系统提示、不换工具集,代价是很多设置的默认语义变成「下个会话生效」。项目要求会改动系统提示状态的斜杠命令必须缓存感知:默认延迟失效,需要立刻生效得显式加参数。你如果抱着「改完马上见效」的预期去用,会误判成功能没生效。

内核工具位是稀缺资源,很多能力被有意推到边缘。 好处是模型每轮看到的工具 schema不会无限膨胀,代价是你想要的能力可能不以工具形式存在,而是一条 CLI 子命令加一份技能,需要 agent 自己去跑。找不到某个「显然该有的工具」时,先去 skills/ 和 CLI 子命令里翻一遍,别急着下结论说它缺功能。

第三方产品别指望进主仓。 仓库写明了两条已生效的政策:plugins/memory/ 下内置记忆后端的集合已经封闭,新的记忆后端要作为独立插件仓库发布、由用户装到自己的插件目录;更广义地,集成别人产品的插件(可观测性后端、厂商 SaaS 连接器、分析面板一类)同样不落到树内。理由写得很坦白,是维护负担而不是质量判断——一个跑得很快的内核,加上一个自己不拥有的后端,等于一份持续的兼容成本。已经在树里的那几个目录属于既有先例,不构成允许新增的依据。

常驻带来的风险是真实的,不是理论上的。 它会开终端执行命令、连你的聊天软件账号、往磁盘写文件、访问外部服务,README.mdAGENTS.md 里几处细节把代价说得很直白:原生 Windows 安装会解包一份便携 Git Bash 专门用来执行 shell 命令;某些杀毒引擎会把它捆绑的 Python 包管理器可执行文件误判为恶意软件,README 给了验证签名和加白名单的步骤,并提醒把目录而不是文件哈希加白;手动 clone 的开发路径下,虚拟环境必须建在源码树之外,否则 agent 对自己的 checkout 跑一条相对路径命令就可能把正在运行的运行时抹掉。

几件它明确不管的事。 .env 只放凭证,超时、阈值、特性开关、显示偏好这类行为配置一律归 config.yaml,让用户「在 .env 里设个变量」的改动会被退回;后台派发的子任务只在进程内活着,要跨进程重启存活得改用定时任务或后台终端加完成通知;仪表盘那条 PTY 通道走 POSIX PTY,原生 Windows 不支持,WSL 可以。测试纪律上还有两条硬禁忌:不写会随正常数据更新而失败的变更探测型断言,不写读源码文本来做正则匹配的测试。

六、上手与避坑清单

别直接敲 pytest。 为什么会踩:本机装着一堆提供方的 API key、核数又多,直接跑会自动探测到凭证并走上跟 CI 不同的路径,于是出现「本地绿、CI 红」和反向的情况。怎么避:一律用 scripts/run_tests.sh。它统一了环境——清掉除少数几个之外的环境变量、把时区固定为 UTC、把区域设置固定成 C.UTF-8、并按测试文件分子进程隔离,模块级的字典与上下文变量不会跨文件泄漏。

工具注册了却调不到。 为什么会踩:自动发现只做两件事——导入 tools/ 下含顶层注册调用的文件、收集它的 schema。怎么避:记住暴露给 agent 是另一步,工具名必须出现在 toolsets.py 的某个工具集里;默认捆绑那份列表不是死代码,是各平台基础工具集继承的来源。

硬编码 ~/.hermes 为什么会踩:这个项目支持多套完全隔离的实例,每套有自己的主目录,路径靠一个环境变量在任何模块导入前被改写。怎么避:代码里读写状态用 hermes_constantsget_hermes_home(),打印给人看的字符串用 display_hermes_home()。仓库注明这类硬编码曾一次性造成五个 bug。

把行为开关塞进 .env。 为什么会踩:环境变量看起来最快。怎么避:非密钥配置写进 config.yaml 的对应小节,内部若需要环境变量镜像,从配置桥接过去,而不是让用户直接设。

读插件状态时插件还没被发现。 为什么会踩:插件发现只作为导入 model_tools.py 的副作用运行。怎么避:不经过 model_tools.py 的代码路径要显式调一次发现函数,它是幂等的。

在 Python 测试里断言 package.json 或 TS 源文件的内容。 为什么会踩:CI 有个改动分类器(scripts/ci/classify_changes.py)按改了哪些文件挑要跑的 job,只动 JS 侧的 PR 不会跑这些 Python 测试,于是 PR 绿而合进 main 后红。怎么避:这类断言放到 vitest 那一侧去。

中文会话搜不到一两个字的短词。 为什么会踩:默认分词器对 CJK 短词无能为力,查询会退化成全表扫描。怎么避:native/fts5_cjk/ 带了一个双字组分词的 SQLite 扩展,用它自带的构建脚本装到主目录下的库路径里;装好后下次打开会话库时会建对应索引,已有数据要跑一次存储优化子命令回填,配置里也有开关可以关掉它。

桌面端把技能斜杠命令过滤没了。 为什么会踩:桌面端的命令面板做了一份客户端白名单来隐藏纯终端和纯消息平台的命令,收紧白名单时容易顺手把用户自己的技能命令一起挡掉——这个 bug 真实发生过,表现是技能命令手打能跑但补全里看不见。怎么避:改 apps/desktop/src/lib/desktop-slash-commands.ts 时,确认「这不是内置命令」那条判断仍然同时流进补全路径和目录过滤路径。

在 Windows 上用裸 open() 读写文件。 为什么会踩:不指定编码时会落到系统区域编码,非 ASCII 内容静默损坏。怎么避:这条被写进了 pyproject.toml 的 lint 配置——其余 lint 规则基本关着,唯独这条编码规则开着,注释说明是因为一次调试里连续出了三个 Windows 沙箱回归。写中文内容的人尤其别赌这条。

收尾自检。 clone 下来之后,按这个顺序过一遍再动手:README.md 看它自称是什么;AGENTS.md 的 Project Structure 与 Footprint Ladder 两节看清红线;package.json 的 workspaces 认清 TS 边界;toolsets.py 看能力清单;tools/registry.py 加任意一个具体工具文件看注册形状;tui_gateway/server.py 看界面与内核之间到底传了什么。这六个文件读完,你对这个仓库的判断就不再靠猜。至于要不要让它常驻在自己的机器上,那是另一道题——先把上面第五节里那几项代价按你自己的环境算一遍,再决定。

本篇属于一个把开源常驻自托管 Agent 项目 Hermes Agent逐层拆开讲的系列,整体地图见 开源自托管 Agent 项目 Hermes Agent 是什么;沿着这条线往下,还可以看 开源自托管 Agent 项目 Hermes Agent开源自托管项目 Hermes Agent

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。