hermes-memory-pgvector 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hermes_memory_pgvector-0.3.0 → hermes_memory_pgvector-0.4.0}/PKG-INFO +51 -9
- {hermes_memory_pgvector-0.3.0 → hermes_memory_pgvector-0.4.0}/README.md +269 -227
- {hermes_memory_pgvector-0.3.0 → hermes_memory_pgvector-0.4.0}/hermes_memory_pgvector.egg-info/PKG-INFO +51 -9
- {hermes_memory_pgvector-0.3.0 → hermes_memory_pgvector-0.4.0}/hermes_memory_pgvector.egg-info/SOURCES.txt +7 -1
- hermes_memory_pgvector-0.4.0/hermes_memory_pgvector.egg-info/entry_points.txt +2 -0
- {hermes_memory_pgvector-0.3.0 → hermes_memory_pgvector-0.4.0}/pgvector/__init__.py +1046 -806
- hermes_memory_pgvector-0.4.0/pgvector/__main__.py +233 -0
- {hermes_memory_pgvector-0.3.0 → hermes_memory_pgvector-0.4.0}/pgvector/embed.py +125 -103
- hermes_memory_pgvector-0.4.0/pgvector/identity.py +131 -0
- hermes_memory_pgvector-0.4.0/pgvector/migrations/002_agent_attribution.sql +130 -0
- hermes_memory_pgvector-0.4.0/pgvector/plugin.yaml +10 -0
- hermes_memory_pgvector-0.4.0/pgvector/store.py +931 -0
- {hermes_memory_pgvector-0.3.0 → hermes_memory_pgvector-0.4.0}/pyproject.toml +72 -67
- hermes_memory_pgvector-0.4.0/tests/test_identity.py +130 -0
- hermes_memory_pgvector-0.4.0/tests/test_store_v04.py +190 -0
- hermes_memory_pgvector-0.3.0/pgvector/plugin.yaml +0 -9
- hermes_memory_pgvector-0.3.0/pgvector/store.py +0 -507
- {hermes_memory_pgvector-0.3.0 → hermes_memory_pgvector-0.4.0}/LICENSE +0 -0
- {hermes_memory_pgvector-0.3.0 → hermes_memory_pgvector-0.4.0}/hermes_memory_pgvector.egg-info/dependency_links.txt +0 -0
- {hermes_memory_pgvector-0.3.0 → hermes_memory_pgvector-0.4.0}/hermes_memory_pgvector.egg-info/requires.txt +0 -0
- {hermes_memory_pgvector-0.3.0 → hermes_memory_pgvector-0.4.0}/hermes_memory_pgvector.egg-info/top_level.txt +0 -0
- {hermes_memory_pgvector-0.3.0 → hermes_memory_pgvector-0.4.0}/pgvector/migrations/001_schema.sql +0 -0
- {hermes_memory_pgvector-0.3.0 → hermes_memory_pgvector-0.4.0}/pgvector/writer.py +0 -0
- {hermes_memory_pgvector-0.3.0 → hermes_memory_pgvector-0.4.0}/setup.cfg +0 -0
- {hermes_memory_pgvector-0.3.0 → hermes_memory_pgvector-0.4.0}/tests/test_smoke.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hermes-memory-pgvector
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Postgres + pgvector memory provider plugin for hermes-agent. Multi-agent storage layer with per-minion themes, async writer, no LLM in the memory hot path.
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Postgres + pgvector memory provider plugin for hermes-agent. Multi-agent storage layer with per-minion themes, identity governance, agent attribution + delegation provenance, async writer, no LLM in the memory hot path.
|
|
5
5
|
Author: Andrea Borghi
|
|
6
6
|
License: BSD-3-Clause
|
|
7
7
|
Project-URL: Homepage, https://github.com/andreab67/hermes-memory-pgvector
|
|
@@ -75,11 +75,37 @@ What was missing: a **storage layer** that gives the built-in `memory` model dur
|
|
|
75
75
|
|
|
76
76
|
Internals:
|
|
77
77
|
|
|
78
|
-
- **`psycopg_pool.ConnectionPool`** (min=
|
|
78
|
+
- **`psycopg_pool.ConnectionPool`** (min=0, max=4, lazy + thread-safe, `max_idle=30s` / `max_lifetime=300s`) shared across the agent thread and the async-writer drain thread. `min_size=0` keeps an idle — or abandoned — pool at **zero** open connections, so a session the gateway never explicitly shuts down cannot strand a Postgres backend (see *Fixed in v0.3.1* below).
|
|
79
79
|
- **`AsyncWriter`** — bounded queue + daemon drain thread. Memory write hooks return in microseconds. Worker embeds + writes in the background. Crash-resilient (auto-restart on next enqueue).
|
|
80
80
|
- **Single migration** (`pgvector/migrations/001_schema.sql`) — `memory_entries` + `conversations` + HNSW indexes. Same tuning operators typically use elsewhere.
|
|
81
81
|
- **Boilerplate filter** for turn capture — length floor + acknowledgement regex (`"ok"`, `"thanks"`, `"continue"`, …) so the recall table stays high-signal.
|
|
82
82
|
|
|
83
|
+
### Fixed in v0.3.1 — connection-leak hotfix
|
|
84
|
+
|
|
85
|
+
A single registered provider has `initialize()` called again for each new session. It previously
|
|
86
|
+
reassigned `self._store` / `self._writer` without closing the prior ones, **abandoning a
|
|
87
|
+
`ConnectionPool`** whose warm (`min_size=1`) connection lingered in Postgres — committed-but-idle —
|
|
88
|
+
until the server's `idle_session_timeout`. Under a burst of concurrent sessions (e.g. a swarm of
|
|
89
|
+
systemd-run minions firing on the same minute) these orphaned backends saturated the database's
|
|
90
|
+
connection slots. Fixed by:
|
|
91
|
+
|
|
92
|
+
1. **`initialize()` teardown** — drain the prior `AsyncWriter` + close the prior pool before
|
|
93
|
+
re-initializing (the call is idempotent and skipped on first init).
|
|
94
|
+
2. **Self-draining pool** — `min_size=0` (an idle or abandoned pool holds *zero* connections) plus
|
|
95
|
+
`max_idle=30s` / `max_lifetime=300s`, so connections are short-lived when idle and pooled only
|
|
96
|
+
under active load.
|
|
97
|
+
|
|
98
|
+
## New in v0.4.0
|
|
99
|
+
|
|
100
|
+
Four capabilities, all storage-layer (still no LLM in the hot path):
|
|
101
|
+
|
|
102
|
+
- **Identity governance.** The resolved `agent_identity` is normalized once at init: direct-message session keys like `agent:main:whatsapp:dm:<phone>` collapse to a single `whatsapp-dm` bucket (no PII, no per-contact theme explosion), benchmark traffic (`skill-bench*`) is isolated to `_bench`, and an optional `allowed_themes` allow-list routes typo'd/unknown themes to `default`. The M3 resolution priority is preserved — normalization runs *after* the chain, never at read time. See [`pgvector/identity.py`](pgvector/identity.py).
|
|
103
|
+
- **Agent attribution + delegation (M4).** Migration `002` adds `memory_agents` (registry) and `memory_agent_edges` (parent→child delegation provenance) plus `conversations.parent_session_id`. The `on_delegation` / `on_session_end` hooks capture which agent delegated what to whom — strictly enqueue-only and fail-soft. **Provenance only** (who/when), never a fact-store ontology. Query it via the `v_agent_memory` view.
|
|
104
|
+
- **Embedding backfill + writer resilience.** Rows written text-only during an embed-endpoint outage are no longer permanently unsearchable: `python -m pgvector backfill` re-embeds `NULL`-embedding rows (idempotent, 768-dim-guarded). The background writer gains a small bounded retry; the hot path stays single-attempt.
|
|
105
|
+
- **Conversation TTL + embed policy.** `python -m pgvector prune --days N` trims old turns (operator-triggered only; `memory_entries` are never pruned). `conversation_embed_policy` (`all` default / `substantive_only` / `none`) tunes embedding cost.
|
|
106
|
+
|
|
107
|
+
Maintenance CLI (`python -m pgvector`, installed as `hermes-pgvector`): `migrate · stats · backfill · prune · cleanup · remap` — destructive commands default to dry-run. v0.4.0 is a clean upgrade from v0.3.x: apply migration `002` to light up attribution/delegation; without it the new hooks no-op and everything else runs unchanged.
|
|
108
|
+
|
|
83
109
|
## Multi-agent / per-minion themes
|
|
84
110
|
|
|
85
111
|
Each systemd-run minion sets one header on its OpenAI client; everything else flows automatically:
|
|
@@ -94,10 +120,13 @@ client = AsyncOpenAI(
|
|
|
94
120
|
|
|
95
121
|
The gateway plumbs `X-Hermes-Session-Key` through as `gateway_session_key=…` in `MemoryProvider.initialize` kwargs. The plugin reads it with **priority over the profile default**, so `agent_identity='default'` from unprofiled API traffic does not collapse every minion into one shared scope.
|
|
96
122
|
|
|
97
|
-
Convention: lowercase, dash-separated, stable.
|
|
123
|
+
Convention: lowercase, dash-separated, stable. Active themes in this deployment:
|
|
124
|
+
|
|
125
|
+
- product/report themes: `marketing`, `sales`, `morning-report`, `morning-report-sr`, `sr-marketing`, `sr-cloud`
|
|
126
|
+
- per-worker minions: `agent-trading`, `agent-sre`, `agent-marketing`, `agent-gitlab`, `agent-cloud`, `agent-hermes`
|
|
127
|
+
- governed sinks (v0.4): `whatsapp-dm` (collapsed DM/session keys), `_bench` (benchmark traffic), `default` (last resort)
|
|
98
128
|
|
|
99
|
-
|
|
100
|
-
- `intraday-<agent_name>` for fan-out workers (e.g. `intraday-trading`, `intraday-sre`, `intraday-marketing`)
|
|
129
|
+
Set `plugins.pgvector.allowed_themes` to that product/worker list to enforce it — an unknown or typo'd header then falls back to `default` (with a one-time warning) instead of silently minting a new theme.
|
|
101
130
|
|
|
102
131
|
## Install
|
|
103
132
|
|
|
@@ -133,7 +162,14 @@ cp -r pgvector ~/.hermes/plugins/pgvector
|
|
|
133
162
|
sudo -u postgres psql -d <your-memory-db> \
|
|
134
163
|
-f ~/.hermes/plugins/pgvector/migrations/001_schema.sql
|
|
135
164
|
|
|
136
|
-
#
|
|
165
|
+
# v0.4.0: apply the agent-attribution migration too (adds memory_agents /
|
|
166
|
+
# memory_agent_edges / conversations.parent_session_id and the GRANTs the
|
|
167
|
+
# runtime role needs — it self-grants, so no extra OWNER step for these).
|
|
168
|
+
sudo -u postgres psql -d <your-memory-db> \
|
|
169
|
+
-f ~/.hermes/plugins/pgvector/migrations/002_agent_attribution.sql
|
|
170
|
+
# (or apply every migration in order: hermes-pgvector migrate --admin-dsn "user=postgres host=/var/run/postgresql dbname=<your-memory-db>")
|
|
171
|
+
|
|
172
|
+
# Hand ownership of the v0.1 tables to the hermes runtime role
|
|
137
173
|
sudo -u postgres psql -d <your-memory-db> -c "
|
|
138
174
|
ALTER TABLE memory_entries OWNER TO hermes;
|
|
139
175
|
ALTER SEQUENCE memory_entries_id_seq OWNER TO hermes;
|
|
@@ -165,6 +201,12 @@ plugins:
|
|
|
165
201
|
bulk_sync_on_init: true
|
|
166
202
|
sync_turns: true
|
|
167
203
|
turn_min_chars: 40
|
|
204
|
+
# --- v0.4 identity governance + maintenance ---
|
|
205
|
+
allowed_themes: [] # empty = governance off; a list enforces an allow-list
|
|
206
|
+
bench_mode: "bucket" # bucket -> _bench | reject -> default
|
|
207
|
+
conversation_embed_policy: "all" # all | substantive_only | none
|
|
208
|
+
ttl_days: 0 # 0 = off; only `pgvector prune` ever deletes (never automatic)
|
|
209
|
+
embed_write_retries: 2 # writer-path only; hot path stays single-attempt
|
|
168
210
|
```
|
|
169
211
|
|
|
170
212
|
The embed endpoint can be any OpenAI-compatible `/v1/embeddings` or Ollama-native `/api/embed` URL that returns **768-dim vectors** (the schema is hard-coded to `vector(768)` to match `nomic-embed-text`). Use a different model only if it produces 768-dim output, or edit the migration before applying it.
|
|
@@ -221,8 +263,8 @@ See [`ROADMAP.md`](ROADMAP.md) for the full milestone table. Highlights:
|
|
|
221
263
|
- **M1 (v0.1, v0.1.1)** ✅ Shared storage with per-agent themes, async writer, connection pool, bulk import from `MEMORY.md`/`USER.md`
|
|
222
264
|
- **M2 (v0.2)** ✅ Conversation transcript table with `sync_turn` capture + `recall_conversation` tool
|
|
223
265
|
- **M3 (v0.3)** ✅ Identity propagation for stateless API minions via `X-Hermes-Session-Key`
|
|
224
|
-
- **M4 (v0.4)**
|
|
225
|
-
- **M5 (v0.5–v0.6)** ⏳
|
|
266
|
+
- **M4 (v0.4)** ✅ Identity governance + `on_delegation()`/`on_session_end()` capture + agent attribution (`memory_agents`/`memory_agent_edges`), embedding backfill, conversation TTL, maintenance CLI
|
|
267
|
+
- **M5 (v0.5–v0.6)** ⏳ Decay scoring, partial HNSW indexes per-theme, Prometheus metrics, cross-provider bulk-import
|
|
226
268
|
- **M6 (v1.0)** ⏳ Stable config schema, full docs, CI coverage
|
|
227
269
|
|
|
228
270
|
The roadmap exists so the multi-agent positioning isn't a one-off claim — each milestone has to pass the test *"does this make N cooperating agents more capable?"* before it lands. The `What's not on the roadmap` section in `ROADMAP.md` lists what was deliberately rejected (LLM-mediated dialectic, fact-store ontologies, background derivers, in-plugin RBAC) so the boundaries are explicit.
|
|
@@ -1,227 +1,269 @@
|
|
|
1
|
-
# hermes-memory-pgvector
|
|
2
|
-
|
|
3
|
-
**Postgres + pgvector memory provider for [hermes-agent](https://github.com/NousResearch/hermes-agent).** A shared memory substrate for a fleet of cooperating hermes-agent minions — built on Postgres and a single embedding endpoint you probably already run, with no LLM in the memory hot path.
|
|
4
|
-
|
|
5
|
-
```text
|
|
6
|
-
each minion → X-Hermes-Session-Key: <theme>
|
|
7
|
-
→ hermes-agent gateway
|
|
8
|
-
→ pgvector plugin
|
|
9
|
-
├── memory_entries (mirrors built-in MEMORY.md / USER.md per theme)
|
|
10
|
-
└── conversations (every substantive turn, semantically searchable)
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## Why it exists
|
|
14
|
-
|
|
15
|
-
Existing memory providers each solve a piece of the problem; the gap for **fleet deployments** is wide:
|
|
16
|
-
|
|
17
|
-
- **Built-in `memory` tool** persists to per-host `MEMORY.md` / `USER.md`. Two minions on the same host stomp on each other; minions on different hosts have no shared substrate.
|
|
18
|
-
- **Honcho** offers cross-session user modelling but requires a full external service, an LLM in the memory hot path for its deriver + dialectic loops, and its own ontology layered on top of the built-in tool. In high-concurrency fleet use it produces retry storms, embedding-endpoint queue backups, and gateway↔Honcho circular dependencies.
|
|
19
|
-
- **Holographic** is a fine in-process fact store but uses SQLite — a poor fit for many minions writing concurrently from many hosts.
|
|
20
|
-
- **Other providers** (Mem0, Hindsight, OpenViking, ByteRover, RetainDB, Supermemory) all either require a paid cloud, require LLM mediation for memory ops, or both.
|
|
21
|
-
|
|
22
|
-
What was missing: a **storage layer** that gives the built-in `memory` model durable, multi-tenant, semantically-searchable backing, with no LLM in the hot path, scoped cleanly per-minion so a marketing agent's notes don't pollute a trading agent's recall. That's what this plugin provides.
|
|
23
|
-
|
|
24
|
-
## Design philosophy
|
|
25
|
-
|
|
26
|
-
1. **Storage layer, not a memory model.** The agent keeps using `memory(action='add', target='memory'|'user', …)`. We mirror those writes via `on_memory_write`. No new ontology for the agent to learn.
|
|
27
|
-
2. **No LLM in the memory hot path.** Embeddings are vector math, not LLM calls. There is no deriver, no dialectic, no dream cycle — the failure modes that hurt Honcho cannot occur here by construction.
|
|
28
|
-
3. **Per-agent themes by default, cross-theme recall on explicit demand.** Every row carries `agent_identity` (resolved from `X-Hermes-Session-Key` header, profile name, workspace, or `'default'`). Recall is scoped to the current theme unless the agent asks for `scope='all'`.
|
|
29
|
-
4. **Fail-soft everywhere.** Embed endpoint down → degrade to text-only writes. Async writer queue full → drop with a one-time warning. DB down → log + skip. No exception escapes into the agent loop.
|
|
30
|
-
5. **Admin/runtime separation.** DDL (`CREATE EXTENSION vector`, `CREATE TABLE`, `CREATE INDEX`) runs once with superuser. The runtime user has DML only on the migrated schema. `ensure_schema()` at runtime is verify-only with a clear `SchemaNotApplied` error if the operator forgot the migration.
|
|
31
|
-
|
|
32
|
-
## Features (v0.3.0)
|
|
33
|
-
|
|
34
|
-
| Hook / surface | Behavior |
|
|
35
|
-
|---|---|
|
|
36
|
-
| `initialize()` | Verifies schema, opens `psycopg_pool.ConnectionPool`, bulk-imports existing `MEMORY.md` + `USER.md` content. |
|
|
37
|
-
| `on_memory_write(action, target, content, meta)` | Mirrors built-in `memory` writes into `memory_entries` (add / replace / remove). |
|
|
38
|
-
| `sync_turn(user, assistant, session_id)` | Captures every substantive (`>= 40` chars + not boilerplate) chat turn into `conversations`. |
|
|
39
|
-
| `prefetch(query)` | Top-K semantically similar `memory_entries` in current theme, injected ambient. |
|
|
40
|
-
| `recall_memory(query, scope, target, limit)` tool | Explicit cross-theme search of durable memory entries. |
|
|
41
|
-
| `recall_conversation(query, scope, limit)` tool | Explicit search over past chat turns. `scope ∈ {current, session, all, <theme>}`. |
|
|
42
|
-
|
|
43
|
-
Internals:
|
|
44
|
-
|
|
45
|
-
- **`psycopg_pool.ConnectionPool`** (min=
|
|
46
|
-
- **`AsyncWriter`** — bounded queue + daemon drain thread. Memory write hooks return in microseconds. Worker embeds + writes in the background. Crash-resilient (auto-restart on next enqueue).
|
|
47
|
-
- **Single migration** (`pgvector/migrations/001_schema.sql`) — `memory_entries` + `conversations` + HNSW indexes. Same tuning operators typically use elsewhere.
|
|
48
|
-
- **Boilerplate filter** for turn capture — length floor + acknowledgement regex (`"ok"`, `"thanks"`, `"continue"`, …) so the recall table stays high-signal.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
plugins
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
#
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
#
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
1
|
+
# hermes-memory-pgvector
|
|
2
|
+
|
|
3
|
+
**Postgres + pgvector memory provider for [hermes-agent](https://github.com/NousResearch/hermes-agent).** A shared memory substrate for a fleet of cooperating hermes-agent minions — built on Postgres and a single embedding endpoint you probably already run, with no LLM in the memory hot path.
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
each minion → X-Hermes-Session-Key: <theme>
|
|
7
|
+
→ hermes-agent gateway
|
|
8
|
+
→ pgvector plugin
|
|
9
|
+
├── memory_entries (mirrors built-in MEMORY.md / USER.md per theme)
|
|
10
|
+
└── conversations (every substantive turn, semantically searchable)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Why it exists
|
|
14
|
+
|
|
15
|
+
Existing memory providers each solve a piece of the problem; the gap for **fleet deployments** is wide:
|
|
16
|
+
|
|
17
|
+
- **Built-in `memory` tool** persists to per-host `MEMORY.md` / `USER.md`. Two minions on the same host stomp on each other; minions on different hosts have no shared substrate.
|
|
18
|
+
- **Honcho** offers cross-session user modelling but requires a full external service, an LLM in the memory hot path for its deriver + dialectic loops, and its own ontology layered on top of the built-in tool. In high-concurrency fleet use it produces retry storms, embedding-endpoint queue backups, and gateway↔Honcho circular dependencies.
|
|
19
|
+
- **Holographic** is a fine in-process fact store but uses SQLite — a poor fit for many minions writing concurrently from many hosts.
|
|
20
|
+
- **Other providers** (Mem0, Hindsight, OpenViking, ByteRover, RetainDB, Supermemory) all either require a paid cloud, require LLM mediation for memory ops, or both.
|
|
21
|
+
|
|
22
|
+
What was missing: a **storage layer** that gives the built-in `memory` model durable, multi-tenant, semantically-searchable backing, with no LLM in the hot path, scoped cleanly per-minion so a marketing agent's notes don't pollute a trading agent's recall. That's what this plugin provides.
|
|
23
|
+
|
|
24
|
+
## Design philosophy
|
|
25
|
+
|
|
26
|
+
1. **Storage layer, not a memory model.** The agent keeps using `memory(action='add', target='memory'|'user', …)`. We mirror those writes via `on_memory_write`. No new ontology for the agent to learn.
|
|
27
|
+
2. **No LLM in the memory hot path.** Embeddings are vector math, not LLM calls. There is no deriver, no dialectic, no dream cycle — the failure modes that hurt Honcho cannot occur here by construction.
|
|
28
|
+
3. **Per-agent themes by default, cross-theme recall on explicit demand.** Every row carries `agent_identity` (resolved from `X-Hermes-Session-Key` header, profile name, workspace, or `'default'`). Recall is scoped to the current theme unless the agent asks for `scope='all'`.
|
|
29
|
+
4. **Fail-soft everywhere.** Embed endpoint down → degrade to text-only writes. Async writer queue full → drop with a one-time warning. DB down → log + skip. No exception escapes into the agent loop.
|
|
30
|
+
5. **Admin/runtime separation.** DDL (`CREATE EXTENSION vector`, `CREATE TABLE`, `CREATE INDEX`) runs once with superuser. The runtime user has DML only on the migrated schema. `ensure_schema()` at runtime is verify-only with a clear `SchemaNotApplied` error if the operator forgot the migration.
|
|
31
|
+
|
|
32
|
+
## Features (v0.3.0)
|
|
33
|
+
|
|
34
|
+
| Hook / surface | Behavior |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `initialize()` | Verifies schema, opens `psycopg_pool.ConnectionPool`, bulk-imports existing `MEMORY.md` + `USER.md` content. |
|
|
37
|
+
| `on_memory_write(action, target, content, meta)` | Mirrors built-in `memory` writes into `memory_entries` (add / replace / remove). |
|
|
38
|
+
| `sync_turn(user, assistant, session_id)` | Captures every substantive (`>= 40` chars + not boilerplate) chat turn into `conversations`. |
|
|
39
|
+
| `prefetch(query)` | Top-K semantically similar `memory_entries` in current theme, injected ambient. |
|
|
40
|
+
| `recall_memory(query, scope, target, limit)` tool | Explicit cross-theme search of durable memory entries. |
|
|
41
|
+
| `recall_conversation(query, scope, limit)` tool | Explicit search over past chat turns. `scope ∈ {current, session, all, <theme>}`. |
|
|
42
|
+
|
|
43
|
+
Internals:
|
|
44
|
+
|
|
45
|
+
- **`psycopg_pool.ConnectionPool`** (min=0, max=4, lazy + thread-safe, `max_idle=30s` / `max_lifetime=300s`) shared across the agent thread and the async-writer drain thread. `min_size=0` keeps an idle — or abandoned — pool at **zero** open connections, so a session the gateway never explicitly shuts down cannot strand a Postgres backend (see *Fixed in v0.3.1* below).
|
|
46
|
+
- **`AsyncWriter`** — bounded queue + daemon drain thread. Memory write hooks return in microseconds. Worker embeds + writes in the background. Crash-resilient (auto-restart on next enqueue).
|
|
47
|
+
- **Single migration** (`pgvector/migrations/001_schema.sql`) — `memory_entries` + `conversations` + HNSW indexes. Same tuning operators typically use elsewhere.
|
|
48
|
+
- **Boilerplate filter** for turn capture — length floor + acknowledgement regex (`"ok"`, `"thanks"`, `"continue"`, …) so the recall table stays high-signal.
|
|
49
|
+
|
|
50
|
+
### Fixed in v0.3.1 — connection-leak hotfix
|
|
51
|
+
|
|
52
|
+
A single registered provider has `initialize()` called again for each new session. It previously
|
|
53
|
+
reassigned `self._store` / `self._writer` without closing the prior ones, **abandoning a
|
|
54
|
+
`ConnectionPool`** whose warm (`min_size=1`) connection lingered in Postgres — committed-but-idle —
|
|
55
|
+
until the server's `idle_session_timeout`. Under a burst of concurrent sessions (e.g. a swarm of
|
|
56
|
+
systemd-run minions firing on the same minute) these orphaned backends saturated the database's
|
|
57
|
+
connection slots. Fixed by:
|
|
58
|
+
|
|
59
|
+
1. **`initialize()` teardown** — drain the prior `AsyncWriter` + close the prior pool before
|
|
60
|
+
re-initializing (the call is idempotent and skipped on first init).
|
|
61
|
+
2. **Self-draining pool** — `min_size=0` (an idle or abandoned pool holds *zero* connections) plus
|
|
62
|
+
`max_idle=30s` / `max_lifetime=300s`, so connections are short-lived when idle and pooled only
|
|
63
|
+
under active load.
|
|
64
|
+
|
|
65
|
+
## New in v0.4.0
|
|
66
|
+
|
|
67
|
+
Four capabilities, all storage-layer (still no LLM in the hot path):
|
|
68
|
+
|
|
69
|
+
- **Identity governance.** The resolved `agent_identity` is normalized once at init: direct-message session keys like `agent:main:whatsapp:dm:<phone>` collapse to a single `whatsapp-dm` bucket (no PII, no per-contact theme explosion), benchmark traffic (`skill-bench*`) is isolated to `_bench`, and an optional `allowed_themes` allow-list routes typo'd/unknown themes to `default`. The M3 resolution priority is preserved — normalization runs *after* the chain, never at read time. See [`pgvector/identity.py`](pgvector/identity.py).
|
|
70
|
+
- **Agent attribution + delegation (M4).** Migration `002` adds `memory_agents` (registry) and `memory_agent_edges` (parent→child delegation provenance) plus `conversations.parent_session_id`. The `on_delegation` / `on_session_end` hooks capture which agent delegated what to whom — strictly enqueue-only and fail-soft. **Provenance only** (who/when), never a fact-store ontology. Query it via the `v_agent_memory` view.
|
|
71
|
+
- **Embedding backfill + writer resilience.** Rows written text-only during an embed-endpoint outage are no longer permanently unsearchable: `python -m pgvector backfill` re-embeds `NULL`-embedding rows (idempotent, 768-dim-guarded). The background writer gains a small bounded retry; the hot path stays single-attempt.
|
|
72
|
+
- **Conversation TTL + embed policy.** `python -m pgvector prune --days N` trims old turns (operator-triggered only; `memory_entries` are never pruned). `conversation_embed_policy` (`all` default / `substantive_only` / `none`) tunes embedding cost.
|
|
73
|
+
|
|
74
|
+
Maintenance CLI (`python -m pgvector`, installed as `hermes-pgvector`): `migrate · stats · backfill · prune · cleanup · remap` — destructive commands default to dry-run. v0.4.0 is a clean upgrade from v0.3.x: apply migration `002` to light up attribution/delegation; without it the new hooks no-op and everything else runs unchanged.
|
|
75
|
+
|
|
76
|
+
## Multi-agent / per-minion themes
|
|
77
|
+
|
|
78
|
+
Each systemd-run minion sets one header on its OpenAI client; everything else flows automatically:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
client = AsyncOpenAI(
|
|
82
|
+
base_url="http://127.0.0.1:8642/v1",
|
|
83
|
+
api_key=API_KEY,
|
|
84
|
+
default_headers={"X-Hermes-Session-Key": "marketing"}, # ← theme
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The gateway plumbs `X-Hermes-Session-Key` through as `gateway_session_key=…` in `MemoryProvider.initialize` kwargs. The plugin reads it with **priority over the profile default**, so `agent_identity='default'` from unprofiled API traffic does not collapse every minion into one shared scope.
|
|
89
|
+
|
|
90
|
+
Convention: lowercase, dash-separated, stable. Active themes in this deployment:
|
|
91
|
+
|
|
92
|
+
- product/report themes: `marketing`, `sales`, `morning-report`, `morning-report-sr`, `sr-marketing`, `sr-cloud`
|
|
93
|
+
- per-worker minions: `agent-trading`, `agent-sre`, `agent-marketing`, `agent-gitlab`, `agent-cloud`, `agent-hermes`
|
|
94
|
+
- governed sinks (v0.4): `whatsapp-dm` (collapsed DM/session keys), `_bench` (benchmark traffic), `default` (last resort)
|
|
95
|
+
|
|
96
|
+
Set `plugins.pgvector.allowed_themes` to that product/worker list to enforce it — an unknown or typo'd header then falls back to `default` (with a one-time warning) instead of silently minting a new theme.
|
|
97
|
+
|
|
98
|
+
## Install
|
|
99
|
+
|
|
100
|
+
### Option 1: clone + run the installer script (recommended)
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
git clone https://github.com/andreab67/hermes-memory-pgvector.git
|
|
104
|
+
cd hermes-memory-pgvector
|
|
105
|
+
./scripts/install.sh
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
That:
|
|
109
|
+
|
|
110
|
+
1. `pip install`s `psycopg[binary]`, `psycopg-pool`, `PyYAML` (with the upper-bound pins).
|
|
111
|
+
2. Copies `pgvector/` into `$HERMES_HOME/plugins/pgvector/` (defaults to `~/.hermes/plugins/pgvector/`).
|
|
112
|
+
3. Prints the admin migration + activation commands you run next.
|
|
113
|
+
|
|
114
|
+
### Option 2: manual
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# Python deps
|
|
118
|
+
pip install 'psycopg[binary]>=3.3.4,<4' 'psycopg-pool>=3.3.1,<4' 'PyYAML>=6.0,<7'
|
|
119
|
+
|
|
120
|
+
# Plugin module
|
|
121
|
+
mkdir -p ~/.hermes/plugins
|
|
122
|
+
cp -r pgvector ~/.hermes/plugins/pgvector
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Then (admin once)
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
# Apply the schema migration (CREATE EXTENSION needs superuser)
|
|
129
|
+
sudo -u postgres psql -d <your-memory-db> \
|
|
130
|
+
-f ~/.hermes/plugins/pgvector/migrations/001_schema.sql
|
|
131
|
+
|
|
132
|
+
# v0.4.0: apply the agent-attribution migration too (adds memory_agents /
|
|
133
|
+
# memory_agent_edges / conversations.parent_session_id and the GRANTs the
|
|
134
|
+
# runtime role needs — it self-grants, so no extra OWNER step for these).
|
|
135
|
+
sudo -u postgres psql -d <your-memory-db> \
|
|
136
|
+
-f ~/.hermes/plugins/pgvector/migrations/002_agent_attribution.sql
|
|
137
|
+
# (or apply every migration in order: hermes-pgvector migrate --admin-dsn "user=postgres host=/var/run/postgresql dbname=<your-memory-db>")
|
|
138
|
+
|
|
139
|
+
# Hand ownership of the v0.1 tables to the hermes runtime role
|
|
140
|
+
sudo -u postgres psql -d <your-memory-db> -c "
|
|
141
|
+
ALTER TABLE memory_entries OWNER TO hermes;
|
|
142
|
+
ALTER SEQUENCE memory_entries_id_seq OWNER TO hermes;
|
|
143
|
+
ALTER TABLE conversations OWNER TO hermes;
|
|
144
|
+
ALTER SEQUENCE conversations_id_seq OWNER TO hermes;
|
|
145
|
+
"
|
|
146
|
+
|
|
147
|
+
# Activate
|
|
148
|
+
hermes config set memory.provider pgvector
|
|
149
|
+
sudo systemctl restart hermes.service # or however you run hermes
|
|
150
|
+
hermes memory status # expect: Provider: pgvector; Status: available
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Configuration
|
|
154
|
+
|
|
155
|
+
Lives in `$HERMES_HOME/config.yaml` under `plugins.pgvector` — every value optional, sensible defaults shown:
|
|
156
|
+
|
|
157
|
+
```yaml
|
|
158
|
+
plugins:
|
|
159
|
+
pgvector:
|
|
160
|
+
dsn: "dbname=hermes_memory user=hermes host=/var/run/postgresql"
|
|
161
|
+
embed_url: "http://your-embed-endpoint:11434"
|
|
162
|
+
embed_model: "nomic-embed-text"
|
|
163
|
+
prefetch_limit: 5
|
|
164
|
+
min_similarity: 0.30
|
|
165
|
+
embed_on_write: true
|
|
166
|
+
scope_default: "current"
|
|
167
|
+
write_queue_maxsize: 256
|
|
168
|
+
bulk_sync_on_init: true
|
|
169
|
+
sync_turns: true
|
|
170
|
+
turn_min_chars: 40
|
|
171
|
+
# --- v0.4 identity governance + maintenance ---
|
|
172
|
+
allowed_themes: [] # empty = governance off; a list enforces an allow-list
|
|
173
|
+
bench_mode: "bucket" # bucket -> _bench | reject -> default
|
|
174
|
+
conversation_embed_policy: "all" # all | substantive_only | none
|
|
175
|
+
ttl_days: 0 # 0 = off; only `pgvector prune` ever deletes (never automatic)
|
|
176
|
+
embed_write_retries: 2 # writer-path only; hot path stays single-attempt
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The embed endpoint can be any OpenAI-compatible `/v1/embeddings` or Ollama-native `/api/embed` URL that returns **768-dim vectors** (the schema is hard-coded to `vector(768)` to match `nomic-embed-text`). Use a different model only if it produces 768-dim output, or edit the migration before applying it.
|
|
180
|
+
|
|
181
|
+
## Schema
|
|
182
|
+
|
|
183
|
+
```sql
|
|
184
|
+
CREATE TABLE memory_entries (
|
|
185
|
+
id BIGSERIAL PRIMARY KEY,
|
|
186
|
+
agent_identity TEXT NOT NULL DEFAULT 'default',
|
|
187
|
+
target TEXT NOT NULL CHECK (target IN ('memory', 'user')),
|
|
188
|
+
content TEXT NOT NULL,
|
|
189
|
+
embedding vector(768),
|
|
190
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
191
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
192
|
+
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
193
|
+
UNIQUE (agent_identity, target, content)
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
CREATE TABLE conversations (
|
|
197
|
+
id BIGSERIAL PRIMARY KEY,
|
|
198
|
+
session_id TEXT NOT NULL,
|
|
199
|
+
agent_identity TEXT NOT NULL DEFAULT 'default',
|
|
200
|
+
role TEXT NOT NULL CHECK (role IN ('user','assistant','system','tool')),
|
|
201
|
+
content TEXT NOT NULL,
|
|
202
|
+
ts TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
203
|
+
embedding vector(768),
|
|
204
|
+
metadata JSONB NOT NULL DEFAULT '{}'::jsonb
|
|
205
|
+
);
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Indexes: HNSW on each `embedding` column (m=16, ef_construction=64) plus per-agent + per-session btree timelines. Full DDL in [`pgvector/migrations/001_schema.sql`](pgvector/migrations/001_schema.sql).
|
|
209
|
+
|
|
210
|
+
## Tests
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
pip install -e ".[test]"
|
|
214
|
+
|
|
215
|
+
# Skip mode (no DB, no embed endpoint): everything skips gracefully
|
|
216
|
+
pytest tests/
|
|
217
|
+
|
|
218
|
+
# Live mode (against a throwaway Postgres + your embed endpoint)
|
|
219
|
+
export PG_TEST_DSN='dbname=hermes_test user=postgres host=/var/run/postgresql'
|
|
220
|
+
export PG_TEST_EMBED_URL='http://your-embed-endpoint:11434'
|
|
221
|
+
pytest tests/
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
DB tests skip when `PG_TEST_DSN` is unset; live embed tests skip when `PG_TEST_EMBED_URL` is unset.
|
|
225
|
+
|
|
226
|
+
## Roadmap
|
|
227
|
+
|
|
228
|
+
See [`ROADMAP.md`](ROADMAP.md) for the full milestone table. Highlights:
|
|
229
|
+
|
|
230
|
+
- **M1 (v0.1, v0.1.1)** ✅ Shared storage with per-agent themes, async writer, connection pool, bulk import from `MEMORY.md`/`USER.md`
|
|
231
|
+
- **M2 (v0.2)** ✅ Conversation transcript table with `sync_turn` capture + `recall_conversation` tool
|
|
232
|
+
- **M3 (v0.3)** ✅ Identity propagation for stateless API minions via `X-Hermes-Session-Key`
|
|
233
|
+
- **M4 (v0.4)** ✅ Identity governance + `on_delegation()`/`on_session_end()` capture + agent attribution (`memory_agents`/`memory_agent_edges`), embedding backfill, conversation TTL, maintenance CLI
|
|
234
|
+
- **M5 (v0.5–v0.6)** ⏳ Decay scoring, partial HNSW indexes per-theme, Prometheus metrics, cross-provider bulk-import
|
|
235
|
+
- **M6 (v1.0)** ⏳ Stable config schema, full docs, CI coverage
|
|
236
|
+
|
|
237
|
+
The roadmap exists so the multi-agent positioning isn't a one-off claim — each milestone has to pass the test *"does this make N cooperating agents more capable?"* before it lands. The `What's not on the roadmap` section in `ROADMAP.md` lists what was deliberately rejected (LLM-mediated dialectic, fact-store ontologies, background derivers, in-plugin RBAC) so the boundaries are explicit.
|
|
238
|
+
|
|
239
|
+
## Rollback
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
hermes config set memory.provider none
|
|
243
|
+
sudo systemctl restart hermes.service
|
|
244
|
+
|
|
245
|
+
# Optional — drop the tables (data loss, irreversible)
|
|
246
|
+
sudo -u postgres psql -d <your-memory-db> -c "
|
|
247
|
+
DROP TABLE IF EXISTS conversations;
|
|
248
|
+
DROP TABLE IF EXISTS memory_entries;
|
|
249
|
+
"
|
|
250
|
+
|
|
251
|
+
# Optional — remove the plugin files
|
|
252
|
+
rm -rf ~/.hermes/plugins/pgvector
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Why a standalone plugin (not an upstream PR)?
|
|
256
|
+
|
|
257
|
+
Per the hermes-agent [`CONTRIBUTING.md`](https://github.com/NousResearch/hermes-agent/blob/main/CONTRIBUTING.md):
|
|
258
|
+
|
|
259
|
+
> We are no longer accepting new memory providers into this repo. The set of built-in providers under `plugins/memory/` is closed. If you want to add a new memory backend, publish it as a standalone plugin repo that users install into `~/.hermes/plugins/` (or via a pip entry point).
|
|
260
|
+
|
|
261
|
+
The discovery system (`plugins/memory/__init__.py` in hermes-agent) scans `$HERMES_HOME/plugins/<name>/` for any directory whose `__init__.py` calls `register_memory_provider`. This plugin's `pgvector/__init__.py` does exactly that — no upstream change required.
|
|
262
|
+
|
|
263
|
+
## Contributing
|
|
264
|
+
|
|
265
|
+
Bug reports + PRs welcome. Open an issue describing the failure mode + your environment (hermes-agent version, Postgres version, embed endpoint), or a PR with a focused change + test.
|
|
266
|
+
|
|
267
|
+
## License
|
|
268
|
+
|
|
269
|
+
[BSD 3-Clause](LICENSE) © 2026 Andrea Borghi.
|