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.

An agent framework with the Oort SDKYour agent framework calls Oort through a handle from createOort(), in the same Node.js process. The adapter or wrapper recalls before the model call and observes the finished turn.in processYour agentAI SDK, LangGraph.js, ...oortmema scoped handleMemory~/.oort or Oort Cloud

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.

Terminal
npm i oortmem ai
TypeScript
import { 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, remember and forget tools. { judge: true } adds judge and record_judgment. recallLimit (default 8) and budgetChars bound what recall returns.
oortInstructions(oort, query, opts?)
What it gives youThe <oort-memory> block for instructions, or an empty string. Takes limit, speed and budgetChars.
oortObserve(oort, prompt, opts?)
What it gives youAn onEnd callback that saves the turn. Takes turnRef and onError: 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 formatForPrompt block. The judgment tools never offer includeAgentRecorded or includeImported, 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) }. Passing oortTools(oort) directly runs, but typechecks against AI SDK 7's ToolSet only 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.

Terminal
npm i oortmem @langchain/langgraph @langchain/anthropic zod
TypeScript
import { 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.

Terminal
npm i oortmem @openai/agents
TypeScript
import { 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);