From 30bdc2cdc18050d4d8c9674f20bfd20bf634289f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Boschi?= Date: Wed, 25 Mar 2026 14:46:18 +0100 Subject: [PATCH 1/3] feat(hermes): file-based config + updated docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the old dataclass/configure() singleton with a plain dict config loaded from ~/.hindsight/hermes.json — same field names and conventions as the openclaw and claude-code integrations. Loading order: defaults → config file → env var overrides. - config.py: rewritten with load_config() returning a plain dict, DEFAULTS matching openclaw/claude-code fields, ENV_OVERRIDES with typed casting - tools.py: register() uses load_config() instead of raw env vars - __init__.py: clean exports (removed configure/get_config/reset_config) - README.md: full rewrite with config file examples, tables by category - docs/hermes.md: full rewrite with quick start, architecture, all config tables, gateway section, troubleshooting - tests: updated for new config pattern, 46 tests pass --- .../docs/sdks/integrations/hermes.md | 280 +++++++---------- hindsight-integrations/hermes/README.md | 296 ++++++------------ .../hermes/hindsight_hermes/__init__.py | 20 +- .../hermes/hindsight_hermes/config.py | 238 +++++++++----- .../hermes/hindsight_hermes/tools.py | 48 +-- .../hermes/tests/test_config.py | 151 +++++---- .../hermes/tests/test_tools.py | 24 +- 7 files changed, 508 insertions(+), 549 deletions(-) diff --git a/hindsight-docs/docs/sdks/integrations/hermes.md b/hindsight-docs/docs/sdks/integrations/hermes.md index 68f2d97c1..af48eabba 100644 --- a/hindsight-docs/docs/sdks/integrations/hermes.md +++ b/hindsight-docs/docs/sdks/integrations/hermes.md @@ -4,216 +4,172 @@ sidebar_position: 10 # Hermes Agent -Hindsight memory integration for [Hermes Agent](https://github.com/NousResearch/hermes-agent). Gives your Hermes agent persistent long-term memory via retain, recall, and reflect tools. +Persistent long-term memory for [Hermes Agent](https://github.com/NousResearch/hermes-agent) using [Hindsight](https://vectorize.io/hindsight). Automatically recalls relevant context before every LLM call and retains conversations for future sessions — plus explicit retain/recall/reflect tools. -## What it does - -This package registers three tools into Hermes via its plugin system: - -- **`hindsight_retain`** — Stores information to long-term memory. Hermes calls this when the user shares facts, preferences, or anything worth remembering. -- **`hindsight_recall`** — Searches long-term memory for relevant information. Returns a numbered list of matching memories. -- **`hindsight_reflect`** — Synthesizes a thoughtful answer from stored memories. Use this when you want Hermes to reason over what it knows rather than return raw facts. - -These tools appear under the `[hindsight]` toolset in Hermes's `/tools` list. - -## Setup - -### 1. Install hindsight-hermes into the Hermes venv - -The package must be installed in the **same Python environment** that Hermes runs in, so the entry point is discoverable. +## Quick Start ```bash +# 1. Install the plugin into Hermes's Python environment uv pip install hindsight-hermes --python $HOME/.hermes/hermes-agent/venv/bin/python + +# 2. Configure (choose one) +# Option A: Config file (recommended) +mkdir -p ~/.hindsight +cat > ~/.hindsight/hermes.json << 'EOF' +{ + "hindsightApiUrl": "http://localhost:9077", + "bankId": "hermes" +} +EOF + +# Option B: Environment variables +export HINDSIGHT_API_URL=http://localhost:9077 +export HINDSIGHT_BANK_ID=hermes + +# 3. Start Hermes — the plugin activates automatically +hermes ``` -### 2. Set environment variables +## Features -The plugin reads its configuration from environment variables. Set these before launching Hermes: +- **Auto-recall** — on every turn, queries Hindsight for relevant memories and injects them into the system prompt (via `pre_llm_call` hook) +- **Auto-retain** — after every response, retains the user/assistant exchange to Hindsight (via `post_llm_call` hook) +- **Explicit tools** — `hindsight_retain`, `hindsight_recall`, `hindsight_reflect` for direct model control +- **Config file** — `~/.hindsight/hermes.json` with the same field names as openclaw and claude-code integrations +- **Zero config overhead** — env vars still work as overrides for CI/automation -```bash -# Required — tells the plugin where Hindsight is running -export HINDSIGHT_API_URL=http://localhost:8888 +:::note +The lifecycle hooks (`pre_llm_call`/`post_llm_call`) require hermes-agent with [PR #2823](https://github.com/NousResearch/hermes-agent/pull/2823) or later. On older versions, only the three tools are registered — hooks are silently skipped. +::: -# Required — the memory bank to read/write. Think of this as a "brain" for one user or agent. -export HINDSIGHT_BANK_ID=my-agent +## Architecture -# Optional — only needed if using Hindsight Cloud (https://api.hindsight.vectorize.io) -export HINDSIGHT_API_KEY=your-api-key +The plugin registers via Hermes's `hermes_agent.plugins` entry point system: -# Optional — recall budget: low (fast), mid (default), high (thorough) -export HINDSIGHT_BUDGET=mid -``` +| Component | Purpose | +|-----------|---------| +| `pre_llm_call` hook | **Auto-recall** — query memories, inject as ephemeral system prompt context | +| `post_llm_call` hook | **Auto-retain** — store user/assistant exchange to Hindsight | +| `hindsight_retain` tool | Explicit memory storage (model-initiated) | +| `hindsight_recall` tool | Explicit memory search (model-initiated) | +| `hindsight_reflect` tool | LLM-synthesized answer from stored memories | -If neither `HINDSIGHT_API_URL` nor `HINDSIGHT_API_KEY` is set, the plugin silently skips registration — Hermes starts normally without the Hindsight tools. +## Connection Modes -### 3. Disable Hermes's built-in memory tool +### 1. External API (recommended for production) -Hermes has its own `memory` tool that saves to local files (`~/.hermes/`). If both are active, the LLM tends to prefer the built-in one since it's familiar. Disable it so the LLM uses Hindsight instead: +Connect to a running Hindsight server (cloud or self-hosted). No local LLM needed — the server handles fact extraction. -```bash -hermes tools disable memory +```json +{ + "hindsightApiUrl": "https://your-hindsight-server.com", + "hindsightApiToken": "your-token", + "bankId": "hermes" +} ``` -This persists across sessions. You can re-enable it later with `hermes tools enable memory`. - -### 4. Start Hindsight API +### 2. Local Daemon -Follow the [Quick Start](/developer/api/quickstart) guide to get the Hindsight API running, then come back here. +If you're running `hindsight-embed` locally, point to it: -### 5. Launch Hermes - -```bash -hermes +```json +{ + "hindsightApiUrl": "http://localhost:9077", + "bankId": "hermes" +} ``` -Verify the plugin loaded by typing `/tools` — you should see: +Follow the [Quick Start](/developer/api/quickstart) guide to get the Hindsight API running. -``` -[hindsight] - * hindsight_recall - Search long-term memory for relevant information. - * hindsight_reflect - Synthesize a thoughtful answer from long-term memories. - * hindsight_retain - Store information to long-term memory for later retrieval. -``` +## Configuration -### 6. Test it +All settings are in `~/.hindsight/hermes.json`. Every setting can also be overridden via environment variables (env vars take priority). -**Store a memory:** -> Remember that my favourite colour is red +### Connection & Daemon -You should see `⚡ hindsight` in the response, confirming it called `hindsight_retain`. +| Setting | Default | Env Var | Description | +|---------|---------|---------|-------------| +| `hindsightApiUrl` | — | `HINDSIGHT_API_URL` | Hindsight API URL | +| `hindsightApiToken` | `null` | `HINDSIGHT_API_TOKEN` / `HINDSIGHT_API_KEY` | Auth token for API | +| `apiPort` | `9077` | `HINDSIGHT_API_PORT` | Port for local Hindsight daemon | +| `daemonIdleTimeout` | `0` | `HINDSIGHT_DAEMON_IDLE_TIMEOUT` | Seconds before idle daemon shuts down (0 = never) | +| `embedVersion` | `"latest"` | `HINDSIGHT_EMBED_VERSION` | `hindsight-embed` version for `uvx` | -**Recall a memory:** -> What's my favourite colour? +### LLM Provider (daemon mode only) -**Reflect on memories:** -> Based on what you know about me, suggest a colour scheme for my IDE +| Setting | Default | Env Var | Description | +|---------|---------|---------|-------------| +| `llmProvider` | auto-detect | `HINDSIGHT_LLM_PROVIDER` | LLM provider: `openai`, `anthropic`, `gemini`, `groq`, `ollama` | +| `llmModel` | provider default | `HINDSIGHT_LLM_MODEL` | Model override | -This calls `hindsight_reflect`, which synthesizes a response from all stored memories. +### Memory Bank -**Verify via API:** +| Setting | Default | Env Var | Description | +|---------|---------|---------|-------------| +| `bankId` | — | `HINDSIGHT_BANK_ID` | Memory bank ID | +| `bankMission` | `""` | `HINDSIGHT_BANK_MISSION` | Agent identity/purpose for the memory bank | +| `retainMission` | `null` | — | Custom retain mission (what to extract from conversations) | +| `bankIdPrefix` | `""` | — | Prefix for all bank IDs | -```bash -curl -s http://localhost:8888/v1/default/banks/my-agent/memories/recall \ - -H "Content-Type: application/json" \ - -d '{"query": "favourite colour", "budget": "low"}' | python3 -m json.tool -``` +### Auto-Recall -## Troubleshooting +| Setting | Default | Env Var | Description | +|---------|---------|---------|-------------| +| `autoRecall` | `true` | `HINDSIGHT_AUTO_RECALL` | Enable automatic memory recall via `pre_llm_call` hook | +| `recallBudget` | `"mid"` | `HINDSIGHT_RECALL_BUDGET` | Recall effort: `low`, `mid`, `high` | +| `recallMaxTokens` | `4096` | `HINDSIGHT_RECALL_MAX_TOKENS` | Max tokens in recall response | +| `recallMaxQueryChars` | `800` | `HINDSIGHT_RECALL_MAX_QUERY_CHARS` | Max chars of user message used as query | +| `recallPromptPreamble` | see below | — | Header text injected before recalled memories | -### Tools don't appear in `/tools` +Default preamble: +> Relevant memories from past conversations (prioritize recent when conflicting). Only use memories that are directly useful to continue this conversation; ignore the rest: -1. **Check the plugin is installed in the right venv.** Run this from the Hermes venv: - ```bash - python -c "from hindsight_hermes import register; print('OK')" - ``` +### Auto-Retain -2. **Check the entry point is registered:** - ```bash - python -c " - import importlib.metadata - eps = importlib.metadata.entry_points(group='hermes_agent.plugins') - print(list(eps)) - " - ``` - You should see `EntryPoint(name='hindsight', value='hindsight_hermes', group='hermes_agent.plugins')`. +| Setting | Default | Env Var | Description | +|---------|---------|---------|-------------| +| `autoRetain` | `true` | `HINDSIGHT_AUTO_RETAIN` | Enable automatic retention via `post_llm_call` hook | +| `retainEveryNTurns` | `1` | — | Retain every Nth turn | +| `retainOverlapTurns` | `2` | — | Extra overlap turns for continuity | +| `retainRoles` | `["user", "assistant"]` | — | Which message roles to retain | -3. **Check env vars are set.** The plugin skips registration silently if `HINDSIGHT_API_URL` and `HINDSIGHT_API_KEY` are both unset. +### Miscellaneous -### Hermes uses built-in memory instead of Hindsight +| Setting | Default | Env Var | Description | +|---------|---------|---------|-------------| +| `debug` | `false` | `HINDSIGHT_DEBUG` | Enable debug logging to stderr | -Run `hermes tools disable memory` and restart. The built-in `memory` tool and Hindsight tools have overlapping purposes — the LLM will prefer whichever it's more familiar with, which is usually the built-in one. +## Hermes Gateway (Telegram, Discord, Slack) -### Bank not found errors +When using Hermes in gateway mode (multi-platform messaging), the plugin works across all platforms. Hermes creates a fresh `AIAgent` per message, and the plugin's `pre_llm_call` hook ensures relevant memories are recalled for each turn regardless of platform. -The plugin auto-creates banks on first use. If you see bank errors, check that the Hindsight API is running and `HINDSIGHT_API_URL` is correct. +## Disabling Hermes's Built-in Memory -### Connection refused +Hermes has a built-in `memory` tool that saves to local markdown files. If both are active, the LLM may prefer the built-in one. Disable it: -Make sure the Hindsight API is running and listening on the URL you configured. Test with: ```bash -curl http://localhost:8888/health -``` - -## Manual registration (advanced) - -If you don't want to use the plugin system, you can register tools directly in a Hermes startup script or custom agent: - -```python -from hindsight_hermes import register_tools - -register_tools( - bank_id="my-agent", - hindsight_api_url="http://localhost:8888", - budget="mid", - tags=["hermes"], # applied to all retained memories - recall_tags=["hermes"], # filter recall to only these tags -) -``` - -This imports `tools.registry` from Hermes at call time and registers the three tools directly. This approach gives you more control over parameters but requires Hermes to be importable. - -## Memory instructions (system prompt injection) - -Pre-recall memories at startup and inject them into the system prompt, so the agent starts every conversation with relevant context: - -```python -from hindsight_hermes import memory_instructions - -context = memory_instructions( - bank_id="my-agent", - hindsight_api_url="http://localhost:8888", - query="user preferences and important context", - budget="low", - max_results=5, -) -# Returns: -# Relevant memories: -# 1. User's favourite colour is red -# 2. User prefers dark mode +hermes tools disable memory ``` -This never raises — if the API is down or no memories exist, it returns an empty string. - -## Global configuration (advanced) - -Instead of passing parameters to every call, configure once: +Re-enable later with `hermes tools enable memory`. -```python -from hindsight_hermes import configure +## Troubleshooting -configure( - hindsight_api_url="http://localhost:8888", - api_key="your-key", - budget="mid", - tags=["hermes"], -) +**Plugin not loading**: Verify the entry point is registered: +```bash +python -c " +import importlib.metadata +eps = importlib.metadata.entry_points(group='hermes_agent.plugins') +print(list(eps)) +" ``` +You should see `EntryPoint(name='hindsight', value='hindsight_hermes', ...)`. -Subsequent calls to `register_tools()` or `memory_instructions()` will use these defaults if no explicit values are provided. - -## MCP alternative +**Tools don't appear in `/tools`**: Check that `hindsightApiUrl` (or `HINDSIGHT_API_URL`) is set. The plugin silently skips registration when unconfigured. -Hermes also supports MCP servers natively. You can use Hindsight's MCP server directly instead of this plugin — no `hindsight-hermes` package needed: - -```yaml -# In your Hermes config -mcp_servers: - - name: hindsight - url: http://localhost:8888/mcp +**Connection refused**: Verify the Hindsight API is running: +```bash +curl http://localhost:9077/health ``` -This exposes the same retain/recall/reflect operations through Hermes's MCP integration. The tradeoff is that MCP tools may have different naming and the LLM needs to discover them, whereas the plugin registers tools with Hermes-native schemas. - -## Configuration reference - -| Parameter | Env Var | Default | Description | -|-----------|---------|---------|-------------| -| `hindsight_api_url` | `HINDSIGHT_API_URL` | `https://api.hindsight.vectorize.io` | Hindsight API URL | -| `api_key` | `HINDSIGHT_API_KEY` | — | API key for authentication | -| `bank_id` | `HINDSIGHT_BANK_ID` | — | Memory bank ID | -| `budget` | `HINDSIGHT_BUDGET` | `mid` | Recall budget (low/mid/high) | -| `max_tokens` | — | `4096` | Max tokens for recall results | -| `tags` | — | — | Tags applied when storing memories | -| `recall_tags` | — | — | Tags to filter recall results | -| `recall_tags_match` | — | `any` | Tag matching mode (any/all/any_strict/all_strict) | -| `toolset` | — | `hindsight` | Hermes toolset group name | +**Recall returning no memories**: Memories need at least one retain cycle. Try storing a fact first, then asking about it in a new session. diff --git a/hindsight-integrations/hermes/README.md b/hindsight-integrations/hermes/README.md index 842c2b608..afd2f6696 100644 --- a/hindsight-integrations/hermes/README.md +++ b/hindsight-integrations/hermes/README.md @@ -1,247 +1,133 @@ # hindsight-hermes -Hindsight memory integration for [Hermes Agent](https://github.com/NousResearch/hermes-agent). Gives your Hermes agent persistent long-term memory via retain, recall, and reflect tools. +Persistent long-term memory for [Hermes Agent](https://github.com/NousResearch/hermes-agent) using [Hindsight](https://vectorize.io/hindsight). Automatically recalls relevant context before every LLM call and retains conversations for future sessions. -## What it does - -**Automatic memory on every turn** — no tool calls required: - -- **`pre_llm_call` hook** — Before each LLM call, recalls relevant memories and injects them into the system prompt. The model sees cross-session context automatically. -- **`post_llm_call` hook** — After each turn, retains the user/assistant exchange so it can be recalled in future sessions. - -**Three explicit tools** for when the model wants direct control: - -- **`hindsight_retain`** — Stores information to long-term memory. Hermes calls this when the user shares facts, preferences, or anything worth remembering. -- **`hindsight_recall`** — Searches long-term memory for relevant information. Returns a numbered list of matching memories. -- **`hindsight_reflect`** — Synthesizes a thoughtful answer from stored memories. Use this when you want Hermes to reason over what it knows rather than return raw facts. - -These tools appear under the `[hindsight]` toolset in Hermes's `/tools` list. - -> **Note:** The lifecycle hooks require hermes-agent with [PR #2823](https://github.com/NousResearch/hermes-agent/pull/2823) or later. On older versions, only the tools are registered — hooks are silently skipped. - -## Setup - -### 1. Install hindsight-hermes into the Hermes venv - -The package must be installed in the **same Python environment** that Hermes runs in, so the entry point is discoverable. +## Quick Start ```bash -# If you installed Hermes from source: -cd /path/to/hermes-agent -source .venv/bin/activate -pip install hindsight-hermes - -# Or from a local checkout: -pip install -e /path/to/hindsight-integrations/hermes +# 1. Install into Hermes's Python environment +uv pip install hindsight-hermes --python $HOME/.hermes/hermes-agent/venv/bin/python + +# 2. Configure +mkdir -p ~/.hindsight +cat > ~/.hindsight/hermes.json << 'EOF' +{ + "hindsightApiUrl": "http://localhost:9077", + "bankId": "hermes" +} +EOF + +# 3. Start Hermes — the plugin activates automatically +hermes ``` -### 2. Set environment variables - -The plugin reads its configuration from environment variables. Set these before launching Hermes: - -```bash -# Required — tells the plugin where Hindsight is running -export HINDSIGHT_API_URL=http://localhost:8888 - -# Required — the memory bank to read/write. Think of this as a "brain" for one user or agent. -export HINDSIGHT_BANK_ID=my-agent - -# Optional — only needed if using Hindsight Cloud (https://api.hindsight.vectorize.io) -export HINDSIGHT_API_KEY=your-api-key +## What it does -# Optional — recall budget: low (fast), mid (default), high (thorough) -export HINDSIGHT_BUDGET=mid -``` +**Automatic memory on every turn** (via Hermes lifecycle hooks): -If neither `HINDSIGHT_API_URL` nor `HINDSIGHT_API_KEY` is set, the plugin silently skips registration — Hermes starts normally without the Hindsight tools. +- **`pre_llm_call`** — Recalls relevant memories and injects them into the system prompt. The model sees cross-session context automatically, no tool call needed. +- **`post_llm_call`** — Retains the user/assistant exchange so it can be recalled in future sessions. -### 3. Disable Hermes's built-in memory tool +**Three explicit tools** (via Hermes plugin system): -Hermes has its own `memory` tool that saves to local files (`~/.hermes/`). If both are active, the LLM tends to prefer the built-in one since it's familiar. Disable it so the LLM uses Hindsight instead: +- **`hindsight_retain`** — Store information to long-term memory +- **`hindsight_recall`** — Search long-term memory for relevant information +- **`hindsight_reflect`** — Synthesize a reasoned answer from stored memories -```bash -hermes tools disable memory -``` +> The lifecycle hooks require hermes-agent with [PR #2823](https://github.com/NousResearch/hermes-agent/pull/2823) or later. On older versions, only the tools are registered — hooks are silently skipped. -This persists across sessions. You can re-enable it later with `hermes tools enable memory`. +## Configuration -### 4. Start Hindsight API +All settings live in `~/.hindsight/hermes.json`. Environment variables override file values. -In a separate terminal, start the Hindsight API server: +Same field names as the [openclaw](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/openclaw) and [claude-code](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/claude-code) integrations. -```bash -# From the hindsight repo -cd /path/to/hindsight-main -.venv/bin/hindsight-api +### Example config -# Or if using Hindsight Cloud, skip this — just point HINDSIGHT_API_URL -# to https://api.hindsight.vectorize.io +```json +{ + "hindsightApiUrl": "http://localhost:9077", + "bankId": "hermes", + "autoRecall": true, + "autoRetain": true, + "recallBudget": "mid", + "recallMaxTokens": 4096, + "bankMission": "Focus on user preferences, project context, and technical decisions." +} ``` -Wait for the health check to pass: +### Connection -```bash -curl http://localhost:8888/health -# {"status":"healthy","database":"connected"} -``` +| Setting | Env Var | Default | Description | +|---------|---------|---------|-------------| +| `hindsightApiUrl` | `HINDSIGHT_API_URL` | — | Hindsight API URL | +| `hindsightApiToken` | `HINDSIGHT_API_TOKEN` / `HINDSIGHT_API_KEY` | — | Auth token | +| `apiPort` | `HINDSIGHT_API_PORT` | `9077` | Local daemon port | +| `daemonIdleTimeout` | `HINDSIGHT_DAEMON_IDLE_TIMEOUT` | `0` | Idle shutdown (seconds, 0 = never) | +| `embedVersion` | `HINDSIGHT_EMBED_VERSION` | `"latest"` | `hindsight-embed` version | -### 5. Launch Hermes +### Memory Bank -```bash -hermes -``` +| Setting | Env Var | Default | Description | +|---------|---------|---------|-------------| +| `bankId` | `HINDSIGHT_BANK_ID` | — | Memory bank ID | +| `bankMission` | `HINDSIGHT_BANK_MISSION` | `""` | Agent purpose for the bank | +| `retainMission` | — | — | Custom extraction prompt | +| `bankIdPrefix` | — | `""` | Prefix for bank IDs | -Verify the plugin loaded by typing `/tools` — you should see: +### Auto-Recall -``` -[hindsight] - * hindsight_recall - Search long-term memory for relevant information. - * hindsight_reflect - Synthesize a thoughtful answer from long-term memories. - * hindsight_retain - Store information to long-term memory for later retrieval. -``` +| Setting | Env Var | Default | Description | +|---------|---------|---------|-------------| +| `autoRecall` | `HINDSIGHT_AUTO_RECALL` | `true` | Enable `pre_llm_call` recall | +| `recallBudget` | `HINDSIGHT_RECALL_BUDGET` | `"mid"` | Effort: `low`/`mid`/`high` | +| `recallMaxTokens` | `HINDSIGHT_RECALL_MAX_TOKENS` | `4096` | Max tokens in response | +| `recallMaxQueryChars` | `HINDSIGHT_RECALL_MAX_QUERY_CHARS` | `800` | Max query chars | +| `recallPromptPreamble` | — | see below | Header before recalled memories | + +### Auto-Retain -### 6. Test it +| Setting | Env Var | Default | Description | +|---------|---------|---------|-------------| +| `autoRetain` | `HINDSIGHT_AUTO_RETAIN` | `true` | Enable `post_llm_call` retain | +| `retainEveryNTurns` | — | `1` | Retain every Nth turn | +| `retainOverlapTurns` | — | `2` | Overlap turns for continuity | +| `retainRoles` | — | `["user", "assistant"]` | Roles to retain | -**Store a memory:** -> Remember that my favourite colour is red +### LLM (daemon mode) -You should see `⚡ hindsight` in the response, confirming it called `hindsight_retain`. +| Setting | Env Var | Default | Description | +|---------|---------|---------|-------------| +| `llmProvider` | `HINDSIGHT_LLM_PROVIDER` | auto-detect | `openai`/`anthropic`/`gemini`/`groq`/`ollama` | +| `llmModel` | `HINDSIGHT_LLM_MODEL` | provider default | Model override | -**Recall a memory:** -> What's my favourite colour? +### Misc -**Reflect on memories:** -> Based on what you know about me, suggest a colour scheme for my IDE +| Setting | Env Var | Default | Description | +|---------|---------|---------|-------------| +| `debug` | `HINDSIGHT_DEBUG` | `false` | Debug logging | -This calls `hindsight_reflect`, which synthesizes a response from all stored memories. +## Disabling Hermes's built-in memory -**Verify via API:** +Hermes has a built-in `memory` tool that saves to local files. Disable it so the LLM uses Hindsight instead: ```bash -curl -s http://localhost:8888/v1/default/banks/my-agent/memories/recall \ - -H "Content-Type: application/json" \ - -d '{"query": "favourite colour", "budget": "low"}' | python3 -m json.tool +hermes tools disable memory ``` ## Troubleshooting -### Tools don't appear in `/tools` - -1. **Check the plugin is installed in the right venv.** Run this from the Hermes venv: - ```bash - python -c "from hindsight_hermes import register; print('OK')" - ``` - -2. **Check the entry point is registered:** - ```bash - python -c " - import importlib.metadata - eps = importlib.metadata.entry_points(group='hermes_agent.plugins') - print(list(eps)) - " - ``` - You should see `EntryPoint(name='hindsight', value='hindsight_hermes', group='hermes_agent.plugins')`. - -3. **Check env vars are set.** The plugin skips registration silently if `HINDSIGHT_API_URL` and `HINDSIGHT_API_KEY` are both unset. - -### Hermes uses built-in memory instead of Hindsight - -Run `hermes tools disable memory` and restart. The built-in `memory` tool and Hindsight tools have overlapping purposes — the LLM will prefer whichever it's more familiar with, which is usually the built-in one. - -### Bank not found errors - -The plugin auto-creates banks on first use. If you see bank errors, check that the Hindsight API is running and `HINDSIGHT_API_URL` is correct. - -### Connection refused - -Make sure the Hindsight API is running and listening on the URL you configured. Test with: +**Plugin not loading** — verify the entry point: ```bash -curl http://localhost:8888/health -``` - -## Manual registration (advanced) - -If you don't want to use the plugin system, you can register tools directly in a Hermes startup script or custom agent: - -```python -from hindsight_hermes import register_tools - -register_tools( - bank_id="my-agent", - hindsight_api_url="http://localhost:8888", - budget="mid", - tags=["hermes"], # applied to all retained memories - recall_tags=["hermes"], # filter recall to only these tags -) +python -c " +import importlib.metadata +eps = importlib.metadata.entry_points(group='hermes_agent.plugins') +print(list(eps)) +" ``` -This imports `tools.registry` from Hermes at call time and registers the three tools directly. This approach gives you more control over parameters but requires Hermes to be importable. +**Tools missing from `/tools`** — the plugin skips registration when `hindsightApiUrl` is not configured. Check `~/.hindsight/hermes.json` or env vars. -## Memory instructions (system prompt injection) - -Pre-recall memories at startup and inject them into the system prompt, so the agent starts every conversation with relevant context: - -```python -from hindsight_hermes import memory_instructions - -context = memory_instructions( - bank_id="my-agent", - hindsight_api_url="http://localhost:8888", - query="user preferences and important context", - budget="low", - max_results=5, -) -# Returns: -# Relevant memories: -# 1. User's favourite colour is red -# 2. User prefers dark mode -``` - -This never raises — if the API is down or no memories exist, it returns an empty string. - -## Global configuration (advanced) - -Instead of passing parameters to every call, configure once: - -```python -from hindsight_hermes import configure - -configure( - hindsight_api_url="http://localhost:8888", - api_key="your-key", - budget="mid", - tags=["hermes"], -) -``` - -Subsequent calls to `register_tools()` or `memory_instructions()` will use these defaults if no explicit values are provided. - -## MCP alternative - -Hermes also supports MCP servers natively. You can use Hindsight's MCP server directly instead of this plugin — no `hindsight-hermes` package needed: - -```yaml -# In your Hermes config -mcp_servers: - - name: hindsight - url: http://localhost:8888/mcp -``` +**Connection refused** — verify the API is running: `curl http://localhost:9077/health` -This exposes the same retain/recall/reflect operations through Hermes's MCP integration. The tradeoff is that MCP tools may have different naming and the LLM needs to discover them, whereas the plugin registers tools with Hermes-native schemas. - -## Configuration reference - -| Parameter | Env Var | Default | Description | -|-----------|---------|---------|-------------| -| `hindsight_api_url` | `HINDSIGHT_API_URL` | `https://api.hindsight.vectorize.io` | Hindsight API URL | -| `api_key` | `HINDSIGHT_API_KEY` | — | API key for authentication | -| `bank_id` | `HINDSIGHT_BANK_ID` | — | Memory bank ID | -| `budget` | `HINDSIGHT_BUDGET` | `mid` | Recall budget (low/mid/high) | -| — | `HINDSIGHT_AUTO_RETAIN` | `true` | Auto-retain conversation turns via `post_llm_call` hook | -| — | `HINDSIGHT_RECALL_BUDGET` | same as `budget` | Budget for the `pre_llm_call` recall hook | -| — | `HINDSIGHT_RECALL_MAX_TOKENS` | `4096` | Max tokens for the `pre_llm_call` recall hook | -| `max_tokens` | — | `4096` | Max tokens for recall results (tools) | -| `tags` | — | — | Tags applied when storing memories | -| `recall_tags` | — | — | Tags to filter recall results | -| `recall_tags_match` | — | `any` | Tag matching mode (any/all/any_strict/all_strict) | -| `toolset` | — | `hindsight` | Hermes toolset group name | +**No memories recalled** — memories need at least one retain cycle. Store a fact, start a new session, then ask about it. diff --git a/hindsight-integrations/hermes/hindsight_hermes/__init__.py b/hindsight-integrations/hermes/hindsight_hermes/__init__.py index 613e15363..e9058902c 100644 --- a/hindsight-integrations/hermes/hindsight_hermes/__init__.py +++ b/hindsight-integrations/hermes/hindsight_hermes/__init__.py @@ -12,8 +12,8 @@ Plugin usage (auto-discovery):: pip install hindsight-hermes - export HINDSIGHT_API_URL=http://localhost:8888 - export HINDSIGHT_BANK_ID=my-agent + # Configure via ~/.hindsight/hermes.json: + # {"hindsightApiUrl": "http://localhost:9077", "bankId": "my-agent"} Manual usage:: @@ -21,15 +21,14 @@ register_tools( bank_id="my-agent", - hindsight_api_url="http://localhost:8888", + hindsight_api_url="http://localhost:9077", ) """ from .config import ( - HindsightHermesConfig, - configure, - get_config, - reset_config, + USER_CONFIG_PATH, + load_config, + write_config, ) from .errors import HindsightError from .tools import ( @@ -42,10 +41,9 @@ __version__ = "0.1.0" __all__ = [ - "configure", - "get_config", - "reset_config", - "HindsightHermesConfig", + "USER_CONFIG_PATH", + "load_config", + "write_config", "HindsightError", "register_tools", "register", diff --git a/hindsight-integrations/hermes/hindsight_hermes/config.py b/hindsight-integrations/hermes/hindsight_hermes/config.py index 9d43ad3d4..94792c0bb 100644 --- a/hindsight-integrations/hermes/hindsight_hermes/config.py +++ b/hindsight-integrations/hermes/hindsight_hermes/config.py @@ -1,92 +1,170 @@ -"""Global configuration for Hindsight-Hermes integration.""" +"""Configuration management for Hindsight-Hermes plugin. -from __future__ import annotations - -import os -from dataclasses import dataclass - -DEFAULT_HINDSIGHT_API_URL = "https://api.hindsight.vectorize.io" -HINDSIGHT_API_KEY_ENV = "HINDSIGHT_API_KEY" - - -@dataclass -class HindsightHermesConfig: - """Connection and default settings for the Hermes integration. - - Attributes: - hindsight_api_url: URL of the Hindsight API server. - api_key: API key for Hindsight authentication. - budget: Default recall budget level (low/mid/high). - max_tokens: Default maximum tokens for recall results. - tags: Default tags applied when storing memories. - recall_tags: Default tags to filter when searching memories. - recall_tags_match: Tag matching mode (any/all/any_strict/all_strict). - verbose: Enable verbose logging. - """ +Loads settings from ``~/.hindsight/hermes.json`` merged with environment +variable overrides. Follows the same conventions as the openclaw and +claude-code integrations. - hindsight_api_url: str = DEFAULT_HINDSIGHT_API_URL - api_key: str | None = None - budget: str = "mid" - max_tokens: int = 4096 - tags: list[str] | None = None - recall_tags: list[str] | None = None - recall_tags_match: str = "any" - verbose: bool = False - - -_global_config: HindsightHermesConfig | None = None +Loading order (later entries win): + 1. Built-in defaults + 2. User config (``~/.hindsight/hermes.json``) + 3. Environment variable overrides +""" +from __future__ import annotations -def configure( - hindsight_api_url: str | None = None, - api_key: str | None = None, - budget: str = "mid", - max_tokens: int = 4096, - tags: list[str] | None = None, - recall_tags: list[str] | None = None, - recall_tags_match: str = "any", - verbose: bool = False, -) -> HindsightHermesConfig: - """Configure Hindsight connection and default settings. +import json as _json +import logging +import os +import sys +from pathlib import Path +from typing import Any + +logger = logging.getLogger(__name__) + +USER_CONFIG_PATH = Path.home() / ".hindsight" / "hermes.json" + +# --------------------------------------------------------------------------- +# Defaults — same field names as openclaw / claude-code settings.json +# --------------------------------------------------------------------------- + +DEFAULTS: dict[str, Any] = { + # Connection + "hindsightApiUrl": None, + "hindsightApiToken": None, + "apiPort": 9077, + "daemonIdleTimeout": 0, + "embedVersion": "latest", + "embedPackagePath": None, + # Bank + "bankId": None, + "bankIdPrefix": "", + "bankMission": "", + "retainMission": None, + # Recall + "autoRecall": True, + "recallBudget": "mid", + "recallMaxTokens": 4096, + "recallTypes": ["world", "experience"], + "recallContextTurns": 1, + "recallMaxQueryChars": 800, + "recallRoles": ["user", "assistant"], + "recallPromptPreamble": ( + "Relevant memories from past conversations (prioritize recent when " + "conflicting). Only use memories that are directly useful to continue " + "this conversation; ignore the rest:" + ), + "recallTopK": None, + # Retain + "autoRetain": True, + "retainRoles": ["user", "assistant"], + "retainEveryNTurns": 1, + "retainOverlapTurns": 2, + "retainContext": "hermes", + # LLM (for daemon mode) + "llmProvider": None, + "llmModel": None, + "llmApiKeyEnv": None, + # Misc + "debug": False, +} + +# --------------------------------------------------------------------------- +# Env var → config key mapping (same convention as claude-code) +# --------------------------------------------------------------------------- + +ENV_OVERRIDES: dict[str, tuple[str, type]] = { + "HINDSIGHT_API_URL": ("hindsightApiUrl", str), + "HINDSIGHT_API_TOKEN": ("hindsightApiToken", str), + "HINDSIGHT_API_KEY": ("hindsightApiToken", str), # alias + "HINDSIGHT_BANK_ID": ("bankId", str), + "HINDSIGHT_AUTO_RECALL": ("autoRecall", bool), + "HINDSIGHT_AUTO_RETAIN": ("autoRetain", bool), + "HINDSIGHT_RECALL_BUDGET": ("recallBudget", str), + "HINDSIGHT_RECALL_MAX_TOKENS": ("recallMaxTokens", int), + "HINDSIGHT_RECALL_MAX_QUERY_CHARS": ("recallMaxQueryChars", int), + "HINDSIGHT_API_PORT": ("apiPort", int), + "HINDSIGHT_DAEMON_IDLE_TIMEOUT": ("daemonIdleTimeout", int), + "HINDSIGHT_EMBED_VERSION": ("embedVersion", str), + "HINDSIGHT_EMBED_PACKAGE_PATH": ("embedPackagePath", str), + "HINDSIGHT_BANK_MISSION": ("bankMission", str), + "HINDSIGHT_LLM_PROVIDER": ("llmProvider", str), + "HINDSIGHT_LLM_MODEL": ("llmModel", str), + "HINDSIGHT_DEBUG": ("debug", bool), +} + + +# --------------------------------------------------------------------------- +# Loading +# --------------------------------------------------------------------------- + + +def _cast_env(value: str, typ: type) -> Any: + """Cast environment variable string to target type. Returns None on failure.""" + try: + if typ is bool: + return value.lower() in ("true", "1", "yes") + if typ is int: + return int(value) + return value + except (ValueError, AttributeError): + return None + + +def _load_json_file(path: Path | str) -> dict[str, Any]: + """Read a JSON file, returning {} on any error.""" + p = Path(path) + if not p.exists(): + return {} + try: + return _json.loads(p.read_text(encoding="utf-8")) or {} + except Exception as exc: + _debug_log(None, f"Failed to read {p}: {exc}") + return {} + + +def load_config(config_path: Path | str | None = None) -> dict[str, Any]: + """Load plugin configuration. + + Loading order (later entries win): + 1. Built-in defaults + 2. User config (``~/.hindsight/hermes.json``) + 3. Environment variable overrides Args: - hindsight_api_url: Hindsight API URL (default: production). - api_key: API key. Falls back to HINDSIGHT_API_KEY env var. - budget: Default recall budget (low/mid/high). - max_tokens: Default max tokens for recall. - tags: Default tags for retain operations. - recall_tags: Default tags to filter recall/search. - recall_tags_match: Tag matching mode. - verbose: Enable verbose logging. + config_path: Override the user config path (for testing). Returns: - The configured HindsightHermesConfig. + A plain dict with all configuration values. """ - global _global_config - - resolved_url = hindsight_api_url or DEFAULT_HINDSIGHT_API_URL - resolved_key = api_key or os.environ.get(HINDSIGHT_API_KEY_ENV) - - _global_config = HindsightHermesConfig( - hindsight_api_url=resolved_url, - api_key=resolved_key, - budget=budget, - max_tokens=max_tokens, - tags=tags, - recall_tags=recall_tags, - recall_tags_match=recall_tags_match, - verbose=verbose, + config = dict(DEFAULTS) + + # User config — stable, version-independent + user_path = Path(config_path) if config_path else USER_CONFIG_PATH + file_cfg = _load_json_file(user_path) + config.update({k: v for k, v in file_cfg.items() if v is not None}) + + # Environment variable overrides (highest priority) + for env_name, (key, typ) in ENV_OVERRIDES.items(): + val = os.environ.get(env_name) + if val is not None: + cast_val = _cast_env(val, typ) + if cast_val is not None: + config[key] = cast_val + + return config + + +def write_config(data: dict[str, Any], config_path: Path | str | None = None) -> None: + """Write configuration to the user config file.""" + p = Path(config_path) if config_path else USER_CONFIG_PATH + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text( + _json.dumps(data, indent=2, ensure_ascii=False) + "\n", + encoding="utf-8", ) - return _global_config - - -def get_config() -> HindsightHermesConfig | None: - """Get the current global configuration.""" - return _global_config - -def reset_config() -> None: - """Reset global configuration to None.""" - global _global_config - _global_config = None +def _debug_log(config: dict | None, *args: Any) -> None: + """Log to stderr if debug mode is enabled.""" + if config and config.get("debug"): + print("[Hindsight]", *args, file=sys.stderr) diff --git a/hindsight-integrations/hermes/hindsight_hermes/tools.py b/hindsight-integrations/hermes/hindsight_hermes/tools.py index 3ad0723d2..a4c19122f 100644 --- a/hindsight-integrations/hermes/hindsight_hermes/tools.py +++ b/hindsight-integrations/hermes/hindsight_hermes/tools.py @@ -14,7 +14,7 @@ from hindsight_client import Hindsight -from .config import get_config +from .config import load_config from .errors import HindsightError logger = logging.getLogger(__name__) @@ -94,23 +94,19 @@ def _resolve_client( hindsight_api_url: str | None, api_key: str | None, ) -> Hindsight: - """Resolve a Hindsight client from explicit args or global config.""" + """Resolve a Hindsight client from explicit args.""" if client is not None: return client - config = get_config() - url = hindsight_api_url or (config.hindsight_api_url if config else None) - key = api_key or (config.api_key if config else None) - - if url is None: + if hindsight_api_url is None: raise HindsightError( "No Hindsight API URL configured. " - "Pass client= or hindsight_api_url=, or call configure() first." + "Pass client= or hindsight_api_url=, or create ~/.hindsight/hermes.json." ) - kwargs: dict[str, Any] = {"base_url": url, "timeout": 30.0} - if key: - kwargs["api_key"] = key + kwargs: dict[str, Any] = {"base_url": hindsight_api_url, "timeout": 30.0} + if api_key: + kwargs["api_key"] = api_key return Hindsight(**kwargs) @@ -264,20 +260,23 @@ async def handle_reflect(args: dict[str, Any], **kwargs: Any) -> str: def register(ctx: Any) -> None: """Hermes plugin entry point — called via ``hermes_agent.plugins`` entry point. - Reads configuration from environment variables and registers tools - using ``ctx.register_tool()``. + Reads configuration from ``~/.hindsight/hermes.json`` and environment + variables (env vars take priority), then registers tools and hooks. Args: ctx: Hermes PluginContext. """ - hindsight_api_url = os.environ.get("HINDSIGHT_API_URL") - api_key = os.environ.get("HINDSIGHT_API_KEY") - bank_id = os.environ.get("HINDSIGHT_BANK_ID") - budget = os.environ.get("HINDSIGHT_BUDGET", "mid") + cfg = load_config() + + hindsight_api_url = cfg.get("hindsightApiUrl") + api_key = cfg.get("hindsightApiToken") + bank_id = cfg.get("bankId") + budget = cfg.get("recallBudget", "mid") if not hindsight_api_url and not api_key: logger.debug( - "Hindsight plugin: no API URL or key configured, skipping registration" + "Hindsight plugin: not configured (need hindsightApiUrl or hindsightApiToken). " + "Create ~/.hindsight/hermes.json or set HINDSIGHT_API_URL." ) return @@ -354,9 +353,10 @@ async def handle_reflect(args: dict[str, Any], **kwargs: Any) -> str: # When running on an older hermes-agent the hooks are simply never called, # so registering them is always safe. - recall_budget = os.environ.get("HINDSIGHT_RECALL_BUDGET", budget) - recall_max_tokens = int(os.environ.get("HINDSIGHT_RECALL_MAX_TOKENS", "4096")) - retain_enabled = os.environ.get("HINDSIGHT_AUTO_RETAIN", "true").lower() in {"1", "true", "yes", "on"} + recall_budget = cfg.get("recallBudget", budget) + recall_max_tokens = cfg.get("recallMaxTokens", 4096) + retain_enabled = cfg.get("autoRetain", True) + recall_preamble = cfg.get("recallPromptPreamble", "") async def _on_pre_llm_call( *, @@ -381,12 +381,12 @@ async def _on_pre_llm_call( if not response.results: return None lines = [f"- {r.text}" for r in response.results] - context = ( + header = recall_preamble or ( "# Hindsight Memory (persistent cross-session context)\n" "Use this to answer questions about the user and prior sessions. " - "Do not call tools to look up information that is already present here.\n\n" - + "\n".join(lines) + "Do not call tools to look up information that is already present here." ) + context = header + "\n\n" + "\n".join(lines) return {"context": context} except Exception as exc: logger.warning("Hindsight pre_llm_call recall failed: %s", exc) diff --git a/hindsight-integrations/hermes/tests/test_config.py b/hindsight-integrations/hermes/tests/test_config.py index f41ae0f95..fd75f1994 100644 --- a/hindsight-integrations/hermes/tests/test_config.py +++ b/hindsight-integrations/hermes/tests/test_config.py @@ -1,57 +1,98 @@ """Tests for hindsight_hermes.config module.""" -from hindsight_hermes.config import ( - HindsightHermesConfig, - configure, - get_config, - reset_config, -) - - -class TestConfigure: - def setup_method(self): - reset_config() - - def teardown_method(self): - reset_config() - - def test_configure_returns_config(self): - cfg = configure(hindsight_api_url="http://localhost:8888", api_key="test-key") - assert isinstance(cfg, HindsightHermesConfig) - assert cfg.hindsight_api_url == "http://localhost:8888" - assert cfg.api_key == "test-key" - - def test_configure_defaults(self): - cfg = configure() - assert cfg.hindsight_api_url == "https://api.hindsight.vectorize.io" - assert cfg.api_key is None - assert cfg.budget == "mid" - assert cfg.max_tokens == 4096 - assert cfg.tags is None - assert cfg.recall_tags is None - assert cfg.recall_tags_match == "any" - assert cfg.verbose is False - - def test_get_config_returns_none_before_configure(self): - assert get_config() is None - - def test_get_config_returns_configured(self): - configure(api_key="k") - cfg = get_config() - assert cfg is not None - assert cfg.api_key == "k" - - def test_reset_config(self): - configure(api_key="k") - reset_config() - assert get_config() is None - - def test_configure_reads_env_var(self, monkeypatch): - monkeypatch.setenv("HINDSIGHT_API_KEY", "env-key") - cfg = configure() - assert cfg.api_key == "env-key" - - def test_explicit_key_overrides_env(self, monkeypatch): - monkeypatch.setenv("HINDSIGHT_API_KEY", "env-key") - cfg = configure(api_key="explicit-key") - assert cfg.api_key == "explicit-key" +import json + +from hindsight_hermes.config import DEFAULTS, load_config + + +class TestLoadConfig: + def test_defaults_when_no_file(self, tmp_path, monkeypatch): + monkeypatch.delenv("HINDSIGHT_API_URL", raising=False) + monkeypatch.delenv("HINDSIGHT_API_KEY", raising=False) + monkeypatch.delenv("HINDSIGHT_BANK_ID", raising=False) + cfg = load_config(config_path=tmp_path / "nope.json") + assert cfg["hindsightApiUrl"] is None + assert cfg["bankId"] is None + assert cfg["recallBudget"] == "mid" + assert cfg["autoRecall"] is True + assert cfg["autoRetain"] is True + + def test_reads_from_file(self, tmp_path, monkeypatch): + monkeypatch.delenv("HINDSIGHT_API_URL", raising=False) + monkeypatch.delenv("HINDSIGHT_API_KEY", raising=False) + monkeypatch.delenv("HINDSIGHT_BANK_ID", raising=False) + monkeypatch.delenv("HINDSIGHT_AUTO_RETAIN", raising=False) + f = tmp_path / "hermes.json" + f.write_text(json.dumps({ + "hindsightApiUrl": "http://localhost:9077", + "hindsightApiToken": "file-token", + "bankId": "my-bank", + "recallBudget": "high", + "autoRetain": False, + "recallMaxTokens": 2048, + })) + cfg = load_config(config_path=f) + assert cfg["hindsightApiUrl"] == "http://localhost:9077" + assert cfg["hindsightApiToken"] == "file-token" + assert cfg["bankId"] == "my-bank" + assert cfg["recallBudget"] == "high" + assert cfg["autoRetain"] is False + assert cfg["recallMaxTokens"] == 2048 + + def test_env_overrides_file(self, tmp_path, monkeypatch): + f = tmp_path / "hermes.json" + f.write_text(json.dumps({ + "hindsightApiUrl": "http://from-file:9077", + "bankId": "file-bank", + "recallBudget": "low", + })) + monkeypatch.setenv("HINDSIGHT_API_URL", "http://from-env:9077") + monkeypatch.setenv("HINDSIGHT_BANK_ID", "env-bank") + monkeypatch.delenv("HINDSIGHT_RECALL_BUDGET", raising=False) + cfg = load_config(config_path=f) + assert cfg["hindsightApiUrl"] == "http://from-env:9077" + assert cfg["bankId"] == "env-bank" + assert cfg["recallBudget"] == "low" # from file (env not set) + + def test_api_key_env_maps_to_token(self, tmp_path, monkeypatch): + """HINDSIGHT_API_KEY is an alias for hindsightApiToken.""" + monkeypatch.setenv("HINDSIGHT_API_KEY", "my-key") + cfg = load_config(config_path=tmp_path / "nope.json") + assert cfg["hindsightApiToken"] == "my-key" + + def test_bool_env_casting(self, tmp_path, monkeypatch): + monkeypatch.setenv("HINDSIGHT_AUTO_RETAIN", "false") + monkeypatch.setenv("HINDSIGHT_AUTO_RECALL", "1") + cfg = load_config(config_path=tmp_path / "nope.json") + assert cfg["autoRetain"] is False + assert cfg["autoRecall"] is True + + def test_int_env_casting(self, tmp_path, monkeypatch): + monkeypatch.setenv("HINDSIGHT_RECALL_MAX_TOKENS", "2048") + cfg = load_config(config_path=tmp_path / "nope.json") + assert cfg["recallMaxTokens"] == 2048 + + def test_malformed_file_returns_defaults(self, tmp_path, monkeypatch): + monkeypatch.delenv("HINDSIGHT_API_URL", raising=False) + monkeypatch.delenv("HINDSIGHT_BANK_ID", raising=False) + f = tmp_path / "hermes.json" + f.write_text("not json {{{") + cfg = load_config(config_path=f) + assert cfg["recallBudget"] == "mid" + + def test_null_values_in_file_ignored(self, tmp_path, monkeypatch): + """null values in JSON should not override defaults.""" + monkeypatch.delenv("HINDSIGHT_API_URL", raising=False) + monkeypatch.delenv("HINDSIGHT_BANK_ID", raising=False) + f = tmp_path / "hermes.json" + f.write_text(json.dumps({"bankId": None, "recallBudget": "high"})) + cfg = load_config(config_path=f) + assert cfg["bankId"] is None # stays default (None) + assert cfg["recallBudget"] == "high" + + def test_all_defaults_present(self): + """Every key in DEFAULTS should exist in a freshly loaded config.""" + # Use a path that doesn't exist and clean env + cfg = DEFAULTS.copy() + for key in DEFAULTS: + assert key in cfg diff --git a/hindsight-integrations/hermes/tests/test_tools.py b/hindsight-integrations/hermes/tests/test_tools.py index 696e630dd..605dd617e 100644 --- a/hindsight-integrations/hermes/tests/test_tools.py +++ b/hindsight-integrations/hermes/tests/test_tools.py @@ -7,7 +7,7 @@ import pytest -from hindsight_hermes.config import configure, reset_config +from hindsight_hermes.config import DEFAULTS from hindsight_hermes.errors import HindsightError from hindsight_hermes.tools import ( RECALL_SCHEMA, @@ -26,10 +26,11 @@ @pytest.fixture(autouse=True) -def _clean_config(): - reset_config() - yield - reset_config() +def _clean_env(monkeypatch): + """Ensure no stale env vars leak between tests.""" + for key in ("HINDSIGHT_API_URL", "HINDSIGHT_API_KEY", "HINDSIGHT_API_TOKEN", + "HINDSIGHT_BANK_ID", "HINDSIGHT_AUTO_RETAIN", "HINDSIGHT_RECALL_BUDGET"): + monkeypatch.delenv(key, raising=False) @pytest.fixture() @@ -125,10 +126,9 @@ class TestResolveClient: def test_returns_provided_client(self, mock_client): assert _resolve_client(mock_client, None, None) is mock_client - def test_uses_global_config(self): - configure(hindsight_api_url="http://localhost:9999", api_key="key") + def test_creates_client_from_args(self): with patch("hindsight_hermes.tools.Hindsight") as MockH: - _resolve_client(None, None, None) + _resolve_client(None, "http://localhost:9999", "key") MockH.assert_called_once_with(base_url="http://localhost:9999", timeout=30.0, api_key="key") def test_raises_without_url(self): @@ -222,11 +222,11 @@ def test_register_calls_ctx_register_tool(self, monkeypatch, mock_client): register(ctx) assert ctx.register_tool.call_count == 3 - def test_register_skips_without_config(self, monkeypatch): - monkeypatch.delenv("HINDSIGHT_API_URL", raising=False) - monkeypatch.delenv("HINDSIGHT_API_KEY", raising=False) + def test_register_skips_without_config(self): + empty_cfg = dict(DEFAULTS) # no apiUrl, no apiToken ctx = MagicMock() - register(ctx) + with patch("hindsight_hermes.tools.load_config", return_value=empty_cfg): + register(ctx) ctx.register_tool.assert_not_called() def test_register_hooks(self, monkeypatch, mock_client): From e8a9bfb65ec82889b278ce7aa5ca4f7d8cee0184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Boschi?= Date: Wed, 25 Mar 2026 15:42:37 +0100 Subject: [PATCH 2/3] ci: add test job for hermes integration --- .github/workflows/test.yml | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 42d344758..89d80b142 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,6 +35,7 @@ jobs: integrations-crewai: ${{ steps.filter.outputs.integrations-crewai }} integrations-litellm: ${{ steps.filter.outputs.integrations-litellm }} integrations-pydantic-ai: ${{ steps.filter.outputs.integrations-pydantic-ai }} + integrations-hermes: ${{ steps.filter.outputs.integrations-hermes }} dev: ${{ steps.filter.outputs.dev }} ci: ${{ steps.filter.outputs.ci }} steps: @@ -93,6 +94,8 @@ jobs: - 'hindsight-integrations/litellm/**' integrations-pydantic-ai: - 'hindsight-integrations/pydantic-ai/**' + integrations-hermes: + - 'hindsight-integrations/hermes/**' dev: - 'hindsight-dev/**' ci: @@ -1586,6 +1589,40 @@ jobs: working-directory: ./hindsight-integrations/pydantic-ai run: uv run pytest tests -v + test-hermes-integration: + needs: [detect-changes] + if: >- + github.event_name == 'workflow_dispatch' || + needs.detect-changes.outputs.integrations-hermes == 'true' || + needs.detect-changes.outputs.ci == 'true' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + prune-cache: false + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version-file: ".python-version" + + - name: Build hermes integration + working-directory: ./hindsight-integrations/hermes + run: uv build + + - name: Install dependencies + working-directory: ./hindsight-integrations/hermes + run: uv sync --frozen + + - name: Run tests + working-directory: ./hindsight-integrations/hermes + run: uv run pytest tests -v + test-pip-slim: needs: [detect-changes] if: >- From feea9a37e6f914c8965d5a82dc2af4bb11ba2f0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Boschi?= Date: Wed, 25 Mar 2026 15:55:07 +0100 Subject: [PATCH 3/3] chore: regenerate docs skill for hermes integration --- .../references/sdks/integrations/hermes.md | 280 ++++++++---------- 1 file changed, 118 insertions(+), 162 deletions(-) diff --git a/skills/hindsight-docs/references/sdks/integrations/hermes.md b/skills/hindsight-docs/references/sdks/integrations/hermes.md index 766687dee..873846295 100644 --- a/skills/hindsight-docs/references/sdks/integrations/hermes.md +++ b/skills/hindsight-docs/references/sdks/integrations/hermes.md @@ -4,216 +4,172 @@ sidebar_position: 10 # Hermes Agent -Hindsight memory integration for [Hermes Agent](https://github.com/NousResearch/hermes-agent). Gives your Hermes agent persistent long-term memory via retain, recall, and reflect tools. +Persistent long-term memory for [Hermes Agent](https://github.com/NousResearch/hermes-agent) using [Hindsight](https://vectorize.io/hindsight). Automatically recalls relevant context before every LLM call and retains conversations for future sessions — plus explicit retain/recall/reflect tools. -## What it does - -This package registers three tools into Hermes via its plugin system: - -- **`hindsight_retain`** — Stores information to long-term memory. Hermes calls this when the user shares facts, preferences, or anything worth remembering. -- **`hindsight_recall`** — Searches long-term memory for relevant information. Returns a numbered list of matching memories. -- **`hindsight_reflect`** — Synthesizes a thoughtful answer from stored memories. Use this when you want Hermes to reason over what it knows rather than return raw facts. - -These tools appear under the `[hindsight]` toolset in Hermes's `/tools` list. - -## Setup - -### 1. Install hindsight-hermes into the Hermes venv - -The package must be installed in the **same Python environment** that Hermes runs in, so the entry point is discoverable. +## Quick Start ```bash +# 1. Install the plugin into Hermes's Python environment uv pip install hindsight-hermes --python $HOME/.hermes/hermes-agent/venv/bin/python + +# 2. Configure (choose one) +# Option A: Config file (recommended) +mkdir -p ~/.hindsight +cat > ~/.hindsight/hermes.json << 'EOF' +{ + "hindsightApiUrl": "http://localhost:9077", + "bankId": "hermes" +} +EOF + +# Option B: Environment variables +export HINDSIGHT_API_URL=http://localhost:9077 +export HINDSIGHT_BANK_ID=hermes + +# 3. Start Hermes — the plugin activates automatically +hermes ``` -### 2. Set environment variables +## Features -The plugin reads its configuration from environment variables. Set these before launching Hermes: +- **Auto-recall** — on every turn, queries Hindsight for relevant memories and injects them into the system prompt (via `pre_llm_call` hook) +- **Auto-retain** — after every response, retains the user/assistant exchange to Hindsight (via `post_llm_call` hook) +- **Explicit tools** — `hindsight_retain`, `hindsight_recall`, `hindsight_reflect` for direct model control +- **Config file** — `~/.hindsight/hermes.json` with the same field names as openclaw and claude-code integrations +- **Zero config overhead** — env vars still work as overrides for CI/automation -```bash -# Required — tells the plugin where Hindsight is running -export HINDSIGHT_API_URL=http://localhost:8888 +:::note +The lifecycle hooks (`pre_llm_call`/`post_llm_call`) require hermes-agent with [PR #2823](https://github.com/NousResearch/hermes-agent/pull/2823) or later. On older versions, only the three tools are registered — hooks are silently skipped. +::: -# Required — the memory bank to read/write. Think of this as a "brain" for one user or agent. -export HINDSIGHT_BANK_ID=my-agent +## Architecture -# Optional — only needed if using Hindsight Cloud (https://api.hindsight.vectorize.io) -export HINDSIGHT_API_KEY=your-api-key +The plugin registers via Hermes's `hermes_agent.plugins` entry point system: -# Optional — recall budget: low (fast), mid (default), high (thorough) -export HINDSIGHT_BUDGET=mid -``` +| Component | Purpose | +|-----------|---------| +| `pre_llm_call` hook | **Auto-recall** — query memories, inject as ephemeral system prompt context | +| `post_llm_call` hook | **Auto-retain** — store user/assistant exchange to Hindsight | +| `hindsight_retain` tool | Explicit memory storage (model-initiated) | +| `hindsight_recall` tool | Explicit memory search (model-initiated) | +| `hindsight_reflect` tool | LLM-synthesized answer from stored memories | -If neither `HINDSIGHT_API_URL` nor `HINDSIGHT_API_KEY` is set, the plugin silently skips registration — Hermes starts normally without the Hindsight tools. +## Connection Modes -### 3. Disable Hermes's built-in memory tool +### 1. External API (recommended for production) -Hermes has its own `memory` tool that saves to local files (`~/.hermes/`). If both are active, the LLM tends to prefer the built-in one since it's familiar. Disable it so the LLM uses Hindsight instead: +Connect to a running Hindsight server (cloud or self-hosted). No local LLM needed — the server handles fact extraction. -```bash -hermes tools disable memory +```json +{ + "hindsightApiUrl": "https://your-hindsight-server.com", + "hindsightApiToken": "your-token", + "bankId": "hermes" +} ``` -This persists across sessions. You can re-enable it later with `hermes tools enable memory`. - -### 4. Start Hindsight API +### 2. Local Daemon -Follow the [Quick Start](../../developer/api/quickstart.md) guide to get the Hindsight API running, then come back here. +If you're running `hindsight-embed` locally, point to it: -### 5. Launch Hermes - -```bash -hermes +```json +{ + "hindsightApiUrl": "http://localhost:9077", + "bankId": "hermes" +} ``` -Verify the plugin loaded by typing `/tools` — you should see: +Follow the [Quick Start](../../developer/api/quickstart.md) guide to get the Hindsight API running. -``` -[hindsight] - * hindsight_recall - Search long-term memory for relevant information. - * hindsight_reflect - Synthesize a thoughtful answer from long-term memories. - * hindsight_retain - Store information to long-term memory for later retrieval. -``` +## Configuration -### 6. Test it +All settings are in `~/.hindsight/hermes.json`. Every setting can also be overridden via environment variables (env vars take priority). -**Store a memory:** -> Remember that my favourite colour is red +### Connection & Daemon -You should see `⚡ hindsight` in the response, confirming it called `hindsight_retain`. +| Setting | Default | Env Var | Description | +|---------|---------|---------|-------------| +| `hindsightApiUrl` | — | `HINDSIGHT_API_URL` | Hindsight API URL | +| `hindsightApiToken` | `null` | `HINDSIGHT_API_TOKEN` / `HINDSIGHT_API_KEY` | Auth token for API | +| `apiPort` | `9077` | `HINDSIGHT_API_PORT` | Port for local Hindsight daemon | +| `daemonIdleTimeout` | `0` | `HINDSIGHT_DAEMON_IDLE_TIMEOUT` | Seconds before idle daemon shuts down (0 = never) | +| `embedVersion` | `"latest"` | `HINDSIGHT_EMBED_VERSION` | `hindsight-embed` version for `uvx` | -**Recall a memory:** -> What's my favourite colour? +### LLM Provider (daemon mode only) -**Reflect on memories:** -> Based on what you know about me, suggest a colour scheme for my IDE +| Setting | Default | Env Var | Description | +|---------|---------|---------|-------------| +| `llmProvider` | auto-detect | `HINDSIGHT_LLM_PROVIDER` | LLM provider: `openai`, `anthropic`, `gemini`, `groq`, `ollama` | +| `llmModel` | provider default | `HINDSIGHT_LLM_MODEL` | Model override | -This calls `hindsight_reflect`, which synthesizes a response from all stored memories. +### Memory Bank -**Verify via API:** +| Setting | Default | Env Var | Description | +|---------|---------|---------|-------------| +| `bankId` | — | `HINDSIGHT_BANK_ID` | Memory bank ID | +| `bankMission` | `""` | `HINDSIGHT_BANK_MISSION` | Agent identity/purpose for the memory bank | +| `retainMission` | `null` | — | Custom retain mission (what to extract from conversations) | +| `bankIdPrefix` | `""` | — | Prefix for all bank IDs | -```bash -curl -s http://localhost:8888/v1/default/banks/my-agent/memories/recall \ - -H "Content-Type: application/json" \ - -d '{"query": "favourite colour", "budget": "low"}' | python3 -m json.tool -``` +### Auto-Recall -## Troubleshooting +| Setting | Default | Env Var | Description | +|---------|---------|---------|-------------| +| `autoRecall` | `true` | `HINDSIGHT_AUTO_RECALL` | Enable automatic memory recall via `pre_llm_call` hook | +| `recallBudget` | `"mid"` | `HINDSIGHT_RECALL_BUDGET` | Recall effort: `low`, `mid`, `high` | +| `recallMaxTokens` | `4096` | `HINDSIGHT_RECALL_MAX_TOKENS` | Max tokens in recall response | +| `recallMaxQueryChars` | `800` | `HINDSIGHT_RECALL_MAX_QUERY_CHARS` | Max chars of user message used as query | +| `recallPromptPreamble` | see below | — | Header text injected before recalled memories | -### Tools don't appear in `/tools` +Default preamble: +> Relevant memories from past conversations (prioritize recent when conflicting). Only use memories that are directly useful to continue this conversation; ignore the rest: -1. **Check the plugin is installed in the right venv.** Run this from the Hermes venv: - ```bash - python -c "from hindsight_hermes import register; print('OK')" - ``` +### Auto-Retain -2. **Check the entry point is registered:** - ```bash - python -c " - import importlib.metadata - eps = importlib.metadata.entry_points(group='hermes_agent.plugins') - print(list(eps)) - " - ``` - You should see `EntryPoint(name='hindsight', value='hindsight_hermes', group='hermes_agent.plugins')`. +| Setting | Default | Env Var | Description | +|---------|---------|---------|-------------| +| `autoRetain` | `true` | `HINDSIGHT_AUTO_RETAIN` | Enable automatic retention via `post_llm_call` hook | +| `retainEveryNTurns` | `1` | — | Retain every Nth turn | +| `retainOverlapTurns` | `2` | — | Extra overlap turns for continuity | +| `retainRoles` | `["user", "assistant"]` | — | Which message roles to retain | -3. **Check env vars are set.** The plugin skips registration silently if `HINDSIGHT_API_URL` and `HINDSIGHT_API_KEY` are both unset. +### Miscellaneous -### Hermes uses built-in memory instead of Hindsight +| Setting | Default | Env Var | Description | +|---------|---------|---------|-------------| +| `debug` | `false` | `HINDSIGHT_DEBUG` | Enable debug logging to stderr | -Run `hermes tools disable memory` and restart. The built-in `memory` tool and Hindsight tools have overlapping purposes — the LLM will prefer whichever it's more familiar with, which is usually the built-in one. +## Hermes Gateway (Telegram, Discord, Slack) -### Bank not found errors +When using Hermes in gateway mode (multi-platform messaging), the plugin works across all platforms. Hermes creates a fresh `AIAgent` per message, and the plugin's `pre_llm_call` hook ensures relevant memories are recalled for each turn regardless of platform. -The plugin auto-creates banks on first use. If you see bank errors, check that the Hindsight API is running and `HINDSIGHT_API_URL` is correct. +## Disabling Hermes's Built-in Memory -### Connection refused +Hermes has a built-in `memory` tool that saves to local markdown files. If both are active, the LLM may prefer the built-in one. Disable it: -Make sure the Hindsight API is running and listening on the URL you configured. Test with: ```bash -curl http://localhost:8888/health -``` - -## Manual registration (advanced) - -If you don't want to use the plugin system, you can register tools directly in a Hermes startup script or custom agent: - -```python -from hindsight_hermes import register_tools - -register_tools( - bank_id="my-agent", - hindsight_api_url="http://localhost:8888", - budget="mid", - tags=["hermes"], # applied to all retained memories - recall_tags=["hermes"], # filter recall to only these tags -) -``` - -This imports `tools.registry` from Hermes at call time and registers the three tools directly. This approach gives you more control over parameters but requires Hermes to be importable. - -## Memory instructions (system prompt injection) - -Pre-recall memories at startup and inject them into the system prompt, so the agent starts every conversation with relevant context: - -```python -from hindsight_hermes import memory_instructions - -context = memory_instructions( - bank_id="my-agent", - hindsight_api_url="http://localhost:8888", - query="user preferences and important context", - budget="low", - max_results=5, -) -# Returns: -# Relevant memories: -# 1. User's favourite colour is red -# 2. User prefers dark mode +hermes tools disable memory ``` -This never raises — if the API is down or no memories exist, it returns an empty string. - -## Global configuration (advanced) - -Instead of passing parameters to every call, configure once: +Re-enable later with `hermes tools enable memory`. -```python -from hindsight_hermes import configure +## Troubleshooting -configure( - hindsight_api_url="http://localhost:8888", - api_key="your-key", - budget="mid", - tags=["hermes"], -) +**Plugin not loading**: Verify the entry point is registered: +```bash +python -c " +import importlib.metadata +eps = importlib.metadata.entry_points(group='hermes_agent.plugins') +print(list(eps)) +" ``` +You should see `EntryPoint(name='hindsight', value='hindsight_hermes', ...)`. -Subsequent calls to `register_tools()` or `memory_instructions()` will use these defaults if no explicit values are provided. - -## MCP alternative +**Tools don't appear in `/tools`**: Check that `hindsightApiUrl` (or `HINDSIGHT_API_URL`) is set. The plugin silently skips registration when unconfigured. -Hermes also supports MCP servers natively. You can use Hindsight's MCP server directly instead of this plugin — no `hindsight-hermes` package needed: - -```yaml -# In your Hermes config -mcp_servers: - - name: hindsight - url: http://localhost:8888/mcp +**Connection refused**: Verify the Hindsight API is running: +```bash +curl http://localhost:9077/health ``` -This exposes the same retain/recall/reflect operations through Hermes's MCP integration. The tradeoff is that MCP tools may have different naming and the LLM needs to discover them, whereas the plugin registers tools with Hermes-native schemas. - -## Configuration reference - -| Parameter | Env Var | Default | Description | -|-----------|---------|---------|-------------| -| `hindsight_api_url` | `HINDSIGHT_API_URL` | `https://api.hindsight.vectorize.io` | Hindsight API URL | -| `api_key` | `HINDSIGHT_API_KEY` | — | API key for authentication | -| `bank_id` | `HINDSIGHT_BANK_ID` | — | Memory bank ID | -| `budget` | `HINDSIGHT_BUDGET` | `mid` | Recall budget (low/mid/high) | -| `max_tokens` | — | `4096` | Max tokens for recall results | -| `tags` | — | — | Tags applied when storing memories | -| `recall_tags` | — | — | Tags to filter recall results | -| `recall_tags_match` | — | `any` | Tag matching mode (any/all/any_strict/all_strict) | -| `toolset` | — | `hindsight` | Hermes toolset group name | +**Recall returning no memories**: Memories need at least one retain cycle. Try storing a fact first, then asking about it in a new session.