mnemo-engine 0.4.1__tar.gz → 0.5.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.
Files changed (38) hide show
  1. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/PKG-INFO +60 -1
  2. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/README.md +59 -0
  3. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/Cargo.lock +2 -2
  4. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/Cargo.toml +1 -1
  5. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/README.md +76 -5
  6. mnemo_engine-0.5.0/mnemo/examples/embedder_server.py +51 -0
  7. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/bin/mnemo.rs +273 -15
  8. mnemo_engine-0.5.0/mnemo/src/embed.rs +280 -0
  9. mnemo_engine-0.5.0/mnemo/src/embed_cmd.rs +572 -0
  10. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/lib.rs +4 -0
  11. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/mcp.rs +273 -82
  12. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/memory.rs +13 -1
  13. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/store.rs +159 -0
  14. mnemo_engine-0.5.0/mnemo/tests/cli_smoke.rs +1336 -0
  15. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/tests/integration.rs +370 -10
  16. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo-python/Cargo.lock +2 -2
  17. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo-python/Cargo.toml +1 -1
  18. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo-python/README.md +59 -0
  19. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo-python/src/lib.rs +253 -7
  20. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo-python/test_mnemo.py +164 -0
  21. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/pyproject.toml +1 -1
  22. mnemo_engine-0.4.1/mnemo/tests/cli_smoke.rs +0 -553
  23. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/.gitignore +0 -0
  24. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/deny.toml +0 -0
  25. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/examples/quickstart.rs +0 -0
  26. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/examples/session.rs +0 -0
  27. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/cache.rs +0 -0
  28. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/crypto.rs +0 -0
  29. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/error.rs +0 -0
  30. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/format.rs +0 -0
  31. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/index.rs +0 -0
  32. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/pager.rs +0 -0
  33. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/result_cache.rs +0 -0
  34. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/session.rs +0 -0
  35. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/src/wal.rs +0 -0
  36. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/tests/proptest_store.proptest-regressions +0 -0
  37. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo/tests/proptest_store.rs +0 -0
  38. {mnemo_engine-0.4.1 → mnemo_engine-0.5.0}/mnemo-python/.gitignore +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mnemo-engine
3
- Version: 0.4.1
3
+ Version: 0.5.0
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: License :: OSI Approved :: Apache Software License
@@ -161,6 +161,10 @@ its own agent.
161
161
  | `memories()` | Enumerate every live (non-deleted, non-expired) memory as a list of dicts |
162
162
  | `dimensions()` | Embedding dimensionality this database was created with (also on `stats()["dimensions"]`) |
163
163
  | `rekey(new_passphrase, fast=False)` | Change the passphrase. Re-derives the KEK and re-wraps the DEK; encrypted pages are not rewritten (O(1) in db size). `fast=True` uses the cheap-Argon2 test params — omit for production |
164
+ | `set_embedder(fn, *, name, dimensions, normalize=True)` | Register a Python callable `embed(texts: list[str]) -> list[list[float]]` as this handle's text embedder (Phase 3). Dimension-checks against the database's width; identity-checks against (and on first use upserts) the scaffold manifest's `embedder_name` / `embedder_dimensions` / `embedder_normalize` keys, so swapping embedders between opens fails loudly |
165
+ | `remember_text(content, memory_type, **kwargs)` | Embed `content` via the registered embedder and `remember` the result. Accepts the same per-memory kwargs as `remember` (`agent_id`, `importance`, `session_id`, `ttl_secs`, `shared`, `metadata`) |
166
+ | `recall_text(query, top_k=10, memory_types=None, agent_id=None, track_access=True)` | Embed `query` via the registered embedder and run `recall`. Same return shape as `recall` |
167
+ | `cache_get_semantic_text(namespace, text, threshold=0.97)` | Text-side semantic cache lookup; uses the embedder's `name` as the semantic cache's `model` component |
164
168
  | `stats()` | Summary statistics |
165
169
  | `export_encrypted(dest)` | Copy the (already-encrypted) file elsewhere |
166
170
  | `len(db)` | Live memory count |
@@ -370,6 +374,61 @@ your embedder's output — `text-embedding-3-small` is 1536-dim,
370
374
  consistent across hosts (use Redis / DynamoDB DAX). MNemo's cache is
371
375
  optimized for a single-host agent that owns its cache file.
372
376
 
377
+ ### Recipe: `set_embedder` + text-only `remember` / `recall`
378
+
379
+ Phase 3 ships a per-handle embedder hook so callers never have to
380
+ pass vectors by hand. Register once at open time; every subsequent
381
+ `remember_text` / `recall_text` / `cache_get_semantic_text` call on
382
+ the handle uses it.
383
+
384
+ ```python
385
+ import mnemo
386
+ from sentence_transformers import SentenceTransformer
387
+
388
+ model = SentenceTransformer("all-MiniLM-L6-v2") # 384-dim, normalized
389
+
390
+ def embed(texts: list[str]) -> list[list[float]]:
391
+ return model.encode(texts, normalize_embeddings=True).tolist()
392
+
393
+ db = mnemo.open("agent.mnemo", "pw", dimensions=384)
394
+ db.set_embedder(embed, name="all-MiniLM-L6-v2",
395
+ dimensions=384, normalize=True)
396
+
397
+ db.remember_text("the user prefers tea", "semantic",
398
+ importance=0.8, agent_id="assistant")
399
+ hits = db.recall_text("how does the user like their drinks?", top_k=5)
400
+ for h in hits:
401
+ print(h["score"], h["content"])
402
+ ```
403
+
404
+ Or with OpenAI's hosted embeddings:
405
+
406
+ ```python
407
+ from openai import OpenAI
408
+ client = OpenAI()
409
+
410
+ def embed(texts: list[str]) -> list[list[float]]:
411
+ r = client.embeddings.create(model="text-embedding-3-small", input=texts)
412
+ return [item.embedding for item in r.data]
413
+
414
+ db = mnemo.open("agent.mnemo", "pw", dimensions=1536)
415
+ db.set_embedder(embed, name="text-embedding-3-small",
416
+ dimensions=1536, normalize=False)
417
+ ```
418
+
419
+ The identity (`name`, `dimensions`, `normalize`) is persisted on the
420
+ scaffold manifest the first time you `set_embedder`. Reopening with a
421
+ *different* embedder — say you swap MiniLM for BGE on the same file —
422
+ raises `RuntimeError: embedder identity mismatch: database manifest
423
+ records embedder (name="all-MiniLM-L6-v2", …), but this handle was
424
+ passed embedder (name="bge-large-en-v1.5", …). Mixing embedders
425
+ silently corrupts recall quality — open the file with the original
426
+ embedder, or create a fresh database for the new one.`
427
+
428
+ Mnemo also now refuses any `remember` call with an all-zero vector,
429
+ so a mis-wired embedder (returning `[0.0] * dims`) fails loudly on
430
+ the first write instead of polluting recall silently.
431
+
373
432
  ## License
374
433
 
375
434
  Apache-2.0.
@@ -147,6 +147,10 @@ its own agent.
147
147
  | `memories()` | Enumerate every live (non-deleted, non-expired) memory as a list of dicts |
148
148
  | `dimensions()` | Embedding dimensionality this database was created with (also on `stats()["dimensions"]`) |
149
149
  | `rekey(new_passphrase, fast=False)` | Change the passphrase. Re-derives the KEK and re-wraps the DEK; encrypted pages are not rewritten (O(1) in db size). `fast=True` uses the cheap-Argon2 test params — omit for production |
150
+ | `set_embedder(fn, *, name, dimensions, normalize=True)` | Register a Python callable `embed(texts: list[str]) -> list[list[float]]` as this handle's text embedder (Phase 3). Dimension-checks against the database's width; identity-checks against (and on first use upserts) the scaffold manifest's `embedder_name` / `embedder_dimensions` / `embedder_normalize` keys, so swapping embedders between opens fails loudly |
151
+ | `remember_text(content, memory_type, **kwargs)` | Embed `content` via the registered embedder and `remember` the result. Accepts the same per-memory kwargs as `remember` (`agent_id`, `importance`, `session_id`, `ttl_secs`, `shared`, `metadata`) |
152
+ | `recall_text(query, top_k=10, memory_types=None, agent_id=None, track_access=True)` | Embed `query` via the registered embedder and run `recall`. Same return shape as `recall` |
153
+ | `cache_get_semantic_text(namespace, text, threshold=0.97)` | Text-side semantic cache lookup; uses the embedder's `name` as the semantic cache's `model` component |
150
154
  | `stats()` | Summary statistics |
151
155
  | `export_encrypted(dest)` | Copy the (already-encrypted) file elsewhere |
152
156
  | `len(db)` | Live memory count |
@@ -356,6 +360,61 @@ your embedder's output — `text-embedding-3-small` is 1536-dim,
356
360
  consistent across hosts (use Redis / DynamoDB DAX). MNemo's cache is
357
361
  optimized for a single-host agent that owns its cache file.
358
362
 
363
+ ### Recipe: `set_embedder` + text-only `remember` / `recall`
364
+
365
+ Phase 3 ships a per-handle embedder hook so callers never have to
366
+ pass vectors by hand. Register once at open time; every subsequent
367
+ `remember_text` / `recall_text` / `cache_get_semantic_text` call on
368
+ the handle uses it.
369
+
370
+ ```python
371
+ import mnemo
372
+ from sentence_transformers import SentenceTransformer
373
+
374
+ model = SentenceTransformer("all-MiniLM-L6-v2") # 384-dim, normalized
375
+
376
+ def embed(texts: list[str]) -> list[list[float]]:
377
+ return model.encode(texts, normalize_embeddings=True).tolist()
378
+
379
+ db = mnemo.open("agent.mnemo", "pw", dimensions=384)
380
+ db.set_embedder(embed, name="all-MiniLM-L6-v2",
381
+ dimensions=384, normalize=True)
382
+
383
+ db.remember_text("the user prefers tea", "semantic",
384
+ importance=0.8, agent_id="assistant")
385
+ hits = db.recall_text("how does the user like their drinks?", top_k=5)
386
+ for h in hits:
387
+ print(h["score"], h["content"])
388
+ ```
389
+
390
+ Or with OpenAI's hosted embeddings:
391
+
392
+ ```python
393
+ from openai import OpenAI
394
+ client = OpenAI()
395
+
396
+ def embed(texts: list[str]) -> list[list[float]]:
397
+ r = client.embeddings.create(model="text-embedding-3-small", input=texts)
398
+ return [item.embedding for item in r.data]
399
+
400
+ db = mnemo.open("agent.mnemo", "pw", dimensions=1536)
401
+ db.set_embedder(embed, name="text-embedding-3-small",
402
+ dimensions=1536, normalize=False)
403
+ ```
404
+
405
+ The identity (`name`, `dimensions`, `normalize`) is persisted on the
406
+ scaffold manifest the first time you `set_embedder`. Reopening with a
407
+ *different* embedder — say you swap MiniLM for BGE on the same file —
408
+ raises `RuntimeError: embedder identity mismatch: database manifest
409
+ records embedder (name="all-MiniLM-L6-v2", …), but this handle was
410
+ passed embedder (name="bge-large-en-v1.5", …). Mixing embedders
411
+ silently corrupts recall quality — open the file with the original
412
+ embedder, or create a fresh database for the new one.`
413
+
414
+ Mnemo also now refuses any `remember` call with an all-zero vector,
415
+ so a mis-wired embedder (returning `[0.0] * dims`) fails loudly on
416
+ the first write instead of polluting recall silently.
417
+
359
418
  ## License
360
419
 
361
420
  Apache-2.0.
@@ -462,7 +462,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
462
462
 
463
463
  [[package]]
464
464
  name = "mnemo-engine"
465
- version = "0.4.1"
465
+ version = "0.5.0"
466
466
  dependencies = [
467
467
  "aes-gcm",
468
468
  "argon2",
@@ -485,7 +485,7 @@ dependencies = [
485
485
 
486
486
  [[package]]
487
487
  name = "mnemo-node"
488
- version = "0.4.1"
488
+ version = "0.5.0"
489
489
  dependencies = [
490
490
  "mnemo-engine",
491
491
  "napi",
@@ -6,7 +6,7 @@
6
6
  # below stay `mnemo`, so consumers still write `use mnemo::...` and invoke
7
7
  # `mnemo` on the command line after `cargo add mnemo-engine`.
8
8
  name = "mnemo-engine"
9
- version = "0.4.1"
9
+ version = "0.5.0"
10
10
  edition = "2021"
11
11
  rust-version = "1.75"
12
12
  description = "Encrypted, single-file, portable agent memory engine with multi-signal recall, sessions, snapshots, and an IVF+PQ index"
@@ -41,10 +41,55 @@ mnemo serve --mcp path/to/agent.mnemo
41
41
 
42
42
  The passphrase is read from `MNEMO_PASSPHRASE` (never a CLI flag — that
43
43
  would leak into shell history and process listings). Tools exposed:
44
- `about`, `remember`, `recall`, `forget`, `list`, `snapshot_list`, `stats`.
45
- Every mutation flushes before returning. `remember` and `recall` are
46
- embedder-agnostic — the caller supplies vectors; text-only variants
47
- land with Phase 3 of the level-up plan.
44
+ `about`, `remember`, `recall`, `forget`, `list`, `snapshot_list`, `stats`,
45
+ `cache_put`, `cache_get`, `cache_stats`, `cache_put_semantic`,
46
+ `cache_get_semantic`. Every mutation flushes before returning.
47
+
48
+ #### Text-in via `--embed-cmd` (Phase 3)
49
+
50
+ `remember`, `recall`, `cache_put_semantic`, and `cache_get_semantic`
51
+ accept **either** a caller-supplied `vector`/`query` **or** a `text`
52
+ string that the server embeds for them. Enable the text path by passing
53
+ an embedder command at serve start:
54
+
55
+ ```sh
56
+ MNEMO_PASSPHRASE=... mnemo serve --mcp agent.mnemo \
57
+ --embed-cmd "python3 mnemo/examples/embedder_server.py" \
58
+ --embed-name "sentence-transformers/all-MiniLM-L6-v2"
59
+ ```
60
+
61
+ The embedder is a **persistent child process** spawned once at serve
62
+ start. Protocol: it reads one JSON-encoded string per line from stdin
63
+ and writes one JSON array of floats per line to stdout, flushed after
64
+ every reply. See `mnemo/examples/embedder_server.py` for a 10-line
65
+ reference implementation (sentence-transformers + MiniLM, 384-dim).
66
+
67
+ > **Stdout is for replies only.** Any line the embedder prints to
68
+ > stdout outside a reply — startup banners, model-load progress,
69
+ > warnings, "model loaded" messages — is a protocol violation and
70
+ > would shift every subsequent reply by one line. EmbedCmd detects
71
+ > it, kills the child, and surfaces a clear error ("stdout is
72
+ > reserved for replies only; log to stderr instead"); the next call
73
+ > respawns the child. **Log to stderr.** Python: `print("...",
74
+ > file=sys.stderr, flush=True)`, or configure `logging` with
75
+ > `stream=sys.stderr`. Stderr is inherited to the parent and is
76
+ > safe for anything you want to see while the server runs.
77
+
78
+ Per-request timeout defaults to 60 seconds; override with
79
+ `--embed-timeout <secs>` on `serve`, `remember`, and `recall`. Set
80
+ it longer than your slowest cold-start reply (sentence-transformers'
81
+ first `encode()` on CPU can be 3–5s). On timeout — or any other
82
+ protocol error (malformed JSON, wrong dimensionality, zero vector,
83
+ unexpected EOF) — the child is killed and the next call respawns a
84
+ fresh child with the same argv/env/timeout. The guarantee is
85
+ asymmetric and deliberate: **an error is allowed; a mismatched
86
+ vector never is.**
87
+
88
+ The `--embed-name` string is matched against (and persisted on) the
89
+ scaffold manifest's `metadata.embedder = { name, dimensions, normalize }`
90
+ object. Starting a server with a different `--embed-name` on an existing
91
+ file fails loudly — mixing embedders in one file silently corrupts
92
+ recall quality.
48
93
 
49
94
  Claude Desktop config snippet (`~/Library/Application Support/Claude/claude_desktop_config.json`):
50
95
 
@@ -53,13 +98,39 @@ Claude Desktop config snippet (`~/Library/Application Support/Claude/claude_desk
53
98
  "mcpServers": {
54
99
  "mnemo": {
55
100
  "command": "mnemo",
56
- "args": ["serve", "--mcp", "/absolute/path/to/agent.mnemo"],
101
+ "args": [
102
+ "serve", "--mcp", "/absolute/path/to/agent.mnemo",
103
+ "--embed-cmd", "python3 /absolute/path/to/embedder_server.py",
104
+ "--embed-name", "sentence-transformers/all-MiniLM-L6-v2"
105
+ ],
57
106
  "env": { "MNEMO_PASSPHRASE": "your-passphrase-here" }
58
107
  }
59
108
  }
60
109
  }
61
110
  ```
62
111
 
112
+ Omit `--embed-cmd`/`--embed-name` to keep the pre-Phase-3
113
+ embedder-agnostic behavior (`vector`/`query` only).
114
+
115
+ #### CLI text path
116
+
117
+ The same `--embed-cmd` + `--embed-name` pair works on the one-shot
118
+ CLI commands:
119
+
120
+ ```sh
121
+ mnemo remember agent.mnemo --text "the user prefers tea" \
122
+ --type semantic --importance 0.8 --agent assistant \
123
+ --embed-cmd "python3 embedder_server.py" \
124
+ --embed-name "sentence-transformers/all-MiniLM-L6-v2"
125
+
126
+ mnemo recall agent.mnemo --text "what drink?" --top-k 5 \
127
+ --embed-cmd "python3 embedder_server.py" \
128
+ --embed-name "sentence-transformers/all-MiniLM-L6-v2" \
129
+ --no-track
130
+ ```
131
+
132
+ `--text` is mutually exclusive with `--query`/`--query-file`.
133
+
63
134
  The server holds an exclusive write lock on the file for its lifetime
64
135
  (see [Concurrency](#concurrency--read-only-opens)), so two MCP servers
65
136
  on the same file will conflict — one will fail to start with
@@ -0,0 +1,51 @@
1
+ """Reference embedder for `mnemo` --embed-cmd / MCP serve (Phase 3 PR 10b).
2
+
3
+ Protocol:
4
+ * Read one JSON-encoded string per line from stdin.
5
+ * Write one JSON array of floats per line to stdout.
6
+ * Flush stdout after every reply, or mnemo will time out waiting.
7
+ * Close on EOF of stdin (which mnemo sends when the handle drops).
8
+
9
+ The script runs persistently — one spawn per `Mnemo` handle, reused for
10
+ every subsequent remember/recall/cache call. Keep heavy imports (model
11
+ load) at module scope so the cost is paid once.
12
+
13
+ **Stdout is reserved for replies only.** Any `print(...)` without
14
+ `file=sys.stderr` — a startup banner, a "model loaded" line, a
15
+ progress bar, a warning — shifts every subsequent reply by one line
16
+ and mnemo's EmbedCmd will kill the child with a clear error.
17
+ Log to stderr instead: `print("...", file=sys.stderr, flush=True)`.
18
+ `sentence-transformers`/`transformers`/`torch` already log to stderr
19
+ by default, so this is only a hazard for your own debug prints.
20
+
21
+ Usage::
22
+
23
+ # CLI, one-shot:
24
+ mnemo remember notes.mnemo \\
25
+ --text "the user prefers tea" \\
26
+ --embed-cmd "python3 embedder_server.py" \\
27
+ --embed-name "sentence-transformers/all-MiniLM-L6-v2"
28
+
29
+ # MCP, persistent server:
30
+ MNEMO_PASSPHRASE=... mnemo serve --mcp notes.mnemo \\
31
+ --embed-cmd "python3 embedder_server.py" \\
32
+ --embed-name "sentence-transformers/all-MiniLM-L6-v2"
33
+
34
+ Requires `sentence-transformers` + `torch`. Swap the model slug (and
35
+ pass the matching --embed-name) to use something else; just keep the
36
+ database's `dimensions` consistent with the embedder's output size.
37
+ """
38
+
39
+ import json
40
+ import sys
41
+
42
+ from sentence_transformers import SentenceTransformer
43
+
44
+ MODEL = "sentence-transformers/all-MiniLM-L6-v2" # 384-dim, normalized
45
+
46
+ model = SentenceTransformer(MODEL)
47
+
48
+ for line in sys.stdin:
49
+ text = json.loads(line)
50
+ vector = model.encode([text], normalize_embeddings=True)[0].tolist()
51
+ print(json.dumps(vector), flush=True)