LLMs forget. Stuffing the last N messages into every request works for ten turns. It dies on tool dumps, new threads, and any goal that sat in message one.
This is Mastra’s four-level ladder, in the order they shipped it. Alex Becker walks through the same stack in Four levels of agent memory. It is not the CoALA taxonomy (working / episodic / semantic / procedural). Those names describe kinds of knowledge. These four levels describe where the tokens live and what you pay to keep them.
Storage is required for all of them. new Memory() with no options is already level 1. The rest are flags on the same object.
1. Conversation history
A thread is one conversation. A resource is the owner: a user, an org, a project. Studio invents both. When you call generate or stream yourself, you pass them.
Mastra loads the last N messages from storage and puts them in the context window. Default lastMessages is 10.
import { Agent } from "@mastra/core/agent"
import { Memory } from "@mastra/memory"
export const agent = new Agent({
id: "chat-agent",
name: "Chat agent",
instructions: "You are a helpful assistant.",
model: "openai/gpt-5-mini",
memory: new Memory({
options: {
lastMessages: 10,
},
}),
})- From the client, send only the new message. Mastra already has the thread. Shipping the full history is redundant, and client timestamps will reorder messages against the store.
- The window is a sliding cut. If the user’s goal was the first message and you then burned 30 tool turns, the goal is gone.
- A new thread has no history. Same agent, same human, empty context. That is a session cookie, not intelligence.
- Tool results make the cut arrive sooner than you expect. A single
llms.txtfetch can be 10k tokens.
Failure: treating history as a memory system. It is the right default for short chats.
2. Working memory
Working memory is a scratchpad the model can see on every turn — even hundreds of messages later, even on a new thread if the scope is resource.
You give it empty fields. The agent fills them with updateWorkingMemory. The filled block is injected into the system instruction, which the user never sees.
memory: new Memory({
options: {
workingMemory: {
enabled: true,
scope: "resource",
template: `# User Profile
- **Name**:
- **Preferences**:
- **Current Goal**:
`,
},
},
})- It supplements history. It does not replace it. Use it for facts that do not change: name, preferences, current goal. Coding agents and any long-horizon task live here.
scope: "resource"is the default: one scratchpad per user across threads.scope: "thread"isolates it to this conversation. Switching scopes does not migrate data.- You can use a Zod
schemainstead of a Markdowntemplate. Not both. Templates replace the whole block on each update. Schemas merge: send only the fields that changed; set a field tonullto delete it. - Working memory is small on purpose. You have to predefine the fields. When observational memory is on,
observationalMemory.observation.manageWorkingMemorylets the Observer write the scratchpad so the main agent does not have to remember the tool.
Failure: stuffing conversation summaries into the template, or growing it into an event log. That is level 4.
3. Semantic recall
Semantic recall is RAG over message history, not over your product corpus.
Every new message is embedded, and future messages query that vector store for similar turns. “I like dogs, mine is called Nas Barkley.” Later, in another thread: “what animals do I like?” The lookup is by meaning. Mastra injects the hits as an extra system block.
memory: new Memory({
storage: new LibSQLStore({ id: "agent-storage", url: "file:./local.db" }),
vector: new LibSQLVector({ id: "agent-vector", url: "file:./local.db" }),
embedder: new ModelRouterEmbeddingModel("openai/text-embedding-3-small"),
options: {
semanticRecall: { topK: 3, messageRange: 2, scope: "resource" },
},
})topKis how many hits.messageRangeis the surrounding turns to pull with each hit. Too much and the model drowns. Too little and you miss the fact.scope: "resource"searches all threads for that user; LibSQL, Postgres, MongoDB, OracleDB, and Upstash support it.- Disabled by default. It needs a vector store and an embedder. Same embedding model on write and query, same rule as document RAG.
- Recall is imprecise — you will tune
topKandmessageRangeper product. You now run an embedder and a vector store. Latency on every turn. - The org-scoped document index in How to build a RAG system is a library the agent searches with a tool. Message RAG is “what did we already say.” Different indexes. Different tenancy. Different tools.
Failure: the injected system block changes with the query. The prompt prefix is never stable enough to hit the cache. In production, cached input tokens are usually the largest saving. Semantic recall spends them.
4. Observational memory
Observational memory is modeled on how people remember, and how they forget. Two ambient agents: an Observer and a Reflector. They are always there. They are not always running.
When message tokens cross a threshold (default 30,000; demos often use 2k so you can watch it), the Observer compresses the raw history into a dense observation log: priority markers, timestamps, the sliver that still matters. A 10k-token tool result can become ~160 tokens.
When the observation log itself crosses its threshold (default 40,000), the Reflector rewrites the whole log. It drops low-priority lines, merges related facts, and keeps the window bounded. Reflections do not stack as a third infinite layer. Each reflection is the new log. New observations append after it.
Recent messages → Agent context
Recent messages
-(crosses messageTokens)→ Observer → Observation log → Agent context
Observation log
-(crosses observationTokens)→ Reflector → Observation log- The observations are stable. They append. They do not reshuffle the system prefix every turn — the cache argument against semantic recall, inverted.
- The Observer and Reflector run in the background. In Studio you can click them for a demo. In a real agent they are async and non-blocking. Compaction in a coding harness often pauses the user for a minute and throws away the wrong details. This loop is supposed to do neither.
memory: new Memory({
storage: new LibSQLStore({ id: "memory-storage", url: "file:./memory.db" }),
options: {
observationalMemory: { model: "google/gemini-2.5-flash" },
},
})observationalMemory: truedefaults the Observer/Reflector model togoogle/gemini-2.5-flash. Storage is required. Supported adapters today:@mastra/pg,@mastra/libsql,@mastra/mysql,@mastra/mongodb,@mastra/convex,@mastra/oracledb.- Mastra’s LongMemEval numbers from the video, same model across the first three rows: working memory ~55% (not designed for this benchmark), semantic recall ~80%, observational memory ~84%. Gemini 2.5 Flash on OM was reported at ~95%. Treat those as Mastra’s published scores, not an independent bake-off.
- This is the level that survives noisy tool calls: page snapshots,
llms.txt, MCP dumps. It also compounds — a workshop-helper that writes event copy for weeks will keep “second person, short hook, no hype” as observations, not as a field you remembered to put in a template. retrieval: truegives the agent arecalltool over the raw messages that produced an observation.{ vector: true }adds semantic search on that store. Compression does not have to mean the original wording is gone.observation.manageWorkingMemorylets OM own the scratchpad. Working memory stays small and structured; OM stops the main agent from spending a tool call on it.
5. Which level
| Level | Primitive | Use when | Breaks when |
|---|---|---|---|
| Conversation history | lastMessages | Short threads, UI transcript | Goal falls out of the window; new thread |
| Working memory | workingMemory | Stable facts and the current goal | You need an event log or undeclared fields |
| Semantic recall | semanticRecall | Sparse facts across a long, multi-thread history | You need prompt cache, or recall is too fuzzy |
| Observational memory | observationalMemory | Long horizon, noisy tools, cache-stable context | You refused to run a storage adapter |
- Mastra’s current recommendation is observational memory for long-context agents. The earlier levels still exist and still compose.
- History is what the model sees right now. Working memory is the form. Semantic recall is a search. Observational memory is how the window stays small without going blank.
- A
new Memory()with no options is conversation history. That is the weather agent in this repo. Graduate when the thread is no longer a chat.
6. Invariants
- Persist through a storage adapter. Memory is not the context window.
- The client sends the new message. The server loads the thread. Never both.
threadis the conversation.resourceis the owner. Cross-thread recall is a resource query, not a missingthreadId.- Working memory is a small, always-on block. Do not grow it into a diary.
- Semantic recall over messages is not document RAG. Different index, different tenant story. Same embedding model on write and query in both.
- Observational memory keeps a cacheable prefix by appending observations. Semantic recall busts that prefix on purpose.
- Isolation is a server concern.
resourceis not something the model gets to pick.
Four levels. Same Memory object. The sophistication is which tokens you keep, and which you are willing to forget.