Skip to main content
The @atomicmemory/langchain adapter is a thin wrapper around an injected MemoryClient from @atomicmemory/sdk. It provides two complementary surfaces: agent-callable tools you can hand directly to createToolCallingAgent or any @langchain/core/tools-compatible runner, and framework-agnostic helpers you can call inside LCEL chains, callbacks, 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/langchain @atomicmemory/sdk @langchain/core zod
@langchain/core and zod are declared as peer dependencies so you can pin them to the versions your project already uses.

Two surfaces at a glance

SurfaceUse when
createMemoryTools()You want memory_search and memory_ingest as agent-callable tools for createToolCallingAgent or a LangGraph tool node.
searchMemory() / ingestTurn()You want to call AtomicMemory inside a callback, an LCEL RunnableLambda, or any other arbitrary code path.

Quick start — agent tools

The most common pattern is to create both tools once at application startup and include them in your agent’s tool list alongside your other tools:
import { MemoryClient } from '@atomicmemory/sdk';
import { createMemoryTools } from '@atomicmemory/langchain';

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,
});

// Hand the tools to any LangChain agent runner:
const tools = [searchTool, ingestTool /*, ...your other tools */];
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 directly, for example in a preprocessing step before the model call or in a callback:
import { searchMemory, ingestTurn } from '@atomicmemory/langchain';

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

if (context) {
  // Prepend `context` to your prompt or attach it as a system message.
}

// After the model call — persist the completed turn
await ingestTurn(memory, {
  messages: turn.messages,
  completion: turn.responseText,
  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 instruction-shaped content hijacking the model. You can override the formatter per call:
await searchMemory(memory, {
  query,
  scope,
  formatter(results) {
    return `# Prior context\n\n${results
      .map((r) => `- [${r.memory.createdAt.toISOString()}] ${r.memory.content}`)
      .join('\n')}`;
  },
});
The default formatter is a mitigation, not a guarantee. If you store content that may contain instruction-shaped text (for example, user-supplied notes or external documents), add your own sanitization before injecting context into prompts.

System-message handling on ingest

ingestTurn() excludes system messages by default. Applications typically use system messages for hidden instructions and policies that should not become durable memory. If your system messages are genuinely user-authored content worth remembering, opt in explicitly:
await ingestTurn(memory, {
  messages,
  completion,
  scope,
  includeRoles: ['system', 'user', 'assistant', 'tool'],
});

Scope

Scope fields follow the SDK’s Scope type: user, agent, namespace, and thread. You must provide at least one field — the SDK rejects scopeless requests.
For most single-user applications, setting user and namespace is sufficient. Use agent when you want memories scoped to a specific agent identity rather than a person, and thread when you want per-conversation isolation within a user’s memory space.