memd-engine 0.5.0__tar.gz → 0.5.2__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.
- memd_engine-0.5.2/PKG-INFO +319 -0
- memd_engine-0.5.2/README.md +267 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/pyproject.toml +5 -2
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/__init__.py +1 -1
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/engine/memory.py +7 -1
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/hosted/metering.py +16 -5
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/index/ann_usearch.py +1 -1
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/index/sqlite_index.py +25 -2
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/index/tantivy_lexical.py +1 -1
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/pipeline/extractor.py +13 -3
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/query/rerank.py +2 -2
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/server/http.py +29 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/storage/engine.py +16 -7
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/storage/replica.py +9 -4
- memd_engine-0.5.2/src/memd_engine.egg-info/PKG-INFO +319 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd_engine.egg-info/SOURCES.txt +2 -1
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_http.py +33 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_llm_extractor.py +56 -4
- memd_engine-0.5.2/tests/test_pass19_sigkill.py +147 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass27_scale_perf.py +6 -1
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pending_fixes.py +4 -1
- memd_engine-0.5.2/tests/test_pypi_readme.py +31 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_read_replicas.py +3 -1
- memd_engine-0.5.2/tests/test_ste_check.py +66 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_write_forwarding.py +42 -7
- memd_engine-0.5.0/PKG-INFO +0 -1230
- memd_engine-0.5.0/README-engine.md +0 -1181
- memd_engine-0.5.0/README.md +0 -172
- memd_engine-0.5.0/src/memd_engine.egg-info/PKG-INFO +0 -1230
- memd_engine-0.5.0/tests/test_pass19_sigkill.py +0 -111
- {memd_engine-0.5.0 → memd_engine-0.5.2}/LICENSE +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/setup.cfg +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/cli.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/core/__init__.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/core/schema.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/engine/__init__.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/engine/forward.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/harness/core.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/harness/run.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/harness/suites/__init__.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/harness/suites/adversarial.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/harness/suites/halumem_ops.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/harness/suites/longmemeval_synthetic.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/hosted/__init__.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/hosted/app.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/hosted/billing.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/hosted/plans.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/hosted/store.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/index/__init__.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/metrics.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/pipeline/__init__.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/pipeline/consolidation.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/pipeline/embedder.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/query/__init__.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/query/dates.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/query/fusion.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/query/packing.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/query/planner.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/sdk/__init__.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/sdk/client.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/server/__init__.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/server/auth.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/server/cluster.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/server/mcp_server.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/storage/__init__.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/storage/audit.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/storage/crypto.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/storage/objectstore.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd/storage/s3store.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd_engine.egg-info/dependency_links.txt +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd_engine.egg-info/entry_points.txt +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd_engine.egg-info/requires.txt +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/src/memd_engine.egg-info/top_level.txt +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_adversarial_probes.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_ann_usearch.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_billing_hardening.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_billing_reverify.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_billing_stripe.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_cluster_handoff.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_cluster_router.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_cluster_unit.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_concurrency.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_dates.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_embedder_lifecycle.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_engine.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_evidence_packing.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_evidence_packing_api.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_examples.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_forward_packing.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_gate2.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_hosted_tenancy.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_index_rowids.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_key_custody.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_key_providers.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_key_shred_crash.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_loop_fixes.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_mcp.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_metrics.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_objectstore_contract.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_optimizations.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass10_egress_io.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass11_boundary_attacks.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass12_extractor_eviction.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass13_cache_bytes.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass14_soak_fuzz.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass15_entity_tokens.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass16_time_lane.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass17_gate_integrity.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass18_harness_scale.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass21_segment_replay.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass22_metrics_fidelity.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass23_audit_ledger.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass24_metrics_tenancy.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass25_data_integrity.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass26_authz_abuse.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass28_maintenance_cost.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass29_write_amplification.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass2_durability.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass30_cold_start.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass31_reaudit.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass32_s3_hardening.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass33_acked_delete_replay.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass34_upgrade_integrity.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass35_snapshot_migration_gc.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass36_legacy_ledger_deletes.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass37_ambiguous_ledger_deletes.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass38_rest_contract.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass39_acked_op_reaches_index.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass3_boundaries.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass40_damaged_frame_liveness.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass41_scrub_foreign_reader.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass42_scrub_eviction_and_open.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass43_export_incomplete_signal.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass44_stale_node_cache.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass45_settle_supersede_quarantine.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass46_concurrent_first_key.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass47_set_vector_is_derived.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass48_error_without_response.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass49_crashed_key_creation.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass4_multiproc_metrics.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass50_shared_cache_sweep.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass6_purge.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass7_doors.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass8_lanes.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass9_ns_lifecycle.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_pass_fixes.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_read_replicas_cluster.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_read_replicas_http.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_rerank_pipeline.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_retrieval_v2.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_robustness.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_roundtrip.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_s3_backend.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_schema.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_sdk.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_security_deep.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_session_pack_scale.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_storage.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_sweep_io_embed_backlog.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_tantivy_lexical.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_trust_boundary.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_vector_eligibility.py +0 -0
- {memd_engine-0.5.0 → memd_engine-0.5.2}/tests/test_vector_followups_regressions.py +0 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: memd-engine
|
|
3
|
+
Version: 0.5.2
|
|
4
|
+
Summary: memd - embedded-first agent memory engine: raw+fact lanes, bitemporal supersedence, provenance/trust tiers, hybrid retrieval
|
|
5
|
+
Author: memd contributors
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/siinghd/memd
|
|
8
|
+
Project-URL: Documentation, https://siinghd.github.io/memd/
|
|
9
|
+
Project-URL: Issues, https://github.com/siinghd/memd/issues
|
|
10
|
+
Keywords: agent,memory,llm,retrieval,mcp
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: numpy>=1.26
|
|
23
|
+
Requires-Dist: fastapi>=0.110
|
|
24
|
+
Requires-Dist: uvicorn>=0.29
|
|
25
|
+
Requires-Dist: httpx>=0.27
|
|
26
|
+
Requires-Dist: cryptography>=42
|
|
27
|
+
Provides-Extra: mcp
|
|
28
|
+
Requires-Dist: mcp>=2.0; extra == "mcp"
|
|
29
|
+
Provides-Extra: s3
|
|
30
|
+
Requires-Dist: boto3>=1.34; extra == "s3"
|
|
31
|
+
Provides-Extra: local-embeddings
|
|
32
|
+
Requires-Dist: fastembed>=0.4; extra == "local-embeddings"
|
|
33
|
+
Provides-Extra: fast
|
|
34
|
+
Requires-Dist: tantivy>=0.26; extra == "fast"
|
|
35
|
+
Provides-Extra: ann
|
|
36
|
+
Requires-Dist: usearch>=2.25; extra == "ann"
|
|
37
|
+
Provides-Extra: jev
|
|
38
|
+
Requires-Dist: typesafe-sdk>=0.7; extra == "jev"
|
|
39
|
+
Provides-Extra: billing
|
|
40
|
+
Requires-Dist: stripe>=15; extra == "billing"
|
|
41
|
+
Provides-Extra: dev
|
|
42
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
43
|
+
Requires-Dist: pytest-timeout>=2; extra == "dev"
|
|
44
|
+
Requires-Dist: stripe>=15; extra == "dev"
|
|
45
|
+
Requires-Dist: moto[server]>=5; extra == "dev"
|
|
46
|
+
Provides-Extra: docs
|
|
47
|
+
Requires-Dist: mkdocs<2,>=1.6; extra == "docs"
|
|
48
|
+
Requires-Dist: mkdocs-material>=9.5; extra == "docs"
|
|
49
|
+
Requires-Dist: mkdocstrings[python]>=0.26; extra == "docs"
|
|
50
|
+
Requires-Dist: mkdocs-include-markdown-plugin>=7; extra == "docs"
|
|
51
|
+
Dynamic: license-file
|
|
52
|
+
|
|
53
|
+
# memd
|
|
54
|
+
|
|
55
|
+
[](https://pypi.org/project/memd-engine/)
|
|
56
|
+
[](https://www.npmjs.com/package/memd-engine)
|
|
57
|
+
[](https://github.com/siinghd/memd/actions/workflows/ci.yml)
|
|
58
|
+
[](https://siinghd.github.io/memd/)
|
|
59
|
+
[](https://github.com/siinghd/memd/blob/master/LICENSE)
|
|
60
|
+
|
|
61
|
+
**The SQLite of agent memory.** memd is a memory engine for AI agents. It
|
|
62
|
+
runs in your process and keeps its data in one directory. It needs no
|
|
63
|
+
database, no server and no account. Apache-2.0.
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
mem = Memory("./my-data") # a directory, not a service
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Status: alpha. The badges show the current release. The [limits](#limits)
|
|
70
|
+
section tells what memd does not do.
|
|
71
|
+
|
|
72
|
+
## Features
|
|
73
|
+
|
|
74
|
+
- **Embedded, with no keys necessary.** Without API keys, memd uses local
|
|
75
|
+
ONNX embeddings (BAAI/bge-small-en-v1.5, with the `local-embeddings`
|
|
76
|
+
extra) or hash embeddings (without the extra). It extracts facts with
|
|
77
|
+
patterns. `stats()` shows the active mode. An OpenAI-compatible key adds
|
|
78
|
+
API embeddings or LLM fact extraction.
|
|
79
|
+
- **No model call on the write path.** memd acknowledges a write when the
|
|
80
|
+
write is durable in the log of the namespace. memd makes the embeddings
|
|
81
|
+
in the background. BM25 search finds the record as soon as the call
|
|
82
|
+
returns.
|
|
83
|
+
- **Raw and fact lanes.** memd keeps the text of each turn, word for word
|
|
84
|
+
(the raw lane). Facts go in a second lane. You save a fact with `remember`, or
|
|
85
|
+
memd extracts facts when a session closes. memd keeps the raw lane, thus
|
|
86
|
+
you can run the extraction again.
|
|
87
|
+
- **Facts that change.** A new fact on an entity key replaces the old fact,
|
|
88
|
+
but memd does not delete the old fact. `get(id, history=True)` shows the
|
|
89
|
+
chain. `search(as_of=...)` shows what was true at a past time.
|
|
90
|
+
- **Provenance and trust.** Each record has a source tier: user > agent >
|
|
91
|
+
tool > web > import. memd puts lower-trust content in a data fence in
|
|
92
|
+
the packed context. An explicit save in a session gets the lowest tier
|
|
93
|
+
that the session saw. memd quarantines bursts of writes and repeated
|
|
94
|
+
near-identical content.
|
|
95
|
+
- **Hybrid retrieval, packed as evidence.** A rules-based planner sends the
|
|
96
|
+
query to the BM25, entity, time and vector lanes. Reciprocal rank fusion
|
|
97
|
+
merges the lanes, and an optional reranker can change the order. memd
|
|
98
|
+
packs the result into a token budget (12,000 by default) as dated
|
|
99
|
+
excerpts of past sessions. Each hit comes with the turns around it. A
|
|
100
|
+
fact shows under the turn that it came from.
|
|
101
|
+
- **Deletion that holds.** memd has hard delete with a physical-purge
|
|
102
|
+
deadline, forget-by-query with a preview, crypto-shred for each
|
|
103
|
+
namespace, and a hash-chained audit log.
|
|
104
|
+
|
|
105
|
+
## Measured quality
|
|
106
|
+
|
|
107
|
+
Session retrieval on LongMemEval_S, through the public `Memory.search`.
|
|
108
|
+
The values are dev-fold means ± fold std, from
|
|
109
|
+
[BENCHMARKS.md](https://github.com/siinghd/memd/blob/master/BENCHMARKS.md). Each row names the measured release.
|
|
110
|
+
|
|
111
|
+
| memd configuration | ndcg@5 | recall_all@5 | mean search time |
|
|
112
|
+
|---|---|---|---|
|
|
113
|
+
| v0.1.0 as shipped | 0.727 ± 0.027 | 0.697 | ~130 ms |
|
|
114
|
+
| v0.2.0, zero-key default (hash embedder, no reranker) | 0.866 ± 0.017 | 0.835 | ~16 ms |
|
|
115
|
+
| v0.2.0 + Jev reranker (`TYPESAFE_API_KEY` set) | 0.955 ± 0.026 | 0.928 | ~1 s (network) |
|
|
116
|
+
|
|
117
|
+
End-to-end QA on LongMemEval_S: 160 questions, stratified by type, one run.
|
|
118
|
+
The reader is DeepSeek V4.1 Flash. The judge is gpt-6-luna-pro.
|
|
119
|
+
|
|
120
|
+
| context given to the reader | accuracy |
|
|
121
|
+
|---|---|
|
|
122
|
+
| previous defaults: hash embedder, no reranker, 2K tokens, flat | 0.779 [0.718, 0.838] |
|
|
123
|
+
| bge-small + local cross-encoder reranker, 12K tokens, flat | 0.823 |
|
|
124
|
+
| bge-small + local cross-encoder reranker, 12K tokens, session layout, relative dates on | 0.875 [0.823, 0.920] |
|
|
125
|
+
| the whole history in the prompt (~105K tokens; exploratory) | 0.906 |
|
|
126
|
+
|
|
127
|
+
The session layout and the 12K budget are now the defaults. The 0.875 run
|
|
128
|
+
also used bge-small embeddings, a reranker and relative-date annotations.
|
|
129
|
+
The defaults use bge-small only with the `local-embeddings` extra, and no
|
|
130
|
+
reranker. The defaults ship the annotations off. Nobody measured the
|
|
131
|
+
defaults as shipped, end to end. The previous-defaults row and the
|
|
132
|
+
whole-history row use the answers of an earlier run.
|
|
133
|
+
|
|
134
|
+
These results come from one public dataset, one reader and one judge. For
|
|
135
|
+
the setup and the limits, see
|
|
136
|
+
[README-engine.md](https://github.com/siinghd/memd/blob/master/README-engine.md#packing-and-the-budget) and
|
|
137
|
+
[BENCHMARKS.md](https://github.com/siinghd/memd/blob/master/BENCHMARKS.md).
|
|
138
|
+
|
|
139
|
+
## Install
|
|
140
|
+
|
|
141
|
+
Install from PyPI. memd needs Python 3.11 or later. The package is
|
|
142
|
+
`memd-engine`. The import name and the command are `memd`.
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
pip install "memd-engine[local-embeddings]"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The `local-embeddings` extra runs BAAI/bge-small-en-v1.5 on the CPU,
|
|
149
|
+
through fastembed. The model downloads on first use. memd fuses it with
|
|
150
|
+
BM25. The extra gives much better recall. On LongMemEval_S session
|
|
151
|
+
retrieval, 97.5% of questions had all their evidence sessions in the top
|
|
152
|
+
10 (recall_all@10 0.975). With BM25 alone, the result was 92.0%. memd ranks
|
|
153
|
+
by BM25 alone without the extra. These are lane-level measurements on 153
|
|
154
|
+
questions. The hash embedder's own vector lane scores 0.640, and memd does
|
|
155
|
+
not use it for ranking.
|
|
156
|
+
|
|
157
|
+
`pip install memd-engine` also works, offline and with no model. memd then
|
|
158
|
+
uses hash embeddings, and it logs one line about it.
|
|
159
|
+
|
|
160
|
+
| extra | adds |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `local-embeddings` | local ONNX embeddings (BAAI/bge-small-en-v1.5 through fastembed), fused with BM25. Recommended. |
|
|
163
|
+
| `mcp` | `memd serve --mcp`, the MCP server |
|
|
164
|
+
| `s3` | `s3://` data roots and the `aws-kms` key provider (boto3) |
|
|
165
|
+
| `fast` | the tantivy accelerator for the BM25 lane |
|
|
166
|
+
| `ann` | the usearch HNSW sidecar for the vector lane |
|
|
167
|
+
| `jev` | the Jev reranker (active only with `TYPESAFE_API_KEY`) |
|
|
168
|
+
| `billing` | Stripe billing for hosted mode |
|
|
169
|
+
|
|
170
|
+
For TypeScript, install the REST client from npm. It needs a memd server.
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
npm install memd-engine
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
To get the latest code from this repository, use one of these commands:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
pip install "memd-engine[local-embeddings] @ git+https://github.com/siinghd/memd.git"
|
|
180
|
+
|
|
181
|
+
git clone https://github.com/siinghd/memd.git && cd memd
|
|
182
|
+
pip install -e ".[local-embeddings,mcp,s3]"
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Quickstart
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from memd import Memory
|
|
189
|
+
|
|
190
|
+
mem = Memory("./my-data")
|
|
191
|
+
mem.add("We deploy with `make ship`, never CI", user_id="u1", session_id="s1")
|
|
192
|
+
mem.remember("The user prefers dark mode", user_id="u1", entity_keys=["user.theme"])
|
|
193
|
+
|
|
194
|
+
hits = mem.search("how do we deploy?", user_id="u1", budget_tokens=500)
|
|
195
|
+
print(hits.items[0].content) # We deploy with `make ship`, never CI
|
|
196
|
+
print(hits.packed_context) # dated session excerpts, ready to put in a prompt
|
|
197
|
+
mem.close()
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
In an agent loop, use two calls. Before your LLM call, call
|
|
201
|
+
`messages = mem.pack(messages, user_id="u1")`. After it, call
|
|
202
|
+
`mem.observe(messages, response, user_id="u1")`. If you start the process
|
|
203
|
+
again, the memory is still in `./my-data`. The
|
|
204
|
+
[`examples/`](https://github.com/siinghd/memd/blob/master/examples/README.md) directory has more scripts that you can run.
|
|
205
|
+
|
|
206
|
+
## One engine, four doors
|
|
207
|
+
|
|
208
|
+
- **Python:** `from memd import Memory`. `Memory(path)` runs the engine in
|
|
209
|
+
your process. `Memory(api_key=..., base_url=...)` gives the same API
|
|
210
|
+
over REST.
|
|
211
|
+
- **HTTP:** `memd serve --http` serves the REST API on port 8700. Each
|
|
212
|
+
namespace gets its own API keys (`memd key create --namespace acme`).
|
|
213
|
+
- **MCP:** `memd serve --mcp` gives four tools to an MCP client:
|
|
214
|
+
`memory_search`, `memory_save`, `memory_forget` and `memory_status`
|
|
215
|
+
([setup](https://github.com/siinghd/memd/blob/master/examples/mcp/README.md)).
|
|
216
|
+
- **TypeScript:** `npm install memd-engine` gives a typed REST client for
|
|
217
|
+
Node 18 or later, Bun, Deno and edge runtimes
|
|
218
|
+
([SDK guide](https://github.com/siinghd/memd/blob/master/sdk-ts/README.md)).
|
|
219
|
+
|
|
220
|
+
## How memd grows
|
|
221
|
+
|
|
222
|
+
The same `Memory` API works at each step. Your code does not change.
|
|
223
|
+
|
|
224
|
+
- **Namespaces.** A namespace is the unit of isolation and of scale. Each
|
|
225
|
+
namespace has its own log, index, data key and audit log. Use one
|
|
226
|
+
namespace for each user, agent or tenant.
|
|
227
|
+
- **Several processes on one data root.** One process at a time writes a
|
|
228
|
+
namespace. The other processes send their writes and strong reads to it
|
|
229
|
+
(write forwarding, on by default). If that process stops, another
|
|
230
|
+
process becomes the writer. Thus `uvicorn --workers N`, one
|
|
231
|
+
`memd serve --mcp` for each MCP client, and a script next to a server can
|
|
232
|
+
share one directory. `forwarding="off"` turns this off.
|
|
233
|
+
- **Object storage.** The same `Memory` runs on an `s3://` root: AWS S3,
|
|
234
|
+
Cloudflare R2 or another S3-compatible store. CI runs the S3 tests
|
|
235
|
+
against RustFS. A durable write is one PUT. A warm search reads no object.
|
|
236
|
+
- **Read replicas.** Reads are strong by default. A search or a get can
|
|
237
|
+
ask for eventual consistency. Then a read replica serves it, within a
|
|
238
|
+
staleness limit.
|
|
239
|
+
- **Multi-node.** Several `memd serve --http` processes on one `s3://` root
|
|
240
|
+
share the namespaces. A lease in the bucket gives each namespace one
|
|
241
|
+
writer. A node sends each request to the node that holds the lease. AWS
|
|
242
|
+
KMS or Vault transit wraps the data keys, thus each node can open each
|
|
243
|
+
namespace.
|
|
244
|
+
- **Search throughput.** The default search (a 12K session pack) is CPU
|
|
245
|
+
work in Python. In one process, more threads do not give more searches
|
|
246
|
+
per second, because of the GIL. To serve more searches, run more
|
|
247
|
+
processes: `uvicorn --workers N`, more processes on one data root, read
|
|
248
|
+
replicas or more nodes. Retrieval alone (a 2K flat pack) scales with
|
|
249
|
+
threads, because SQLite releases the GIL.
|
|
250
|
+
|
|
251
|
+
[README-engine.md](https://github.com/siinghd/memd/blob/master/README-engine.md#several-processes-on-one-data-root)
|
|
252
|
+
gives the details and the measurements.
|
|
253
|
+
|
|
254
|
+
## Security and deletion
|
|
255
|
+
|
|
256
|
+
- memd encrypts data at rest by default, with one data key for each
|
|
257
|
+
namespace. A `local` key file, AWS KMS or Vault transit holds the root key.
|
|
258
|
+
- Key custody fails closed. If a data key is missing or wrong, memd raises
|
|
259
|
+
`KeyCustodyError` and changes nothing. memd never reads such a namespace
|
|
260
|
+
as empty.
|
|
261
|
+
- `delete(id, hard=True)` purges the record from all files within a
|
|
262
|
+
deadline (72 hours by default).
|
|
263
|
+
- `find_ids(query)` shows what `forget(query)` will delete. With the
|
|
264
|
+
fingerprint of that preview, `forget` deletes nothing if the matches
|
|
265
|
+
changed.
|
|
266
|
+
- `destroy_namespace()` destroys the data key of the namespace
|
|
267
|
+
(crypto-shred).
|
|
268
|
+
- A hash-chained audit log records each delete and each forget.
|
|
269
|
+
- The fences around lower-trust content mark it as data. They cannot force
|
|
270
|
+
a model to obey that mark.
|
|
271
|
+
|
|
272
|
+
[SECURITY.md](https://github.com/siinghd/memd/blob/master/SECURITY.md) gives the threat model, what it does not cover,
|
|
273
|
+
and how to report a vulnerability.
|
|
274
|
+
|
|
275
|
+
## Limits
|
|
276
|
+
|
|
277
|
+
- **Many processes can use one namespace. One of them writes.** Each
|
|
278
|
+
process can open the same namespace and write to it. One process (the
|
|
279
|
+
holder) writes the log. The other processes send their writes and their
|
|
280
|
+
strong reads to the holder automatically. If the holder stops, another
|
|
281
|
+
process becomes the holder. Thus, the write capacity of one namespace is
|
|
282
|
+
the capacity of one process. To get more write capacity, use more
|
|
283
|
+
namespaces (for example, one namespace for each user or agent). To get
|
|
284
|
+
more read capacity, use read replicas (opt-in eventual reads, with a
|
|
285
|
+
staleness limit). memd does not let several processes write one
|
|
286
|
+
namespace's log at the same time.
|
|
287
|
+
- **Fact extraction uses patterns by default.** An optional LLM extractor
|
|
288
|
+
uses your own API key. On 30 LongMemEval_S questions it gave no
|
|
289
|
+
measurable accuracy gain: 0.667 against 0.700 for the pattern extractor
|
|
290
|
+
(difference -0.033, 95% CI [-0.167, +0.067]). It also makes a session
|
|
291
|
+
close slower (1.5 s against 0.08 s at the median). A larger evaluation
|
|
292
|
+
is necessary before we recommend it.
|
|
293
|
+
- **The quality evidence comes from one public dataset (LongMemEval_S).**
|
|
294
|
+
Nobody measured the current defaults end to end.
|
|
295
|
+
[BENCHMARKS.md](https://github.com/siinghd/memd/blob/master/BENCHMARKS.md) tells what each run shows and what it
|
|
296
|
+
does not show.
|
|
297
|
+
- **Alpha software.** What changed in each release is in the
|
|
298
|
+
[changelog](https://github.com/siinghd/memd/blob/master/CHANGELOG.md).
|
|
299
|
+
- **Out of scope:** an agent framework or runtime, a platform for RAG over
|
|
300
|
+
documents, and a graph database.
|
|
301
|
+
|
|
302
|
+
## Documentation
|
|
303
|
+
|
|
304
|
+
- Docs site: <https://siinghd.github.io/memd/> (quickstart, concepts, API
|
|
305
|
+
reference, operations). The source is in [`docs/`](https://github.com/siinghd/memd/tree/master/docs). To build it
|
|
306
|
+
locally, run `pip install -e ".[docs]" && mkdocs serve`.
|
|
307
|
+
- [README-engine.md](https://github.com/siinghd/memd/blob/master/README-engine.md): the full engine guide (how to run
|
|
308
|
+
it, several processes, S3, key custody, multi-node, read replicas, hosted
|
|
309
|
+
mode, retrieval options, operations)
|
|
310
|
+
- [BENCHMARKS.md](https://github.com/siinghd/memd/blob/master/BENCHMARKS.md): how we measured quality and latency
|
|
311
|
+
- [SECURITY.md](https://github.com/siinghd/memd/blob/master/SECURITY.md): the threat model, what it does not cover,
|
|
312
|
+
and how to report a vulnerability
|
|
313
|
+
- [CHANGELOG.md](https://github.com/siinghd/memd/blob/master/CHANGELOG.md): each release
|
|
314
|
+
- [CONTRIBUTING.md](https://github.com/siinghd/memd/blob/master/CONTRIBUTING.md): setup, tests, and what a change needs
|
|
315
|
+
- [RELEASING.md](https://github.com/siinghd/memd/blob/master/RELEASING.md): how to publish a release
|
|
316
|
+
|
|
317
|
+
## License
|
|
318
|
+
|
|
319
|
+
[Apache-2.0](https://github.com/siinghd/memd/blob/master/LICENSE).
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# memd
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/memd-engine/)
|
|
4
|
+
[](https://www.npmjs.com/package/memd-engine)
|
|
5
|
+
[](https://github.com/siinghd/memd/actions/workflows/ci.yml)
|
|
6
|
+
[](https://siinghd.github.io/memd/)
|
|
7
|
+
[](https://github.com/siinghd/memd/blob/master/LICENSE)
|
|
8
|
+
|
|
9
|
+
**The SQLite of agent memory.** memd is a memory engine for AI agents. It
|
|
10
|
+
runs in your process and keeps its data in one directory. It needs no
|
|
11
|
+
database, no server and no account. Apache-2.0.
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
mem = Memory("./my-data") # a directory, not a service
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Status: alpha. The badges show the current release. The [limits](#limits)
|
|
18
|
+
section tells what memd does not do.
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- **Embedded, with no keys necessary.** Without API keys, memd uses local
|
|
23
|
+
ONNX embeddings (BAAI/bge-small-en-v1.5, with the `local-embeddings`
|
|
24
|
+
extra) or hash embeddings (without the extra). It extracts facts with
|
|
25
|
+
patterns. `stats()` shows the active mode. An OpenAI-compatible key adds
|
|
26
|
+
API embeddings or LLM fact extraction.
|
|
27
|
+
- **No model call on the write path.** memd acknowledges a write when the
|
|
28
|
+
write is durable in the log of the namespace. memd makes the embeddings
|
|
29
|
+
in the background. BM25 search finds the record as soon as the call
|
|
30
|
+
returns.
|
|
31
|
+
- **Raw and fact lanes.** memd keeps the text of each turn, word for word
|
|
32
|
+
(the raw lane). Facts go in a second lane. You save a fact with `remember`, or
|
|
33
|
+
memd extracts facts when a session closes. memd keeps the raw lane, thus
|
|
34
|
+
you can run the extraction again.
|
|
35
|
+
- **Facts that change.** A new fact on an entity key replaces the old fact,
|
|
36
|
+
but memd does not delete the old fact. `get(id, history=True)` shows the
|
|
37
|
+
chain. `search(as_of=...)` shows what was true at a past time.
|
|
38
|
+
- **Provenance and trust.** Each record has a source tier: user > agent >
|
|
39
|
+
tool > web > import. memd puts lower-trust content in a data fence in
|
|
40
|
+
the packed context. An explicit save in a session gets the lowest tier
|
|
41
|
+
that the session saw. memd quarantines bursts of writes and repeated
|
|
42
|
+
near-identical content.
|
|
43
|
+
- **Hybrid retrieval, packed as evidence.** A rules-based planner sends the
|
|
44
|
+
query to the BM25, entity, time and vector lanes. Reciprocal rank fusion
|
|
45
|
+
merges the lanes, and an optional reranker can change the order. memd
|
|
46
|
+
packs the result into a token budget (12,000 by default) as dated
|
|
47
|
+
excerpts of past sessions. Each hit comes with the turns around it. A
|
|
48
|
+
fact shows under the turn that it came from.
|
|
49
|
+
- **Deletion that holds.** memd has hard delete with a physical-purge
|
|
50
|
+
deadline, forget-by-query with a preview, crypto-shred for each
|
|
51
|
+
namespace, and a hash-chained audit log.
|
|
52
|
+
|
|
53
|
+
## Measured quality
|
|
54
|
+
|
|
55
|
+
Session retrieval on LongMemEval_S, through the public `Memory.search`.
|
|
56
|
+
The values are dev-fold means ± fold std, from
|
|
57
|
+
[BENCHMARKS.md](https://github.com/siinghd/memd/blob/master/BENCHMARKS.md). Each row names the measured release.
|
|
58
|
+
|
|
59
|
+
| memd configuration | ndcg@5 | recall_all@5 | mean search time |
|
|
60
|
+
|---|---|---|---|
|
|
61
|
+
| v0.1.0 as shipped | 0.727 ± 0.027 | 0.697 | ~130 ms |
|
|
62
|
+
| v0.2.0, zero-key default (hash embedder, no reranker) | 0.866 ± 0.017 | 0.835 | ~16 ms |
|
|
63
|
+
| v0.2.0 + Jev reranker (`TYPESAFE_API_KEY` set) | 0.955 ± 0.026 | 0.928 | ~1 s (network) |
|
|
64
|
+
|
|
65
|
+
End-to-end QA on LongMemEval_S: 160 questions, stratified by type, one run.
|
|
66
|
+
The reader is DeepSeek V4.1 Flash. The judge is gpt-6-luna-pro.
|
|
67
|
+
|
|
68
|
+
| context given to the reader | accuracy |
|
|
69
|
+
|---|---|
|
|
70
|
+
| previous defaults: hash embedder, no reranker, 2K tokens, flat | 0.779 [0.718, 0.838] |
|
|
71
|
+
| bge-small + local cross-encoder reranker, 12K tokens, flat | 0.823 |
|
|
72
|
+
| bge-small + local cross-encoder reranker, 12K tokens, session layout, relative dates on | 0.875 [0.823, 0.920] |
|
|
73
|
+
| the whole history in the prompt (~105K tokens; exploratory) | 0.906 |
|
|
74
|
+
|
|
75
|
+
The session layout and the 12K budget are now the defaults. The 0.875 run
|
|
76
|
+
also used bge-small embeddings, a reranker and relative-date annotations.
|
|
77
|
+
The defaults use bge-small only with the `local-embeddings` extra, and no
|
|
78
|
+
reranker. The defaults ship the annotations off. Nobody measured the
|
|
79
|
+
defaults as shipped, end to end. The previous-defaults row and the
|
|
80
|
+
whole-history row use the answers of an earlier run.
|
|
81
|
+
|
|
82
|
+
These results come from one public dataset, one reader and one judge. For
|
|
83
|
+
the setup and the limits, see
|
|
84
|
+
[README-engine.md](https://github.com/siinghd/memd/blob/master/README-engine.md#packing-and-the-budget) and
|
|
85
|
+
[BENCHMARKS.md](https://github.com/siinghd/memd/blob/master/BENCHMARKS.md).
|
|
86
|
+
|
|
87
|
+
## Install
|
|
88
|
+
|
|
89
|
+
Install from PyPI. memd needs Python 3.11 or later. The package is
|
|
90
|
+
`memd-engine`. The import name and the command are `memd`.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pip install "memd-engine[local-embeddings]"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The `local-embeddings` extra runs BAAI/bge-small-en-v1.5 on the CPU,
|
|
97
|
+
through fastembed. The model downloads on first use. memd fuses it with
|
|
98
|
+
BM25. The extra gives much better recall. On LongMemEval_S session
|
|
99
|
+
retrieval, 97.5% of questions had all their evidence sessions in the top
|
|
100
|
+
10 (recall_all@10 0.975). With BM25 alone, the result was 92.0%. memd ranks
|
|
101
|
+
by BM25 alone without the extra. These are lane-level measurements on 153
|
|
102
|
+
questions. The hash embedder's own vector lane scores 0.640, and memd does
|
|
103
|
+
not use it for ranking.
|
|
104
|
+
|
|
105
|
+
`pip install memd-engine` also works, offline and with no model. memd then
|
|
106
|
+
uses hash embeddings, and it logs one line about it.
|
|
107
|
+
|
|
108
|
+
| extra | adds |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `local-embeddings` | local ONNX embeddings (BAAI/bge-small-en-v1.5 through fastembed), fused with BM25. Recommended. |
|
|
111
|
+
| `mcp` | `memd serve --mcp`, the MCP server |
|
|
112
|
+
| `s3` | `s3://` data roots and the `aws-kms` key provider (boto3) |
|
|
113
|
+
| `fast` | the tantivy accelerator for the BM25 lane |
|
|
114
|
+
| `ann` | the usearch HNSW sidecar for the vector lane |
|
|
115
|
+
| `jev` | the Jev reranker (active only with `TYPESAFE_API_KEY`) |
|
|
116
|
+
| `billing` | Stripe billing for hosted mode |
|
|
117
|
+
|
|
118
|
+
For TypeScript, install the REST client from npm. It needs a memd server.
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
npm install memd-engine
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
To get the latest code from this repository, use one of these commands:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pip install "memd-engine[local-embeddings] @ git+https://github.com/siinghd/memd.git"
|
|
128
|
+
|
|
129
|
+
git clone https://github.com/siinghd/memd.git && cd memd
|
|
130
|
+
pip install -e ".[local-embeddings,mcp,s3]"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Quickstart
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from memd import Memory
|
|
137
|
+
|
|
138
|
+
mem = Memory("./my-data")
|
|
139
|
+
mem.add("We deploy with `make ship`, never CI", user_id="u1", session_id="s1")
|
|
140
|
+
mem.remember("The user prefers dark mode", user_id="u1", entity_keys=["user.theme"])
|
|
141
|
+
|
|
142
|
+
hits = mem.search("how do we deploy?", user_id="u1", budget_tokens=500)
|
|
143
|
+
print(hits.items[0].content) # We deploy with `make ship`, never CI
|
|
144
|
+
print(hits.packed_context) # dated session excerpts, ready to put in a prompt
|
|
145
|
+
mem.close()
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
In an agent loop, use two calls. Before your LLM call, call
|
|
149
|
+
`messages = mem.pack(messages, user_id="u1")`. After it, call
|
|
150
|
+
`mem.observe(messages, response, user_id="u1")`. If you start the process
|
|
151
|
+
again, the memory is still in `./my-data`. The
|
|
152
|
+
[`examples/`](https://github.com/siinghd/memd/blob/master/examples/README.md) directory has more scripts that you can run.
|
|
153
|
+
|
|
154
|
+
## One engine, four doors
|
|
155
|
+
|
|
156
|
+
- **Python:** `from memd import Memory`. `Memory(path)` runs the engine in
|
|
157
|
+
your process. `Memory(api_key=..., base_url=...)` gives the same API
|
|
158
|
+
over REST.
|
|
159
|
+
- **HTTP:** `memd serve --http` serves the REST API on port 8700. Each
|
|
160
|
+
namespace gets its own API keys (`memd key create --namespace acme`).
|
|
161
|
+
- **MCP:** `memd serve --mcp` gives four tools to an MCP client:
|
|
162
|
+
`memory_search`, `memory_save`, `memory_forget` and `memory_status`
|
|
163
|
+
([setup](https://github.com/siinghd/memd/blob/master/examples/mcp/README.md)).
|
|
164
|
+
- **TypeScript:** `npm install memd-engine` gives a typed REST client for
|
|
165
|
+
Node 18 or later, Bun, Deno and edge runtimes
|
|
166
|
+
([SDK guide](https://github.com/siinghd/memd/blob/master/sdk-ts/README.md)).
|
|
167
|
+
|
|
168
|
+
## How memd grows
|
|
169
|
+
|
|
170
|
+
The same `Memory` API works at each step. Your code does not change.
|
|
171
|
+
|
|
172
|
+
- **Namespaces.** A namespace is the unit of isolation and of scale. Each
|
|
173
|
+
namespace has its own log, index, data key and audit log. Use one
|
|
174
|
+
namespace for each user, agent or tenant.
|
|
175
|
+
- **Several processes on one data root.** One process at a time writes a
|
|
176
|
+
namespace. The other processes send their writes and strong reads to it
|
|
177
|
+
(write forwarding, on by default). If that process stops, another
|
|
178
|
+
process becomes the writer. Thus `uvicorn --workers N`, one
|
|
179
|
+
`memd serve --mcp` for each MCP client, and a script next to a server can
|
|
180
|
+
share one directory. `forwarding="off"` turns this off.
|
|
181
|
+
- **Object storage.** The same `Memory` runs on an `s3://` root: AWS S3,
|
|
182
|
+
Cloudflare R2 or another S3-compatible store. CI runs the S3 tests
|
|
183
|
+
against RustFS. A durable write is one PUT. A warm search reads no object.
|
|
184
|
+
- **Read replicas.** Reads are strong by default. A search or a get can
|
|
185
|
+
ask for eventual consistency. Then a read replica serves it, within a
|
|
186
|
+
staleness limit.
|
|
187
|
+
- **Multi-node.** Several `memd serve --http` processes on one `s3://` root
|
|
188
|
+
share the namespaces. A lease in the bucket gives each namespace one
|
|
189
|
+
writer. A node sends each request to the node that holds the lease. AWS
|
|
190
|
+
KMS or Vault transit wraps the data keys, thus each node can open each
|
|
191
|
+
namespace.
|
|
192
|
+
- **Search throughput.** The default search (a 12K session pack) is CPU
|
|
193
|
+
work in Python. In one process, more threads do not give more searches
|
|
194
|
+
per second, because of the GIL. To serve more searches, run more
|
|
195
|
+
processes: `uvicorn --workers N`, more processes on one data root, read
|
|
196
|
+
replicas or more nodes. Retrieval alone (a 2K flat pack) scales with
|
|
197
|
+
threads, because SQLite releases the GIL.
|
|
198
|
+
|
|
199
|
+
[README-engine.md](https://github.com/siinghd/memd/blob/master/README-engine.md#several-processes-on-one-data-root)
|
|
200
|
+
gives the details and the measurements.
|
|
201
|
+
|
|
202
|
+
## Security and deletion
|
|
203
|
+
|
|
204
|
+
- memd encrypts data at rest by default, with one data key for each
|
|
205
|
+
namespace. A `local` key file, AWS KMS or Vault transit holds the root key.
|
|
206
|
+
- Key custody fails closed. If a data key is missing or wrong, memd raises
|
|
207
|
+
`KeyCustodyError` and changes nothing. memd never reads such a namespace
|
|
208
|
+
as empty.
|
|
209
|
+
- `delete(id, hard=True)` purges the record from all files within a
|
|
210
|
+
deadline (72 hours by default).
|
|
211
|
+
- `find_ids(query)` shows what `forget(query)` will delete. With the
|
|
212
|
+
fingerprint of that preview, `forget` deletes nothing if the matches
|
|
213
|
+
changed.
|
|
214
|
+
- `destroy_namespace()` destroys the data key of the namespace
|
|
215
|
+
(crypto-shred).
|
|
216
|
+
- A hash-chained audit log records each delete and each forget.
|
|
217
|
+
- The fences around lower-trust content mark it as data. They cannot force
|
|
218
|
+
a model to obey that mark.
|
|
219
|
+
|
|
220
|
+
[SECURITY.md](https://github.com/siinghd/memd/blob/master/SECURITY.md) gives the threat model, what it does not cover,
|
|
221
|
+
and how to report a vulnerability.
|
|
222
|
+
|
|
223
|
+
## Limits
|
|
224
|
+
|
|
225
|
+
- **Many processes can use one namespace. One of them writes.** Each
|
|
226
|
+
process can open the same namespace and write to it. One process (the
|
|
227
|
+
holder) writes the log. The other processes send their writes and their
|
|
228
|
+
strong reads to the holder automatically. If the holder stops, another
|
|
229
|
+
process becomes the holder. Thus, the write capacity of one namespace is
|
|
230
|
+
the capacity of one process. To get more write capacity, use more
|
|
231
|
+
namespaces (for example, one namespace for each user or agent). To get
|
|
232
|
+
more read capacity, use read replicas (opt-in eventual reads, with a
|
|
233
|
+
staleness limit). memd does not let several processes write one
|
|
234
|
+
namespace's log at the same time.
|
|
235
|
+
- **Fact extraction uses patterns by default.** An optional LLM extractor
|
|
236
|
+
uses your own API key. On 30 LongMemEval_S questions it gave no
|
|
237
|
+
measurable accuracy gain: 0.667 against 0.700 for the pattern extractor
|
|
238
|
+
(difference -0.033, 95% CI [-0.167, +0.067]). It also makes a session
|
|
239
|
+
close slower (1.5 s against 0.08 s at the median). A larger evaluation
|
|
240
|
+
is necessary before we recommend it.
|
|
241
|
+
- **The quality evidence comes from one public dataset (LongMemEval_S).**
|
|
242
|
+
Nobody measured the current defaults end to end.
|
|
243
|
+
[BENCHMARKS.md](https://github.com/siinghd/memd/blob/master/BENCHMARKS.md) tells what each run shows and what it
|
|
244
|
+
does not show.
|
|
245
|
+
- **Alpha software.** What changed in each release is in the
|
|
246
|
+
[changelog](https://github.com/siinghd/memd/blob/master/CHANGELOG.md).
|
|
247
|
+
- **Out of scope:** an agent framework or runtime, a platform for RAG over
|
|
248
|
+
documents, and a graph database.
|
|
249
|
+
|
|
250
|
+
## Documentation
|
|
251
|
+
|
|
252
|
+
- Docs site: <https://siinghd.github.io/memd/> (quickstart, concepts, API
|
|
253
|
+
reference, operations). The source is in [`docs/`](https://github.com/siinghd/memd/tree/master/docs). To build it
|
|
254
|
+
locally, run `pip install -e ".[docs]" && mkdocs serve`.
|
|
255
|
+
- [README-engine.md](https://github.com/siinghd/memd/blob/master/README-engine.md): the full engine guide (how to run
|
|
256
|
+
it, several processes, S3, key custody, multi-node, read replicas, hosted
|
|
257
|
+
mode, retrieval options, operations)
|
|
258
|
+
- [BENCHMARKS.md](https://github.com/siinghd/memd/blob/master/BENCHMARKS.md): how we measured quality and latency
|
|
259
|
+
- [SECURITY.md](https://github.com/siinghd/memd/blob/master/SECURITY.md): the threat model, what it does not cover,
|
|
260
|
+
and how to report a vulnerability
|
|
261
|
+
- [CHANGELOG.md](https://github.com/siinghd/memd/blob/master/CHANGELOG.md): each release
|
|
262
|
+
- [CONTRIBUTING.md](https://github.com/siinghd/memd/blob/master/CONTRIBUTING.md): setup, tests, and what a change needs
|
|
263
|
+
- [RELEASING.md](https://github.com/siinghd/memd/blob/master/RELEASING.md): how to publish a release
|
|
264
|
+
|
|
265
|
+
## License
|
|
266
|
+
|
|
267
|
+
[Apache-2.0](https://github.com/siinghd/memd/blob/master/LICENSE).
|
|
@@ -4,9 +4,9 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "memd-engine"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.2"
|
|
8
8
|
description = "memd - embedded-first agent memory engine: raw+fact lanes, bitemporal supersedence, provenance/trust tiers, hybrid retrieval"
|
|
9
|
-
readme = "README
|
|
9
|
+
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
11
11
|
license = { text = "Apache-2.0" }
|
|
12
12
|
authors = [{ name = "memd contributors" }]
|
|
@@ -16,6 +16,9 @@ classifiers = [
|
|
|
16
16
|
"Intended Audience :: Developers",
|
|
17
17
|
"License :: OSI Approved :: Apache Software License",
|
|
18
18
|
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
19
22
|
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
20
23
|
]
|
|
21
24
|
dependencies = [
|
|
@@ -2024,7 +2024,10 @@ class Memory:
|
|
|
2024
2024
|
`extraction_errors` counts the extraction calls that failed and
|
|
2025
2025
|
`raw_failed` their turns: the LLM extractor's turns of a failed call
|
|
2026
2026
|
go through the pattern extractor instead; an extractor that raised
|
|
2027
|
-
outright counts 1 call, all its turns, and adds no facts.
|
|
2027
|
+
outright counts 1 call, all its turns, and adds no facts.
|
|
2028
|
+
`raw_failed_by_reason` gives those turns by failure reason (the
|
|
2029
|
+
reason labels of memd_extraction_chunks_failed_total; "error" for an
|
|
2030
|
+
extractor that raised)."""
|
|
2028
2031
|
impl = self._hosted()
|
|
2029
2032
|
if impl is not None:
|
|
2030
2033
|
return impl.close_session(session_id, user_id=user_id, namespace=namespace)
|
|
@@ -2054,6 +2057,7 @@ class Memory:
|
|
|
2054
2057
|
# through the pattern extractor instead - reported, not silent
|
|
2055
2058
|
extraction_errors = list(getattr(extracted, "errors", None) or [])
|
|
2056
2059
|
raw_failed = int(getattr(extracted, "failed_records", 0) or 0)
|
|
2060
|
+
raw_failed_by_reason = dict(getattr(extracted, "failed_by_reason", None) or {})
|
|
2057
2061
|
if extraction_errors:
|
|
2058
2062
|
self._audit_for(ns.namespace).append(
|
|
2059
2063
|
actor="system", action="extraction_degraded", target=session_id,
|
|
@@ -2068,6 +2072,7 @@ class Memory:
|
|
|
2068
2072
|
extracted = []
|
|
2069
2073
|
extraction_errors = ["error"]
|
|
2070
2074
|
raw_failed = len(to_extract)
|
|
2075
|
+
raw_failed_by_reason = {"error": raw_failed} if raw_failed else {}
|
|
2071
2076
|
facts_capped = 0
|
|
2072
2077
|
if callable(max_facts):
|
|
2073
2078
|
max_facts = max_facts(len(extracted))
|
|
@@ -2100,6 +2105,7 @@ class Memory:
|
|
|
2100
2105
|
"raw_considered": len(to_extract),
|
|
2101
2106
|
"raw_skipped": len(seg_records) - len(to_extract),
|
|
2102
2107
|
"raw_failed": raw_failed,
|
|
2108
|
+
"raw_failed_by_reason": raw_failed_by_reason,
|
|
2103
2109
|
"facts_extracted": len(extracted),
|
|
2104
2110
|
"extraction_errors": len(extraction_errors),
|
|
2105
2111
|
"facts_capped": facts_capped,
|