A2M

#Agent-to-Memory Protocol (A2M), version 0.1

Status: Draft Protocol identifier: a2m/0.1 Canonical home: https://a2m-protocol.org Base protocol: JSON-RPC 2.0

A2M gives an agent a memory it does not own. It puts a memory store behind the same kind of boundary MCP puts a tool behind, so that 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.

The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY and OPTIONAL in this document are to be interpreted as described in RFC 2119.


#1. Design intent

Two observations shape this protocol.

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 that demands 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 interesting behaviour into capabilities a server declares and a client checks.


#2. Conformance

A server is A2M 0.1 conformant if it implements every method in the core capability, declares its capabilities truthfully from memory/describe, and returns the error codes of §7 in the circumstances described.

CapabilityMethods it addsRecord fields it adds
core (REQUIRED)memory/describe, memory/remember, memory/recall, memory/timeline, memory/forgetid, content, created_at, role, metadata, group
tiersmemory/promote, memory/consolidatetier
saliencememory/reinforcesalience, access_count, accessed_at
scopesowner
sessionsmemory/session/list, memory/session/closesession
keysmemory/fetchkey, revision
embeddingsembedding
externaluri, media_type
eventsmemory/events, memory/events/subscribe, memory/events/unsubscribe
summarizememory/summarize
prompt(adds a prompt parameter and result field to memory/recall and memory/timeline)

A server MUST NOT advertise a capability it does not fully implement.

A server MUST respond to a call belonging to an undeclared capability with CAPABILITY_NOT_SUPPORTED (§7). It MUST NOT respond with METHOD_NOT_FOUND, because a client cannot distinguish that from a malformed request.

A client MUST call memory/describe before any other method, and MUST NOT call a method belonging to a capability the server did not declare.

A client MUST tolerate record fields it does not recognise. A server MUST NOT reject a request carrying parameters it does not recognise; it MUST ignore them. This is what allows 0.2 clients to talk to 0.1 servers.

#2.1 Read-only servers

A server MAY refuse to accept writes. Such a server MUST answer both memory/remember and memory/forget with READ_ONLY (§7), and MUST implement memory/describe, memory/recall and memory/timeline normally. It is conformant: refusing a write with the error the specification allocates for exactly that purpose is implementing the method.

A client MUST tolerate READ_ONLY from either method. A client that treats it as a transport failure, or retries, is broken against a legitimate server.

A server that refuses writes MUST refuse them consistently — it MUST NOT accept some writes and refuse others with READ_ONLY — because a client has no way to discover which is which. A store that rejects an individual write for a reason other than being read-only has other codes for it (§7).

This is what a pre-existing corpus looks like as an A2M server: a retrieval system whose contents are loaded by its own ingestion pipeline, exposed for recall and nothing more. It is the smallest useful A2M server, and it is deliberately reachable without adopting any of the write-side model.


#3. Data model

#3.1 Record

A record is one remembered thing.

FieldTypeRequiredCapabilityNotes
idstringyescoreOpaque. See §3.2.
contentstringyescoreThe text that is remembered and searched.
created_attimestampyescoreSee §3.3.
rolestringnocoreWho produced it: user, assistant, tool, system, memory, or any other value. Advisory.
metadataobjectnocoreArbitrary JSON. Servers MUST round-trip it unchanged.
groupstringnocoreSee §3.4.
tierstringnotiersWhich layer the record currently lives in.
saliencenumbernosalienceHow much the record is worth keeping. Higher is more.
accessed_attimestampnosalienceWhen it was last recalled.
access_countintegernosalienceHow often it has been recalled.
ownerstring \nullnoscopesWhich agent wrote it. See §6.
sessionstring \nullnosessionsWhich conversation it belongs to. See §3.5.
keystring \nullnokeysA caller-chosen address. See §3.6.
revisionintegernokeysHow many times that key has been rewritten.
embeddingnumber[]noembeddingsA caller-supplied vector. See §3.7.
uristring \nullnoexternalWhere the real thing lives. See §3.8.
media_typestring \nullnoexternalThe referent's media type.
scorenumberrecall onlycoreSee §5.3.

Servers MUST round-trip metadata byte-for-byte in structure. A server that cannot store arbitrary JSON metadata MUST NOT silently discard it; it MUST fail the write with INVALID_PARAMS.

#3.2 Identifiers

id is an opaque string. Clients MUST NOT parse it, derive meaning from it, or assume any format, length or ordering.

A client MAY supply id when writing. If it does, the server MUST treat the write as idempotent: if a record with that id already exists, the server MUST NOT create a second one and MUST return the existing id. This is the only retry-safety mechanism in A2M, and it exists because a network failure between memory/remember and its response is otherwise indistinguishable from a failure before it.

If the client does not supply id, the server MUST assign one that is unique within that store.

#3.3 Timestamps

All timestamps are strings in RFC 3339 format, in UTC, with a Z suffix and millisecond precision:

"2026-07-28T10:15:30.123Z"

Numeric epoch timestamps are not permitted. They are ambiguous about timezone and unit, and lose precision in languages whose only number is a double.

#3.4 Groups

group ties records that MUST be kept or discarded together. Its motivating case is a tool call: an assistant message carrying tool_calls and the tool results answering it form one indivisible unit, and a store that evicts one without the other produces a transcript that most model providers reject.

The second case is a chunked document. A2M has no chunker and defines none: by the time memory/remember is called the boundaries are already decided, and the caller knows what it split where the server does not. What the protocol provides is the part that outlives the split — chunks of one document SHOULD share a group, so a store that evicts under pressure moves them whole rather than leaving a document half-present, answering questions from an introduction whose body is gone.

A server implementing tiers MUST NOT move some records of a group to a different tier while leaving others behind. A server MAY ignore group entirely if it never evicts anything.


#3.5 Sessions

group ties one turn together and owner says which agent a record belongs to. session is the level between them: one conversation.

Without it a store has no way to tell two concurrent conversations apart, and they compete for the same working-tier capacity — a busy chat evicts a quiet one's context purely by talking more. A server declaring sessions:

A record MAY have no session. Records with no session form one implicit bucket and MUST NOT be treated as belonging to any named one.

#3.6 Keys

id identifies a write; key addresses a fact. The difference decides whether a memory can be corrected or only appended to.

A key is a caller-chosen string, slash-delimited by convention (myapp/wf-42/user/city). A server declaring keys:

Prefix matching is what makes keys hierarchical, and is why A2M has no separate namespace field: myapp/wf-42/ already selects everything beneath it, and a second addressing dimension would have to be kept consistent with the first for no additional expressive power.

Replacement is the point. A superseded fact that is merely outnumbered by its successor is still there to be recalled — and will be, with exactly the same confidence as the truth.

#3.7 Embeddings

A server declaring embeddings accepts a vector on write and a vector as a query, and MUST store what it is given verbatim. It MUST NOT generate a vector for a record that already carries one, and MUST NOT replace one it was given.

This is what makes A2M model-agnostic. Two frameworks embedding with different models can share one store only if neither has its vectors silently rewritten into the other's space — and vectors from different models are not merely less accurate when compared, they are meaningless.

metric names how two vectors are comparedcosine, dot or l2 — and it is reported rather than chosen. The metric belongs to the store's index, not to a query: an index built for one cannot answer another without being built again, so a per-call metric would oblige every server declaring embeddings to maintain several indexes. Declaring one is what a small capability can afford.

A server SHOULD also report metrics, every comparison its storage could be configured for, with metric naming the one actually in use. The two answer different questions and a caller needs both: metric says whether the vectors it holds can be compared here today, and metrics says whether this store is worth configuring differently, or worth pointing a second deployment at. A store reporting {"metric": "cosine", "metrics": ["cosine", "dot", "l2"]} can serve inner-product vectors; one reporting {"metric": "cosine", "metrics": ["cosine"]} never will, and a caller holding such vectors should look elsewhere rather than send them and be silently misranked.

Where a deployment spans several stores whose indexes differ — a federation, or a stack with a different backend per tier — metrics SHOULD report only what all of them can do, since a record may move between tiers and must remain comparable after it lands.

It is not the score a caller gets back. Scores are blended with whatever else the server ranks by (§5.3); metric describes the comparison underneath.

A caller supplying its own vectors SHOULD read metric before doing so, and this is the one mismatch the protocol cannot catch for it. A vector of the wrong width is refused with EMBEDDING_MISMATCH, because width is visible. A vector from a model trained against a different metric is not detectable at all — the server has no way to know what space it is in — so it will be compared, ranked and returned with complete confidence and no warning. Where dimensions are enforced, metric is only disclosed.

A server MAY additionally generate embeddings for records that arrive without one. That is an implementation choice and does not weaken the rule above.

#3.8 External records

A record MAY point at something rather than contain it: a file, a URL, a blob in object storage. uri holds an RFC 3986 reference and media_type optionally names what is at the other end.

content keeps its ordinary meaning: the text that is indexed. For an external record that is a title, a summary, or an extracted passage — whatever should make the reference findable. A record carrying a uri and no content is legal, and will be recallable only by its key or by metadata, because there is nothing for a scorer to rank.

#3.8.1 Documents that are not text

That distinction — content is the indexed representation, uri is the thing — is what lets A2M carry an image, an audio file, a video or a 3D model without a single field for any of them. The record is the same shape; only what goes in content changes:

The referentmedia_typeWhat belongs in content
imageimage/pngcaption, alt text, or extracted text
audioaudio/mpegtranscript, or a summary of one
videovideo/mp4transcript, plus captions of what is on screen
3D modelmodel/gltf+jsondescription, part names, the metadata a search would use
PDFapplication/pdfextracted text, per page or per section

Producing that representation is the caller's work and A2M does not specify it: an OCR pass, a speech-to-text model, a captioner, or a human writing alt text are all the same to the protocol. What matters is that something ranked lives in content, because a scorer has nothing else to work with.

Two consequences worth stating, because both are easy to assume the other way:

A server declaring external:

That second rule is the important one. A server that fetches a caller's URI is making requests of its own choosing to addresses its caller supplied, which is a server-side request forgery primitive in a component whose whole job is to accept arbitrary strings from agents. Resolution belongs to the client, which already has the credentials, the network position and the reason.

external is deliberately not a fifth tier kind. The four kinds describe lifetime and access pattern; "points at a file" describes content. A referenced document is a fact and belongs in semantic; a referenced runbook is a procedure and belongs in procedural. Making it a kind would force a choice that is not the caller's to make.

#4. Methods

Every method name is namespaced memory/. This namespace is chosen so that one endpoint MAY serve A2M alongside MCP, whose methods live under tools/, resources/ and prompts/.

All parameters are passed by name (a JSON object). Servers MUST accept by-name parameters and MAY additionally accept by-position.

#4.1 memory/describe — core

Discovery and version negotiation. A client MUST call this first.

Params

NameTypeRequiredNotes
protocolstringnoThe protocol version the client speaks, e.g. "a2m/0.1".
ownerstringnoScope the counts. Requires scopes.

Result

FieldTypeRequiredNotes
protocolstringyesThe version this server speaks.
namestringyesHuman-readable server name.
capabilitiesstring[]yesMUST include "core".
methodsstring[]yesEvery method this server accepts.
limitsobjectnoSee below.
tiersobject[]if tiersSee §4.7.
eventsobjectif eventsCarries push (boolean). See §4.13.
summarizeobjectif summarizeCarries model (string or null). See §4.15.
promptobjectif promptCarries styles and methods (string[]), and optionally model. See §4.16.

limits MAY carry max_records_per_call, max_recall_limit and max_content_length, all integers. A client SHOULD respect them; a server MUST enforce them regardless, with INVALID_PARAMS or QUOTA_EXCEEDED.

If the client declares a protocol the server cannot serve, the server MAY respond with PROTOCOL_NOT_SUPPORTED. It MUST NOT pretend to speak a version it does not.

Version compatibility: while the major version is 0, two versions are compatible only if the minor version matches exactly. From 1.0 onward, a client and server are compatible if the major version matches.

#4.2 memory/remember — core

Write records.

Params

NameTypeRequiredNotes
recordsobject[]yesOne or more partial records. Each MUST carry content.
ownerstringnoDefault owner for records that do not set one. Requires scopes.

Each entry MAY carry id, role, metadata, group, tier, salience and owner. A server MUST ignore fields belonging to capabilities it does not declare rather than failing — except tier, which MUST fail with CAPABILITY_NOT_SUPPORTED when tiers is not declared, because silently dropping a caller's placement is a correctness bug rather than a cosmetic one.

Result

FieldTypeRequired
idsstring[]yes — one per input record, in the same order

Writes SHOULD be atomic across the batch. A server that cannot guarantee that MUST say so in its documentation; it MUST NOT return a partial ids array.

#4.3 memory/recall — core

Search by relevance. This is the method that matters.

Params

NameTypeRequiredNotes
querystringnoWhat to rank against.
tierstringnoRestrict to one tier. Requires tiers.
limitintegernoMaximum records to return. Server default applies when absent.
whereobjectnoMetadata filter. See §5.2.
min_scorenumbernoDrop results scoring below this.
ownerstringnoRequires scopes.
embeddingnumber[]noA query vector. Requires embeddings.
key_prefixstringnoRestrict to keys at or under this. Requires keys.
embeddingsbooleannoInclude stored vectors in the result.

Result

FieldTypeRequired
recordsobject[]yes — ordered by descending score

If query is absent or empty, the server MUST return records ranked by whatever ordering it considers most useful in the absence of a query (recency is RECOMMENDED) rather than an error.

limit of 0 MUST be interpreted as "no limit" only when the server declares no max_recall_limit. Clients SHOULD NOT rely on unbounded responses over a network transport.

#4.4 memory/timeline — core

Read records in creation order, oldest first.

Params: tier (requires tiers), limit, owner (requires scopes), where (same semantics as §5.2), key_prefix (requires keys), embeddings.

where is what makes replaying one conversation possible. A server declaring sessions MUST support it; any other server SHOULD.

Result: records, an array MUST be sorted by ascending created_at. When limit is given, the server MUST return the most recent limit records, still in ascending order.

timeline exists separately from recall because they answer different questions and reversing them silently corrupts data. Relevance order is what a search wants; creation order is what rebuilding a conversation requires. A server MUST NOT implement one in terms of the other.

#4.5 memory/forget — core

Delete records.

Params: ids (string[]), query (string), tier (string), where (object), owner (string), key_prefix (string, requires keys).

A server MUST reject a call in which ids, query, tier, where and key_prefix are all absent, with INVALID_PARAMS. Deleting an entire store MUST require something more deliberate than an empty request.

Result: {"forgotten": <integer>} — how many records were removed.

#4.6 memory/promotetiers

Move records to another tier because they earned it, rather than because something overflowed.

Params: ids (string[], REQUIRED), tier (string, REQUIRED), salience (number, OPTIONAL — how much to add on arrival).

Result: {"promoted": <integer>}.

Unknown tier MUST fail with UNKNOWN_TIER. Ids that do not exist, or are already in the target tier, MUST NOT fail; they are simply not counted.

#4.7 memory/consolidatetiers

Ask the store to reorganise itself: move what has earned durability, evict what no longer fits.

Params: none.

Result

FieldTypeNotes
movedintegerrecords that changed tier under capacity pressure
promotedintegerrecords that changed tier because they earned it
droppedintegerrecords deleted because there was nowhere below
summarizedintegergroups replaced by rewritten records
countsobjecttier name → record count, after the operation

Consolidation MAY delete records. A client MUST NOT assume a record it wrote is still present after calling it.

memory/describe reports the tier layout under tiers, each entry carrying name (string, REQUIRED), count (integer, REQUIRED) and optionally kind, capacity, spill_to, promote_to, shared, per_session.

kind, when present, SHOULD be one of working, episodic, semantic or procedural. It tells a client what a tier is for independently of what it is named, so a client can find the tier holding the live transcript without hardcoding a name. A server MAY name its tiers anything.

What those four mean, how records flow between them, and what each is worth storing in — a list in process, pgvector, a plain fact table, files in git — is covered in implementing-a2m.md. That document is non-normative: this specification does not mandate any storage technology.

#4.8 memory/reinforcesalience

Raise the salience of records that proved useful.

Params: ids (string[], REQUIRED), amount (number, OPTIONAL).

Result: {"reinforced": <integer>}.

#4.9 memory/fetchkeys

Read the record at an address.

Params: key (string, REQUIRED), owner (OPTIONAL), embeddings (boolean, OPTIONAL).

Result: {"record": ...}, or {"record": null} when the key is unused. An unused key is not an error — asking whether a fact is known yet is ordinary.

A missing or empty key MUST fail with INVALID_PARAMS.

#4.10 memory/session/listsessions

Params: owner (OPTIONAL).

Result: {"sessions": [...]}, each entry carrying session (string, REQUIRED), records (integer, REQUIRED), and optionally tiers (object mapping tier name to count), opened_at and touched_at (timestamps).

#4.11 memory/session/closesessions

End a conversation. Params: session (string, REQUIRED), owner (OPTIONAL). A missing or empty session MUST fail with INVALID_PARAMS.

Result: the memory/consolidate result (§4.7) plus closed (the session) and flushed (how many records left the working tier).

Closing is not one hop down the stack. A finished conversation will never be replayed, so its records SHOULD leave the working tier immediately rather than wait for capacity pressure — and every tier below then applies its own ordinary rules to what arrives: what earned promotion is promoted, a tier over capacity spills, a consolidator rewrites what it is given. The effect is that closing a session percolates it through the whole stack in one operation.

Closing MUST NOT destroy the conversation. Records move; they are not deleted, unless a tier's own policy would have dropped them anyway.

Procedural memory is untouched, because nothing spills into procedural (§4.7). A finished conversation does not become a procedure.

Closing a session that does not exist MUST NOT fail; it is a no-op.

#4.12 memory/eventsevents

Read what has changed since a cursor. This is how a client watches a store without the store initiating anything: the client brings the position it has reached, the server returns everything that happened after it, in order, with the position to bring next time.

Params

NameTypeRequiredNotes
cursorstringnoWhere to read from. Opaque: clients MUST NOT parse it, and it is valid only on the server that issued it. Absent means now: the reply carries no events, only the current cursor.
limitintegernoMaximum events to return.
kindsstring[]noRestrict to these event kinds.
ownerstringnoRequires scopes.

Result

FieldTypeRequiredNotes
eventsobject[]yesOldest first.
cursorstringyesThe position after the last returned event — or, when events is empty, the current head.
morebooleannoRetained events remain beyond limit.
resetbooleannotrue when the supplied cursor could not be honoured. Events may have been missed.

An event

FieldTypeRequiredNotes
kindstringyesOne of the kinds below, or a server-defined value a client MUST tolerate.
attimestampyesWhen it happened (§3.3).
KindAdditional fieldsEmitted when
writtenid (REQUIRED); tier, session, key, revision when the relevant capabilities applyOne per record written by memory/remember. A revision greater than 0 is how a key replacement (§3.6) is visible.
forgottencount (REQUIRED); ids OPTIONALOne per memory/forget call, however many records it removed.
promotedids, tier (both REQUIRED)One per memory/promote call; tier is the destination.
consolidatedthe counts of §4.7: moved, promoted, dropped, summarizedOne per memory/consolidate or memory/session/close, and one per server-internal reorganisation (a spill under capacity pressure, a background pass).
session_closedsession (REQUIRED)One per memory/session/close.

Volume is answered by coalescing. A consolidation that moves ten thousand records is one consolidated event carrying counts, never ten thousand notifications. A forget is one event. Only written is per-record, because a write is what a watching client most often needs to act on record by record.

Ordering and completeness. Successive polls, each carrying the cursor the previous reply returned, MUST see every retained event exactly once, in order, with no duplicates and no gaps. A server MAY bound how many events it retains; when a supplied cursor lies before the retained window the server MUST set reset: true and continue from the oldest event it still holds. A client seeing reset knows it may have missed events and SHOULD re-read whatever state it was tracking. A cursor the server never issued MAY be refused with INVALID_PARAMS.

Scoping is recall's. A server declaring scopes MUST deliver an event only to a caller who could have recalled the record it describes, under exactly the rules of §6 — over a network transport the scope comes from the authenticated principal, never from the client. An event channel that broadcasts turns owner from partitioning into a leak, which is the question that kept events out of earlier drafts of this document.

#4.13 memory/events/subscribeevents

Push: the same events, delivered as they happen instead of when asked for.

Push is optional within the capability, because not every transport can carry it (§8). A server declaring events MUST report an events object from memory/describe carrying push (boolean). Polling (§4.12) is always available when events is declared. Push additionally requires that the server enabled it and that the connection's transport can deliver a notification; a server MUST NOT report "push": true on a connection whose transport cannot, and MUST fail memory/events/subscribe there with CAPABILITY_NOT_SUPPORTED.

Params: kinds (string[], OPTIONAL), owner (OPTIONAL, requires scopes).

Result: {"subscribed": true}.

After subscribing, the server sends each event as a JSON-RPC notification on the same connection:

{"jsonrpc":"2.0","method":"memory/event",
 "params":{"kind":"written","id":"01J8Z9","tier":"episodic",
           "at":"2026-07-28T10:15:30.123Z"}}

One event per notification; params is the event object of §4.12, under the same coalescing, ordering and scoping rules. A notification is not a request (§8): it carries no id and expects no reply.

Subscription is per connection and opt-in. Declaring the capability MUST NOT by itself cause notifications; a subscription ends with memory/events/unsubscribe or when the connection closes, and is never persisted. Events delivered by push remain readable by poll — the two views are one log.

#4.14 memory/events/unsubscribeevents

Params: none. Result: {"subscribed": false}. Unsubscribing when not subscribed MUST NOT fail; it is a no-op.

#4.15 memory/summarizesummarize

Rewrite a set of records into durable statements, and store the result.

A store already does this when records spill (§4.7): consolidation may replace a group with rewritten content. This method exposes the same operation on demand, for the cases where waiting for capacity pressure is the wrong trigger — closing out a topic, distilling a search result into a fact, or maintaining a page that several conversations contributed to.

Params

At least one selector is REQUIRED. A call carrying none MUST fail with INVALID_PARAMS: summarizing an entire store is expensive, and it MUST NOT be the result of an empty request.

NameTypeRequiredNotes
idsstring[]noSelector. Summarize exactly these.
querystringnoSelector. Summarize what this recalls.
tierstringnoSelector. Restrict the selection to one tier. Requires tiers.
whereobjectnoSelector. Metadata filter (§5.2).
key_prefixstringnoSelector. Requires keys.
limitintegernoAt most this many source records. A server MAY cap it.
ownerstringnoRequires scopes.
intostringnoDestination tier for the summary. Requires tiers.
keystringnoAddress for the summary. Requires keys.

tier selects what is read; into says where the result is written. They are separate because summarizing episodic memory into a semantic fact is the motivating case, and one field could not say both.

Result

FieldTypeRequiredNotes
recordsobject[]yesThe summary records written, in wire form. Empty when the server declined.
readintegeryesHow many source records were considered.
writtenintegeryesHow many summary records were written.

Rules.

How the summary is produced is not specified. A language model, an extractive ranker, or a template are all conformant. This is the same position §5.1 takes on ranking, and for the same reason: it is where implementations should compete, and mandating a mechanism would make the capability implementable only by servers that had chosen the same one.

#4.16 prompt — rendered text, alongside the records

Recalled records usually end up in a model's prompt, and turning them into text is a step every caller writes. A server declaring prompt will do it on request.

This adds no method. It adds one optional parameter to memory/recall and memory/timeline, and one optional field to their results.

Paramprompt, either true for the server's defaults, or an object:

NameTypeNotes
methodstringHow the text is produced. template, model, or none. Default template.
stylestringWhat shape it takes. facts, transcript, or auto to decide from the records. Servers MUST support auto and SHOULD support the other two. Advisory when method is model.
modelstringWhich model to use, when method is model and the server offers a choice.
budgetintegerMaximum characters. The server MUST drop whole records to fit, never truncate one.
citebooleanMark each entry with its source — its uri, or its key.

method and style are separate axes because they answer different questions: one is what it costs, the other is what it looks like. A model can emit a bulleted list and a template can emit a transcript, so a single field could not express "a transcript, without paying for inference".

methodMeans
templateDeterministic assembly from the records — no model, no network, same input same output. Every server declaring prompt MUST support this, and it is the default.
modelA language model writes the text. Optional. A server MUST NOT offer it unless it can actually do it.
noneRender nothing; the result carries no prompt field. Identical to omitting the parameter, and provided for callers whose request body is templated and cannot easily drop a field — an n8n node, a shell script.

The default MUST be template. A client that asks for prompt: true MUST NOT trigger an inference call it did not request. Cost and latency are the caller's to opt into, and a protocol that lets a server quietly spend the caller's money on a convenience field is not one anyone should deploy.

A server asked for a method or model it does not offer MUST fail with INVALID_PARAMS. It MUST NOT silently substitute another: a caller who asked for a written summary and received a bullet list, with no indication, has been given materially different text than it requested. memory/describe says in advance what is available, so this error is always avoidable.

Result — when and only when the caller asked, the result carries:

FieldTypeNotes
promptstringThe rendered block. Empty when nothing survived the budget.
prompt_idsstring[]The ids of the records the block actually contains.

Rules.

How the text is rendered is still not specified, beyond the style names and the two cost classes. method says whether a model is involved, not which algorithm assembles the words — exactly as describe reports what a server's scorer is while §5.1 declines to say how it should rank. A client that needs an exact shape should take the records and render them itself.

Not to be confused with memory/summarize (§4.15), which may also use a model. The difference is what survives: summarize writes a record, and the distillation becomes part of the store. Rendering a prompt changes nothing — the text is returned and forgotten. Ask for a summary when the store should be smarter afterwards; ask for a prompt when this one call needs text.

memory/describe MUST report a prompt object carrying styles and methods, and SHOULD report model naming what method: model would use. A client can then tell, before asking, both what shape it can get and whether asking will cost an inference call.

Why this is in the protocol and not only in a library. A helper can only serve callers who share its language. The client in an n8n HTTP node, a shell script, or a language nobody here has written in cannot call a Python function — and A2M's premise is that all of them reach one store.


#5. Ranking

#5.1 What a server may do

A2M does not specify how records are ranked. Lexical scoring, embeddings, hybrids and learned rankers are all conformant. This is deliberate: ranking is where implementations should compete.

#5.2 where

where is a conjunction of equality tests against record fields, falling back to metadata keys. A value that is an array means "any of". A server MAY support a richer filter language, but MUST support at least this.

{"where": {"role": "user", "tier": ["episodic", "semantic"]}}

#5.3 score

score is ranking information only.

A client MUST NOT compare scores between two servers, between two calls, or against any fixed threshold it did not obtain from the same server in the same call. Scores are not probabilities, not distances, and not calibrated. Different scorers occupy entirely different ranges, and a model that rates everything 0.9 may discriminate better than one that spreads across 0..1.

Servers SHOULD emit scores in the range [0, 1] and MUST emit them in descending order within a recall result.

min_score is therefore a server-relative knob. Clients SHOULD leave it alone unless they have calibrated against that specific server.


#6. Identity and scoping

owner is not a security boundary.

The scopes capability lets one store serve several agents: records carry an owner, and a server filters what each agent can see. This is data partitioning, and on a local transport, where the client and server are the same trust domain, it is sufficient.

It is not access control. A client asserts its own owner value, and nothing in the protocol prevents it from asserting a different one.

Therefore:

Defining the authentication mechanism itself is out of scope for 0.1. What is in scope is that no implementer mistakes owner for protection.

#6.1 Authenticating over HTTP — guidance

This subsection is non-normative. It exists because the rule above — scope comes from the transport — is only implementable if the transport authenticates, and an implementer should not have to invent the shape.


#7. Errors

A2M uses JSON-RPC 2.0 error objects. Beyond the standard codes, it allocates from the -32000..-32099 block that JSON-RPC reserves for implementation-defined server errors.

CodeNameWhen
-32700PARSE_ERRORMalformed JSON.
-32600INVALID_REQUESTNot a valid JSON-RPC request object.
-32601METHOD_NOT_FOUNDMethod is not an A2M method at all.
-32602INVALID_PARAMSMissing or malformed parameters.
-32603INTERNAL_ERRORServer fault.
-32001UNKNOWN_RECORDA referenced id does not exist and the operation cannot ignore it.
-32002UNKNOWN_TIERA referenced tier does not exist.
-32003CAPABILITY_NOT_SUPPORTEDA method or field of an undeclared capability was used.
-32004READ_ONLYThe store does not accept writes.
-32005SCOPE_DENIEDThe caller may not act on that owner's records.
-32006QUOTA_EXCEEDEDA documented limit was exceeded.
-32007PROTOCOL_NOT_SUPPORTEDThe client declared an incompatible version.
-32008EMBEDDING_MISMATCHA vector's width disagrees with the store's.

The message field is for humans and MUST NOT be parsed by clients. The data field MAY carry structured detail.


#8. Transports

A2M is transport-agnostic. Any transport that carries JSON-RPC 2.0 request and response objects intact is conformant. Three bindings are defined.

Two rules hold on every binding.

Messages are single. A message is one JSON-RPC request, response or notification. A2M does not use JSON-RPC batches: a batch has no id of its own for a response to bind to, and no A2M method needs one — memory/remember already takes many records in one call, which is where batching actually pays. A server MUST refuse an array with -32600 INVALID_REQUEST.

Servers do not initiate requests. A server sends responses and, if it has a capability that defines them, notifications. It MUST NOT send a JSON-RPC request to a client. A client therefore never sends a response, and a transport only has to carry one direction of request.

Both rules match MCP, which removed batching in its 2025-06-18 revision and forbids server-initiated requests outright. A2M adopts them for the same reason: they are what make a binding implementable over a plain request/response channel.

#8.1 In-process

Client and server in one process, exchanging JSON-RPC objects directly.

Implementations SHOULD serialise to JSON and back even in-process. A local server that accepts Python objects a remote one could never receive is a local server that will disagree with the network in production.

A binding with no byte stream can still deliver notifications (§4.13): an in-process server SHOULD accept a caller-supplied callback and invoke it with each notification object — serialised to JSON and back like everything else, for the same reason as above.

#8.2 stdio

The server reads requests from stdin and writes responses to stdout, one JSON value per line, UTF-8 encoded, \n-terminated.

This framing is deliberately identical to MCP's stdio transport, down to the stdout/stderr split. An implementation that already speaks one can carry the other over the same pipe machinery, and a custom byte-stream transport — a Unix socket, a TCP connection — SHOULD reuse this framing rather than invent one.

#8.3 HTTP

A single endpoint accepting POST with Content-Type: application/json.

#8.3.1 Origin

A server MUST validate the Origin header. A request carrying an Origin the server does not permit MUST be refused with 403 Forbidden. A request carrying no Origin did not come from a browser and is unaffected.

A server intended for local use SHOULD bind loopback rather than every interface, and SHOULD permit no origin by default.

Without this, any page the user is browsing can drive a memory server listening on their own machine, and read back everything the agent has remembered. The protection is required precisely because the interesting deployment is local and unauthenticated — the case where §6's authentication rules do not apply.

#8.3.2 Protocol version header

A client SHOULD send A2M-Protocol-Version on every POST, carrying the same version it would pass to memory/describe:

A2M-Protocol-Version: a2m/0.1

A server receiving a version it does not speak MUST refuse the request with 400 Bad Request carrying a -32007 PROTOCOL_NOT_SUPPORTED error object. A server MUST NOT treat an absent header as an error: the header exists so that a gateway can route and reject without parsing a body, and memory/describe (§4.1) remains the negotiation that decides anything.

#8.3.3 Well-known profile

A server SHOULD serve its memory/describe result as a JSON document at:

GET /.well-known/a2m-server.json

This makes a server discoverable before it is called — a directory, a gateway or an operator can learn its protocol version, capabilities and tiers without holding an A2M client. The document is advisory and MAY be stale; a client that needs the truth calls memory/describe.


#9. Versioning

This document specifies a2m/0.1.

While the major version is 0, any minor version MAY introduce breaking changes, and compatibility requires an exact minor match. From 1.0, additions are minor version bumps and breaking changes are major version bumps.

New functionality SHOULD arrive as a new capability rather than as a change to an existing method, since a capability is invisible to clients that do not ask for it.

#9.1 Reserved for a future version

No capability names are currently reserved. events, reserved here while this document was a draft, is now specified (§4.12–§4.14): its polling form is carried by every binding in §8, which answered the objection that had deferred it.


#10. Licensing

This specification and the reference implementation accompanying it are released under the MIT License.

This is deliberate. A protocol that is expensive to implement does not get implemented. MIT imposes no obligation on an implementation, commercial or otherwise, hosted or embedded — which is the same choice MCP made, and for the same reason.

An independent implementation written from this document alone is in any case not a derivative work of the reference code, and this specification imposes no obligation on it. implementations/server_minimal.py exists partly to demonstrate that implementing from the document is achievable.


#Appendix A — A minimal conformant exchange

// →
{"jsonrpc":"2.0","id":1,"method":"memory/describe","params":{"protocol":"a2m/0.1"}}
// ←
{"jsonrpc":"2.0","id":1,"result":{
  "protocol":"a2m/0.1","name":"example","capabilities":["core"],
  "methods":["memory/describe","memory/remember","memory/recall",
             "memory/timeline","memory/forget"]}}

// →
{"jsonrpc":"2.0","id":2,"method":"memory/remember","params":{
  "records":[{"content":"the deploy key rotates every 90 days","role":"user"}]}}
// ←
{"jsonrpc":"2.0","id":2,"result":{"ids":["01J8Z9"]}}

// →
{"jsonrpc":"2.0","id":3,"method":"memory/recall","params":{
  "query":"how often does the deploy key change?","limit":1}}
// ←
{"jsonrpc":"2.0","id":3,"result":{"records":[{
  "id":"01J8Z9","content":"the deploy key rotates every 90 days",
  "role":"user","created_at":"2026-07-28T10:15:30.123Z","score":0.72}]}}

// → a capability this server did not declare
{"jsonrpc":"2.0","id":4,"method":"memory/consolidate"}
// ←
{"jsonrpc":"2.0","id":4,"error":{
  "code":-32003,"message":"Server does not implement the 'tiers' capability"}}

#Appendix B — Implementation checklist