mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
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:
parent
068d7812a4
commit
43874d1a96
73 changed files with 656 additions and 292 deletions
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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() ==
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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`).
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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()`
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue