Skip to content
Memtru
Documentation menu
Reference

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:

  1. 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).
  2. 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 http and https; 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 need allow_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_hosts and 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.