Skip to content

Architecture Overview

源码版本v2026.7.20

Hermes Agent is an open-source (MIT) desktop AI agent by Nous Research, positioned as "a persistent agent that grows with you": a message arrives from Telegram/Discord/Slack/etc, the core loop calls the model + tools to complete the task, and the experience is distilled into memory and skills for reuse next time.

Layered View

Each layer in one sentence

  • UI Layer: TUI / CLI / Web / ACP (editor protocol) — just entry points, hold no business logic.
  • Gateway: gateway/run.py unifies access; platforms are adapters; session/delivery separated; streams and hooks pluggable.
  • Core Loop: run_conversation orchestrates "model call → tool call → result backfill → budget decrement", with fallback on failure.
  • Capabilities: Tools are atomic functions (tools/registry.py); Skills are higher-order evolvable flows (rewritten by the learning loop); Provider/MCP/Plugins supply models and external capabilities.
  • Scheduling: Cron drives timed tasks, Subagent delegates in isolation (zero context cost), 6 sandbox backends isolate execution.
  • Learning Loop: Distills conversation experience into skills and a user model — the flywheel behind "better the more you use it".

Top-Down: Representative Signatures per Layer

The fastest way to understand Hermes' layering is to look at the "signature" each layer exposes in the code.

UI Layer → forwarder into the core loop. Every shell calls AIAgent.run_conversation, but it's just a forwarder (run_agent.py:6350):

python
def run_conversation(self, user_message, system_message=None,
                     conversation_history=None, ..., moa_config=None) -> Dict[str, Any]:
    """Forwarder — see ``agent.conversation_loop.run_conversation``."""
    from agent.conversation_loop import run_conversation
    token = set_conversation_context(self._conversation_root_id())
    with scoped_runtime_main({}):
        return run_conversation(self, user_message, ...)

The conversation ID is pushed down via a ContextVar — every LLM call inside the main loop (compression, vision, MoA slot, background-review fork) picks up the same tag automatically.

Gateway Layer → the platform-adapter manager. GatewayRunner mixes in authorization/dashboard/slash-command capabilities via mixins (gateway/run.py:22392); the entry start_gateway(replace=True) kills any old instance first — a backdoor for systemd restarts where the previous process hasn't fully exited:

python
class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, GatewaySlashCommandsMixin):
    """Main gateway controller. Manages the lifecycle of all platform adapters
    and routes messages to/from the agent."""

Core Loop → where the real work happens. run_conversation lives in agent/conversation_loop.py, orchestrating "model call → tool call → result backfill → budget decrement" with fallback on failure; signature above.

Capabilities → tool registration. Each tools/*.py calls registry.register at module-import time (tools/registry.py:365); override=True is required for a plugin to replace a built-in tool:

python
def register(self, name, toolset, schema, handler, check_fn=None,
             requires_env=None, is_async=False, description="", emoji="",
             max_result_size_chars=None, dynamic_schema_overrides=None, override=False):
    """Register a tool.  Called at module-import time by each tool file.
    ``override=True`` is an explicit opt-in for plugins ... Without it,
    registrations that would shadow an existing tool are rejected."""

A plugin that wants to replace a built-in tool must explicitly pass override=True and go through the allow_tool_override config — preventing a plugin from silently swapping out the browser tool. Same-name MCP-tool overrides go through a separate channel.

Scheduling → the cron tick. tick (cron/scheduler.py:3888) is a single scan guarded by a file lock; the gateway calls it once every 60 seconds from a background thread. The adapters parameter lets cron tasks reuse the platform connections the gateway has already built, instead of reconnecting on its own.

Learning Loop → learning-graph aggregation. build_learning_graph (agent/learning_graph.py:248-328) pieces learned (non-built-in) skills and memory into nodes/edges. It first calls build_skill_nodes(_skill_roots()) to scan both the base and profile roots, then filters skills with source != "base" and (created_by == "agent" or use_count > 0) as "learned" — built-in skills stay out of the learning graph so noise doesn't drown out the evolution signal.

Layering Motivation

Why this layering? In one line: let the layers that can change not pollute the layers that can't.

  • The UI layer swaps shells often (TUI/Web/IDE protocols each iterate at their own pace), and swapping shells must not force business changes; so shells only attach callbacks (step_callback/event_callback) and don't enter run_conversation internals.
  • The gateway layer adds new platforms all the time (QQbot, Enterprise WeChat, new Discord styles), but the core loop must not be platform-aware — so platform logic stays in adapters, and the gateway only decouples messages from agent calls.
  • The capabilities layer is "atoms"; the learning loop is "evolution" — atoms must not be polluted by the evolution path (otherwise a background-review bug breaks a tool and takes the main loop down with it), so learning_mutations writes to skills/ not tools/.
  • Scheduling is a cross-cutting concern: Cron and Subagent both "trigger the main loop from outside the main loop", sharing run_conversation without directly holding agent state.

Common Misreadings

  • "Hermes is an agent framework" — it isn't. It's a concrete desktop agent implementation; the core loop, tool set, and skills directory all ship with Hermes. The framework-like surfaces (MCP, Plugin, Provider abstractions) are boundaries it exposes for extensibility, not its essence.
  • "The learning loop auto-optimizes the model prompt" — it doesn't. It only edits SKILL.md under skills/ and memory under ~/.hermes/skills/, never the system prompt templates. Prompt templates are fixed files in the repo; changing them takes a release.
  • "Subagent is distributed" — it isn't. Subagent forks the agent in-process (shared runtime); sandboxes are execution isolation (code runs in Docker/SSH/Modal/Daytona), not distribution of agent state.
  • "Gateway layer = messaging-platform access" — only half right. The gateway also owns session state (session/), delivery deduplication (delivery_ledger), streaming dispatch (stream_*), slash commands, and kanban watchers — these are all "between the messaging platform and the agent", not just access.

Suggested reading order

  1. Startup & Entry — how a command becomes a running agent
  2. Main Loop — what happens when a message arrives
  3. Capabilities — the difference between tools and skills
  4. Gateway — how multi-platform delivery works
  5. Scheduling — automation and isolation
  6. Learning Loop — the self-improvement mechanism

Unofficial community learning site. Content based on the MIT-licensed NousResearch/hermes-agent source.