Intent
Let an agent runtime treat memory as a replaceable component: one interface, many backends, chosen by configuration. Then answer the question the interface makes urgent — when memory lives in someone else's store, who is responsible for forgetting?
The page makes two kinds of claim. The first half is reported practice: host runtimes that mount memory behind one lifecycle contract are common in this atlas, from Hermes and OpenClaw to ADK, AutoGen, Agno and Strands. The second half is argued: carrying scope and deletion across that contract. ADK and MateClaw declare scope; the Pydantic AI Harness declares a targeted delete; no contract read here declares both. That half rests on one or two instances, and the page is arguing for it rather than reporting it.
The problem
A host runtime that hard-codes its memory model forces every user into one set of trade-offs. A local Markdown store cannot do cross-session user modelling; a hosted graph service is overkill for a single developer's laptop. So mature runtimes make memory pluggable.
The moment they do, memory stops being one system and becomes two: the host, which owns the conversation, the prompt, the user relationship, and any built-in memory of its own; and the provider, which owns durable storage, extraction, and retrieval.
Responsibilities that were implicit in a single system now have to cross a boundary:
- The user tells the host to forget something. Does that reach the provider?
- The provider extracts a claim the host never reviewed. Whose trust model applies?
- The host has its own built-in memory and a provider is mounted. Which wins, and are they deduplicated?
- The host wraps messages in scaffolding. Does the provider know to strip it?
- The host is multi-tenant. Does the interface even carry a scope?
Interfaces built around the happy path — write, read, prefetch — answer none of these.
The pattern
Define a lifecycle contract the host calls at known points, and make the ownership questions explicit parts of it rather than leaving them to each plugin.
A workable contract has four groups:
Lifecycle initialize / shutdown / on_session_start / on_session_end
Read prefetch(query, scope) -> context block
system_prompt_block() -> static text
Write sync_turn(user, assistant, scope)
on_memory_write(action, target, content) # mirror host writes
Governance forget(scope | id) -> result # <-- usually missing
scope on EVERY call # <-- usually missing
capabilities() -> what this provider actually supports
Diagram source
%% caption: the interface is where deletion is lost — a host with no delete hook stops the request at the boundary, and a mirrored write with no reconciliation leaves two stores disagreeing
flowchart TD
User["User: forget me"] --> Host["Host runtime"]
Host --> Built["Built-in memory"]
Host --> Iface["MemoryProvider<br/>interface"]
Iface --> P1["Local plugin"]
Iface --> P2["Hosted service"]
Iface --> P3["Graph backend"]
Host -.->|"no delete hook<br/>= request stops here"| Iface
Built -.->|"mirrored write,<br/>no reconciliation"| P1The dotted edges are where real implementations break.
Why it works
A narrow interface at a real seam lets the host own conversation, prompt assembly, and policy while the provider owns storage and retrieval. Users pick a backend matching their privacy and scale needs without the host reimplementing five memory architectures. Providers reach many hosts by implementing one contract.
It also concentrates governance in one place: if the interface requires scope on every call and defines a deletion path, every backend inherits those properties whether or not its author thought about them.
Tradeoffs
- Deletion is the hard one. Without a
forgethook, a host-level erasure request has nowhere to go. The host can delete its own copy and still leave the content in a hosted provider indefinitely. - Trust state does not survive the boundary. A host with candidate/verified semantics cannot express them to a provider that stores flat facts, and vice versa.
- Mirroring creates duplicates. Copying host writes into the provider is the obvious way to keep them in sync, and it silently creates two records with independent lifecycles unless removals mirror too.
- Capabilities vary wildly. One provider does hybrid retrieval and bi-temporal validity; another does vector-only lookup. If the interface cannot report capabilities, the host must assume the weakest.
- One provider at a time is the usual limit, chosen to avoid tool-schema bloat and conflicting backends — which means no composition, and switching backends usually means abandoning accumulated memory.
- The host's scaffolding leaks. Providers receive whatever text the host passes; if that includes context markers, reply headers, or compaction summaries, it becomes memory.
Do not adopt this pattern to postpone deciding on a memory model. It relocates the decision; it does not remove it.
Cost to adopt
Build: an interface, a lifecycle for mounting providers, and — the part most implementations skip — scope and deletion in the contract itself.
Forces elsewhere: the contract is the ceiling on every backend's capabilities. Anything absent from it (deletion, scope, trust state) is unreachable no matter what the backend supports, and widening a published contract breaks implementers.
Ongoing: every provider needs its own tests, and cross-cutting concerns like retry and metrics belong in decorators rather than in each backend.
Skip it if you will only ever run one backend. An interface with one implementation is a guess about the future.
Seen in the atlas
Hermes Agent has the most
explicit contract: a MemoryProvider ABC with more than
twenty members covering identity and availability, initialization,
prompt blocks, prefetch and its queued form, recall status, turn start
and per-turn sync, tool schemas and dispatch, session end and switch,
pre-compression extraction, delegation observation, config, backup
paths, and shutdown, with a MemoryManager that mounts
exactly one external provider. First-party adapters ship in-tree for
Honcho, Mem0, Hindsight, Supermemory, OpenViking, ByteRover, RetainDB,
and the built-in Holographic plugin — so the adapters are reviewable
even when a backing service is not. The contract has no deletion
hook and no per-call tenant scope:
initialize(session_id, **kwargs) documents
user_id among optional keyword arguments, and
prefetch and sync_turn take a
session_id, so a provider learns its user once at startup
rather than on each read or write.
OpenClaw reaches the same shape
from the other direction: the memory system is itself a plugin,
memory-core, holding the index, the consolidation pass and
the deletion path, and memory-lancedb is one optional
vector backend beside it. The contract covers capture and injection but
not inspection or repair. Its LanceDB store shows the discipline the
interface itself lacks — scopedPredicate composes agent
scope and operator filter into one predicate "so scope cannot be lost",
and deletes are scoped too.
Holographic demonstrates the
mirroring hazard concretely. Its on_memory_write copies the
host's built-in memory additions into its own SQLite store, but
implements only the add action — so removing an entry from
the host's MEMORY.md leaves the mirrored fact in place,
with no reconciliation path.
Pi is the strongest form of the
argument, because it has no memory contract at all. Its
ExtensionAPI exposes more than twenty lifecycle events —
session_start, session_before_fork,
session_before_compact, context with a result
type, and more — which is ample mechanism, and none of it is
memory-shaped. A plugin can capture, inject, and consolidate; it cannot
be handed a scope or told to forget, because nothing in the host knows
memory exists. Magic Context
consequently rebuilds message indexing, FTS, embeddings, and scope from
scratch on top of those events, and has to decide for itself what a
forked session inherits.
TencentDB Agent
Memory sidesteps the host contract altogether: at its pin it is a
reverse proxy in front of the model API, reached by pointing an agent's
base_url at it, with SDKs and an OpenClaw plugin shipped
beside it. Because it owns the traffic rather than a slot in a host's
interface, deletion and scope are its own to define, and it defines both
— atomic/delete, scenario/rm and a
clear-memory path, each journalled to memory_audit, with
the team/user/agent/task key composed into the delete predicates as well
as the reads.
MateClaw is the counterexample
that shows the gap is a choice, not an inevitability. Its
MemoryProvider SPI declares paired overloads —
prefetch(agentId, query) and
prefetch(agentId, query, ownerKey),
syncTurn(...) with and without ownerKey — so
scope crosses the boundary. It also handles the resilience question the
other contracts leave to each plugin: spi/decorator/ wraps
any provider in MetricsMemoryProvider and
RetryableMemoryProvider, so retry and instrumentation are
solved once for every backend rather than reimplemented per plugin.
Default interface methods give implicit capability negotiation — a
provider implements what it supports.
MateClaw has no deletion hook, so it closes half the governance gap and leaves the other half open.
Across the host contracts read for this page —
Hermes, OpenClaw, Pi (which has none), MateClaw, ADK, AutoGen, Agno's
LearningStore, Microsoft's ContextProvider,
the Pydantic AI Harness's MemoryStore, CAMEL's
AgentMemory, Cosmonapse's Engram and Strands'
MemoryStore — two declare scope: ADK by required keyword
argument, MateClaw by paired overloads. One declares targeted
deletion — the Pydantic AI Harness, whose Protocol is
read, get_operation, write,
delete, list_paths. Everything else offers
clear(), or nothing.
Agno separates two things the other
contracts conflate. Its LearningStore Protocol declares
recall, process, build_context,
instructions and get_tools beside a
learning_type property, and the three that receive the
caller's context — recall, process and
get_tools — take it only as **kwargs. So the
contract expresses neither scope nor deletion, and both
exist anyway: recall(user_id) returns None
when the key is missing, retire_fact keeps a superseded
row, forget archives an entity. The reason is that Agno
writes the contract and six implementations behind it, so what
the interface fails to require, the same team supplies. That is worth
naming because it inverts the finding above — the contract is the
limiting factor when the contract author is not the implementer, and a
permissive interface is survivable exactly when they are the same
people. It is also fragile in the predictable way: a third-party store
satisfying that Protocol can ignore user_id entirely and
nothing detects it. Agno's own _filter_store_kwargs exists
to keep narrow third-party stores working when the framework adds
context kwargs, which is thoughtful, and which also means a store that
quietly drops the scope key is indistinguishable from one that never
wanted it.
Cosmonapse's
Engram is the only contract here with a failure vocabulary,
and the omission is glaring once you have seen one. Every other
interface on this page models the happy path: write, read, prefetch.
This one ships five typed errors — EngramTimeout for an
elapsed deadline, EngramCancelled for a task killed
mid-call, EngramNotBound for storage nobody wired,
EngramOverloaded for a backend that shed
load, and EngramError above them — and two methods
that follow from taking failure seriously.
can_serve(query) -> bool lets a backend decline (the
docstring's example is a BM25 engram asked for vector search, and the
hosting Dendrite then skips responding), which is the only place on this
page where "retrieval can return nothing" is a capability the contract
can express. compensate(trace_id) reverses every journaled
write for a trace, replaying inverses LIFO so nested overwrites unwind,
with commit(trace_id) discarding the journal at the
workflow's commit point.
Two limits are documented rather than left to be found, which is the
right way to ship a rollback: compensation is best-effort, so a failing
inverse is logged and the rest still run, and only Engram state is
reversed — external side effects are explicitly out of scope. The limit
that is not documented is where the journal lives:
self._saga_journal is a dictionary on the instance, so in a
protocol whose transports include NATS and Kafka precisely because
workers are disposable, the rollback guarantee lasts exactly as long as
the process. It also carries no scope parameter.
Outside the corpus, and read without a pinned report, LightAgent shows the
pattern at its smallest — a 196-line shared_memory.py with
swappable adapters shipped as examples
(clawmem_memory_adapter.py,
vector_memory_adapter.py). The shape survives the scale
reduction; so does the missing deletion path.
SmythOS SRE is the
degenerate case, and it is deliberately not counted in the tally
above. LLMMemoryConnector is an abstract class
extending the runtime's secure-connector base, declaring one method —
load(messages) — and it is exported from the package index
at packages/core/src/index.ts. There is no subclass
anywhere in the tree, no registration in the connector service, and no
call site. Nothing mounts it, so it is not a provider contract that
anyone has had to satisfy; it is the seam without the socket. It earns a
mention here because the failure it illustrates is cheap to reach: the
same repository ships a working connector pattern for cache, storage,
vector search, LLM providers and accounts, each with real
implementations behind it, and a reader encountering
MemoryManager reasonably assumes memory has one too. An
exported abstract class is a promise the type system will not keep, and
the distance between "we defined the interface" and "a provider exists"
is the entire pattern.
Two providers in this ecosystem — Redis Agent Memory Server and OpenViking — have far richer internal scope and lifecycle models than the interfaces mounting them can express, which is the clearest evidence that the contract, not the backend, is the limiting factor.
The vendor-neutral contract nobody signed up for
Every interface above is a host's, and each was designed by people
who had to make one runtime work. One is not: OpenTelemetry's GenAI
semantic conventions define, at
46d43c89…, a
gen_ai.operation.name enum containing seven memory
operations — create_memory, search_memory,
update_memory, upsert_memory,
delete_memory, create_memory_store,
delete_memory_store — all at development stability. It
stores nothing and is not a memory system, so it gets no report here; it
is an observability vocabulary, which makes it the closest thing the
field has to an agreed answer about what operations memory
has.
Read against this page's own tally, two things stand out.
It names deletion, twice, and at both granularities.
Of the host contracts read here, one declares targeted deletion. The
vocabulary written by nobody's runtime declares
delete_memory for records and
delete_memory_store for the whole store, and gives
gen_ai.memory.record.count an explicit meaning for a delete
— "the number of memory records the operation attempted to
delete". It also names upsert_memory separately from
create and update, defined as create-or-update "without the caller
choosing which", which is the operation most of these systems
actually implement and almost none of them name.
It declares no scope at all. There is no tenant,
user, project or agent attribute on a memory operation — only
gen_ai.memory.store.id, with the note that each component
"SHOULD document what gen_ai.memory.store.id maps
to". Isolation is left as an implementation detail of the store id,
which is the same gap most host contracts on this page have, arriving
from another direction.
The record schema is the sharper version of the same point.
model/gen-ai/gen-ai-memory-records.json defines a
MemoryRecord as content (the only required
field), id, score, and an opaque
metadata object. No validity interval, no trust state, no
provenance, no supersession pointer — every mechanism the tombstone, bi-temporal validity and trust state machine pages are about
falls into metadata, where it is by construction not
comparable across implementations. That is not a criticism of a
development-stage spec, which is right to standardise the common shape
first. It is a statement about what the common shape currently is:
a scored string with an id.
RushDB ships the contract without the providers. Its agent-memory contract package is provider-neutral by design — a versioned event schema, deterministic ids, a conformance fixture for other languages, and an eight-step adapter skill with a lifecycle matrix covering pre-inference recall, completed-turn capture, compaction, session end and shutdown. The two runtimes it names are first-class in the protocol's own enum and absent from the repository, occurring in exactly two lines of a reference file. Every hard part of the contract — the durable outbox, the fail-open timeout, the capture exclusions, deactivating a superseded fact — is an adapter obligation, so the conformance matrix is a promise made on an unreviewable implementation's behalf.
Strands Agents answers
two of this page's tradeoffs and confirms the first one exactly. Its
MemoryStore Protocol has one required method,
search, and four optional ones, and the host detects which
a backend implements by inspecting the type —
_has_method resolves the name on the store's class and
returns false when it lands on the Protocol's own stub. So the contract
reports capabilities without a capability flag anybody has to keep in
sync, and the host is not forced to assume the weakest backend. Nor is
it one provider at a time: MemoryManager holds a list, fans
searches out concurrently, and attributes each result to its store — at
the documented price that results are "concatenated in
store-registration order with no cross-store ranking," so the
injection cap can be filled by whichever store was registered first.
Extension points live in context objects
(AddMessagesContext, InjectionQueryContext)
precisely so the method signatures can stay fixed while the calling
convention grows.
Then the ceiling. There is no deletion hook, in
either the Python or the TypeScript contract, and the
Storage layer the vended file store sits on declares
delete that three other subsystems call and no memory path
does. The design doc names the intended substitute — "Corrections
are handled by storing updated facts. Newer entries take precedence via
recency weighting in search results" — and of the three vended
stores one implements recency weighting, as a tie-break between equal
scores. That is this page's "Deletion is the hard one" in its
purest form: an erasure request reaching a well-built interface with
nowhere to go, and a correction story that depends on a ranking property
the contract cannot require.
Resonant mounts its
provider by tool name, and nothing notices when the names move.
Two hooks post JSON-RPC tools/call to Resonant Mind, the same
organisation's store: mind_orient and
mind_ground once per session, and mind_search,
mind_surface, mind_spark and
mind_thread on every prompt
(packages/backend/src/services/hooks.ts:248-258,
:1846-1851). At Resonant Mind's pinned commit and at its 4
October 2026 head, the registry has renamed the first two to
ritual_* and holds none of the last three. Each unknown
name comes back as a JSON-RPC error that callMindTool
returns as null without logging, and the request carries no
credential although the server requires one. The agent can hold the same
server's tools directly through .mcp.json with headers, so
the provider works for the model and fails for the host. Check
the provider's tools/list against the names the host calls,
at startup.
Tests to require
- Delete a memory through the host and prove it is gone from the mounted provider, not just from the host's own store.
- Start the host against the provider version it documents and assert every tool name the host calls is in that provider's tool list.
- Mirror a host write into a provider, remove it at the host, and assert the mirrored copy is removed or explicitly marked orphaned.
- Switch providers and verify the host does not silently serve stale context from the previous backend.
- Issue a request under one scope and assert no provider call can return another scope's memory.
- Feed the provider a message carrying the host's own envelope markers or compaction summaries and assert none of it becomes durable memory.
- Mount a provider that fails or times out, and assert the agent degrades rather than blocking or losing the turn.
- Fork or branch a session and assert the memory each branch sees is what you intended — an untested question wherever the host supports branching.
- Wrap a deliberately failing provider in the host's resilience layer and assert the turn degrades rather than stalling — and that the failure is visible in metrics, not just logged.
- Assert that a provider advertising a capability it lacks is detected — for example, one reporting "hybrid" retrieval while running vector-only.
- Run the host's built-in memory and a provider together, and assert the same fact is not injected twice.