AI-OS (Second-Brain)

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:line deep 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.pycli.pyrun_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.pyGatewayRunner

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.pyacp_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, and parent_session_id chains 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 deterministic session_key. SessionEntry maps that key to the current session_id (format YYYYMMDD_HHMMSS_<8hex>) and is persisted to {sessions_dir}/sessions.json. GatewayRunner caches the live AIAgent per 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 a SKILL.md itself via the skill_manage tool, 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 than interval_hours ago, maybe_run_curator() spawns a forked AIAgent to do the review"). It only ever touches agent-created skills (checked via tools/skill_usage.is_agent_created), never auto-deletes (archive only, recoverable), and pinned skills are exempt from all auto-transitions. State persists in .curator_state under 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):

  1. Bundled<repo>/plugins/<name>/ (memory/ and context_engine/ excluded — separate discovery)
  2. User~/.hermes/plugins/<name>/
  3. Project./.hermes/plugins/<name>/ (opt-in via HERMES_ENABLE_PROJECT_PLUGINS)
  4. Pip — packages exposing the hermes_agent.plugins entry-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:

  1. SOUL.md (~/.hermes/SOUL.md, default template in hermes_cli/default_soul.py) — swap the agent's persona/identity. Zero code changes, applies uniformly across CLI/gateway/ACP because all three load it via agent/prompt_builder.py:load_soul_md().
  2. 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(), per gateway/platforms/ADDING_A_PLATFORM.md), or a new memory backend (mirror plugins/memory/honcho/). This is the extension point the maintainers explicitly designed for third parties — "requires zero changes to core Hermes code."
  3. 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.
  4. Toolsets (toolsets.py) — change what capability surface is exposed by default per platform/scenario, without writing a new tool.
  5. 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.
  6. 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.
  7. agent/agent_init.py — if AIAgent itself needs new constructor-level behavior (new provider, new credential source, new terminal backend). This is the extraction target for AIAgent.__init__; edit here, not run_agent.py, to keep the file's stated separation of "stable public class" vs. "1,400 lines of setup."
  8. A new front door (fourth transport beyond CLI/gateway/ACP) — construct AIAgent(**kwargs) directly (see acp_adapter/session.py:608-656 for the minimal pattern) and drive agent.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