Skip to main content

Project structure

User config: ~/.hermes/config.yaml (settings), ~/.hermes/.env (API keys)

File dependency chain

The import chain is strictly one-directional. tools/registry.py has no upstream dependencies and is safe to import from any tool file without risk of circular imports.

AIAgent class

Defined in run_agent.py. All agent sessions go through this class, whether invoked from the CLI, the messaging gateway, batch processing, or RL environments.

Constructor parameters

chat() method

The simple interface. Takes a single message string, runs the full agent loop, and returns the final response string. Suitable for programmatic use where you don’t need conversation history.

run_conversation() method

The full interface. Returns a dict with final_response (string) and messages (full conversation history). Accepts optional system_message to override the built-in system prompt, conversation_history for multi-turn sessions, and task_id for terminal/browser session isolation.

Agent loop

The core loop lives in run_conversation() and is entirely synchronous. Async tool handlers are bridged internally via _run_async() in model_tools.py.
Iteration budget: A IterationBudget object is shared between the parent agent and all subagents spawned via delegate_task. This ensures the total number of LLM calls across the entire tree stays within max_iterations. execute_code turns are refunded so they don’t consume budget. Budget pressure: As the agent approaches max_iterations, pressure warnings are injected into tool result JSON (not as separate messages) to nudge the model toward wrapping up.

Message format

Messages follow the OpenAI-compatible role/content format:
Reasoning/thinking content is stored in assistant_msg["reasoning"] and stripped from the user-facing response.

CLI architecture

The CLI entry point is HermesCLI in cli.py. It composes several libraries:
  • Rich — banner panels, formatted output
  • prompt_toolkit — fixed input area, slash command autocomplete, history navigation
  • KawaiiSpinner (agent/display.py) — animated faces during API calls, activity feed for tool results

Config loading

There are two separate config-loading systems that serve different consumers: load_cli_config() merges hardcoded defaults with the user’s ~/.hermes/config.yaml. Do not mix these loaders — they serve different code paths.

Slash command dispatch

process_command() is a method on HermesCLI. It resolves incoming text against the central COMMAND_REGISTRY via resolve_command(), which handles aliases, and then dispatches on the canonical command name. Skill slash commands are handled separately by agent/skill_commands.py. It scans ~/.hermes/skills/ and injects skill invocations as user messages (not system prompt additions) to preserve prompt caching.

Gateway architecture

The messaging gateway (gateway/run.py) runs a GatewayRunner that manages:
  • Platform adapters — one per messaging platform (Telegram, Discord, Slack, WhatsApp, Signal, Email, Home Assistant), each translating platform events into the shared message model
  • Session store (gateway/session.py) — SessionStore for per-user conversation persistence, context prompts, and reset policies
  • HooksGATEWAY_KNOWN_COMMANDS frozenset triggers hook emission on recognized slash commands
The gateway loads config via a direct YAML read (not load_cli_config()). Gateway sessions are isolated per user and per platform.

Prompt caching

Hermes automatically enables Anthropic prompt caching for Claude models on OpenRouter and for native Anthropic API calls. Caching reduces input token costs by ~75% on multi-turn conversations by caching the stable conversation prefix.
Do not break prompt caching. Any change that alters past context, swaps toolsets, or rebuilds the system prompt mid-conversation invalidates the cache and dramatically increases costs. The ONLY legitimate time to alter context mid-conversation is during context compression.Do NOT:
  • Alter past messages mid-conversation
  • Change toolsets mid-conversation
  • Reload memories or rebuild system prompts mid-conversation
Skill content is injected as user messages (not system prompt) specifically to keep the cached system prefix stable.

Context compression

Context compression is handled by ContextCompressor in agent/context_compressor.py. It activates automatically when the conversation approaches the model’s context limit (default threshold: 50%). What it does:
  1. Summarizes older messages in the conversation history using an auxiliary LLM call
  2. Replaces the summarized messages with a compact summary message
  3. Preserves the most recent messages (configurable protect_last_n)
  4. Preserves the first few messages (configurable protect_first_n)
Compression is the one legitimate exception to the no-mid-conversation-context-change rule — it is the only operation that may alter the cached prefix. Configuration via config.yaml: