Files
hermes-agent/AGENTS.md

470 lines
20 KiB
Markdown
Raw Normal View History

# Hermes Agent - Development Guide
Instructions for AI coding assistants and developers working on the hermes-agent codebase.
## Development Environment
```bash
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
docs: comprehensive AGENTS.md audit and corrections Major fixes: - Default model: claude-sonnet-4.6 → claude-opus-4.6 - max_iterations default: 60 → 90 (also fixed in config.py OPTIONAL_ENV_VARS description) - chat() signature: chat(user_message, task_id) → chat(message) - Agent loop: _run_agent_loop() doesn't exist, loop is in run_conversation() - Removed async/await references (agent is entirely synchronous) - KawaiiSpinner location: run_agent.py → agent/display.py - NOUS_API_KEY removed (not used by any tool), replaced with VOICE_TOOLS_OPENAI_KEY - OPENAI_API_KEY for Whisper → VOICE_TOOLS_OPENAI_KEY - check_for_missing_config() → check_config_version() + get_missing_env_vars() - Adding tools: '2 files' → '3 files' (tool + model_tools.py + toolsets.py) - Venv path: venv/ → .venv/ - Trajectory output path: trajectories/*.jsonl → trajectory_samples.jsonl - process_command() location clarified (HermesCLI in cli.py, not commands.py) - REQUIRED_ENV_VARS noted as intentionally empty - _config_version noted as currently at version 5 New content: - Project structure: added 40+ missing files across agent/, hermes_cli/, tools/, gateway/ - Full gateway/ directory listing with all modules and platforms/ - Added honcho_integration/, scripts/, tests/ directories - Added hermes_constants.py, hermes_time.py, trajectory_compressor.py, utils.py - CLI commands table: added 25+ missing commands (model, login, logout, whatsapp, skills subsystem, tools, insights, gateway start/stop/restart/status/uninstall, sessions export/delete/prune/stats, config path/env-path/show) - Gateway slash commands section with all 20+ commands - Platform toolsets: added hermes-cli, hermes-slack, hermes-homeassistant, hermes-gateway - Gateway: added Home Assistant as supported platform
2026-03-08 17:38:05 -07:00
│ ├── auxiliary_client.py # Auxiliary LLM client (vision, summarization)
│ ├── model_metadata.py # Model context lengths, token estimation
feat: overhaul context length detection with models.dev and provider-aware resolution (#2158) Replace the fragile hardcoded context length system with a multi-source resolution chain that correctly identifies context windows per provider. Key changes: - New agent/models_dev.py: Fetches and caches the models.dev registry (3800+ models across 100+ providers with per-provider context windows). In-memory cache (1hr TTL) + disk cache for cold starts. - Rewritten get_model_context_length() resolution chain: 0. Config override (model.context_length) 1. Custom providers per-model context_length 2. Persistent disk cache 3. Endpoint /models (local servers) 4. Anthropic /v1/models API (max_input_tokens, API-key only) 5. OpenRouter live API (existing, unchanged) 6. Nous suffix-match via OpenRouter (dot/dash normalization) 7. models.dev registry lookup (provider-aware) 8. Thin hardcoded defaults (broad family patterns) 9. 128K fallback (was 2M) - Provider-aware context: same model now correctly resolves to different context windows per provider (e.g. claude-opus-4.6: 1M on Anthropic, 128K on GitHub Copilot). Provider name flows through ContextCompressor. - DEFAULT_CONTEXT_LENGTHS shrunk from 80+ entries to ~16 broad patterns. models.dev replaces the per-model hardcoding. - CONTEXT_PROBE_TIERS changed from [2M, 1M, 512K, 200K, 128K, 64K, 32K] to [128K, 64K, 32K, 16K, 8K]. Unknown models no longer start at 2M. - hermes model: prompts for context_length when configuring custom endpoints. Supports shorthand (32k, 128K). Saved to custom_providers per-model config. - custom_providers schema extended with optional models dict for per-model context_length (backward compatible). - Nous Portal: suffix-matches bare IDs (claude-opus-4-6) against OpenRouter's prefixed IDs (anthropic/claude-opus-4.6) with dot/dash normalization. Handles all 15 current Nous models. - Anthropic direct: queries /v1/models for max_input_tokens. Only works with regular API keys (sk-ant-api*), not OAuth tokens. Falls through to models.dev for OAuth users. Tests: 5574 passed (18 new tests for models_dev + updated probe tiers) Docs: Updated configuration.md context length section, AGENTS.md Co-authored-by: Test <test@test.com>
2026-03-20 06:04:33 -07:00
│ ├── models_dev.py # models.dev registry integration (provider-aware context)
│ ├── 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
docs: comprehensive AGENTS.md audit and corrections Major fixes: - Default model: claude-sonnet-4.6 → claude-opus-4.6 - max_iterations default: 60 → 90 (also fixed in config.py OPTIONAL_ENV_VARS description) - chat() signature: chat(user_message, task_id) → chat(message) - Agent loop: _run_agent_loop() doesn't exist, loop is in run_conversation() - Removed async/await references (agent is entirely synchronous) - KawaiiSpinner location: run_agent.py → agent/display.py - NOUS_API_KEY removed (not used by any tool), replaced with VOICE_TOOLS_OPENAI_KEY - OPENAI_API_KEY for Whisper → VOICE_TOOLS_OPENAI_KEY - check_for_missing_config() → check_config_version() + get_missing_env_vars() - Adding tools: '2 files' → '3 files' (tool + model_tools.py + toolsets.py) - Venv path: venv/ → .venv/ - Trajectory output path: trajectories/*.jsonl → trajectory_samples.jsonl - process_command() location clarified (HermesCLI in cli.py, not commands.py) - REQUIRED_ENV_VARS noted as intentionally empty - _config_version noted as currently at version 5 New content: - Project structure: added 40+ missing files across agent/, hermes_cli/, tools/, gateway/ - Full gateway/ directory listing with all modules and platforms/ - Added honcho_integration/, scripts/, tests/ directories - Added hermes_constants.py, hermes_time.py, trajectory_compressor.py, utils.py - CLI commands table: added 25+ missing commands (model, login, logout, whatsapp, skills subsystem, tools, insights, gateway start/stop/restart/status/uninstall, sessions export/delete/prune/stats, config path/env-path/show) - Gateway slash commands section with all 20+ commands - Platform toolsets: added hermes-cli, hermes-slack, hermes-homeassistant, hermes-gateway - Gateway: added Home Assistant as supported platform
2026-03-08 17:38:05 -07:00
│ ├── commands.py # Slash command definitions + SlashCommandCompleter
│ ├── callbacks.py # Terminal callbacks (clarify, sudo, approval)
│ ├── setup.py # Interactive setup wizard
│ ├── skin_engine.py # Skin/theme engine — CLI visual customization
│ ├── skills_config.py # `hermes skills` — enable/disable skills per platform
│ ├── tools_config.py # `hermes tools` — enable/disable tools per platform
│ ├── skills_hub.py # `/skills` slash command (search, browse, install)
│ ├── models.py # Model catalog, provider model lists
│ ├── model_switch.py # Shared /model switch pipeline (CLI + gateway)
│ └── auth.py # Provider credential resolution
├── 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
feat(web): add Parallel as alternative web search/extract backend (#1696) * feat(web): add Parallel as alternative web search/extract backend Adds Parallel (parallel.ai) as a drop-in alternative to Firecrawl for web_search and web_extract tools using the official parallel-web SDK. - Backend selection via WEB_SEARCH_BACKEND env var (auto/parallel/firecrawl) - Auto mode prefers Firecrawl when both keys present; Parallel when sole backend - web_crawl remains Firecrawl-only with clear error when unavailable - Lazy SDK imports, interrupt support, singleton clients - 16 new unit tests for backend selection and client config Co-authored-by: s-jag <s-jag@users.noreply.github.com> * fix: add PARALLEL_API_KEY to config registry and fix web_crawl policy tests Follow-up for Parallel backend integration: - Add PARALLEL_API_KEY to OPTIONAL_ENV_VARS (hermes doctor, env blocklist) - Add to set_config_value api_keys list (hermes config set) - Add to doctor keys display - Fix 2 web_crawl policy tests that didn't set FIRECRAWL_API_KEY (needed now that web_crawl has a Firecrawl availability guard) * refactor: explicit backend selection via hermes tools, not auto-detect Replace the auto-detect backend selection with explicit user choice: - hermes tools saves WEB_SEARCH_BACKEND to .env when user picks a provider - _get_backend() reads the explicit choice first - Fallback only for manual/legacy config (uses whichever key is present) - _is_provider_active() shows [active] for the selected web backend - Updated tests, docs, and .env.example to remove 'auto' mode language * refactor: use config.yaml for web backend, not env var Match the TTS/browser pattern — web.backend is stored in config.yaml (set by hermes tools), not as a WEB_SEARCH_BACKEND env var. - _load_web_config() reads web: section from config.yaml - _get_backend() reads web.backend from config, falls back to key detection - _configure_provider() saves to config dict (saved to config.yaml) - _is_provider_active() reads from config dict - Removed WEB_SEARCH_BACKEND from .env.example, set_config_value, docs - Updated all tests to mock _load_web_config instead of env vars --------- Co-authored-by: s-jag <s-jag@users.noreply.github.com>
2026-03-17 04:02:02 -07:00
│ ├── web_tools.py # Web search/extract (Parallel + Firecrawl)
│ ├── browser_tool.py # Browserbase browser automation
docs: comprehensive AGENTS.md audit and corrections Major fixes: - Default model: claude-sonnet-4.6 → claude-opus-4.6 - max_iterations default: 60 → 90 (also fixed in config.py OPTIONAL_ENV_VARS description) - chat() signature: chat(user_message, task_id) → chat(message) - Agent loop: _run_agent_loop() doesn't exist, loop is in run_conversation() - Removed async/await references (agent is entirely synchronous) - KawaiiSpinner location: run_agent.py → agent/display.py - NOUS_API_KEY removed (not used by any tool), replaced with VOICE_TOOLS_OPENAI_KEY - OPENAI_API_KEY for Whisper → VOICE_TOOLS_OPENAI_KEY - check_for_missing_config() → check_config_version() + get_missing_env_vars() - Adding tools: '2 files' → '3 files' (tool + model_tools.py + toolsets.py) - Venv path: venv/ → .venv/ - Trajectory output path: trajectories/*.jsonl → trajectory_samples.jsonl - process_command() location clarified (HermesCLI in cli.py, not commands.py) - REQUIRED_ENV_VARS noted as intentionally empty - _config_version noted as currently at version 5 New content: - Project structure: added 40+ missing files across agent/, hermes_cli/, tools/, gateway/ - Full gateway/ directory listing with all modules and platforms/ - Added honcho_integration/, scripts/, tests/ directories - Added hermes_constants.py, hermes_time.py, trajectory_compressor.py, utils.py - CLI commands table: added 25+ missing commands (model, login, logout, whatsapp, skills subsystem, tools, insights, gateway start/stop/restart/status/uninstall, sessions export/delete/prune/stats, config path/env-path/show) - Gateway slash commands section with all 20+ commands - Platform toolsets: added hermes-cli, hermes-slack, hermes-homeassistant, hermes-gateway - Gateway: added Home Assistant as supported platform
2026-03-08 17:38:05 -07:00
│ ├── 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
docs: comprehensive AGENTS.md audit and corrections Major fixes: - Default model: claude-sonnet-4.6 → claude-opus-4.6 - max_iterations default: 60 → 90 (also fixed in config.py OPTIONAL_ENV_VARS description) - chat() signature: chat(user_message, task_id) → chat(message) - Agent loop: _run_agent_loop() doesn't exist, loop is in run_conversation() - Removed async/await references (agent is entirely synchronous) - KawaiiSpinner location: run_agent.py → agent/display.py - NOUS_API_KEY removed (not used by any tool), replaced with VOICE_TOOLS_OPENAI_KEY - OPENAI_API_KEY for Whisper → VOICE_TOOLS_OPENAI_KEY - check_for_missing_config() → check_config_version() + get_missing_env_vars() - Adding tools: '2 files' → '3 files' (tool + model_tools.py + toolsets.py) - Venv path: venv/ → .venv/ - Trajectory output path: trajectories/*.jsonl → trajectory_samples.jsonl - process_command() location clarified (HermesCLI in cli.py, not commands.py) - REQUIRED_ENV_VARS noted as intentionally empty - _config_version noted as currently at version 5 New content: - Project structure: added 40+ missing files across agent/, hermes_cli/, tools/, gateway/ - Full gateway/ directory listing with all modules and platforms/ - Added honcho_integration/, scripts/, tests/ directories - Added hermes_constants.py, hermes_time.py, trajectory_compressor.py, utils.py - CLI commands table: added 25+ missing commands (model, login, logout, whatsapp, skills subsystem, tools, insights, gateway start/stop/restart/status/uninstall, sessions export/delete/prune/stats, config path/env-path/show) - Gateway slash commands section with all 20+ commands - Platform toolsets: added hermes-cli, hermes-slack, hermes-homeassistant, hermes-gateway - Gateway: added Home Assistant as supported platform
2026-03-08 17:38:05 -07:00
│ ├── session.py # SessionStore — conversation persistence
│ └── platforms/ # Adapters: telegram, discord, slack, whatsapp, homeassistant, signal
├── acp_adapter/ # ACP server (VS Code / Zed / JetBrains integration)
├── cron/ # Scheduler (jobs.py, scheduler.py)
├── environments/ # RL training environments (Atropos)
├── tests/ # Pytest suite (~3000 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)
```python
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,
docs: comprehensive AGENTS.md audit and corrections Major fixes: - Default model: claude-sonnet-4.6 → claude-opus-4.6 - max_iterations default: 60 → 90 (also fixed in config.py OPTIONAL_ENV_VARS description) - chat() signature: chat(user_message, task_id) → chat(message) - Agent loop: _run_agent_loop() doesn't exist, loop is in run_conversation() - Removed async/await references (agent is entirely synchronous) - KawaiiSpinner location: run_agent.py → agent/display.py - NOUS_API_KEY removed (not used by any tool), replaced with VOICE_TOOLS_OPENAI_KEY - OPENAI_API_KEY for Whisper → VOICE_TOOLS_OPENAI_KEY - check_for_missing_config() → check_config_version() + get_missing_env_vars() - Adding tools: '2 files' → '3 files' (tool + model_tools.py + toolsets.py) - Venv path: venv/ → .venv/ - Trajectory output path: trajectories/*.jsonl → trajectory_samples.jsonl - process_command() location clarified (HermesCLI in cli.py, not commands.py) - REQUIRED_ENV_VARS noted as intentionally empty - _config_version noted as currently at version 5 New content: - Project structure: added 40+ missing files across agent/, hermes_cli/, tools/, gateway/ - Full gateway/ directory listing with all modules and platforms/ - Added honcho_integration/, scripts/, tests/ directories - Added hermes_constants.py, hermes_time.py, trajectory_compressor.py, utils.py - CLI commands table: added 25+ missing commands (model, login, logout, whatsapp, skills subsystem, tools, insights, gateway start/stop/restart/status/uninstall, sessions export/delete/prune/stats, config path/env-path/show) - Gateway slash commands section with all 20+ commands - Platform toolsets: added hermes-cli, hermes-slack, hermes-homeassistant, hermes-gateway - Gateway: added Home Assistant as supported platform
2026-03-08 17:38:05 -07:00
save_trajectories: bool = False,
platform: str = None, # "cli", "telegram", etc.
docs: comprehensive AGENTS.md audit and corrections Major fixes: - Default model: claude-sonnet-4.6 → claude-opus-4.6 - max_iterations default: 60 → 90 (also fixed in config.py OPTIONAL_ENV_VARS description) - chat() signature: chat(user_message, task_id) → chat(message) - Agent loop: _run_agent_loop() doesn't exist, loop is in run_conversation() - Removed async/await references (agent is entirely synchronous) - KawaiiSpinner location: run_agent.py → agent/display.py - NOUS_API_KEY removed (not used by any tool), replaced with VOICE_TOOLS_OPENAI_KEY - OPENAI_API_KEY for Whisper → VOICE_TOOLS_OPENAI_KEY - check_for_missing_config() → check_config_version() + get_missing_env_vars() - Adding tools: '2 files' → '3 files' (tool + model_tools.py + toolsets.py) - Venv path: venv/ → .venv/ - Trajectory output path: trajectories/*.jsonl → trajectory_samples.jsonl - process_command() location clarified (HermesCLI in cli.py, not commands.py) - REQUIRED_ENV_VARS noted as intentionally empty - _config_version noted as currently at version 5 New content: - Project structure: added 40+ missing files across agent/, hermes_cli/, tools/, gateway/ - Full gateway/ directory listing with all modules and platforms/ - Added honcho_integration/, scripts/, tests/ directories - Added hermes_constants.py, hermes_time.py, trajectory_compressor.py, utils.py - CLI commands table: added 25+ missing commands (model, login, logout, whatsapp, skills subsystem, tools, insights, gateway start/stop/restart/status/uninstall, sessions export/delete/prune/stats, config path/env-path/show) - Gateway slash commands section with all 20+ commands - Platform toolsets: added hermes-cli, hermes-slack, hermes-homeassistant, hermes-gateway - Gateway: added Home Assistant as supported platform
2026-03-08 17:38:05 -07:00
session_id: str = None,
skip_context_files: bool = False,
skip_memory: bool = False,
# ... plus provider, api_mode, callbacks, routing params
): ...
docs: comprehensive AGENTS.md audit and corrections Major fixes: - Default model: claude-sonnet-4.6 → claude-opus-4.6 - max_iterations default: 60 → 90 (also fixed in config.py OPTIONAL_ENV_VARS description) - chat() signature: chat(user_message, task_id) → chat(message) - Agent loop: _run_agent_loop() doesn't exist, loop is in run_conversation() - Removed async/await references (agent is entirely synchronous) - KawaiiSpinner location: run_agent.py → agent/display.py - NOUS_API_KEY removed (not used by any tool), replaced with VOICE_TOOLS_OPENAI_KEY - OPENAI_API_KEY for Whisper → VOICE_TOOLS_OPENAI_KEY - check_for_missing_config() → check_config_version() + get_missing_env_vars() - Adding tools: '2 files' → '3 files' (tool + model_tools.py + toolsets.py) - Venv path: venv/ → .venv/ - Trajectory output path: trajectories/*.jsonl → trajectory_samples.jsonl - process_command() location clarified (HermesCLI in cli.py, not commands.py) - REQUIRED_ENV_VARS noted as intentionally empty - _config_version noted as currently at version 5 New content: - Project structure: added 40+ missing files across agent/, hermes_cli/, tools/, gateway/ - Full gateway/ directory listing with all modules and platforms/ - Added honcho_integration/, scripts/, tests/ directories - Added hermes_constants.py, hermes_time.py, trajectory_compressor.py, utils.py - CLI commands table: added 25+ missing commands (model, login, logout, whatsapp, skills subsystem, tools, insights, gateway start/stop/restart/status/uninstall, sessions export/delete/prune/stats, config path/env-path/show) - Gateway slash commands section with all 20+ commands - Platform toolsets: added hermes-cli, hermes-slack, hermes-homeassistant, hermes-gateway - Gateway: added Home Assistant as supported platform
2026-03-08 17:38:05 -07:00
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:
```python
docs: comprehensive AGENTS.md audit and corrections Major fixes: - Default model: claude-sonnet-4.6 → claude-opus-4.6 - max_iterations default: 60 → 90 (also fixed in config.py OPTIONAL_ENV_VARS description) - chat() signature: chat(user_message, task_id) → chat(message) - Agent loop: _run_agent_loop() doesn't exist, loop is in run_conversation() - Removed async/await references (agent is entirely synchronous) - KawaiiSpinner location: run_agent.py → agent/display.py - NOUS_API_KEY removed (not used by any tool), replaced with VOICE_TOOLS_OPENAI_KEY - OPENAI_API_KEY for Whisper → VOICE_TOOLS_OPENAI_KEY - check_for_missing_config() → check_config_version() + get_missing_env_vars() - Adding tools: '2 files' → '3 files' (tool + model_tools.py + toolsets.py) - Venv path: venv/ → .venv/ - Trajectory output path: trajectories/*.jsonl → trajectory_samples.jsonl - process_command() location clarified (HermesCLI in cli.py, not commands.py) - REQUIRED_ENV_VARS noted as intentionally empty - _config_version noted as currently at version 5 New content: - Project structure: added 40+ missing files across agent/, hermes_cli/, tools/, gateway/ - Full gateway/ directory listing with all modules and platforms/ - Added honcho_integration/, scripts/, tests/ directories - Added hermes_constants.py, hermes_time.py, trajectory_compressor.py, utils.py - CLI commands table: added 25+ missing commands (model, login, logout, whatsapp, skills subsystem, tools, insights, gateway start/stop/restart/status/uninstall, sessions export/delete/prune/stats, config path/env-path/show) - Gateway slash commands section with all 20+ commands - Platform toolsets: added hermes-cli, hermes-slack, hermes-homeassistant, hermes-gateway - Gateway: added Home Assistant as supported platform
2026-03-08 17:38:05 -07:00
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:
docs: comprehensive AGENTS.md audit and corrections Major fixes: - Default model: claude-sonnet-4.6 → claude-opus-4.6 - max_iterations default: 60 → 90 (also fixed in config.py OPTIONAL_ENV_VARS description) - chat() signature: chat(user_message, task_id) → chat(message) - Agent loop: _run_agent_loop() doesn't exist, loop is in run_conversation() - Removed async/await references (agent is entirely synchronous) - KawaiiSpinner location: run_agent.py → agent/display.py - NOUS_API_KEY removed (not used by any tool), replaced with VOICE_TOOLS_OPENAI_KEY - OPENAI_API_KEY for Whisper → VOICE_TOOLS_OPENAI_KEY - check_for_missing_config() → check_config_version() + get_missing_env_vars() - Adding tools: '2 files' → '3 files' (tool + model_tools.py + toolsets.py) - Venv path: venv/ → .venv/ - Trajectory output path: trajectories/*.jsonl → trajectory_samples.jsonl - process_command() location clarified (HermesCLI in cli.py, not commands.py) - REQUIRED_ENV_VARS noted as intentionally empty - _config_version noted as currently at version 5 New content: - Project structure: added 40+ missing files across agent/, hermes_cli/, tools/, gateway/ - Full gateway/ directory listing with all modules and platforms/ - Added honcho_integration/, scripts/, tests/ directories - Added hermes_constants.py, hermes_time.py, trajectory_compressor.py, utils.py - CLI commands table: added 25+ missing commands (model, login, logout, whatsapp, skills subsystem, tools, insights, gateway start/stop/restart/status/uninstall, sessions export/delete/prune/stats, config path/env-path/show) - Gateway slash commands section with all 20+ commands - Platform toolsets: added hermes-cli, hermes-slack, hermes-homeassistant, hermes-gateway - Gateway: added Home Assistant as supported platform
2026-03-08 17:38:05 -07:00
result = handle_function_call(tool_call.name, tool_call.args, task_id)
messages.append(tool_result_message(result))
docs: comprehensive AGENTS.md audit and corrections Major fixes: - Default model: claude-sonnet-4.6 → claude-opus-4.6 - max_iterations default: 60 → 90 (also fixed in config.py OPTIONAL_ENV_VARS description) - chat() signature: chat(user_message, task_id) → chat(message) - Agent loop: _run_agent_loop() doesn't exist, loop is in run_conversation() - Removed async/await references (agent is entirely synchronous) - KawaiiSpinner location: run_agent.py → agent/display.py - NOUS_API_KEY removed (not used by any tool), replaced with VOICE_TOOLS_OPENAI_KEY - OPENAI_API_KEY for Whisper → VOICE_TOOLS_OPENAI_KEY - check_for_missing_config() → check_config_version() + get_missing_env_vars() - Adding tools: '2 files' → '3 files' (tool + model_tools.py + toolsets.py) - Venv path: venv/ → .venv/ - Trajectory output path: trajectories/*.jsonl → trajectory_samples.jsonl - process_command() location clarified (HermesCLI in cli.py, not commands.py) - REQUIRED_ENV_VARS noted as intentionally empty - _config_version noted as currently at version 5 New content: - Project structure: added 40+ missing files across agent/, hermes_cli/, tools/, gateway/ - Full gateway/ directory listing with all modules and platforms/ - Added honcho_integration/, scripts/, tests/ directories - Added hermes_constants.py, hermes_time.py, trajectory_compressor.py, utils.py - CLI commands table: added 25+ missing commands (model, login, logout, whatsapp, skills subsystem, tools, insights, gateway start/stop/restart/status/uninstall, sessions export/delete/prune/stats, config path/env-path/show) - Gateway slash commands section with all 20+ commands - Platform toolsets: added hermes-cli, hermes-slack, hermes-homeassistant, hermes-gateway - Gateway: added Home Assistant as supported platform
2026-03-08 17:38:05 -07:00
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 YAML
- **Skin engine** (`hermes_cli/skin_engine.py`) — data-driven CLI theming; initialized from `display.skin` config key at startup; skins customize banner colors, spinner faces/verbs/wings, tool prefix, response box, branding text
refactor: centralize slash command registry (#1603) * refactor: centralize slash command registry Replace 7+ scattered command definition sites with a single CommandDef registry in hermes_cli/commands.py. All downstream consumers now derive from this registry: - CLI process_command() resolves aliases via resolve_command() - Gateway _known_commands uses GATEWAY_KNOWN_COMMANDS frozenset - Gateway help text generated by gateway_help_lines() - Telegram BotCommands generated by telegram_bot_commands() - Slack subcommand map generated by slack_subcommand_map() Adding a command or alias is now a one-line change to COMMAND_REGISTRY instead of touching 6+ files. Bugfixes included: - Telegram now registers /rollback, /background (were missing) - Slack now has /voice, /update, /reload-mcp (were missing) - Gateway duplicate 'reasoning' dispatch (dead code) removed - Gateway help text can no longer drift from CLI help Backwards-compatible: COMMANDS and COMMANDS_BY_CATEGORY dicts are rebuilt from the registry, so existing imports work unchanged. * docs: update developer docs for centralized command registry Update AGENTS.md with full 'Slash Command Registry' and 'Adding a Slash Command' sections covering CommandDef fields, registry helpers, and the one-line alias workflow. Also update: - CONTRIBUTING.md: commands.py description - website/docs/reference/slash-commands.md: reference central registry - docs/plans/centralize-command-registry.md: mark COMPLETED - plans/checkpoint-rollback.md: reference new pattern - hermes-agent-dev skill: architecture table * chore: remove stale plan docs
2026-03-16 23:21:03 -07:00
- `process_command()` is a method on `HermesCLI` — dispatches on canonical command name resolved via `resolve_command()` from the central registry
- Skill slash commands: `agent/skill_commands.py` scans `~/.hermes/skills/`, injects as **user message** (not system prompt) to preserve prompt caching
refactor: centralize slash command registry (#1603) * refactor: centralize slash command registry Replace 7+ scattered command definition sites with a single CommandDef registry in hermes_cli/commands.py. All downstream consumers now derive from this registry: - CLI process_command() resolves aliases via resolve_command() - Gateway _known_commands uses GATEWAY_KNOWN_COMMANDS frozenset - Gateway help text generated by gateway_help_lines() - Telegram BotCommands generated by telegram_bot_commands() - Slack subcommand map generated by slack_subcommand_map() Adding a command or alias is now a one-line change to COMMAND_REGISTRY instead of touching 6+ files. Bugfixes included: - Telegram now registers /rollback, /background (were missing) - Slack now has /voice, /update, /reload-mcp (were missing) - Gateway duplicate 'reasoning' dispatch (dead code) removed - Gateway help text can no longer drift from CLI help Backwards-compatible: COMMANDS and COMMANDS_BY_CATEGORY dicts are rebuilt from the registry, so existing imports work unchanged. * docs: update developer docs for centralized command registry Update AGENTS.md with full 'Slash Command Registry' and 'Adding a Slash Command' sections covering CommandDef fields, registry helpers, and the one-line alias workflow. Also update: - CONTRIBUTING.md: commands.py description - website/docs/reference/slash-commands.md: reference central registry - docs/plans/centralize-command-registry.md: mark COMPLETED - plans/checkpoint-rollback.md: reference new pattern - hermes-agent-dev skill: architecture table * chore: remove stale plan docs
2026-03-16 23:21:03 -07:00
### Slash Command Registry (`hermes_cli/commands.py`)
refactor: centralize slash command registry (#1603) * refactor: centralize slash command registry Replace 7+ scattered command definition sites with a single CommandDef registry in hermes_cli/commands.py. All downstream consumers now derive from this registry: - CLI process_command() resolves aliases via resolve_command() - Gateway _known_commands uses GATEWAY_KNOWN_COMMANDS frozenset - Gateway help text generated by gateway_help_lines() - Telegram BotCommands generated by telegram_bot_commands() - Slack subcommand map generated by slack_subcommand_map() Adding a command or alias is now a one-line change to COMMAND_REGISTRY instead of touching 6+ files. Bugfixes included: - Telegram now registers /rollback, /background (were missing) - Slack now has /voice, /update, /reload-mcp (were missing) - Gateway duplicate 'reasoning' dispatch (dead code) removed - Gateway help text can no longer drift from CLI help Backwards-compatible: COMMANDS and COMMANDS_BY_CATEGORY dicts are rebuilt from the registry, so existing imports work unchanged. * docs: update developer docs for centralized command registry Update AGENTS.md with full 'Slash Command Registry' and 'Adding a Slash Command' sections covering CommandDef fields, registry helpers, and the one-line alias workflow. Also update: - CONTRIBUTING.md: commands.py description - website/docs/reference/slash-commands.md: reference central registry - docs/plans/centralize-command-registry.md: mark COMPLETED - plans/checkpoint-rollback.md: reference new pattern - hermes-agent-dev skill: architecture table * chore: remove stale plan docs
2026-03-16 23:21:03 -07:00
All slash commands are defined in a central `COMMAND_REGISTRY` list of `CommandDef` objects. Every downstream consumer derives from this registry automatically:
- **CLI** — `process_command()` resolves aliases via `resolve_command()`, dispatches on canonical name
- **Gateway** — `GATEWAY_KNOWN_COMMANDS` frozenset for hook emission, `resolve_command()` for dispatch
- **Gateway help** — `gateway_help_lines()` generates `/help` output
- **Telegram** — `telegram_bot_commands()` generates the BotCommand menu
- **Slack** — `slack_subcommand_map()` generates `/hermes` subcommand routing
- **Autocomplete** — `COMMANDS` flat dict feeds `SlashCommandCompleter`
- **CLI help** — `COMMANDS_BY_CATEGORY` dict feeds `show_help()`
### Adding a Slash Command
1. Add a `CommandDef` entry to `COMMAND_REGISTRY` in `hermes_cli/commands.py`:
```python
CommandDef("mycommand", "Description of what it does", "Session",
aliases=("mc",), args_hint="[arg]"),
```
2. Add handler in `HermesCLI.process_command()` in `cli.py`:
```python
elif canonical == "mycommand":
self._handle_mycommand(cmd_original)
```
3. If the command is available in the gateway, add a handler in `gateway/run.py`:
```python
if canonical == "mycommand":
return await self._handle_mycommand(event)
```
4. For persistent settings, use `save_config_value()` in `cli.py`
**CommandDef fields:**
- `name` — canonical name without slash (e.g. `"background"`)
- `description` — human-readable description
- `category` — one of `"Session"`, `"Configuration"`, `"Tools & Skills"`, `"Info"`, `"Exit"`
- `aliases` — tuple of alternative names (e.g. `("bg",)`)
- `args_hint` — argument placeholder shown in help (e.g. `"<prompt>"`, `"[name]"`)
- `cli_only` — only available in the interactive CLI
- `gateway_only` — only available in messaging platforms
feat: config-gated /verbose command for messaging gateway (#3262) * feat: config-gated /verbose command for messaging gateway Add gateway_config_gate field to CommandDef, allowing cli_only commands to be conditionally available in the gateway based on a config value. - CommandDef gains gateway_config_gate: str | None — a config dotpath that, when truthy, overrides cli_only for gateway surfaces - /verbose uses gateway_config_gate='display.tool_progress_command' - Default is off (cli_only behavior preserved) - When enabled, /verbose cycles tool_progress mode (off/new/all/verbose) in the gateway, saving to config.yaml — same cycle as the CLI - Gateway helpers (help, telegram menus, slack mapping) dynamically check config to include/exclude config-gated commands - GATEWAY_KNOWN_COMMANDS always includes config-gated commands so the gateway recognizes them and can respond appropriately - Handles YAML 1.1 bool coercion (bare 'off' parses as False) - 8 new tests for the config gate mechanism + gateway handler * docs: document gateway_config_gate and /verbose messaging support - AGENTS.md: add gateway_config_gate to CommandDef fields - slash-commands.md: note /verbose can be enabled for messaging, update Notes - configuration.md: add tool_progress_command to display section + usage note - cli.md: cross-link to config docs for messaging enablement - messaging/index.md: show tool_progress_command in config snippet - plugins.md: add gateway_config_gate to register_command parameter table
2026-03-26 14:41:04 -07:00
- `gateway_config_gate` — config dotpath (e.g. `"display.tool_progress_command"`); when set on a `cli_only` command, the command becomes available in the gateway if the config value is truthy. `GATEWAY_KNOWN_COMMANDS` always includes config-gated commands so the gateway can dispatch them; help/menus only show them when the gate is open.
refactor: centralize slash command registry (#1603) * refactor: centralize slash command registry Replace 7+ scattered command definition sites with a single CommandDef registry in hermes_cli/commands.py. All downstream consumers now derive from this registry: - CLI process_command() resolves aliases via resolve_command() - Gateway _known_commands uses GATEWAY_KNOWN_COMMANDS frozenset - Gateway help text generated by gateway_help_lines() - Telegram BotCommands generated by telegram_bot_commands() - Slack subcommand map generated by slack_subcommand_map() Adding a command or alias is now a one-line change to COMMAND_REGISTRY instead of touching 6+ files. Bugfixes included: - Telegram now registers /rollback, /background (were missing) - Slack now has /voice, /update, /reload-mcp (were missing) - Gateway duplicate 'reasoning' dispatch (dead code) removed - Gateway help text can no longer drift from CLI help Backwards-compatible: COMMANDS and COMMANDS_BY_CATEGORY dicts are rebuilt from the registry, so existing imports work unchanged. * docs: update developer docs for centralized command registry Update AGENTS.md with full 'Slash Command Registry' and 'Adding a Slash Command' sections covering CommandDef fields, registry helpers, and the one-line alias workflow. Also update: - CONTRIBUTING.md: commands.py description - website/docs/reference/slash-commands.md: reference central registry - docs/plans/centralize-command-registry.md: mark COMPLETED - plans/checkpoint-rollback.md: reference new pattern - hermes-agent-dev skill: architecture table * chore: remove stale plan docs
2026-03-16 23:21:03 -07:00
**Adding an alias** requires only adding it to the `aliases` tuple on the existing `CommandDef`. No other file changes needed — dispatch, help text, Telegram menu, Slack mapping, and autocomplete all update automatically.
Add background process management with process tool, wait, PTY, and stdin support New process registry and tool for managing long-running background processes across all terminal backends (local, Docker, Singularity, Modal, SSH). Process Registry (tools/process_registry.py): - ProcessSession tracking with rolling 200KB output buffer - spawn_local() with optional PTY via ptyprocess for interactive CLIs - spawn_via_env() for non-local backends (runs inside sandbox, never on host) - Background reader threads per process (Popen stdout or PTY) - wait() with timeout clamping, interrupt support, and transparent limit reporting - JSON checkpoint to ~/.hermes/processes.json for gateway crash recovery - Module-level singleton shared across agent loop, gateway, and RL Process Tool (model_tools.py): - 7 actions: list, poll, log, wait, kill, write, submit - Paired with terminal in all toolsets (CLI, messaging, RL) - Timeout clamping with transparent notes in response Terminal Tool Updates (tools/terminal_tool.py): - Replaced nohup background mode with registry spawn (returns session_id) - Added workdir parameter for per-command working directory - Added check_interval parameter for gateway auto-check watchers - Added pty parameter for interactive CLI tools (Codex, Claude Code) - Updated TERMINAL_TOOL_DESCRIPTION with full background workflow docs - Cleanup thread now respects active background processes (won't reap sandbox) Gateway Integration (gateway/run.py, session.py, config.py): - Session reset protection: sessions with active processes exempt from reset - Default idle timeout increased from 2 hours to 24 hours - from_dict fallback aligned to match (was 120, now 1440) - session_key env var propagated to process registry for session mapping - Crash recovery on gateway startup via checkpoint probe - check_interval watcher: asyncio task polls process, delivers updates to platform RL Safety (environments/): - tool_context.py cleanup() kills background processes on episode end - hermes_base_env.py warns when enabled_toolsets is None (loads all tools) - Process tool safe in RL via wait() blocking the agent loop Also: - Added ptyprocess as optional dependency (in pyproject.toml [pty] extra + [all]) - Fixed pre-existing bug: rl_test_inference missing from TOOL_TO_TOOLSET_MAP - Updated AGENTS.md with process management docs and project structure - Updated README.md terminal section with process management overview
2026-02-17 02:51:31 -08:00
---
## Adding New Tools
Requires changes in **3 files**:
**1. Create `tools/your_tool.py`:**
```python
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.
feat: add profiles — run multiple isolated Hermes instances (#3681) Each profile is a fully independent HERMES_HOME with its own config, API keys, memory, sessions, skills, gateway, cron, and state.db. Core module: hermes_cli/profiles.py (~900 lines) - Profile CRUD: create, delete, list, show, rename - Three clone levels: blank, --clone (config), --clone-all (everything) - Export/import: tar.gz archive for backup and migration - Wrapper alias scripts (~/.local/bin/<name>) - Collision detection for alias names - Sticky default via ~/.hermes/active_profile - Skill seeding via subprocess (handles module-level caching) - Auto-stop gateway on delete with disable-before-stop for services - Tab completion generation for bash and zsh CLI integration (hermes_cli/main.py): - _apply_profile_override(): pre-import -p/--profile flag + sticky default - Full 'hermes profile' subcommand: list, use, create, delete, show, alias, rename, export, import - 'hermes completion bash/zsh' command - Multi-profile skill sync in hermes update Display (cli.py, banner.py, gateway/run.py): - CLI prompt: 'coder ❯' when using a non-default profile - Banner shows profile name - Gateway startup log includes profile name Gateway safety: - Token locks: Discord, Slack, WhatsApp, Signal (extends Telegram pattern) - Port conflict detection: API server, webhook adapter Diagnostics (hermes_cli/doctor.py): - Profile health section: lists profiles, checks config, .env, aliases - Orphan alias detection: warns when wrapper points to deleted profile Tests (tests/hermes_cli/test_profiles.py): - 71 automated tests covering: validation, CRUD, clone levels, rename, export/import, active profile, isolation, alias collision, completion - Full suite: 6760 passed, 0 new failures Documentation: - website/docs/user-guide/profiles.md: full user guide (12 sections) - website/docs/reference/profile-commands.md: command reference (12 commands) - website/docs/reference/faq.md: 6 profile FAQ entries - website/sidebars.ts: navigation updated
2026-03-29 10:41:20 -07:00
**Path references in tool schemas**: If the schema description mentions file paths (e.g. default output directories), use `display_hermes_home()` to make them profile-aware. The schema is generated at import time, which is after `_apply_profile_override()` sets `HERMES_HOME`.
**State files**: If a tool stores persistent state (caches, logs, checkpoints), use `get_hermes_home()` for the base directory — never `Path.home() / ".hermes"`. This ensures each profile gets its own state.
**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:
1. Add to `DEFAULT_CONFIG` in `hermes_cli/config.py`
2. Bump `_config_version` (currently 5) to trigger migration for existing users
### .env variables:
1. Add to `OPTIONAL_ENV_VARS` in `hermes_cli/config.py` with metadata:
```python
"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` |
---
## Skin/Theme System
The skin engine (`hermes_cli/skin_engine.py`) provides data-driven CLI visual customization. Skins are **pure data** — no code changes needed to add a new skin.
### Architecture
```
hermes_cli/skin_engine.py # SkinConfig dataclass, built-in skins, YAML loader
~/.hermes/skins/*.yaml # User-installed custom skins (drop-in)
```
- `init_skin_from_config()` — called at CLI startup, reads `display.skin` from config
- `get_active_skin()` — returns cached `SkinConfig` for the current skin
- `set_active_skin(name)` — switches skin at runtime (used by `/skin` command)
- `load_skin(name)` — loads from user skins first, then built-ins, then falls back to default
- Missing skin values inherit from the `default` skin automatically
### What skins customize
| Element | Skin Key | Used By |
|---------|----------|---------|
| Banner panel border | `colors.banner_border` | `banner.py` |
| Banner panel title | `colors.banner_title` | `banner.py` |
| Banner section headers | `colors.banner_accent` | `banner.py` |
| Banner dim text | `colors.banner_dim` | `banner.py` |
| Banner body text | `colors.banner_text` | `banner.py` |
| Response box border | `colors.response_border` | `cli.py` |
| Spinner faces (waiting) | `spinner.waiting_faces` | `display.py` |
| Spinner faces (thinking) | `spinner.thinking_faces` | `display.py` |
| Spinner verbs | `spinner.thinking_verbs` | `display.py` |
| Spinner wings (optional) | `spinner.wings` | `display.py` |
| Tool output prefix | `tool_prefix` | `display.py` |
| Per-tool emojis | `tool_emojis` | `display.py``get_tool_emoji()` |
| Agent name | `branding.agent_name` | `banner.py`, `cli.py` |
| Welcome message | `branding.welcome` | `cli.py` |
| Response box label | `branding.response_label` | `cli.py` |
| Prompt symbol | `branding.prompt_symbol` | `cli.py` |
### Built-in skins
- `default` — Classic Hermes gold/kawaii (the current look)
- `ares` — Crimson/bronze war-god theme with custom spinner wings
- `mono` — Clean grayscale monochrome
- `slate` — Cool blue developer-focused theme
### Adding a built-in skin
Add to `_BUILTIN_SKINS` dict in `hermes_cli/skin_engine.py`:
```python
"mytheme": {
"name": "mytheme",
"description": "Short description",
"colors": { ... },
"spinner": { ... },
"branding": { ... },
"tool_prefix": "┊",
},
```
### User skins (YAML)
Users create `~/.hermes/skins/<name>.yaml`:
```yaml
name: cyberpunk
description: Neon-soaked terminal theme
colors:
banner_border: "#FF00FF"
banner_title: "#00FFFF"
banner_accent: "#FF1493"
spinner:
thinking_verbs: ["jacking in", "decrypting", "uploading"]
wings:
- ["⟨⚡", "⚡⟩"]
branding:
agent_name: "Cyber Agent"
response_label: " ⚡ Cyber "
tool_prefix: "▏"
```
Activate with `/skin cyberpunk` or `display.skin: cyberpunk` in config.yaml.
---
## 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_CWD` env var (default: home directory)
### Background Process Notifications (Gateway)
When `terminal(background=true, check_interval=...)` is used, the gateway runs a watcher that
pushes status updates to the user's chat. Control verbosity with `display.background_process_notifications`
in config.yaml (or `HERMES_BACKGROUND_NOTIFICATIONS` env var):
- `all` — running-output updates + final message (default)
- `result` — only the final completion message
- `error` — only the final message when exit code != 0
- `off` — no watcher messages at all
---
feat: add profiles — run multiple isolated Hermes instances (#3681) Each profile is a fully independent HERMES_HOME with its own config, API keys, memory, sessions, skills, gateway, cron, and state.db. Core module: hermes_cli/profiles.py (~900 lines) - Profile CRUD: create, delete, list, show, rename - Three clone levels: blank, --clone (config), --clone-all (everything) - Export/import: tar.gz archive for backup and migration - Wrapper alias scripts (~/.local/bin/<name>) - Collision detection for alias names - Sticky default via ~/.hermes/active_profile - Skill seeding via subprocess (handles module-level caching) - Auto-stop gateway on delete with disable-before-stop for services - Tab completion generation for bash and zsh CLI integration (hermes_cli/main.py): - _apply_profile_override(): pre-import -p/--profile flag + sticky default - Full 'hermes profile' subcommand: list, use, create, delete, show, alias, rename, export, import - 'hermes completion bash/zsh' command - Multi-profile skill sync in hermes update Display (cli.py, banner.py, gateway/run.py): - CLI prompt: 'coder ❯' when using a non-default profile - Banner shows profile name - Gateway startup log includes profile name Gateway safety: - Token locks: Discord, Slack, WhatsApp, Signal (extends Telegram pattern) - Port conflict detection: API server, webhook adapter Diagnostics (hermes_cli/doctor.py): - Profile health section: lists profiles, checks config, .env, aliases - Orphan alias detection: warns when wrapper points to deleted profile Tests (tests/hermes_cli/test_profiles.py): - 71 automated tests covering: validation, CRUD, clone levels, rename, export/import, active profile, isolation, alias collision, completion - Full suite: 6760 passed, 0 new failures Documentation: - website/docs/user-guide/profiles.md: full user guide (12 sections) - website/docs/reference/profile-commands.md: command reference (12 commands) - website/docs/reference/faq.md: 6 profile FAQ entries - website/sidebars.ts: navigation updated
2026-03-29 10:41:20 -07:00
## Profiles: Multi-Instance Support
Hermes supports **profiles** — multiple fully isolated instances, each with its own
`HERMES_HOME` directory (config, API keys, memory, sessions, skills, gateway, etc.).
The core mechanism: `_apply_profile_override()` in `hermes_cli/main.py` sets
`HERMES_HOME` before any module imports. All 119+ references to `get_hermes_home()`
automatically scope to the active profile.
### Rules for profile-safe code
1. **Use `get_hermes_home()` for all HERMES_HOME paths.** Import from `hermes_constants`.
NEVER hardcode `~/.hermes` or `Path.home() / ".hermes"` in code that reads/writes state.
```python
# GOOD
from hermes_constants import get_hermes_home
config_path = get_hermes_home() / "config.yaml"
# BAD — breaks profiles
config_path = Path.home() / ".hermes" / "config.yaml"
```
2. **Use `display_hermes_home()` for user-facing messages.** Import from `hermes_constants`.
This returns `~/.hermes` for default or `~/.hermes/profiles/<name>` for profiles.
```python
# GOOD
from hermes_constants import display_hermes_home
print(f"Config saved to {display_hermes_home()}/config.yaml")
# BAD — shows wrong path for profiles
print("Config saved to ~/.hermes/config.yaml")
```
3. **Module-level constants are fine** — they cache `get_hermes_home()` at import time,
which is AFTER `_apply_profile_override()` sets the env var. Just use `get_hermes_home()`,
not `Path.home() / ".hermes"`.
4. **Tests that mock `Path.home()` must also set `HERMES_HOME`** — since code now uses
`get_hermes_home()` (reads env var), not `Path.home() / ".hermes"`:
```python
with patch.object(Path, "home", return_value=tmp_path), \
patch.dict(os.environ, {"HERMES_HOME": str(tmp_path / ".hermes")}):
...
```
5. **Gateway platform adapters should use token locks** — if the adapter connects with
a unique credential (bot token, API key), call `acquire_scoped_lock()` from
`gateway.status` in the `connect()`/`start()` method and `release_scoped_lock()` in
`disconnect()`/`stop()`. This prevents two profiles from using the same credential.
See `gateway/platforms/telegram.py` for the canonical pattern.
6. **Profile operations are HOME-anchored, not HERMES_HOME-anchored**`_get_profiles_root()`
returns `Path.home() / ".hermes" / "profiles"`, NOT `get_hermes_home() / "profiles"`.
This is intentional — it lets `hermes -p coder profile list` see all profiles regardless
of which one is active.
2026-03-07 22:14:21 -08:00
## Known Pitfalls
feat: add profiles — run multiple isolated Hermes instances (#3681) Each profile is a fully independent HERMES_HOME with its own config, API keys, memory, sessions, skills, gateway, cron, and state.db. Core module: hermes_cli/profiles.py (~900 lines) - Profile CRUD: create, delete, list, show, rename - Three clone levels: blank, --clone (config), --clone-all (everything) - Export/import: tar.gz archive for backup and migration - Wrapper alias scripts (~/.local/bin/<name>) - Collision detection for alias names - Sticky default via ~/.hermes/active_profile - Skill seeding via subprocess (handles module-level caching) - Auto-stop gateway on delete with disable-before-stop for services - Tab completion generation for bash and zsh CLI integration (hermes_cli/main.py): - _apply_profile_override(): pre-import -p/--profile flag + sticky default - Full 'hermes profile' subcommand: list, use, create, delete, show, alias, rename, export, import - 'hermes completion bash/zsh' command - Multi-profile skill sync in hermes update Display (cli.py, banner.py, gateway/run.py): - CLI prompt: 'coder ❯' when using a non-default profile - Banner shows profile name - Gateway startup log includes profile name Gateway safety: - Token locks: Discord, Slack, WhatsApp, Signal (extends Telegram pattern) - Port conflict detection: API server, webhook adapter Diagnostics (hermes_cli/doctor.py): - Profile health section: lists profiles, checks config, .env, aliases - Orphan alias detection: warns when wrapper points to deleted profile Tests (tests/hermes_cli/test_profiles.py): - 71 automated tests covering: validation, CRUD, clone levels, rename, export/import, active profile, isolation, alias collision, completion - Full suite: 6760 passed, 0 new failures Documentation: - website/docs/user-guide/profiles.md: full user guide (12 sections) - website/docs/reference/profile-commands.md: command reference (12 commands) - website/docs/reference/faq.md: 6 profile FAQ entries - website/sidebars.ts: navigation updated
2026-03-29 10:41:20 -07:00
### DO NOT hardcode `~/.hermes` paths
Use `get_hermes_home()` from `hermes_constants` for code paths. Use `display_hermes_home()`
for user-facing print/log messages. Hardcoding `~/.hermes` breaks profiles — each profile
has its own `HERMES_HOME` directory. This was the source of 5 bugs fixed in PR #3575.
2026-03-07 22:14:21 -08:00
### 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.
2026-03-07 22:14:21 -08:00
### 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}"`.
2026-03-07 22:14:21 -08:00
### `_last_resolved_tool_names` is a process-global in `model_tools.py`
`_run_single_child()` in `delegate_tool.py` saves and restores this global around subagent execution. If you add new code that reads this global, be aware it may be temporarily stale during child agent runs.
2026-03-07 22:14:21 -08:00
### DO NOT hardcode cross-tool references in schema descriptions
Tool schema descriptions must not mention tools from other toolsets by name (e.g., `browser_navigate` saying "prefer web_search"). Those tools may be unavailable (missing API keys, disabled toolset), causing the model to hallucinate calls to non-existent tools. If a cross-reference is needed, add it dynamically in `get_tool_definitions()` in `model_tools.py` — see the `browser_navigate` / `execute_code` post-processing blocks for the pattern.
2026-03-07 22:14:21 -08:00
### 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.
2026-03-07 22:14:21 -08:00
feat: add profiles — run multiple isolated Hermes instances (#3681) Each profile is a fully independent HERMES_HOME with its own config, API keys, memory, sessions, skills, gateway, cron, and state.db. Core module: hermes_cli/profiles.py (~900 lines) - Profile CRUD: create, delete, list, show, rename - Three clone levels: blank, --clone (config), --clone-all (everything) - Export/import: tar.gz archive for backup and migration - Wrapper alias scripts (~/.local/bin/<name>) - Collision detection for alias names - Sticky default via ~/.hermes/active_profile - Skill seeding via subprocess (handles module-level caching) - Auto-stop gateway on delete with disable-before-stop for services - Tab completion generation for bash and zsh CLI integration (hermes_cli/main.py): - _apply_profile_override(): pre-import -p/--profile flag + sticky default - Full 'hermes profile' subcommand: list, use, create, delete, show, alias, rename, export, import - 'hermes completion bash/zsh' command - Multi-profile skill sync in hermes update Display (cli.py, banner.py, gateway/run.py): - CLI prompt: 'coder ❯' when using a non-default profile - Banner shows profile name - Gateway startup log includes profile name Gateway safety: - Token locks: Discord, Slack, WhatsApp, Signal (extends Telegram pattern) - Port conflict detection: API server, webhook adapter Diagnostics (hermes_cli/doctor.py): - Profile health section: lists profiles, checks config, .env, aliases - Orphan alias detection: warns when wrapper points to deleted profile Tests (tests/hermes_cli/test_profiles.py): - 71 automated tests covering: validation, CRUD, clone levels, rename, export/import, active profile, isolation, alias collision, completion - Full suite: 6760 passed, 0 new failures Documentation: - website/docs/user-guide/profiles.md: full user guide (12 sections) - website/docs/reference/profile-commands.md: command reference (12 commands) - website/docs/reference/faq.md: 6 profile FAQ entries - website/sidebars.ts: navigation updated
2026-03-29 10:41:20 -07:00
**Profile tests**: When testing profile features, also mock `Path.home()` so that
`_get_profiles_root()` and `_get_default_hermes_home()` resolve within the temp dir.
Use the pattern from `tests/hermes_cli/test_profiles.py`:
```python
@pytest.fixture
def profile_env(tmp_path, monkeypatch):
home = tmp_path / ".hermes"
home.mkdir()
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setenv("HERMES_HOME", str(home))
return home
```
2026-03-07 22:14:21 -08:00
---
## Testing
```bash
source venv/bin/activate
python -m pytest tests/ -q # Full suite (~3000 tests, ~3 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.