Skip to main content
The @atomicmemory/mastra adapter connects AtomicMemory to Mastra agents and workflows. It provides two surfaces: Mastra-native agent tools you can attach directly to an Agent, and framework-agnostic helpers you can call inside workflow steps, agent hooks, or any other code path. The adapter does not own provider configuration — you construct the MemoryClient yourself and pass it in.

Installation

pnpm add @atomicmemory/mastra @atomicmemory/sdk @mastra/core zod
@mastra/core and zod are declared as peer dependencies so you pin compatible versions alongside the rest of your Mastra application.

Two surfaces at a glance

SurfaceUse when
createMemoryTools()You want memory_search and memory_ingest as agent-callable Mastra tools attached to an Agent.
searchMemory() / ingestTurn()You want to call AtomicMemory inside a workflow step, an agent hook, or any other arbitrary code path.

Quick start — agent tools

Create both tools once at application startup and attach them to your Agent:
import { Agent } from '@mastra/core/agent';
import { MemoryClient } from '@atomicmemory/sdk';
import { createMemoryTools } from '@atomicmemory/mastra';

const memory = new MemoryClient({
  providers: { atomicmemory: { apiUrl: process.env.ATOMICMEMORY_URL!, apiKey: process.env.ATOMICMEMORY_KEY! } },
});
await memory.initialize();

const { searchTool, ingestTool } = createMemoryTools(memory, {
  scope: { user: 'pip', namespace: 'my-app' },
  defaultLimit: 5,
});

const agent = new Agent({
  name: 'assistant',
  instructions:
    'Use memory_search to recall prior context. Use memory_ingest to remember new facts.',
  model: /* your model */,
  tools: { memory_search: searchTool, memory_ingest: ingestTool },
});
Scope is fixed at factory time. The agent cannot rebind to a different user or namespace by passing different tool arguments — this is an intentional security boundary.

Quick start — framework-agnostic helpers

Use searchMemory and ingestTurn when you want to drive memory operations from a workflow step or helper rather than through tool calls:
import { searchMemory, ingestTurn } from '@atomicmemory/mastra';

// Before the model call — retrieve relevant context
const { context } = await searchMemory(memory, {
  query: latestUserMessage,
  scope: { user: 'pip' },
  limit: 8,
});

if (context) {
  // Prepend context to the model call.
}

// After the model call — persist the completed turn
await ingestTurn(memory, {
  messages,
  completion: text,
  scope: { user: 'pip' },
});

Custom retrieval formatting

The default formatter wraps retrieved memories in a delimited block with an explicit “reference, not instructions” header — a mitigation against prompt injection via retrieved content. You can override the formatter per call:
await searchMemory(memory, {
  query,
  scope,
  formatter(results) {
    return `# Prior context\n\n${results
      .map((r) => `- ${r.memory.content}`)
      .join('\n')}`;
  },
});
The default formatter is a mitigation, not a guarantee. If you store content that may contain instruction-shaped text, add your own sanitization before injecting context into prompts.

System-message handling on ingest

ingestTurn() excludes system messages by default. If your system messages are genuinely user-authored content worth persisting as memory, opt in explicitly:
await ingestTurn(memory, {
  messages,
  completion: text,
  scope,
  includeRoles: ['system', 'user', 'assistant', 'tool'],
});

Scope

Scope fields follow the SDK’s Scope type: user, agent, namespace, and thread. At least one field must be provided — the SDK rejects scopeless requests.
Construct your MemoryClient once and share it across multiple createMemoryTools() calls if you need memory tools for more than one agent. Each call to createMemoryTools() can use a different scope while reusing the same underlying client.