Skip to content

Latest commit

 

History

History
467 lines (329 loc) · 28.5 KB

File metadata and controls

467 lines (329 loc) · 28.5 KB

Hermes Agent Plugin

Remnic MemoryProvider for Hermes Agent. Provides automatic memory recall on every LLM turn and automatic observation of every response via the Hermes MemoryProvider protocol. The deepest available integration — memory is structural, not optional.

Hermes integration follows the same boundary as every other host: Remnic core owns memory semantics, while the Hermes package stays a thin adapter over Hermes' real plugin and MemoryProvider contracts.

Canonical upstream references:

Contents


Which Hermes plugin slot Remnic uses

Remnic registers as a Hermes memory provider (plugin.yaml declares kind: exclusive, the Hermes manifest kind used for provider plugins selected through memory.provider). This is the only slot Remnic occupies and the only slot it needs.

The manifest declares Hermes-supported capability metadata with provides_tools and provides_hooks. It does not declare or register a context engine.

Remnic does not, and should not, register as a Hermes context_engine. Hermes' context_engine slot replaces the built-in ContextCompressor — it controls how Hermes compresses its own outgoing conversation history before sending to the LLM. That is a different concern from external memory recall. Remnic delivers all of the following through the memory_provider hook chain (pre_llm_call → recall envelope), with no context_engine registration involved:

  • Recalled memories from the Remnic store
  • Lossless Context Management (LCM) compressed-history sections, when the Remnic daemon has lcmEnabled: true
  • Entity context, identity anchors, continuity loops, and any other recall-side enrichment served by the daemon

If a static analysis tool, AI reviewer, or third-party guide tells you "Remnic needs register_context_engine in Hermes to enable LCM," that guidance is incorrect. LCM lives on the Remnic daemon. It is delivered to Hermes via the recall response. The memory_provider hook is the correct and sufficient integration point.

A Remnic-backed ContextEngine (one that uses Remnic's LCM to compress Hermes' local history) is a possible future additive feature. It is not required for any of the capabilities Remnic exposes today.


Why MemoryProvider

MCP gives Hermes tools it can call, but the agent must decide to call them. The MemoryProvider protocol hooks into Hermes at the framework level so memory operations happen regardless of what the agent chooses to do.

Aspect MCP Only MemoryProvider
Recall Agent must call remnic_recall Automatic on every turn
Observe Agent must call remnic_store Automatic after every response
Latency Tool call overhead per turn Pre-fetched, non-blocking
Reliability Agent may omit the call Structural — cannot be skipped

The plugin also registers the remnic_* tools for cases where the agent should control recall or storage explicitly — for example, pinning a specific fact mid-session. The two approaches are complementary.


Prerequisites

  • Remnic daemon running on 127.0.0.1:4318 (configurable). See the Remnic repository for installation.
  • Hermes Agent v0.7.0 or later — the MemoryProvider protocol was introduced in v0.7.0.
  • Python 3.10 or later.

Installation

Option A: pip + CLI (recommended)

pip install --upgrade remnic-hermes
remnic connectors install hermes

remnic connectors install hermes generates an auth token, writes ~/.remnic/tokens.json, adds the remnic: block to your Hermes config.yaml, and runs a daemon health check. It does not start the daemon — if unreachable, it prints remnic daemon start as the next step. Restart Hermes after running it.

Option B: pip only (manual config)

pip install --upgrade remnic-hermes

Then add the config block manually — see Configuration reference.

Option C: editable install from source

cd packages/plugin-hermes
pip install -e ".[dev]"

Configuration reference

The plugin entry point is register(ctx) in remnic_hermes/__init__.py. It reads configuration from ctx.config["remnic"], falling back to ctx.config["engram"] if the remnic key is absent. The extracted dict is passed directly to RemnicMemoryProvider.

In Hermes config.yaml, the config block sits at the top level under a remnic: key (or engram: for legacy configs), alongside the plugins: list:

plugins:
  - remnic_hermes

remnic:
  host: "127.0.0.1"
  port: 4318
  token: ""
  session_key: ""
  timeout: 30.0

Field reference

Field Type Default Description
host string "127.0.0.1" Hostname or IP of the Remnic daemon. Overridden by REMNIC_HOST env var.
port integer 4318 TCP port of the Remnic daemon. Overridden by REMNIC_PORT env var.
token string "" Auth token for the daemon. If empty, auto-loaded from the token store (see Token bootstrap).
session_key string "" Session identifier passed on every recall/observe call. If empty, auto-generated as hermes-<12 random hex chars> at startup.
timeout float 30.0 HTTP request timeout in seconds applied to all daemon calls.

No other fields are read. Fields documented elsewhere (such as namespace, recall_top_k, recall_mode, or token_env) do not exist in this implementation.


Environment variable overrides

Environment variables are consulted only when the corresponding field is absent from the config block. Inline config values win.

Variable Overrides Notes
REMNIC_HOST remnic.host Primary
REMNIC_PORT remnic.port Primary
ENGRAM_HOST remnic.host Legacy fallback; checked when REMNIC_HOST is unset
ENGRAM_PORT remnic.port Legacy fallback; checked when REMNIC_PORT is unset

Precedence (highest to lowest): inline config field → REMNIC_* env var → ENGRAM_* env var → compiled default.

The auth token is not read from an environment variable. It comes from the inline token: field or the token store file.


Token bootstrap

Automatic (via CLI)

remnic connectors install hermes handles the full flow:

  1. Validates the Hermes profile and config directory.
  2. Generates a per-connector auth token scoped to Hermes.
  3. Adds the remnic: block to Hermes config.yaml (with rollback on failure).
  4. Commits the token to ~/.remnic/tokens.json.
  5. Writes the connector config to ~/.config/engram/.engram-connectors/connectors/hermes.json.
  6. Runs a daemon health check — reports whether the daemon is reachable but does not start it. If unreachable, install still succeeds and prints remnic daemon start as the next step.

To install into a non-default Hermes profile:

remnic connectors install hermes --config profile=Research

Manual

Write ~/.remnic/tokens.json in the following format:

{
  "tokens": [
    { "connector": "hermes", "token": "remnic_hm_...", "createdAt": "2026-01-01T00:00:00Z" }
  ]
}

The token loader searches for connector: "hermes" first, then connector: "openclaw". It also checks ~/.engram/tokens.json as a legacy fallback.

Token resolution order

  1. Inline token: field in config.yaml.
  2. connector: "hermes" entry in ~/.remnic/tokens.json.
  3. connector: "openclaw" entry in ~/.remnic/tokens.json.
  4. connector: "hermes" entry in ~/.engram/tokens.json (legacy fallback).
  5. connector: "openclaw" entry in ~/.engram/tokens.json (legacy fallback).
  6. Empty string — daemon calls will return 401 until a token is configured.

How the provider works

initialize

Called when the plugin loads. Creates an httpx.AsyncClient pointed at http://<host>:<port>/engram/v1 and issues a GET /health request. A failed health check is swallowed and treated as non-fatal — the daemon may become available later in the session. If the client is not initialized (daemon was never reachable), all subsequent hook methods return early without errors.

Note: the HTTP base path currently uses /engram/v1 because the Remnic daemon exposes a legacy surface during the v1.x compat window. This will change to /remnic/v1 once the daemon ships the dual-path rollout.

pre_llm_call

Called before every LLM request. Behavior:

  1. Scans messages in reverse to find the last message with role: "user".
  2. Skips recall entirely if the user message is absent or fewer than 3 words (whitespace-split). This avoids triggering recall on very short acknowledgments like "ok" or "thanks".
  3. Issues POST /recall with the user message as the query, sessionKey, and topK: 8. The plugin leaves recall mode unset so the daemon default can include LCM compressed-history sections when lcmEnabled: true.
  4. If the response has a non-empty context field and count > 0, returns a <remnic-memory count="N"> block that Hermes injects into the system prompt.
  5. Exceptions are swallowed; returns "" on any error so the LLM call proceeds normally.

sync_turn

Called after every agent response. Takes the full session transcript and sends the last 2 messages (user + assistant) to POST /observe. This provides near-real-time observation without the cost of replaying the entire transcript on every turn.

Exceptions are swallowed.

extract_memories

Called when the session ends. Receives a session dict; reads session["messages"] and sends the full transcript to POST /observe. This is the deep extraction pass — the daemon analyses the complete conversation for structured memory candidates.

Exceptions are swallowed.

on_session_switch

Called by Hermes when its active session id changes without tearing down the provider. If remnic.session_key is explicitly configured, Remnic preserves that stable key. If session_key is omitted and the plugin generated an ephemeral key, Remnic updates it to the new Hermes session id so recalls and observations remain scoped to the current Hermes conversation after /new, /reset, or similar session-boundary operations.

shutdown

Closes the httpx.AsyncClient. Safe to call when the client was never initialized.


Lifecycle parity audit

Audit date: 2026-04-30. Upstream Hermes reference used: NousResearch/hermes-agent commit 1d8068d (2026-04-30T12:57:02-07:00).

Remnic remains a Hermes memory provider plugin. In plugin.yaml, kind: exclusive marks the package as an exclusive provider selected by memory.provider, provides_tools enumerates the Remnic and legacy Engram tool surfaces, and provides_hooks declares the wired on_session_reset hook. The Hermes context_engine slot is still intentionally unused because it replaces Hermes' local ContextCompressor; it is not the right slot for recall, observation, heartbeat, dreams, or reset handling.

OpenClaw surface Hermes surface Status Remnic behavior
agent_heartbeat Hermes cron scheduler (cron/scheduler.py) and agent/tool jobs Equivalent but different Remnic does not register a plugin tick hook. Hermes scheduled jobs can call Remnic maintenance tools such as remnic_memory_summarize_hourly, remnic_conversation_index_update, remnic_dreams_run, and remnic_compounding_weekly_synthesize.
before_reset Plugin hooks on_session_finalize and on_session_reset, plus MemoryProvider on_session_end / on_session_switch Wired for session scoping Remnic registers an on_session_reset hook when Hermes exposes ctx.register_hook, and the provider implements on_session_switch so generated session keys follow the new Hermes session id. Stable configured session_key values are preserved.
commands.list / registerCommand ctx.register_command for in-session slash commands and ctx.register_cli_command for hermes <subcommand> commands Available, not wired Remnic exposes explicit capabilities as agent tools rather than Hermes slash/CLI commands today. The command surfaces exist upstream and can be used later for operator-style commands if there is a concrete UX need.
dreaming slot No dedicated dreaming plugin slot; Hermes uses cron/background jobs and context-engine lifecycle for compression only Equivalent but different Remnic keeps dream/consolidation semantics inside the daemon. Hermes can invoke them through remnic_dreams_status and remnic_dreams_run; recurring background execution should be modeled as a Hermes cron job, not as a context_engine.

No upstream Hermes feature request is needed from this audit: each OpenClaw lifecycle surface has either a direct Hermes plugin hook or a supported Hermes scheduling/command equivalent. The only non-equivalent detail is naming: Hermes does not have an OpenClaw-style dreaming slot, but its cron/background-task model is the correct host-native way to schedule Remnic dream work.


Tools registered

Tool name Parameters Description
remnic_recall query: string Recall memories from Remnic matching a natural language query
remnic_store content: string Store a memory in Remnic for future recall
remnic_search query: string Full-text search across all Remnic memories
remnic_lcm_search query: string, sessionKey?: string, namespace?: string, limit?: integer Search the daemon-side LCM conversation archive
remnic_recall_explain sessionKey?: string, namespace?: string Return the last recall snapshot
remnic_recall_tier_explain sessionKey?: string, namespace?: string Return tier attribution for the last direct-answer recall
remnic_recall_xray query: string, sessionKey?: string, namespace?: string, budget?: integer, `disclosure?: chunk section
remnic_memory_last_recall sessionKey?: string Fetch the memory IDs injected in the last recall
remnic_memory_intent_debug namespace?: string Inspect the latest intent/planner snapshot
remnic_memory_qmd_debug namespace?: string Inspect the latest QMD recall snapshot
remnic_memory_graph_explain namespace?: string Inspect graph recall expansion from the last recall
remnic_memory_feedback_last_recall memoryId: string, `vote: up down, note?: string`
remnic_set_coding_context sessionKey: string, `codingContext?: object null, projectTag?: string`
remnic_memory_get memoryId: string, namespace?: string Fetch one stored memory by id
remnic_memory_store content: string, sessionKey?: string, category?: string, confidence?: number, namespace?: string, tags?: string[], entityRef?: string, ttl?: string, sourceReason?: string Store a memory with the daemon's richer memory-store schema
remnic_memory_timeline memoryId: string, namespace?: string, limit?: number Fetch the timeline for one stored memory
remnic_memory_profile namespace?: string Read the user profile surface
remnic_memory_entities namespace?: string List tracked entities
remnic_memory_questions namespace?: string List open memory questions
remnic_memory_identity namespace?: string Read identity memory state
remnic_memory_promote memoryId: string, namespace?: string, sessionKey?: string Promote a memory candidate or stored memory
remnic_memory_outcome memoryId: string, `outcome: success failure, namespace?: string, sessionKey?: string, timestamp?: string`
remnic_entity_get name: string, namespace?: string Fetch one tracked entity by name
remnic_memory_capture content: string, namespace?: string, category?: string, tags?: string[], entityRef?: string, confidence?: number, ttl?: string, sourceReason?: string Capture an explicit memory note
remnic_memory_action_apply action: string, category?: string, content?: string, `outcome?: applied skipped
remnic_continuity_audit_generate `period?: weekly monthly, key?: string`
remnic_continuity_incident_open symptom: string, namespace?: string, triggerWindow?: string, suspectedCause?: string Open a continuity incident
remnic_continuity_incident_close id: string, fixApplied: string, verificationResult: string, namespace?: string, preventiveRule?: string Close a continuity incident with verification
remnic_continuity_incident_list `state?: open closed
remnic_continuity_loop_add_or_update id: string, cadence: string, purpose: string, status: string, killCondition: string Add or update a continuity improvement loop
remnic_continuity_loop_review id: string, namespace?: string, status?: string, notes?: string, reviewedAt?: string Review an existing continuity improvement loop
remnic_identity_anchor_get namespace?: string Read the identity continuity anchor
remnic_identity_anchor_update namespace?: string, identityTraits?: string, communicationPreferences?: string, operatingPrinciples?: string, continuityNotes?: string Conservatively merge identity anchor sections
remnic_review_queue_list runId?: string, namespace?: string Fetch the latest review queue artifact bundle
remnic_review_list filter?: string, namespace?: string, limit?: number List contradiction review items
remnic_review_resolve pairId: string, verb: string Resolve a contradiction review pair
remnic_suggestion_submit content: string, schemaVersion?: number, idempotencyKey?: string, dryRun?: boolean, sessionKey?: string, category?: string, confidence?: number, namespace?: string, tags?: string[], entityRef?: string, ttl?: string, sourceReason?: string Queue a suggested memory for review
remnic_work_task action: string, id?: string, title?: string, description?: string, status?: string, priority?: string, owner?: string, assignee?: string, projectId?: string, tags?: string[], dueAt?: string Manage work-layer tasks
remnic_work_project action: string, id?: string, name?: string, description?: string, status?: string, owner?: string, tags?: string[], taskId?: string, projectId?: string Manage work-layer projects
remnic_work_board action: string, projectId?: string, snapshotJson?: string, linkToMemory?: boolean Export or import work-layer board snapshots and markdown
remnic_shared_context_write_output agentId: string, title: string, content: string Write agent work product into shared context
remnic_shared_feedback_record agent: string, decision: string, reason: string Record shared feedback for peer modeling
remnic_shared_priorities_append agentId: string, text: string Append priorities notes for curator merge
remnic_shared_context_cross_signals_run date?: string Generate shared-context cross-signal artifacts
remnic_shared_context_curate_daily date?: string Generate the daily shared-context roundtable
remnic_compounding_weekly_synthesize weekId?: string Generate weekly compounding outputs
remnic_compounding_promote_candidate weekId: string, candidateId: string Promote a compounding candidate into durable memory
remnic_compression_guidelines_optimize dryRun?: boolean, eventLimit?: number Run compression-guideline policy optimization
remnic_compression_guidelines_activate expectedContentHash?: string, expectedGuidelineVersion?: number Activate a staged compression-guideline draft
remnic_memory_governance_run namespace?: string, `mode?: shadow apply, recentDays?: number, maxMemories?: number, batchSize?: number`
remnic_procedure_mining_run namespace?: string Run procedural memory mining
remnic_procedural_stats namespace?: string Read procedural memory stats
remnic_contradiction_scan_run namespace?: string Run an on-demand contradiction scan
remnic_memory_summarize_hourly none Generate hourly conversation summaries
remnic_conversation_index_update sessionKey?: string, hours?: number, embed?: boolean Update the conversation index
remnic_day_summary memories?: string, sessionKey?: string, namespace?: string Generate a structured end-of-day summary
remnic_briefing since?: string, focus?: string, namespace?: string, `format?: markdown json, maxFollowups?: number`
remnic_context_checkpoint sessionKey: string, context: string, namespace?: string Save a structured context checkpoint for a session
remnic_profiling_report `format?: ascii json, limit?: number`

Each tool handler returns the raw JSON response from the daemon or {"error": "Not connected to Remnic"} when the client is not initialized. Direct memory tools use the daemon's REST endpoints where available; debug, explain, and MCP-native memory surfaces are forwarded through the daemon MCP endpoint.

The remnic_* tools give the agent explicit control for cases where automatic recall is insufficient — for example, storing a specific fact the agent has derived mid-session, searching the LCM archive directly, inspecting why a recall result appeared, opening a continuity incident, curating stored memories, saving a checkpoint, or generating a profiling report.


Profile and session isolation

Hermes profiles live under ~/.hermes/profiles/<name>/ and each loads its own config.yaml. You can use different session_key values to keep memory contexts distinct across profiles:

# ~/.hermes/profiles/research/config.yaml
plugins:
  - remnic_hermes

remnic:
  host: "127.0.0.1"
  port: 4318
  session_key: "research"
# ~/.hermes/profiles/coding/config.yaml
plugins:
  - remnic_hermes

remnic:
  host: "127.0.0.1"
  port: 4318
  session_key: "coding"

The session_key is passed on every /recall and /observe call, so the daemon can scope retrieval to sessions with matching keys. If session_key is omitted, the provider generates a random key (hermes-<12hex>) at startup; this means recall will only find memories from the same process lifetime unless you set a stable key.

To share memories across all profiles, omit session_key in both configs and rely on the Remnic daemon's global index.


Error handling philosophy

Every MemoryProvider hook (initialize, pre_llm_call, sync_turn, extract_memories) wraps its daemon call in a bare except Exception: pass block. This is intentional: Remnic being unavailable must never break the agent. The agent continues normally; it just loses memory context for that turn or session.

This design means:

  • The daemon can be restarted mid-session without crashing Hermes.
  • Misconfigured tokens produce silent auth failures rather than agent crashes.
  • Network blips are non-fatal.

If you need to diagnose silent failures, check daemon health directly:

remnic daemon status
curl -s http://127.0.0.1:4318/engram/v1/health

Engram compat window

During the Engram to Remnic rebrand, the plugin registers legacy aliases for every Remnic tool:

Tool name Status Notes
remnic_recall Current Use for new integrations
remnic_store Current Use for new integrations
remnic_search Current Use for new integrations
remnic_lcm_search Current Use for new integrations
engram_recall Legacy alias Routes to the same handler as remnic_recall
engram_store Legacy alias Routes to the same handler as remnic_store
engram_search Legacy alias Routes to the same handler as remnic_search
engram_lcm_search Legacy alias Routes to the same handler as remnic_lcm_search

The legacy tool schemas deliberately describe themselves as "Engram" tools (e.g., "Recall memories from Engram..."). This is intentional: when a language model surfaces the engram_* names, the description must agree with the name so the model does not confuse the two tool sets. Do not update these descriptions to say "Remnic".

The Python class aliases EngramMemoryProvider, EngramClient, and EngramHermesConfig are preserved for import-path compatibility and will be removed in a future major release.

The engram: config block is also still accepted as a fallback. If your config.yaml has engram: instead of remnic:, everything works without changes.


Troubleshooting

"MemoryProvider remnic failed to initialize" or 401 errors

The auth token is missing or invalid. Re-run the connector install to regenerate it:

remnic connectors install hermes
cat ~/.remnic/tokens.json    # verify a hermes entry exists

Daemon not running

remnic daemon status
remnic daemon install        # installs and starts the launchd/systemd service

Verify the HTTP surface is responding:

curl -s http://127.0.0.1:4318/engram/v1/health

ModuleNotFoundError: No module named 'remnic_hermes'

The package is not installed in the Python environment Hermes uses:

which python && pip show remnic-hermes
hermes --version

Install into the correct environment: <path-to-hermes-python> -m pip install --upgrade remnic-hermes.

Memories not appearing in context

  1. Confirm the daemon is healthy: remnic daemon status.
  2. Confirm the query is at least 3 words — pre_llm_call skips recall for shorter messages.
  3. Confirm the token is valid: a 401 is swallowed silently, so daemon health does not catch it.
  4. Use the explicit tool to test the round-trip: call remnic_recall with a query. If it returns {"error": "Not connected to Remnic"}, initialize never completed successfully.

Memories from a previous session are not recalled

If session_key is not set, a new random key is generated each startup. Set a stable session_key in the config if you want cross-session recall to scope correctly:

remnic:
  session_key: "my-agent"

Or leave it blank to rely on the Remnic daemon's global search (the daemon indexes all sessions, but sessionKey may affect ranking).


Migration notes (Engram era)

If you are upgrading from a configuration that used the engram-hermes package or an engram: config block:

  1. pip install --upgrade remnic-hermes replaces engram-hermes. Uninstall the old package first: pip uninstall engram-hermes.
  2. Your config.yaml engram: block continues to work without changes. You can rename it to remnic: at any time — both are accepted.
  3. Tool calls to engram_recall, engram_store, and engram_search continue to work. No Hermes system prompt or tool-list changes are required.
  4. Python imports of EngramMemoryProvider, EngramClient, and EngramHermesConfig continue to resolve.
  5. When you are ready to fully migrate: rename engram: to remnic: in config.yaml and update any explicit tool references to remnic_*.

Uninstall

pip uninstall remnic-hermes
remnic connectors remove hermes

remnic connectors remove hermes revokes the token and removes the remnic: block from Hermes config.yaml.