docs: accuracy sweep + coverage for 2 months of shipped features

Accuracy pass (all 373 pages audited against code, 13 parallel audits):
- configuration.md: 12 stale defaults/keys (file-sync rewrite, clarify
  timeout, streaming knobs, iteration budget, TTS/STT enums)
- reference/: commands/env-vars/toolsets/tools synced with
  COMMAND_REGISTRY, argparse tree, OPTIONAL_ENV_VARS, TOOLSETS
  (28 env vars added, 3 phantom removed, mcp__ naming, webhook
  platform restricted toolset)
- features/, messaging/, developer-guide/, guides/: ~60 factual fixes
  (web_extract truncation, dashboard auth fail-closed, delegation
  blocked tools, adapter signatures, session schema v23, phantom
  Matrix env vars, hermes setup tts, auth spotify, webhook --skills)
- zh-Hans: explicit heading IDs fix 2 broken WSL2 anchors

New coverage for features shipped in the last 2 months (verified
against code before writing):
- compression.in_place, verify-on-stop (+v31/v32 migration reality),
  ${env:VAR} SecretRef, display.timestamp_format, session:compress
  hook + thread_id/chat_type fields
- /journey learning timeline, per-channel model/system-prompt
  overrides, /sessions search, clarify multi-select, -z --usage-file,
  uninstall --dry-run, config get/unset
- MCP elicitation, extra_headers, discover_models, api-server run cap,
  Bedrock cachePoint, Discord reasoning_style, Google Chat clarify
  cards, vibe reactions, resume cwd restore, Yuanbao forwarded
  messages, api_content sidecar, roaming pet, tool_progress log mode,
  WhatsApp polls/locations, kanban per-task model + lifecycle hooks
This commit is contained in:
Teknium 2026-07-29 00:55:15 -07:00
parent 068d7812a4
commit 43874d1a96
73 changed files with 656 additions and 292 deletions

View file

@ -77,7 +77,7 @@ class MyPlatformAdapter(BasePlatformAdapter):
extra = config.extra or {}
self.token = os.getenv("MY_PLATFORM_TOKEN") or extra.get("token", "")
async def connect(self) -> bool:
async def connect(self, *, is_reconnect: bool = False) -> bool:
# Connect to the platform API, start listeners
self._mark_connected()
return True
@ -469,7 +469,7 @@ This checklist is for adding a platform directly to the Hermes core codebase —
Add your platform to the `Platform` enum in `gateway/config.py`:
```python
class Platform(str, Enum):
class Platform(Enum):
# ... existing platforms ...
NEWPLAT = "newplat"
```
@ -495,7 +495,7 @@ class NewPlatAdapter(BasePlatformAdapter):
extra = config.extra or {}
self._api_key = extra.get("api_key") or os.getenv("NEWPLAT_API_KEY", "")
async def connect(self) -> bool:
async def connect(self, *, is_reconnect: bool = False) -> bool:
# Set up connection, start polling/webhook
self._mark_connected()
return True
@ -541,7 +541,7 @@ Three touchpoints:
### 4. Gateway Runner (`gateway/run.py`)
Five touchpoints:
Six touchpoints:
1. **`_create_adapter()`** — Add an `elif platform == Platform.NEWPLAT:` branch
2. **`_is_user_authorized()` allowed_users map** — `Platform.NEWPLAT: "NEWPLAT_ALLOWED_USERS"`
@ -576,10 +576,10 @@ Five touchpoints:
### 9. Optional: Platform Hints
**`agent/prompt_builder.py`** — If your platform has specific rendering limitations (no markdown, message length limits, etc.), add an entry to the `_PLATFORM_HINTS` dict. This injects platform-specific guidance into the system prompt:
**`agent/prompt_builder.py`** — If your platform has specific rendering limitations (no markdown, message length limits, etc.), add an entry to the `PLATFORM_HINTS` dict. This injects platform-specific guidance into the system prompt:
```python
_PLATFORM_HINTS = {
PLATFORM_HINTS = {
# ...
"newplat": (
"You are chatting via NewPlat. It supports markdown formatting "
@ -672,8 +672,9 @@ If the adapter holds a persistent connection with a unique credential, add a sco
```python
from gateway.status import acquire_scoped_lock, release_scoped_lock
async def connect(self):
if not acquire_scoped_lock("newplat", self._token):
async def connect(self, *, is_reconnect: bool = False):
acquired, _existing = acquire_scoped_lock("newplat", self._token)
if not acquired:
logger.error("Token already in use by another profile")
return False
# ... connect
@ -688,5 +689,5 @@ async def disconnect(self):
|---------|---------|------------|-------------------|
| `bluebubbles.py` | REST + webhook | Medium | Simple REST API integration |
| `weixin.py` | Long-poll + CDN | High | Media handling, encryption |
| `wecom_callback.py` | Callback/webhook | Medium | HTTP server, AES crypto, multi-app |
| `plugins/platforms/wecom/callback_adapter.py` | Callback/webhook | Medium | HTTP server, AES crypto, multi-app |
| `plugins/platforms/irc/adapter.py` | Long-poll + IRC protocol | High | Full-featured plugin adapter with scoped token lock |

View file

@ -117,11 +117,13 @@ hermes-agent/
│ ├── mirror.py # Cross-session message mirroring
│ ├── status.py # Token locks, profile-scoped process tracking
│ ├── builtin_hooks/ # Extension point for always-registered hooks (none shipped)
│ └── platforms/ # 20 adapters: telegram, discord, slack, whatsapp,
│ # signal, matrix, mattermost, email, sms,
│ # dingtalk, feishu, wecom, wecom_callback, weixin,
│ # bluebubbles, qqbot, homeassistant, webhook, api_server,
│ # yuanbao
│ └── platforms/ # Built-in adapters: signal, weixin, bluebubbles,
│ # qqbot, whatsapp_cloud, yuanbao, webhook, api_server
├── plugins/platforms/ # Bundled platform plugins: telegram, discord, slack,
│ # whatsapp, matrix, mattermost, email, sms, dingtalk,
│ # feishu, wecom, homeassistant, irc, line, teams,
│ # google_chat, buzz, ntfy, photon, raft, simplex
├── acp_adapter/ # ACP server (VS Code / Zed / JetBrains)
├── cron/ # Scheduler (jobs.py, scheduler.py)
@ -223,7 +225,7 @@ SQLite-based session storage with FTS5 full-text search. Sessions have lineage t
### Messaging Gateway
Long-running process with 20 platform adapters, unified session routing, user authorization (allowlists + DM pairing), slash command dispatch, hook system, cron ticking, and background maintenance.
Long-running process with 25+ platform adapters (built-in + bundled plugins), unified session routing, user authorization (allowlists + DM pairing), slash command dispatch, hook system, cron ticking, and background maintenance.
→ [Gateway Internals](./gateway-internals.md)

View file

@ -91,6 +91,7 @@ compression:
codex_gpt55_autoraise: true # gpt-5.5 on Codex OAuth: raise trigger to 85% (default: true)
codex_gpt55_autoraise_notice: true # Show the one-time autoraise notice (default: true)
codex_app_server_auto: native # native|hermes|off for Codex app-server thread compaction
in_place: true # Compact on the same session id, no rotation (default: true)
# Summarization model/provider configured under auxiliary:
auxiliary:
@ -114,6 +115,18 @@ auxiliary:
| `codex_gpt55_autoraise` | `true` | bool | Raise the trigger to 85% for gpt-5.5 on the ChatGPT Codex OAuth route (see below). Set `false` to keep the global `threshold` |
| `codex_gpt55_autoraise_notice` | `true` | bool | Show the one-time Codex gpt-5.5 autoraise notice. Set `false` to keep the 85% autoraise but suppress the banner |
| `codex_app_server_auto` | `native` | `native`, `hermes`, `off` | Thread-compaction mode for Codex app-server sessions (see below) |
| `in_place` | `true` | bool | Compact on the same session id instead of rotating to a new one (see below) |
### In-place compaction (single stable session id)
With `compression.in_place: true` (the default), a compaction **rewrites the live message list on the same session id**: the system prompt is rebuilt, the summarized middle is swapped in, and the pre-compaction turns are soft-archived under the same id (`active=0, compacted=1` in the session store) — still searchable via `session_search` and recoverable, never deleted. There is no `parent_session_id` chain and no `name #N` renumbering; one conversation keeps one durable id for its whole life. This eliminated the session-rotation bug cluster (lost `/goal` state, orphaned sessions, search gaps across boundaries).
Consumers observe the mode rather than diffing session ids:
- The `session:compress` event carries `in_place: true/false` and `old_session_id` (empty string in in-place mode, since there is no old id).
- The gateway re-baselines transcript handling from the agent's rotation-independent `_last_compaction_in_place` flag, not from an id-change diff.
Set `in_place: false` to restore the legacy rotating path, where each compaction commits a new session id linked to the previous one via `parent_session_id`.
### Per-model threshold overrides

View file

@ -114,7 +114,7 @@ The active provider is chosen by the `cron.provider` config key:
historical in-process loop calling `scheduler.tick()` every 60 seconds. This
is byte-identical to the pre-provider behavior.
- **a named provider** (e.g. `chronos`, a managed-cron provider for
scale-to-zero deployments) → discovered from `plugins/cron/<name>/` or
scale-to-zero deployments) → discovered from `plugins/cron_providers/<name>/` or
`$HERMES_HOME/plugins/<name>/`.
If a named provider is missing, fails to load, or reports `is_available() ==

View file

@ -180,8 +180,8 @@ Experimental connector-backed platforms use the generic relay adapter in `gatewa
Adapters implement a common interface:
- `connect()` / `disconnect()` — lifecycle management
- `send_message()` — outbound message delivery
- `on_message()` — inbound message normalization → `MessageEvent`
- `send()` — outbound message delivery
- inbound events are normalized into a `MessageEvent` and forwarded via `handle_message()`
### Token Locks

View file

@ -6,7 +6,7 @@ description: "How to build an image-generation backend plugin for Hermes Agent"
# Building an Image Generation Provider Plugin
Image-gen provider plugins register a backend that services every `image_generate` tool call — DALL·E, gpt-image, Grok, Flux, Imagen, Stable Diffusion, fal, Replicate, a local ComfyUI rig, anything. Built-in providers (OpenAI, OpenAI-Codex, xAI) all ship as plugins. You can add a new one, or override a bundled one, by dropping a directory into `plugins/image_gen/<name>/`.
Image-gen provider plugins register a backend that services every `image_generate` tool call — DALL·E, gpt-image, Grok, Flux, Imagen, Stable Diffusion, fal, Replicate, a local ComfyUI rig, anything. Built-in providers (OpenAI, OpenAI-Codex, xAI, FAL, Krea, DeepInfra, OpenRouter) all ship as plugins. You can add a new one, or override a bundled one, by dropping a directory into `plugins/image_gen/<name>/`.
:::tip
Image-gen is one of several **backend plugins** Hermes supports. The others (with more specialized ABCs) are [Memory Provider Plugins](/developer-guide/memory-provider-plugin), [Context Engine Plugins](/developer-guide/context-engine-plugin), and [Model Provider Plugins](/developer-guide/model-provider-plugin). General tool/hook/CLI plugins live in [Build a Hermes Plugin](/developer-guide/plugins).

View file

@ -76,8 +76,8 @@ class MyMemoryProvider(MemoryProvider):
|--------|-----------|----------|
| `system_prompt_block()` | System prompt assembly | Static provider info |
| `prefetch(query, *, session_id="")` | Before each API call | Return recalled context |
| `queue_prefetch(query)` | After each turn | Pre-warm for next turn |
| `sync_turn(user, assistant, *, session_id="")` | After each completed turn | Persist conversation |
| `queue_prefetch(query, *, session_id="")` | After each turn | Pre-warm for next turn |
| `sync_turn(user, assistant, *, session_id="", messages=None)` | After each completed turn | Persist conversation |
| `on_session_end(messages)` | Conversation ends | Final extraction/flush |
| `on_pre_compress(messages)` | Before context compression | Save insights before discard |
| `on_memory_write(action, target, content)` | Built-in memory writes | Mirror to your backend |

View file

@ -136,11 +136,11 @@ class AcmeProfile(ProviderProfile):
reasoning dict). Default: ({}, {})."""
return {}, {}
def fetch_models(self, *, api_key=None, timeout=8.0) -> list[str] | None:
def fetch_models(self, *, api_key=None, base_url=None, timeout=8.0) -> list[str] | None:
"""Live catalog fetch. Default hits {models_url or base_url}/models with
Bearer auth. Override for: custom auth (Anthropic), no REST endpoint
(Bedrock → None), or public/unauthenticated catalogs (OpenRouter)."""
return super().fetch_models(api_key=api_key, timeout=timeout)
return super().fetch_models(api_key=api_key, base_url=base_url, timeout=timeout)
```
## Hook reference examples

View file

@ -300,7 +300,8 @@ class PluginLlmCompleteResult:
audit: Dict[str, Any] # plugin_id, purpose, profile
@dataclass
class PluginLlmStructuredResult(PluginLlmCompleteResult):
class PluginLlmStructuredResult:
# same fields as PluginLlmCompleteResult, plus:
parsed: Optional[Any] # JSON object when content_type == "json"
content_type: str # "json" or "text"
# audit also carries schema_name when supplied

View file

@ -577,7 +577,11 @@ def register(ctx):
Without `override=True`, the registry rejects any registration that would
shadow an existing tool from a different toolset — this prevents
accidental overwrites. The override is logged at INFO level so it's
accidental overwrites. Overriding a **built-in** tool additionally
requires the operator to opt in via
`plugins.entries.<plugin_id>.allow_tool_override: true` in `config.yaml`;
without that gate, `register_tool(override=True)` raises
`PluginToolOverrideError`. The override is logged so it's
auditable in `~/.hermes/logs/agent.log`. Plugins load after built-in
tools, so the registration order is correct: your handler replaces the
built-in one.
@ -599,7 +603,7 @@ Each hook is documented in full on the **[Event Hooks reference](/user-guide/fea
| Hook | Fires when | Callback signature | Returns |
|------|-----------|-------------------|---------|
| [`pre_tool_call`](/user-guide/features/hooks#pre_tool_call) | Before any tool executes | `tool_name: str, args: dict, task_id: str` | ignored |
| [`pre_tool_call`](/user-guide/features/hooks#pre_tool_call) | Before any tool executes | `tool_name: str, args: dict, task_id: str` | optional directive: `{"action": "block", "message": ...}` vetoes the call; `{"action": "approve", "message": ...}` escalates to the human-approval gate |
| [`post_tool_call`](/user-guide/features/hooks#post_tool_call) | After any tool returns | `tool_name: str, args: dict, result: str, task_id: str, duration_ms: int` | ignored |
| [`pre_llm_call`](/user-guide/features/hooks#pre_llm_call) | Once per turn, before the tool-calling loop | `session_id: str, user_message: str, conversation_history: list, is_first_turn: bool, model: str, platform: str` | [context injection](#pre_llm_call-context-injection) |
| [`post_llm_call`](/user-guide/features/hooks#post_llm_call) | Once per turn, after the tool-calling loop (successful turns only) | `session_id: str, user_message: str, assistant_response: str, conversation_history: list, model: str, platform: str` | ignored |
@ -611,7 +615,7 @@ Each hook is documented in full on the **[Event Hooks reference](/user-guide/fea
| `kanban_task_completed` | A kanban task completes (worker process) | `task_id, board, assignee, run_id, profile_name, summary: str \| None` | ignored |
| `kanban_task_blocked` | A kanban task is blocked (worker process) | `task_id, board, assignee, run_id, profile_name, reason: str \| None` | ignored |
Most hooks are fire-and-forget observers — their return values are ignored. The exception is `pre_llm_call`, which can inject context into the conversation.
Most hooks are fire-and-forget observers — their return values are ignored. The exceptions are `pre_llm_call`, which can inject context into the conversation, and `pre_tool_call`, which can return a block/approve directive.
All callbacks should accept `**kwargs` for forward compatibility. If a hook callback crashes, it's logged and skipped. Other hooks and the agent continue normally.
@ -1063,7 +1067,8 @@ class MyContextEngine(ContextEngine):
def update_from_response(self, usage) -> None: ...
def should_compress(self, prompt_tokens: int = None) -> bool: ...
def compress(self, messages, current_tokens=None, focus_topic=None) -> list: ...
def compress(self, messages, current_tokens=None, focus_topic=None,
force=False, memory_context="") -> list: ...
def register(ctx):
ctx.register_context_engine(MyContextEngine())

View file

@ -28,7 +28,8 @@ Full lifecycle, event bridge, and approval flow: [ACP Internals](./acp-internals
```bash
hermes acp # serve ACP on stdio
hermes acp --bootstrap # print install snippet for an ACP-capable IDE
hermes acp --check # verify ACP dependencies and adapter imports
hermes acp --setup # interactive provider/model setup for ACP terminal auth
```
---

View file

@ -165,7 +165,7 @@ def load_soul_md() -> Optional[str]:
return None
content = soul_path.read_text(encoding="utf-8").strip()
content = _scan_context_content(content, "SOUL.md") # Security scan
content = _truncate_content(content, "SOUL.md") # Cap defaults to 20k chars, configurable
content = _truncate_content(content, "SOUL.md") # Cap scales with model context window (20k floor); config override wins
return content
```
@ -232,7 +232,7 @@ def build_context_files_prompt(cwd=None, skip_soul=False):
All context files are:
- **Security scanned** — checked for prompt injection patterns (invisible unicode, "ignore previous instructions", credential exfiltration attempts)
- **Truncated** — capped at `context_file_max_chars` characters (default 20,000) using 70/20 head/tail ratio with a truncation marker
- **Truncated** — capped at `context_file_max_chars` characters using a 70/20 head/tail split with a truncation marker. The cap scales with the model's context window (20,000-char floor, 500K ceiling); an explicit `context_file_max_chars` in `config.yaml` always wins.
- **YAML frontmatter stripped**`.hermes.md` frontmatter is removed (reserved for future config overrides)
## API-call-time-only layers

View file

@ -16,7 +16,7 @@ Hermes has a shared provider runtime resolver used across:
Primary implementation:
- `hermes_cli/runtime_provider.py` — credential resolution, `_resolve_custom_runtime()`
- `hermes_cli/runtime_provider.py` — credential resolution, custom-endpoint runtime resolution
- `hermes_cli/auth.py` — provider registry, `resolve_provider()`
- `hermes_cli/model_switch.py` — shared `/model` switch pipeline (CLI + gateway)
- `agent/auxiliary_client.py` — auxiliary model routing
@ -180,7 +180,7 @@ Hermes supports a configured fallback provider chain — a list of `(provider, m
- Resets retry count to 0 and continues the loop
4. **Config flow**:
- CLI: `cli.py` reads `CLI_CONFIG["fallback_model"]` → passes to `AIAgent(fallback_model=...)`
- CLI: reads the fallback chain via `hermes_cli/fallback_config.get_fallback_chain()` → passes to `AIAgent(fallback_model=...)`
- Gateway: `gateway/run.py._load_fallback_model()` reads `config.yaml` → passes to `AIAgent`
- Validation: both `provider` and `model` keys must be non-empty, or fallback is disabled

View file

@ -6,7 +6,7 @@ description: "How to build a secret-manager backend plugin for Hermes Agent"
# Building a Secret Source Plugin
Secret sources resolve provider credentials from an external secret manager (a vault, a password manager, an OS keystore, a custom script) into environment variables at process startup — after `~/.hermes/.env` loads, before Hermes reads credentials. Bitwarden and 1Password ship in-tree; **every other backend is a plugin**. This guide covers building one.
Secret sources resolve provider credentials from an external secret manager (a vault, a password manager, an OS keystore, a custom script) into environment variables at process startup — after `~/.hermes/.env` loads, before Hermes reads credentials. Bitwarden, 1Password, and a generic command-helper source ship in-tree; **every other backend is a plugin**. This guide covers building one.
:::tip
The bundled set is deliberately closed, same policy as [memory providers](/developer-guide/memory-provider-plugin): PRs adding new vault backends under `agent/secret_sources/` are closed with a pointer to this guide. Publish your backend as a standalone plugin repo and share it in the Nous Research Discord (`#plugins-skills-and-skins`).

View file

@ -13,9 +13,14 @@ Source file: `hermes_state.py`
~/.hermes/state.db (SQLite, WAL mode)
├── sessions — Session metadata, token counts, billing
├── messages — Full message history per session
├── session_model_usage — Per-model/per-task usage attribution rows
├── messages_fts — FTS5 virtual table (content + tool_name + tool_calls)
├── messages_fts_trigram — FTS5 virtual table with trigram tokenizer (CJK / substring search)
├── messages_fts_cjk — FTS5 virtual table with cjk_unicode61 tokenizer
├── state_meta — Key/value metadata table
├── gateway_routing — Gateway routing metadata
├── compression_locks — Cross-process compression locking
├── async_delegations — Async delegation bookkeeping
└── schema_version — Single-row table tracking migration state
```
@ -31,6 +36,13 @@ Key design decisions:
### Sessions Table
Abridged — see `SCHEMA_SQL` in `hermes_state.py` for the full current column list
(which also includes gateway routing metadata such as `session_key`, `chat_id`,
`chat_type`, `thread_id`, `display_name`, `origin_json`, `expiry_finalized`,
workspace fields `cwd` / `git_branch` / `git_repo_root`, handoff and
compression-failure fields, `profile_name`, `rewind_count`, `archived`, and
`pinned`):
```sql
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
@ -60,6 +72,7 @@ CREATE TABLE IF NOT EXISTS sessions (
pricing_version TEXT,
title TEXT,
api_call_count INTEGER DEFAULT 0,
-- ... additional gateway/workspace/handoff/compression columns ...
FOREIGN KEY (parent_session_id) REFERENCES sessions(id)
);
@ -72,6 +85,10 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_title_unique
### Messages Table
Abridged — the full schema also includes `effect_disposition`,
`platform_message_id`, `observed`, `active`, `compacted`, `api_content`,
`display_kind`, and `display_metadata`:
```sql
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -89,6 +106,7 @@ CREATE TABLE IF NOT EXISTS messages (
reasoning_details TEXT,
codex_reasoning_items TEXT,
codex_message_items TEXT
-- ... additional display/compaction columns ...
);
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, timestamp);
@ -98,6 +116,7 @@ Notes:
- `tool_calls` is stored as a JSON string (serialized list of tool call objects)
- `reasoning_details`, `codex_reasoning_items`, and `codex_message_items` are stored as JSON strings
- `reasoning` stores the raw reasoning text for providers that expose it
- `api_content` is a byte-fidelity sidecar: the exact content string sent to the API for this message when it differs from `content` (ephemeral memory/plugin injections, persist overrides). It preserves the wire bytes for prompt-cache-stable replay — stored as sent, except lone surrogates, which sqlite3 cannot bind and which the conversation loop scrubs from every outgoing payload anyway. `NULL` means `content` was sent verbatim.
- Timestamps are Unix epoch floats (`time.time()`)
### FTS5 Full-Text Search
@ -105,35 +124,23 @@ Notes:
```sql
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
content,
content=messages,
content_rowid=id
tool_name,
tool_calls,
content='messages',
content_rowid='id'
);
```
The FTS5 table is kept in sync via three triggers that fire on INSERT, UPDATE,
and DELETE of the `messages` table:
```sql
CREATE TRIGGER IF NOT EXISTS messages_fts_insert AFTER INSERT ON messages BEGIN
INSERT INTO messages_fts(rowid, content) VALUES (new.id, new.content);
END;
CREATE TRIGGER IF NOT EXISTS messages_fts_delete AFTER DELETE ON messages BEGIN
INSERT INTO messages_fts(messages_fts, rowid, content)
VALUES('delete', old.id, old.content);
END;
CREATE TRIGGER IF NOT EXISTS messages_fts_update AFTER UPDATE ON messages BEGIN
INSERT INTO messages_fts(messages_fts, rowid, content)
VALUES('delete', old.id, old.content);
INSERT INTO messages_fts(rowid, content) VALUES (new.id, new.content);
END;
```
and DELETE of the `messages` table. The current triggers are gated on the
`fts_rebuild_high_water` / `fts_rebuild_progress` markers in `state_meta` (so a
background FTS rebuild can proceed without double-indexing) and cover all three
indexed columns — see `SCHEMA_SQL` in `hermes_state.py` for the exact SQL.
## Schema Version and Migrations
Current schema version: **21**
Current schema version: **23**
The `schema_version` table stores a single integer. Simple column additions are handled declaratively by `_reconcile_columns()` (which diffs live columns against `SCHEMA_SQL` and ADDs any missing ones). The version-gated chain is reserved for data migrations and index/FTS changes that can't be expressed declaratively:
@ -153,6 +160,8 @@ The `schema_version` table stores a single integer. Simple column additions are
| 16 | Tag delegate subagent rows in `model_config` (`$._delegate_from`) so session pickers stay clean after parent deletes orphan them |
| 18 | Gateway metadata consolidation — backfill `display_name` / `origin_json` / `expiry_finalized` from `sessions.json` |
| 20 | Per-model usage attribution — seed `session_model_usage` rows from historical per-session aggregate totals |
| 22 | Task-dimension usage attribution — rebuild `session_model_usage` so the `task` column participates in the PRIMARY KEY |
| 23 | FTS storage redesign — external-content FTS tables replacing the v11 inline-mode copies (opt-in transition for existing DBs) |
Versions not listed above were declarative column additions handled by `_reconcile_columns()` (version bump only, no data migration).

View file

@ -40,7 +40,7 @@ registry.register(
)
```
Each call creates a `ToolEntry` stored in the singleton `ToolRegistry._tools` dict keyed by tool name. If a name collision occurs across toolsets, a warning is logged and the later registration wins.
Each call creates a `ToolEntry` stored in the singleton `ToolRegistry._tools` dict keyed by tool name. A registration that would shadow an existing tool from a **different** toolset is rejected (with an error log) unless the caller passes `override=True`; plugin overrides of built-in tools additionally require the operator opt-in `plugins.entries.<plugin_id>.allow_tool_override: true` in `config.yaml`.
### Discovery: `discover_builtin_tools()`

View file

@ -215,17 +215,16 @@ preventing Arrow schema mismatch errors during dataset loading.
## Controlling Trajectory Saving
In the CLI, trajectory saving is controlled by:
Trajectory saving is a `run_agent.py` / library-level switch — the `hermes` CLI
does not expose a config key or flag for it:
```yaml
# config.yaml
agent:
save_trajectories: true # default: false
```bash
python run_agent.py --save_trajectories --query='your question here'
```
Or via the `--save-trajectories` flag. When the agent initializes with
`save_trajectories=True`, the `_save_trajectory()` method is called at the end
of each conversation turn.
Or programmatically: `AIAgent(..., save_trajectories=True)` /
`initialize_agent(..., save_trajectories=True)`. When enabled, the
`_save_trajectory()` method is called at the end of each conversation turn.
The batch runner always saves trajectories (that's its primary purpose).

View file

@ -6,7 +6,7 @@ description: "How to build a video-generation backend plugin for Hermes Agent"
# Building a Video Generation Provider Plugin
Video-gen provider plugins register a backend that services every `video_generate` tool call. Built-in providers (xAI, FAL) ship as plugins. Add a new one, or override a bundled one, by dropping a directory into `plugins/video_gen/<name>/`.
Video-gen provider plugins register a backend that services every `video_generate` tool call. Built-in providers (xAI, FAL, DeepInfra) ship as plugins. Add a new one, or override a bundled one, by dropping a directory into `plugins/video_gen/<name>/`.
:::tip
Video-gen mirrors [Image Generation Provider Plugins](/developer-guide/image-gen-provider-plugin) almost line-for-line — if you've built an image-gen backend, you already know the shape. The main differences: a `capabilities()` method advertising modalities/aspect-ratios/durations, and a routing convention (pass `image_url` to use image-to-video, omit it to use text-to-video — the provider picks the right endpoint internally).