English lecture · source-traced

Hermes Agent source analysis (for a CS beginner)

**Subject**: [NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent) v0.19.0 **Based on commit**: `d9165d7a678d4105f42921a7fc1886df3804531b` (2026-07-23, `fix: resolve current entry unlocked in try_refresh_matching no-hint branch`) **Analysis date**: 2026-07-24 **Size**: about 3300 Python files (backend core) + about 1600 TypeScript/TSX files (TUI, desktop, Web dashboard) **How to read**: this lectu

EN
lecture
source
file:line
no
embellishment
中文
sibling

Subject: NousResearch/hermes-agent v0.19.0 Based on commit: d9165d7a678d4105f42921a7fc1886df3804531b (2026-07-23, fix: resolve current entry unlocked in try_refresh_matching no-hint branch) Analysis date: 2026-07-24 Size: about 3300 Python files (backend core) + about 1600 TypeScript/TSX files (TUI, desktop, Web dashboard) How to read: this lecture is aligned with a “Claude Code source analysis” series, 12 chapters. Each chapter tries: analogy → real source (file:line) → line-by-line → design trade-offs. Every “not found / not supported” claim was checked against the tree; nothing is invented.


🗺️ Companion architecture diagram: this project has a dedicated figure in the seven-agent architecture gallery—control/data plane + dual-timeline state ownership (memory writes to disk immediately, but only the next session enters the prompt). Every gallery figure first states “what question it answers” and “the load-bearing claim,” then goes through three review and render-acceptance rounds.

Part 1

Chapter 1 · Project overview: a “self-evolving personal agent”

1.1 One sentence

Hermes Agent is Nous Research’s open-source personal AI agent whose biggest selling point is a self-improving learning loop: it does not only answer questions; it also turns successful experience into “skills,” writes facts into “memory,” can search its own past conversations, and does not live only in the terminal — it can hang on Telegram, Discord, Slack, WhatsApp and about 20 messaging platforms at once, on a $5 VPS or a GPU cluster.

The README’s first sentence states the positioning (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 Who builds it

Nous Research — an AI research company known for open-weight models (they train the Hermes fine-tune series). That explains two “research flavors”:

  • built-in trajectory batch generation and compression tools (batch_runner.py, trajectory_compressor.py) to produce training data for the next generation of tool-calling models;
  • the model layer is fully open: default attach is their own Nous Portal (300+ models), but also OpenRouter, OpenAI, self-hosted endpoints, and 30+ other providers.

1.3 Stack

Layer Tech Location
Backend core (agent loop, tools, gateway) Python 3.11–3.13 (pyproject.toml:13 requires >=3.11,<3.14) repo root plus agent/, tools/, gateway/, etc.
Terminal UI (TUI) TypeScript + React + Ink ui-tui/
Desktop app Electron + React (TS) apps/desktop/
Web dashboard Vite + React (TS) web/
Package manager uv (all deps exact-version locked, no ranges) pyproject.toml, uv.lock

One counter-intuitive point: Python is the brain; TypeScript is only the face. The TUI README is blunt (ui-tui/README.md): "TypeScript owns the screen. Python owns sessions, tools, model calls, and most command logic." — the TS side talks to the Python process over newline-delimited JSON-RPC on stdio (tui_gateway/).

1.4 Vs Claude Code (one-sentence level)

  • Same: both are “model + tool-calling loop” agents; both have slash commands, AGENTS.md/CLAUDE.md context files, sub-agents, MCP, a skill system.
  • Different (one sentence): Claude Code is a programming assistant that lives in the terminal (closed-source, bound to Anthropic models); Hermes Agent is a cross-session self-learning, model-agnostic, all-platform personal agent that lives in messaging apps (MIT open source, model-agnostic).
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

Part 2

Chapter 2 · Panorama: one Python brain, many faces

2.1 Layered overview

Hermes architecture in one sentence: one shared Python agent core, wrapped in N UI shells. AGENTS.md:9-14 (design guide for developers) verbatim:

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.

flowchart TB
    subgraph UI["界面层(多张脸)"]
        CLI["cli.py
prompt_toolkit REPL"] TUI["ui-tui/ (TS: React+Ink)
──JSON-RPC/stdio──▶ tui_gateway/ (Python)"] DESK["apps/desktop/
Electron"] WEB["web/
Vite 仪表盘"] ACP["acp_adapter/
ACP 服务器(给 Zed 等编辑器)"] GW["gateway/
消息网关"] end subgraph INPUT["输入与分流"] SLASH["slash 命令注册表
hermes_cli/commands.py"] PLAT["plugins/platforms/
telegram/discord/slack/whatsapp/signal/email…约20个"] end subgraph CORE["核心(Python 大脑)"] AGENT["run_agent.py : AIAgent 类"] LOOP["agent/conversation_loop.py
run_conversation() 主循环"] CTX["agent/system_prompt.py
三层系统提示词"] MODEL["model_tools.py
工具分发 handle_function_call()"] end subgraph TOOLS["工具与能力层"] REG["tools/registry.py 注册表
74 个内置工具"] ENV["tools/environments/
local/docker/ssh/singularity/modal/daytona"] MCP["tools/mcp_tool.py
MCP 客户端"] PLUGIN["plugins/
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 Key directories, quick lookup

Directory/file Role
run_agent.py (6907 lines) the AIAgent class itself (run_agent.py:400), the agent’s “ID card + toolbox”
agent/conversation_loop.py (6163 lines) main loop run_conversation() (line 669), the heart of the whole project
agent/ (~160 .py files, including __init__.py) system prompt, compression, budget, adapters (anthropic/gemini/bedrock/vertex…)
tools/ (102 files) implementations of 74 built-in tools
toolsets.py toolset definitions: group tools, switch by platform/scene
hermes_cli/ (160+ files) the hermes CLI itself (setup, auth, model, doctor…)
cli.py (16590 lines) interactive terminal REPL (the largest single file)
gateway/ + plugins/platforms/ messaging gateway and ~20 platform adapters
acp_adapter/ ACP protocol server
plugins/model-providers/ 33 model-provider plugins
cron/ built-in scheduled-job scheduler
skills/ built-in skill library (software engineering, data science, smart home……)
ui-tui/, apps/desktop/, web/ three TS UIs

2.3 What is acp_adapter?

ACP = Agent Client Protocol (a Zed-led standard protocol between editors ↔ AI agents, analogous to LSP for language servers). The comment in acp_adapter/server.py:1 is one line: "ACP agent server — exposes Hermes Agent via the Agent Client Protocol." Entry is acp_adapter/entry.py: after start, stdout is reserved for ACP JSON-RPC transport; logs all go to stderr. So editors that support ACP, such as Zed, can drive Hermes as an embedded agent backend — Claude Code has this capability too (Claude Code also supports ACP); the two projects stand on the same ecosystem line for this protocol.


Part 3

Chapter 3 · Startup: from the hermes command to the chat UI

3.1 Where is the entry

The Python package CLI entry is defined in pyproject.toml:308-311:

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

So when you type hermes in the terminal, you actually run main() in hermes_cli/main.py (hermes_cli/main.py:14339).

3.2 The start of main() does a pile of “self-rescue”

The beginning of this main() is interesting — it does not parse arguments first; it chains self-repair (hermes_cli/main.py:14339-14384):

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: 默认子命令 = 聊天

Line by line:

  1. Rename the process: purely UX, so you can recognize it in htop;
  2. UTF-8 fix: Windows consoles default to not UTF-8; printing Chinese/emoji would UnicodeEncodeError crash, so it must be fixed before any print;
  3. venv self-heal: if the last hermes update crashed between git reset --hard and uv pip install, the environment is “new code citing old packages.” main() uses the pure-stdlib module _early_recovery to repair before importing any third-party library (main.py:67-84 comments are candid: without this, the user cannot even run hermes update, and can never self-heal).

This is defensive design unique to software that “runs long-term on a personal computer and self-updates” — a Web service that dies has ops; a personal CLI that dies can only crawl back itself.

3.3 Argument parse and the chat entry

main() registers dozens of subcommands with argparse (model, gateway, setup, cron, doctor, claw migrate……), then defaults into cmd_chat (main.py:2435).

Before chat, the CLI still has a long prep: read ~/.hermes/config.yaml and .env, resolve the current provider’s runtime credentials (refresh OAuth tokens if needed), enable toolsets from config, create an AIAgent instance, restore the last session (/resume), run startup security checks (security_audit_startup.py), and only then start the prompt_toolkit input UI. Chat flow roughly:

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 Auth

Auth config lives in two places (hermes_cli/auth.py:1-16 module comment):

  • ~/.hermes/.env: traditional API keys (OpenRouter, OpenAI, custom endpoints……);
  • ~/.hermes/auth.json: OAuth credential state (Nous Portal uses OAuth device code, hermes setup --portal one-command login), with a cross-process file lock.

First-run hermes setup wizard walks you through provider, key, default model; with no auth, cmd_chat takes you into setup first. If it detects ~/.openclaw (config dir of the similar project OpenClaw), it will actively offer a one-click migrate of persona, memory, skills, and API keys (README.md:187-213) — even user-grabbing is in the source. Very geek.


Part 4

Chapter 4 · Input capture and routing: where every character you type goes

4.1 CLI side: prompt_toolkit harvests input

Interactive terminal input is prompt_toolkit (cli.py:60-75 imports a dozen components at once: Application, Layout, KeyBindings, TextArea, …), providing multiline edit, slash autocomplete, history (FileHistory), paste detection — a full terminal experience.

Once input is in hand, routing is roughly this decision chain:

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

4.2 Slash commands: one central registry

All slash commands are defined in one registry COMMAND_REGISTRY (hermes_cli/commands.py:64), 83 CommandDefs in the file. Each definition looks like (commands.py:46-61):

@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   # 只在消息网关可用

The clever bit is the comment (commands.py:3-5): "Every consumer — CLI help, gateway dispatch, Telegram BotCommands, Slack subcommand mapping, autocomplete — derives its data from COMMAND_REGISTRY." The registry is the single source of truth: Telegram bot command menus, Slack subcommands, CLI Tab complete, all generated from this table. Add one command, five UIs light up.

A few representative commands (all from commands.py):

Command Role Routes to
/new /reset open a new session rebuild session locally, do not send to the model
/model [name] switch model (session-level, --global persists) change config locally
/compress manual context compact call the compressor (Chapter 8)
/background <prompt> (aliases /bg, /btw) throw a task to the background asynchronously spawn an agent task
/queue <prompt> do not interrupt current work; queue for next round local queue
/steer <prompt> quietly insert an instruction after the next tool call steering channel
/yolo toggle “skip-approval mode” local switch
/reasoning [level] adjust thinking strength (none/minimal/low/medium/high/xhigh/max/ultra, eight gears) change reasoning config

4.3 Busy-time input: three philosophies — /queue, /steer, /interrupt

When you press Enter while the agent is working, CLI behavior is configurable (/busy command, options queue|steer|interrupt, commands.py:171):

  • queue: you keep talking; it does not interrupt; it looks next round;
  • steer: your words are injected “after the next tool call”; the model changes direction mid-flight;
  • interrupt: stop immediately and listen.

That is one more gear than Claude Code’s Esc-interrupt + queue dual mode: “mid-task steer,” answering the real pain of “long task half-done, want to add a requirement.”

4.4 Gateway side: messaging-platform routing

After the messaging gateway (gateway/) receives a Telegram (etc.) message, routing is a set of _handle_xxx_command async methods in gateway/slash_commands.py (e.g. _handle_model_command at :1435, _handle_stop_command at :1070). The comment at hermes_cli/commands.py:396-404 explains the gateway Level-2 handler: when a running task receives /model, /compress mid-flight, you cannot brutally interrupt; they enter a pending queue until the current step ends.

The gateway registry also has an engineering-detail: platforms have hard caps on bot command count, written as constants — Telegram menus at most 60 (Bot API hard cap 100, commands.py:554-555), Slack at most 50 (:1133). So the registry must pick a “most worth exposing” subset per platform, not blindly sync everything.

File attachments have dedicated handling: a path typed in the CLI (cli.py:3109-3127 recognizes /, ~, ./, C:\, even quoted paths) is parsed as an attachment; images go the vision path; multi-image has compact badges for narrow terminals (cli.py:3158-3176, even Termux small screens). On the gateway, voice messages go to text (agent/transcription_provider.py family).

Three routing examples, to pin the flowchart to reality:

  1. Input /model openrouter:anthropic/claude-sonnet-4 → hits the registry → local model switch, no API request at all;
  2. Input ./report.pdf 帮我总结 → path detection hits → PDF content as attachment plus “帮我总结” compose a user message into the main loop;
  3. While the agent is busy, input 顺便把结果发到邮箱 → follows /busy as queue/steer/interrupt; in steer mode this sentence is injected after the next tool call (conversation_loop.py:823-830 _drain_pending_redirect does exactly that).

Part 5

Chapter 5 · Context assembly: how the “first page” sent to the model is built

5.1 A three-layer sandwich

System-prompt assembly is in agent/system_prompt.py. The module comment (system_prompt.py:10-19) splits it into three layers, joined with \n\n:

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

Why three layers? The answer hides in the design principle AGENTS.md:19-23 treats as gospel for the whole project:

Per-conversation prompt caching is sacred.

Upstream APIs bill by “prefix” cache: if you change the system prompt every round, the cache all misses, the user’s bill doubles. So Hermes made an iron law — the system prompt is byte-stable for the life of a conversation (AGENTS.md:90-91 verbatim "a system prompt that is byte-stable for the life of a conversation"). Memory wrote new content? Persist to disk only, do not change the prompt; it takes effect next session (tools/memory_tool.py:9-12 explicitly describes this “frozen snapshot” mode). Stable layer first (never changes), volatile last (minimize change) — to maximize cache hits.

5.2 Identity and persona: SOUL.md

Default identity is a hardcoded constant (agent/prompt_builder.py:139-147):

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

Users can write a custom persona in ~/.hermes/SOUL.md (load_soul_md(), prompt_builder.py:1875); /personality switches preset personalities.

5.3 Project context files: AGENTS.md / CLAUDE.md / .cursorrules

agent/coding_context.py:86 defines files automatically read into context:

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

That is: Hermes directly compatible with Claude Code’s CLAUDE.md — switch from a Claude Code project, no context-file rewrite. coding_context.py also does something smarter: detect the workspace “project fingerprint” — recognize pyproject.toml, package.json, Cargo.toml and other project markers (coding_context.py:76-83), infer the package manager from lockfiles (uv/pnpm/yarn…, :142-146), mine “verify commands” (test/lint/build) from package.json scripts and Makefile (:149-150), inject those as environment hints.

5.4 An underrated detail: prompts customized by model family

_EDIT_FORMAT_GUIDANCE in coding_context.py:171-187 is, in this author’s view, one of the most “in-the-know” designs in the whole project:

_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'` ..."),
}

Meaning: GPT/Codex-family models in their official harness (codex-rs) only learned apply_patch-format edits, so tell them to use V4A diff; Claude, Gemini, DeepSeek, Qwen, Kimi, etc. saw more “string find-replace” editors in training, so tell them to use replace mode. The same tool, different instruction manuals for different models — the comment is clear: "Matching the edit tool format to how a model was trained reduces mistakes and wasted reasoning." A model-agnostic agent going this deep is rare.

5.5 Where environment info and memory are injected

The volatile layer injects: snapshots of MEMORY.md and USER.md (Chapter 8), external memory (Honcho, Chapter 8), current timestamp/session ID/model name. Injection is fixed at the end of the system prompt — again for prefix cache.


Part 6

Chapter 6 · The agent loop (the heart): a nearly 5500-line function

6.1 Where the loop body is

The core of the whole agent is run_conversation() in agent/conversation_loop.py (line 669). This function runs from line 669 to the end of the file (6163), nearly 5500 lines — the largest and most critical single function in the project. The loop condition is at line 822:

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

Three exits are obvious:

  1. api_call_count < agent.max_iterations — per-round API call cap (default 90, run_agent.py:434);
  2. iteration_budget.remaining > 0 — iteration budget shared across sub-agents (see 6.5);
  3. _budget_grace_call — the “last chance” after budget exhaustion (grace call, so it can wrap up decently).

6.2 What each iteration does

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

In spoken-technical English: ask the model → the model says it wants tools → record “it wants tools” into the dialogue → execute tools → record tool results into the dialogue → ask the model again with the full dialogue → until the model no longer wants tools and just speaks. That is the common heartbeat of every tool-calling agent (the engineered version of ReAct). Claude Code’s heartbeat is the same rhythm; the difference is all in the details.

To “see” the loop, here is how the messages array evolves in one real conversation (every request sends this array as a whole to the API):

# 第 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 落定,循环结束。

Note the strict alternation invariant: user → assistant → tool → assistant…… AGENTS.md:90-91 lists "never two same-role messages in a row" as an iron law alongside caching, because most providers’ APIs reject role-scrambled sequences. A lot of loop code (e.g. merging adjacent user messages near conversation_loop.py:1142, filling error results for illegal tool calls at :5346-5355 to keep pairing) is carefully maintaining this invariant.

6.3 An easy-to-miss, life-saving order: persist first, then execute tools

Note step J in the flowchart above. The comment at conversation_loop.py:5361-5366:

# 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)

Meaning: write “the model decided to call tools” into the database first, then execute tools. Why? Tools can have side effects — e.g. the terminal tool ran hermes restart, the process dies on the spot. Without persist-first, after restart the agent would not know what it just did, and might repeat a destructive operation. Order bought with blood.

6.4 Streaming

The streaming path is agent/chat_completion_helpers.py:2683 (stream = request_client.chat.completions.create(**stream_kwargs), where stream_kwargs includes stream=True). Stream deltas go via stream_delta_callback all the way to CLI/TUI/gateway render layers; TTS can even start speaking before text generation finishes (run_conversation’s stream_callback parameter, conversation_loop.py:688-690).

6.5 Iteration budget: a fuel gauge for “thinking”

agent/iteration_budget.py defines a thread-safe counter IterationBudget:

  • parent agent budget = max_iterations (default 90);
  • each sub-agent budget is independent (delegation.max_iterations, default 50);
  • consume() spends, refund() returns (e.g. programmatic tool calls from execute_code do not eat budget, iteration_budget.py:25-27).

Why “budget” and not only a “count cap”? In a real task, some API calls are “free” (retries, re-sends after compact) and should not debit. Budget allows semantic fuel rather than count fuel: refundable, last-chance grace call, a clear message when exhausted (conversation_loop.py:856 prints "⚠️ Iteration budget exhausted").

6.6 Interrupt

Interrupt is “cooperative”: each loop round starts by checking agent._interrupt_requested (conversation_loop.py:837); if set, break. CLI Ctrl+C, gateway /stop, a new message arriving, all eventually set this flag. Cooperative means an in-flight tool call itself is not force-killed — safe, but also means a stuck shell command must die by timeout, not interrupt.


Part 7

Chapter 7 · Tools and permissions: 74 weapons, grouped locks, approval to pass

7.1 How tools are registered

All built-in tools live under tools/, declaring themselves through the central registry in tools/registry.py. File tools as an example (tools/file_tools.py, one long call per line):

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, ...)

Each tool registers five things: name, JSON Schema (the instruction manual for the model), handler (the function that actually works), check_fn (availability check), owning toolset. registry.py also has a clever design: AST-scan each tools/*.py file; only modules that actually call registry.register() at top level are imported (registry.py:29-47) — avoid running every tool directory’s side effects just to discover tools.

Schema is the “user manual” for the model; quality directly decides whether the model will use it. A real example (tools/file_tools.py:1947):

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"]
    }
}

Note the wording in description is entirely for the model: “use this instead of cat,” “cannot read images, use vision_analyze” — natural-language routing. Neighbor write_file schema (file_tools.py:1961) hides two engineering details: after writing .py/.json etc., auto-run syntax check, only report “errors you newly introduced this time” (old errors filtered, so the model does not go fix unrelated code); and a cross_profile soft guardrail, default blocking the agent from modifying another Hermes profile’s skills and memory. These “schemes inside the manual” are the least flashy and most valuable assets of an agent project.

check_fn is “service gating”: e.g. desktop-only read_terminal, open_preview appear in the tool list only when a GUI environment is detected (HERMES_DESKTOP) (toolsets.py:36-39 comment). That echoes the AGENTS.md “Footprint Ladder”: new capability first as CLI command + skill, then service-gated tools, then plugins, last consider adding a core tool — because every core tool’s schema is sent with every API call, a continuous bill.

7.2 Full built-in tool inventory (by group)

The table below is compiled from every registry.register(name="...") in tools/*.py (74 total), grouped by TOOLSETS in toolsets.py:96:

Group Tools Function Key params/examples
Terminal and process terminal run shell commands (six backends, see 7.4) command, background=True
process manage background processes (list/logs/kill) action="list"
read_terminal close_terminal focus_pane open_preview desktop pane ops (GUI-gated) read embedded terminal content
Files read_file read file (with line numbers) path
write_file write/create file path, content
patch edit file: mode="replace" (find-replace) or mode="patch" (V4A multi-file diff) path, mode
search_files content search (ripgrep) pattern, path
Web web_search web search (multi-backend: Firecrawl/Exa/Brave…) query
web_extract fetch and extract page body url
x_search X (Twitter) search query
Browser 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 full browser automation (snapshot=a11y tree, vision=screenshot) browser_click(element="登录按钮")
computer_use desktop-level mouse/keyboard screenshot + click coords
Vision and generation vision_analyze look at images (multimodal) image_path, prompt
video_analyze look at video video_path
image_generate text-to-image (FAL etc.) prompt
video_generate xai_video_edit xai_video_extend video generate/edit prompt
text_to_speech text to speech text, voice
Memory and planning memory read/write MEMORY.md / USER.md (Chapter 8) action="add", target="user"
todo task-list management action="add"
session_search FTS5 full-text search of historical sessions query
Skills skills_list list available skills (metadata only) —
skill_view read a skill’s full SKILL.md name
skill_manage the agent itself creates/modifies/deletes skills action="create"
Collab and delegation delegate_task spawn a sub-agent (Chapter 9) goal, tasks=[...]
clarify actively ask the user a clarifying question question
execute_code write a Python script that RPC-calls tools; fold a multi-step pipeline into one round code
Cron and kanban cronjob manage scheduled jobs 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 built-in kanban (task board for multi-agent collab) kanban_create(title="修 bug")
Project and platforms project_list project_create project_switch desktop multi-project switch —
discord discord_admin Discord messages and admin —
send_message cross-platform send platform, target
Smart home / office ha_list_entities ha_get_state ha_list_services ha_call_service Home Assistant family 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 Feishu docs/drive comments —
yb_query_group_info yb_query_group_members yb_send_dm yb_search_sticker yb_send_sticker Yuanbao (Tencent) group chat —

Additionally, enabling “tool search” adds 3 bridge tools tool_search / tool_describe / tool_call (Chapter 10); MCP server tools also merge in dynamically.

7.3 Toolsets: weapon racks switched by scene

TOOLSETS in toolsets.py:96 groups tools into switchable sets (web, file, terminal, browser, skills, memory, delegation, coding……); hermes tools can toggle interactively. Core set _HERMES_CORE_TOOLS (toolsets.py:31) is the default base. The motive is the same as the slash-command registry: freeze the schema at session start, do not swap mid-session (swapping toolsets would destroy prefix cache).

7.4 Permissions and approval: dangerous commands need a “confirm”

Dangerous-command detection is in tools/approval.py: DANGEROUS_PATTERNS (:606) is a pattern table, detect_dangerous_command() (:1988) decides, prompt_dangerous_approval() (:2273) pops confirm in CLI / sends an async approval message on the gateway. Approval results are graded: allow once / allow for the session / add to permanent allowlist (written into config.yaml). There is also “smart approval”: a cheap helper model scores command risk; low risk auto-passes (approval.py:1-11 module comment).

The most reassuring one-liner is (approval.py:35):

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

YOLO (You Only Live Once, skip-approval) mode is frozen at module import. The comment explains why: if every call read the env var, any in-process skill could bypass all approval by changing the env — a prompt-injection privilege-escalation channel. The safety switch must be typed earlier than injectable code. That detail shows threat-modeling awareness.

Two more lines of defense:

  • Tirith pre-exec scan (tools/tirith_security.py): call an external tirith binary to scan content-level threats in the command (lookalike phishing URLs, pipe-to-interpreter, terminal injection); exit codes 0/1/2 = allow/block/warn; auto-download of the binary forces SHA-256 check, and with cosign also verifies supply-chain signatures.
  • Path security (tools/path_security.py) and URL safety (url_safety.py): limit file-tool range, intercept intranet addresses, etc.
flowchart LR
    M[模型请求 terminal 执行命令] --> D{"detect_dangerous_command
approval.py:1988"} D -->|安全| R[直接执行] D -->|危险| T{"tirith 扫描
tirith_security.py"} T -->|拦截| X[拒绝并告知模型] T -->|警告/放行| Y{"YOLO 已冻结开启?"} Y -->|是| R Y -->|否| P["prompt_dangerous_approval
CLI 弹窗 / 网关异步消息"] P -->|允许| R P -->|拒绝| X P -->|永久允许| W[写入 config.yaml 白名单] --> R

7.5 Shell-exec safety design: six “cells”

terminal tool backends live in tools/environments/, six kinds: local (bare on this machine), docker (container isolation), ssh (remote machine), singularity (HPC container), modal and daytona (two serverless cloud sandboxes). The comment in terminal_tool.py:5-9 states the trade-off: "local" is fastest but unisolated; "docker" isolates but needs Docker installed; Modal/Daytona provide sleep/wake persistent sandboxes — idle environments hibernate, almost free, wake with state still there.

That is: Hermes’s security model is not “sandbox everything by default,” but let the user pick isolation by task: reply to email with local; dump untrusted code into Docker/Modal. Combined with the approval flow, three layers: “detect → approve → isolate.”


Part 8

Chapter 8 · Compaction and memory: short memory via compact, long memory via files

8.1 Why compact is needed

Model context windows are finite (e.g. 200K tokens), and agent sessions’ tool results (especially web pages, logs) eat space. Chat long enough and you must throw something away — Hermes’s approach is let a cheap model write “meeting minutes.”

8.2 The compressor: keep head and tail, squeeze the middle

The module comment of agent/context_compressor.py (:1-18) writes the strategy plainly:

  • use a helper model (the cheap/fast one) to summarize the middle of the conversation;
  • protect the head (system prompt, original instructions) and the tail (recent work site);
  • structured summary template: track “resolved/todo” issues;
  • historical section headings instead of “next step” wording (so old summaries are not misread as new instructions);
  • iterative summary update: later compacts keep information from previous summaries;
  • tail protection is by token budget (not a fixed message count);
  • a cheap pre-scan of “tool-output pruning” before summarizing.

Trigger threshold defaults to 50% of the context window (agent/agent_init.py:1765: compression_threshold = float(_compression_cfg.get("threshold", 0.50))), auto-raised per model; at most 3 compact attempts per round (conversation_loop.py:781 max_compression_attempts default). Compact is also the only scene allowed to rebuild the system prompt — AGENTS.md:19-23 says prefix cache is sacred, "the one exception is context compression".

A concrete example: you ask the agent to “research 20 papers on Rust async runtimes and write a survey.” After paper 12, context hits 50% of the window; compact starts — dozens of web_extract results from the first 12 papers (maybe hundreds of thousands of characters) go to a cheap model, compressed into a few-thousand-word structured minute: “read 12 papers, core claims are……; todo: 8 unread, the list is……”; then the agent continues paper 13 with that minute. Head (your original instruction) and tail (recent work site) are untouched. For the model, “memory” is seamlessly replaced by “notes”; work does not stop.

Another compressor with a completely different use is root trajectory_compressor.py: it is not used at runtime, but offline-processes finished agent trajectories (JSONL), same “keep head and tail, squeeze middle,” pressing trajectories into a target token budget — for producing training data (trajectory_compressor.py:5-16 strategy list is almost isomorphic to the online compressor). One project keeping both “online money-saving compact” and “offline data-making compact” is the twin design unique to a research-company project.

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

8.3 Long-term memory: two markdown files

Cross-session memory is provided by tools/memory_tool.py, extremely naive — just two files (memory_tool.py:4-7):

  • MEMORY.md: the agent’s own notes (environment facts, project conventions, tool quirks);
  • USER.md: what it knows about “you” (preferences, communication style, work habits).

Entries are separated by §; limits are in characters (because character count is model-agnostic). The memory tool has only three actions: add, replace, remove; the latter two locate entries by “short unique substrings” (memory_tool.py:17-19).

The most critical design is the frozen snapshot (memory_tool.py:9-12): both files are injected as a snapshot at session start; mid-session writes persist immediately, but do not update the system prompt — keep prefix cache; they take effect next session. Prefer that the agent “temporarily does not know what it just remembered” rather than touch the cache. That is Chapter 5’s iron law taken all the way.

8.4 Memory-system panorama: far more than two files

flowchart TB
    subgraph MEM["Hermes 的记忆与学习体系"]
        A["MEMORY.md / USER.md
声明性记忆(事实与偏好)"] B["skills/
程序性记忆(怎么做)
skill_manage 工具自动沉淀"] C["会话库 SQLite + FTS5
session_search 跨会话全文搜索"] D["curator.py 后台策展
空闲时评审/归档/合并旧技能"] E["Honcho 外部记忆
辩证式用户建模(可选)"] F["记忆提醒 nudge
定期提示自己'该存知识了'"] end A --> G[系统提示词易变层] B --> G E --> G C -.按需工具调用.-> G
  • Curator: agent/curator.py starts a background review agent when the agent is idle, touching only “skills the agent itself created”; it can pin/archive/merge/patch, but never auto-deletes (archive is recoverable); it uses the helper model, never touching the main session’s prompt cache.
  • session_search: SQLite FTS5 full-text indexes all historical sessions; session_search_tool.py:8 onward describes discover (with query) → expand (±window context) two modes, and can LLM-summarize old sessions.
  • Honcho: optional third-party “dialectical user modeling” service (hermes honcho family of commands, main.py:21-36), so the agent builds a deepening cross-session model of “who you are.”

This combo of “declarative memory + procedural skills + searchable history + background curation + user modeling” is the entity behind README’s “closed learning loop.” Versus Claude Code’s auto-memory (also markdown files + load into context), Hermes adds skill self-creation, background curation, and session search.


Part 9

Chapter 9 · Sub-agents and multi-agent: send a doppelgänger to work

9.1 delegate_task: spawn a doppelgänger

Sub-agents are implemented by the delegate_task tool (tools/delegate_tool.py). The module comment (delegate_tool.py:1-19) describes what each “child” gets:

  • a brand-new conversation (without parent history);
  • its own task_id (its own terminal session, file cache);
  • inherits the parent toolset, but strips tools blocked for sub-agents;
  • a dedicated system prompt built from “goal + context.”

The parent context only sees “the delegation call” and “the summary result” — the child’s intermediate reasoning and tool calls never pollute parent context. That is the famous “context isolation” pattern from Anthropic’s multi-agent research.

The blocklist (delegate_tool.py:46-55):

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

Each line is a permission boundary: the child is a “worker,” not “another you.”

9.2 Foreground or background? Depends who sent it

The comment at run_agent.py:6497-6507 clarifies foreground/background semantics, quite decisive:

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

Delegations initiated by the top-level model always run in the background: delegate_task immediately returns a handle; when the sub-agent finishes, its result returns to the parent session as a new message. The only exception: delegations from an “orchestrator” sub-agent (depth > 0) stay synchronous — because the orchestrator needs worker results in its own round to write a summary, and sub-agents do not own a gateway session, so async results have nowhere to go.

Two typical uses:

  • Fan-out: one delegate_task with tasks=[调研A, 调研B, 调研C], three sub-agents start in parallel, the parent keeps working, three summaries “reply” in sequence;
  • Orchestrator mode: spawn an orchestrator sub-agent, which itself synchronously spawns several workers, collects results into one comprehensive report, then hands it to the parent — parent context from start to finish only sees one report, does not even know three workers existed.
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: 汇总两个结果,回复用户

Safety detail: sub-agents run in a thread pool, default auto-deny every dangerous-command approval (_subagent_auto_deny, delegate_tool.py:74-86) — because a background thread cannot get the CLI’s interactive approval callback; calling input() would deadlock with the parent’s TUI. The comment (delegate_tool.py:65-73) writes this deadlock scene very specifically; they clearly stepped on it. Only explicit delegation.subagent_auto_approve: true allows (an “opt-in YOLO” for cron/batch).

9.3 Budget and depth control

  • each sub-agent has an independent IterationBudget (default 50, §6.5); parent 90 + each child 50, total can exceed the parent cap;
  • max concurrent sub-agents, max async sub-agents, max spawn depth (_get_max_spawn_depth) all have limits (delegate_tool.py:354-467), preventing the agent from “having grandchildren” out of control.

9.4 execute_code: fold a multi-step pipeline into one round

Another multi-agent-related capability is the execute_code tool: the agent writes a Python script; the script RPC-calls other tools — e.g. “search 10 pages, extract each, assemble a table” would be 11 request rounds; now one script, intermediate results never enter model context (README:28 so-called "zero-context-cost turns"). That is a root-cause solution to “tool results explode the context.”


Part 10

Chapter 10 · Ecosystem: commands, skills, MCP, plugins, ACP

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

10.1 Command inventory

The 83 slash commands were introduced in Chapter 4 (registry at hermes_cli/commands.py:64). The CLI top level also has dozens of hermes xxx subcommands (main.py:6-43 docstring is a catalog): gateway, setup, cron, doctor, honcho, claw migrate, sessions browse……

10.2 Skill system: “procedural memory” compatible with agentskills.io

A skill = a directory containing SKILL.md (YAML frontmatter for name and description, body for steps), plus optional references/, templates/, scripts/, assets/ (tools/skills_tool.py:7-29). It uses progressive disclosure: skills_list only shows the model metadata (name ≤64 chars, description ≤1024 chars); if interested, skill_view reads the full text, files on demand — so hundreds of skills will not explode context.

Most distinctive is the skill_manage tool (tools/skill_manager_tool.py): the agent can create a skill itself after finishing a complex task, writing “the method just verified” as reusable SKILL.md into ~/.hermes/skills/, later calling it on similar tasks. Plus Chapter 8’s curator background review, “smarter the more you use it” is not copy; it is a code path. Skill format is compatible with the agentskills.io open standard; there is also a Skills Hub for installing community skills.

10.3 MCP support: both client and server

  • Client: tools/mcp_tool.py supports stdio, HTTP/StreamableHTTP, SSE three transports; config under mcp_servers in ~/.hermes/config.yaml (module comment :12-39 has a full example including timeout, keepalive, idle recycle). Discovered MCP tools register into the tool registry, treated the same as builtins. Also supports MCP OAuth (mcp_oauth_manager.py).
  • Server: mcp_serve.py inversely exposes Hermes’s own capabilities as an MCP server to other MCP hosts. Bidirectional.

10.4 Plugin system

plugins/ is organized by category: platforms/ (~20 messaging-platform adapters), model-providers/ (33 model vendors), kanban, memory, web, security-guidance, spotify…… Platform plugins self-register via PluginContext.register_platform() in gateway/platform_registry.py (platform_registry.py:8), and are lazy-loaded (:175-198): do not import discord.py for a telegram user for nothing.

10.5 ACP: plugging into the editor ecosystem

As in Chapter 2, acp_adapter/ wraps Hermes as an ACP (Agent Client Protocol) server; ACP clients such as editors drive it over stdio JSON-RPC: session management (session.py), permission-approval forwarding (permissions.py, edit_approval.py), event stream (events.py). Start with hermes acp or the standalone hermes-acp command (acp_adapter/entry.py:9-14).

10.6 tool_search: “on-demand shelf” when there are too many tools

When MCP + plugin tools are many, sending the full schema every request is wasteful. tools/tool_search.py’s solution (module comment :1-26): pull deferrable tools off the model-visible list, leave only 3 bridge tools — tool_search (search tools), tool_describe (read the manual), tool_call (actually call). Threshold default 10%: if deferrable tools’ schema is under 10% of the context window, pass through as-is. Core tools (_HERMES_CORE_TOOLS) are never deferred. The comment also mentions a lesson from an OpenClaw cron-regression accident (the catalog must be rebuilt every time, cannot be session-cached) — you can see this project’s documentation culture: every design decision writes “why” and “what pit we stepped in.”


Part 11

Chapter 11 · Features: model freedom, auth freedom

11.1 Auth and providers

Both worlds are supported (hermes_cli/auth.py:1-16):

  • OAuth device code: Nous Portal (hermes setup --portal, one login unlocks 300+ models and a tool gateway);
  • API key: OpenRouter, OpenAI, custom OpenAI-compatible endpoints, etc., stored in ~/.hermes/.env or an external secret manager (Bitwarden, 1Password, hermes secrets command).

plugins/model-providers/ lists 33 provider plugins, including: nous, openrouter, anthropic, openai-codex, gemini, bedrock, vertex, azure-foundry, deepseek, xai, groq-family fireworks/deepinfra/novita, alibaba (including coding-plan), kimi-coding, minimax, zai, qwen-oauth, ollama-cloud, huggingface, copilot, arcee, stepfun, upstage, xiaomi, custom…… plus native adapters in agent/ for anthropic/gemini/bedrock/vertex/codex (handling each vendor’s non-OpenAI-compatible protocol).

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

11.2 Model selection and switching

hermes model interactive select + /model in-session switch + --global persist (commands.py:134). Fallback chain (hermes fallback): when the primary is rate-limited/overloaded/disconnected, try backups in order. Also MoA (Mixture of Agents): /moa <question> sends the same question to multiple models then synthesizes (commands.py:116).

11.3 Local models and self-hosting (NousResearch’s home turf)

As a home of open-weight models, Nous naturally treats self-hosting as first-class:

  • custom provider + any OpenAI-compatible endpoint (vLLM, SGLang, llama.cpp all work);
  • ollama-cloud plugin; provider-catalog comments also mention lmstudio, openai-api and other “undocumented spec providers” (hermes_cli/provider_catalog.py:22);
  • Nous’s own Hermes-series open-weight models (e.g. Hermes 4) are trained for this kind of tool-calling loop — a matching “model + harness” pair with this project.

11.4 thinking / effort

The /reasoning command provides eight gears of reasoning strength (none/minimal/low/medium/high/xhigh/max/ultra, commands.py:158-160), and can control display of reasoning content (show/hide/full). Internally there is agent/think_scrubber.py (scrub thinking tags) and agent/reasoning_timeouts.py (reasoning timeout “de-pressure” guidance for the model, thinking_timeout_guidance.py).

11.5 Feature tour

Feature Location Notes
Sustained goal /goal hermes_cli/goals.py after each round, a helper model referees: “is the goal done?” if not, auto-continue one more round — the comment calls it "the Ralph loop"; referee failure fail-opens and continues
Scheduled jobs cron/scheduler.py built-in scheduler (60s tick, file lock against re-entry), natural-language task description, results can deliver to any messaging platform
Voice mode tools/tts_tool.py, voice_mode.py streaming TTS, start reading before text finishes; voice messages to text
Kanban tools/kanban_tools.py multi-agent shared task board, 12 kanban_* tools
Trajectory production batch_runner.py, trajectory_compressor.py batch-run tasks to generate training trajectories, then compact the middle to a token budget — data for the next model
Desktop apps/desktop/ Electron app (Hermes Desktop), with project switch, preview pane
Web dashboard web/ Vite + React browser console

Part 12

Chapter 12 · Summary: design philosophy and trade-offs

12.1 Three beliefs that run through everything

After reading the code, almost every corner repeats the two-and-a-half principles in AGENTS.md:16-27:

① Prefix cache is sacred. System prompt byte-stable, toolsets frozen at session level, memory via frozen snapshot, skills via progressive disclosure, compact the only exception. Everything is for “long sessions hit the cached prefix every round,” pressing the user’s API bill to a minimum. That is economics promoted to an architectural constraint.

② Narrow-waist core; capability grows at the edge. "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) Priority ladder for new capability: extend existing code → CLI command + skill → service-gated tool (check_fn) → plugin → MCP → only as a last resort add a core tool. So we see: platforms are plugins, providers are plugins, even smart home and Feishu are gated tools. The core does one thing — keep the loop stable.

③ Supply-chain paranoia and a self-healing constitution. All deps exact-version locked (pyproject.toml:17-30 comments record the full 2026-05-12 tightening after a PyPI poisoning incident); chained venv self-heal at startup; tirith download forces signature check; YOLO flag frozen at import. That is the basic self-requirement of software that “must run unattended for months on a stranger’s computer.”

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

12.2 Real highlights

  1. Closed-loop learning is a code path, not a slogan: skill_manage (skills from experience) → curator.py (review skills when idle) → memory (write facts) → session_search (search history) → Honcho (user modeling), five pieces interlocked.
  2. Model-agnostic taken to the extreme: 33 provider plugins + prompts customized by model family (§5.4 edit-format guidance) + fallback chain + MoA.
  3. Gateway-first “agent as a contact”: ~20 platform adapters, async approval, voice messages, cron delivery — it is not a tool you open; it is a member of your address book.

12.3 An honest trade-off list

Trade-off Cost What you get
Python monolith kernel + giant files (cli.py 16590 lines, run_agent.py 6907 lines, conversation_loop.py 6163 lines) beginners bounce; merge conflicts high (AGENTS.md itself calls for splitting the god-file into mixins) iteration speed, monkey-patch-friendly test ecology, simple single-process deploy
Cooperative interrupt a stuck tool cannot be force-killed, only timeout will not kill a half-written file
Frozen-snapshot memory in-session “remember then forget” (takes effect next round) prefix cache 100% stable
Safety via “approval + optional isolation,” not default sandbox under local backend, a mistaken approve still has risk low friction for a personal-assistant scene
Extremely wide feature surface (kanban, voice, video gen, Yuanbao, Feishu……) huge maintenance surface, uneven edge quality the ambition of “one agent for digital life”

12.4 A reading path for beginners

If you want to read this source yourself, this shallow-to-deep path is recommended:

  1. README.md + AGENTS.md (understand design principles first; code will go twice as far);
  2. pyproject.toml:309 → hermes_cli/main.py:14339 main() (entry);
  3. agent/conversation_loop.py:669 run_conversation(), with Chapter 6’s flowchart for the while loop;
  4. tools/file_tools.py (what the simplest tool looks like) → tools/terminal_tool.py (what the most complex tool looks like);
  5. tools/delegate_tool.py (sub-agents) → agent/context_compressor.py (compact) → tools/memory_tool.py (memory).

Honesty statement: every file:line citation in this lecture comes from actually reading the commit above; a few functions not deeply verified (e.g. Honcho protocol details, implementation quality of some platform adapters) are only restated from official docs and module comments. The project iterates extremely fast (this commit is one day old); line numbers will drift in new versions, but architecture and principles are stable.

↑