Hermes Agent
Data Model — v0.14.0 — Nous Research
The data structures at the heart of Hermes Agent — the database schema that persists sessions and messages, the message format that flows between the agent and LLM providers, the tool registry that powers 40+ capabilities, the skill file format, the layered configuration system, and the pluggable memory provider interface.
Core Entity Map
Where each entity lives. Sessions and messages persist in SQLite, tools and config rebuild from code and YAML at startup, skills and plugins live on the filesystem, and credentials rotate through a managed JSON store.
Entity Storage Map · Ctrl/Cmd + wheel to zoom · Drag to pan · Double-click to fit
SQLite Schema
All state lives in ~/.hermes/state.db — a SQLite database with WAL mode for
concurrent reads. Schema version 11. FTS5 for full-text search across message content,
tool names, and tool-call arguments, with a trigram index for CJK/substring queries.
BEGIN IMMEDIATE transactions with jitter retry
(15 retries, 20–150ms random sleep). Passive WAL checkpoint every 50 writes.
PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;
■ sessions v1 + v3 + v4 + v5
| Column | Type | Notes |
|---|---|---|
| id | TEXT | PK UUID |
| source | TEXT NOT NULL | IDX "cli", "telegram", "discord", etc. |
| user_id | TEXT | Platform user identifier |
| model | TEXT | LLM model slug |
| model_config | TEXT | JSON-encoded dict |
| system_prompt | TEXT | Frozen at session start for cache |
| parent_session_id | TEXT | FK IDX Compression chain link |
| started_at | REAL NOT NULL | IDX Epoch timestamp, DESC |
| ended_at | REAL | NULL while active |
| end_reason | TEXT | "user", "max_iterations", etc. |
| message_count | INTEGER | Default 0 |
| tool_call_count | INTEGER | Default 0 |
| input_tokens | INTEGER | Default 0 |
| output_tokens | INTEGER | Default 0 |
| cache_read_tokens | INTEGER | v5 Anthropic cache hits |
| cache_write_tokens | INTEGER | v5 Anthropic cache writes |
| reasoning_tokens | INTEGER | v5 Extended thinking tokens |
| billing_provider | TEXT | v5 |
| billing_base_url | TEXT | v5 |
| billing_mode | TEXT | v5 |
| estimated_cost_usd | REAL | v5 |
| actual_cost_usd | REAL | v5 |
| cost_status | TEXT | v5 |
| cost_source | TEXT | v5 |
| pricing_version | TEXT | v5 |
| title | TEXT | v3 UNIQUE partial (WHERE NOT NULL) |
| api_call_count | INTEGER | Default 0, tool-calling API rounds |
■ messages v1 + v2 + v6
| Column | Type | Notes |
|---|---|---|
| id | INTEGER | PK AUTOINCREMENT |
| session_id | TEXT NOT NULL | FK IDX Composite with timestamp |
| role | TEXT NOT NULL | system | user | assistant | tool |
| content | TEXT | Message text (also indexed in FTS5) |
| tool_call_id | TEXT | Links tool result to its call |
| tool_calls | TEXT | JSON-encoded array of tool calls |
| tool_name | TEXT | Which tool produced this result |
| timestamp | REAL NOT NULL | Epoch timestamp |
| token_count | INTEGER | Estimated tokens |
| finish_reason | TEXT | v2 stop | length | tool_calls |
| reasoning | TEXT | v6 Extended thinking content |
| reasoning_content | TEXT | Provider-specific reasoning text |
| reasoning_details | TEXT | v6 JSON-encoded details |
| codex_reasoning_items | TEXT | v6 JSON-encoded Codex items |
| codex_message_items | TEXT | JSON-encoded Codex message items |
■ messages_fts FTS5 Virtual Table
Full-text search index over content || tool_name || tool_calls.
Inline-mode FTS5 (since v11); triggers keep the index synced with the
messages table.
CREATE VIRTUAL TABLE messages_fts USING fts5(content);
-- Auto-sync triggers (v11 — indexes content + tool_name + tool_calls)
AFTER INSERT ON messages →
INSERT INTO messages_fts(rowid, content) VALUES (
new.id,
COALESCE(new.content, '') || ' ' ||
COALESCE(new.tool_name, '') || ' ' ||
COALESCE(new.tool_calls, '')
);
AFTER DELETE ON messages →
DELETE FROM messages_fts WHERE rowid = old.id;
AFTER UPDATE ON messages →
DELETE old entry, INSERT new entry (same concat as INSERT).
Database ER Diagram · Ctrl/Cmd + wheel to zoom · Drag to pan · Double-click to fit
Migration History
| Version | Change |
|---|---|
| v1 | Initial schema: sessions, messages, messages_fts, schema_version |
| v2 | Added finish_reason to messages |
| v3 | Added title to sessions |
| v4 | Added unique partial index on sessions.title (WHERE NOT NULL) |
| v5 | Added 11 billing/cost/cache columns to sessions |
| v6 | Added reasoning, reasoning_details, codex_reasoning_items to messages |
| v7–v9 | Declarative column reconciliation — new columns added via schema diff, no explicit migrations |
| v10 | Added messages_fts_trigram (trigram FTS5 for CJK/substring search) with row backfill |
| v11 | FTS5 re-indexed to cover content || tool_name || tool_calls (inline-mode triggers replace external-content); enables session_search to find tool names and tool-call arguments, not just message content |
Message Format
Messages follow the OpenAI chat completions format. This is the lingua franca — regardless of whether the backend is Anthropic, OpenAI, or a custom endpoint, messages are normalized to this shape.
User Message
{
"role": "user",
"content": "Search for recent papers on GRPO"
}
Assistant + Tool Calls
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "web_search",
"arguments": "{\"query\": \"GRPO ...\"}"
}
}
]
}
Tool Result
{
"role": "tool",
"tool_call_id": "call_abc123",
"tool_name": "web_search",
"content": "{\"data\": {\"web\": [...]}}"
}
System Message
{
"role": "system",
"content": "You are a CLI AI Agent..."
}
// Frozen at session start.
// Never modified mid-conversation
// (preserves Anthropic prompt cache).
Message Flow Through the Agent Loop · Ctrl/Cmd + wheel to zoom · Drag to pan · Double-click to fit
reasoning field stores chain-of-thought. It is persisted to SQLite but
not sent back to the LLM in subsequent turns — it's for display and debugging only.
Tool Registry & Toolsets
Tools self-register at import time into a singleton registry. Toolsets group related tools and compose into platform-level bundles. See C4 Architecture § Tools for the high-level view.
Tool Registry Class Diagram · Ctrl/Cmd + wheel to zoom · Drag to pan · Double-click to fit
OpenAI Function Schema Format
Every tool registers a schema dict following the OpenAI function calling format.
When served to the LLM via get_definitions(), each is wrapped as
{"type": "function", "function": {schema}}.
{
"name": "read_file",
"description": "Read a text file with line numbers...",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the file to read"
},
"offset": {
"type": "integer",
"default": 1,
"minimum": 1
},
"limit": {
"type": "integer",
"default": 500,
"maximum": 2000
}
},
"required": ["path"]
}
}
Toolset Composition
Toolsets are either leaf (directly list tools) or composite (include other toolsets). Platform toolsets compose everything needed for a deployment mode.
safe = moa + web + vision + image_gen
_invoke_tool() inside run_agent.py — they never
hit the registry. Everything else goes through handle_function_call() →
registry.dispatch(). See
Sequences § Tool Execution.
Skill File Structure
Skills are Hermes's procedural memory — reusable markdown instructions with optional supporting files. They are injected as user messages (not system prompt) to preserve prompt cache efficiency.
Directory Layout
- skills/
- <category>/
- <skill-name>/
- SKILL.md — Required. YAML frontmatter + markdown body
- scripts/ — Helper scripts (Python, Bash, etc.)
- references/ — Reference documents
- templates/ — File templates
- assets/ — Static assets
- <skill-name>/
- <category>/
Categories: creative, devops, github,
mlops, productivity, research,
software-development, social-media, etc.
YAML Frontmatter Schema
---
name: visual-explainer # Required: kebab-case slug
description: Generate rich HTML docs... # Required: when to use
version: 1.0.0 # Required: semver
author: Hermes Agent # Required
license: MIT # Required
dependencies: [] # Optional: pip/system deps
prerequisites: # Optional
env_vars: [LINEAR_API_KEY] # Required env vars
commands: [curl] # Required CLI tools
metadata:
hermes:
tags: [architecture, diagrams] # Searchable tags
related_skills: [excalidraw] # Cross-references
---
# Skill Title
## When to Use
...
## Workflow
...
## Pitfalls
...
Skill Loading and Injection · Ctrl/Cmd + wheel to zoom · Drag to pan · Double-click to fit
Config Hierarchy
Configuration resolves through four layers. Lower layers provide defaults; higher layers override.
${VAR} references in YAML values are expanded from environment variables.
${VAR} expansion in config values. Also AUXILIARY_*_MODEL,
AUXILIARY_*_PROVIDER for per-task overrides.~/.hermes/config.yaml — deep-merged over defaults.
Persisted by save_config_value().hermes_cli/config.py. 40+ top-level keys.
Lowest priority. Always present.Top-Level Config Keys
providers
fallback_providers
toolsets
agent
delegation
browser
compression
auxiliary
tts
stt
voice
human_delay
skills
checkpoints
privacy
security
approvals
cron
gateway
Auxiliary Task Configuration
Side tasks can each be routed to a different LLM provider. Every task has the same
schema: provider, model, base_url,
api_key, timeout, extra_body.
| Task | Default Timeout | Purpose |
|---|---|---|
| vision | 120s | Image analysis (vision_analyze, browser_vision) |
| web_extract | 360s (6min) | Web page summarization |
| compression | 120s | Context compression summaries |
| skills_hub | 30s | Skill search and matching |
| approval | 30s | Smart approval risk assessment |
| mcp | 30s | MCP server interactions |
| title_generation | 30s | Session title generation |
| triage_specifier | 120s | Kanban triage → spec expansion |
| kanban_decomposer | 180s | Decompose triage task into child-task graph |
| profile_describer | 60s | Auto-generate profile descriptions |
| curator | 600s | Skill-usage curator review pass |
Config Resolution Flow · Ctrl/Cmd + wheel to zoom · Drag to pan · Double-click to fit
Plugin System
Hermes uses a plugin architecture for extensibility. Five plugin categories exist: memory providers,
context engines, general-purpose plugins, dashboard plugins, and platform plugins. The
PluginManager discovers and loads plugins from four sources
in precedence order.
Plugin Architecture · Ctrl/Cmd + wheel to zoom · Drag to pan · Double-click to fit
plugin.yaml Schema
| Field | Type | Description |
|---|---|---|
| name | str | Unique plugin identifier |
| version | str | Semver version |
| description | str | Human-readable description |
| author | str | Plugin author |
| requires_env | list | Required env vars (strings or dicts with details) |
| provides_tools | list | Tool names this plugin will register |
| provides_hooks | list | Lifecycle hooks this plugin will register |
| kind | str | Plugin category (standalone, backend, exclusive, platform) |
Plugin Categories
| Category | Constraint |
|---|---|
| Memory Provider | At most 1 active external provider |
| Context Engine | At most 1 replaces built-in compressor |
| Model Provider | Inference backend (kind: model-provider); registers default/aux model + API mode; lives in plugins/model-providers/ |
| Web Search Provider | Search/extract backend (kind: backend, provides_web_providers); lives in plugins/web/<name>/ (brave_free, ddgs, exa, firecrawl, parallel, searxng, tavily, xai) |
| Browser Provider | Cloud browser backend (kind: backend, provides_browser_providers); lives in plugins/browser/<name>/ (browserbase, browser_use, firecrawl) |
| General Plugin | Opt-in via config.yaml enable list |
| Dashboard Plugin | JS bundle + manifest.json for web UI tabs |
| Platform Plugin | Registers adapter via PlatformRegistry; lives in plugins/platforms/ |
14 Lifecycle Hooks
Plugins register callbacks for any combination of these events via
ctx.register_hook(event, callback).
Memory Provider Interface
Memory is pluggable. The built-in provider (file-based, always active) handles memory
and user targets. At most one external provider (Honcho, etc.) can be added.
The MemoryManager fans out calls to all active providers.
See C4 Architecture § State
for the provider list and runtime behaviour.
Memory Provider Class Hierarchy · Ctrl/Cmd + wheel to zoom · Drag to pan · Double-click to fit
Lifecycle Hooks
| Hook | When |
|---|---|
| on_turn_start | Before each LLM call |
| on_pre_compress | Before context compression drops messages |
| on_memory_write | When builtin memory is modified |
| on_delegation | After a subagent completes |
| on_session_end | Session teardown |
initialize() kwargs
| Param | Type |
|---|---|
| session_id | str |
| hermes_home | str (auto-injected) |
| platform | str |
| user_id | str |
| agent_context | str |
| agent_identity | str |
| agent_workspace | str |
| parent_session_id | str |