Hermes Agent architecture: how a message moves through the source
The Hermes Agent architecture is one Python agent class with several front doors. The CLI, the terminal UI, the desktop app, a messaging gateway for Telegram, Slack and the rest, and a cron ticker all build the same AIAgent and call the same loop. That loop assembles a cached system prompt, asks whichever model provider you configured for a reply, and runs any tool calls through one registry filtered by per-platform toolsets. Shell commands pass a pattern-based approval check and then run on a terminal backend, which is your own machine unless you pick Docker, SSH or a cloud sandbox. Sessions land in a SQLite file and curated memory in Markdown files under ~/.hermes. Official system overview; Data flows.
This is a source reading of the public repository at revision 0b89435e7e4f7fab88a34eb69f13bf339eb2fd00. We didn't run Hermes, so everything below describes what the code and its documentation say, and where those two disagree we say so. For setup, costs and first tasks, start with our Hermes Agent review.
- Every surface, from Telegram to cron, ends in the same
run_conversationloop; platform differences live at the entry point. - Tools reach the model in three filters: toolset selection, a per-tool availability check, then optional deferral of MCP and plugin tools behind a search tool.
- The dangerous-command check is regex-based and lives inside the terminal and code-execution tools. On isolated backends only your own deny rules still apply.
- Scheduled runs need the gateway's ticker, or a manual
hermes cron tick, and each job's toolsets fail closed when they can't be read. - The developer docs lag the code in places: five API modes in code against three documented, and a subagent iteration default of 250 against a documented 50.
How a message moves through the Hermes Agent architecture
The table follows one message from arrival to reply. Messaging traffic takes every row; a CLI turn skips the gateway rows.
| Step | Component | What happens | Source |
|---|---|---|---|
| 1 | Entry point | You type in the CLI or TUI, the desktop app sends a JSON-RPC call, a platform adapter receives a chat message, or the cron ticker finds a due job. | Entry points; TUI and desktop |
| 2 | Gateway admission | The inbound handler runs authorisation, command checks and session lookup before any agent work. | _handle_message |
| 3 | Toolset resolution | The platform's configured toolsets are read, then agent.disabled_toolsets is subtracted. | Per-source toolsets |
| 4 | AIAgent | A cached agent for the session is reused or a fresh one is built with those toolsets. | Fresh agent |
| 5 | Prompt assembly | Identity, guidance and context files, then the skills index and memory snapshot, joined in three cache tiers. | Tiers |
| 6 | Model provider | The request goes out in one of five wire formats chosen from provider and base URL. | API modes |
| 7 | Tool round | Tool calls are dispatched and their results appended; the loop calls the model again. | Loop |
| 8 | Approval and backend | A shell command passes the guards, then runs on the selected terminal backend. | Guards |
| 9 | Persist and deliver | The turn is saved to state.db and the reply goes back through the adapter. | CLI flow; Gateway flow |
Hermes Agent entry points: one agent class behind every surface
The developer guide states the design rule plainly: one AIAgent class serves the CLI, gateway, editor integration, batch runs and the API server, and platform differences live in the entry point, not the agent. The code we traced matches. The classic CLI calls self.agent.run_conversation(...), the gateway's turn runner calls agent.run_conversation(api_message, ...), and the cron scheduler constructs its own AIAgent per job. Design principles; CLI call; Gateway call; Cron agent.
The terminal UI and the desktop app are thinner than they look. A Node renderer draws the screen and talks newline-delimited JSON-RPC to a Python process, tui_gateway, which owns sessions, tools and model calls. The desktop app reaches the same Python server over WebSocket. When the agent needs you, for an approval or a clarifying question, the server sends a request back to the client and blocks the agent thread until it answers. TUI and desktop process model.
The messaging gateway is a long-running process. Built-in adapters sit in gateway/platforms/ and the common chat platforms ship as bundled plugins under plugins/platforms/, so Telegram, Discord, Slack and WhatsApp are all plugins over one adapter interface. Before a message reaches the agent, the gateway checks who sent it. The order runs through chat-scoped grants, per-platform allow-all flags, adapter role checks, approved pairing codes and allowlists, and with no allowlist set it denies unless GATEWAY_ALLOW_ALL_USERS is true. Webhook events skip the sender check because they're HMAC-verified instead. Gateway layout; Authorisation order; Default deny.
The Hermes Agent loop, turn by turn
run_agent.py still defines AIAgent, but the loop itself now lives in _run_conversation_turn in agent/conversation_loop.py, and each phase is a separate function: begin the iteration, prepare it, assemble the API request, run a pre-call check that can stop or compress the turn, call the model with retries, normalise the response, then either run a tool round or finish with text. If the model asked for tools, the loop goes round again. The loop.
The loop stops when the model answers without tool calls, when an error ends the turn, or when it runs out of iterations. That last limit is looser than the docs suggest. The constructor defaults max_iterations to sys.maxsize, commented as unlimited, and the shipped config sets agent.max_turns to null, which it also calls unlimited because caps "caused silent mid-task truncation". The agent-loop guide still describes a default of 500. Constructor default; Config default; Documented budget.
Inside a tool round, the guide says a single call runs directly and several run concurrently on a thread pool, with interactive tools such as clarify forced to run one at a time and results put back in the original order. It also says todo, memory, session_search and delegate_task are intercepted by the agent before the registry, because they change the agent's own state. We read the loop and the executor's entry points but didn't trace concurrency end to end, so treat those two points as documented behaviour. Tool execution.
How tools reach the model in Hermes Agent
Tools register themselves. At import, model_tools.py calls discover_builtin_tools(), which scans every file in tools/ for a top-level registry.register() call and imports the ones that have it. Each entry carries a name, a toolset, a JSON schema, a handler and an optional check_fn. Discovery; Tool entry.
What the model actually sees passes three filters:
- Toolsets.
toolsets.pydefines a core list of about 50 names, fromterminalandread_filethrough the browser tools,memory,delegate_taskand the kanban tools. Platform bundles such ashermes-cliandhermes-telegramare that core list, sometimes with extras. Enabled toolsets are expanded, then disabled ones are subtracted last, so a disabled toolset wins even inside a bundle. Core list; Bundles; Selection. - Availability. The registry drops any tool whose
check_fnfails, for example when Docker or a browser isn't present, with results cached for about 30 seconds. Definitions. - Tool search. When MCP and plugin tools would take too much of the context window, they're swapped for a search, describe and call bridge. Core tools are never deferred. Deferral.
The Telegram bundle is described in the source as "full access for personal use", so a chat platform gets the same core tools as the CLI, terminal included. The webhook bundle is the exception: incoming webhook payloads are treated as untrusted and get only web search, web extract, vision and clarify. Webhook-safe tools; Webhook bundle.
Model providers in the Hermes Agent architecture
Hermes doesn't ship a model. resolve_runtime_provider() turns your configured provider into credentials, a base URL and an API mode, working down a documented ladder: disabled-provider guard, named custom providers, local endpoints, explicit keys, a credential pool, OAuth providers and finally registry providers with API keys. CLI, gateway, cron and side tasks all share this resolver. Resolution ladder; Shared resolver.
The API mode decides the wire format. The developer guide lists three: chat_completions, codex_responses and anthropic_messages. The code accepts five, adding bedrock_converse and an opt-in codex_app_server that hands the whole turn to a Codex subprocess. Some hosts are pinned to one format, api.anthropic.com to Anthropic Messages for instance, and anything unmatched falls back to chat_completions. Internally every mode converges on OpenAI-style message dicts. Valid modes; Fallback order; Documented modes.
A second, auxiliary model handles side jobs such as summarising for compression, vision and the smart approval check described below, where it rates a flagged command's risk. Smart approval.
Where Hermes Agent runs commands, and what you approve first
The terminal tool reads TERMINAL_ENV, bridged from terminal.backend, and defaults to local. A table maps seven names to builders: local, docker, singularity, modal, daytona, vercel_sandbox and ssh. Any other name is handed to a plugin backend. Backend read; Builders.
Approval sits inside the terminal tool rather than in the loop. check_all_command_guards() runs in this order:
- If the backend counts as isolated, skip the prompts. Singularity, Modal, Daytona and Vercel always count; Docker counts only without host bind mounts. Isolation test
- On a non-isolated backend, apply floors no setting can bypass: hardline patterns such as deleting the root filesystem, your own
approvals.denyrules, and deleting the interpreter Hermes runs from. On an isolated backend, step 1 has already returned and only yourapprovals.denyrules still apply. Floors; Isolated shortcut - YOLO or mode
offapproves. Unattended contexts such as cron apply theirdenyorapprovesetting. Otherwise the command is matched againstDANGEROUS_PATTERNS, and only a match goes to a human, or to the auxiliary model first insmartmode. Decision; Patterns
The shipped defaults are smart, a 300-second timeout, and deny for cron, single-query and unattended sessions. The terminal and code-execution tools call these guards in the code we searched (the TUI's direct shell command reuses the same pattern detector to refuse dangerous commands outright), so sending a message or calling an MCP tool doesn't pass through this check. TUI shell check. The project's own security policy says the operating system is the only real boundary and calls the approval gate a heuristic. Approval defaults; Security boundary. The Hermes vs OpenClaw comparison puts these defaults next to OpenClaw's.
Skills, memory and subagents in Hermes Agent
Skills are Markdown procedures, not code. If the session has any of skills_list, skill_view or skill_manage, the system prompt gets an index of installed skills, and the agent loads a full skill with skill_view when it needs one. The index leads the volatile tier, the end part of the prompt that is most likely to change between rebuilds, so editing skills doesn't invalidate the cached identity and guidance above it. Skills gate; Volatile tier.
Memory follows the skills index in that same tier: a snapshot of MEMORY.md and USER.md, plus a block from an external memory provider if one is configured. The snapshot is taken when the prompt is built, so a fact saved mid-conversation shows up in the next session, or sooner if context compression or /undo rebuilds the prompt. Our Hermes Agent memory page covers the files, limits and providers in detail. Memory blocks; Rebuild reloads memory; /undo rebuild.
delegate_task spawns child agents with their own budgets. Children can't delegate further, ask you questions, write memory, send messages or schedule cron jobs, and default to the terminal, file and web toolsets. Nesting is flat by default (MAX_DEPTH = 1), and each child gets 250 iterations in the shipped config, though the agent-loop guide says 50. Blocked child tools; Depth; Child iterations.
Hermes Agent cron and state on disk
Cron jobs are agent prompts, not shell lines. They're stored in cron/jobs.json under the Hermes home, with output written per run beside them. The ticker is started by the gateway, and the user guide says it outright: cron execution is handled by the gateway daemon, every 60 seconds. Without the gateway nothing fires on its own; hermes cron tick runs due jobs once by hand. Each job gets a fresh AIAgent with no history. Its toolsets come from the job or the cron platform config, and an unreadable restriction makes the run fail rather than widen to every tool. Job store; Store path; Gateway starts the ticker; User guide; Manual tick; Fail-closed toolsets.
Everything else hangs off one directory. get_hermes_home() takes a context override, then HERMES_HOME, then the platform default, and each profile gets its own home with its own config, memory, sessions and gateway. Sessions go to state.db, a SQLite database with full-text search. Settings live in config.yaml, secrets in .env, and skills, memories and cron in subdirectories. Home resolution; Session database; Settings and secrets; Profile isolation.
Where the Hermes Agent architecture docs and code disagree
The developer guide is good and mostly current, but at this revision these points don't line up with the code:
- API modes. Three documented, five accepted in code.
- Loop budget. A documented default of 500 iterations, against an unlimited default in the constructor and config.
- Subagent budget. 50 documented, 250 in the shipped config.
- Skills placement. The prompt-assembly guide lists the skills index in the volatile tier, then says a few lines later that skills are part of the stable tier. The code puts it first in the volatile tier. Prompt tiers
- Code execution. The security policy describes the code-execution tool as a host subprocess that a terminal backend doesn't confine. The tool's own module notes describe a remote kernel on remote backends. We didn't trace which path each configuration takes. Policy; Module notes
None of this changes the overall shape. It does mean the source is the better reference for limits and defaults. We also didn't establish what the agent sends to each provider on the wire, how concurrent tool calls interact with approvals, or how the many platform plugins differ. For the same axes across other projects, see the 15-agent comparison and how we take agents apart. Personal Agent Bench is run by the Zentor team, which builds agent software; we aren't affiliated with Nous Research.
FAQ
Is there an official Hermes Agent architecture diagram? Yes. The developer guide opens with a text diagram of entry points, the AIAgent core, session storage and tool backends. The table at the top of this page follows the same path with a pinned code link for each step.
Does the Telegram bot use a different agent from the CLI? No. The gateway builds the same AIAgent class and calls the same loop. What changes is the toolset bundle for the platform and the sender authorisation in front of it.
Does Hermes Agent run the model on my machine? Only if you point it at a local endpoint. Otherwise each turn goes to the provider you configured, in one of five API formats.
Do Hermes Agent subagents share memory with the main agent? They can't write it. memory is on the list of tools stripped from children, along with delegation, messaging and cron.
Where does Hermes Agent store its data? In one Hermes home directory. It's a per-context override if one is set, then the HERMES_HOME environment variable, then ~/.hermes (or %LOCALAPPDATA%\hermes on Windows). Inside it are config.yaml for settings, .env for secrets, memories/ for MEMORY.md and USER.md, state.db for sessions, and cron/jobs.json for scheduled jobs. Each profile gets its own home. Home resolution; Platform default; Memories directory.