Memory Provider Plugins
إضافات مزوّد الذاكرة
What this page is, and what it holds.
This page covers Memory Provider Plugins. You will use hermes memory setup and hermes plugins here; about 8 minutes to read. The priciest model is not always best for your task. Compare on one task and set a spend cap.
How to build a memory provider plugin for Hermes Agent
Outcomes taken from this page, not a template.
- Understand what المزوّد والنموذج is and when you need it.
- Run
hermes memory setupandhermes pluginsand understand what happens next. - Read the table and take only the row that applies to you.
- Set
HERMES_HOMEin the right place.
Exactly as they appear in Hermes.
hermes memory setuphermes pluginshermes my-provider confighermes my-provider status
HERMES_HOMESKILLS_DIR
Jump to the part you need.
Nothing summarised away.
The documentation body below is reproduced from the official source so commands and identifiers stay exact. Each section carries a short note describing what it contains.
Memory provider plugins give Hermes Agent persistent, cross-session knowledge beyond the built-in MEMORY.md and USER.md. This guide covers how to build one.
Installation Layouts
Ordered, practical steps. Run one and confirm it worked before moving on.
Hermes discovers memory providers from four sources, in this precedence order:
| Source | Location | Notes |
|---|---|---|
| Bundled | plugins/memory/<name>/ | Ships with Hermes. Closed to new providers — see CONTRIBUTING ↗. |
| User | $HERMES_HOME/plugins/<name>/ | Dropped in by the user, per profile. |
| Project | ./.hermes/plugins/<name>/ | Opt-in via HERMES_ENABLE_PROJECT_PLUGINS=1. |
| Package | hermes_agent.memory_providers entry point | pip install, nothing to copy. |
Earlier sources win on a name collision, so a directory dropped into a working tree can never shadow a shipped provider.
Discovery only enumerates — it never imports a provider. Nothing runs until
memory.provider names it.
Directory Provider
A directory provider lives in plugins/memory/<name>/ when bundled with
Hermes, in $HERMES_HOME/plugins/<name>/ when installed by a user, or in
./.hermes/plugins/<name>/ for a project-local one:
plugins/memory/my-provider/
├── __init__.py # MemoryProvider implementation + register() entry point
├── plugin.yaml # Metadata (name, description, hooks)
└── README.md # Setup instructions, config reference, toolsPackaged Provider
A pip-installed provider publishes an entry point in the
hermes_agent.memory_providers group. The entry-point name is the provider
name users select in memory.provider; its value points to the provider's
register(ctx) function:
[project.entry-points."hermes_agent.memory_providers"]
my-provider = "my_provider:register"Point the entry point at the package, or at a register(ctx) inside it, and
keep your implementation, skills, and other resources in the normal Python
package layout. No copy under $HERMES_HOME/plugins/ is required.
A package entry point gets everything a directory install does, including the
two files Hermes reads from disk rather than importing — config_schema.py
(the dashboard config panel) and cli.py (your hermes <provider>
subcommands). Both are found next to your package's __init__.py, so point the
entry point at a package rather than a single module if you ship either.
The MemoryProvider ABC
Settings you configure once. Change one at a time so you can see what each does. Set HERMES_HOME in your environment, not in the chat.
Your plugin implements the MemoryProvider abstract base class from agent/memory_provider.py:
from agent.memory_provider import MemoryProvider
class MyMemoryProvider(MemoryProvider):
@property
def name(self) -> str:
return "my-provider"
def is_available(self) -> bool:
"""Check if this provider can activate. NO network calls."""
return bool(os.environ.get("MY_API_KEY"))
def initialize(self, session_id: str, **kwargs) -> None:
"""Called once at agent startup.
kwargs always includes:
hermes_home (str): Active HERMES_HOME path. Use for storage.
"""
self._api_key = os.environ.get("MY_API_KEY", "")
self._session_id = session_id
# ... implement remaining methodsRequired Methods
A lookup table. Do not read it all; find the row that applies to you.
Core Lifecycle
| Method | When Called | Must Implement? |
|---|---|---|
name (property) | Always | Yes |
is_available() | Agent init, before activation | Yes — no network calls |
initialize(session_id, **kwargs) | Agent startup | Yes |
get_tool_schemas() | After init, for tool injection | Yes |
handle_tool_call(tool_name, args, **kwargs) | When agent uses your tools | Yes (if you have tools) |
Config
| Method | Purpose | Must Implement? |
|---|---|---|
get_config_schema() | Declare config fields for hermes memory setup | Yes |
save_config(values, hermes_home) | Write non-secret config to native location | Yes (unless env-var-only) |
Optional Hooks
| Method | When Called | Use Case |
|---|---|---|
system_prompt_block() | System prompt assembly | Static provider info |
prefetch(query, *, session_id="") | Before each API call | Return recalled context |
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 |
shutdown() | Process exit | Clean up connections |
Config Schema
Commands you type in a terminal. Understand what one does before copying it. Commands here: hermes memory setup.
get_config_schema() returns a list of field descriptors used by hermes memory setup:
def get_config_schema(self):
return [
{
"key": "api_key",
"description": "My Provider API key",
"secret": True, # → written to .env
"required": True,
"env_var": "MY_API_KEY", # explicit env var name
"url": "https://my-provider.com/keys", # where to get it
},
{
"key": "region",
"description": "Server region",
"default": "us-east",
"choices": ["us-east", "eu-west", "ap-south"],
},
{
"key": "project",
"description": "Project identifier",
"default": "hermes",
},
]Fields with secret: True and env_var go to .env. Non-secret fields are passed to save_config().
Save Config
Explains the idea itself. Read it slowly; the later sections build on it.
def save_config(self, values: dict, hermes_home: str) -> None:
"""Write non-secret config to your native location."""
import json
from pathlib import Path
config_path = Path(hermes_home) / "my-provider.json"
config_path.write_text(json.dumps(values, indent=2))For env-var-only providers, leave the default no-op.
Plugin Entry Point
Settings you configure once. Change one at a time so you can see what each does. Set SKILLS_DIR in your environment, not in the chat.
def register(ctx) -> None:
"""Called by the memory plugin discovery system."""
ctx.register_memory_provider(MyMemoryProvider())A provider may also expose read-only skills from the same callback. Skills are qualified by the entry-point name and are loaded only when that memory provider is active:
from pathlib import Path
SKILLS_DIR = Path(__file__).parent / "skills"
def register(ctx) -> None:
ctx.register_memory_provider(MyMemoryProvider())
ctx.register_skill(
"maintenance",
SKILLS_DIR / "maintenance" / "SKILL.md",
"Maintain the provider's memory store",
)With the my-provider entry point active, the skill is available as
my-provider:maintenance through skill_view().
plugin.yaml
Settings you configure once. Change one at a time so you can see what each does.
name: my-provider
version: 1.0.0
description: "Short description of what this provider does."
hooks:
- on_session_end # list hooks you implementThreading Contract
Explains the idea itself. Read it slowly; the later sections build on it.
sync_turn() MUST be non-blocking. If your backend has latency (API calls, LLM processing), run the work in a daemon thread:
def sync_turn(self, user_content, assistant_content, *, session_id="", messages=None):
def _sync():
try:
self._api.ingest(user_content, assistant_content, session_id=session_id, messages=messages)
except Exception as e:
logger.warning("Sync failed: %s", e)
if self._sync_thread and self._sync_thread.is_alive():
self._sync_thread.join(timeout=5.0)
self._sync_thread = threading.Thread(target=_sync, daemon=True)
self._sync_thread.start()messages is optional OpenAI-style conversation context as of the completed
turn. When present, it includes user/assistant messages, assistant tool calls,
and tool result messages. Providers that do not need raw turn context can omit
the messages parameter; Hermes will continue calling them with the legacy
signature.
Cloud providers should document what parts of messages are sent off-device.
Tool calls and tool results may contain file paths, command output, or other
workspace data.
Profile Isolation
Explains the idea itself. Read it slowly; the later sections build on it.
All storage paths must use the hermes_home kwarg from initialize(), not hardcoded ~/.hermes:
# CORRECT — profile-scoped
from hermes_constants import get_hermes_home
data_dir = get_hermes_home() / "my-provider"
# WRONG — shared across all profiles
data_dir = Path("~/.hermes/my-provider").expanduser()Testing
Explains the idea itself. Read it slowly; the later sections build on it.
See tests/agent/test_memory_provider.py and adjacent memory tests (tests/agent/test_memory_session_switch.py, tests/agent/test_memory_user_id.py, tests/run_agent/test_memory_provider_init.py) for end-to-end patterns.
from agent.memory_manager import MemoryManager
mgr = MemoryManager()
mgr.add_provider(my_provider)
mgr.initialize_all(session_id="test-1", platform="cli")
# Test tool routing
result = mgr.handle_tool_call("my_tool", {"action": "add", "content": "test"})
# Test lifecycle
mgr.sync_all("user msg", "assistant msg")
mgr.on_session_end([])
mgr.shutdown_all()Adding CLI Commands
Commands you type in a terminal. Understand what one does before copying it. Commands here: hermes my-provider config, hermes my-provider status.
Memory provider plugins can register their own CLI subcommand tree (e.g. hermes my-provider status, hermes my-provider config). This uses a convention-based discovery system — no changes to core files needed.
How it works
- Add a
cli.pyfile to your plugin directory - Define a
register_cli(subparser)function that builds the argparse tree - The memory plugin system discovers it at startup via
discover_plugin_cli_commands() - Your commands appear under
hermes <provider-name> <subcommand>
Active-provider gating: Your CLI commands only appear when your provider is the active memory.provider in config. If a user hasn't configured your provider, your commands won't show in hermes --help.
Example
# plugins/memory/my-provider/cli.py
def my_command(args):
"""Handler dispatched by argparse."""
sub = getattr(args, "my_command", None)
if sub == "status":
print("Provider is active and connected.")
elif sub == "config":
print("Showing config...")
else:
print("Usage: hermes my-provider <status|config>")
def register_cli(subparser) -> None:
"""Build the hermes my-provider argparse tree.
Called by discover_plugin_cli_commands() at argparse setup time.
"""
subs = subparser.add_subparsers(dest="my_command")
subs.add_parser("status", help="Show provider status")
subs.add_parser("config", help="Show provider config")
subparser.set_defaults(func=my_command)Reference implementation
See plugins/memory/honcho/cli.py for a full example with 13 subcommands, cross-profile management (--target-profile), and config read/write.
Directory structure with CLI
plugins/memory/my-provider/
├── __init__.py # MemoryProvider implementation + register()
├── plugin.yaml # Metadata
├── cli.py # register_cli(subparser) — CLI commands
└── README.md # Setup instructionsSingle Provider Rule
Explains the idea itself. Read it slowly; the later sections build on it.
Only one external memory provider can be active at a time. If a user tries to register a second, the MemoryManager rejects it with a warning. This prevents tool schema bloat and conflicting backends.
4 questions answered by this page alone.
Every option is a real identifier from the Hermes documentation. The wrong ones are real too, just from other pages.