A memory an agent does not own
A2M puts a memory store behind the same kind of boundary MCP puts a tool behind, so an agent can talk to a Python object in its own process, a subprocess, or a hosted service — without any of its own code changing.
draft a2m/0.1 JSON-RPC 2.0 MIT standard library only
The problem
LangChain, Agno, n8n, CrewAI and AutoGen each ship their own memory model. Agents from different frameworks cannot share state, history or knowledge — even when they are running inside the same workflow.
LangChain agent Agno agent n8n node CrewAI crew
[BufferMemory] [AgentMemory] [$json ctx] [EntityMemory]
│ │ │ │
in-process PostgreSQL workflow ctx ChromaDB
✗ no shared state ✗ lost across runs ✗ no cross-framework recall
Sixty seconds
pip install a2m-protocol
from a2m import connect_local, connect_stdio, connect_http
from a2m.memory import MemoryStack
memory = connect_local(MemoryStack()) # in-process
memory = connect_stdio(["python", "-m", "a2m"]) # a child process
memory = connect_http("http://127.0.0.1:8778/") # over the network
memory.remember("the deploy key rotates every ninety days")
memory.recall(query="how often does the key change?")
The transport changes; the client does not. Methods live under
memory/, a namespace chosen so one endpoint can serve A2M alongside
MCP's tools/, resources/ and prompts/.
Nothing above calls a model. Out of the box, ranking is lexical — term overlap weighted by inverse document frequency — so it runs offline, costs nothing and returns the same answer twice. Embeddings are opt-in, and so is the model behind them:
from a2m.retrieval import make_scorer, ollama_embedder
MemoryStack(scorer=make_scorer("hybrid", embed=ollama_embedder("bge-m3")))
A caller may also bring its own vectors, which are stored verbatim and never regenerated — that is what lets two frameworks using different models share one store.
A small core, and capabilities on top
A vector database can write, search and delete in an afternoon, and has no concept of tiers, consolidation or salience. A protocol demanding all of it would be implementable only by its own reference implementation. So A2M defines a small mandatory core that any store can satisfy, and layers the rest into capabilities a server declares and a client checks.
| Capability | Methods | Required |
|---|---|---|
core | describe remember recall timeline forget | yes |
tiers | promote consolidate | no |
salience | reinforce | no |
scopes | adds owner | no |
sessions | session/list session/close | no |
keys | fetch — addressable, upsert | no |
embeddings | adds embedding — caller-owned, verbatim | no |
external | adds uri — points at a file or blob | no |
events | events events/subscribe events/unsubscribe | no |
summarize | summarize — distil records into a durable statement | no |
prompt | adds rendered prompt text beside the records | no |
Which means classic RAG is the degenerate case: one tier,
read-only, recall only. An existing RAG stack becomes an A2M server
by implementing two methods and refusing writes, and every A2M client then works
against it.
A2M and MCP
They are not alternatives. MCP connects one agent to its capabilities. A2M connects many programs to one store.
You notice the difference the moment there are two of anything:
MCP A2M
Claude Code Claude Code ──MCP──▶ [bridge] ─┐
│ LangChain job ─────────────────┤
tools n8n workflow ──────────────────┼──▶ one store
│ CrewAI crew ────────────────────┤
┌────┴────┐ nightly sync script ───────────┘
files search memory
every one of them sees the same memory
MCP is a client-to-server protocol for a model to call capabilities. It works well, and A2M does not replace it — the bridge is a supported way in, and one of those five lanes above. What MCP does not do is give a second program a way to agree with the first about what a memory record is: put two MCP memory servers side by side and they share nothing, because a tool result is prose written for a model to read.
Which is fine when a model is reading, and useless when a program is:
{ "id": "01J8Z9", "content": "the deploy key rotates every ninety days",
"created_at": "2026-07-28T10:15:30.123Z", "tier": "semantic",
"key": "ops/deploy-key", "revision": 2, "score": 0.72 }
A router merging four backends needs id and score. A
framework adapter needs tier to know whether to replay or search. A
sync job needs revision to tell a correction from a duplicate. None
of them has a model in the loop, and none should need one to read a memory
record — which is why A2M returns records and adds text only when asked.
Using it from an agent takes one line, and every MCP client works against any A2M store:
python -m implementations.bridge_mcp --stdio python -m implementations.store_sqlite memory.db
One dependency-free file, with its tool list derived from whatever the store
underneath declares. Same plumbing on both sides — same JSON-RPC 2.0, same stdio
framing, methods under memory/ so one endpoint can serve both — so
bridging is a method table, not a translation.
Four kinds of memory
| Kind | Holds | Read as | Bounded by |
|---|---|---|---|
working | the live transcript | replay, chronological | capacity, per conversation |
episodic | what happened | search, relevance + filter | capacity |
semantic | what is true | search, relevance | unbounded |
procedural | how to do things | search, at task start | unbounded |
working ──spill──▶ episodic ──spill──▶ semantic
│
└──promote──▶ semantic procedural
(recalled 3×) ▲
└── written deliberately
Two different forces move a record, and keeping them apart is the point. Spilling is pressure: a tier is over capacity, so its weakest records are displaced. Promotion is reinforcement: a record recalled often enough has stopped being an episode and become a fact. Consolidation runs promotion first, so a record that keeps proving useful is never displaced by sheer volume of newer material.
Nothing spills into procedural. A fact does not decay into a
procedure.
Seven implementations, one suite
The conformance suite speaks only the protocol — it never imports the server it
tests, so an implementation in another language is checked exactly as a Python
one is. Declaring a capability and then not honouring it is a failure:
a client trusts describe, so a server lying there breaks clients in
ways no defensive coding on their side can fix.
| storage | declares | conformance | |
|---|---|---|---|
python -m a2m | a dict in memory | everything | 130/130 |
server_minimal.py | a dict, stdlib only | core | 42/42 |
server_minimal.ts | a Map, TypeScript | core + keys | 52/52 |
server_readonly.py | a fixed corpus, read-only | core | 33/33 |
store_sqlite.py | SQLite + sqlite-vec | everything | 130/130 |
store_postgres.py | PostgreSQL + pgvector | everything | 130/130 |
server_federated.py | four A2M servers | everything but summarize | 106/106 |
python -m tools.conformance --stdio python -m a2m
python -m tools.conformance --stdio node --experimental-strip-types implementations/server_minimal.ts
server_minimal.py imports nothing from the
repository. It exists to answer a question the reference implementation
cannot: is the specification enough on its own? Writing it found a real
bug — the reference was rejecting unrecognised parameters, breaking the
forward-compatibility rule that lets a newer client talk to an older server.
Before you implement
owner is not a security boundary. The
scopes capability partitions data; it does not control access. A
client asserts its own owner and nothing in the protocol stops it
asserting a different one. On a local transport that is fine. Over a network a
server must derive the scope from the authenticated principal
and ignore what the client claimed.
score is ranking information only. Never
comparable between servers, between calls, or against a fixed threshold.
Different scorers occupy entirely different ranges, and a model rating
everything 0.9 may discriminate worse than one spreading across
0..1.
Start here
Read the specification Browse the source
Implementing A2M? Copy server_minimal.py — a whole server in 470 lines, written from the specification alone — then run the conformance suite against it.