# Hermes Agent 源码分析（写给计算机初学者）

> **分析对象**：[NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent) v0.19.0
> **基于 commit**：`d9165d7a678d4105f42921a7fc1886df3804531b`（2026-07-23，`fix: resolve current entry unlocked in try_refresh_matching no-hint branch`）
> **分析日期**：2026-07-24
> **代码规模**：约 3300 个 Python 文件（后端核心）+ 约 1600 个 TypeScript/TSX 文件（TUI、桌面端、Web 仪表盘）
> **阅读建议**：本文与一个"Claude Code 源码分析"系列对标，共 12 章。每一章都尽量做到：先打比方 → 再看真实源码（带 `文件:行号`）→ 逐行解释 → 说设计取舍。文中所有"未找到/不支持"的表述都是真实核查后的结论，没有编造。

---

> 🗺️ **配套架构图**：本项目在[**七大 Agent 架构图库**](../架构图库.html#ch1)里有一张专门的图——**控制/数据平面 + 状态归属双时间线（记忆写盘立即生效、但下个会话才进提示词）**。
> 图库的每张图都先写清「回答什么问题」和「承重墙论点」，并经三轮审阅与渲染验收。

## 第 1 章 项目概览：这是一个"会自我进化的 personal agent"

### 1.1 一句话说明白

**Hermes Agent 是 Nous Research 开源的、以"自我改进学习闭环"为最大卖点的个人 AI agent**：它不只回答问题，还会把成功经验沉淀成"技能（skill）"、把事实写进"记忆（memory）"、能搜索自己过去的对话，并且不只活在终端里——它能同时挂在 Telegram、Discord、Slack、WhatsApp 等约 20 个消息平台上，跑在 5 美元的 VPS 或 GPU 集群上。

README 开篇第一句就点明了定位（`README.md:19`）：

> **The self-improving AI agent built by Nous Research.** It's the only agent with a built-in learning loop — it creates skills from experience, improves them during use, nudges itself to persist knowledge, searches its own past conversations, and builds a deepening model of who you are across sessions.

### 1.2 谁在做

**Nous Research**——一家以开放权重模型闻名的 AI 研究公司（Hermes 系列微调模型就是他们训练的）。这解释了项目的两个"研究气质"：

- 内置**轨迹批量生成与压缩**工具（`batch_runner.py`、`trajectory_compressor.py`），用来给下一代工具调用模型生产训练数据；
- 模型层完全开放，默认对接自家的 **Nous Portal**（300+ 模型），但也支持 OpenRouter、OpenAI、自托管端点等 30 多种 provider。

### 1.3 技术栈

| 层 | 技术 | 位置 |
|---|---|---|
| 后端核心（agent 循环、工具、网关） | Python 3.11–3.13（`pyproject.toml:13` 要求 `>=3.11,<3.14`） | 仓库根目录与 `agent/`、`tools/`、`gateway/` 等 |
| 终端 UI（TUI） | TypeScript + React + Ink | `ui-tui/` |
| 桌面应用 | Electron + React（TS） | `apps/desktop/` |
| Web 仪表盘 | Vite + React（TS） | `web/` |
| 包管理 | uv（全部依赖**精确版本锁定**，无版本范围） | `pyproject.toml`、`uv.lock` |

注意一个反直觉的点：**Python 才是大脑，TypeScript 只是脸**。TUI 的 README 写得很直白（`ui-tui/README.md`）："TypeScript owns the screen. Python owns sessions, tools, model calls, and most command logic."——TS 端通过 stdio 上的换行分隔 JSON-RPC 调用 Python 进程（`tui_gateway/`）。

### 1.4 与 Claude Code 的异同（一句话级）

- **相同**：都是"模型 + 工具调用循环"的 agent，都有 slash 命令、AGENTS.md/CLAUDE.md 上下文文件、子代理、MCP、技能系统。
- **不同（一句话）**：Claude Code 是一个**住在终端里的编程助手**（闭源、绑定 Anthropic 模型）；Hermes Agent 是一个**住在消息软件里、跨会话自我学习、模型随便换的全平台个人 agent**（MIT 开源、模型无关）。

```mermaid
flowchart LR
    subgraph CC["Claude Code"]
        A1[终端编程助手] --> A2[绑定 Anthropic 模型]
    end
    subgraph HA["Hermes Agent"]
        B1[个人 agent + 学习闭环] --> B2[30+ 模型供应商任选]
        B1 --> B3[约 20 个消息平台接入]
        B1 --> B4[技能/记忆/会话搜索 自我改进]
    end
    CC -.->|都对标| HA
```

---

## 第 2 章 全景架构：一个 Python 大脑，许多张脸

### 2.1 分层总览

Hermes 的架构可以用一句话概括：**一个共享的 Python agent 内核，外面套了 N 个界面壳**。`AGENTS.md:9-14`（给开发者的设计指南）原文：

> Hermes is a personal AI agent that runs the same agent core across a CLI, a messaging gateway (Telegram, Discord, Slack, and ~20 other platforms), a TUI, and an Electron desktop app. … It is extended primarily through **plugins and skills**, not by growing the core.

```mermaid
flowchart TB
    subgraph UI["界面层（多张脸）"]
        CLI["cli.py<br/>prompt_toolkit REPL"]
        TUI["ui-tui/ (TS: React+Ink)<br/>──JSON-RPC/stdio──▶ tui_gateway/ (Python)"]
        DESK["apps/desktop/<br/>Electron"]
        WEB["web/<br/>Vite 仪表盘"]
        ACP["acp_adapter/<br/>ACP 服务器（给 Zed 等编辑器）"]
        GW["gateway/<br/>消息网关"]
    end

    subgraph INPUT["输入与分流"]
        SLASH["slash 命令注册表<br/>hermes_cli/commands.py"]
        PLAT["plugins/platforms/<br/>telegram/discord/slack/whatsapp/signal/email…约20个"]
    end

    subgraph CORE["核心（Python 大脑）"]
        AGENT["run_agent.py : AIAgent 类"]
        LOOP["agent/conversation_loop.py<br/>run_conversation() 主循环"]
        CTX["agent/system_prompt.py<br/>三层系统提示词"]
        MODEL["model_tools.py<br/>工具分发 handle_function_call()"]
    end

    subgraph TOOLS["工具与能力层"]
        REG["tools/registry.py 注册表<br/>74 个内置工具"]
        ENV["tools/environments/<br/>local/docker/ssh/singularity/modal/daytona"]
        MCP["tools/mcp_tool.py<br/>MCP 客户端"]
        PLUGIN["plugins/<br/>provider/平台/看板等插件"]
    end

    CLI --> SLASH --> AGENT
    TUI --> AGENT
    DESK --> GW
    WEB --> GW
    ACP --> AGENT
    GW --> PLAT --> AGENT
    AGENT --> LOOP --> MODEL --> REG
    LOOP --> CTX
    REG --> ENV
    REG --> MCP
    REG --> PLUGIN
```

### 2.2 关键目录速查

| 目录/文件 | 角色 |
|---|---|
| `run_agent.py`（6907 行） | `AIAgent` 类本体（`run_agent.py:400`），agent 的"身份证 + 工具箱" |
| `agent/conversation_loop.py`（6163 行） | 主循环 `run_conversation()`（第 669 行），全项目的心脏 |
| `agent/`（~160 个 .py 文件，含 `__init__.py`） | 系统提示词、压缩、预算、适配器（anthropic/gemini/bedrock/vertex…） |
| `tools/`（102 个文件） | 74 个内置工具的实现 |
| `toolsets.py` | 工具集（toolset）定义：把工具分组，按平台/场景开关 |
| `hermes_cli/`（160+ 文件） | `hermes` 命令行程序本身（setup、auth、model、doctor…） |
| `cli.py`（16590 行） | 交互式终端 REPL（最大的单文件） |
| `gateway/` + `plugins/platforms/` | 消息网关与约 20 个平台适配器 |
| `acp_adapter/` | ACP 协议服务器 |
| `plugins/model-providers/` | 33 个模型供应商插件 |
| `cron/` | 内置定时任务调度器 |
| `skills/` | 内置技能库（软件工程、数据科学、智能家居……） |
| `ui-tui/`、`apps/desktop/`、`web/` | 三种 TS 界面 |

### 2.3 acp_adapter 是什么？

**ACP = Agent Client Protocol**（由 Zed 编辑器主导的、编辑器 ↔ AI agent 的标准协议，类比语言服务器的 LSP）。`acp_adapter/server.py:1` 的注释只有一行："ACP agent server — exposes Hermes Agent via the Agent Client Protocol."。入口在 `acp_adapter/entry.py`：启动后把 stdout 专门留给 ACP 的 JSON-RPC 传输，日志全走 stderr。这样 **Zed 等支持 ACP 的编辑器可以直接把 Hermes 当成内嵌 agent 后端**来驱动——这是 Claude Code 也有的能力（Claude Code 同样支持 ACP），两个项目在这个协议上站在了同一条生态线上。

---

## 第 3 章 启动流程：从 `hermes` 命令到聊天界面

### 3.1 入口在哪里

Python 包的命令行入口定义在 `pyproject.toml:308-311`：

```toml
[project.scripts]
hermes = "hermes_cli.main:main"        # pyproject.toml:309
hermes-acp = "acp_adapter.entry:main"  # pyproject.toml:311
```

也就是说，你在终端敲 `hermes`，实际执行的是 `hermes_cli/main.py` 里的 `main()` 函数（`hermes_cli/main.py:14339`）。

### 3.2 main() 开头做了一堆"自救"

这个 `main()` 的开头非常有意思——它先不解析参数，而是连环做**自我修复**（`hermes_cli/main.py:14339-14384`）：

```python
def main():                                  # main.py:14339
    _set_process_title()                     # 14343: 让 ps/top 里显示 'hermes' 而不是 'python3.11'
    configure_windows_stdio()                # 14348: Windows 上强制 UTF-8，防中文乱码崩溃
    _cleanup_quarantined_exes()              # 14356: 清扫上次 hermes update 留下的旧 exe
    if "update" not in sys.argv[1:]:         # 14371
        _recover_from_interrupted_install()  # 14372: 上次更新被 Ctrl+C 打断？先把 venv 修好
    ...
    parser, subparsers, chat_parser = build_top_level_parser()  # 14383: 现在才开始解析参数
    chat_parser.set_defaults(func=cmd_chat)                     # 14384: 默认子命令 = 聊天
```

逐行解释：

1. **改进程名**：纯粹为了用户体验，让你在 `htop` 里能认出它；
2. **UTF-8 修复**：Windows 控制台默认编码不是 UTF-8，打印中文/emoji 会直接 `UnicodeEncodeError` 崩掉，所以必须在任何 `print` 之前修好；
3. **venv 自愈**：如果上一次 `hermes update` 在 `git reset --hard` 和 `uv pip install` 之间崩溃，环境会处于"新代码引用旧包"的半坏状态。`main()` 在导入任何第三方库**之前**先用纯标准库模块 `_early_recovery` 修一遍（`main.py:67-84` 的注释把原因写得很坦诚：不修的话用户连 `hermes update` 都跑不了，永远无法自愈）。

这是"长期运行在个人电脑上、会自我更新"的软件特有的防御性设计——Web 服务挂了有运维，个人 CLI 挂了只能靠自己爬起来。

### 3.3 参数解析与聊天入口

`main()` 用 argparse 注册了几十个子命令（`model`、`gateway`、`setup`、`cron`、`doctor`、`claw migrate`……），然后默认进入 `cmd_chat`（`main.py:2435`）。

进入聊天前，CLI 还要完成一长串准备：读取 `~/.hermes/config.yaml` 与 `.env`、解析当前 provider 的运行时凭证（必要时自动刷新 OAuth token）、按配置启用工具集、创建 `AIAgent` 实例、恢复上次会话（`/resume`）、跑启动安全检查（`security_audit_startup.py`），最后才启动 prompt_toolkit 输入界面。聊天流程大致是：

```mermaid
sequenceDiagram
    participant U as 用户
    participant M as hermes_cli/main.py: main()
    participant R as _early_recovery
    participant C as cli.py 交互循环
    participant A as run_agent.AIAgent
    participant L as conversation_loop

    U->>M: 敲 `hermes`
    M->>R: venv 自愈 / UTF-8 / 清旧文件
    M->>M: argparse 解析（默认 cmd_chat）
    M->>C: 加载 config.yaml + .env，创建 AIAgent
    C->>A: AIAgent(base_url, api_key, model, toolsets...)
    C->>C: prompt_toolkit 启动输入界面
    loop 每一轮对话
        C->>L: run_conversation(user_message)
        L-->>C: 流式输出 + 工具执行
    end
```

### 3.4 认证环节

认证配置住在两个地方（`hermes_cli/auth.py:1-16` 的模块注释）：

- `~/.hermes/.env`：传统 API key（OpenRouter、OpenAI、自定义端点……）；
- `~/.hermes/auth.json`：OAuth 凭证状态（**Nous Portal** 走 OAuth device code 流程，`hermes setup --portal` 一条命令登录），带跨进程文件锁。

首次运行的 `hermes setup` 向导会引导你选 provider、填 key、选默认模型；没有认证信息时 `cmd_chat` 会先把你带进 setup 流程。如果检测到 `~/.openclaw`（同类项目 OpenClaw 的配置目录），还会主动提出**一键迁移**对方的人设、记忆、技能和 API key（`README.md:187-213`）——抢用户的方式都做到源码里了，很极客。

---

## 第 4 章 输入捕获与分流：你敲下的每个字符去了哪

### 4.1 CLI 侧：prompt_toolkit 收割输入

交互式终端的输入由 **prompt_toolkit** 负责（`cli.py:60-75` 一口气导入了它的 `Application`、`Layout`、`KeyBindings`、`TextArea` 等十几个组件），提供多行编辑、slash 自动补全、历史记录（`FileHistory`）、粘贴检测等完整终端体验。

输入到手之后，分流逻辑大致是这样一个判断链：

```mermaid
flowchart TD
    A[用户输入一行文本] --> B{以 / 开头?}
    B -->|是| C["查 COMMAND_REGISTRY<br/>hermes_cli/commands.py:64"]
    C -->|命中| D[本地执行 slash 命令<br/>/model /compress /new /yolo...]
    C -->|是 /<br/>skill 名| E["加载技能内容<br/>作为本轮提示"]
    B -->|否| F{看起来像文件路径?<br/>cli.py:3109}
    F -->|是| G[作为附件读取<br/>图片走视觉]
    F -->|否| H[普通消息]
    D --> Z[回到输入提示符]
    E --> H
    G --> H
    H --> I["agent.run_conversation()<br/>cli.py:12500"]
```

### 4.2 slash 命令：一张中央注册表

所有 slash 命令定义在一张注册表 `COMMAND_REGISTRY`（`hermes_cli/commands.py:64`）里，全文共 **83 个 `CommandDef`**。每条定义长这样（`commands.py:46-61`）：

```python
@dataclass(frozen=True)
class CommandDef:
    name: str            # 不带斜杠的正式名，如 "background"
    description: str     # 给人看的描述
    category: str        # 分类："Session"、"Configuration"…
    aliases: tuple = ()  # 别名，如 ("bg",)
    args_hint: str = ""  # 参数提示，如 "<prompt>"
    cli_only: bool = False       # 只在 CLI 可用
    gateway_only: bool = False   # 只在消息网关可用
```

设计精妙之处在于注释里那句话（`commands.py:3-5`）：**"Every consumer — CLI help, gateway dispatch, Telegram BotCommands, Slack subcommand mapping, autocomplete — derives its data from COMMAND_REGISTRY."** 也就是说，注册表是单一事实来源：Telegram 上的 bot 命令菜单、Slack 的子命令、CLI 的 Tab 补全，全部从这张表自动生成。加一条命令，五个界面同时生效。

举几个有代表性的命令（都来自 `commands.py`）：

| 命令 | 作用 | 分流到哪 |
|---|---|---|
| `/new` `/reset` | 开新会话 | 本地重建 session，不发给模型 |
| `/model [名]` | 换模型（会话级，`--global` 持久化） | 本地改配置 |
| `/compress` | 手动压缩上下文 | 调压缩器（见第 8 章） |
| `/background <提示>`（别名 `/bg`、`/btw`） | 把任务丢到后台跑 | 异步派生 agent 任务 |
| `/queue <提示>` | 不中断当前工作，排队到下一轮 | 本地队列 |
| `/steer <提示>` | 在下一次工具调用后悄悄插入指令 | 转向通道 |
| `/yolo` | 开关"免审批模式" | 本地开关 |
| `/reasoning [级别]` | 调整思考强度（none/minimal/low/medium/high/xhigh/max/ultra 八档） | 改推理配置 |

### 4.3 忙时输入：`/queue`、`/steer`、`/interrupt` 三种哲学

CLI 在 agent 正在工作时按回车，行为是可配置的（`/busy` 命令，选项 `queue|steer|interrupt`，`commands.py:171`）：

- **queue**：你继续说，它不打断，下轮一起看；
- **steer**：你的话在"下一个工具调用之后"被注入，模型中途改方向；
- **interrupt**：立刻停下听你说。

这比 Claude Code 的 Esc 中断 + 排队双模式多出一档"中途转向"，是对"长任务跑一半想补充要求"这个真实痛点的回应。

### 4.4 网关侧：消息平台的分流

消息网关（`gateway/`）收到 Telegram 等平台的消息后，分流逻辑在 `gateway/slash_commands.py` 里以一组 `_handle_xxx_command` 异步方法实现（如 `_handle_model_command` 在 `:1435`、`_handle_stop_command` 在 `:1070`）。`hermes_cli/commands.py:396-404` 的注释解释了网关的 **Level-2 handler** 机制：正在运行的任务中途收到 `/model`、`/compress` 这类命令时，不能粗暴打断，而是进 pending 队列等当前步骤结束再处理。

网关注册表还有一个体现工程细节的地方：不同平台对 bot 命令数量有硬上限，代码里直接写成了常量——Telegram 菜单最多 60 条（Bot API 硬上限 100，`commands.py:554-555`），Slack 最多 50 条（`:1133`）。注册表因此要给每个平台挑选"最值得暴露"的命令子集，而不是无脑全量同步。

文件附件也有专门处理：CLI 里输入一个路径（`cli.py:3109-3127` 识别 `/`、`~`、`./`、`C:\` 甚至带引号的路径），会被解析成附件；图片走视觉通道，多图还有适配窄终端的紧凑徽标（`cli.py:3158-3176`，连 Termux 小屏都考虑到了）。网关中则支持语音消息转文字（`agent/transcription_provider.py` 一族）。

**举三个分流实例**，帮你把上面的流程图落到实处：

1. 输入 `/model openrouter:anthropic/claude-sonnet-4` → 命中注册表 → 本地切换模型，**完全不发 API 请求**；
2. 输入 `./report.pdf 帮我总结` → 路径检测命中 → PDF 内容作为附件与"帮我总结"一起组成 user 消息进主循环；
3. agent 忙时输入 `顺便把结果发到邮箱` → 按 `/busy` 设置走 queue/steer/interrupt 之一；steer 模式下这句话会在下一个工具调用结束后被注入对话（`conversation_loop.py:823-830` 的 `_drain_pending_redirect` 就是干这个的）。

---

## 第 5 章 上下文组装：发给模型的"第一页"是怎么拼出来的

### 5.1 三层三明治

系统提示词的组装在 `agent/system_prompt.py`。模块注释（`system_prompt.py:10-19`）把它分成三层，用 `\n\n` 拼接：

```python
# Three tiers are joined with ``\n\n``:
#
# * ``stable``   — identity (SOUL.md or DEFAULT_AGENT_IDENTITY), tool
#   guidance, computer-use guidance, nous subscription block, ...
# * ``context``  — caller-supplied ``system_message`` plus context files
#   (AGENTS.md / .cursorrules / etc.) discovered under ``TERMINAL_CWD``.
# * ``volatile`` — memory snapshot, USER.md profile, external memory
#   provider block, timestamp/session/model/provider line.
```

```mermaid
flowchart TB
    subgraph SP["系统提示词 = 三层拼接"]
        S["stable 稳定层<br/>身份(SOUL.md 或默认身份) + 工具使用规约<br/>+ 按模型家族定制的操作指导 + 平台提示"]
        C["context 上下文层<br/>AGENTS.md / CLAUDE.md / .cursorrules<br/>+ 调用方给的 system_message"]
        V["volatile 易变层<br/>MEMORY.md 快照 + USER.md 用户画像<br/>+ 时间戳/会话/模型行"]
    end
    S --> C --> V
    V --> R["每次请求原样发送<br/>（前缀缓存命中 → 省钱）"]
```

**为什么分这三层？** 答案藏在 `AGENTS.md:19-23` 那条被全项目奉为圭臬的设计原则里：

> **Per-conversation prompt caching is sacred.**（每个会话的提示词前缀缓存是神圣不可侵犯的。）

上游 API 按"前缀"缓存计费：如果你每轮都改系统提示词，缓存全失效，用户账单翻倍。所以 Hermes 立了铁律——**系统提示词在一个会话的生命周期内逐字节稳定**（`AGENTS.md:90-91` 原话 "a system prompt that is byte-stable for the life of a conversation"）。记忆写了新内容？只落盘，不改提示词，下个会话才生效（`tools/memory_tool.py:9-12` 明确说明这个"冻结快照"模式）。稳定层放最前面（永远不变），易变层放最后面（变化最少化），这正是为了最大化缓存命中。

### 5.2 身份与人设：SOUL.md

默认身份是一段硬编码常量（`agent/prompt_builder.py:139-147`）：

```python
DEFAULT_AGENT_IDENTITY = (
    "You are Hermes Agent, an intelligent AI assistant created by Nous Research. "
    ...
)
```

用户可以在 `~/.hermes/SOUL.md` 写自定义人设（`load_soul_md()`，`prompt_builder.py:1875`），`/personality` 命令切换预设人格。

### 5.3 项目上下文文件：AGENTS.md / CLAUDE.md / .cursorrules

`agent/coding_context.py:86` 定义了会被自动读进上下文的文件：

```python
_CONTEXT_FILES = ("AGENTS.md", "CLAUDE.md", ".cursorrules")
```

也就是说 Hermes **直接兼容 Claude Code 的 CLAUDE.md**——你从 Claude Code 项目切过来，上下文文件不用改。此外 `coding_context.py` 还会做一件更聪明的事：**探测工作区的"项目指纹"**——认 `pyproject.toml`、`package.json`、`Cargo.toml` 等项目标记（`coding_context.py:76-83`），识别 lockfile 推断包管理器（uv/pnpm/yarn…，`:142-146`），从 `package.json` scripts 和 Makefile 里挖"验证命令"（test/lint/build，`:149-150`），把这些作为环境提示注入。

### 5.4 一个被低估的细节：按模型家族定制提示

`coding_context.py:171-187` 的 `_EDIT_FORMAT_GUIDANCE` 是笔者认为全项目最"懂行"的设计之一：

```python
_EDIT_FORMAT_GUIDANCE = {
    "patch": (("gpt", "codex"),
        "- Edit format: ... use `patch` with `mode='patch'` (V4A diff) ..."),
    "replace": (("claude", "sonnet", "opus", "haiku",
        "gemini", "gemma", "deepseek", "qwen", "kimi", "glm", "grok",
        "hermes", "llama", "mistral", "devstral", "minimax"),
        "- Edit format: ... prefer `patch` in `mode='replace'` ..."),
}
```

含义：GPT/Codex 系模型在它们官方 harness（codex-rs）里只学过 apply_patch 格式的编辑，就提示它用 V4A diff；而 Claude、Gemini、DeepSeek、Qwen、Kimi 等模型在训练时多见"字符串查找替换"式编辑器，就提示它用 replace 模式。**同一份工具，给不同模型不同的使用说明书**——因为注释里说得很清楚："Matching the edit tool format to how a model was trained reduces mistakes and wasted reasoning"（贴合模型的训练分布，能减少错误和无效推理）。模型无关的 agent 做到这个深度，少见。

### 5.5 环境信息与记忆注入位置

易变层里注入：MEMORY.md 与 USER.md 的快照（见第 8 章）、外部记忆（Honcho，见第 8 章）、当前时间戳/会话 ID/模型名。注入位置固定在系统提示词末尾——同样是为了前缀缓存。

---

## 第 6 章 Agent 主循环（心脏）：一个近 5500 行的函数

### 6.1 循环本体在哪

整个 agent 的核心是 `agent/conversation_loop.py` 里的 `run_conversation()`（**第 669 行**）。这个函数从 669 行一直写到文件末尾（6163 行），体量近 5500 行——全项目最大、也最关键的单一函数。它的循环条件在**第 822 行**：

```python
while (api_call_count < agent.max_iterations
       and agent.iteration_budget.remaining > 0) or agent._budget_grace_call:
```

三个退出口一目了然：

1. `api_call_count < agent.max_iterations`——单轮 API 调用次数上限（默认 **90**，`run_agent.py:434`）；
2. `iteration_budget.remaining > 0`——跨子代理共享的**迭代预算**（见 6.5）；
3. `_budget_grace_call`——预算耗尽后给模型的"最后一次机会"（grace call，让它体面收尾）。

### 6.2 每轮迭代做什么

```mermaid
flowchart TD
    A["每轮开始"] --> B["排出用户中途转向消息<br/>_drain_pending_redirect (loop:823)"]
    B --> C{"用户按了中断?<br/>loop:837"}
    C -->|是| Z["退出: interrupted_by_user"]
    C -->|否| D["消耗 1 点迭代预算<br/>loop:853"]
    D --> E["组装 API 请求<br/>messages + 工具定义"]
    E --> F["调用模型 API<br/>chat.completions.create<br/>chat_completion_helpers.py:430/2683"]
    F --> G{"模型回复里有<br/>tool_calls?"}
    G -->|没有| H["final_response = 文本<br/>loop 结束"]
    G -->|有| I["messages.append(assistant_msg)<br/>loop:5337"]
    I --> J["先把工具调用块持久化到会话库<br/>loop:5366"]
    J --> K["_execute_tool_calls 执行全部工具<br/>loop:5387 → run_agent.py:6443"]
    K --> L["工具结果以 role=tool 消息<br/>追加回 messages"]
    L --> M{"护栏触发停机?<br/>loop:5389"}
    M -->|是| Y["受控停机 guardrail_halt"]
    M -->|否| A
    H --> N["返回最终回复"]
```

用大白话讲这个循环：**问模型 → 模型说要调工具 → 把"它要调工具"这条消息记进对话 → 执行工具 → 把工具结果也记进对话 → 带着完整对话再问模型 → 直到模型不再要工具、直接说话为止**。这就是所有 tool-calling agent 的共同心跳（ReAct 模式的工程化版本），Claude Code 的心跳也是同一个节奏，区别全在细节上。

为了让你"看见"这个循环，下面是一轮真实对话中 `messages` 数组的演化（每轮请求都把这个数组整体发给 API）：

```python
# 第 1 次请求时：
[{"role": "system", "content": "You are Hermes Agent…(三层提示词)"},
 {"role": "user",   "content": "帮我查一下明天北京天气"}]

# 模型第 1 次回复后追加（loop:5337 的 messages.append(assistant_msg)）：
 {"role": "assistant", "content": None,
  "tool_calls": [{"id": "call_1", "function":
                  {"name": "web_search", "arguments": '{"query":"北京 明天 天气"}'}}]}

# 工具执行完再追加（role=tool 的结果消息）：
 {"role": "tool", "tool_call_id": "call_1", "name": "web_search",
  "content": "{\"results\": [...]} "}

# 第 2 次请求时：以上 4 条全部发出。模型这次不再调工具，
# 直接回复文本 → final_response 落定，循环结束。
```

注意那个**严格交替**的不变量：user → assistant → tool → assistant……`AGENTS.md:90-91` 把"never two same-role messages in a row"（绝不出现两条同角色消息相邻）列为与缓存并列的铁律，因为多数 provider 的 API 会拒绝角色错乱的消息序列。循环里大量代码（如 `conversation_loop.py:1142` 附近合并相邻 user 消息、`:5346-5355` 给非法工具调用补错误结果保持配对）都是在小心翼翼地维护这个不变量。

### 6.3 一个容易忽略但救命的顺序：先持久化，再执行工具

注意上面流程图里第 J 步，`conversation_loop.py:5361-5366` 的注释：

```python
# Persist the assistant tool-call turn before any tool
# side effects run. If a destructive tool restarts or
# terminates Hermes mid-turn, resume logic still sees the
# exact tool-call block that already executed.
agent._flush_messages_to_session_db(messages, conversation_history)
```

意思是：**先把"模型决定调用工具"这件事写进数据库，再去执行工具**。为什么？因为工具可能是有副作用的——比如 `terminal` 工具执行了 `hermes restart`，进程当场死亡。如果没先持久化，重启后 agent 就不知道自己刚才干过什么，可能重复执行破坏性操作。这是用血泪换来的顺序。

### 6.4 流式输出

流式路径在 `agent/chat_completion_helpers.py:2683`（`stream = request_client.chat.completions.create(**stream_kwargs)`，其中 `stream_kwargs` 含 `stream=True`）。流式增量通过 `stream_delta_callback` 一路送到 CLI/TUI/网关的渲染层；TTS（语音合成）甚至能在文本还没生成完时就开始转语音（`run_conversation` 的 `stream_callback` 参数，`conversation_loop.py:688-690`）。

### 6.5 迭代预算：给"思考"装上油表

`agent/iteration_budget.py` 定义了一个线程安全的计数器 `IterationBudget`：

- 父 agent 预算 = `max_iterations`（默认 90）；
- 每个子代理预算独立（`delegation.max_iterations`，默认 50）；
- `consume()` 消耗、`refund()` 退还（比如 `execute_code` 的编程式工具调用不吃预算，`iteration_budget.py:25-27`）。

为什么要"预算"而不只是"次数上限"？因为一次真实任务里，有些 API 调用是"白送的"（比如重试、压缩后重发），不应该扣额度。预算机制允许**按语义扣油**而不是按次数扣油：可退还、可给最后一次机会（grace call）、耗尽时有明确提示（`conversation_loop.py:856` 打印 "⚠️ Iteration budget exhausted"）。

### 6.6 中断机制

中断是"协作式"的：循环每轮开头检查 `agent._interrupt_requested` 标志（`conversation_loop.py:837`），置位就跳出。CLI 的 Ctrl+C、网关的 `/stop`、新消息到达，最终都是置这个标志。协作式意味着正在执行的工具调用本身不会被强杀——安全，但也意味着一个卡死的 shell 命令要靠超时而非中断来终结。

---

## 第 7 章 工具系统与权限：74 件兵器、分组上锁、审批放行

### 7.1 工具是怎么注册的

所有内置工具集中在 `tools/` 目录，通过 `tools/registry.py` 的中央注册表声明自己。以文件工具为例（`tools/file_tools.py`，单行长调用）：

```python
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)
registry.register(name="write_file", toolset="file", schema=WRITE_FILE_SCHEMA,
                  handler=_handle_write_file, check_fn=_check_file_reqs, emoji="✍️", ...)
registry.register(name="patch",      toolset="file", schema=PATCH_SCHEMA, ...)
registry.register(name="search_files", toolset="file", schema=SEARCH_FILES_SCHEMA, ...)
```

每个工具注册五件事：**名字、JSON Schema（给模型看的说明书）、handler（真正干活的函数）、check_fn（可用性检查）、归属的工具集**。`registry.py` 还有一个巧妙设计：用 AST 静态扫描每个 `tools/*.py` 文件，只有真正在模块顶层调用了 `registry.register()` 的才会被导入（`registry.py:29-47`）——避免为了发现工具而把整个工具目录的副作用全跑一遍。

**Schema 就是给模型的"使用说明书"**，质量直接决定模型会不会用。看一个真实例子（`tools/file_tools.py:1947`）：

```python
READ_FILE_SCHEMA = {
    "name": "read_file",
    "description": "Read a text file with line numbers and pagination. "
                   "Use this instead of cat/head/tail in terminal. ... "
                   "Cannot read images — use vision_analyze for images.",
    "parameters": {
        "type": "object",
        "properties": {
            "path":   {"type": "string",  "description": "Path to the file ..."},
            "offset": {"type": "integer", "default": 1, "minimum": 1},
            "limit":  {"type": "integer", "default": 500, "maximum": 2000}
        },
        "required": ["path"]
    }
}
```

注意 description 里的话术完全是写给模型看的："用这个而不是 cat"、"不能读图片、图片请用 vision_analyze"——这是在用自然语言做路由。隔壁 `write_file` 的 schema（`file_tools.py:1961`）还藏了两个工程细节：写完 `.py`/`.json` 等文件**自动跑语法检查**、只报告"你这次新引入的错误"（旧错误过滤掉，防止模型去修不相干的代码）；以及 `cross_profile` 软护栏，默认阻止 agent 修改另一个 Hermes profile 的技能和记忆。这些"说明书里的心机"是 agent 项目最不起眼也最值钱的资产。

`check_fn` 是"服务门控"：比如桌面端专属的 `read_terminal`、`open_preview` 等工具只在检测到 GUI 环境（`HERMES_DESKTOP`）时才出现在工具列表里（`toolsets.py:36-39` 注释）。这呼应了 `AGENTS.md` 的"Footprint Ladder"（足迹阶梯）：新能力优先做成 CLI 命令 + 技能，其次是服务门控工具，再其次是插件，**最后才考虑加核心工具**——因为每个核心工具的 schema 都要在每次 API 调用时随请求发送，是持续付费的。

### 7.2 全部内置工具清单（按组分类）

下面这张表根据 `tools/*.py` 中全部 `registry.register(name="...")` 调用整理（共 **74 个**），并按 `toolsets.py:96` 的 `TOOLSETS` 分组：

| 分组 | 工具 | 功能 | 关键参数/例子 |
|---|---|---|---|
| **终端与进程** | `terminal` | 执行 shell 命令（六种后端，见 7.4） | `command`, `background=True` |
| | `process` | 管理后台进程（列出/查日志/杀） | `action="list"` |
| | `read_terminal` `close_terminal` `focus_pane` `open_preview` | 桌面端窗格操作（GUI 门控） | 读取内嵌终端内容 |
| **文件** | `read_file` | 读文件（带行号） | `path` |
| | `write_file` | 写/新建文件 | `path`, `content` |
| | `patch` | 改文件：`mode="replace"`（找串替换）或 `mode="patch"`（V4A 多文件 diff） | `path`, `mode` |
| | `search_files` | 内容搜索（ripgrep） | `pattern`, `path` |
| **Web** | `web_search` | 网页搜索（多后端：Firecrawl/Exa/Brave…） | `query` |
| | `web_extract` | 抓取并提取网页正文 | `url` |
| | `x_search` | X(Twitter) 搜索 | `query` |
| **浏览器** | `browser_navigate` `browser_snapshot` `browser_click` `browser_type` `browser_scroll` `browser_back` `browser_press` `browser_get_images` `browser_vision` `browser_console` `browser_cdp` `browser_dialog` | 完整浏览器自动化（快照=无障碍树，vision=截图看） | `browser_click(element="登录按钮")` |
| | `computer_use` | 桌面级键鼠控制 | 截屏+点击坐标 |
| **视觉与生成** | `vision_analyze` | 看图（多模态） | `image_path`, `prompt` |
| | `video_analyze` | 看视频 | `video_path` |
| | `image_generate` | 文生图（FAL 等后端） | `prompt` |
| | `video_generate` `xai_video_edit` `xai_video_extend` | 视频生成/编辑 | `prompt` |
| | `text_to_speech` | 文字转语音 | `text`, `voice` |
| **记忆与规划** | `memory` | 读写 MEMORY.md / USER.md（见第 8 章） | `action="add"`, `target="user"` |
| | `todo` | 任务清单管理 | `action="add"` |
| | `session_search` | FTS5 全文搜索历史会话 | `query` |
| **技能** | `skills_list` | 列出可用技能（只看元数据） | — |
| | `skill_view` | 读某个技能的完整 SKILL.md | `name` |
| | `skill_manage` | **agent 自己创建/修改/删除技能** | `action="create"` |
| **协作与委派** | `delegate_task` | 派生子代理（见第 9 章） | `goal`, `tasks=[...]` |
| | `clarify` | 主动向用户提问澄清 | `question` |
| | `execute_code` | 写 Python 脚本经 RPC 调工具，多步流水线折叠成一轮 | `code` |
| **定时与看板** | `cronjob` | 管理定时任务 | `action="create"`, `schedule` |
| | `kanban_show` `kanban_list` `kanban_create` `kanban_complete` `kanban_block` `kanban_unblock` `kanban_comment` `kanban_link` `kanban_heartbeat` `kanban_attach` `kanban_attach_url` `kanban_attachments` | 内置看板（多 agent 协作的任务板） | `kanban_create(title="修 bug")` |
| **项目与平台** | `project_list` `project_create` `project_switch` | 桌面端多项目切换 | — |
| | `discord` `discord_admin` | Discord 消息与管理 | — |
| | `send_message` | 跨平台发消息 | `platform`, `target` |
| **智能家居/办公** | `ha_list_entities` `ha_get_state` `ha_list_services` `ha_call_service` | Home Assistant 全家桶 | `ha_call_service(domain="light")` |
| | `feishu_doc_read` `feishu_drive_list_comments` `feishu_drive_list_comment_replies` `feishu_drive_reply_comment` `feishu_drive_add_comment` | 飞书文档/云盘评论 | — |
| | `yb_query_group_info` `yb_query_group_members` `yb_send_dm` `yb_search_sticker` `yb_send_sticker` | 元宝（腾讯）群聊 | — |

此外，启用"工具搜索"后还会出现 3 个桥接工具 `tool_search` / `tool_describe` / `tool_call`（见第 10 章），MCP 服务器的工具也会动态并入。

### 7.3 工具集（toolset）：按场景开关兵器架

`toolsets.py:96` 的 `TOOLSETS` 把工具分组成可开关的集合（web、file、terminal、browser、skills、memory、delegation、coding……），`hermes tools` 命令可交互开关。核心工具集 `_HERMES_CORE_TOOLS`（`toolsets.py:31`）是默认底座。工具集设计的动机和 slash 命令注册表一样：**会话启动时把 schema 冻结**，中途不换（换工具集会摧毁前缀缓存）。

### 7.4 权限与审批：危险命令要按"确认"

**危险命令检测**在 `tools/approval.py`：`DANGEROUS_PATTERNS`（`:606`）是一张模式表，`detect_dangerous_command()`（`:1988`）负责判定，`prompt_dangerous_approval()`（`:2273`）负责在 CLI 弹确认、在网关发异步审批消息。审批结果分级：单次允许 / 会话内允许 / 加入永久白名单（写进 `config.yaml`）。还有一个"智能审批"：用一个便宜的辅助模型给命令风险打分，低风险自动放行（`approval.py:1-11` 模块注释）。

最有安全感的一行代码是（`approval.py:35`）：

```python
_YOLO_MODE_FROZEN: bool = is_truthy_value(os.getenv("HERMES_YOLO_MODE", ""))
```

YOLO（You Only Live Once，免审批）模式在**模块导入时就被冻结**。注释解释了为什么：如果每次调用都读环境变量，那么任何在进程内运行的技能都可以通过改环境变量瞬间绕过所有审批——那是一条提示注入提权通道。**安全开关必须比可被注入的代码更早定型**，这个细节体现了威胁建模意识。

此外还有两道防线：

- **Tirith 预执行扫描**（`tools/tirith_security.py`）：调外部 tirith 二进制扫描命令里的内容级威胁（形似字符钓鱼 URL、pipe 到解释器、终端注入），退出码 0/1/2 = 放行/拦截/警告；二进制自动下载时强制 SHA-256 校验，有 cosign 时还验证供应链签名。
- **路径安全**（`tools/path_security.py`）与 URL 安全（`url_safety.py`）：限制文件工具的操作范围、拦截内网地址等。

```mermaid
flowchart LR
    M[模型请求 terminal 执行命令] --> D{"detect_dangerous_command<br/>approval.py:1988"}
    D -->|安全| R[直接执行]
    D -->|危险| T{"tirith 扫描<br/>tirith_security.py"}
    T -->|拦截| X[拒绝并告知模型]
    T -->|警告/放行| Y{"YOLO 已冻结开启?"}
    Y -->|是| R
    Y -->|否| P["prompt_dangerous_approval<br/>CLI 弹窗 / 网关异步消息"]
    P -->|允许| R
    P -->|拒绝| X
    P -->|永久允许| W[写入 config.yaml 白名单] --> R
```

### 7.5 Shell 执行的安全设计：六种"牢房"

`terminal` 工具的执行后端在 `tools/environments/`，共六种：`local`（裸跑在本机）、`docker`（容器隔离）、`ssh`（远程机）、`singularity`（HPC 容器）、`modal` 与 `daytona`（两个 serverless 云沙箱）。`terminal_tool.py:5-9` 的注释写明了取舍："local" 最快但不隔离；"docker" 隔离但要装 Docker；Modal/Daytona 提供**休眠唤醒的持久沙箱**——空闲时环境冬眠、几乎不花钱，唤醒时状态还在。

也就是说，Hermes 的安全模型不是"默认沙箱一切"，而是**让用户按任务选隔离级别**：回个邮件用 local，跑不可信代码丢进 Docker/Modal。配合上面的审批流，形成"检测 → 审批 → 隔离"三层。

---

## 第 8 章 上下文压缩与记忆：短记忆靠压缩，长记忆靠文件

### 8.1 为什么需要压缩

模型上下文窗口有限（比如 200K token），而 agent 会话里工具结果（尤其网页、日志）极其占地方。聊到天荒地老时总得扔掉点什么——Hermes 的做法是**让便宜模型写"会议纪要"**。

### 8.2 压缩器：掐头去尾留中间

`agent/context_compressor.py` 的模块注释（`:1-18`）把策略写得明明白白：

- 用**辅助模型**（便宜/快的那个）总结**中段**对话；
- **保护头部**（系统提示、最初指令）和**尾部**（最近的工作现场）；
- 结构化摘要模板：追踪"已解决/待办"问题；
- 历史段落标题代替"下一步"字样（防止旧摘要被误读成新指令）；
- **迭代式摘要更新**：多次压缩时保留之前摘要的信息；
- 尾部按 token 预算保护（而不是固定消息数）；
- 摘要前先做一次"工具输出剪枝"的便宜预扫。

触发阈值默认是上下文窗口的 **50%**（`agent/agent_init.py:1765`：`compression_threshold = float(_compression_cfg.get("threshold", 0.50))`），可按模型自动上调；单轮最多尝试 3 次压缩（`conversation_loop.py:781` 的 `max_compression_attempts` 默认值）。压缩也是**唯一被允许重建系统提示词的场景**——`AGENTS.md:19-23` 明说前缀缓存神圣，"the one exception is context compression"。

**举个具体例子**：你让 agent"调研 20 篇关于 Rust 异步运行时的文章并写综述"。第 12 篇读完时上下文到了窗口的 50%，压缩启动——前 12 篇的几十条 `web_extract` 结果（可能几十万字符）被送给一个便宜模型，压缩成一条几千字的结构化纪要："已读 12 篇，核心论点分别是……；待办：还有 8 篇未读，清单是……"；然后 agent 带着这条纪要继续读第 13 篇。头部（你的原始指令）和尾部（最近的工作现场）原封不动。对模型来说，"记忆"被无缝替换成了"笔记"，工作不中断。

另一个用途完全不同的压缩器是根目录的 `trajectory_compressor.py`：它**不在运行时使用**，而是离线处理已完成的 agent 轨迹（JSONL），同样"保头保尾压中段"，把轨迹压进目标 token 预算——用途是生产训练数据（`trajectory_compressor.py:5-16` 的策略列表与在线压缩器几乎同构）。一个项目里同时养着"在线省钱的压缩"和"离线造数据的压缩"，这就是研究公司的项目特有的双生子设计。

```mermaid
flowchart LR
    subgraph BEFORE["压缩前（接近窗口上限）"]
        H1[系统提示+早期指令] --> M1[几十轮工具调用与结果<br/>网页/日志/代码…]
        M1 --> T1[最近几轮工作现场]
    end
    subgraph AFTER["压缩后"]
        H2[系统提示+早期指令<br/>原样保留] --> S["一条结构化摘要<br/>（辅助模型撰写：<br/>已解决/待办/关键结论）"]
        S --> T2[最近几轮工作现场<br/>原样保留]
    end
    BEFORE -->|context_compressor.py| AFTER
```

### 8.3 长期记忆：两个 markdown 文件

跨会话记忆由 `tools/memory_tool.py` 提供，设计极为朴素——**就是两个文件**（`memory_tool.py:4-7`）：

- `MEMORY.md`：agent 自己的笔记（环境事实、项目惯例、工具怪癖）；
- `USER.md`：它了解的"你"（偏好、沟通风格、工作习惯）。

条目以 `§` 分节符分隔，限制按字符数算（因为字符数与模型无关）。记忆工具只有三个动作：`add`、`replace`、`remove`，后两者用"短的唯一子串"定位条目（`memory_tool.py:17-19`）。

最关键的设计是**冻结快照**（`memory_tool.py:9-12`）：两个文件在会话开始时作为快照注入系统提示词；会话中途写入立即落盘，但**不更新系统提示词**——保住前缀缓存，下个会话才生效。宁可让 agent"暂时不知道自己刚记住的东西"，也不动缓存，这是把第 5 章那条铁律贯彻到底。

### 8.4 记忆体系全景：远不止两个文件

```mermaid
flowchart TB
    subgraph MEM["Hermes 的记忆与学习体系"]
        A["MEMORY.md / USER.md<br/>声明性记忆（事实与偏好）"]
        B["skills/<br/>程序性记忆（怎么做）<br/>skill_manage 工具自动沉淀"]
        C["会话库 SQLite + FTS5<br/>session_search 跨会话全文搜索"]
        D["curator.py 后台策展<br/>空闲时评审/归档/合并旧技能"]
        E["Honcho 外部记忆<br/>辩证式用户建模（可选）"]
        F["记忆提醒 nudge<br/>定期提示自己'该存知识了'"]
    end
    A --> G[系统提示词易变层]
    B --> G
    E --> G
    C -.按需工具调用.-> G
```

- **Curator（策展人）**：`agent/curator.py` 在 agent **空闲**时启动一个后台评审 agent，只碰"agent 自己创建的技能"，可以置顶/归档/合并/打补丁，但**永远不自动删除**（归档可恢复）；它走辅助模型，绝不动主会话的提示词缓存。
- **session_search**：SQLite FTS5 全文索引所有历史会话，`session_search_tool.py:8` 起描述了发现（带查询）→ 展开（±窗口上下文）两种模式，还能用 LLM 总结旧会话。
- **Honcho**：可选接入的第三方"辩证用户建模"服务（`hermes honcho` 一族命令，`main.py:21-36`），让 agent 对"你是谁"建立跨会话不断加深的模型。

这套"声明性记忆 + 程序性技能 + 可搜索历史 + 后台策展 + 用户建模"的组合，就是 README 里"closed learning loop"（闭环学习）的实体。对比 Claude Code 的 auto-memory（也是 markdown 文件 + 加载进上下文），Hermes 多出了**技能自创建、后台策展和会话搜索**三样东西。

---

## 第 9 章 子 agent 与多 agent：派分身去干活

### 9.1 delegate_task：生成分身

子代理由 `delegate_task` 工具实现（`tools/delegate_tool.py`）。模块注释（`delegate_tool.py:1-19`）描述了每个"孩子"得到什么：

- 全新对话（**不带父代历史**）；
- 自己的 task_id（自己的终端会话、文件缓存）；
- 继承父代工具集，但**剥掉子代理禁用的工具**；
- 一个由"目标 + 上下文"构成的专用系统提示。

父代上下文里只看到"委派调用"和"摘要结果"——孩子的中间推理和工具调用永远不污染父代上下文。这正是 Anthropic 多 agent 研究里著名的"上下文隔离"模式。

禁用清单（`delegate_tool.py:46-55`）：

```python
DELEGATE_BLOCKED_TOOLS = frozenset([
    "delegate_task",  # 不许递归委派
    "clarify",        # 不许找用户聊天
    "memory",         # 不许写共享记忆
    "send_message",   # 不许跨平台发消息
    "cronjob",        # 不许以父代名义排定时任务
])
```

每一行都是一道权限边界：孩子是个"干活的"，不是"另一个你"。

### 9.2 前台还是后台？看谁派的

`run_agent.py:6497-6507` 的注释讲清了前后台语义，设计相当果断：

> Delegations from the top-level MODEL always run in the background — the model does not get to choose.

**顶层模型发起的委派一律后台执行**：`delegate_task` 立即返回一个句柄，子代理完成后，其结果作为**一条新消息**回到父会话。唯一的例外是"编排者子代理"（orchestrator，深度 > 0）发起的委派保持同步——因为编排者需要在自己的这一轮里等到工人的结果来写总结，而且子代理并不拥有网关会话，异步结果没地方回。

**两种典型用法**：

- **扇出（fan-out）**：一次 `delegate_task` 传 `tasks=[调研A, 调研B, 调研C]`，三个子代理并行开工，父代理该干嘛干嘛，三份摘要陆续"回帖"；
- **编排者模式**：派一个 orchestrator 子代理，它自己再同步派几个工人，收齐结果写成一份综合报告再交给父代——父代上下文自始至终只看到一份报告，连中间有三个工人存在过都不知道。

```mermaid
sequenceDiagram
    participant P as 父 agent（主会话）
    participant D as delegate_task
    participant S1 as 子代理 A
    participant S2 as 子代理 B
    P->>D: tasks=[调研X, 调研Y]
    D->>S1: 后台启动（独立上下文/预算50）
    D->>S2: 后台启动（独立上下文/预算50）
    D-->>P: 立即返回句柄，父继续干活
    S1-->>P: 完成 → 结果作为新消息注入
    S2-->>P: 完成 → 结果作为新消息注入
    P->>P: 汇总两个结果，回复用户
```

安全细节：子代理跑在线程池里，默认**自动拒绝**一切危险命令审批（`_subagent_auto_deny`，`delegate_tool.py:74-86`）——因为后台线程拿不到 CLI 的交互式审批回调，调 `input()` 会和父代的 TUI 死锁。注释（`delegate_tool.py:65-73`）把这个死锁场景写得非常具体，显然是真踩过坑。只有显式配置 `delegation.subagent_auto_approve: true` 才会放行（给 cron/批处理场景的"opt-in YOLO"）。

### 9.3 预算与深度控制

- 每个子代理有独立 `IterationBudget`（默认 50，第 6.5 节），父代 90 + 各子代 50，总量可以超过父代上限；
- 最大并发子代数、最大异步子代数、最大派生深度（`_get_max_spawn_depth`）都有限制（`delegate_tool.py:354-467`），防止 agent 失控地"生孙子"。

### 9.4 execute_code：把多步流水线折叠成一轮

另一个多 agent 相关的能力是 `execute_code` 工具：agent 写一段 Python 脚本，脚本里通过 RPC 调用其它工具——比如"搜 10 个网页、逐个提取、拼成表格"原本要 11 轮请求，现在一个脚本搞定，**中间结果根本不进模型上下文**（README:28 所谓 "zero-context-cost turns"）。这是对"工具结果吃爆上下文"问题的釜底抽薪式解法。

---

## 第 10 章 生态：命令、技能、MCP、插件与 ACP

```mermaid
flowchart TB
    H["Hermes 核心（窄腰）"] --- A["83 个 slash 命令<br/>COMMAND_REGISTRY"]
    H --- B["技能系统<br/>agentskills.io 标准 + 自建技能"]
    H --- C["MCP 客户端 + mcp_serve.py<br/>（既是 MCP 食客也是 MCP 餐馆）"]
    H --- D["插件系统 plugins/<br/>平台/provider/看板/记忆…"]
    H --- E["acp_adapter<br/>ACP 服务器，接入 Zed 等编辑器"]
    H --- F["tool_search<br/>渐进式工具披露"]
```

### 10.1 命令清单

83 个 slash 命令已在第 4 章介绍（注册表在 `hermes_cli/commands.py:64`）。CLI 顶层还有几十个 `hermes xxx` 子命令（`main.py:6-43` 的 docstring 就是一份目录）：`gateway`、`setup`、`cron`、`doctor`、`honcho`、`claw migrate`、`sessions browse`……

### 10.2 技能系统：兼容 agentskills.io 的"程序性记忆"

技能 = 一个含 `SKILL.md` 的目录（YAML frontmatter 写名字和描述，正文写操作步骤），外加可选的 `references/`、`templates/`、`scripts/`、`assets/`（`tools/skills_tool.py:7-29`）。采用**渐进式披露**：`skills_list` 只给模型看元数据（名字 ≤64 字符、描述 ≤1024 字符），感兴趣才用 `skill_view` 读全文，支持文件按需再读——这样几百个技能也不会撑爆上下文。

最独特的是 `skill_manage` 工具（`tools/skill_manager_tool.py`）：**agent 可以在完成复杂任务后自己创建技能**，把"刚验证有效的方法"写成可复用的 SKILL.md 存进 `~/.hermes/skills/`，以后遇到同类任务直接调用。再加上第 8 章的 curator 后台评审，"越用越聪明"就不是宣传语而是代码路径了。技能格式兼容 [agentskills.io](https://agentskills.io) 开放标准，还有 Skills Hub 可以安装社区技能。

### 10.3 MCP 支持：既是客户端也是服务器

- **客户端**：`tools/mcp_tool.py` 支持 stdio、HTTP/StreamableHTTP、SSE 三种传输，配置写在 `~/.hermes/config.yaml` 的 `mcp_servers` 下（模块注释 `:12-39` 给了完整示例，包括超时、保活、空闲回收等参数）。发现的 MCP 工具直接注册进工具注册表，和内置工具一视同仁。还支持 MCP OAuth（`mcp_oauth_manager.py`）。
- **服务器**：`mcp_serve.py` 反向把 Hermes 自己的能力以 MCP server 形式暴露给其它 MCP 宿主。双向通吃。

### 10.4 插件系统

`plugins/` 目录按类别组织：`platforms/`（约 20 个消息平台适配器）、`model-providers/`（33 个模型供应商）、`kanban`、`memory`、`web`、`security-guidance`、`spotify`……平台插件通过 `gateway/platform_registry.py` 的 `PluginContext.register_platform()` 自注册（`platform_registry.py:8`），而且是**延迟加载**（`:175-198`）：不为 telegram 用户白白导入 discord.py。

### 10.5 ACP：接入编辑器生态

如第 2 章所述，`acp_adapter/` 把 Hermes 包装成 ACP（Agent Client Protocol）服务器，编辑器等 ACP 客户端经 stdio JSON-RPC 驱动它：会话管理（`session.py`）、权限审批转发（`permissions.py`、`edit_approval.py`）、事件流（`events.py`）。`hermes acp` 或独立的 `hermes-acp` 命令启动（`acp_adapter/entry.py:9-14`）。

### 10.6 tool_search：工具太多时的"按需取货架"

当 MCP + 插件工具很多时，全量 schema 每次请求都发送太浪费。`tools/tool_search.py` 的解法（模块注释 `:1-26`）：把可延迟的工具从模型可见列表里撤下，只留 3 个桥接工具——`tool_search`（搜工具）、`tool_describe`（看说明书）、`tool_call`（真正调用）。阈值默认 10%：可延迟工具的 schema 占上下文窗口不到 10% 时原样放行。核心工具（`_HERMES_CORE_TOOLS`）**永远不延迟**。注释里还专门提到吸取了 OpenClaw 一次 cron 回归事故的教训（目录必须每次重建、不能按会话缓存），可见这个项目的文档文化：每个设计决定都写明"为什么"和"踩过什么坑"。

---

## 第 11 章 功能特性：模型自由、认证自由

### 11.1 认证与 provider

两个世界都支持（`hermes_cli/auth.py:1-16`）：

- **OAuth device code**：Nous Portal（`hermes setup --portal`，一次登录打通 300+ 模型和工具网关）；
- **API key**：OpenRouter、OpenAI、自定义 OpenAI 兼容端点等，存 `~/.hermes/.env` 或外部密钥管理器（Bitwarden、1Password，`hermes secrets` 命令）。

`plugins/model-providers/` 下列出 **33 个 provider 插件**，包括：nous、openrouter、anthropic、openai-codex、gemini、bedrock、vertex、azure-foundry、deepseek、xai、groq 系的 fireworks/deepinfra/novita、alibaba（含 coding-plan）、kimi-coding、minimax、zai、qwen-oauth、ollama-cloud、huggingface、copilot、arcee、stepfun、upstage、xiaomi、custom……还有 `agent/` 里的 anthropic/gemini/bedrock/vertex/codex 原生适配器（处理各家的非 OpenAI 兼容协议）。

```mermaid
flowchart LR
    U[用户] -->|hermes setup --portal| P["Nous Portal<br/>OAuth 一键登录"]
    U -->|hermes model| K["API key 提供商<br/>33 个插件"]
    P --> M["300+ 模型<br/>/model 随时换"]
    K --> M
    M --> F["fallback 链<br/>主模型挂了自动换备胎"]
    M --> MO["MoA 混部<br/>/moa 多问几个模型再综合"]
```

### 11.2 模型选择与切换

`hermes model` 交互式选择 + `/model` 会话内切换 + `--global` 持久化（`commands.py:134`）。**fallback 链**（`hermes fallback`）：主模型限流/过载/断连时按序尝试备胎。还有 **MoA（Mixture of Agents）**：`/moa <问题>` 把同一个问题发给多个模型再综合答案（`commands.py:116`）。

### 11.3 本地模型与自托管（NousResearch 的家底）

作为开放权重模型的大本营，Nous 自然把自托管当一等公民：

- `custom` provider + 任意 OpenAI 兼容端点（vLLM、SGLang、llama.cpp 都行）；
- `ollama-cloud` 插件；provider 目录注释里还提到 lmstudio、openai-api 等"无档案的规范 provider"（`hermes_cli/provider_catalog.py:22`）；
- Nous 自家的 **Hermes 系列开放权重模型**（如 Hermes 4）就是为这种工具调用循环训练的，和本项目是"模型 + harness"的配套关系。

### 11.4 thinking / effort

`/reasoning` 命令提供八档推理强度（none/minimal/low/medium/high/xhigh/max/ultra，`commands.py:158-160`），还能控制推理内容的显示（show/hide/full）。内部有 `agent/think_scrubber.py`（清洗思考标签）和 `agent/reasoning_timeouts.py`（推理超时给模型"减压"指导，`thinking_timeout_guidance.py`）。

### 11.5 特色功能速览

| 特性 | 位置 | 说明 |
|---|---|---|
| 持续目标 `/goal` | `hermes_cli/goals.py` | 每轮结束后让辅助模型当裁判："目标达成了吗？"没有就自动续一轮——注释自称 "the Ralph loop"，裁判故障时 fail-open 继续 |
| 定时任务 | `cron/scheduler.py` | 内置调度器（60 秒 tick，文件锁防重入），自然语言描述任务，结果可投递到任意消息平台 |
| 语音模式 | `tools/tts_tool.py`、`voice_mode.py` | 流式 TTS，文本没生成完就开始朗读；语音消息转文字 |
| 看板 | `tools/kanban_tools.py` | 多 agent 共享的任务看板，12 个 kanban_* 工具 |
| 轨迹生产 | `batch_runner.py`、`trajectory_compressor.py` | 批量跑任务生成训练轨迹，再按 token 预算压缩中段——给下一代模型造数据 |
| 桌面端 | `apps/desktop/` | Electron 应用（Hermes Desktop），带项目切换、预览窗格 |
| Web 仪表盘 | `web/` | Vite + React 的浏览器控制台 |

---

## 第 12 章 总结：设计哲学与取舍

### 12.1 三条贯穿始终的信念

读完代码，会发现几乎每个角落都在重复 `AGENTS.md:16-27` 立下的两条半原则：

**① 前缀缓存神圣。** 系统提示词逐字节稳定、工具集会话级冻结、记忆用冻结快照、技能用渐进式披露、压缩是唯一例外。一切为了"长会话每轮都命中缓存前缀"，把用户的 API 账单压到最低。这是把**经济性**上升为**架构约束**。

**② 核心窄腰，能力长在边缘。** "Every model tool we add is sent on every API call, so the bar for a new *core* tool is high."（`AGENTS.md:24-27`）新能力的优先级阶梯：扩展现有代码 → CLI 命令 + 技能 → 服务门控工具（check_fn）→ 插件 → MCP → 万不得已才加核心工具。于是我们看到：平台是插件、provider 是插件、连智能家居和飞书都是门控工具。核心只做一件事——把循环跑稳。

**③ 供应链偏执与自愈体质。** 所有依赖精确版本锁定（`pyproject.toml:17-30` 的注释记录了 2026-05-12 因 PyPI 投毒事件收紧政策的全过程）；启动时连环自愈 venv；tirith 下载强制校验签名；YOLO 标志导入即冻结。这是一个"要在陌生人的电脑上无人值守跑几个月"的软件对自己的基本要求。

```mermaid
flowchart TB
    P1["① 前缀缓存神圣<br/>省钱：提示词 byte-stable"] --> C["Hermes 设计内核"]
    P2["② 核心窄腰<br/>省心：能力全走插件/技能/MCP"] --> C
    P3["③ 自愈与供应链偏执<br/>省事：无人值守也要活着"] --> C
    C --> R["结果：一个能在 5 美元 VPS 上<br/>长期自主运行、越用越懂你的 agent"]
```

### 12.2 真正的亮点

1. **闭环学习不是口号而是代码路径**：`skill_manage`（用经验造技能）→ `curator.py`（空闲时评审技能）→ `memory`（写事实）→ `session_search`（搜历史）→ Honcho（用户建模），五件套环环相扣。
2. **模型无关做到极致**：33 个 provider 插件 + 按模型家族定制提示（第 5.4 节的编辑格式引导）+ fallback 链 + MoA。
3. **网关优先的"agent 即联系人"**：约 20 个平台适配器、异步审批、语音消息、cron 投递——它不是你打开的工具，而是你通讯录里的一员。

### 12.3 诚实的取舍清单

| 取舍 | 代价 | 换来了什么 |
|---|---|---|
| Python 单体内核 + 巨型文件（`cli.py` 16590 行、`run_agent.py` 6907 行、`conversation_loop.py` 6163 行） | 新手阅读劝退；合并冲突高发（`AGENTS.md` 自己也在呼吁把 god-file 拆成 mixin） | 迭代速度、 Monkey-patch 友好的测试生态、单进程部署简单 |
| 协作式中断 | 卡死的工具不能强杀，只能靠超时 | 不会杀出半个写坏的文件 |
| 冻结快照式记忆 | 会话内"记了就忘"（下轮才生效） | 前缀缓存 100% 稳定 |
| 安全靠"审批+可选隔离"而非默认沙箱 | local 后端下误批准仍有风险 | 个人助理场景的低摩擦 |
| 功能面极宽（看板、语音、视频生成、元宝、飞书……） | 维护面巨大，边缘功能质量参差 | "一个 agent 包办数字生活"的野心 |

### 12.4 给初学者的阅读路线

如果想亲自读这份源码，推荐这条由浅入深的路径：

1. `README.md` + `AGENTS.md`（先理解设计原则，后看代码会事半功倍）；
2. `pyproject.toml:309` → `hermes_cli/main.py:14339` 的 `main()`（入口）；
3. `agent/conversation_loop.py:669` 的 `run_conversation()`，配合第 6 章的流程图读 while 循环；
4. `tools/file_tools.py`（最简单的工具长什么样）→ `tools/terminal_tool.py`（最复杂的工具长什么样）；
5. `tools/delegate_tool.py`（子代理）→ `agent/context_compressor.py`（压缩）→ `tools/memory_tool.py`（记忆）。

---

> **诚实声明**：本文所有 `文件:行号` 引用均来自对上述 commit 的实际阅读；少数未深入验证的功能（如 Honcho 的具体协议细节、部分平台适配器的实现质量）仅按官方文档与模块注释转述。项目迭代极快（本 commit 距今一天），行号在新版本中会漂移，但架构与原则稳定。
