mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
* feat(plugins): host-owned LLM access via ctx.llm
Plugins can now ask the host to run a one-shot chat or structured
completion against the user's active model and auth, without ever
seeing an OAuth token or API key. Closes the gap where plugins that
needed bounded structured inference (receipts, CRM extraction,
support classification) had to either bring their own provider keys
or register a tool the agent had to call.
New surface on PluginContext:
- ctx.llm.complete(messages, ...)
- ctx.llm.complete_structured(instructions, input, json_schema, ...)
- async siblings ctx.llm.acomplete / acomplete_structured
Backed by the existing auxiliary_client.call_llm pipeline — every
provider, fallback chain, vision routing, and timeout policy Hermes
already supports applies automatically.
Trust gate (fail-closed by default):
- plugins.entries.<id>.llm.allow_model_override
- plugins.entries.<id>.llm.allowed_models (allowlist; '*' = any)
- plugins.entries.<id>.llm.allow_agent_id_override
- plugins.entries.<id>.llm.allow_profile_override
Embedded model@profile shorthand goes through the same gate as
explicit profile=, so it can't bypass the auth-profile policy.
Conflicting explicit and embedded profiles fail closed.
Also lands:
- plugins/plugin-llm-example/ — reference plugin that registers
/receipt-extract, demonstrating image+text structured input,
jsonschema validation, and the trust-gate config.
- website/docs/developer-guide/plugin-llm-access.md — full API docs.
- 45 unit tests covering trust gates, JSON parsing, schema
validation, image encoding, async surface, and config loading.
Validation:
- 2628 tests pass in tests/agent/
- E2E: bundled plugin loaded with isolated HERMES_HOME, slash
command produced parsed JSON via stubbed call_llm
- response_format extra_body wired correctly for both json_object
and json_schema modes
* docs(plugin-llm): rewrite quickstart and framing
The quickstart now uses a meeting-notes-to-tasks example instead of
a receipt extractor, and the page leads with hook-time / gateway
pre-filter / scheduled-job framing rather than the OpenClaw
KB/support/CRM/finance/migration enumeration that the original
upstream PR used. Receipt example moved to a separate worked
example link so the docs page itself doesn't echo any of the
upstream framing.
Also clarifies where ctx.llm fits in the broader plugin surface
(table comparing register_tool / register_platform / register_hook
/ etc.) and what makes this lane different from auxiliary_client
internals.
No code change.
* docs(plugin-llm): reframe as any LLM call, not just structured output
The original draft leaned heavily on complete_structured() and made
the chat lane (complete() / acomplete()) feel like a footnote.
Restructure so:
- The page title and description say 'any LLM call.'
- The lead shows BOTH a plain chat call (error rewriter) AND a
structured call (triage scorer) up top.
- Quick start has two complete plugin examples — /tldr (chat) and
/paste-to-tasks (structured).
- New 'When to use which' table for choosing complete() vs
complete_structured() vs the async siblings.
- Trust-gate sections explicitly note 'all four methods,' and the
request-shaping list calls out chat-only fields (messages) and
structured-only fields (instructions, input, json_schema)
alongside each other.
- The 'Where this fits' section now says 'for any reason,
structured or not.'
The receipt-extractor reference plugin still exists under
plugins/plugin-llm-example/ — but the docs page no longer treats
it as the canonical surface example. It's now described as 'a third
worked example, this time with image input.'
No code change.
* feat(plugin-llm): split provider/model into independent explicit kwargs
The first cut accepted a single 'provider/model' slug on every method
and split it internally. That looked clean but broke under live test:
the model-override path tried to use the slug's vendor prefix as a
literal Hermes provider id, which silently switched the user off
their aggregator (e.g. plugin asks for 'openai/gpt-4o-mini' on a user
who routes through OpenRouter — host attempted to call the 'openai'
provider directly, failed because OPENAI_API_KEY wasn't set).
New shape mirrors the host's main config:
ctx.llm.complete(
messages=[...],
provider='openrouter', # gated, optional
model='openai/gpt-4o-mini', # gated, optional
profile='work', # gated, optional
...
)
Each is independently gated by its own allow_*_override flag.
Granting model-override does NOT auto-grant provider-override.
Allowlists are now per-axis (allowed_providers, allowed_models)
matched literally against whatever string the plugin sends.
Dropped 'model@profile' embedded-suffix shorthand entirely. Hermes
doesn't use that pattern anywhere else; profile= is its own kwarg.
Live E2E (against real OpenRouter via Teknium's config) confirms:
- zero-config call works
- default-deny blocks each override with a helpful error
- model-only override stays on user's active provider (the bug)
- provider+model override switches cleanly
- allowlist refuses non-listed entries
- structured output round-trip parses + schema-validates
Tests: 49 cases (up from 45); all green. Docs updated to match the
new shape, including a 'most plugins never need this section' callout
on the trust-gate config block.
* fix+cleanup(plugin-llm): real attribution, hook-mode coverage, move example out of core
Three integration fixes for the ctx.llm surface:
1. Attribution bug — result.provider and result.model now reflect
what call_llm actually used, not placeholder fallbacks ('auto',
'default'). New _resolve_attribution() helper:
- explicit overrides win (what the call targeted)
- response.model wins for the recorded model (provider
canonicalisation: 'gpt-4o' → 'gpt-4o-2024-08-06' etc.)
- falls back to _read_main_provider() / _read_main_model()
when no override is set, so audit logs reflect the user's
active main provider/model
- 'auto' / 'default' only when EVERYTHING is empty
Live verified: zero-config call now records
provider='openrouter', model='anthropic/claude-4.7-opus-20260416'
instead of provider='auto', model='default'.
2. Hook-mode coverage — TestHookMode confirms ctx.llm.complete
works from inside a registered post_tool_call callback. The
docs page promised hook integration; now there's a test that
exercises the lazy-import path through the real invoke_hook
machinery. Two cases: traceback-rewrite hook with conditional
ctx.llm.complete, and minimal hook regression for the
sync-hook + sync-llm path.
3. Reference plugin moved out of core. plugins/plugin-llm-example/
is gone from hermes-agent — it now lives in the new
NousResearch/hermes-example-plugins companion repo. The docs
page links there. Hermes' bundled plugins should be plugins
users actually run; reference / docs-companion plugins live
externally.
Test count: 56 (up from 49). Wider sweep on tests/hermes_cli/
+ tests/gateway/ + tests/tools/ + tests/agent/ shows 16770
passing; the 12 failures are all pre-existing on origin/main
(verified by stashing this branch's changes and re-running) —
kanban-boards, delegate-task, gateway-restart, tts-routing —
none touch the plugin_llm surface.
* chore(plugins): move all example plugins to companion repo
Reference / docs-companion plugins now live exclusively in
NousResearch/hermes-example-plugins, not bundled with the core repo:
- example-dashboard
- strike-freedom-cockpit
A new fourth example, plugin-llm-async-example, was added to that
repo demonstrating ctx.llm's async surface (acomplete()) with
asyncio.gather() — registers /translate <lang>: <text> which fires
forward translation + sentiment classifier in parallel, then a
back-translation for QA. Live-tested at 2.5s for three real
provider round-trips (would be ~5-6s sequential).
Docs updated:
- developer-guide/plugin-llm-access.md links both sync and async
examples in the Reference section
- user-guide/features/extending-the-dashboard.md repoints both demo
sections to the companion repo with corrected install paths
- user-guide/features/built-in-plugins.md drops the two demo rows
- AGENTS.md notes that example plugins live in the companion repo
Net: hermes-agent's plugins/ directory now contains only plugins
users actually run (memory providers, dashboard tabs that ship real
features, the disk-cleanup hook, platform adapters). All four
demo / reference plugins live externally where they can be cloned
on demand instead of inflating the core install.
344 lines
22 KiB
Markdown
344 lines
22 KiB
Markdown
---
|
|
sidebar_position: 11
|
|
sidebar_label: "Plugins"
|
|
title: "Plugins"
|
|
description: "Extend Hermes with custom tools, hooks, and integrations via the plugin system"
|
|
---
|
|
|
|
# Plugins
|
|
|
|
Hermes has a plugin system for adding custom tools, hooks, and integrations without modifying core code.
|
|
|
|
If you want to create a custom tool for yourself, your team, or one project,
|
|
this is usually the right path. The developer guide's
|
|
[Adding Tools](/docs/developer-guide/adding-tools) page is for built-in Hermes
|
|
core tools that live in `tools/` and `toolsets.py`.
|
|
|
|
**→ [Build a Hermes Plugin](/docs/guides/build-a-hermes-plugin)** — step-by-step guide with a complete working example.
|
|
|
|
## Quick overview
|
|
|
|
Drop a directory into `~/.hermes/plugins/` with a `plugin.yaml` and Python code:
|
|
|
|
```
|
|
~/.hermes/plugins/my-plugin/
|
|
├── plugin.yaml # manifest
|
|
├── __init__.py # register() — wires schemas to handlers
|
|
├── schemas.py # tool schemas (what the LLM sees)
|
|
└── tools.py # tool handlers (what runs when called)
|
|
```
|
|
|
|
Start Hermes — your tools appear alongside built-in tools. The model can call them immediately.
|
|
|
|
### Minimal working example
|
|
|
|
Here is a complete plugin that adds a `hello_world` tool and logs every tool call via a hook.
|
|
|
|
**`~/.hermes/plugins/hello-world/plugin.yaml`**
|
|
|
|
```yaml
|
|
name: hello-world
|
|
version: "1.0"
|
|
description: A minimal example plugin
|
|
```
|
|
|
|
**`~/.hermes/plugins/hello-world/__init__.py`**
|
|
|
|
```python
|
|
"""Minimal Hermes plugin — registers a tool and a hook."""
|
|
|
|
import json
|
|
|
|
|
|
def register(ctx):
|
|
# --- Tool: hello_world ---
|
|
schema = {
|
|
"name": "hello_world",
|
|
"description": "Returns a friendly greeting for the given name.",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Name to greet",
|
|
}
|
|
},
|
|
"required": ["name"],
|
|
},
|
|
}
|
|
|
|
def handle_hello(params, **kwargs):
|
|
del kwargs
|
|
name = params.get("name", "World")
|
|
return json.dumps({"success": True, "greeting": f"Hello, {name}!"})
|
|
|
|
ctx.register_tool(
|
|
name="hello_world",
|
|
toolset="hello_world",
|
|
schema=schema,
|
|
handler=handle_hello,
|
|
description="Return a friendly greeting for the given name.",
|
|
)
|
|
|
|
# --- Hook: log every tool call ---
|
|
def on_tool_call(tool_name, params, result):
|
|
print(f"[hello-world] tool called: {tool_name}")
|
|
|
|
ctx.register_hook("post_tool_call", on_tool_call)
|
|
```
|
|
|
|
Drop both files into `~/.hermes/plugins/hello-world/`, restart Hermes, and the model can immediately call `hello_world`. The hook prints a log line after every tool invocation.
|
|
|
|
Project-local plugins under `./.hermes/plugins/` are disabled by default. Enable them only for trusted repositories by setting `HERMES_ENABLE_PROJECT_PLUGINS=true` before starting Hermes.
|
|
|
|
## What plugins can do
|
|
|
|
Every `ctx.*` API below is available inside a plugin's `register(ctx)` function.
|
|
|
|
| Capability | How |
|
|
|-----------|-----|
|
|
| Add tools | `ctx.register_tool(name=..., toolset=..., schema=..., handler=...)` |
|
|
| Add hooks | `ctx.register_hook("post_tool_call", callback)` |
|
|
| Add slash commands | `ctx.register_command(name, handler, description)` — adds `/name` in CLI and gateway sessions |
|
|
| Dispatch tools from commands | `ctx.dispatch_tool(name, args)` — invokes a registered tool with parent-agent context auto-wired |
|
|
| Add CLI commands | `ctx.register_cli_command(name, help, setup_fn, handler_fn)` — adds `hermes <plugin> <subcommand>` |
|
|
| Inject messages | `ctx.inject_message(content, role="user")` — see [Injecting Messages](#injecting-messages) |
|
|
| Ship data files | `Path(__file__).parent / "data" / "file.yaml"` |
|
|
| Bundle skills | `ctx.register_skill(name, path)` — namespaced as `plugin:skill`, loaded via `skill_view("plugin:skill")` |
|
|
| Gate on env vars | `requires_env: [API_KEY]` in plugin.yaml — prompted during `hermes plugins install` |
|
|
| Distribute via pip | `[project.entry-points."hermes_agent.plugins"]` |
|
|
| Register a gateway platform (Discord, Telegram, IRC, …) | `ctx.register_platform(name, label, adapter_factory, check_fn, ...)` — see [Adding Platform Adapters](/docs/developer-guide/adding-platform-adapters) |
|
|
| Register an image-generation backend | `ctx.register_image_gen_provider(provider)` — see [Image Generation Provider Plugins](/docs/developer-guide/image-gen-provider-plugin) |
|
|
| Register a context-compression engine | `ctx.register_context_engine(engine)` — see [Context Engine Plugins](/docs/developer-guide/context-engine-plugin) |
|
|
| Register a memory backend | Subclass `MemoryProvider` in `plugins/memory/<name>/__init__.py` — see [Memory Provider Plugins](/docs/developer-guide/memory-provider-plugin) (uses a separate discovery system) |
|
|
| Run a host-owned LLM call | `ctx.llm.complete(...)` / `ctx.llm.complete_structured(...)` — borrow the user's active model + auth for a one-shot completion with optional JSON schema validation. See [Plugin LLM Access](/docs/developer-guide/plugin-llm-access) |
|
|
| Register an inference backend (LLM provider) | `register_provider(ProviderProfile(...))` in `plugins/model-providers/<name>/__init__.py` — see [Model Provider Plugins](/docs/developer-guide/model-provider-plugin) (uses a separate discovery system) |
|
|
|
|
## Plugin discovery
|
|
|
|
| Source | Path | Use case |
|
|
|--------|------|----------|
|
|
| Bundled | `<repo>/plugins/` | Ships with Hermes — see [Built-in Plugins](/docs/user-guide/features/built-in-plugins) |
|
|
| User | `~/.hermes/plugins/` | Personal plugins |
|
|
| Project | `.hermes/plugins/` | Project-specific plugins (requires `HERMES_ENABLE_PROJECT_PLUGINS=true`) |
|
|
| pip | `hermes_agent.plugins` entry_points | Distributed packages |
|
|
| Nix | `services.hermes-agent.extraPlugins` / `extraPythonPackages` | NixOS declarative installs — see [Nix Setup](/docs/getting-started/nix-setup#plugins) |
|
|
|
|
Later sources override earlier ones on name collision, so a user plugin with the same name as a bundled plugin replaces it.
|
|
|
|
### Plugin sub-categories
|
|
|
|
Within each source, Hermes also recognizes sub-category directories that route plugins to specialized discovery systems:
|
|
|
|
| Sub-directory | What it holds | Discovery system |
|
|
|---|---|---|
|
|
| `plugins/` (root) | General plugins — tools, hooks, slash commands, CLI commands, bundled skills | `PluginManager` (kind: `standalone` or `backend`) |
|
|
| `plugins/platforms/<name>/` | Gateway channel adapters (`ctx.register_platform()`) | `PluginManager` (kind: `platform`, one level deeper) |
|
|
| `plugins/image_gen/<name>/` | Image-generation backends (`ctx.register_image_gen_provider()`) | `PluginManager` (kind: `backend`, one level deeper) |
|
|
| `plugins/memory/<name>/` | Memory providers (subclass `MemoryProvider`) | **Own loader** in `plugins/memory/__init__.py` (kind: `exclusive` — one active at a time) |
|
|
| `plugins/context_engine/<name>/` | Context-compression engines (`ctx.register_context_engine()`) | **Own loader** in `plugins/context_engine/__init__.py` (one active at a time) |
|
|
| `plugins/model-providers/<name>/` | LLM provider profiles (`register_provider(ProviderProfile(...))`) | **Own loader** in `providers/__init__.py` (lazily scanned on first `get_provider_profile()` call) |
|
|
|
|
User plugins at `~/.hermes/plugins/model-providers/<name>/` and `~/.hermes/plugins/memory/<name>/` override bundled plugins of the same name — last-writer-wins in `register_provider()` / `register_memory_provider()`. Drop a directory in, and it replaces the built-in without any repo edits.
|
|
|
|
## Plugins are opt-in (with a few exceptions)
|
|
|
|
**General plugins and user-installed backends are disabled by default** — discovery finds them (so they show up in `hermes plugins` and `/plugins`), but nothing with hooks or tools loads until you add the plugin's name to `plugins.enabled` in `~/.hermes/config.yaml`. This stops third-party code from running without your explicit consent.
|
|
|
|
```yaml
|
|
plugins:
|
|
enabled:
|
|
- my-tool-plugin
|
|
- disk-cleanup
|
|
disabled: # optional deny-list — always wins if a name appears in both
|
|
- noisy-plugin
|
|
```
|
|
|
|
Three ways to flip state:
|
|
|
|
```bash
|
|
hermes plugins # interactive toggle (space to check/uncheck)
|
|
hermes plugins enable <name> # add to allow-list
|
|
hermes plugins disable <name> # remove from allow-list + add to disabled
|
|
```
|
|
|
|
After `hermes plugins install owner/repo`, you're asked `Enable 'name' now? [y/N]` — defaults to no. Skip the prompt for scripted installs with `--enable` or `--no-enable`.
|
|
|
|
### What the allow-list does NOT gate
|
|
|
|
Several categories of plugin bypass `plugins.enabled` — they're part of Hermes' built-in surface and would break basic functionality if gated off by default:
|
|
|
|
| Plugin kind | How it's activated instead |
|
|
|---|---|
|
|
| **Bundled platform plugins** (IRC, Teams, etc. under `plugins/platforms/`) | Auto-loaded so every shipped gateway channel is available. The actual channel turns on via `gateway.platforms.<name>.enabled` in `config.yaml`. |
|
|
| **Bundled backends** (image-gen providers under `plugins/image_gen/`, etc.) | Auto-loaded so the default backend "just works". Selection happens via `<category>.provider` in `config.yaml` (e.g. `image_gen.provider: openai`). |
|
|
| **Memory providers** (`plugins/memory/`) | All discovered; exactly one is active, chosen by `memory.provider` in `config.yaml`. |
|
|
| **Context engines** (`plugins/context_engine/`) | All discovered; one is active, chosen by `context.engine` in `config.yaml`. |
|
|
| **Model providers** (`plugins/model-providers/`) | All bundled providers under `plugins/model-providers/` discover and register at the first `get_provider_profile()` call. The user picks one at a time via `--provider` or `config.yaml`. |
|
|
| **Pip-installed `backend` plugins** | Opt-in via `plugins.enabled` (same as general plugins). |
|
|
| **User-installed platforms** (under `~/.hermes/plugins/platforms/`) | Opt-in via `plugins.enabled` — third-party gateway adapters need explicit consent. |
|
|
|
|
In short: **bundled "always-works" infrastructure loads automatically; third-party general plugins are opt-in.** The `plugins.enabled` allow-list is the gate specifically for arbitrary code a user drops into `~/.hermes/plugins/`.
|
|
|
|
### Migration for existing users
|
|
|
|
When you upgrade to a version of Hermes that has opt-in plugins (config schema v21+), any user plugins already installed under `~/.hermes/plugins/` that weren't already in `plugins.disabled` are **automatically grandfathered** into `plugins.enabled`. Your existing setup keeps working. Bundled standalone plugins are NOT grandfathered — even existing users have to opt in explicitly. (Bundled platform/backend plugins never needed grandfathering because they were never gated.)
|
|
|
|
## Available hooks
|
|
|
|
Plugins can register callbacks for these lifecycle events. See the **[Event Hooks page](/docs/user-guide/features/hooks#plugin-hooks)** for full details, callback signatures, and examples.
|
|
|
|
| Hook | Fires when |
|
|
|------|-----------|
|
|
| [`pre_tool_call`](/docs/user-guide/features/hooks#pre_tool_call) | Before any tool executes |
|
|
| [`post_tool_call`](/docs/user-guide/features/hooks#post_tool_call) | After any tool returns |
|
|
| [`pre_llm_call`](/docs/user-guide/features/hooks#pre_llm_call) | Once per turn, before the LLM loop — can return `{"context": "..."}` to [inject context into the user message](/docs/user-guide/features/hooks#pre_llm_call) |
|
|
| [`post_llm_call`](/docs/user-guide/features/hooks#post_llm_call) | Once per turn, after the LLM loop (successful turns only) |
|
|
| [`on_session_start`](/docs/user-guide/features/hooks#on_session_start) | New session created (first turn only) |
|
|
| [`on_session_end`](/docs/user-guide/features/hooks#on_session_end) | End of every `run_conversation` call + CLI exit handler |
|
|
| [`on_session_finalize`](/docs/user-guide/features/hooks#on_session_finalize) | CLI/gateway tears down an active session (`/new`, GC, CLI quit) |
|
|
| [`on_session_reset`](/docs/user-guide/features/hooks#on_session_reset) | Gateway swaps in a new session key (`/new`, `/reset`, `/clear`, idle rotation) |
|
|
| [`subagent_stop`](/docs/user-guide/features/hooks#subagent_stop) | Once per child after `delegate_task` finishes |
|
|
| [`pre_gateway_dispatch`](/docs/user-guide/features/hooks#pre_gateway_dispatch) | Gateway received a user message, before auth + dispatch. Return `{"action": "skip" \| "rewrite" \| "allow", ...}` to influence flow. |
|
|
|
|
## Plugin types
|
|
|
|
Hermes has four kinds of plugins:
|
|
|
|
| Type | What it does | Selection | Location |
|
|
|------|-------------|-----------|----------|
|
|
| **General plugins** | Add tools, hooks, slash commands, CLI commands | Multi-select (enable/disable) | `~/.hermes/plugins/` |
|
|
| **Memory providers** | Replace or augment built-in memory | Single-select (one active) | `plugins/memory/` |
|
|
| **Context engines** | Replace the built-in context compressor | Single-select (one active) | `plugins/context_engine/` |
|
|
| **Model providers** | Declare an inference backend (OpenRouter, Anthropic, …) | Multi-register, picked by `--provider` / `config.yaml` | `plugins/model-providers/` |
|
|
|
|
Memory providers and context engines are **provider plugins** — only one of each type can be active at a time. Model providers are also plugins, but many load simultaneously; the user picks one at a time via `--provider` or `config.yaml`. General plugins can be enabled in any combination.
|
|
|
|
## Pluggable interfaces — where to go for each
|
|
|
|
The table above shows the four plugin categories, but within "General plugins" the `PluginContext` exposes several distinct extension points — and Hermes also accepts extensions outside the Python plugin system (config-driven backends, shell-hooked commands, external servers, etc.). Use this table to find the right doc for what you want to build:
|
|
|
|
| Want to add… | How | Authoring guide |
|
|
|---|---|---|
|
|
| A **tool** the LLM can call | Python plugin — `ctx.register_tool()` | [Build a Hermes Plugin](/docs/guides/build-a-hermes-plugin) · [Adding Tools](/docs/developer-guide/adding-tools) |
|
|
| A **lifecycle hook** (pre/post LLM, session start/end, tool filter) | Python plugin — `ctx.register_hook()` | [Hooks reference](/docs/user-guide/features/hooks) · [Build a Hermes Plugin](/docs/guides/build-a-hermes-plugin) |
|
|
| A **slash command** for the CLI / gateway | Python plugin — `ctx.register_command()` | [Build a Hermes Plugin](/docs/guides/build-a-hermes-plugin) · [Extending the CLI](/docs/developer-guide/extending-the-cli) |
|
|
| A **subcommand** for `hermes <thing>` | Python plugin — `ctx.register_cli_command()` | [Extending the CLI](/docs/developer-guide/extending-the-cli) |
|
|
| A bundled **skill** that your plugin ships | Python plugin — `ctx.register_skill()` | [Creating Skills](/docs/developer-guide/creating-skills) |
|
|
| An **inference backend** (LLM provider: OpenAI-compat, Codex, Anthropic-Messages, Bedrock) | Provider plugin — `register_provider(ProviderProfile(...))` in `plugins/model-providers/<name>/` | **[Model Provider Plugins](/docs/developer-guide/model-provider-plugin)** · [Adding Providers](/docs/developer-guide/adding-providers) |
|
|
| A **gateway channel** (Discord / Telegram / IRC / Teams / etc.) | Platform plugin — `ctx.register_platform()` in `plugins/platforms/<name>/` | [Adding Platform Adapters](/docs/developer-guide/adding-platform-adapters) |
|
|
| A **memory backend** (Honcho, Mem0, Supermemory, …) | Memory plugin — subclass `MemoryProvider` in `plugins/memory/<name>/` | [Memory Provider Plugins](/docs/developer-guide/memory-provider-plugin) |
|
|
| A **context-compression strategy** | Context-engine plugin — `ctx.register_context_engine()` | [Context Engine Plugins](/docs/developer-guide/context-engine-plugin) |
|
|
| An **image-generation backend** (DALL·E, SDXL, …) | Backend plugin — `ctx.register_image_gen_provider()` | [Image Generation Provider Plugins](/docs/developer-guide/image-gen-provider-plugin) |
|
|
| A **TTS backend** (any CLI — Piper, VoxCPM, Kokoro, xtts, voice-cloning scripts, …) | Config-driven — declare under `tts.providers.<name>` with `type: command` in `config.yaml` | [TTS setup](/docs/user-guide/features/tts#custom-command-providers) |
|
|
| An **STT backend** (custom whisper binary, local ASR CLI) | Config-driven — set `HERMES_LOCAL_STT_COMMAND` env var to a shell template | [Voice Message Transcription (STT)](/docs/user-guide/features/tts#voice-message-transcription-stt) |
|
|
| **External tools via MCP** (filesystem, GitHub, Linear, Notion, any MCP server) | Config-driven — declare `mcp_servers.<name>` with `command:` / `url:` in `config.yaml`. Hermes auto-discovers the server's tools and registers them alongside built-ins. | [MCP](/docs/user-guide/features/mcp) |
|
|
| **Additional skill sources** (custom GitHub repos, private skill indexes) | CLI — `hermes skills tap add <repo>` | [Skills Hub](/docs/user-guide/features/skills#skills-hub) · [Publishing a custom tap](/docs/user-guide/features/skills#publishing-a-custom-skill-tap) |
|
|
| **Gateway event hooks** (fire on `gateway:startup`, `session:start`, `agent:end`, `command:*`) | Drop `HOOK.yaml` + `handler.py` into `~/.hermes/hooks/<name>/` | [Event Hooks](/docs/user-guide/features/hooks#gateway-event-hooks) |
|
|
| **Shell hooks** (run a shell command on events — notifications, audit logs, desktop alerts) | Config-driven — declare under `hooks:` in `config.yaml` | [Shell Hooks](/docs/user-guide/features/hooks#shell-hooks) |
|
|
|
|
:::note
|
|
Not everything is a Python plugin. Some extension surfaces intentionally use **config-driven shell commands** (TTS, STT, shell hooks) so any CLI you already have becomes a plugin without writing Python. Others are **external servers** (MCP) the agent connects to and auto-registers tools from. And some are **drop-in directories** (gateway hooks) with their own manifest format. Pick the right surface for the integration style that fits your use case; the authoring guides in the table above each cover placeholders, discovery, and examples.
|
|
:::
|
|
|
|
## NixOS declarative plugins
|
|
|
|
On NixOS, plugins can be installed declaratively via the module options — no `hermes plugins install` needed. See the **[Nix Setup guide](/docs/getting-started/nix-setup#plugins)** for full details.
|
|
|
|
```nix
|
|
services.hermes-agent = {
|
|
# Directory plugin (source tree with plugin.yaml)
|
|
extraPlugins = [ (pkgs.fetchFromGitHub { ... }) ];
|
|
# Entry-point plugin (pip package)
|
|
extraPythonPackages = [ (pkgs.python312Packages.buildPythonPackage { ... }) ];
|
|
# Enable in config
|
|
settings.plugins.enabled = [ "my-plugin" ];
|
|
};
|
|
```
|
|
|
|
Declarative plugins are symlinked with a `nix-managed-` prefix — they coexist with manually installed plugins and are cleaned up automatically when removed from the Nix config.
|
|
|
|
## Managing plugins
|
|
|
|
```bash
|
|
hermes plugins # unified interactive UI
|
|
hermes plugins list # table: enabled / disabled / not enabled
|
|
hermes plugins install user/repo # install from Git, then prompt Enable? [y/N]
|
|
hermes plugins install user/repo --enable # install AND enable (no prompt)
|
|
hermes plugins install user/repo --no-enable # install but leave disabled (no prompt)
|
|
hermes plugins update my-plugin # pull latest
|
|
hermes plugins remove my-plugin # uninstall
|
|
hermes plugins enable my-plugin # add to allow-list
|
|
hermes plugins disable my-plugin # remove from allow-list + add to disabled
|
|
```
|
|
|
|
### Interactive UI
|
|
|
|
Running `hermes plugins` with no arguments opens a composite interactive screen:
|
|
|
|
```
|
|
Plugins
|
|
↑↓ navigate SPACE toggle ENTER configure/confirm ESC done
|
|
|
|
General Plugins
|
|
→ [✓] my-tool-plugin — Custom search tool
|
|
[ ] webhook-notifier — Event hooks
|
|
[ ] disk-cleanup — Auto-cleanup of ephemeral files [bundled]
|
|
|
|
Provider Plugins
|
|
Memory Provider ▸ honcho
|
|
Context Engine ▸ compressor
|
|
```
|
|
|
|
- **General Plugins section** — checkboxes, toggle with SPACE. Checked = in `plugins.enabled`, unchecked = in `plugins.disabled` (explicit off).
|
|
- **Provider Plugins section** — shows current selection. Press ENTER to drill into a radio picker where you choose one active provider.
|
|
- Bundled plugins appear in the same list with a `[bundled]` tag.
|
|
|
|
Provider plugin selections are saved to `config.yaml`:
|
|
|
|
```yaml
|
|
memory:
|
|
provider: "honcho" # empty string = built-in only
|
|
|
|
context:
|
|
engine: "compressor" # default built-in compressor
|
|
```
|
|
|
|
### Enabled vs. disabled vs. neither
|
|
|
|
Plugins occupy one of three states:
|
|
|
|
| State | Meaning | In `plugins.enabled`? | In `plugins.disabled`? |
|
|
|---|---|---|---|
|
|
| `enabled` | Loaded on next session | Yes | No |
|
|
| `disabled` | Explicitly off — won't load even if also in `enabled` | (irrelevant) | Yes |
|
|
| `not enabled` | Discovered but never opted in | No | No |
|
|
|
|
The default for a newly-installed or bundled plugin is `not enabled`. `hermes plugins list` shows all three distinct states so you can tell what's been explicitly turned off vs. what's just waiting to be enabled.
|
|
|
|
In a running session, `/plugins` shows which plugins are currently loaded.
|
|
|
|
## Injecting Messages
|
|
|
|
Plugins can inject messages into the active conversation using `ctx.inject_message()`:
|
|
|
|
```python
|
|
ctx.inject_message("New data arrived from the webhook", role="user")
|
|
```
|
|
|
|
**Signature:** `ctx.inject_message(content: str, role: str = "user") -> bool`
|
|
|
|
How it works:
|
|
|
|
- If the agent is **idle** (waiting for user input), the message is queued as the next input and starts a new turn.
|
|
- If the agent is **mid-turn** (actively running), the message interrupts the current operation — the same as a user typing a new message and pressing Enter.
|
|
- For non-`"user"` roles, the content is prefixed with `[role]` (e.g. `[system] ...`).
|
|
- Returns `True` if the message was queued successfully, `False` if no CLI reference is available (e.g. in gateway mode).
|
|
|
|
This enables plugins like remote control viewers, messaging bridges, or webhook receivers to feed messages into the conversation from external sources.
|
|
|
|
:::note
|
|
`inject_message` is only available in CLI mode. In gateway mode, there is no CLI reference and the method returns `False`.
|
|
:::
|
|
|
|
See the **[full guide](/docs/guides/build-a-hermes-plugin)** for handler contracts, schema format, hook behavior, error handling, and common mistakes.
|