SDK
SDK in agent frameworks
Give a TypeScript agent memory in its own process, with no MCP server. The Vercel AI SDK has a shipped adapter. LangGraph.js and OpenAI Agents JS take a short wrapper you copy.
Python, Mastra or any other MCP client? Start oort mcp instead, as Agent frameworks shows. For many end users, pass each framework a handle from scoped({ user }), never the root handle (see Multi-user apps).
Vercel AI SDK
Experimentaloortmem/ai-sdk works today and may change in any minor release.
npm i oortmem aiimport { generateText, isStepCount } from 'ai';
import { createOort } from 'oortmem';
import { oortInstructions, oortObserve, oortTools } from 'oortmem/ai-sdk';
await using oort = await createOort();
const prompt = 'When do we deploy billing?';
const { text } = await generateText({
model: 'anthropic/claude-sonnet-5.5', // any AI SDK model; a string goes through the AI Gateway
instructions: await oortInstructions(oort, prompt), // `system` before AI SDK 7
tools: { ...oortTools(oort) }, // spread in 0.6.0; recall, remember, forget; { judge: true } adds judge and record_judgment
stopWhen: isStepCount(5),
prompt,
onEnd: oortObserve(oort, prompt), // `onFinish` before AI SDK 7
});
console.log(text);oortTools(oort, opts?)- What it gives youThe
recall,rememberandforgettools.{ judge: true }addsjudgeandrecord_judgment.recallLimit(default 8) andbudgetCharsbound what recall returns. oortInstructions(oort, query, opts?)- What it gives youThe
<oort-memory>block forinstructions, or an empty string. Takeslimit,speedandbudgetChars. oortObserve(oort, prompt, opts?)- What it gives youAn
onEndcallback that saves the turn. TakesturnRefandonError: a failed save never fails the generation.
- The tools always run in the agent role, and never expose
scoped. - Memory reaches the model only as a
formatForPromptblock. The judgment tools never offerincludeAgentRecordedorincludeImported, so a model cannot widen what counts as precedent. - A refusal comes back to the model as
{ error: { code, message } }, never a stack. - In 0.6.0, spread the tools:
tools: { ...oortTools(oort) }. PassingoortTools(oort)directly runs, but typechecks against AI SDK 7'sToolSetonly from 0.6.1. - The adapter does not import
ai. AI SDK 7 is an optional peer dependency. - In a serverless function, await the generation, so the save finishes before the runtime freezes.
LangGraph.js
An example, not a shipped adapter: a BaseStore that maps LangGraph's store calls onto a scoped handle, and a node that recalls before the model call and observes after it. Your scopeOf decides which namespace is which user.
npm i oortmem @langchain/langgraph @langchain/anthropic zodimport { ChatAnthropic } from '@langchain/anthropic';
import {
BaseStore,
MessagesValue,
START,
StateGraph,
StateSchema,
type GraphNode,
type Item,
type Operation,
type OperationResults,
} from '@langchain/langgraph';
import {
createOort,
formatForPrompt,
OortNotFoundError,
type Oort,
type ScopedOptions,
} from 'oortmem';
import { z } from 'zod';
/**
* A LangGraph `BaseStore` over Oort: `search` with a query recalls, `put` remembers `{ text }`,
* a `null` put (`delete`) forgets, and `get` explains. Oort assigns the ids, so a put's key is not
* kept: `get` and `delete` take Oort ids. `scopeOf` (your code) maps a namespace to a scope.
*/
export class OortStore extends BaseStore {
readonly oort: Oort;
readonly scopeOf: (namespace: string[]) => ScopedOptions;
constructor(oort: Oort, scopeOf: (namespace: string[]) => ScopedOptions) {
super();
this.oort = oort;
this.scopeOf = scopeOf;
}
handle(namespace: string[]): Oort {
return this.oort.scoped({ ...this.scopeOf(namespace), judgeRole: 'agent' });
}
async batch<Op extends Operation[]>(operations: Op): Promise<OperationResults<Op>> {
const results = await Promise.all(operations.map((op) => this.run(op)));
return results as OperationResults<Op>;
}
private async run(op: Operation): Promise<Item | Item[] | string[][] | null | void> {
if ('namespacePrefix' in op) {
if (op.query === undefined) return []; // Oort lists by relevance only
const r = await this.handle(op.namespacePrefix).recall(op.query, { limit: op.limit ?? 10 });
return r.memories.map((m) => ({ ...item(op.namespacePrefix, m), score: m.score }));
}
if ('value' in op) {
const mem = this.handle(op.namespace);
const text: unknown = op.value?.['text'];
if (op.value === null) await mem.forget(op.key);
else if (typeof text === 'string') await mem.remember(text);
else throw new TypeError('OortStore stores values of the form { text: string }');
return;
}
if ('key' in op) {
try {
return item(op.namespace, (await this.handle(op.namespace).explain(op.key)).memory);
} catch (e) {
if (e instanceof OortNotFoundError) return null;
throw e;
}
}
return []; // listNamespaces: users are your app's, not Oort's
}
}
const item = (
namespace: string[],
m: { id: string; text: string; recordedAt: string; updatedAt: string },
): Item => ({
namespace,
key: m.id,
value: { text: m.text },
createdAt: new Date(m.recordedAt),
updatedAt: new Date(m.updatedAt),
});
const oort = await createOort({ mode: 'local', org: 'acme', project: null, projectConfig: false });
// The first namespace part is the end user. A missing one throws (`user-missing`), never
// falling back to the tenant's own memory.
const store = new OortStore(oort, ([user]) => ({ user: user ?? '' }));
const model = new ChatAnthropic({ model: 'claude-haiku-5-5' });
const State = new StateSchema({ messages: MessagesValue });
const Context = z.object({ userId: z.string() });
const answer: GraphNode<typeof State, z.infer<typeof Context>> = async (state, runtime) => {
const userId = runtime.context?.userId ?? '';
const question = state.messages.at(-1)?.text ?? '';
const mem = store.handle([userId]);
const memory = formatForPrompt(await mem.recall(question));
const reply = await model.invoke([
{ role: 'system', content: `You help customers.\n${memory}` },
...state.messages,
]);
await mem.observe({ user: question, assistant: reply.text });
return { messages: [reply] };
};
const graph = new StateGraph(State, Context)
.addNode('answer', answer)
.addEdge(START, 'answer')
.compile({ store });
await store.put(['user-ada'], 'order', { text: 'My order number is 1042.' });
const out = await graph.invoke(
{ messages: [{ role: 'user', content: 'What is my order number?' }] },
{ context: { userId: 'user-ada' } },
);
console.log(out.messages.at(-1)?.text);
await oort.close();OpenAI Agents JS
An example, not a shipped adapter: wrap any Session, so storage stays where it is and each user and assistant pair is observed. Recall goes through dynamic instructions, never by rewriting history.
npm i oortmem @openai/agentsimport { Agent, MemorySession, run, type AgentInputItem, type Session } from '@openai/agents';
import { createOort, formatForPrompt, type Oort } from 'oortmem';
/** Wraps any `Session`: storage stays where it is, and each user and assistant pair is observed. */
export function oortSession(inner: Session, oort: Oort): Session {
let pending: string | undefined;
return {
getSessionId: () => inner.getSessionId(),
getItems: (limit) => inner.getItems(limit),
popItem: () => inner.popItem(),
clearSession: () => inner.clearSession(),
async addItems(items) {
await inner.addItems(items);
for (const i of items) {
if (!('role' in i)) continue;
if (i.role === 'user') pending = textOf(i);
else if (i.role === 'assistant' && pending !== undefined) {
await oort.observe({ user: pending, assistant: textOf(i) });
pending = undefined;
}
}
},
};
}
function textOf(i: AgentInputItem): string {
if (!('content' in i)) return '';
if (typeof i.content === 'string') return i.content;
return i.content.map((c) => ('text' in c ? c.text : '')).join('');
}
await using root = await createOort();
const oort = root.scoped({ agent: 'support-bot', judgeRole: 'agent' });
// Recall goes through dynamic instructions, never by rewriting the history.
const agent = new Agent<{ question: string }>({
name: 'support',
instructions: async (ctx) =>
`You help customers.\n${formatForPrompt(await oort.recall(ctx.context.question))}`,
});
const question = 'When do we deploy billing?';
const result = await run(agent, question, {
session: oortSession(new MemorySession(), oort),
context: { question },
});
console.log(result.finalOutput);