myos/hermes/hermes-source-map.md
Hermes Agent — Source Map
Repo: /Users/arijitchowdhury/Dropbox/AI-Development/AI-OS/hermes-agent (Nous Research, MIT, Python ≥3.11 <3.14, version 0.18.0 per pyproject.toml)
Hermes Agent is not one program — it's one agent runtime (AIAgent in run_agent.py, ~3 dozen supporting modules under agent/) wrapped by three different front doors: a terminal CLI, a messaging gateway (Telegram/Discord/Slack/WhatsApp/Signal/…), and an editor protocol adapter (ACP, for Zed). All three construct the same AIAgent class and drive the same conversation loop. Everything else — skills, plugins, hooks, memory, the curator — hangs off that one core loop.
What Hermes is FOR, and the rest of the learning area
We study Hermes because it is the execution engine for AI-OS — the CEO/operator brain that runs work on the VPS while the vault stays the source of truth. Everything we learned about it lives together here:
- This file (source map) — the fastest orientation: what file does what.
- Deep Architecture Map — the cited
file:linedeep read (the primary). - Docs Overview — the official-docs synthesis, Karpathy-wiki style.
- Peer-session teardown — leads from a parallel deep-read, to reconcile against the primary.
- Storage Architecture — how Hermes persists data, and what AI-OS borrows from it.
This doc is a map for someone about to fork this into a custom OS: what each piece does, which file to read first, and where the seams are.
1. The three entry points
All three are declared in pyproject.toml under [project.scripts] (line 308-310):
wzxhzdk:0
1a. CLI — hermes_cli/main.py → cli.py → run_agent.AIAgent
hermes_cli/main.py:main() (line 12667) does process setup (Windows stdio, stale-install cleanup, first-run provider check) then routes subcommands. Chat is cmd_chat() (line 2215): it resolves --resume/--continue, handles --yolo/--safe-mode/--ignore-rules flags by setting env vars, then either launches the TUI (_launch_tui, Node/Electron-adjacent terminal UI under ui-tui/) or calls into cli.py:main() (line 15581) — the classic prompt_toolkit interactive loop.
cli.py never subclasses AIAgent — it imports it lazily:
wzxhzdk:1
This lazy-import indirection is deliberate (comment at cli.py:832: "Bare interactive startup only needs the prompt; the full agent/tool registry is initialized on first use") — startup latency matters for a CLI.
1b. Gateway — gateway/run.py → GatewayRunner
python -m gateway.run or hermes gateway start. GatewayRunner (gateway/run.py:2668, subclasses GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, GatewaySlashCommandsMixin) is the daemon: it owns one AIAgent instance per session key, cached in an LRU (_AGENT_CACHE_MAX_SIZE = 128, idle-evicted after 1h — gateway/run.py:63-65), and fans inbound messages from N platform adapters (gateway/platforms/*.py) into that cache. See §4 for session identity.
1c. ACP adapter — acp_adapter/entry.py → acp_adapter/server.py:HermesACPAgent
hermes acp speaks the Agent Client Protocol over stdio JSON-RPC (used by Zed and other ACP-aware editors). acp_adapter/session.py's ACPSessionManager maps one ACP session → one AIAgent:
wzxhzdk:2
So an ACP session and a CLI session and a gateway session are the same class, constructed with different kwargs (platform="acp" vs "cli" vs "telegram", different clarify_callback, different terminal backend). This is the load-bearing architectural fact: one runtime, three transports.
2. The agent runtime (agent/ + run_agent.py)
run_agent.py (268K, mostly the AIAgent class definition at line 403) is deliberately thin. AIAgent.__init__ (line 426) calls init_agent(self, ...) at line 501, and that function body — 60+ parameters, ~1,400 lines of state setup, provider auto-detection, credential resolution — actually lives in agent/agent_init.py. The doc-comment at the top of that file explains why: "keeping it in run_agent.py bloats that file with code that's mostly setup state, then forget."
The same extraction pattern repeats for the conversation loop:
wzxhzdk:3
agent/conversation_loop.py's docstring calls this out explicitly: it's "the roughly 3,900-line run_conversation body that drives one user turn through the agent (model call, tool dispatch, retries, fallbacks, compression, post-turn hooks, background memory/skill review nudges)." It takes the parent AIAgent as its first arg and mutates it via attribute access — so run_agent.py stays the stable public surface tests patch against (_ra() lazy-reference pattern used throughout agent/*.py, e.g. agent/system_prompt.py:44) while the actual logic lives in the extracted module. If you're hunting for "where does a turn actually happen," start in agent/conversation_loop.py, not run_agent.py.
Adjacent single-purpose modules worth knowing by name (all under agent/):
- agent/tool_dispatch_helpers.py, agent/tool_executor.py — the tool-calling half of a turn
- agent/context_engine.py, agent/context_compressor.py, agent/context_breakdown.py — context-window management and compaction
- agent/memory_manager.py — builds the memory block injected per-turn (see §5)
- agent/curator.py — background skill-maintenance daemon (see §6)
- agent/anthropic_adapter.py, agent/bedrock_adapter.py, agent/vertex_adapter.py, agent/gemini_native_adapter.py, agent/codex_responses_adapter.py — one adapter per non-OpenAI-shaped API surface; agent/chat_completion_helpers.py is the OpenAI-shape default path
3. System-prompt assembly: SOUL.md, three tiers, and the cache invariant
This is the answer to "how is an agent + its personality configured."
agent/system_prompt.py's module docstring lays out the model precisely: the system prompt is built once per session, three tiers joined with \n\n, and it is not rebuilt every turn — only a context-compression event triggers a rebuild, because rebuilding it would blow the upstream provider's prompt-cache prefix (Anthropic/OpenAI prompt caching keys off a stable prefix).
wzxhzdk:4
SOUL.md — the persona file
agent/prompt_builder.py:load_soul_md() (line 1797) reads {HERMES_HOME}/SOUL.md (default ~/.hermes/SOUL.md) and returns it as-is (after a content-injection scan and a truncation pass) to fill the stable/identity slot — it is loaded fresh at prompt-build time, not baked into anything at install; the docstring for the legacy-template detector in hermes_cli/default_soul.py notes "This file is loaded fresh each message — no restart needed."
The default seeded into a fresh ~/.hermes/SOUL.md is DEFAULT_SOUL_MD in hermes_cli/default_soul.py:3 — a short "You are Hermes Agent... helpful, direct" paragraph. is_legacy_template_soul() in the same file detects and silently upgrades pre-existing comment-only scaffolds that older installers wrote (no user persona in them), so this is not a snapshot-and-forget file — the CLI actively repairs it across upgrades if it detects the user never customized it.
This is the primary "who is my agent" edit point for a fork: replace the content of SOUL.md (or override DEFAULT_SOUL_MD) and every surface — CLI, gateway, ACP — inherits the new identity, because all three construct AIAgent and all three route through load_soul_md().
Project context — separate, priority-ordered slot
build_context_files_prompt() (agent/prompt_builder.py:1937) loads one of, first match wins:
1. .hermes.md / HERMES.md (walks up to the git root)
2. AGENTS.md / agents.md (cwd only)
3. CLAUDE.md / claude.md (cwd only)
4. .cursorrules + .cursor/rules/*.mdc (cwd only)
SOUL.md is independent of this list and always included when present (unless skip_soul=True, used when the identity slot already loaded it — see agent/agent_init.py:281's skip_context_files/load_soul_identity params). Each file is capped (CONTEXT_FILE_MAX_CHARS = 20_000, scaling with the model's context window when known) and content-scanned for injection before use.
Volatile tier — memory + USER.md
Per-turn dynamic state: a memory snapshot (built by agent/memory_manager.py:build_memory_context_block), a USER.md cross-session profile, an optional external memory provider block (Honcho, mem0, etc. — see §5), and a timestamp/session/model line. Because this tier is rebuilt every turn but sits after the cached stable+context prefix, it doesn't defeat prompt caching (Anthropic-style caching only needs a stable prefix, not a stable suffix).
4. Sessions
Two different session stores exist for two different transports — read docs/session-lifecycle.md in the repo for the authoritative version; summary here:
- CLI/ACP sessions persist in a SQLite store,
hermes_state.py(docstring: "Provides persistent session storage with FTS5 full-text search, replacing the per-session JSONL file approach"). WAL mode for concurrent readers, FTS5 virtual table for full-text search across message history, andparent_session_idchains for compression-triggered session splitting. - Gateway sessions are keyed by
SessionSource(gateway/session.py) — a frozen record of platform + chat_id + user_id + thread_id, etc. — hashed into a deterministicsession_key.SessionEntrymaps that key to the currentsession_id(formatYYYYMMDD_HHMMSS_<8hex>) and is persisted to{sessions_dir}/sessions.json.GatewayRunnercaches the liveAIAgentper session_key (§1b) so a Telegram DM and a Discord thread never share state even if the same user is on both.
Session resume (--resume, -c/--continue, ACP's LoadSessionResponse) rehydrates conversation history from the SQLite store back into a fresh AIAgent — it does not keep the process alive between CLI invocations (the CLI is not a daemon); the gateway and ACP server, by contrast, are long-running and keep AIAgent instances warm in memory.
5. Memory
agent/memory_manager.py builds the per-turn memory block injected into the volatile prompt tier. Beyond that in-process memory, Hermes ships pluggable external memory backends as first-class plugins under plugins/memory/: honcho, mem0, hindsight, retaindb, byterover, holographic, openviking, supermemory — each a directory with a plugin.yaml manifest (name, version, description, pip_dependencies, hooks: list) and an __init__.py with register(ctx). Example, plugins/memory/honcho/plugin.yaml:
wzxhzdk:5
This is the pattern for the whole plugin system (§7) — memory backends are just plugins that hook on_session_end (and others) to write/read state, nothing structurally special.
6. The self-improvement / curator loop
The README's headline feature ("creates skills from experience, improves them during use") is implemented by two cooperating pieces:
/learn(agent/learn_prompt.py) — user-invoked.build_learn_prompt()constructs one prompt instructing the live agent to gather sources (files, URLs, or "what I just did in this conversation") and author aSKILL.mditself via theskill_managetool, following house authoring standards embedded in the prompt (_AUTHORING_STANDARDS, e.g. description ≤60 chars). There is deliberately no separate distillation model — the docstring notes this "works identically on local, Docker, and remote terminal backends" because it's just another agent turn.- The curator (
agent/curator.py) — unattended, inactivity-triggered (no cron daemon; docstring: "when the agent is idle and the last curator run was longer thaninterval_hoursago,maybe_run_curator()spawns a forked AIAgent to do the review"). It only ever touches agent-created skills (checked viatools/skill_usage.is_agent_created), never auto-deletes (archive only, recoverable), and pinned skills are exempt from all auto-transitions. State persists in.curator_stateunder HERMES_HOME. It runs on the auxiliary client so it never disturbs the main session's prompt cache (explicit invariant in the module docstring).
Skills created this way land in the same ~/.hermes/skills/ (or profile-scoped) tree that hand-authored skills use — see §8.
7. Plugins and hooks — the two extension mechanisms
Hermes has two distinct extension systems that are easy to conflate. Know which one you need.
7a. Plugins (hermes_cli/plugins.py) — capability extensions with lifecycle hooks + tool registration
Docstring at hermes_cli/plugins.py:1-30 is explicit about discovery order (later overrides earlier on name collision):
- Bundled —
<repo>/plugins/<name>/(memory/ and context_engine/ excluded — separate discovery) - User —
~/.hermes/plugins/<name>/ - Project —
./.hermes/plugins/<name>/(opt-in viaHERMES_ENABLE_PROJECT_PLUGINS) - Pip — packages exposing the
hermes_agent.pluginsentry-point group
Each plugin directory needs a plugin.yaml manifest and an __init__.py with register(ctx). PluginContext (hermes_cli/plugins.py:337) is the facade handed to register(ctx):
- ctx.register_tool(name, toolset, schema, handler, ...) — delegates to tools/registry.py's ToolRegistry (class at line 208), so plugin tools appear alongside built-ins
- ctx.llm — a host-owned agent/plugin_llm.py:PluginLlm facade so a plugin can run completions against the user's own configured model/auth without bringing its own API key (fail-closed by default; opt-in via plugins.entries.<id>.llm.* config)
- ctx.profile_name — active Hermes profile, works uniformly across CLI/gateway/kanban-worker contexts
Plugins subscribe to any of VALID_HOOKS (hermes_cli/plugins.py:135), a fairly complete set: pre_tool_call/post_tool_call, transform_terminal_output/transform_tool_result/transform_llm_output (mutate model output before the user sees it), pre_llm_call/post_llm_call, pre_verify (gate the stop-and-verify loop — a callback can force the turn to keep going), pre_api_request/post_api_request/api_request_error, on_session_start/on_session_end/on_session_finalize/on_session_reset, subagent_start/subagent_stop, pre_gateway_dispatch (can skip/rewrite an inbound gateway message before auth/dispatch), and approval-lifecycle observer hooks.
The existing plugins/ tree (browser, context_engine, cron_providers, dashboard_auth, disk-cleanup, google_meet, image_gen, kanban, memory, model-providers, observability, security-guidance, spotify, teams_pipeline, video_gen, web) is the reference implementation set — read one (e.g. plugins/memory/honcho/) as a template.
This is the primary "add a new capability" seam. Adding a messaging platform, a new memory backend, a new tool, or output post-processing all go through plugins, not core-file edits — gateway/platforms/ADDING_A_PLATFORM.md states this outright: "Create a plugin directory... This requires zero changes to core Hermes code."
7b. Gateway hooks (gateway/hooks.py) — lightweight lifecycle event handlers, gateway-only
A separate, simpler system scoped to the gateway process. Discovered from ~/.hermes/hooks/<name>/ — each directory needs HOOK.yaml (name, description, events: list) and handler.py (async def handle(event_type, context)). Events: gateway:startup, session:start/session:end/session:reset, agent:start/agent:step/agent:end, command:* (wildcard slash-command match). Errors in a hook are caught and logged, never block the pipeline. gateway/builtin_hooks/ is currently an empty extension point (docstring: "no shipped built-in hooks... kept as the extension point for future always-on gateway hooks").
Use plugins (7a) for anything that needs tool registration or a lifecycle hook reachable from the CLI/ACP as well as the gateway. Use gateway hooks (7b) only for gateway-specific, drop-a-file automation that doesn't need to register a tool.
8. Skills — procedural memory, SKILL.md format
Skills are markdown files with YAML frontmatter, loaded on demand (via /, e.g. /dogfood) or auto-surfaced by relevance. Format (see skills/dogfood/SKILL.md):
wzxhzdk:6
klzzwxh:0206 is the shared metadata layer (deliberately import-light — no tool registry, no CLI config — so it can be imported without triggering the heavy tool-registration chain). It defines EXCLUDED_SKILL_DIRS (.git, node_modules, .venv, etc.) and SKILL_SUPPORT_DIRS (references/, templates/, assets/, scripts/ — these are supporting files inside a skill package, not standalone skills, and must be excluded from scans even under an archive).
Skills live in three places, same override-order pattern as plugins:
- skills/ — bundled core (apple, autonomous-ai-agents, computer-use, creative, data-science, dogfood, email, github, index-cache, media, mlops, note-taking, productivity, research, smart-home, social-media, software-development, yuanbao)
- optional-skills/ — bundled but opt-in, larger catalog (blockchain, devops, finance, gaming, health, mcp, migration, payments, security, web-development, …)
- ~/.hermes/skills/ (or profile-scoped) — user + agent-created skills, the ones the curator (§6) manages
agent/skill_bundles.py adds one more layer: a bundle (~/.hermes/skill-bundles/*.yaml) is a named alias that loads several skills at once under one slash command — name, description, skills: [...], optional instruction:. If a bundle and a skill share a slash name, the bundle wins (explicit conflict rule in the module docstring).
Also compatible with the open agentskills.io standard per the README, and skill-authoring rules are HARDLINE-enforced (see agent/learn_prompt.py's embedded _AUTHORING_STANDARDS) so agent-authored skills stay uniform with hand-written ones.
9. Tools and toolsets
tools/registry.py:ToolRegistry (line 208) is the actual tool table; toolsets.py is a naming/grouping layer on top of it — a toolset is a named list of tool-name strings (composable: a toolset can reference other toolsets). _HERMES_CORE_TOOLS (toolsets.py:29) is the shared baseline list used across CLI and every messaging platform: web search/extract, terminal/process, file ops (read/write/patch/search), vision/image-gen, skills tools (skills_list, skill_view, skill_manage), and the full browser-automation surface (browser_navigate, browser_snapshot, browser_click, etc.).
tools/ (90 files) holds one module per tool family — notable ones: tools/approval.py (dangerous-command approval gate, hooked into the plugin approval-lifecycle events from §7a), tools/mcp_tool.py (MCP server integration — bring any MCP server in as tools), tools/delegate_tool.py/agent/async_utils.py (subagent spawning — "delegates and parallelizes" from the README), tools/kanban_tools.py, tools/cronjob_tools.py (backs the cron/ scheduler), tools/memory_tool.py.
New built-in tool → add to tools/, register in tools/registry.py, expose via a toolset in toolsets.py. New plugin-supplied tool → ctx.register_tool(...) from a plugin's register(ctx) (§7a) — no core-file edit needed, and PluginToolOverrideError in hermes_cli/plugins.py is the fail-closed guard against a plugin silently shadowing a built-in tool without operator opt-in (plugins.entries.<plugin_id>.allow_tool_override).
10. Where to modify this to build a custom OS
Ranked by leverage, cheapest/safest first:
SOUL.md(~/.hermes/SOUL.md, default template inhermes_cli/default_soul.py) — swap the agent's persona/identity. Zero code changes, applies uniformly across CLI/gateway/ACP because all three load it viaagent/prompt_builder.py:load_soul_md().- A new plugin (
plugins/<name>/or~/.hermes/plugins/<name>/,plugin.yaml+register(ctx)) — the sanctioned way to add capability: new tools (ctx.register_tool), new lifecycle behavior (VALID_HOOKS), a new messaging platform (ctx.register_platform(), pergateway/platforms/ADDING_A_PLATFORM.md), or a new memory backend (mirrorplugins/memory/honcho/). This is the extension point the maintainers explicitly designed for third parties — "requires zero changes to core Hermes code." - Skills (
skills/,optional-skills/,~/.hermes/skills/) — procedural knowledge/workflows as markdown+frontmatter. Cheapest way to teach the agent a new process without touching Python at all. - Toolsets (
toolsets.py) — change what capability surface is exposed by default per platform/scenario, without writing a new tool. agent/conversation_loop.py— if the turn-taking logic itself needs to change (new retry policy, new compression trigger, new fallback chain), this ~3,900-line function is where a turn actually happens. High blast radius — it's the one function every session on every transport runs through.agent/system_prompt.py+agent/prompt_builder.py— if the shape of the system prompt needs to change (new tier, new context-file type beyond AGENTS.md/CLAUDE.md/.cursorrules, different tier ordering). Respect the stable/context/volatile cache-invariant documented in the module docstring — get this wrong and every provider's prompt cache stops hitting, which is a silent cost/latency regression, not a crash.agent/agent_init.py— ifAIAgentitself needs new constructor-level behavior (new provider, new credential source, new terminal backend). This is the extraction target forAIAgent.__init__; edit here, notrun_agent.py, to keep the file's stated separation of "stable public class" vs. "1,400 lines of setup."- A new front door (fourth transport beyond CLI/gateway/ACP) — construct
AIAgent(**kwargs)directly (seeacp_adapter/session.py:608-656for the minimal pattern) and driveagent.run_conversation(user_message, ...). Everything else — prompt assembly, tools, memory, skills, plugins — comes for free because it's the same class.
What NOT to fork if avoidable: hermes_cli/main.py (163K lines) and gateway/run.py (~16,800 lines per docs/session-lifecycle.md) are enormous CLI-argument-parsing and daemon-orchestration surfaces respectively — almost everything a custom-OS build needs to change lives one layer down, in agent/*, plugins/*, or skills/*, where the actual behavior is defined and the blast radius of a change is contained to one file.
Key files quick-reference
| Concern | File(s) |
|---|---|
| Entry points | pyproject.toml:308-310, hermes_cli/main.py:cmd_chat, gateway/run.py:GatewayRunner, acp_adapter/entry.py / server.py:HermesACPAgent |
| Agent class | run_agent.py:403 (AIAgent), init body in agent/agent_init.py |
| Turn loop | agent/conversation_loop.py (run_conversation) |
| System prompt | agent/system_prompt.py, agent/prompt_builder.py |
| Persona | ~/.hermes/SOUL.md, hermes_cli/default_soul.py |
| Sessions (CLI/ACP) | hermes_state.py (SQLite + FTS5) |
| Sessions (gateway) | gateway/session.py, docs/session-lifecycle.md |
| Memory | agent/memory_manager.py, plugins/memory/*/plugin.yaml |
| Self-improvement | agent/learn_prompt.py (/learn), agent/curator.py (background) |
| Plugins | hermes_cli/plugins.py (VALID_HOOKS, PluginContext), plugins/* |
| Gateway hooks | gateway/hooks.py, ~/.hermes/hooks/<name>/HOOK.yaml |
| Skills | agent/skill_utils.py, agent/skill_bundles.py, skills/, optional-skills/ |
| Tools | tools/registry.py (ToolRegistry), toolsets.py (_HERMES_CORE_TOOLS) |
| Adding a platform | gateway/platforms/ADDING_A_PLATFORM.md |