hermes-memory-pgvector 0.6.0__tar.gz → 1.0.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.6.0 → hermes_memory_pgvector-1.0.0}/LICENSE +1 -1
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/PKG-INFO +26 -7
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/README.md +24 -5
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/hermes_memory_pgvector.egg-info/PKG-INFO +26 -7
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/hermes_memory_pgvector.egg-info/SOURCES.txt +6 -1
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/hermes_pgvector/__init__.py +157 -40
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/hermes_pgvector/__main__.py +9 -5
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/hermes_pgvector/embed.py +18 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/hermes_pgvector/identity.py +17 -3
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/hermes_pgvector/plugin.yaml +1 -1
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/hermes_pgvector/store.py +134 -27
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/pyproject.toml +2 -2
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/tests/test_config_coercion.py +8 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/tests/test_embed_timeouts.py +25 -1
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/tests/test_empty_content.py +70 -5
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/tests/test_identity.py +49 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/tests/test_install_shim.py +4 -1
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/tests/test_read_side_gate.py +39 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/tests/test_save_config_merge.py +15 -18
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/tests/test_store_v04.py +15 -12
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/tests/test_turn_dedup.py +5 -4
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/tests/test_v060_migrations_cli.py +50 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/tests/test_v060_provider_writes.py +48 -6
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/tests/test_v060_store.py +10 -8
- hermes_memory_pgvector-1.0.0/tests/test_v100_pass2_provider.py +213 -0
- hermes_memory_pgvector-1.0.0/tests/test_v100_pass2_store.py +277 -0
- hermes_memory_pgvector-1.0.0/tests/test_v100_pass3.py +99 -0
- hermes_memory_pgvector-1.0.0/tests/test_v100_provider_fixes.py +343 -0
- hermes_memory_pgvector-1.0.0/tests/test_v100_review_fixes.py +239 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/hermes_memory_pgvector.egg-info/dependency_links.txt +0 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/hermes_memory_pgvector.egg-info/entry_points.txt +0 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/hermes_memory_pgvector.egg-info/requires.txt +0 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/hermes_memory_pgvector.egg-info/top_level.txt +0 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/hermes_pgvector/migrations/001_schema.sql +0 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/hermes_pgvector/migrations/002_agent_attribution.sql +0 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/hermes_pgvector/migrations/003_hybrid_search_fts.sql +0 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/hermes_pgvector/migrations/004_runtime_grants.sql +0 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/hermes_pgvector/migrations/005_content_md5_unique.sql +0 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/hermes_pgvector/writer.py +0 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/setup.cfg +0 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/tests/test_async_writer.py +0 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/tests/test_embed_config.py +0 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/tests/test_exclude_identities_live.py +0 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/tests/test_fake_embed_server.py +0 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/tests/test_hybrid_search.py +0 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/tests/test_loader_embed_clobber.py +0 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/tests/test_session_switch_contract.py +0 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/tests/test_smoke.py +0 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/tests/test_system_prompt_block.py +0 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/tests/test_tool_args_hardening.py +0 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/tests/test_v060_hook_hardening.py +0 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/tests/test_v060_identity_writer.py +0 -0
- {hermes_memory_pgvector-0.6.0 → hermes_memory_pgvector-1.0.0}/tests/test_v060_provider_reads.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hermes-memory-pgvector
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
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
|
|
@@ -9,7 +9,7 @@ Project-URL: Source, https://github.com/andreab67/hermes-memory-pgvector
|
|
|
9
9
|
Project-URL: Issues, https://github.com/andreab67/hermes-memory-pgvector/issues
|
|
10
10
|
Project-URL: Roadmap, https://github.com/andreab67/hermes-memory-pgvector/blob/main/ROADMAP.md
|
|
11
11
|
Keywords: hermes-agent,memory-provider,pgvector,postgres,multi-agent,llm-memory,semantic-search
|
|
12
|
-
Classifier: Development Status ::
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
14
|
Classifier: Intended Audience :: System Administrators
|
|
15
15
|
Classifier: License :: OSI Approved :: BSD License
|
|
@@ -73,7 +73,7 @@ What was missing: a **storage layer** that gives the built-in `memory` model dur
|
|
|
73
73
|
| `initialize()` | Verifies schema, opens a `psycopg_pool.ConnectionPool`, bulk-imports existing `MEMORY.md` + `USER.md` content. |
|
|
74
74
|
| `on_memory_write(action, target, content, meta)` | Mirrors built-in `memory` writes into `memory_entries` (add / replace / remove). |
|
|
75
75
|
| `sync_turn(user, assistant, session_id)` / `on_session_end(messages)` | Captures every substantive chat turn into `conversations`; `on_session_end` is a dedup'd backstop. |
|
|
76
|
-
| `on_delegation(task, result, …)` | Records parent→child delegation provenance (`memory_agent_edges`) and stores the exchange as a recallable turn. |
|
|
76
|
+
| `on_delegation(task, result, …)` | Records parent→child delegation provenance (`memory_agent_edges`) and stores the exchange as a recallable turn. `memory_agent_edges.child_identity` is only filled when the host passes a child identity; current upstream hermes-agent passes only the task, result and `child_session_id`, so it is NULL there (`child_session_id` is populated). |
|
|
77
77
|
| `prefetch(query)` / `queue_prefetch(query, session_id=...)` | Ambient recall: top-K similar `memory_entries` for the current theme, injected into the system prompt. `queue_prefetch` pre-computes it off the agent thread. |
|
|
78
78
|
| `recall_memory(query, scope, target, limit)` tool | Explicit cross-theme search of durable memory entries. |
|
|
79
79
|
| `recall_conversation(query, scope, limit)` tool | Explicit search over past chat turns. `scope ∈ {current, session, all, <theme>}`. |
|
|
@@ -121,7 +121,7 @@ mkdir -p ~/.hermes/plugins
|
|
|
121
121
|
cp -r hermes_pgvector ~/.hermes/plugins/pgvector
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
-
Then apply migrations and activate as in Option 1 above (`hermes-pgvector migrate --admin-dsn ...`, or `psql -f` each `migrations/*.sql` in order — see [docs/operations.md](docs/operations.md) for the full admin walkthrough and [docs/upgrading.md](docs/upgrading.md) if you're coming from an older version).
|
|
124
|
+
Then apply migrations and activate as in Option 1 above (`hermes-pgvector migrate --admin-dsn ...`, or `psql -f` each `hermes_pgvector/migrations/*.sql` in order — see [docs/operations.md](docs/operations.md) for the full admin walkthrough and [docs/upgrading.md](docs/upgrading.md) if you're coming from an older version).
|
|
125
125
|
|
|
126
126
|
## Configuration
|
|
127
127
|
|
|
@@ -152,11 +152,14 @@ client = AsyncOpenAI(
|
|
|
152
152
|
|
|
153
153
|
The gateway plumbs `X-Hermes-Session-Key` through as `gateway_session_key=…` in `MemoryProvider.initialize` kwargs, taking priority over the profile default so unprofiled API traffic doesn't collapse every minion into one shared `default` scope.
|
|
154
154
|
|
|
155
|
-
|
|
155
|
+
Identity resolution order in `initialize`: `gateway_session_key` > `agent_identity` (unless it is `default`) > `agent_workspace` > `default`. Upstream hermes-agent always passes `agent_workspace="hermes"` (and `agent_identity=<profile name>`), so a default-profile CLI or cron session with no gateway session key lands in the theme `hermes`, not `default`; named profiles land in their profile name. Set the `X-Hermes-Session-Key` header (or a non-default profile) if you want a scope other than `hermes`. When `allowed_themes` is set, `hermes` and your profile names are not exempt: include them in the list if you want those sessions to keep their own theme, otherwise they fall back to `default`.
|
|
156
|
+
|
|
157
|
+
Convention: lowercase, dash-separated, stable (identities are case-folded automatically — `Marketing` and `marketing` are the same theme). Governed sinks that always exist regardless of your themes: `whatsapp-dm` (collapsed DM/session keys), `external-group` (collapsed group/channel/thread keys), `_bench` (benchmark traffic), `default` (only when the host passes no identity kwargs at all). Set `plugins.pgvector.allowed_themes` to your product/worker list to enforce an allow-list — an unknown or typo'd header then falls back to `default` (one-time warning) instead of silently minting a new theme.
|
|
156
158
|
|
|
157
159
|
## Compatibility policy
|
|
158
160
|
|
|
159
|
-
This plugin follows semantic versioning on its **public surface
|
|
161
|
+
This plugin follows semantic versioning on its **public surface**, stable as
|
|
162
|
+
of `1.0.0`:
|
|
160
163
|
|
|
161
164
|
- `plugins.pgvector.*` config keys (see [docs/configuration.md](docs/configuration.md))
|
|
162
165
|
- Tool names and parameters (`recall_memory`, `recall_conversation`)
|
|
@@ -164,7 +167,11 @@ This plugin follows semantic versioning on its **public surface**:
|
|
|
164
167
|
- Database table/column names
|
|
165
168
|
- The `pgvector` provider name and the package's pip entry point
|
|
166
169
|
|
|
167
|
-
|
|
170
|
+
Within the `1.x` series this surface is guaranteed stable: it may grow (new
|
|
171
|
+
config keys, new optional params), but nothing already listed is renamed or
|
|
172
|
+
removed, and no default changes behaviour, without a `2.0` release.
|
|
173
|
+
|
|
174
|
+
`MemoryStore` and every other Python class/module are **internal** — not covered by semver, safe to change between minor releases, and excluded from the `1.x` guarantee above. A migration file, once released, is never edited in place; schema changes land as a new numbered migration.
|
|
168
175
|
|
|
169
176
|
## Support matrix
|
|
170
177
|
|
|
@@ -198,6 +205,18 @@ pytest -q # no DB/embed endpoint: everything
|
|
|
198
205
|
# Live mode (against a throwaway Postgres + your embed endpoint)
|
|
199
206
|
export PG_TEST_DSN='dbname=hermes_test user=postgres host=/var/run/postgresql'
|
|
200
207
|
export PG_TEST_EMBED_URL='http://your-embed-endpoint:11434'
|
|
208
|
+
# Superuser DSN for the same database: the migration, CLI and empty-content
|
|
209
|
+
# tests skip without it, and CI fails on any skip.
|
|
210
|
+
export PG_TEST_ADMIN_DSN='dbname=hermes_test user=postgres host=/var/run/postgresql'
|
|
211
|
+
pytest -q
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The simplest path is the bundled harness (Postgres + pgvector in Docker and a
|
|
215
|
+
fake embed server), which prints all the variables for you:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
scripts/test-env.sh up pg17
|
|
219
|
+
eval "$(scripts/test-env.sh db hermes_test)"
|
|
201
220
|
pytest -q
|
|
202
221
|
```
|
|
203
222
|
|
|
@@ -36,7 +36,7 @@ What was missing: a **storage layer** that gives the built-in `memory` model dur
|
|
|
36
36
|
| `initialize()` | Verifies schema, opens a `psycopg_pool.ConnectionPool`, bulk-imports existing `MEMORY.md` + `USER.md` content. |
|
|
37
37
|
| `on_memory_write(action, target, content, meta)` | Mirrors built-in `memory` writes into `memory_entries` (add / replace / remove). |
|
|
38
38
|
| `sync_turn(user, assistant, session_id)` / `on_session_end(messages)` | Captures every substantive chat turn into `conversations`; `on_session_end` is a dedup'd backstop. |
|
|
39
|
-
| `on_delegation(task, result, …)` | Records parent→child delegation provenance (`memory_agent_edges`) and stores the exchange as a recallable turn. |
|
|
39
|
+
| `on_delegation(task, result, …)` | Records parent→child delegation provenance (`memory_agent_edges`) and stores the exchange as a recallable turn. `memory_agent_edges.child_identity` is only filled when the host passes a child identity; current upstream hermes-agent passes only the task, result and `child_session_id`, so it is NULL there (`child_session_id` is populated). |
|
|
40
40
|
| `prefetch(query)` / `queue_prefetch(query, session_id=...)` | Ambient recall: top-K similar `memory_entries` for the current theme, injected into the system prompt. `queue_prefetch` pre-computes it off the agent thread. |
|
|
41
41
|
| `recall_memory(query, scope, target, limit)` tool | Explicit cross-theme search of durable memory entries. |
|
|
42
42
|
| `recall_conversation(query, scope, limit)` tool | Explicit search over past chat turns. `scope ∈ {current, session, all, <theme>}`. |
|
|
@@ -84,7 +84,7 @@ mkdir -p ~/.hermes/plugins
|
|
|
84
84
|
cp -r hermes_pgvector ~/.hermes/plugins/pgvector
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
Then apply migrations and activate as in Option 1 above (`hermes-pgvector migrate --admin-dsn ...`, or `psql -f` each `migrations/*.sql` in order — see [docs/operations.md](docs/operations.md) for the full admin walkthrough and [docs/upgrading.md](docs/upgrading.md) if you're coming from an older version).
|
|
87
|
+
Then apply migrations and activate as in Option 1 above (`hermes-pgvector migrate --admin-dsn ...`, or `psql -f` each `hermes_pgvector/migrations/*.sql` in order — see [docs/operations.md](docs/operations.md) for the full admin walkthrough and [docs/upgrading.md](docs/upgrading.md) if you're coming from an older version).
|
|
88
88
|
|
|
89
89
|
## Configuration
|
|
90
90
|
|
|
@@ -115,11 +115,14 @@ client = AsyncOpenAI(
|
|
|
115
115
|
|
|
116
116
|
The gateway plumbs `X-Hermes-Session-Key` through as `gateway_session_key=…` in `MemoryProvider.initialize` kwargs, taking priority over the profile default so unprofiled API traffic doesn't collapse every minion into one shared `default` scope.
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
Identity resolution order in `initialize`: `gateway_session_key` > `agent_identity` (unless it is `default`) > `agent_workspace` > `default`. Upstream hermes-agent always passes `agent_workspace="hermes"` (and `agent_identity=<profile name>`), so a default-profile CLI or cron session with no gateway session key lands in the theme `hermes`, not `default`; named profiles land in their profile name. Set the `X-Hermes-Session-Key` header (or a non-default profile) if you want a scope other than `hermes`. When `allowed_themes` is set, `hermes` and your profile names are not exempt: include them in the list if you want those sessions to keep their own theme, otherwise they fall back to `default`.
|
|
119
|
+
|
|
120
|
+
Convention: lowercase, dash-separated, stable (identities are case-folded automatically — `Marketing` and `marketing` are the same theme). Governed sinks that always exist regardless of your themes: `whatsapp-dm` (collapsed DM/session keys), `external-group` (collapsed group/channel/thread keys), `_bench` (benchmark traffic), `default` (only when the host passes no identity kwargs at all). Set `plugins.pgvector.allowed_themes` to your product/worker list to enforce an allow-list — an unknown or typo'd header then falls back to `default` (one-time warning) instead of silently minting a new theme.
|
|
119
121
|
|
|
120
122
|
## Compatibility policy
|
|
121
123
|
|
|
122
|
-
This plugin follows semantic versioning on its **public surface
|
|
124
|
+
This plugin follows semantic versioning on its **public surface**, stable as
|
|
125
|
+
of `1.0.0`:
|
|
123
126
|
|
|
124
127
|
- `plugins.pgvector.*` config keys (see [docs/configuration.md](docs/configuration.md))
|
|
125
128
|
- Tool names and parameters (`recall_memory`, `recall_conversation`)
|
|
@@ -127,7 +130,11 @@ This plugin follows semantic versioning on its **public surface**:
|
|
|
127
130
|
- Database table/column names
|
|
128
131
|
- The `pgvector` provider name and the package's pip entry point
|
|
129
132
|
|
|
130
|
-
|
|
133
|
+
Within the `1.x` series this surface is guaranteed stable: it may grow (new
|
|
134
|
+
config keys, new optional params), but nothing already listed is renamed or
|
|
135
|
+
removed, and no default changes behaviour, without a `2.0` release.
|
|
136
|
+
|
|
137
|
+
`MemoryStore` and every other Python class/module are **internal** — not covered by semver, safe to change between minor releases, and excluded from the `1.x` guarantee above. A migration file, once released, is never edited in place; schema changes land as a new numbered migration.
|
|
131
138
|
|
|
132
139
|
## Support matrix
|
|
133
140
|
|
|
@@ -161,6 +168,18 @@ pytest -q # no DB/embed endpoint: everything
|
|
|
161
168
|
# Live mode (against a throwaway Postgres + your embed endpoint)
|
|
162
169
|
export PG_TEST_DSN='dbname=hermes_test user=postgres host=/var/run/postgresql'
|
|
163
170
|
export PG_TEST_EMBED_URL='http://your-embed-endpoint:11434'
|
|
171
|
+
# Superuser DSN for the same database: the migration, CLI and empty-content
|
|
172
|
+
# tests skip without it, and CI fails on any skip.
|
|
173
|
+
export PG_TEST_ADMIN_DSN='dbname=hermes_test user=postgres host=/var/run/postgresql'
|
|
174
|
+
pytest -q
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The simplest path is the bundled harness (Postgres + pgvector in Docker and a
|
|
178
|
+
fake embed server), which prints all the variables for you:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
scripts/test-env.sh up pg17
|
|
182
|
+
eval "$(scripts/test-env.sh db hermes_test)"
|
|
164
183
|
pytest -q
|
|
165
184
|
```
|
|
166
185
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hermes-memory-pgvector
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
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
|
|
@@ -9,7 +9,7 @@ Project-URL: Source, https://github.com/andreab67/hermes-memory-pgvector
|
|
|
9
9
|
Project-URL: Issues, https://github.com/andreab67/hermes-memory-pgvector/issues
|
|
10
10
|
Project-URL: Roadmap, https://github.com/andreab67/hermes-memory-pgvector/blob/main/ROADMAP.md
|
|
11
11
|
Keywords: hermes-agent,memory-provider,pgvector,postgres,multi-agent,llm-memory,semantic-search
|
|
12
|
-
Classifier: Development Status ::
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
14
|
Classifier: Intended Audience :: System Administrators
|
|
15
15
|
Classifier: License :: OSI Approved :: BSD License
|
|
@@ -73,7 +73,7 @@ What was missing: a **storage layer** that gives the built-in `memory` model dur
|
|
|
73
73
|
| `initialize()` | Verifies schema, opens a `psycopg_pool.ConnectionPool`, bulk-imports existing `MEMORY.md` + `USER.md` content. |
|
|
74
74
|
| `on_memory_write(action, target, content, meta)` | Mirrors built-in `memory` writes into `memory_entries` (add / replace / remove). |
|
|
75
75
|
| `sync_turn(user, assistant, session_id)` / `on_session_end(messages)` | Captures every substantive chat turn into `conversations`; `on_session_end` is a dedup'd backstop. |
|
|
76
|
-
| `on_delegation(task, result, …)` | Records parent→child delegation provenance (`memory_agent_edges`) and stores the exchange as a recallable turn. |
|
|
76
|
+
| `on_delegation(task, result, …)` | Records parent→child delegation provenance (`memory_agent_edges`) and stores the exchange as a recallable turn. `memory_agent_edges.child_identity` is only filled when the host passes a child identity; current upstream hermes-agent passes only the task, result and `child_session_id`, so it is NULL there (`child_session_id` is populated). |
|
|
77
77
|
| `prefetch(query)` / `queue_prefetch(query, session_id=...)` | Ambient recall: top-K similar `memory_entries` for the current theme, injected into the system prompt. `queue_prefetch` pre-computes it off the agent thread. |
|
|
78
78
|
| `recall_memory(query, scope, target, limit)` tool | Explicit cross-theme search of durable memory entries. |
|
|
79
79
|
| `recall_conversation(query, scope, limit)` tool | Explicit search over past chat turns. `scope ∈ {current, session, all, <theme>}`. |
|
|
@@ -121,7 +121,7 @@ mkdir -p ~/.hermes/plugins
|
|
|
121
121
|
cp -r hermes_pgvector ~/.hermes/plugins/pgvector
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
-
Then apply migrations and activate as in Option 1 above (`hermes-pgvector migrate --admin-dsn ...`, or `psql -f` each `migrations/*.sql` in order — see [docs/operations.md](docs/operations.md) for the full admin walkthrough and [docs/upgrading.md](docs/upgrading.md) if you're coming from an older version).
|
|
124
|
+
Then apply migrations and activate as in Option 1 above (`hermes-pgvector migrate --admin-dsn ...`, or `psql -f` each `hermes_pgvector/migrations/*.sql` in order — see [docs/operations.md](docs/operations.md) for the full admin walkthrough and [docs/upgrading.md](docs/upgrading.md) if you're coming from an older version).
|
|
125
125
|
|
|
126
126
|
## Configuration
|
|
127
127
|
|
|
@@ -152,11 +152,14 @@ client = AsyncOpenAI(
|
|
|
152
152
|
|
|
153
153
|
The gateway plumbs `X-Hermes-Session-Key` through as `gateway_session_key=…` in `MemoryProvider.initialize` kwargs, taking priority over the profile default so unprofiled API traffic doesn't collapse every minion into one shared `default` scope.
|
|
154
154
|
|
|
155
|
-
|
|
155
|
+
Identity resolution order in `initialize`: `gateway_session_key` > `agent_identity` (unless it is `default`) > `agent_workspace` > `default`. Upstream hermes-agent always passes `agent_workspace="hermes"` (and `agent_identity=<profile name>`), so a default-profile CLI or cron session with no gateway session key lands in the theme `hermes`, not `default`; named profiles land in their profile name. Set the `X-Hermes-Session-Key` header (or a non-default profile) if you want a scope other than `hermes`. When `allowed_themes` is set, `hermes` and your profile names are not exempt: include them in the list if you want those sessions to keep their own theme, otherwise they fall back to `default`.
|
|
156
|
+
|
|
157
|
+
Convention: lowercase, dash-separated, stable (identities are case-folded automatically — `Marketing` and `marketing` are the same theme). Governed sinks that always exist regardless of your themes: `whatsapp-dm` (collapsed DM/session keys), `external-group` (collapsed group/channel/thread keys), `_bench` (benchmark traffic), `default` (only when the host passes no identity kwargs at all). Set `plugins.pgvector.allowed_themes` to your product/worker list to enforce an allow-list — an unknown or typo'd header then falls back to `default` (one-time warning) instead of silently minting a new theme.
|
|
156
158
|
|
|
157
159
|
## Compatibility policy
|
|
158
160
|
|
|
159
|
-
This plugin follows semantic versioning on its **public surface
|
|
161
|
+
This plugin follows semantic versioning on its **public surface**, stable as
|
|
162
|
+
of `1.0.0`:
|
|
160
163
|
|
|
161
164
|
- `plugins.pgvector.*` config keys (see [docs/configuration.md](docs/configuration.md))
|
|
162
165
|
- Tool names and parameters (`recall_memory`, `recall_conversation`)
|
|
@@ -164,7 +167,11 @@ This plugin follows semantic versioning on its **public surface**:
|
|
|
164
167
|
- Database table/column names
|
|
165
168
|
- The `pgvector` provider name and the package's pip entry point
|
|
166
169
|
|
|
167
|
-
|
|
170
|
+
Within the `1.x` series this surface is guaranteed stable: it may grow (new
|
|
171
|
+
config keys, new optional params), but nothing already listed is renamed or
|
|
172
|
+
removed, and no default changes behaviour, without a `2.0` release.
|
|
173
|
+
|
|
174
|
+
`MemoryStore` and every other Python class/module are **internal** — not covered by semver, safe to change between minor releases, and excluded from the `1.x` guarantee above. A migration file, once released, is never edited in place; schema changes land as a new numbered migration.
|
|
168
175
|
|
|
169
176
|
## Support matrix
|
|
170
177
|
|
|
@@ -198,6 +205,18 @@ pytest -q # no DB/embed endpoint: everything
|
|
|
198
205
|
# Live mode (against a throwaway Postgres + your embed endpoint)
|
|
199
206
|
export PG_TEST_DSN='dbname=hermes_test user=postgres host=/var/run/postgresql'
|
|
200
207
|
export PG_TEST_EMBED_URL='http://your-embed-endpoint:11434'
|
|
208
|
+
# Superuser DSN for the same database: the migration, CLI and empty-content
|
|
209
|
+
# tests skip without it, and CI fails on any skip.
|
|
210
|
+
export PG_TEST_ADMIN_DSN='dbname=hermes_test user=postgres host=/var/run/postgresql'
|
|
211
|
+
pytest -q
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The simplest path is the bundled harness (Postgres + pgvector in Docker and a
|
|
215
|
+
fake embed server), which prints all the variables for you:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
scripts/test-env.sh up pg17
|
|
219
|
+
eval "$(scripts/test-env.sh db hermes_test)"
|
|
201
220
|
pytest -q
|
|
202
221
|
```
|
|
203
222
|
|
|
@@ -43,4 +43,9 @@ tests/test_v060_identity_writer.py
|
|
|
43
43
|
tests/test_v060_migrations_cli.py
|
|
44
44
|
tests/test_v060_provider_reads.py
|
|
45
45
|
tests/test_v060_provider_writes.py
|
|
46
|
-
tests/test_v060_store.py
|
|
46
|
+
tests/test_v060_store.py
|
|
47
|
+
tests/test_v100_pass2_provider.py
|
|
48
|
+
tests/test_v100_pass2_store.py
|
|
49
|
+
tests/test_v100_pass3.py
|
|
50
|
+
tests/test_v100_provider_fixes.py
|
|
51
|
+
tests/test_v100_review_fixes.py
|
|
@@ -225,7 +225,7 @@ DEFAULTS = {
|
|
|
225
225
|
# header, OpenAI-compatible path with the Ollama-native fallback.
|
|
226
226
|
# embed_dim must equal the model's output AND the vector(N) columns;
|
|
227
227
|
# changing it on an existing database needs a column migration + re-embed
|
|
228
|
-
# (see
|
|
228
|
+
# (see docs/upgrading.md, "Changing the embedding dimension").
|
|
229
229
|
"embed_dim": 768,
|
|
230
230
|
# NAME of an environment variable holding a bearer token (e.g.
|
|
231
231
|
# "OPENROUTER_API_KEY"), never the token itself. Read at call time.
|
|
@@ -306,6 +306,20 @@ DEFAULTS = {
|
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
|
|
309
|
+
# Bounds for prefetch_budget. The maximum sits below the host's 8s hard cap on
|
|
310
|
+
# an external provider's prefetch (upstream _EXTERNAL_PREFETCH_TIMEOUT_S); the
|
|
311
|
+
# config schema and prefetch()'s clamp both read these so they cannot drift.
|
|
312
|
+
_PREFETCH_BUDGET_MIN = 0.1
|
|
313
|
+
_PREFETCH_BUDGET_MAX = 7.5
|
|
314
|
+
|
|
315
|
+
# Bounds for prefetch_limit / min_similarity; get_config_schema advertises
|
|
316
|
+
# them and _compute_prefetch_block clamps to them.
|
|
317
|
+
_PREFETCH_LIMIT_MIN = 1
|
|
318
|
+
_PREFETCH_LIMIT_MAX = 50
|
|
319
|
+
_MIN_SIMILARITY_MIN = 0.0
|
|
320
|
+
_MIN_SIMILARITY_MAX = 1.0
|
|
321
|
+
|
|
322
|
+
|
|
309
323
|
def _as_bool(value: Any, default: bool) -> bool:
|
|
310
324
|
"""Coerce a config value to a real bool.
|
|
311
325
|
|
|
@@ -313,13 +327,22 @@ def _as_bool(value: Any, default: bool) -> bool:
|
|
|
313
327
|
hand-edited config.yaml (YAML bools), and save_config(), which persists
|
|
314
328
|
the config schema's declared values -- the STRINGS "true"/"false". A plain
|
|
315
329
|
truthiness test silently inverts the string form (bool("false") is True),
|
|
316
|
-
so every boolean toggle reads through here.
|
|
330
|
+
so every boolean toggle reads through here. Only the explicit true/false
|
|
331
|
+
vocabularies are honoured; None, blank and unrecognized strings return
|
|
332
|
+
`default`.
|
|
317
333
|
"""
|
|
318
334
|
if value is None:
|
|
319
335
|
return default
|
|
320
336
|
if isinstance(value, bool):
|
|
321
337
|
return value
|
|
322
|
-
|
|
338
|
+
text = str(value).strip().lower()
|
|
339
|
+
if text in ("1", "true", "yes", "on"):
|
|
340
|
+
return True
|
|
341
|
+
if text in ("0", "false", "no", "off"):
|
|
342
|
+
return False
|
|
343
|
+
# Blank / unrecognized: fall back to the default, like _as_int/_as_float,
|
|
344
|
+
# so a blank value for a default-true toggle does not silently disable it.
|
|
345
|
+
return default
|
|
323
346
|
|
|
324
347
|
|
|
325
348
|
def _as_int(value: Any, default: int) -> int:
|
|
@@ -452,18 +475,52 @@ def _embed_with_config(
|
|
|
452
475
|
key_env = config.get("embed_api_key_env")
|
|
453
476
|
return _embed_text(
|
|
454
477
|
text,
|
|
455
|
-
|
|
456
|
-
|
|
478
|
+
# `or`, not a .get default: a blank `embed_url:` in config.yaml loads
|
|
479
|
+
# as None and would otherwise reach embed() as base_url=None.
|
|
480
|
+
base_url=config.get("embed_url") or DEFAULTS["embed_url"],
|
|
481
|
+
model=config.get("embed_model") or DEFAULTS["embed_model"],
|
|
457
482
|
timeout=timeout,
|
|
458
483
|
retries=retries,
|
|
459
484
|
backoff=backoff,
|
|
460
485
|
max_total=max_total,
|
|
461
486
|
dim=_embed_dim(config),
|
|
462
487
|
api_key_env=str(key_env).strip() if key_env else None,
|
|
463
|
-
protocol=config.get("embed_protocol"
|
|
488
|
+
protocol=config.get("embed_protocol") or DEFAULTS["embed_protocol"],
|
|
464
489
|
)
|
|
465
490
|
|
|
466
491
|
|
|
492
|
+
_TEXT_PART_TYPES = frozenset({"text", "input_text", "output_text"})
|
|
493
|
+
_IMAGE_PART_TYPES = frozenset({"image_url", "input_image"})
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _flatten_user_content(content: Any) -> str:
|
|
497
|
+
"""Flatten user message content exactly as the host does before sync_turn().
|
|
498
|
+
|
|
499
|
+
Mirrors upstream agent.codex_responses_adapter._summarize_user_message_for_log
|
|
500
|
+
(called with a newline separator): text parts joined with newlines, images collapsed
|
|
501
|
+
to a "[N image(s)]" prefix, "" for None, str() for other scalars. The
|
|
502
|
+
on_session_end backstop fingerprints this form, so a multimodal turn hashes
|
|
503
|
+
the same as what sync_turn() already recorded.
|
|
504
|
+
"""
|
|
505
|
+
if not isinstance(content, list):
|
|
506
|
+
return "" if content is None else str(content)
|
|
507
|
+
texts: List[str] = []
|
|
508
|
+
images = 0
|
|
509
|
+
for part in content:
|
|
510
|
+
if isinstance(part, str):
|
|
511
|
+
if part:
|
|
512
|
+
texts.append(part)
|
|
513
|
+
elif isinstance(part, dict):
|
|
514
|
+
ptype = str(part.get("type") or "").strip().lower()
|
|
515
|
+
text = part.get("text")
|
|
516
|
+
if ptype in _TEXT_PART_TYPES and isinstance(text, str) and text:
|
|
517
|
+
texts.append(text)
|
|
518
|
+
elif ptype in _IMAGE_PART_TYPES:
|
|
519
|
+
images += 1
|
|
520
|
+
note = f"[{images} image{'s' if images != 1 else ''}]" if images else ""
|
|
521
|
+
return " ".join(bit for bit in (note, "\n".join(texts).strip()) if bit)
|
|
522
|
+
|
|
523
|
+
|
|
467
524
|
def _load_plugin_config() -> dict:
|
|
468
525
|
try:
|
|
469
526
|
from hermes_constants import get_hermes_home
|
|
@@ -508,6 +565,7 @@ class PgvectorMemoryProvider(MemoryProvider):
|
|
|
508
565
|
self._healthy: bool = False
|
|
509
566
|
self._delegation_enabled: bool = False # set in initialize() iff migration 002 applied
|
|
510
567
|
self._embed_warned: bool = False
|
|
568
|
+
self._embed_bug_warned: set = set()
|
|
511
569
|
self._db_warned: bool = False
|
|
512
570
|
# v0.6.0 (M4) -- agent_context gate. True until the first initialize()
|
|
513
571
|
# decides otherwise, so a provider used without initialize() (unit
|
|
@@ -874,7 +932,13 @@ class PgvectorMemoryProvider(MemoryProvider):
|
|
|
874
932
|
# embed() as its timeout, which is now ONE shared deadline across
|
|
875
933
|
# embed_protocol='auto''s two HTTP attempts, not a fresh timeout for
|
|
876
934
|
# each (see embed.py).
|
|
877
|
-
budget =
|
|
935
|
+
budget = min(
|
|
936
|
+
max(
|
|
937
|
+
_as_float(self._config.get("prefetch_budget"), DEFAULTS["prefetch_budget"]),
|
|
938
|
+
_PREFETCH_BUDGET_MIN,
|
|
939
|
+
),
|
|
940
|
+
_PREFETCH_BUDGET_MAX,
|
|
941
|
+
)
|
|
878
942
|
deadline = time.monotonic() + budget
|
|
879
943
|
return self._compute_prefetch_block(query, timeout=budget, deadline=deadline)
|
|
880
944
|
|
|
@@ -962,10 +1026,14 @@ class PgvectorMemoryProvider(MemoryProvider):
|
|
|
962
1026
|
rows = store.search(
|
|
963
1027
|
query_embedding=vec,
|
|
964
1028
|
agent_identity=self._agent_identity,
|
|
965
|
-
limit=
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1029
|
+
limit=max(_PREFETCH_LIMIT_MIN, min(
|
|
1030
|
+
_as_int(self._config.get("prefetch_limit"),
|
|
1031
|
+
DEFAULTS["prefetch_limit"]),
|
|
1032
|
+
_PREFETCH_LIMIT_MAX)),
|
|
1033
|
+
min_similarity=max(_MIN_SIMILARITY_MIN, min(
|
|
1034
|
+
_as_float(self._config.get("min_similarity"),
|
|
1035
|
+
DEFAULTS["min_similarity"]),
|
|
1036
|
+
_MIN_SIMILARITY_MAX)),
|
|
969
1037
|
)
|
|
970
1038
|
except Exception as exc: # noqa: BLE001
|
|
971
1039
|
logger.debug("pgvector prefetch query failed: %s", exc)
|
|
@@ -1090,12 +1158,20 @@ class PgvectorMemoryProvider(MemoryProvider):
|
|
|
1090
1158
|
Guarded import, matching how this module already reaches for
|
|
1091
1159
|
`agent.memory_provider` / `hermes_constants`: outside hermes-agent the
|
|
1092
1160
|
content is simply used as-is.
|
|
1161
|
+
|
|
1162
|
+
When the host helper is present but returns None/empty (a bare
|
|
1163
|
+
`/skill` with no user instruction) the host skips sync_turn()
|
|
1164
|
+
entirely, so this returns "" -- callers must treat that as "nothing
|
|
1165
|
+
to store" rather than falling back to the multi-KB skill body.
|
|
1093
1166
|
"""
|
|
1094
1167
|
try:
|
|
1095
1168
|
from agent.skill_commands import (
|
|
1096
1169
|
extract_user_instruction_from_skill_message as _strip,
|
|
1097
1170
|
)
|
|
1098
|
-
|
|
1171
|
+
except Exception: # noqa: BLE001 -- host internals are optional
|
|
1172
|
+
return content
|
|
1173
|
+
try:
|
|
1174
|
+
return _strip(content) or ""
|
|
1099
1175
|
except Exception: # noqa: BLE001 -- host internals are optional
|
|
1100
1176
|
return content
|
|
1101
1177
|
|
|
@@ -1178,6 +1254,19 @@ class PgvectorMemoryProvider(MemoryProvider):
|
|
|
1178
1254
|
return
|
|
1179
1255
|
try:
|
|
1180
1256
|
child_identity = kwargs.get("child_identity") or kwargs.get("agent_identity")
|
|
1257
|
+
if isinstance(child_identity, str) and child_identity:
|
|
1258
|
+
# Same governance initialize() applies to this agent's own
|
|
1259
|
+
# identity; a raw value would bypass the allow-list/aliases/
|
|
1260
|
+
# bench isolation. Upstream does not pass this today
|
|
1261
|
+
# (tools/delegate_tool_results.py), so None stays None.
|
|
1262
|
+
child_identity, _, _ = normalize_identity(
|
|
1263
|
+
child_identity,
|
|
1264
|
+
allowed_themes=_as_theme_list(self._config.get("allowed_themes")),
|
|
1265
|
+
aliases=_as_alias_map(self._config.get("identity_aliases")),
|
|
1266
|
+
bench_mode=self._config.get("bench_mode", "bucket"),
|
|
1267
|
+
)
|
|
1268
|
+
else:
|
|
1269
|
+
child_identity = None
|
|
1181
1270
|
child_session_id = kwargs.get("child_session_id") or kwargs.get("session_id")
|
|
1182
1271
|
self._writer.enqueue(
|
|
1183
1272
|
action="edge",
|
|
@@ -1265,23 +1354,20 @@ class PgvectorMemoryProvider(MemoryProvider):
|
|
|
1265
1354
|
role = (msg.get("role") or "").lower()
|
|
1266
1355
|
if role not in ("user", "assistant"):
|
|
1267
1356
|
continue
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1357
|
+
# The host flattens every role with the same helper
|
|
1358
|
+
# before sync_turn(), so do the same for a matching hash.
|
|
1359
|
+
content = _flatten_user_content(msg.get("content") or "")
|
|
1360
|
+
# Normalize user turns the way the host does: a /skill
|
|
1361
|
+
# turn reaches sync_turn() as just the user's instruction
|
|
1362
|
+
# (and not at all for a bare /skill). Noise-check, dedup
|
|
1363
|
+
# and STORE that same string, never the skill body.
|
|
1364
|
+
if role == "user":
|
|
1365
|
+
content = self._strip_skill_scaffolding(content)
|
|
1366
|
+
if not content:
|
|
1367
|
+
continue
|
|
1276
1368
|
if self._is_noise(content, min_chars=min_chars):
|
|
1277
1369
|
continue
|
|
1278
|
-
|
|
1279
|
-
# /skill turn matches what sync_turn() was handed.
|
|
1280
|
-
fp_content = (
|
|
1281
|
-
self._strip_skill_scaffolding(content)
|
|
1282
|
-
if role == "user" else content
|
|
1283
|
-
)
|
|
1284
|
-
fp = self._turn_fingerprint(role, fp_content)
|
|
1370
|
+
fp = self._turn_fingerprint(role, content)
|
|
1285
1371
|
if fp in self._turn_fingerprints:
|
|
1286
1372
|
continue # already enqueued by sync_turn this session
|
|
1287
1373
|
accepted = self._writer.enqueue(
|
|
@@ -1866,18 +1952,39 @@ class PgvectorMemoryProvider(MemoryProvider):
|
|
|
1866
1952
|
"""Governance config that must bust a cached gateway agent when it
|
|
1867
1953
|
changes. Upstream calls this on an UNINITIALIZED instance on every
|
|
1868
1954
|
inbound message (agent.memory_provider.MemoryProvider.identity_signature),
|
|
1869
|
-
|
|
1870
|
-
|
|
1955
|
+
and the gateway memoises ONE provider per process, so self._config
|
|
1956
|
+
(frozen at register()) would never reflect an edit. Instead the values
|
|
1957
|
+
come from config.yaml, memoised on (path, st_mtime_ns, st_size) -- one
|
|
1958
|
+
stat per call, a re-parse only when the file changed (same pattern as
|
|
1959
|
+
upstream's Honcho provider). self._config is never mutated and is the
|
|
1960
|
+
fallback on any stat/parse failure. No self._store, never raises.
|
|
1961
|
+
allowed_themes/identity_aliases/bench_mode decide which
|
|
1871
1962
|
theme a raw identity normalizes to (identity.normalize_identity);
|
|
1872
1963
|
write_contexts decides whether a context writes at all -- a cached
|
|
1873
1964
|
agent built under the OLD values would keep routing/writing wrong
|
|
1874
1965
|
until the process restarts, without this.
|
|
1875
1966
|
"""
|
|
1967
|
+
try:
|
|
1968
|
+
from hermes_constants import get_hermes_home
|
|
1969
|
+
path = get_hermes_home() / "config.yaml"
|
|
1970
|
+
stat = path.stat()
|
|
1971
|
+
key = (str(path), stat.st_mtime_ns, stat.st_size)
|
|
1972
|
+
memo = getattr(self, "_sig_memo", None)
|
|
1973
|
+
if memo is not None and memo[0] == key:
|
|
1974
|
+
return dict(memo[1])
|
|
1975
|
+
values = self._signature_values({**DEFAULTS, **_load_plugin_config()})
|
|
1976
|
+
self._sig_memo = (key, values)
|
|
1977
|
+
return dict(values)
|
|
1978
|
+
except Exception: # noqa: BLE001 -- never raise; fall back to the frozen config
|
|
1979
|
+
return self._signature_values(self._config)
|
|
1980
|
+
|
|
1981
|
+
@staticmethod
|
|
1982
|
+
def _signature_values(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
1876
1983
|
return {
|
|
1877
|
-
"pgvector.allowed_themes": _as_theme_list(
|
|
1878
|
-
"pgvector.identity_aliases": _as_alias_map(
|
|
1879
|
-
"pgvector.bench_mode":
|
|
1880
|
-
"pgvector.write_contexts": _as_write_contexts(
|
|
1984
|
+
"pgvector.allowed_themes": _as_theme_list(config.get("allowed_themes")),
|
|
1985
|
+
"pgvector.identity_aliases": _as_alias_map(config.get("identity_aliases")),
|
|
1986
|
+
"pgvector.bench_mode": config.get("bench_mode", DEFAULTS["bench_mode"]),
|
|
1987
|
+
"pgvector.write_contexts": _as_write_contexts(config.get("write_contexts")),
|
|
1881
1988
|
}
|
|
1882
1989
|
|
|
1883
1990
|
def get_config_schema(self) -> List[Dict[str, Any]]:
|
|
@@ -1904,7 +2011,7 @@ class PgvectorMemoryProvider(MemoryProvider):
|
|
|
1904
2011
|
},
|
|
1905
2012
|
{
|
|
1906
2013
|
"key": "embed_dim",
|
|
1907
|
-
"description": "Vector length the embed model returns. Must also match the database's vector(N) columns (768 as created by migration 001). Changing it on an existing database requires migrating those columns and re-embedding every row -- see
|
|
2014
|
+
"description": "Vector length the embed model returns. Must also match the database's vector(N) columns (768 as created by migration 001). Changing it on an existing database requires migrating those columns and re-embedding every row -- see docs/upgrading.md, 'Changing the embedding dimension'.",
|
|
1908
2015
|
"type": "integer",
|
|
1909
2016
|
"default": DEFAULTS["embed_dim"],
|
|
1910
2017
|
"minimum": 1,
|
|
@@ -1927,16 +2034,16 @@ class PgvectorMemoryProvider(MemoryProvider):
|
|
|
1927
2034
|
"description": "Max ambient recall results injected per turn",
|
|
1928
2035
|
"type": "integer",
|
|
1929
2036
|
"default": DEFAULTS["prefetch_limit"],
|
|
1930
|
-
"minimum":
|
|
1931
|
-
"maximum":
|
|
2037
|
+
"minimum": _PREFETCH_LIMIT_MIN,
|
|
2038
|
+
"maximum": _PREFETCH_LIMIT_MAX,
|
|
1932
2039
|
},
|
|
1933
2040
|
{
|
|
1934
2041
|
"key": "min_similarity",
|
|
1935
2042
|
"description": "Cosine similarity cutoff for ambient prefetch",
|
|
1936
2043
|
"type": "number",
|
|
1937
2044
|
"default": DEFAULTS["min_similarity"],
|
|
1938
|
-
"minimum":
|
|
1939
|
-
"maximum":
|
|
2045
|
+
"minimum": _MIN_SIMILARITY_MIN,
|
|
2046
|
+
"maximum": _MIN_SIMILARITY_MAX,
|
|
1940
2047
|
},
|
|
1941
2048
|
{
|
|
1942
2049
|
"key": "embed_on_write",
|
|
@@ -2062,8 +2169,8 @@ class PgvectorMemoryProvider(MemoryProvider):
|
|
|
2062
2169
|
"description": "Hard wall-clock budget (seconds) for prefetch()'s synchronous fallback path (embed + search), used when queue_prefetch() has not already cached a block for the session. Must stay below the host's external-provider prefetch timeout (8s), or the host logs a timeout warning and skips this provider on later turns until the stuck call returns.",
|
|
2063
2170
|
"type": "number",
|
|
2064
2171
|
"default": DEFAULTS["prefetch_budget"],
|
|
2065
|
-
"minimum":
|
|
2066
|
-
"maximum":
|
|
2172
|
+
"minimum": _PREFETCH_BUDGET_MIN,
|
|
2173
|
+
"maximum": _PREFETCH_BUDGET_MAX,
|
|
2067
2174
|
},
|
|
2068
2175
|
]
|
|
2069
2176
|
|
|
@@ -2137,6 +2244,16 @@ class PgvectorMemoryProvider(MemoryProvider):
|
|
|
2137
2244
|
logger.warning("pgvector embed failed (degrading to text-only): %s", exc)
|
|
2138
2245
|
self._embed_warned = True
|
|
2139
2246
|
return None
|
|
2247
|
+
except Exception as exc: # noqa: BLE001 -- an embed-side bug must never drop a durable row
|
|
2248
|
+
# Not an endpoint failure but a bug: never hide it behind the
|
|
2249
|
+
# one-shot EmbeddingError warning (a transient outage earlier
|
|
2250
|
+
# would otherwise silence it); warn once per exception type.
|
|
2251
|
+
kind = type(exc).__name__
|
|
2252
|
+
warned = self.__dict__.setdefault("_embed_bug_warned", set())
|
|
2253
|
+
if kind not in warned:
|
|
2254
|
+
warned.add(kind)
|
|
2255
|
+
logger.warning("pgvector embed raised unexpectedly (degrading to text-only)", exc_info=exc)
|
|
2256
|
+
return None
|
|
2140
2257
|
|
|
2141
2258
|
|
|
2142
2259
|
# ---------------------------------------------------------------------------
|