A shared memory protocol for AI agents across frameworks.
LangChain, Agno, n8n, CrewAI, AutoGen — each ships its own memory model. Agents from different frameworks cannot share state, history, or knowledge, even when running inside the same workflow.
A2M is a thin, open protocol that lets any agent framework read and write to a shared memory store through one interface — without modifying existing agents.
Specification: spec/a2m-0.1.md · Home: https://a2m-protocol.org
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 queries
LangChain agent Agno agent n8n node CrewAI crew
│ │ │ │
▼ ▼ ▼ ▼
A2M client A2M client HTTP Request A2M client
│ │ │ │
└───────────────────┴───────────────┴────────────────┘
│
A2M — JSON-RPC 2.0, `memory/*`
in-process · stdio · HTTP
│
┌─────────────┴──────────────┐
│ │
Relational store Vector index
(SQLite / PostgreSQL) (sqlite-vec / pgvector / …)
✓ Shared state ✓ Persistent across runs ✓ Semantic search built in
memory = connect_local(MemoryStack()) # in-process
memory = connect_stdio([sys.executable, "-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/.
The bindings are deliberately the ones MCP and A2A already use — JSON-RPC 2.0,
newline-delimited over stdio, a single POST endpoint over HTTP, no batches, and
no server-initiated requests. A2M adds only what a memory server specifically
needs: Origin validation, an A2M-Protocol-Version header, and a profile at
/.well-known/a2m-server.json. See spec §8 and DECISION 021.
A memory store is not a database. Agents do not query memory, they recall from it: they hand over the situation they are in and expect back whatever is worth knowing, ranked. Ranking is the primitive, not filtering.
Most stores are not layered. 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 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 (adds key: addressable, upsert) |
no |
embeddings |
(adds embedding: caller-owned, verbatim) |
no |
external |
(adds uri: points at a file, URL or blob) |
no |
events |
events events/subscribe events/unsubscribe |
no |
A call into an undeclared capability returns -32003 CAPABILITY_NOT_SUPPORTED —
not -32601, which a client cannot distinguish from a typo.
events is a cursor first and a push second. A client brings the position
it has reached and gets back everything that happened after it, ordered,
exactly once — on any transport, with no subscription state on the server. On
the transports that can carry a notification back (stdio, in-process), a
client may additionally subscribe and have the same events pushed as they
happen; over HTTP describe honestly reports "push": false and the cursor
is the interface. Volume is answered by coalescing: a consolidation that moves
ten thousand records is one event carrying counts. Spec §4.12–§4.14,
DECISION 026.
Every record has a tier, and every tier declares a kind that determines its
lifetime and how it is read.
| 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 |
Two different forces move a record, and keeping them apart is the point:
working ──spill──▶ episodic ──spill──▶ semantic
│
└──promote──▶ semantic procedural
(recalled 3×) ▲
└── written deliberately
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.
working is bounded per conversation, not per tier — otherwise two
concurrent chats compete for the same slots and the busier one evicts the
quieter one's context purely by talking more.
Where each tier should actually be stored, and why working must never go in
a vector database, is in spec/implementing-a2m.md.
It is the right first question: memory servers already ship as MCP tools, and
every MCP client can call them today. A2M deliberately shares MCP's plumbing —
JSON-RPC 2.0, the same newline-delimited stdio framing, one POST endpoint, no
batches, no server-initiated requests (DECISION 021) — so the two compose
rather than compete. What A2M refuses to do is put the store behind
tools/call, because a tool result is text for a model to read, and a memory
store's consumers are mostly not models:
- Records stay typed.
metadataround-trips byte-for-byte, ids are opaque, timestamps are RFC 3339, a caller's embedding is stored verbatim — rules a conformance suite can check on a wire format, and can only hope for in a tool description. - Negotiation stays checkable.
describedeclares capabilities and an undeclared one answers-32003. Tool lists are per-server vocabulary — two memory MCP servers already disagree on tool names, which is the silo rebuilt one layer up. - The memory semantics survive. Replay versus search —
timelineversusrecall— is a distinction frameworks build on. Flattened into tools it survives only as a sentence a model may or may not read.
The relationship, made runnable: implementations/bridge_mcp.py is a stdlib-only MCP server whose tools are any A2M server. Point Claude Desktop, Claude Code or Cursor at it and every MCP client becomes an A2M client — with the tool list derived from the store's declared capabilities:
python -m implementations.bridge_mcp --stdio python -m implementations.store_sqlite memory.db
MCP is how a model reaches capabilities; A2M is how agent infrastructure shares memory underneath. See DECISION 027.
| spec/a2m-0.1.md | the normative specification |
| spec/implementing-a2m.md | what each tier means, and where it should live |
| spec/schema/ | JSON Schema for every request, response and record |
| a2m/ | the reference library: protocol.py · memory.py · retrieval.py · text.py · jsonrpc.py |
| implementations/server_minimal.py | independent core-only server, standard library only |
| implementations/server_minimal.ts | the same server in TypeScript, no dependencies, no build |
| implementations/client.py | independent client and CLI, standard library only |
| implementations/store.py | the tier logic both SQL backends share — no SQL in it |
| implementations/store_sqlite.py | that logic on SQLite: one file, one store per tier |
| implementations/store_postgres.py | the same logic on PostgreSQL and pgvector |
| implementations/server_federated.py | one A2M server per tier, one router in front |
| implementations/bridge_mcp.py | any A2M server as an MCP tool server, stdlib only |
| implementations/adapters/ | LangChain, Agno, CrewAI and AutoGen, talking to an A2M server unmodified |
| tools/conformance.py | conformance suite for any A2M server |
| tools/test_a2m.py | python -m tools.test_a2m — no test runner, no network |
| tools/bench_embeddings.py | which embedding model backs recall, measured |
| examples/cross_framework.py | both frameworks sharing one store, as a runnable script |
| examples/n8n_workflow.json | n8n against an A2M server — stock HTTP nodes, no custom node |
| DECISIONS.md | why the non-obvious choices are what they are |
Three directories, and the split is the argument. a2m/ is the library an
implementation is built out of. implementations/ is the specification
implemented more than once — two storage engines, two languages, two topologies —
because one implementation only ever proves the document describes the program
that was already written. tools/ is what judges them, and
tools/conformance.py imports nothing from
implementations/ at all.
The protocol, the reference implementation and the conformance suite need
nothing but the standard library. sqlite-vec and ollama are optional and
used only by the reference store and the benchmark.
The files are not equally good places to start, and the two largest are the worst ones.
- spec/a2m-0.1.md — everything else is downstream of it.
- implementations/server_minimal.py (467 lines) — a whole server, written from the specification alone. If you are implementing A2M, copy this.
- implementations/client.py — the other side, under the same rule. Between them they show both halves of a conversation with nothing shared.
- implementations/store.py, implementations/store_sqlite.py and implementations/server_federated.py — 1100, 800 and 1100 lines. These are reference implementations, not samples: they exist to prove the protocol survives a real store and a real federation, and to be read a section at a time when you hit the problem they solve. Reading any of them front to back to learn A2M is the wrong way round.
tools/conformance.py speaks only the protocol — it never imports the server under test, so an implementation in another language is tested exactly as a Python one is.
python -m tools.conformance --stdio python -m a2m
python -m tools.conformance --stdio python implementations/server_minimal.py
python -m tools.conformance --stdio node --experimental-strip-types implementations/server_minimal.ts
python -m tools.conformance --http http://127.0.0.1:8778/
Checks are grouped by capability and skipped when undeclared. 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.
Six implementations ship, and the same unmodified suite passes against all of them:
| storage | declares | conformance | |
|---|---|---|---|
python -m a2m |
a dict in memory | everything | 94/94 |
| server_minimal.py | a dict, stdlib only | core only |
36/36 |
| server_minimal.ts | a Map, TypeScript | core + keys |
46/46 |
| store_sqlite.py | SQLite + sqlite-vec | everything | 94/94 |
| store_postgres.py | PostgreSQL + pgvector | everything | 94/94 |
| server_federated.py | four A2M servers | everything | 94/94 |
The federation takes --backend sqlite or --backend postgres, so the last row
is really two: four SQLite backends, or four processes sharing one PostgreSQL
database. Both pass 94/94, and the router cannot tell which it is talking to —
it reaches its backends over the protocol and has no way to see inside one.
Over stdio the full-capability rows include push delivery end to end: the
suite subscribes, writes, and reads the memory/event notification off the
same pipe. Over HTTP the suite instead verifies that push is honestly refused
(describe reports "push": false, subscribe answers -32003) and runs the
binding checks stdio cannot reach — Origin, the version header, 405 on
GET, the well-known profile — landing on 94/94 there too.
server_minimal.py imports nothing from this repository. It
exists to answer a question the reference implementation cannot: is the
specification enough on its own? An implementation sharing code with the
reference proves only that the reference agrees with itself. Writing it found a
real bug — a2m/protocol.py was rejecting unrecognised parameters, breaking the
forward-compatibility rule that lets a newer client talk to an older server.
server_minimal.ts answers the next question: is it enough in a
language that is not the reference language? It needs no dependencies and no
build — node --experimental-strip-types runs the file as it is. The port is
where a JSON-shaped protocol earns the description, and two rules did the work:
timestamps stay RFC 3339 strings where JavaScript's instinct is an epoch integer
(§3.3), and an id is echoed back with its type intact where JavaScript would
happily turn 1 into "1" (§3.2).
Which makes the useful demonstration a pair: client.py talking to server_minimal.ts is a Python client and a TypeScript server that share not one line of code, and neither was written against the other.
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 — spec §6.
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 — spec §5.3.
Everything runs from the repository root.
python -m tools.test_a2m # 232 checks, offline
python -m tools.demo_stack # the whole stack, on disk
python -m tools.demo_stack --router # same, federated across processes
python -m a2m # the reference server, in memory
python -m implementations.store_sqlite memory.db # a persistent server on stdio
python -m implementations.server_federated memories/ --http 8778 # federated, over HTTP
python -m implementations.server_federated postgresql://a2m:a2m@127.0.0.1:55432/a2m --backend postgres
server_minimal.py and client.py are the exceptions: they import nothing at
all, so they run as plain files from anywhere, which is the whole claim they are
making.
python implementations/server_minimal.py
python implementations/client.py --stdio python -m implementations.store_sqlite memory.db -- remember "the deploy key rotates every ninety days"
python implementations/client.py --stdio python -m implementations.store_sqlite memory.db -- recall "how often does the key change?"
python implementations/client.py --http http://127.0.0.1:8778/ -- describe
Draft a2m/0.1. Pre-1.0, so compatibility requires an exact minor match and any
minor version may introduce breaking changes.
This repository was reset on 2026-07-28 to carry A2M 0.1. The earlier REST draft
is not gone — it remains in this repository's history at commit
7ca383c, recoverable with git checkout 7ca383c.
| draft | 0.1 | |
|---|---|---|
| wire format | REST over HTTP | JSON-RPC 2.0 — in-process, stdio, HTTP |
| memory kinds | working, episodic, semantic, procedural, external | the first four |
| addressing | hierarchical namespaces, recursive reads | key_prefix, plus owner and session |
| identity | caller-set key, upsert by key |
both — opaque id and addressable key |
| embeddings | caller-owned, stored verbatim | both — caller-owned, or server-side |
| record kinds | external as a fifth type |
external as a record property, legal in any tier |
| events | WS /subscribe |
cursor polling on every transport, push where one can carry it |
| conformance | — | executable suite, six passing implementations |
The four memory kinds survived unchanged, having been arrived at twice independently — which is the strongest evidence in this repository that they are the right four.
The draft's ideas are now in 0.1, each improving on what it replaced:
-
Addressable keys (
keys).ididentifies a write;keyaddresses a fact. Writing to an occupied key replaces what is there, keeping the id and advancingrevision. That is what makes a memory correctable rather than merely appendable — a superseded fact that is only outnumbered by its successor is still there to be recalled, with the same confidence as the truth. Slash-delimited keys pluskey_prefixgive the hierarchy and the recursive scope reads the draft wanted from namespaces, without a second addressing dimension to keep consistent with the first. -
Caller-owned embeddings (
embeddings). A vector supplied by the caller is stored verbatim — never regenerated, never replaced. Two frameworks embedding with different models can share a store only if neither has its vectors silently rewritten into the other's space. A store with no model at all still answers vector searches. Mismatched widths are refused with-32008rather than compared: cosine between vectors of different lengths is not a worse answer, it is not an answer. -
External records (
external). A record may point at a file, URL or blob instead of containing it.contentkeeps its ordinary meaning — the text that gets indexed, so the reference is findable — whileurisays where the real thing lives. The server never dereferences it: fetching a caller's URI would make every write a request the server chose to issue to an address its caller supplied. -
Events (
events). The draft wantedWS /subscribe; 0.1 ships something a plain request/response channel can carry: an opaque cursor that replays what happened, exactly once and in order, on any transport — with push as an optional layer on the transports that can deliver a notification. What kept it out of earlier drafts wasownerscoping, and the answer is recall's: an event is delivered only to a caller who could have recalled the record it describes — see DECISIONS.md 026.
Hierarchical namespaces were folded into keys rather than added as a separate dimension; see DECISIONS.md 017. All of the draft's ideas are now in 0.1.
MIT. See LICENSE and spec §10.
A protocol that is expensive to implement does not get implemented.