Documentation menu
Adapters
An adapter connects Memtru to a memory system. It receives a context for every call (tenant, user, optional session, run namespace, run id) and must apply that context the way your production code would. The scenarios then check whether it actually does.
| Type | Status | Verified by |
|---|---|---|
in_memory |
Available | The Memtru test suite |
http |
Available | Integration tests against the bundled reference server |
custom |
Available | You: Memtru only loads your class |
mem0 |
Optional, OSS SDK 2.2.1 | SDK unit contracts and real local Qdrant storage; deterministic embeddings, no external LLM calls |
| Hosted memory providers | Planned | Not implemented; no claims are made about them |
Capabilities
Adapters declare what they support. Missing prerequisites make a scenario SKIPPED, never PASS.
| Capability | Meaning |
|---|---|
| MEMORY_WRITE | Add a record |
| MEMORY_SEARCH | Retrieve records by query |
| MEMORY_READ_BY_ID | Fetch one record by id |
| MEMORY_UPDATE | Change a record's content or metadata |
| MEMORY_DELETE | Delete a record |
| USER_ISOLATION / TENANT_ISOLATION / SESSION_ISOLATION | The system claims to enforce this boundary |
| ASYNC_INDEXING | Writes and deletes become visible after a delay |
| METADATA_FILTERING | Metadata is stored and returned |
| AGENT_RESPOND | The adapter can run the agent (agent mode) |
| LIFECYCLE_* / THREAD_DELETE | Lifecycle hooks for Afterlife: pending writes, conversation history, sessions, consolidation, restart, extra surfaces, thread deletion. Only kept for adapters that implement MemoryLifecycleHooks. |
The built-in in_memory store implements every lifecycle hook and adds two faults, late_write_resurrection and history_reingestion, used by memtru demo --afterlife.
in_memory
adapter:
type: in_memory
faults: [] # optional defects, see below
index_delay_ms: 0 # simulate eventual consistency (0-10000)
A thread-safe reference store that enforces tenant, user and session scoping. Faults reproduce one realistic bug each:
| Fault | Models | Detected by |
|---|---|---|
ignore_user_scope |
Reads filter by tenant only | MT-ISO-001 |
ignore_tenant_scope |
Records keyed by user id only | MT-TEN-002 |
ignore_session_scope |
Session id stored but not filtered | MT-SES-005 |
soft_delete_visible |
Soft-delete flag ignored on read | MT-DEL-003 |
stale_update |
Update implemented as insert | MT-STALE-004 (and MT-INT-006) |
update_by_key |
UPDATE … WHERE key = ? without owner |
MT-INT-006 |
http
For any service that implements this REST contract:
| Method | Path | Body / response |
|---|---|---|
| GET | /capabilities |
{"capabilities": ["MEMORY_WRITE", …]} |
| POST | /memories |
body {content, metadata, scope} → record |
| GET | /memories/search?q=&limit= |
{"results": [record, …]} |
| GET | /memories/{id} |
record, or 404 |
| PATCH | /memories/{id} |
body {content, metadata} → record |
| DELETE | /memories/{id} |
204 |
A record is {id, content, metadata, tenant_id?, user_id?, session_id?, active?}. Context travels as headers: X-Memtru-Tenant, X-Memtru-User, X-Memtru-Session (optional), X-Memtru-Namespace, X-Memtru-Run.
Optional lifecycle endpoints (Afterlife)
Implement these to run Afterlife from any language, and list the matching capability in GET /capabilities. Each event returns a receipt {executed, detail, created: [record], handled: [id]}; executed: false makes the scenario INCONCLUSIVE. Lifecycle requests are never retried, because replaying an event could fake a result.
| Method | Path | Capability | Body / response |
|---|---|---|---|
| POST | /lifecycle/pending-writes |
LIFECYCLE_PENDING_WRITES | body {content, target_id, metadata} → {id, target_id}; start the write and hold it |
| POST | /lifecycle/pending-writes/release |
LIFECYCLE_PENDING_WRITES | receipt; handled lists released write ids |
| POST | /lifecycle/conversation |
LIFECYCLE_CONVERSATION | body {messages: [string]} → receipt |
| POST | /lifecycle/session |
LIFECYCLE_NEW_SESSION | receipt (include session-start ingestion) |
| POST | /lifecycle/consolidate |
LIFECYCLE_CONSOLIDATE | receipt |
| POST | /lifecycle/restart |
LIFECYCLE_RESTART | receipt; no context headers (instance-wide) |
| POST | /lifecycle/thread/delete |
THREAD_DELETE | receipt |
| GET | /lifecycle/surfaces |
LIFECYCLE_SURFACES | {"surfaces": [name, …]}; no context headers |
| GET | /lifecycle/surfaces/{name}/search?q= |
LIFECYCLE_SURFACES | {"results": [record, …]} |
Return every record an event creates in created so the run can clean it up. The reference server (python -m memtru.testing.reference_server) implements all of them, including the faults late_write_resurrection and history_reingestion.
adapter:
type: http
base_url: https://memory-staging.internal.example
token_env: MEMORY_API_TOKEN # sent as Authorization: Bearer
authorization_confirmed: true # required for non-loopback targets
allow_hosts: [memory-staging.internal.example]
allow_private_network: true # only if the target is on a private range
timeout_seconds: 10
max_retries: 2
How identity is mapped is part of what you are testing. The headers above are a test-harness convention. In a real deployment the service should derive identity from authentication, not from headers a client chooses. Two honest ways to use the http adapter:
- Point it at a small test shim in front of your memory layer that maps the headers onto your real authorization path (for example, minting a per-user token).
- Point it at a staging deployment that deliberately trusts the headers. Then a PASS shows your storage filters work, not that your authentication does.
Safety controls
- Only
httpandhttps; credentials in URLs are rejected. - Link-local, metadata (169.254.169.254,
metadata.google.internal), multicast and reserved addresses are always refused. - Loopback needs
allow_loopback: true; private ranges needallow_private_network: true. - Non-loopback targets need
authorization_confirmed: true. - Redirects are not followed. Responses over 1 MB are rejected. Retries are bounded (429/502/503/504 and transport errors only).
- Hostnames are resolved and every address is checked when the adapter is built. DNS that changes after that check (rebinding) is not re-validated per request; use
allow_hostsand a trusted resolver for remote targets.
Try it locally
python -m memtru.testing.reference_server --port 8799
python -m memtru.testing.reference_server --port 8798 --fault ignore_user_scope
Then use examples/provider-integration/memtru.yaml.
custom
adapter:
type: custom
entrypoint: my_package.memory_adapter:build # module:callable
options: {dsn_env: MEMORY_DSN} # passed as keyword arguments
The callable must return an instance of memtru.adapters.base.MemoryAdapter:
from memtru.adapters.base import MemoryAdapter
from memtru.core.models import Capability, MemoryContext, MemoryRecord
class MyAdapter(MemoryAdapter):
name = "my_store"
def capabilities(self):
return frozenset({Capability.MEMORY_WRITE, Capability.MEMORY_SEARCH, Capability.USER_ISOLATION})
def add(self, ctx: MemoryContext, content, *, metadata=None, scope="user") -> MemoryRecord: ...
def search(self, ctx: MemoryContext, query, *, limit=10) -> list[MemoryRecord]: ...
Implement get, update, delete if supported, and teardown(namespace) to release run data. teardown must only touch the given namespace. A complete example is in examples/custom-adapter/.
mem0
Install the pinned optional integration:
pip install "memtru[mem0]"
For Afterlife MT-AF-001 the adapter declares LIFECYCLE_PENDING_WRITES: a pending write is the SDK's real Memory.update paused at its vector-store write. Conversation ingestion needs an LLM, so MT-AF-002 is skipped.
The adapter accepts a synchronous OSS Memory created by an explicit local Python factory. It does not accept the hosted MemoryClient. The factory is trusted executable code, like a custom adapter; use only reviewed factories. SDK version mismatches fail with an actionable error. Memtru disables the SDK's own telemetry before importing the configured factory. Set MEM0_TELEMETRY=false before importing the SDK yourself as well.
adapter:
type: mem0
factory: sandbox:build
authorization_confirmed: true
Writes call add(..., infer=False) so unique synthetic markers are preserved. Search supplies native user_id, run namespace and tenant metadata filters without post-filtering returned records. A PASS verifies this storage integration's filters, not authentication of end users. Update uses text=; deletion and teardown only address exact synthetic ids this adapter created. Broad delete_all and reset are never called.
| Operation / scenario | Support |
|---|---|
| Add, search, metadata | Supported |
| Update | Discovered from the synchronous factory instance |
| Delete and exact-id cleanup | Supported |
| User / tenant isolation | Native search-filter tests only |
| Read by id | Not advertised: OSS get(id) has no principal argument |
| Session / integrity / agent scenarios | SKIPPED; prerequisite capabilities absent |
| Hosted API, real-model semantic retrieval | NOT EXECUTED |
For a no-cost local integration verification, install development dependencies and run:
cd packages/memtru
pip install -e ".[dev,mem0]"
pytest tests/test_mem0_adapter.py tests/test_mem0_local.py
The latter uses the real SDK, Qdrant and SQLite history, with deterministic embeddings and an LLM substitute that raises if invoked. It checks four engine scenarios, expected skips, cleanup and preservation of an unrelated synthetic control. It is not a semantic-quality benchmark.
For an optional real-embedding sandbox run, review examples/mem0/sandbox.py first. It uses local Qdrant and your OpenAI account for embeddings, which may incur charges. Supply OPENAI_API_KEY through the environment; never put it in YAML. Set MEM0_SANDBOX_DIR to a new dedicated path (the example refuses to reuse an existing directory), then run memtru test from examples/mem0. On PowerShell use $env:MEM0_SANDBOX_DIR='C:\\temp\\memtru-sandbox-unique'; on POSIX use export MEM0_SANDBOX_DIR=/tmp/memtru-sandbox-unique. Raw insertion avoids extraction LLM calls, but embedding requests still leave the machine. This optional path was NOT EXECUTED because credentials and spending authorization were not supplied.
If exact markers are not retrievable, the positive control yields INCONCLUSIVE; inspect indexing and model configuration rather than treating absence as privacy proof. Provider errors expose an operation and exception type, never the provider's raw message. After timeout, quarantine that sandbox until late work finishes; see Troubleshooting.
SDK signatures were checked against the Mem0 v2.2.1 source and official quickstart. Other versions are not claimed compatible.
Safe test environments
Run against a sandbox or staging environment. execution.environment: production is refused unless execution.allow_production: true is set, because scenarios write and delete synthetic records.