AGENTS.md is read by AI agents in their context window. Every line costs tokens. The previous version had grown to 927 lines with user-facing documentation that duplicates website/docs/: Removed (belongs in website/docs/, not agent context): - Full CLI commands table (50 lines) - Full gateway slash commands list (20 lines) - Messaging gateway setup, config examples, security details - DM pairing system details - Event hooks format and examples - Tool progress notification details - Full environment variables reference - Auxiliary model configuration section (60 lines) - Background process management details - Trajectory format details - Batch processing CLI usage - Skills system directory tree and hub details - Dangerous command approval flow details - Platform toolsets listing Kept (essential for agents modifying code): - Project structure (condensed to key files only) - File dependency chain - AIAgent class signature and loop mechanics - How to add tools (3 files, full pattern) - How to add config (config.yaml + .env patterns) - How to add CLI commands - Config loader table (two separate systems) - Prompt caching policy (critical constraint) - All known pitfalls - Test commands
9.4 KiB
Hermes Agent - Development Guide
Instructions for AI coding assistants and developers working on the hermes-agent codebase.
Development Environment
source .venv/bin/activate # ALWAYS activate before running Python
Project Structure
hermes-agent/
├── run_agent.py # AIAgent class — core conversation loop
├── model_tools.py # Tool orchestration, _discover_tools(), handle_function_call()
├── toolsets.py # Toolset definitions, _HERMES_CORE_TOOLS list
├── cli.py # HermesCLI class — interactive CLI orchestrator
├── hermes_state.py # SessionDB — SQLite session store (FTS5 search)
├── agent/ # Agent internals
│ ├── prompt_builder.py # System prompt assembly
│ ├── context_compressor.py # Auto context compression
│ ├── prompt_caching.py # Anthropic prompt caching
│ ├── auxiliary_client.py # Auxiliary LLM client (vision, summarization)
│ ├── model_metadata.py # Model context lengths, token estimation
│ ├── display.py # KawaiiSpinner, tool preview formatting
│ ├── skill_commands.py # Skill slash commands (shared CLI/gateway)
│ └── trajectory.py # Trajectory saving helpers
├── hermes_cli/ # CLI subcommands and setup
│ ├── main.py # Entry point — all `hermes` subcommands
│ ├── config.py # DEFAULT_CONFIG, OPTIONAL_ENV_VARS, migration
│ ├── commands.py # Slash command definitions + SlashCommandCompleter
│ ├── callbacks.py # Terminal callbacks (clarify, sudo, approval)
│ └── setup.py # Interactive setup wizard
├── tools/ # Tool implementations (one file per tool)
│ ├── registry.py # Central tool registry (schemas, handlers, dispatch)
│ ├── approval.py # Dangerous command detection
│ ├── terminal_tool.py # Terminal orchestration
│ ├── process_registry.py # Background process management
│ ├── file_tools.py # File read/write/search/patch
│ ├── web_tools.py # Firecrawl search/extract
│ ├── browser_tool.py # Browserbase browser automation
│ ├── code_execution_tool.py # execute_code sandbox
│ ├── delegate_tool.py # Subagent delegation
│ ├── mcp_tool.py # MCP client (~1050 lines)
│ └── environments/ # Terminal backends (local, docker, ssh, modal, daytona, singularity)
├── gateway/ # Messaging platform gateway
│ ├── run.py # Main loop, slash commands, message dispatch
│ ├── session.py # SessionStore — conversation persistence
│ └── platforms/ # Adapters: telegram, discord, slack, whatsapp, homeassistant, signal
├── cron/ # Scheduler (jobs.py, scheduler.py)
├── environments/ # RL training environments (Atropos)
├── tests/ # Pytest suite (~2500+ tests)
└── batch_runner.py # Parallel batch processing
User config: ~/.hermes/config.yaml (settings), ~/.hermes/.env (API keys)
File Dependency Chain
tools/registry.py (no deps — imported by all tool files)
↑
tools/*.py (each calls registry.register() at import time)
↑
model_tools.py (imports tools/registry + triggers tool discovery)
↑
run_agent.py, cli.py, batch_runner.py, environments/
AIAgent Class (run_agent.py)
class AIAgent:
def __init__(self,
model: str = "anthropic/claude-opus-4.6",
max_iterations: int = 90,
enabled_toolsets: list = None,
disabled_toolsets: list = None,
quiet_mode: bool = False,
save_trajectories: bool = False,
platform: str = None, # "cli", "telegram", etc.
session_id: str = None,
skip_context_files: bool = False,
skip_memory: bool = False,
# ... plus provider, api_mode, callbacks, routing params
): ...
def chat(self, message: str) -> str:
"""Simple interface — returns final response string."""
def run_conversation(self, user_message: str, system_message: str = None,
conversation_history: list = None, task_id: str = None) -> dict:
"""Full interface — returns dict with final_response + messages."""
Agent Loop
The core loop is inside run_conversation() — entirely synchronous:
while api_call_count < self.max_iterations and self.iteration_budget.remaining > 0:
response = client.chat.completions.create(model=model, messages=messages, tools=tool_schemas)
if response.tool_calls:
for tool_call in response.tool_calls:
result = handle_function_call(tool_call.name, tool_call.args, task_id)
messages.append(tool_result_message(result))
api_call_count += 1
else:
return response.content
Messages follow OpenAI format: {"role": "system/user/assistant/tool", ...}. Reasoning content is stored in assistant_msg["reasoning"].
CLI Architecture (cli.py)
- Rich for banner/panels, prompt_toolkit for input with autocomplete
- KawaiiSpinner (
agent/display.py) — animated faces during API calls,┊activity feed for tool results load_cli_config()in cli.py merges hardcoded defaults + user config YAMLprocess_command()is a method onHermesCLI(not in commands.py)- Skill slash commands:
agent/skill_commands.pyscans~/.hermes/skills/, injects as user message (not system prompt) to preserve prompt caching
Adding CLI Commands
- Add to
COMMANDSdict inhermes_cli/commands.py - Add handler in
HermesCLI.process_command()incli.py - For persistent settings, use
save_config_value()incli.py
Adding New Tools
Requires changes in 3 files:
1. Create tools/your_tool.py:
import json, os
from tools.registry import registry
def check_requirements() -> bool:
return bool(os.getenv("EXAMPLE_API_KEY"))
def example_tool(param: str, task_id: str = None) -> str:
return json.dumps({"success": True, "data": "..."})
registry.register(
name="example_tool",
toolset="example",
schema={"name": "example_tool", "description": "...", "parameters": {...}},
handler=lambda args, **kw: example_tool(param=args.get("param", ""), task_id=kw.get("task_id")),
check_fn=check_requirements,
requires_env=["EXAMPLE_API_KEY"],
)
2. Add import in model_tools.py _discover_tools() list.
3. Add to toolsets.py — either _HERMES_CORE_TOOLS (all platforms) or a new toolset.
The registry handles schema collection, dispatch, availability checking, and error wrapping. All handlers MUST return a JSON string.
Agent-level tools (todo, memory): intercepted by run_agent.py before handle_function_call(). See todo_tool.py for the pattern.
Adding Configuration
config.yaml options:
- Add to
DEFAULT_CONFIGinhermes_cli/config.py - Bump
_config_version(currently 5) to trigger migration for existing users
.env variables:
- Add to
OPTIONAL_ENV_VARSinhermes_cli/config.pywith metadata:
"NEW_API_KEY": {
"description": "What it's for",
"prompt": "Display name",
"url": "https://...",
"password": True,
"category": "tool", # provider, tool, messaging, setting
},
Config loaders (two separate systems):
| Loader | Used by | Location |
|---|---|---|
load_cli_config() |
CLI mode | cli.py |
load_config() |
hermes tools, hermes setup |
hermes_cli/config.py |
| Direct YAML load | Gateway | gateway/run.py |
Important Policies
Prompt Caching Must Not Break
Hermes-Agent ensures caching remains valid throughout a conversation. Do NOT implement changes that would:
- Alter past context mid-conversation
- Change toolsets mid-conversation
- Reload memories or rebuild system prompts mid-conversation
Cache-breaking forces dramatically higher costs. The ONLY time we alter context is during context compression.
Working Directory Behavior
- CLI: Uses current directory (
.→os.getcwd()) - Messaging: Uses
MESSAGING_CWDenv var (default: home directory)
Known Pitfalls
DO NOT use simple_term_menu for interactive menus
Rendering bugs in tmux/iTerm2 — ghosting on scroll. Use curses (stdlib) instead. See hermes_cli/tools_config.py for the pattern.
DO NOT use \033[K (ANSI erase-to-EOL) in spinner/display code
Leaks as literal ?[K text under prompt_toolkit's patch_stdout. Use space-padding: f"\r{line}{' ' * pad}".
_last_resolved_tool_names is a process-global in model_tools.py
When subagents overwrite this global, execute_code calls after delegation may fail with missing tool imports. Known bug.
Tests must not write to ~/.hermes/
The _isolate_hermes_home autouse fixture in tests/conftest.py redirects HERMES_HOME to a temp dir. Never hardcode ~/.hermes/ paths in tests.
Testing
source .venv/bin/activate
python -m pytest tests/ -q # Full suite (~2500 tests, ~2 min)
python -m pytest tests/test_model_tools.py -q # Toolset resolution
python -m pytest tests/test_cli_init.py -q # CLI config loading
python -m pytest tests/gateway/ -q # Gateway tests
python -m pytest tests/tools/ -q # Tool-level tests
Always run the full suite before pushing changes.