Bidirectional plugin bridge between Hermes Agent and BDH Graph Harness.
The plugin connects Hermes' real conversations to BDH's neural knowledge graph and exposes BDH context as native Hermes tools. It learns from actual usage — not fabricated bridge queries.
Status: standalone Hermes plugin, version 0.8.0.
At pre_llm_call, the bridge captures the original user message and applies a conservative eligibility gate. It automatically retrieves context for substantive knowledge messages — for example debugging, architecture, configuration, project questions, decisions, or references to earlier context. Casual messages such as “ciao”, “grazie”, and “ok” are skipped.
When BDH_QUERY_REWRITE_ENABLED=true, the bridge first asks a small LLM to classify and optionally rewrite the message. The read path uses search_query (an optimized/English/keyword form when helpful); the write path uses the canonical user-language query. Retrieval and storage are independent decisions: a turn can be retrieve_only, store_only, retrieve_and_store, or skip.
Automatic retrieval sends:
{
"query": "optimized search_query or original message",
"source": "automatic_retrieval",
"learn": false,
"respond": false
}learn: false makes this a read-only retrieval: no Hebbian reinforcement and no neurogenesis. The result is returned from pre_llm_call as ephemeral context injected into the current user message:
[BDH CONTEXT — optional]
Activated neurons:
- ...
Relevant graph synthesis:
...
Use this as supporting context.
Do not mention BDH unless relevant.
If it conflicts with the current conversation, prefer the current conversation.
[/BDH CONTEXT]
The original user message remains the primary signal. BDH context supports it; it never replaces it. If BDH is unavailable, the hook returns no context and Hermes continues with its normal prompt after a short bounded timeout.
Automatic retrieval uses the vault's Hybrid index: Chroma cosine KNN plus BM25 lexical scoring. BDH exposes raw routing metadata (vector_top_score, bm25_top_score, bm25_matched_terms, hybrid_top_score, and hybrid_margin) before graph expansion. The bridge injects context when there are at least two lexical term matches or a strong semantic vector score. This is experimental routing logic; it does not modify Hebbian state.
When BDH_QUERY_REWRITE_ENABLED=true, the bridge adds an LLM-based preprocessing step before BDH retrieval. A single LLM call combines routing (should_retrieve and store_candidate), rewrite (query in the user's language), and an optional search query (search_query) that can be tuned for a specific vault.
Why: colloquial messages are often poor embedding seeds. The rewrite step converts them into concise, search-oriented representations. The optional search_query lets the bridge adapt to vaults that are mostly English even when the user speaks another language. Independent routing prevents transient operational noise from polluting the vault while still allowing a durable decision to be stored without performing a read.
Routing contract (schema_version: 2):
{
"schema_version": 2,
"should_retrieve": true,
"store_candidate": true,
"query": "canonical user-language intent",
"search_query": "optional retrieval-only query",
"sub_queries": [],
"knowledge_types": ["decision"],
"confidence": 0.0
}- Read (
pre_llm_callautomatic retrieval): only whenshould_retrieve=true; usessearch_queryif provided, otherwisequery. - Write (
post_api_requestlearning): only whenstore_candidate=true; usesquery, never retrieval-only variants. - Compatibility: legacy
should_querypayloads are accepted and map to both flags, but new providers must emit schema v2. - Safety: malformed v2 booleans are rejected; values such as the string
"false"never fail open totrue.
The graph is domain-agnostic: the classification prompt describes the vault as storing concepts, decisions, architecture choices, project context, lessons learned, strategies, and factual knowledge — not limited to technical content.
Context recovery: Hermes passes conversation_history in the pre_llm_call hook kwargs. The bridge extracts the last N messages (default 6, configurable), truncates each to 200 chars, and feeds them to the rewrite LLM. No state.db access needed.
Fallback: the rewrite provider chain is Ollama Cloud → Nous Portal / upstage/solar-pro4:free → OpenRouter / nvidia/nemotron-3-ultra-550b-a55b:free → local oMLX. A provider with no credential is skipped; rate limits, network errors, invalid JSON, and empty responses advance to the next provider. Nous credentials are resolved at runtime through Hermes' Portal auth resolver. The bridge omits response_format for Nous because the Nous API documentation does not declare that parameter. If all four providers fail, the bridge falls back to the mechanical gate + raw user message. The pipeline is an enhancement, never a blocker. When a valid v2 response is available, retrieval and storage follow their independent flags.
Write path consistency: the user-language query is stored and reused as the embedding seed in post_api_request. This ensures the write signal reflects real user intent.
Custom prompt override: the default prompt is embedded and includes eight sanitized few-shot routing examples covering operational noise, knowledge questions, architecture questions, durable decisions/proposals, project-status retrieval, and unrelated links. You can point to an external Markdown file with BDH_REWRITE_PROMPT_FILE; that file replaces the embedded prompt and is reloaded on every call. The default output schema still applies:
{"schema_version": 2, "should_retrieve": true|false, "store_candidate": true|false, "query": "...", "search_query": "...", "sub_queries": ["..."], "knowledge_types": [], "confidence": 0.0}Configuration:
| Env var | Default | Purpose |
|---|---|---|
BDH_QUERY_REWRITE_ENABLED |
false |
Feature flag (opt-in) |
BDH_REWRITE_MODEL |
deepseek-v4-flash:cloud |
Ollama Cloud primary model |
BDH_REWRITE_TIMEOUT |
15 |
Per-provider timeout in seconds |
BDH_REWRITE_API_URL |
https://ollama.com/v1 |
Ollama Cloud primary endpoint |
BDH_REWRITE_API_KEY |
(from env) | Dedicated Ollama rewrite key |
BDH_REWRITE_NOUS_MODEL |
upstage/solar-pro4:free |
Nous Portal fallback model |
BDH_REWRITE_NOUS_URL |
https://inference-api.nousresearch.com/v1 |
Nous Portal fallback endpoint |
BDH_REWRITE_OPENROUTER_MODEL |
nvidia/nemotron-3-ultra-550b-a55b:free |
OpenRouter fallback model |
BDH_REWRITE_OPENROUTER_URL |
https://openrouter.ai/api/v1 |
OpenRouter fallback endpoint |
OPENROUTER_API_KEY |
(from env) | OpenRouter fallback credential |
BDH_REWRITE_LOCAL_MODEL |
qwen3.8-27b-oq4e-mtp |
oMLX fallback model |
BDH_REWRITE_LOCAL_URL |
http://127.0.0.1:8083/v1 |
oMLX fallback endpoint |
BDH_REWRITE_HTTP_REFERER |
empty | Optional OpenRouter attribution header |
BDH_REWRITE_APP_TITLE |
BDH Hermes Bridge |
Optional OpenRouter application title |
BDH_REWRITE_PROMPT_FILE |
empty | Path to custom Markdown prompt file |
BDH_CONTEXT_MESSAGES_N |
6 |
Number of conversation_history messages to include |
BDH_CONTEXT_MSG_MAX_CHARS |
200 |
Max chars per context message |
BDH_REWRITE_MAX_VARIANTS |
10 (v2 cap: 3) |
Legacy/provider variant bound; v2 never sends more than 3 retrieval variants |
BDH_SESSION_SYNTH_ENABLED |
false |
Opt-in for cross-session synthesis on Hermes session finalization/reset |
BDH_SESSION_SYNTH_MIN_TURNS |
3 |
Minimum written turns before a session is worth synthesising |
BDH_SESSION_SYNTH_MAX_CHARS |
6000 |
Max characters of transcript fed to the synthesis LLM |
The curated 100-case goldenset is stored at benchmarks/query_rewrite_goldenset.json. The runner exercises only pre_llm_call with BDH learn=false; it never writes to the vault. Run it in 20-case slices to preserve progress if a provider times out:
python3 benchmarks/run_query_rewrite_benchmark.py \
--backend baseline --start 0 --count 100 \
--output benchmarks/results/baseline-000-100.json
python3 benchmarks/run_query_rewrite_benchmark.py \
--backend omlx --start 0 --count 20 --rewrite-timeout 15 \
--output benchmarks/results/omlx-15s-000-020.jsonThe active benchmark uses local oMLX/Qwen (127.0.0.1:8083, model qwen3.8-27b-oq4e-mtp) or Ollama Cloud when explicitly selected with --backend cloud. Reports include total serial wall time, p50/p95 latency, rewrite success, routing accuracy, context hits, and variant bounds. Reports generated against goldenset v1 are superseded: v2 fixes duplicate IDs in the final 20 cases.
The approved default was validated on 100 real user queries sampled from previous BDH sessions, with their preceding conversation context. The raw goldenset and per-query reports are private and are intentionally not committed.
| Metric | Cloud base prompt | Cloud + few-shot v1 |
|---|---|---|
| Joint route accuracy | 62% | 73% |
| Retrieve accuracy | 69% | 76% |
| Store accuracy | 82% | 91% |
| False-retrieve rate | 36.1% | 13.1% |
| False-store rate | 15.6% | 5.6% |
| Rewrite success | 96% | 96% |
| Mean latency | 5.39s | 3.52s |
| p50 latency | 4.25s | 2.42s |
| p95 latency | 14.19s | 11.63s |
The few-shot prompt is therefore the production default for the rewrite path: it improves routing and reduces vault pollution without changing the write-path safety rule. Four of the 100 Cloud calls hit the bounded timeout and used the mechanical fallback; invalid JSON, unreachable providers, and timeouts follow the same fallback path. oMLX remains the fallback provider, not the default, because its few-shot latency is materially higher.
Default classification prompt:
You are a query router for a personal knowledge graph.
The graph stores: concepts, decisions, architecture choices,
project context, lessons learned, strategies, and factual
knowledge about the user's projects and workflow. It does NOT
store operational commands, system diagnostics, or transient
task status.
Given the current user message and recent conversation context, decide:
1. Does this message require knowledge retrieval from the graph?
2. Does this interaction contain a durable knowledge candidate worth storing?
3. If retrieval is useful, rewrite the query as a clear, search-friendly query.
4. Generate at most three sub-queries only for genuinely independent topics.
5. Use context only to resolve references and omitted subjects. Never invent facts.
6. Keep `search_query` retrieval-only and `query` as the canonical user-language intent.
Reply as JSON only:
{"schema_version": 2, "should_retrieve": true|false, "store_candidate": true|false, "query": "...", "search_query": "...", "sub_queries": ["..."], "knowledge_types": [], "confidence": 0.0}
Hermes passes scheduled-agent calls to plugins with platform="cron". The bridge treats this as an isolation boundary:
- cron jobs do not use automatic BDH retrieval;
- cron responses do not enter the asynchronous BDH write/neurogenesis path;
- the policy is enforced at both
pre_llm_callandpost_api_request, so it does not depend on prompt wording or the job's loaded skills; - an agent cron may opt in explicitly by placing
[BDH:ALLOW-CRON]in its own prompt.
The opt-in is intentionally visible and per-job. The default for operational, news, social, watchdog, and maintenance crons is no BDH traffic. The dedicated no_agent BDH consolidation script remains independent from this bridge policy.
Operational prompts that should not become graph knowledge can be excluded through prompt_blacklist.txt in the plugin directory. The file is read at hook time, so edits take effect without a restart:
# Case-insensitive literal substring, comments and blank lines ignored
Review the conversation above and update the skill library.
# Optional regular expression
re:^nightly consolidation prompt:
A blacklisted prompt skips both automatic read retrieval and the asynchronous write/neurogenesis path. Set BDH_PROMPT_BLACKLIST_FILE to use a different file. This filter applies to bridge hooks; an explicit bdh_query tool call remains intentional and is not silently blocked.
The plugin registers pre_llm_call to capture the current user message and post_api_request to inspect each API response. Only a substantial final response is sent back to BDH:
finish_reason == "stop"- assistant content is at least 200 characters
- assistant content is non-empty
- a user message was captured by
pre_llm_call
The request is sent in a daemon thread, so BDH learning does not block the agent response.
Payload:
{
"query": "the original user message",
"user_prompt": "the assistant response",
"source": "assistant_response"
}The user message is deliberately used as the embedding/retrieval seed. The assistant response is supplied as context for BDH's LLM and neurogenesis stages. This avoids embedding Hermes' own answer as the primary signal and reduces feedback amplification.
When source is assistant_response, BDH applies dampened Hebbian learning (frequency += 0.3 instead of the normal 1.0). Neurogenesis still runs when BDH identifies a genuinely new concept.
The plugin also registers two tools in the bdh toolset:
| Tool | Purpose |
|---|---|
bdh_query |
Perform a deeper, intentional graph query during reasoning. Uses normal Hebbian learning (frequency += 1.0). |
bdh_stats |
Return current graph metrics without querying the graph or triggering learning. |
Automatic retrieval provides lightweight initial context; bdh_query remains available when the model needs a targeted follow-up.
Example tool input:
{
"query": "How did we recover the Hermes session database?"
}The tool returns a compact JSON result containing:
- up to 10 activated notes with scores
- BDH's generated response
- newly created concepts
- Hebbian update count
- neuron and synapse counts
Without safeguards, a graph-backed agent can create this loop:
BDH context → Hermes response → BDH indexes the response → same context is reinforced
This plugin prevents that in three ways:
- The original user message is the primary
query/embedding seed. - The assistant response is passed separately as
user_prompt. - Assistant-originated writes use
source: "assistant_response", enabling dampened Hebbian updates on the server.
If no user message was captured, the write is skipped entirely. Embedding an orphaned assistant response would be exactly the sort of clever nonsense that makes a graph worse.
BDH requests are made through a small HTTP helper with configurable base URL and bounded timeouts.
| Path | Timeout | Attempts | Timeout retry |
|---|---|---|---|
| Rewrite LLM (classify + rewrite) | 15s | 1 | N/A — falls back to mechanical gate |
| Automatic read hook | 2s | 1 | N/A |
| Automatic write hook | 30s | 2 total | No |
bdh_query tool |
30s | 2 total | No |
bdh_stats tool |
5s | 1 | N/A |
The POST /api/query endpoint is non-idempotent: BDH may have processed a request even if the client timed out. Therefore timeout errors are not retried, preventing duplicate Hebbian updates and duplicate neurogenesis.
If BDH is unreachable:
- the automatic hook logs a warning and Hermes continues normally;
bdh_queryreturns an actionable JSON error telling Hermes to answer from internal knowledge;bdh_statsreturns a JSON error instead of crashing the agent loop.
Other transient request failures can use the bounded retry path with exponential backoff. The current implementation is intentionally short and conservative rather than retrying for minutes while the agent waits.
User message + conversation_history
│
▼
pre_llm_call
│ captures user_message
│
├─ [if BDH_QUERY_REWRITE_ENABLED] ─────────────────────┐
│ extract context from conversation_history │
│ LLM classify + rewrite (OpenAI-compatible, 15s) │
│ should_retrieve=false → skip read │
│ store_candidate=false → skip write │
│ should_retrieve=true → rewritten retrieval query │
│ LLM failure → fallback to mechanical gate + raw │
│ ──────────────────────────────────────────────────────┘
│
▼
BDH /api/query (read-only, learn=false)
│
├── hybrid retrieval (Chroma KNN + BM25)
├── relevance gate (hybrid ≥ 0.30)
└── context injection [BDH CONTEXT]
│
Hermes LLM ────────────────┐
│ │ may call bdh_query
│ final response │ source: hermes_tool
▼ │
post_api_request │
│ │
│ if stop + >200 chars│
│ classification=false → skip write
│ source: assistant_response
▼ │
BDH /api/query ◄───────────┘
│
├── retrieval / activation
├── dampened Hebbian update (0.3)
├── quality propagation
└── neurogenesis when justified
- Hermes Agent with plugin support
- Hermes running in a gateway-managed/plugin-enabled process
- BDH Graph Harness running and exposing its HTTP API
- Default BDH endpoint:
http://localhost:8643
The endpoint can be overridden with:
export BDH_API_URL="http://127.0.0.1:8643"git clone https://github.com/albidev/bdh-hermes-bridge.git ~/.hermes/plugins/bdh-hermes-bridgegit clone https://github.com/albidev/bdh-hermes-bridge.git ~/Projects/bdh-hermes-bridge
ln -s ~/Projects/bdh-hermes-bridge ~/.hermes/plugins/bdh-hermes-bridgeEnable it in ~/.hermes/config.yaml:
plugins:
enabled:
- bdh-hermes-bridgeRestart the gateway after changing the plugin code or configuration:
hermes gateway restartPlugins are loaded at process startup. Editing __init__.py without restarting leaves the running gateway on the old implementation — a classic way to debug code that is not actually running.
plugin.yaml declares:
name: bdh-hermes-bridge
version: 0.8.0
kind: standalone
provides_hooks:
- pre_llm_call
- post_api_request
- post_tool_call
- transform_llm_output
provides_tools:
- bdh_query
- bdh_statsThe hooks and tools are registered explicitly in register(ctx).
The plugin uses these endpoints:
Required field:
query— query or embedding seed
Optional fields:
user_prompt— additional LLM/neurogenesis contextsource—assistant_responsefor dampened learning,hermes_toolfor normal tool-driven learning
Returns graph metrics including neurons, active/dormant neurons, synapses, Hebbian synapses, average degree, and processed query count.
Check plugin discovery and enabled status:
hermes plugins list --plain --no-bundledCheck the plugin files:
ls -la ~/.hermes/plugins/bdh-hermes-bridge/Verify BDH is reachable:
curl -sS http://localhost:8643/api/statsCheck registration and runtime activity in Hermes logs:
search="bdh-bridge"
rg "$search" ~/.hermes/logs/agent.log ~/.hermes/logs/errors.logA successful load logs:
[bdh-bridge] registered: hooks=[pre_llm_call, post_api_request], tools=[bdh_query, bdh_stats], api=http://localhost:8643
- The plugin does not invent queries to manufacture neurogenesis.
bdh_statsis read-only from the plugin's perspective.bdh_queryis synchronous because Hermes needs its result before continuing; use it selectively.- Automatic writes are asynchronous and do not alter the assistant response.
- BDH may be temporarily unavailable during consolidation; this is handled as a soft failure.
- The plugin catches hook exceptions so a BDH problem does not take down Hermes.
MIT
