You can now add hooks to Mastra's memory system, letting you watch observations and reflections, or change what the Observer and Reflector see and store.
There are two types of hooks:
- Lifecycle hooks (
onObservationStart,onObservationEnd,onReflectionStart,onReflectionEnd): These let you report on a cycle externally, e.g. catch a cost spike by logging token usage per cycle, or trace why an agent has forgotten something. - Transform hooks (
beforeObservation,afterObservation,beforeReflection,afterReflection): These let you intercept and modify the actual memory data. You can pare/remove noisy tool output, internal reasoning or redact secrets and PII.
Before hooks, observational memory didn't give levers to affect what happened during an observation or reflection cycle (other than filtering messages before they reached memory). Now the hooks sit inside the cycle.
We also shipped skillResultRedactor(), a ready-made version of a transform hook. When an agent uses a Skill, the tool result contains the skill's entire file contents. The Observer sees it every time. To avoid that, the redactor replaces the content with a placeholder. The agent still remembers the skill.
Get started
Install @mastra/memory and a storage adapter. The storage adapter is required for Observational Memory. Set it up either on the Memory instance or the main Mastra instance:
npm install @mastra/memory @mastra/libsqlCreate the agent
Create an agent and configure memory:
import { Agent } from "@mastra/core/agent";
import { Memory } from "@mastra/memory";
import { LibSQLStore } from "@mastra/libsql";
export const tripPlannerAgent = new Agent({
// ...
memory: new Memory({
storage: new LibSQLStore({
id: "memory-storage",
url: "file:./memory.db"
}),
options: {
observationalMemory: {
model: "google/gemini-2.5-flash"
}
}
})
});Lifecycle hooks
Define lifecycle hooks. They can fire at the start and end of every cycle. This example logs when a cycle starts, and what it costs when it ends:
new Memory({
// ...
options: {
observationalMemory: {
model: "google/gemini-2.5-flash",
hooks: {
onObservationStart: (info) => {
console.log(`[onObservationStart] ${info?.threadId} | ${info?.trigger}`);
},
onObservationEnd: ({ threadId, usage, error }) => {
if (error) {
console.error(`[onObservationEnd] ${threadId} | ${error}`);
} else {
console.log(`[onObservationEnd] ${threadId} | ${usage?.inputTokens} input tokens`);
}
},
onReflectionStart: (info) => {
console.log(`[onReflectionStart] ${info?.threadId} | ${info?.trigger}`);
},
onReflectionEnd: ({ threadId, usage, error }) => {
if (error) {
console.error(`[onReflectionEnd] ${threadId} | ${error}`);
} else {
console.log(`[onReflectionEnd] ${threadId} | ${usage?.inputTokens} input tokens`);
}
},
},
},
},
})Transform hooks
Define transform hooks. This example strips a bulky tool result before the Observer ever sees it:
const SIZE_THRESHOLD = 2000; // chars
new Memory({
// ...
options: {
observationalMemory: {
model: "google/gemini-2.5-flash",
hooks: {
beforeObservation: ({ messages }) => ({
messages: messages.map((message) => ({
...message,
content: {
...message.content,
parts: message.content.parts.map((part) => {
if (part.type !== "tool-invocation") return part;
const resultStr = JSON.stringify(part.toolInvocation.result);
if (resultStr.length <= SIZE_THRESHOLD) return part;
return {
...part,
toolInvocation: {
...part.toolInvocation,
result: "[search results omitted from memory]"
}
};
})
}
}))
}),
// Rewrite observations before they are persisted.
afterObservation: ({ observations, threadId }) => ({
observations: redact(observations),
}),
// Rewrite the text the Reflector condenses.
beforeReflection: ({ observations }) => ({
observations: stripInternalNotes(observations),
}),
// Rewrite the output before it's persisted, or run a side effect.
afterReflection: async ({ observations, resourceId }) => {
await syncToExternalStore(resourceId, observations)
},
},
},
},
})Transform hooks are always awaited, on every path: manual observe()/reflect(), turn-synchronous observation, and async buffering. Returning void from any of them passes the input through unchanged.
For more information and full configuration options, see:
