Hermes Agent 源码架构全解剖
基于 NousResearch 开源仓库 hermes-agent(2026-07-06 最新 commit 7426c09,MIT 协议、Python 为主 + Node/Ink TUI)的完整源码解读:你的一条消息从敲下回车,到回复出现在终端(或 Telegram / Discord),中间发生的每一件事。所有结论都标注了 文件:行号,可直接对照仓库阅读。读过上一篇《Claude Code 源码架构全解剖》的读者,会在文中看到两者的设计对比。
00全景架构:一颗 Python 心脏,多张「脸」
先建立整体心智模型。Hermes 与 Claude Code 最大的不同:它不只是一个「终端里的编程助手」,而是一个「跑在任何地方、从任何聊天软件都能召唤」的通用个人 agent。
Hermes 的本质是:一个 Python 类 AIAgent(run_agent.py:393,管理「调模型 → 执行工具 → 再调模型」的循环)+ 一个进程内工具注册表(tools/registry.py:208)+ 一堆可以互换的「外壳」——经典终端 REPL、Node/Ink 图形化 TUI、桌面 App、Telegram/Discord/Slack/WhatsApp/Signal 网关、cron 定时器、MCP server……全部外壳最终都调用同一个入口函数 run_conversation()(agent/conversation_loop.py:518)。
console_scripts 入口 hermes = hermes_cli.main:main,argparse 分发 40+ 子命令(chat/model/gateway/cron/skills/mcp…),配置加载,setup 向导
经典 prompt_toolkit REPL;Node/Ink TUI(经 stdio JSON-RPC 驱动 Python 子进程);消息平台网关;桌面端。它们只负责输入输出,不含 agent 逻辑
run_conversation() = 真正的 while 循环:组消息 → 调模型 → 解析 tool_calls → 执行工具 → 回填结果 → 再调模型,直到模型不再要工具
80+ 个工具按「toolset」分组自注册;guardrails + 审批门禁 + 并发判定 + 结果预算裁剪;terminal 工具有 6 种执行后端(本地/Docker/SSH/Modal/Daytona/Singularity)
声明式 ProviderProfile + 30+ provider 插件;默认 OpenAI chat.completions 格式,另有 Anthropic/Bedrock/Gemini/Responses 适配器;流式消费、错误分类、fallback 链、多凭据池轮换
Hermes 的招牌「自我改进循环」:MEMORY.md/USER.md 双记忆文件、agent 自建 skill、后台 curator 维护技能库、FTS5 会话搜索;外加 plugins/hooks/MCP/cron
0.1 与 Claude Code 的五个根本差异
| 维度 | Claude Code | Hermes Agent |
|---|---|---|
| 语言/形态 | TypeScript 单进程 CLI,React(Ink fork) 渲染终端 | Python 引擎 + 可选 Node/Ink TUI 前端 + 消息平台网关,多进程协作 |
| 模型 | 只有 Anthropic 系(第一方/Bedrock/Vertex/Foundry) | 任意模型:Nous Portal / OpenRouter / OpenAI / Anthropic / Gemini / 本地 Ollama…30+ provider 插件(providers/README.md) |
| 消息格式 | Anthropic Messages API(content blocks) | OpenAI chat.completions 格式为「通用语」,其余 provider 靠适配器转换(agent/transports/chat_completions.py:1-9) |
| 定位 | 编程助手,绑定当前工作目录 | 常驻个人 agent:可跑在 $5 VPS 上,从 Telegram 召唤,带 cron、语音、桌面控制、自我学习 |
| 学习能力 | 无内置跨会话学习(memory 需手动) | 核心卖点:自动记忆管理 + agent 自创 skill + 后台 curator 维护 + 会话全文检索(README「self-improving」,第 7 章) |
三个最重要的文件:如果只看三个文件,看 agent/conversation_loop.py(主循环 5294 行)、agent/tool_executor.py(工具怎么被并发/串行执行)、agent/chat_completion_helpers.py(请求怎么发出、流式怎么消费)。run_agent.py 里的 AIAgent 类是它们共享的状态容器——Hermes 正在进行一场「god-file 拆解运动」(agent/turn_finalizer.py:3-5 的注释直言不讳),大量逻辑正从 16000 行的 cli.py / 6000 行的 run_agent.py 迁往 agent/ 下的小模块。
01一条消息的一生 ★
这是全文主线。以最常用的经典终端模式(hermes 敲一句话)为例,从按下回车到回复完整出现,共 12 站。每一站都在后面的章节有放大详解。
输入被 prompt_toolkit 捕获,回车提交
经典 REPL 用 Python 库 prompt_toolkit(一个终端行编辑库)接收输入,支持多行、历史、补全。斜杠命令(/model、/compress、/yolo…)在这一层被 CLICommandsMixin 拦截本地处理,普通文本才交给 agent。
cli.py:13036(HermesCLI.run)→ cli.py:15446(app.run)→ hermes_cli/cli_commands_mixin.py
chat 线程起跑:绑定审批回调,调用 run_conversation
CLI 在一个独立线程里跑 agent(主线程留着渲染和接收「运行中输入」)。进线程先注册三个回调:sudo 密码回调、工具审批回调(危险命令时弹窗问你)、secret 捕获回调,然后把「本会话的审批 key」绑进 contextvar——这样 /yolo 开关才知道作用于哪个会话。最后调 self.agent.run_conversation(user_message=…, conversation_history=…)。
cli.py:12210-12228(set_approval_callback / set_current_session_key)· cli.py:12262(run_conversation 调用点)
回合序幕:build_turn_context 一次性做完所有准备
进入引擎后,第一件事是「回合上下文构建」:清洗用户消息里的非法代理字符、恢复或构建 system prompt(缓存的直接复用)、把 todo/nudge 注水、崩溃恢复持久化、预检压缩(上下文快满就先压)、跑插件的 pre_llm_call hook、预取外部记忆。这些全部收进一个函数,主循环只读它返回的局部变量。
agent/conversation_loop.py:571-582(调用)· agent/turn_context.py:1-545(实现)
进入 while 循环:预算检查 + 中断检查
循环条件是「API 调用次数 < max_iterations(默认 90)且迭代预算还有剩余」。每轮开头先看用户有没有发中断请求(_interrupt_requested),有就带着部分结果退出。这里没有递归——和 Claude Code 一样用一个可变状态 + while,避免栈增长。
agent/conversation_loop.py:633(while 条件)· :638-643(中断检查)· run_agent.py:430(max_iterations=90)
组装 api_messages:system prompt「一次构建、逐字复用」
把内部消息列表复制一份做 API 视图:外部记忆召回和插件上下文注入到本轮 user 消息尾部(刻意不动 system prompt,因为 system prompt 每字节都要稳定才能命中厂商的 prompt cache);system 消息插到最前;给 Claude 系模型打 cache_control 断点(system + 最后 3 条消息);清理孤立 tool 结果、丢弃 thinking-only 消息。
agent/conversation_loop.py:782-846(组装)· :885(apply_anthropic_cache_control)· :895,905(清理)
建 kwargs、过中间件、发起流式请求
_build_api_kwargs 按 ProviderProfile 拼出 model/messages/tools/temperature/max_tokens/extra_body;再穿过 LLM 执行中间件(插件可观测/改写请求);默认永远走流式——即使没人看增量,也要靠流式做 90 秒「卡流检测」。真正发出是 OpenAI SDK 的 client.chat.completions.create(stream=True)。
agent/conversation_loop.py:1156(_build_api_kwargs)· :1315(run_llm_execution_middleware)· agent/chat_completion_helpers.py:2034(create 调用)
逐 chunk 消费:文本、思考、工具参数三路累加
for chunk in stream 循环里:reasoning_content 增量喂思考回调;文本增量喂 stream_delta_callback(终端立刻打印);tool_calls 增量按 index 累加——工具名用「赋值」不用「拼接」(防某些厂商重发全名变成 read_fileread_file),参数 JSON 字符串则逐段拼接。流结束后拼装成一个「非流式响应形状」的对象返回。
agent/chat_completion_helpers.py:2106(for chunk)· :2214-2226(tool_calls 累加)
响应体检:JSON 校验、去重、封顶
拿到 tool_calls 先逐个 json.loads 校验参数——坏 JSON 不重试请求,而是给每个调用回一条「Error: Invalid JSON arguments…请重试」的 tool 消息让模型自己改;然后 _cap_delegate_task_calls(限制一轮 spawn 子 agent 数量)、_deduplicate_tool_calls(同名同参去重)。
agent/conversation_loop.py:4560-4574(坏 JSON 反馈)· :4580-4583(cap + 去重)
先落盘再执行:崩溃也不丢 tool_call
assistant 的 tool_call 消息在任何工具副作用发生前就写进 SQLite 会话库——如果某个破坏性工具把 Hermes 自己重启了,resume 时还能看到「已执行过哪个调用」。这是 Hermes 作为「常驻 agent」特有的偏执。
agent/conversation_loop.py:4646-4652(_flush_messages_to_session_db)· hermes_state.py:123(state.db)
执行工具:guardrails → 审批 → 并发或串行
_execute_tool_calls 先判断这批调用可否并行(只读工具白名单 + 文件路径不重叠 → 最多 8 线程并发,其余串行)。每个工具执行前过 ToolGuardrails(循环检测/黑名单)和审批门(危险终端命令弹窗、写文件确认),然后 registry.dispatch(name, args) 调到具体 handler。超大输出会被「落盘 + 预览」替换,一轮所有工具结果合计超 200K 字符还会强制外溢到磁盘。
run_agent.py:5631 · agent/tool_dispatch_helpers.py:104 · agent/tool_executor.py:306,965 · tools/registry.py:574 · tools/tool_result_storage.py:203
结果回填,回到第 4 步
每个结果作为 role:"tool" 消息(带 tool_call_id)追加进 messages,循环 continue 再调一次 API。模型每轮看到自己上一轮工具的产出——这就是 agentic loop 的全部秘密,Hermes 与 Claude Code 在这一点上完全同构。
agent/tool_executor.py:794-865(结果回填)· agent/conversation_loop.py:633(回到循环顶)
终点:无 tool_calls 的那轮 → finalize_turn
模型不再要工具时,final_response = assistant_message.content;post-loop 收尾统一在 finalize_turn:预算耗尽摘要、trajectory 落盘、会话持久化、回合诊断、排队的 /steer 消息释放、记忆/技能复盘触发(Hermes 学习循环的入口,见第 7 章)。UI 收起 spinner,回复停在屏幕上。
agent/conversation_loop.py:4770(final_response)· :5272-5276(finalize_turn 调用)· agent/turn_finalizer.py:30
如果消息来自 Telegram 而不是终端呢?前两站换成:gateway 进程收到平台消息 → 鉴权(配对/白名单)→ 路由到该聊天对应的会话 → 同样调 run_conversation();流式回调换成「聚句节流后编辑平台消息」。引擎层(3-12 站)一字不差。详见第 10 章。
02启动流程:从 hermes 命令到可交互
Hermes 的入口比 Claude Code 复杂:不仅有交互聊天,还有 40+ 个子命令、TUI 的三段式进程链和首次运行向导。
2.1 入口链与 bootstrap 垫片
唯一的 console_scripts 入口定义在 pyproject.toml:307-308:hermes = "hermes_cli.main:main"。所有入口第一个 import 都是 hermes_bootstrap.py 这个「环境垫片」:Windows 强制 UTF-8 stdio(hermes_bootstrap.py:59-122)、harden_import_path() 把 cwd 从 sys.path 移除防止用户项目里的 utils/ 目录 shadow 掉 Hermes 同名模块(:125-158)、不可变 Docker 镜像下恢复 lazy-install 路径(:161-183)。
main()(hermes_cli/main.py:12686)的启动序列:设进程名 → 清理上次 hermes update 的残留 → Termux 快速路径 → 构建顶层 argparse。子命令 parser 拆在 hermes_cli/subcommands/ 的 39 个模块里,覆盖 chat / model / gateway / cron / skills / mcp / secrets / doctor / desktop / kanban / backup …。dispatch 尾部(main.py:14030-14155):容器模式转发 → 顶层 --oneshot 拦截 → 裸 hermes 或 --resume 落到 cmd_chat(args)。
2.2 cmd_chat 的三岔口:经典 REPL / TUI / gateway
cmd_chat(main.py:2215)先跑首次运行守卫:没配任何 provider 就问「Run setup now? [Y/n]」进 setup 向导(main.py:2277-2306)。然后 _resolve_use_tui(main.py:2190-2212)决定界面:--cli 强制经典 REPL;--tui 或配置 display.interface: tui 走图形化 TUI;默认经典 REPL。
经典 REPL 路径
from cli import main as cli_main; cli_main(**kwargs)(main.py:2380-2406)→ 构造 HermesCLI 对象(cli.py:3662,双 Mixin:agent 装配 + 斜杠命令)→ 有 -q 单查询直接跑一轮,否则 cli.run()(cli.py:13036)进 prompt_toolkit 交互循环。agent 本体 惰性构建——第一条消息才 _init_agent import run_agent。
TUI 路径:Python → Node → Python 三段式
_launch_tui(main.py:1984-2146)把 model/provider/query 等塞进 HERMES_TUI_* 环境变量,然后 subprocess.call(["node", "dist/entry.js"]) 阻塞等待(main.py:2117)。Node 端的 Ink TUI 反过来再 spawn 一个 Python 子进程:spawn(python, ['-m', 'tui_gateway.entry'])(ui-tui/src/gatewayClient.ts:356),两者用 stdio 上的 NDJSON 做 RPC。真正跑 agent 的始终是 Python(tui_gateway),Node 只管渲染和输入。TUI 退出码 42 = 请求自更新,重启为 hermes update(main.py:2138-2144)。
gateway / oneshot 路径
hermes gateway start 把网关当 systemd/launchd 服务管理(第 10 章);hermes -z "..."(oneshot)绕过整个 REPL,强制 HERMES_YOLO_MODE=1 全自动批准、把 stdout 重定向 devnull、只输出最终回复(hermes_cli/oneshot.py:125-187)——对标 Claude Code 的 -p print 模式。
2.3 配置体系:HERMES_HOME + 两层 YAML + .env
| 文件 | 位置 | 说明 |
|---|---|---|
| config.yaml | ~/.hermes/config.yaml | 用户配置(优先),首跑 setup 向导生成;cli.py:360 load_cli_config |
| cli-config.yaml | 项目目录 ./cli-config.yaml | 项目级回落;cli.py:379 |
| .env | ~/.hermes/.env | API key 等凭据,python-dotenv 加载,覆盖过期的 shell 导出值(hermes_cli/env_loader.py:228-231) |
| state.db | ~/.hermes/state.db | SQLite(WAL 模式):sessions / messages 表 + FTS5 全文索引(hermes_state.py:123,700,748,814) |
HERMES_HOME 由 hermes_constants.py:55 的 get_hermes_home() 决定:环境变量 HERMES_HOME 优先,否则 ~/.hermes(Windows 用 %LOCALAPPDATA%\hermes);支持 profiles/<name> 多 profile 隔离(hermes_constants.py:117-149)。配置顶层键覆盖 model / terminal / browser / compression / memory / skills / agent / delegation / display … 约 20 个大段(cli-config.yaml.example)。
对比 Claude Code 的五层 settings:Hermes 没有企业托管层与 flag 层,只有「用户 → 项目回落」两层 YAML + 环境变量覆盖,简单得多;换来的是每处配置读取要各自兜底默认值(cli.py:392-674 的内置默认块)。
2.4 首次运行 setup 向导
hermes setup(hermes_cli/setup.py:2716 run_setup_wizard):备份旧配置 → 三选一「Quick Setup (Nous Portal) / Full setup / Blank Slate」(setup.py:2888-2896)。全向导由 SETUP_SECTIONS 驱动(setup.py:2622-2629):model → tts → terminal → gateway → tools → agent 六段。模型段路由到 hermes_cli/model_setup_flows.py 里 14+ 个 provider 专属流程(openrouter:121 / nous:284 / anthropic:2834 / bedrock:2183 / copilot:1564…),Nous Portal 走 device-code OAuth。
会话 ID 形如 20260225_143052_a1b2c3(hermes_state.py:486);会话消息全部落 SQLite,resume 时经 resolve_resume_session_id 沿「压缩 fork 链」找到真正持有最新消息的后代会话(hermes_state.py:3936-3990,第 7 章解释为什么压缩会 fork 会话)。
03Agentic 主循环:conversation_loop.py
整个产品的心脏。5294 行的 run_conversation() 里是一个 while 循环,每次迭代 = 一次「调模型 + 执行工具」的回合。
3.1 三段式结构:序幕 → 循环 → 收尾
外壳(CLI / TUI gateway / 消息网关 / cron / 子agent)
└─ AIAgent.run_conversation() [run_agent.py 转发 → agent/conversation_loop.py:518]
├─ build_turn_context() [turn_context.py] 回合序幕:清洗输入、恢复 system prompt、预检压缩、pre_llm_call hook
├─ while api_call_count < max_iterations and budget.remaining > 0: [conversation_loop.py:633]
│ 调模型 → 解析 → 执行工具 → continue
└─ finalize_turn() [turn_finalizer.py:30] 收尾:落盘、诊断、记忆/技能复盘触发
与 Claude Code 的「三层 async generator 流式吐消息」不同,Hermes 的循环是同步函数 + 回调:流式增量、工具进度、思考过程都通过构造 AIAgent 时注入的十几个回调(stream_delta_callback / tool_progress_callback / thinking_callback / clarify_callback…,run_agent.py:441-460)推给外壳。这让同一个循环能同时服务终端、Node TUI、Telegram、cron——外壳只需要换一组回调。
3.2 循环条件:双保险预算
while (api_call_count < agent.max_iterations and agent.iteration_budget.remaining > 0) or agent._budget_grace_call:
- max_iterations:每回合 API 调用上限,默认 90(run_agent.py:430)。
- iteration_budget:跨「父 agent + 子 agent」的全局迭代预算(agent/iteration_budget.py:17),子 agent 每个独立 50 次;
execute_code回合会refund()不计数。 - grace call:预算耗尽后再给模型「最后一次机会」收个尾(conversation_loop.py:649-659),避免生硬掐断。
3.3 一轮迭代的完整步骤
| 阶段 | 做什么 | 位置(conversation_loop.py) |
|---|---|---|
| A 检查点/中断 | checkpoint 去重复位;用户中断则带部分结果退出 | :635-643 |
| B /steer 排水 | 模型思考期间用户发的「转向」消息,在建 api_messages 之前注入,本轮就生效 | :695-750 |
| C 组消息 | 复制内部消息 → 注入外部记忆/插件上下文到本轮 user 消息 → 插 system → cache_control 断点 → 清理孤儿 tool 结果 | :782-905 |
| D 建请求 | _build_api_kwargs(按 ProviderProfile)→ 中间件 → 流式调用 | :1156,1315 |
| E 内层重试环 | while retry_count < max_retries:错误分类 → 压缩/换凭据/降级/退避 | :1095(第 6 章详述) |
| F 响应体检 | JSON 参数校验(坏参数回 tool 消息让模型重试)、delegate 封顶、去重 | :4418-4583 |
| G 先落盘 | assistant tool_call 消息在工具副作用前写 SQLite | :4646-4652 |
| H 执行工具 | _execute_tool_calls(并发或串行,第 4 章);guardrail 可强制中止整轮 | :4670-4690 |
| I 终点判断 | 无 tool_calls → final_response = content;空回复有 thinking-prefill 等恢复梯 | :4770-5090 |
和 Claude Code 一样,「模型这轮有没有要工具」是唯一的续跑判据——没有计划器,agent 的「自主性」全部来自这个反馈循环。
3.4 System Prompt:三层结构 + 「一次构建、字节稳定」不变量
Hermes 的 system prompt 有一条铁律(agent/system_prompt.py:3-8):每个会话只构建一次、之后逐字节复用,只有上下文压缩才触发重建——目的是让厂商侧 prompt cache 前缀永远命中。三层结构(system_prompt.py:10-21,组装在 build_system_prompt_parts :113):
身份 + 工具指引 + 技能索引
SOUL.md(用户可写的 agent「灵魂文件」,没有则用 DEFAULT_AGENT_IDENTITY)、任务完成/并行工具/按模型定制的操作指引、技能名录、环境提示。git 状态快照也在这层——会话开始解析一次、不再更新,提示里明确写「依赖前请重跑 git status」(agent/coding_context.py:262-263,845)。
项目说明文件
按优先级首个命中即止:.hermes.md / HERMES.md(向上找到 git root)→ AGENTS.md(仅顶层)→ CLAUDE.md(仅 cwd)→ .cursorrules(prompt_builder.py:1931-1935)。是的,Hermes 会读你的 CLAUDE.md。载入内容会做 prompt-injection 扫描。
记忆快照 + 时间戳
MEMORY.md / USER.md 的会话开始冻结快照(会话中写记忆不动 system prompt,保 cache)、外部记忆 provider 块、日期/session/model 行(system_prompt.py:424-461)。
对比:Claude Code 把 CLAUDE.md 拼在消息数组最前而不进 system prompt,且 git 状态每轮动态刷新;Hermes 反其道——一切进 system prompt 但整段冻结。前者牺牲一点缓存换新鲜度,后者牺牲新鲜度换 100% 缓存命中(Hermes 要在 OpenRouter 等没有精细 cache_control 的通道上也能吃到隐式前缀缓存,这个选择很务实)。
3.5 中断、/steer 与实时转向
- 中断:外壳把
agent._interrupt_requested置位(终端 ESC、平台新消息均可触发),循环顶检查到即 break,带着部分结果走 finalize(conversation_loop.py:638-643);正在等模型时则由_interruptible_streaming_api_call负责放弃流(run_agent.py:4734)。 - /steer 转向:Hermes 特色——agent 干活时你可以继续打字「改主意」。转向文本进入排队区,在下一次 API 调用组消息之前被排水注入(conversation_loop.py:695-750),模型本轮就能看到,不用等整个任务跑完。
- guardrail 强停:工具护栏(循环检测等)可以返回「controlled halt」,直接把本轮收掉并把原因作为 assistant 消息发给用户(conversation_loop.py:4672-4690)。
另有一个完全绕开默认循环的逃生门:api_mode == "codex_app_server" 时整个回合交给 OpenAI Codex app-server 子进程执行(终端/文件操作都在 Codex 内),Hermes 只做壳(conversation_loop.py:624-631)。
04工具系统
80+ 个工具、按 toolset 分组、文件 import 即自注册。一个工具 = JSON Schema + handler 函数 + 可用性探针 + 结果预算。
4.1 注册表:ToolEntry 与自注册
每个工具文件在模块底部调用 registry.register(...) 完成注册,例如四个文件工具(tools/file_tools.py:2170-2173):
registry.register(name="read_file", toolset="file", schema=READ_FILE_SCHEMA,
handler=_handle_read_file, check_fn=_check_file_reqs, emoji="📖",
max_result_size_chars=100_000)
ToolEntry(tools/registry.py:78-107)的字段就是工具的全部契约:schema(OpenAI function 格式的 JSON Schema,模型输入的唯一事实源)、handler(同步或异步,异步自动桥接)、check_fn(可用性探针——Docker 装没装、playwright 在不在,结果带 ~30 秒 TTL 缓存 + 抖动抑制,registry.py:110-135 注释)、max_result_size_chars(超限落盘阈值)、dynamic_schema_overrides(运行期改 schema 描述,比如 delegate_task 的描述要反映用户当前的并发上限配置)。注册表带 RLock 和「代数计数器」——MCP 动态刷新会在运行期改注册表,读者按代数做 memoize(registry.py:222-231)。
执行入口 registry.dispatch(name, args)(registry.py:574-600):一切异常都被捕获成 {"error": "..."} JSON 返回——工具永不抛异常给循环,错误是喂给模型的数据,这一点与 Claude Code 的 <tool_use_error> 设计同构。
4.2 Toolsets:给不同场景发不同的「工具套餐」
toolsets.py:31-66 定义 _HERMES_CORE_TOOLS(CLI 和所有消息平台共用的核心 30 余个工具:web_search/terminal/read_file/patch/browser_*/todo/memory/skills…),toolsets.py:95 起的 TOOLSETS 字典把工具组合成可组装的套餐(web / file / browser / cronjob / computer_use / video_gen…),支持 includes 嵌套复用。哪些 toolset 启用由配置、平台(platform_toolsets)、CLI flag 共同决定,model_tools.py:279 的 get_tool_definitions() 最终算出发给模型的 schema 列表。桌面 GUI 专属工具(project_list 等)刻意不进核心套餐,只由 GUI gateway 启用(toolsets.py:53-59 注释)。
4.3 并发执行:白名单 + 路径不重叠
_execute_tool_calls(run_agent.py:5631)用 _should_parallelize_tool_batch(agent/tool_dispatch_helpers.py:104-147)判定一批调用可否并行,规则保守:
- 只读白名单
_PARALLEL_SAFE_TOOLS(tool_dispatch_helpers.py:45-58):read_file / search_files / web_search / web_extract / vision_analyze / session_search / skills_list 等 10 个;MCP 工具需 server 显式声明 parallel-safe。 - 路径互斥:read_file / write_file / patch 属「路径作用域工具」,同批目标路径两两不重叠(含子树判断)才可并行(:61,136-141)。
- 绝不并行:
clarify(要问用户的交互工具)出现即整批串行(:42);参数解析失败也退回串行。
并行批走 execute_tool_calls_concurrent(agent/tool_executor.py:306):最多 8 个 daemon 线程(tool_executor.py:71;用自定义 DaemonThreadPoolExecutor 防止解释器退出时被 atexit 卡住,:642-647),整批默认 420 秒看门狗(:74-75)。串行路径 execute_tool_calls_sequential(:965)逐个跑。两条路径共享同一套前置:越权工具拒绝 → PreToolUse 插件 hook → ToolGuardrails.before_call(:447-460)→ dispatch。
4.4 结果预算:三级防上下文爆炸
工具自截断:terminal 输出默认 50000 字符、read_file 默认 2000 行、单行 2000 字符(tools/tool_output_limits.py:39-41),read_file 超预算按「最后一个完整行」截断并给 next_offset 续读游标而不是报错(tools/file_tools.py _trim 注释)。
单结果落盘:超过工具注册的 max_result_size_chars → 完整输出写进沙箱的 /tmp/hermes-results/{tool_use_id}.txt,上下文里换成 <persisted-output> 预览 + 路径,模型可再 read_file 取全量(tools/tool_result_storage.py:144,119-141)。写入走 stdin 绕过 Linux 128KB argv 限制(:100-116)。
回合聚合预算:一轮所有工具结果合计超 200K 字符 → 从最大的开始逐个外溢到磁盘(tool_result_storage.py:203,228-248)。预算还会按模型上下文窗口缩放——中途切到 65K 的本地小模型,预算跟着缩(agent/tool_executor.py:54-67)。
4.5 代表性工具逐个拆
terminal —— 六种执行后端,「本地」只是其中之一
这是 Hermes 与 Claude Code 差异最大的工具。Claude Code 的 Bash 永远在本机 spawn;Hermes 的 terminal 工具有 6 种后端(tools/terminal_tool.py:1216,1263;实现在 tools/environments/:local.py 1207 行 / docker.py 1460 行 / ssh.py / modal.py / daytona.py / singularity.py):
- local:本机执行,但子进程环境经过两层 secret 剥离(Tier-1 无条件剥、Tier-2 provider key 条件剥,tools/environments/local.py:346-479)——agent 跑的命令看不到你的 API key。
- docker / singularity:容器隔离;docker 若 bind-mount 了宿主目录则丧失「免审批」资格(tools/approval.py:1998-2009)。
- ssh:命令跑在远程机;
~展开留给远端(terminal_tool.py:1219-1230)。 - modal / daytona:serverless 沙箱,空闲休眠、按需唤醒——README 说的「idle 时几乎零成本」的技术底座。
危险命令识别与审批见第 5 章。进程管理配套 process 工具 + tools/process_registry.py(后台进程注册表,也承载异步子 agent 的完成事件队列)。
patch —— 带模糊匹配的编辑工具
Hermes 的文件编辑是 patch(old/new 文本替换),配 tools/fuzzy_match.py 做「模糊匹配」:old_string 不完全一致时按相似度容忍空白/缩进差异,命中即改——比 Claude Code 的严格唯一匹配宽松(后者失败要求模型重发)。写文件前有 file_state 时间戳追踪(tools/file_state.py)与写审批门(第 5 章 write_approval)。write_file/patch/read_file 的结果上限都是 100K 字符(tools/file_tools.py:2170-2173)。
delegate_task —— 子 agent = 再 new 一个 AIAgent
delegate_task()(tools/delegate_tool.py:2342)为每个子任务 child = AIAgent(...)(:1301)然后 child.run_conversation(user_message=goal)(:1921)——和 Claude Code 的「递归调用同一个 query()」思想一致。隔离设计:全新对话(无父历史)、独立 task_id(独立终端会话)、受限 toolset、子 agent 黑名单里有 delegate_task 自己(:47,默认 MAX_DEPTH=1 不许套娃,:125)。危险命令在子 agent 里默认自动拒绝而非弹窗(:74)。
顶层 delegation 一律后台跑(run_agent.py:5654-5677):父 agent 立即拿 handle 继续干活,子完成后其结果经共享 completion 队列「铸成一个新回合」送回父对话(tools/async_delegation.py:5-23)——刻意复用消息轨道以保持 role 交替、不炸 prompt cache。只有「编排者子 agent」(depth>0)的再委派才同步等结果。
clarify —— 把「问用户」也做成工具
tools/clarify_tool.py 让模型在拿不准时结构化提问(选项 + 自由文本),经 clarify_callback 冒泡到外壳:终端渲染成交互菜单,Telegram 渲染成按钮。它被列入「绝不并行」名单(agent/tool_dispatch_helpers.py:42)。对标 Claude Code 的 AskUserQuestion。
execute_code —— 零上下文成本的「RPC 编排」
tools/code_execution_tool.py:模型写一段 Python 脚本,脚本内可通过 RPC 回调 Hermes 的其他工具——把「查 10 个网页 + 汇总」这类多步流水线压缩成一次工具调用,中间结果不进上下文(README「collapsing multi-step pipelines into zero-context-cost turns」)。执行的迭代还会被预算 refund()(agent/iteration_budget.py:45)。
05权限与安全模型
三层纵深:硬底线(任何模式不可绕)→ 用户策略(deny/allowlist)→ 审批门(manual/smart/off)。哲学很坦诚:进程内启发式是「辅助」,真正的 OS 级边界只有容器。
5.1 审批模式:manual / smart / off(+ YOLO)
manual(默认)
危险命令弹窗问用户:[o]nce / [s]ession / [a]lways / [d]eny(tools/approval.py:1662)
smart
辅助 LLM 判 APPROVE/DENY/ESCALATE,灵感来自 OpenAI Codex Smart Approvals;命令用 XML 包裹防注入,异常时 fail-safe 到「升级人工」(approval.py:1920-1995)
off / YOLO
三条绕过源合一(approval.py:1846-1850):进程级 --yolo(import 时冻结,防 prompt injection 中途改环境变量提权,:30-33)、会话级 /yolo、配置 mode:off
容器快速路径
modal/daytona/singularity 后端恒免审批;docker 仅在没有 bind-mount 宿主目录时免(approval.py:1998-2009)——挂了宿主盘的容器里 rm -rf 一样伤到你
5.2 命令风险分级:四道闸门的顺序
| 闸门 | 内容 | YOLO 能过吗 | 位置 |
|---|---|---|---|
| ① hardline 硬底线 | 12 条正则:rm -rf / 根目录、mkfs、dd 写块设备、fork bomb、shutdown/reboot、kill -1… | 不能,任何模式先查它 | approval.py:365-402,2299-2306 |
| ② 用户 deny 规则 | config 的 approvals.deny(fnmatch 通配) | 不能,「即便 yolo 也永不执行」 | approval.py:465-472,2319-2326 |
| ③ dangerous 模式库 | 约 47 条:递归删除、chmod 777、无 WHERE 的 DELETE、curl|sh、base64 解码管到 shell、杀 hermes 自身进程… | 能(yolo 放行,manual 弹窗) | approval.py:546 起 |
| ④ allowlist | config 的 command_allowlist 命中直接放行 | — | approval.py:1596-1652 |
gateway 场景的审批是异步的:agent 线程阻塞等 Telegram 里的 /approve 或 /deny;沉默 ≠ 同意——超时按拒绝处理,且拒绝消息会点名 agent 常见的规避伎俩(换个说法重试、换条命令达成同样效果),防止模型「绕道」(approval.py:2548-2598)。发到聊天软件的命令内容先经 redact_sensitive_text 脱敏(:2538-2543)。
5.3 写保护与路径安全
- 写黑名单(agent/file_safety.py:28-77):
~/.ssh/authorized_keys、.env、/etc/sudoers、~/.aws ~/.kube ~/.gnupg等敏感路径直接拒写;设HERMES_WRITE_SAFE_ROOT后变成「只准写指定目录」的白名单模式(:80-95,134-142)。源码注释坦白:这不是安全边界(同一 OS 用户下 terminal 仍能 cat),只是纵深防御(:184-187)。 - 记忆/技能写审批(tools/write_approval.py:264-273):可选门,开启后 MEMORY.md 写入在交互终端内联审批,SKILL.md 一律进 staging 目录等人批——「只延迟、不静默丢弃」。
- SSRF 防护(tools/url_safety.py:154-168):云 metadata 端点(169.254.169.254、Azure IMDS、阿里云 100.100.100.200…)无条件阻断,不受任何开关影响;私网 IP 默认阻断可配置放开。
5.4 供应链与内容威胁
- tirith(tools/tirith_security.py:730-825):本地 Rust 二进制子进程扫内容级威胁(同形异义 URL、pipe-to-interpreter),退出码即裁决(0 允许/1 阻断/2 警告);二进制从 GitHub releases 自动下载但必验 SHA-256 + cosign provenance(:293-333);默认 fail-open + 连续崩溃熔断。
- OSV 恶意包预检(tools/osv_check.py:22-62):npx/uvx 启动 MCP server 前查 api.osv.dev 的 MAL-* 预警。
- MCP 配置扫描(hermes_cli/mcp_security.py:80-175):内置 2026 年 6 月「hermes-0day」真实攻击的 IOC 黑名单(攻击者 SSH 公钥/IP),外加网络外泄形(
/dev/tcp/、curl 内联脚本)和持久化形(写 authorized_keys/crontab/.bashrc)模式,保存时和 spawn 时都跑。 - Skill 安装门(tools/skills_guard.py:51-61):信任分级 × 危险等级的策略矩阵——builtin 全放行、trusted 源(openai/anthropics/huggingface)dangerous 阻断、community 源 caution 就阻断;
--force也压不过 dangerous 判定(:704-723)。详见第 9 章。 - 凭据不进沙箱(tools/env_passthrough.py:48-119):execute_code 和 terminal 子进程默认剥离 secret;skill 可声明需要透传的环境变量,但 Hermes 托管的 provider 凭据在黑名单上、永远不可注册透传(GHSA-rhgp-j443-p4rf 的修复),import 失败时 fail-closed。多 profile 网关另有 contextvar 级 secret scope,未绑定 scope 时读 secret 直接抛异常而不是回落全局环境(agent/secret_scope.py:148-153)。
与 Claude Code 的对比:Claude Code 有 OS 级轻量沙箱(macOS Seatbelt / Linux bwrap)+ 精细的 Bash(npm:*) 前缀规则;Hermes 走的是「正则风险库 + LLM smart 审批 + 容器后端」路线,没有 bwrap/seatbelt 类本地沙箱(探索代理 grep 全库无命中)。但 Hermes 多了 Claude Code 没有的两块:远程消息平台的鉴权体系(默认 deny + 配对码,第 10 章)和真实事件驱动的供应链 IOC 黑名单。
06模型 / API 层:一颗引擎适配 30+ 厂商
Hermes 的「通用语」是 OpenAI chat.completions 格式;ProviderProfile 声明差异,transports 做翻译,credential_pool 管钥匙。
6.1 ProviderProfile:声明式而非继承式
与其给每个厂商写一个类,Hermes 用一个 dataclass 把「厂商的一切怪癖」declarative 地摆出来(providers/base.py:1-10:「They do NOT own client construction, credential rotation, or streaming. Those stay on AIAgent.」)。关键字段:api_mode(默认 chat_completions)、auth_type(api_key / oauth_device_code / oauth_external / copilot / aws_sdk,base.py:56)、fixed_temperature(哨兵 OMIT_TEMPERATURE = 干脆不发温度,:21)、supports_vision、fallback_models。5 个可覆盖 hook:prepare_messages / build_extra_body / build_api_kwargs_extras / fetch_models / get_hostname(base.py:98-217)。
Profile 本体作为插件放在 plugins/model-providers/<name>/(30+ 个:nous / openrouter / anthropic / gemini / bedrock / vertex / xai / alibaba / zai…),调用 register_provider(...) 注册。Nous 官方端点 https://inference.nousresearch.com/v1(plugins/model-providers/nous/__init__.py:50),OAuth device-code 登录走 hermes portal(hermes_cli/portal_cli.py:28)。
6.2 五种消息方言与适配器
| api_mode | 适配器 | 翻译成什么 |
|---|---|---|
| chat_completions(默认) | agent/transports/chat_completions.py | 近乎恒等——16 个 OpenAI 兼容厂商(OpenRouter/Nous/Ollama/DeepSeek/Kimi…)直接发 |
| anthropic | agent/anthropic_adapter.py | Anthropic Messages API;识别 sk-ant-api / OAuth setup-token / Claude Code 凭据三种鉴权(:441-595 还能识别一堆「伪 Anthropic」第三方端点) |
| bedrock | agent/bedrock_adapter.py | AWS Converse API(boto3),流式走 converse_stream(:777) |
| gemini 原生 | agent/gemini_native_adapter.py | generateContent schema;伪装成 chat_completions 的 facade(:1-16),因为 Google 的 OpenAI 兼容端点在多轮工具循环下太脆 |
| codex_responses | agent/codex_responses_adapter.py | OpenAI Responses API(Codex/xAI/GitHub Models);reasoning.encrypted_content 绑定签发端点,跨端点重放会 400,适配器给它打 issuer 戳过滤(:26) |
6.3 重试、错误分类与 fallback 链
Hermes 面对的是「30 个厂商 × 各自的报错方言」,所以搞了一个 错误分类器(agent/error_classifier.py:24-72):把异常归成约 20 类 FailoverReason(auth / billing / rate_limit / upstream_rate_limit / overloaded / context_overflow / content_policy…),每类带四个动作位:retryable / should_compress / should_rotate_credential / should_fallback(:90-93)。主循环的内层重试环按这些动作位行动:
- 退避:
jittered_backoff= min(5s × 2^(n-1), 120s) + 50% 抖动(agent/retry_utils.py:28-66)。 - 429 细分:上游过载(OpenRouter 转发的上游 429)→ fallback 换模型;账号级 429 → 轮换凭据;Nous 的 429 还写进跨进程共享文件让其他会话直接跳过调用(agent/nous_rate_guard.py:1-12,防「3 次 SDK 重试 × 3 次 Hermes 重试」的 9 倍放大)。
- fallback 链:
try_activate_fallback(agent/chat_completion_helpers.py:1172-1260)沿用户配置的备选链逐个切换 client/model/provider,主 provider 上 60 秒冷却,链耗尽才认输。 - 上下文超限 → 触发压缩后重试(最多 3 次压缩尝试,conversation_loop.py:3339-3607)。
- 一次性恢复守卫:16 个「本回合只许试一次」的修复动作(OAuth 刷新、剥 thinking 签名、缩图、去 multimodal tool 内容…)收进
TurnRetryStatedataclass(agent/turn_retry_state.py:32-75),每次 attempt 重建,杜绝无限自我修复循环。
6.4 多凭据池:一个 provider 可以插很多把钥匙
agent/credential_pool.py:508 的 CredentialPool 支持同一 provider 配多个 key/OAuth 账号,4 种轮换策略:fill_first(默认,用坏了才换)/ round_robin / random / least_used(:98-106,1485-1513)。429 的 key 进 1 小时冷却(:114);选择前还会从磁盘同步其他进程刷新过的 OAuth token(:1384-1394)——因为 CLI、gateway、cron 可能同时在跑。这是 Claude Code 完全没有的一层(它假设单账号)。
6.5 Prompt caching 与计价
对 Claude 系模型自动打 cache_control:唯一布局 system_and_3——4 个断点 = system prompt + 最后 3 条「能承载 marker」的消息(agent/prompt_caching.py:1-7,84-117);OpenRouter 转发和原生 Anthropic 的 marker 摆放位置不同由 native_anthropic 区分。模型目录用 models.dev 数据库(打包快照 → 磁盘缓存 → 网络 → 每小时后台刷新,agent/models_dev.py:1-18);计价三源(官方价目 / OpenRouter / 元数据)在 agent/usage_pricing.py:733;Nous 订阅额度从 x-nous-credits-* 响应头解析(agent/credits_tracker.py:1-23)。
6.6 MoA:一次回合多模型合议
/moa 开启 Mixture-of-Agents 模式:每次主模型迭代前,用线程池并发问最多 8 个「参考模型」(可以来自不同厂商),把它们的意见拼进本轮 user 消息,由主模型(aggregator)综合后再决定行动(agent/moa_loop.py:1-7,336,28)。各参考模型按自己的费率单独计价(:291-295)。工具调用与终止仍由主循环掌管——MoA 只是给主模型「开会」。
07上下文·记忆·学习循环
这是 Hermes 的招牌章节:上下文压缩解决「一次会话装不下」,记忆 + 技能 + curator 解决「跨会话越用越懂你」。
7.1 上下文压缩:先廉价裁剪,再 LLM 摘要
压缩引擎可插拔(agent/context_engine.py:32),内置实现 ContextCompressor(agent/context_compressor.py:662)。触发:prompt token ≥ 阈值(有效输入预算 × 50%,:956-958,912-942;比 Claude Code 的 92% 激进得多)。压缩分四相(:2670-2733 算法注释):
Phase 1:零成本裁剪
把旧工具结果替换成一行摘要(如 [terminal] ran `npm test` → exit 0, 47 lines output),重复读同一文件只留最新副本——不花一个 LLM token(context_compressor.py:1179-1254)。
Phase 2:定边界
保护头部(system prompt + 首个交换)与尾部(按 token 预算反向累计,约为阈值的 20%)(:2738-2742,859-860)。
Phase 3:LLM 摘要
把中间段交给(可配的更便宜的)摘要模型,产出固定 9 小节结构(Active Task / Goal / Constraints / Completed Actions / Active State / Blocked / Key Decisions / Relevant Files / Critical Context),预算 2000-12000 token,凭据必须写 [REDACTED](:1748-1795)。再次压缩是迭代更新上份摘要而非重写(:1799-1808)。
Phase 4:重组 + fork 会话
新列表 = 保护头 + 一条带 [CONTEXT COMPACTION — REFERENCE ONLY] 前缀的摘要消息 + 保护尾(:2874-3004)。同时结束当前 SQLite 会话、fork 一个 child 会话(parent_session_id 链接,end_reason='compression')——这就是 resume 要沿链找后代的原因(hermes_state.py:3936-3990)。
防抖设计:连续两次压缩各省不到 10% 就跳过;摘要 LLM 失败进 600 秒冷却;压缩失败还有确定性静态回退摘要兜底(context_compressor.py:1155-1172,2891-2900)。跨进程用 SQLite compression_locks 表 + 租约防止 CLI 和 gateway 同时压同一会话(agent/conversation_compression.py:75-120)。
7.2 双文件记忆:MEMORY.md 与 USER.md
内置记忆极简却讲究(tools/memory_tool.py):两个 Markdown 文件在 ~/.hermes/memories/——MEMORY.md(agent 自己的笔记:环境事实、项目约定)与 USER.md(对你的了解),条目用 § 分隔(:16,56-59)。字符上限而非 token(模型无关):2200 / 1375 字符(:130,804-805),系统提示里带用量表头(MEMORY [63% — 1,400/2,200 chars],:664-680)逼模型定期整理。模型通过单一 memory 工具 add/replace/remove。关键设计——冻结快照:会话开始时读入注进 system prompt,中途写盘立即持久化但不改提示(保 prompt cache),下个会话才见新版(:11-14,615-626)。
外部记忆走 MemoryProvider 抽象(agent/memory_provider.py:43,Honcho / Hindsight / Mem0 等作为 plugins/memory/ 插件,同时只允许挂一个防 schema 膨胀):每回合前后台 prefetch(query) 召回、回合后 sync_turn 异步写、压缩丢历史前有 on_pre_compress 抢救洞见(:176-230)。召回内容注入本轮 user 消息而非 system prompt——又是为了缓存。
7.3 自我改进循环:nudge → skill → curator
README 说 Hermes 是「the only agent with a built-in learning loop」,源码里这个循环由四件套构成:
- 回合末复盘触发:
finalize_turn末尾的 memory/skill review 触发器(agent/turn_finalizer.py:9-10),加上系统提示里的 skill nudge 计数器——每隔 N 轮没用skill_manage就提醒模型「要不要沉淀点什么」(conversation_loop.py:692-694)。 - agent 自创技能:模型用
skill_manage工具写 SKILL.md(tools/skill_manager_tool.py:1303-1364);/learn命令则构造一段提示让 agent 用现有工具收集素材再写成规范技能(agent/learn_prompt.py:99)。 - curator 后台园丁:agent 闲置 ≥2 小时且距上次运行 ≥7 天时,fork 一个用便宜辅助模型的 AIAgent 审查技能库——按活跃时间戳流转生命周期(30 天标 stale、90 天归档)、合并、修补(agent/curator.py:6,146-157,1480)。严格不变量:只碰 agent 自建技能、只归档从不删除、pinned 豁免(:15-20)。归属由 contextvar 区分:用户让前台 agent 写的技能永不被自动 curate(tools/skill_provenance.py:3-14)。
- 会话全文检索:
session_search工具在 SQLite FTS5 上做跨会话召回(DISCOVERY / SCROLL / BROWSE 三模式),并把 cron 等自动化会话降权防止淹没交互记忆(tools/session_search_tool.py:6-52)。
桌面端还把这个循环可视化:learning graph 把「非内置技能 + 记忆条目」画成节点图,技能间边来自 frontmatter 的 related_skills,记忆→技能边靠词法重叠推导(agent/learning_graph.py:254-323,156-245)。hermes insights 则对标 Claude Code 的 /insights,从 state.db 出 token/成本/工具使用报告(agent/insights.py:1-11)。
08UI 层:真 Ink 的 Node TUI + 经典 Python REPL
Hermes 有两套终端界面:老而稳的 prompt_toolkit REPL,和新的 React/Ink TUI。有趣的对照:Claude Code fork 了 Ink,Hermes 直接用 npm 上的 Ink v6,只加了一个薄补丁包。
8.1 ui-tui 技术栈
ui-tui/package.json:19-27 说明一切:ink ^6.8.0(npm 原版,不是 fork)+ react ^19 + nanostores(轻量状态库,替代 Redux)+ 本地补丁包 @hermes/ink(file:./packages/hermes-ink,覆盖 Ink 的部分行为并暴露 FrameEvent)。开发跑 tsx src/entry.tsx,发布用 esbuild 打成 dist/entry.js。入口 ui-tui/src/entry.tsx:1 的 shebang 就带着性能预算:node --max-old-space-size=8192 --expose-gc;开头一段注释专门处理「上个 TUI 被 kill -9 后终端还开着鼠标追踪模式」的惨案(entry.tsx:26-40,resetTerminalModes 挂 exit 兜底)。
8.2 与 Python 的协议:stdio 上的 JSON-RPC 2.0
Node 端 GatewayClient spawn python -m tui_gateway.entry(ui-tui/src/gatewayClient.ts:356),Python 侧的主循环就是逐行读 stdin(tui_gateway/entry.py:357-373):
for raw in sys.stdin:
# 每行一个 JSON-RPC 2.0 请求
resp = dispatch(req, transport) # tui_gateway/server.py:1160 按 method 路由
if not write_json(resp): break # 响应写回 stdout
dispatch(tui_gateway/server.py:1160-1180)把慢方法(OAuth device flow 等)丢线程池(_LONG_HANDLERS,:178),快方法同步答。RPC 面有多宽?ui-tui/src/gatewayTypes.ts 里导出了 76 个接口/类型:会话创建/恢复/活跃列表、斜杠命令目录与补全、配置读写、计费卡片、setup 状态……tui_gateway/server.py 高达 13873 行——比引擎的 conversation_loop 还长两倍多,因为 TUI 把设置、计费、模型选择这些原本散在 CLI 各处的功能全部 RPC 化了。agent 的流式输出走同一管道反向推送:AIAgent 的回调(stream_delta / tool_progress / thinking)被 server 序列化成事件行,Node 端渲染。
8.3 经典 REPL 与 Python 侧渲染
不开 TUI 时是纯 Python 界面:prompt_toolkit 管输入(多行、历史、补全、「运行中输入」实现 /steer),agent/display.py(1426 行)管输出——工具调用的单行预览(emoji + 截断参数)、diff 上色(display.py:36-89)、皮肤/工具 emoji 映射(:122-139)、流式「响应盒子」边框绘制(cli.py 内的 ╭─ ⚕ Hermes ─╮ 盒子,cli.py:12180-12186)。hermes_cli/curses_ui.py:1-5 则只是 hermes tools/hermes skills 用的 curses 多选清单组件,不是聊天界面。
8.4 输入回传与中断
- 提交链:TUI 输入框回车(ui-tui/src/app/submissionCore.ts:75 prompt.submit)→ GatewayClient 发 JSON-RPC → server 调
agent.run_conversation(...)(tui_gateway/server.py:8674)→ 事件流回推;经典 REPL 则如第 1 章步骤 2,在本进程线程里直接调。 - 中断:两套外壳殊途同归——把
agent._interrupt_requested置位 + 掐断当前流式请求(run_agent.py:4734 _interruptible_streaming_api_call 轮询该标志),循环顶检查后带部分结果退出(agent/conversation_loop.py:638-643)。TUI 侧从 ui-tui/src/app/turnController.ts:298 的 session.interrupt 发起,落到 tools/interrupt.py:39-52 的线程级中断信号,让长工具(浏览器、终端)也能响应。 - 运行中打字 = /steer:不打断、不丢弃,排队到下一次 API 调用前注入(第 3 章 3.5)。
Web 与桌面:web/ 是 Vite + React 的 dashboard(管网关、看会话,用 xterm.js 投影 PTY 终端);桌面 App 是 Electron + Vite + React,经 WebSocket /api/ws 连自带的 hermes serve 后端(第 10 章)。四套 UI(REPL/TUI/Web/桌面)共享同一个 Python 引擎——这是「回调式循环」设计换来的红利。
09扩展生态:Skills、MCP、Plugins、Hooks、多 Agent
和 Claude Code 一样,所有扩展收敛为「注入对话的文本」或「模型可调用的工具」两种本质;不同的是 Hermes 的 skill 有完整的分发市场、信任分级和 agent 自建通道。
9.1 Skills:渐进披露 + 自建 + 市场
技能 = SKILL.md(YAML frontmatter:name/description/version/platforms/related_skills)+ 可选 references/templates/scripts 子目录,存放于仓库 skills/(内置 18 类)、optional-skills/(按需装)和 ~/.hermes/skills/(用户/agent 自建)。触达模型有两条路,都是显式注入而非暗中触发:
- 渐进披露:系统提示只放「名字 + 一句话」索引(agent/system_prompt.py:260-289),模型需要时调
skills_list(tier-1 清单)→skill_view(tier-2 全文 + 支撑文件,tools/skills_tool.py:689,864)。与 Claude Code 的 SkillTool 思路相同。 - 斜杠直载:
/skill-name把 SKILL.md 全文内联成一条消息,附技能目录路径和配置(agent/skill_commands.py:245-334);bundle(~/.hermes/skill-bundles/*.yaml)可一次装载一组(agent/skill_bundles.py:1-10)。frontmatter 支持 inline_shell(渲染时执行内嵌 shell)。
分发侧是 Hermes 独有的重头戏:tools/skills_hub.py:424 的 SkillSource 抽象下挂 8 个源——GitHub、skills.sh、ClawHub、Claude marketplace、LobeHub、well-known URL 等,兼容 agentskills.io 开放标准;scripts/build_skills_index.py 定期爬全部源产出静态索引给 hermes skills search 用(免打 API)。装机必过 skills_guard 信任矩阵(第 5 章)+ prompt-injection 扫描 + 不可见 Unicode 检测(tools/skills_guard.py:797-926)。
9.2 MCP:双向——既当 client 又当 server
- Client 侧(tools/mcp_tool.py,5308 行):stdio / StreamableHTTP / SSE 三种 transport;启动时
asyncio.gather并行连全部 server(外层 120 秒超时,:4577-4643);每个远程工具注册为mcp__<server>__<tool>(:4199)进同一个 registry,与内置工具无差别调度;支持tools/list_changed运行期动态刷新(:1570-1725);OAuth 走 PKCE + 动态客户端注册,token 存~/.hermes/mcp-tokens/,后台线程绝不弹浏览器(tools/mcp_oauth.py:132-202)。安全面:stdio 子进程环境变量白名单、远程 URL 校验(禁 file://)、工具描述注入扫描、OSV 恶意包预检。 - Server 侧(mcp_serve.py:52,564-712):
hermes mcp serve用 FastMCP 把 Hermes 自己暴露成 MCP server——Claude Code / Cursor 可以列 Hermes 的会话、读历史、发消息、回应审批(9+1 个工具,对齐 OpenClaw 桥面)。也就是说你可以在 Claude Code 里把 Hermes 当一个可编程的常驻 agent 使。
9.3 Plugins:四种来源、五种能力
插件 = plugin.yaml + __init__.py::register(ctx),来源有四:仓库内置 plugins/、~/.hermes/plugins/、配置指定路径、Python entry-point 组 hermes_agent.plugins(hermes_cli/plugins.py:5-20)。ctx 能注册:工具(覆盖内置工具需 operator 显式授权,防第三方插件静默替换 write_file 提权,:451-470)、hook 回调、消息平台适配器、模型 provider,还能借宿主的 LLM 凭据跑推理(ctx.llm,:349)。内置 20 个插件:browser / kanban / memory / spotify / observability / 各消息平台 / 各模型厂商——Hermes 把自己的大量一等功能也实现成插件,狗粮吃得很彻底。
9.4 Hooks:两套总线 + shell 桥
| 体系 | 事件 | 能否阻断 |
|---|---|---|
| Plugin hook 总线(hermes_cli/plugins.py:135-215) | 约 30 个:pre/post_tool_call、pre/post_llm_call、transform_tool_result、on_session_*、subagent_*、pre_gateway_dispatch、pre_verify… | pre_tool_call 返回 {"action":"block"} 可拦工具;pre_gateway_dispatch 可 skip/rewrite;pre_verify 可「不让 agent 停」(对标 Claude Code 的 Stop hook exit 2);审批/看板 hook 只观察 |
| Gateway HOOK.yaml(gateway/hooks.py:81-209) | ~/.hermes/hooks/<dir>/HOOK.yaml + handler.py,事件字符串如 agent:start、支持通配 | emit_collect 的返回值可做 allow/deny/rewrite;hook 报错绝不阻断主管线 |
| Shell hooks 桥(agent/shell_hooks.py:4-45) | config.yaml 的 hooks: 块把 shell 脚本挂进 plugin 总线 | 兼容 Claude Code 风格 {"decision":"block"} 输出;平局时 Python 插件的裁决胜过 shell |
9.5 多 Agent:delegate 之外还有 Kanban Swarm
除第 4 章的 delegate_task(进程内 new AIAgent),Hermes 还有一套进程级多 agent 编排:hermes kanban 维护一个共享 SQLite 看板(~/.hermes/kanban.db),swarm 模式往看板写一个任务图——规划根节点 → 并行 specialist workers → verifier → synthesizer(hermes_cli/kanban_swarm.py:1-14);LLM 负责把大任务扇出成子任务图并按「花名册」分派(hermes_cli/kanban_decompose.py:1-13);dispatcher 为每张就绪卡 fire-and-forget 一个独立的 hermes chat 子进程(hermes_cli/kanban_db.py:7811 subprocess.Popen),黑板通信走看板评论。worker 的 kanban 工具只在 HERMES_KANBAN_TASK 环境变量存在时注册,普通会话看不到(tools/kanban_tools.py:1-8)。
9.6 Cron:会思考的定时任务
网关内每 60 秒一次 tick()(文件锁防多进程重叠),到期任务两种执行法:agent job 起完整 AIAgent 跑一个自然语言 prompt(cron 会话禁用 cronjob 工具防套娃);脚本 job 直接 subprocess 跑、零 token(cron/scheduler.py:2-8,2947-3007,1917-2003,116)。任务存 ~/.hermes/cron/jobs.json,输出按 job 归档为 markdown,投递路由可指到任何平台频道(cron/jobs.py:2-6 · gateway/delivery.py:1-9)。agent 自己通过单一 cronjob 工具增删改查(tools/cronjob_tools.py:1-6)——「每天早上八点给我 Telegram 发日报」一句话就能落地。
10Gateway 与多形态部署:Hermes 最独特的子系统
Claude Code 活在你的终端里;Hermes 被设计成「活在服务器上、从任何聊天软件召唤」。gateway/ 就是那台永动机。
10.1 进程模型:单 asyncio 进程、per-session 缓存 AIAgent
python -m gateway.run(或 hermes gateway start 托管为 systemd/launchd 服务)启动一个 asyncio 事件循环进程(gateway/run.py:20411-20451),GatewayRunner(:2738)拉起所有已配置的平台适配器。agent 就在网关进程内跑:每个会话缓存一个 AIAgent 实例以保住 prompt cache(run.py:2914-2918,16491)。路由键 = "{platform}:{chat_id}"(run.py:3176),会话上下文用 ContextVar 承载(gateway/session_context.py:149)。设 GATEWAY_PROXY_URL 可退化成薄中继,把消息转发给远端 Hermes API server(run.py:16131-16153)。
10.2 平台矩阵:内置 + 插件共 30 余个
| 形态 | 平台 | 位置 |
|---|---|---|
| 内置适配器 | Signal、微信个人号、iMessage(BlueBubbles)、WhatsApp Cloud、腾讯元宝、QQ 机器人、通用 Webhook、MS Graph、HTTP API server | gateway/platforms/*.py(基类 base.py 245KB) |
| 插件适配器 | Telegram、Discord、Slack、飞书、钉钉、企业微信、WhatsApp、Teams、Matrix、Mattermost、LINE、IRC、email、SMS、ntfy、Google Chat…共 20 个 | plugins/platforms/*/adapter.py::register(ctx) |
平台注册走 PlatformEntry 元数据 + 懒加载(gateway/platform_registry.py:39,187——不能启动就 import 20 个平台 SDK,否则每次 hermes 都慢几秒)。每个 entry 声明依赖探针、所需 env、授权 env、消息长度上限、cron 投递默认频道、进程外投递函数等(Discord 示例见 plugins/platforms/discord/adapter.py:7879-7913)。出站侧还有一个有趣设计:模型可以回 NO_REPLY 静默 token 主动选择「这条不用回」(gateway/response_filters.py:14-19)。
10.3 谁能命令你的 agent:默认拒绝 + 配对码
入站消息先过 _is_user_authorized(gateway/authz_mixin.py:249-495):默认 deny,按序查系统事件豁免 → relay 上游委托(严格 is True 判断防 mock 提权)→ 群聊 chat allowlist → bot 白名单 → per-platform *_ALLOW_ALL_USERS / *_ALLOWED_USERS env → 配对存储。配对(gateway/pairing.py:5-18):陌生人私聊 bot 会收到一次性 8 位码(32 字符无歧义字母表、密码学随机、1 小时过期、每平台最多 3 个待批、失败 5 次锁 1 小时),bot 主人在 CLI/dashboard 批准——审批入口入站消息永远够不到,从结构上封死「在 Telegram 里说服 agent 给自己开门」的注入路径。
10.4 常驻进程的自我保养
scale-to-zero
无在飞回合 + N 分钟无入站 + 无后台工作 → go_dormant() 关 socket 休眠;在 Fly.io 上由平台挂起、wakeUrl 唤醒——README「idle 时几乎零成本」的实现(gateway/scale_to_zero.py:1-24)
drain 排水
刻意不给运行中网关开 HTTP 控制通道;dashboard 想让它优雅下线只能写标记文件 ~/.hermes/.drain_request.json,由后台 watcher 轮询响应(gateway/drain_control.py:16-24)
重启风暴熔断
agent 自己跑了会杀死自己的命令 → supervisor 拉起 → 自动恢复会话 → 又执行同一命令…跨进程持久化开机时间戳,滚动窗口内重启过多即跳过自动恢复、把人拉回环里(gateway/restart_loop_guard.py:1-24)
relay 零接触注册
hermes gateway enroll 用 Nous Portal token 证明租户身份,向连接器换取 per-gateway secret——自托管网关不暴露公网也能接收消息(hermes_cli/gateway_enroll.py:1-31 · gateway/relay/ WebSocket 传输)
10.5 桌面端、批处理与训练管线
- Electron 桌面 App(apps/desktop/package.json:9):React + Electron 壳,不重写 agent——启动时拉起自带的 headless 后端
hermes serve(就是解耦命名的 gateway,apps/desktop/electron/backend-command.cjs:5-12)。首装引导器反而是 Tauri 写的(apps/bootstrap-installer/package.json,Rust src-tauri/),图它体积小。另有web/(Vite+React 的 dashboard 前端)。 - batch_runner.py:数据集批量跑 agent、multiprocessing 并行、断点续跑、按 ShareGPT 格式存轨迹(batch_runner.py:1-20,325)——Nous 拿它给下一代工具调用模型造训练数据;
trajectory_compressor.py离线把轨迹压到训练 token 预算。mini_swe_runner.py是 SWE 风格任务 runner(复用 local/docker/modal 执行环境;是否直接对接 SWE-bench 榜单未核实)。
11文件索引速查
按「我想看 X」检索,路径相对仓库根。
| 我想看… | 去这里 |
|---|---|
| 进程入口 / 子命令分发 | pyproject.toml:307 · hermes_cli/main.py:12686,14030 · hermes_bootstrap.py:59,125 |
| 经典 REPL / 聊天回合入口 | cli.py:3662(HermesCLI)· :13036(run)· :12262(run_conversation 调用) |
| TUI 三段式进程链 | hermes_cli/main.py:1984,2117 · ui-tui/src/gatewayClient.ts:356 · tui_gateway/entry.py:9 |
| Agent 类 / 主循环 | run_agent.py:393(AIAgent)· agent/conversation_loop.py:518,633 · agent/turn_context.py · agent/turn_finalizer.py:30 |
| System prompt 三层 | agent/system_prompt.py:113 · agent/prompt_builder.py:1827-1935(HERMES.md/AGENTS.md/CLAUDE.md)· agent/coding_context.py:623 |
| 工具注册表 / toolsets | tools/registry.py:78,208,356,574 · toolsets.py:31,95 · model_tools.py:279 |
| 工具执行 / 并发判定 | run_agent.py:5631 · agent/tool_executor.py:306,965 · agent/tool_dispatch_helpers.py:45,104 |
| 结果截断 / 落盘 / 回合预算 | tools/tool_output_limits.py:39 · tools/tool_result_storage.py:144,203 |
| 终端六后端 | tools/terminal_tool.py:1216 · tools/environments/{local,docker,ssh,modal,daytona,singularity}.py |
| 审批 / 危险命令分级 | tools/approval.py:365,546,1662,1796,1846,2299 · tools/write_approval.py:264 |
| 路径 / URL / 内容安全 | agent/file_safety.py:28,80 · tools/url_safety.py:154 · tools/tirith_security.py:730 · tools/threat_patterns.py |
| Provider 抽象 / 适配器 | providers/base.py:38 · agent/transports/chat_completions.py:225,459 · agent/{anthropic,bedrock,gemini_native,codex_responses}_adapter.py |
| 流式消费 / 重试 / fallback | agent/chat_completion_helpers.py:2034,2106,1172 · agent/error_classifier.py:24,515 · agent/retry_utils.py:28 · agent/turn_retry_state.py:32 |
| Prompt caching / 计价 / 凭据池 | agent/prompt_caching.py:84 · agent/usage_pricing.py:733 · agent/models_dev.py · agent/credential_pool.py:508,1485 |
| 上下文压缩 | agent/context_engine.py:32 · agent/context_compressor.py:662,1136,2670 · agent/conversation_compression.py:435 |
| 记忆 / 学习循环 | tools/memory_tool.py:16,130,615 · agent/memory_provider.py:43 · agent/curator.py:6,1480 · agent/learn_prompt.py:99 · tools/session_search_tool.py |
| 会话持久化 / resume | hermes_state.py:123,700,748,3936 · agent/trajectory.py |
| Skills 全家桶 | tools/skills_tool.py:689,864 · tools/skill_manager_tool.py:1303 · tools/skills_hub.py:424 · tools/skills_guard.py:51 · agent/skill_commands.py:245 |
| MCP client / server | tools/mcp_tool.py:4577,4436,4199 · tools/mcp_oauth.py:132 · mcp_serve.py:52,564 |
| Plugins / Hooks | hermes_cli/plugins.py:135,1277,2042 · gateway/hooks.py:81 · agent/shell_hooks.py:4 |
| 子 agent / swarm / cron | tools/delegate_tool.py:1301,1921,2342 · tools/async_delegation.py:124 · hermes_cli/kanban_swarm.py:77 · cron/scheduler.py:2947 |
| Gateway 全景 | gateway/run.py:2738,3176,16491 · gateway/platform_registry.py:39 · gateway/authz_mixin.py:249 · gateway/pairing.py:5 · gateway/relay/ |
| 桌面端 / 批处理 | apps/desktop/electron/backend-command.cjs:5 · apps/bootstrap-installer/ · batch_runner.py:325 · trajectory_compressor.py:1 |