past dev is a memory API for AI agents. This package gives an agent built with the Vercel AI SDK long-term memory through two tools, Recall and Ingest, and an optional middleware that recalls before each model call.
pastTools()returns the AI SDK toolsrecallandingest.pastRecallMiddleware()adds a small, ranked recall to the prompt of each model call.createPastClient()is the small fetch client under both, withdeleteDataPoint()for retraction.
It works with AI SDK 7, any model provider, Node 20 or later, and edge runtimes that have fetch.
npm install @pastdotdev/ai-sdk ai zodGet a project API key (it starts with past_sk_) from the past dev console, and set it:
export PAST_API_KEY=past_sk_...import { generateText, isStepCount } from 'ai';
import { pastTools } from '@pastdotdev/ai-sdk';
const { text } = await generateText({
model: 'openai/gpt-5-mini',
tools: pastTools({ identity: user.id }),
stopWhen: isStepCount(5),
prompt: 'Remember that my favourite editor is Neovim.',
});With an agent:
import { ToolLoopAgent } from 'ai';
import { pastTools } from '@pastdotdev/ai-sdk';
const agent = new ToolLoopAgent({
model: 'anthropic/claude-sonnet-5.5',
instructions: 'You are a helpful assistant with long-term memory.',
tools: pastTools({ identity: user.id }),
});
const result = await agent.generate({ prompt: 'Which editor do I use?' });| Tool | API call | What the model fills |
|---|---|---|
recall |
POST /api/v1/recall |
query |
ingest |
POST /api/v1/ingest |
content, and optionally timestamp (ISO 8601) and label |
recall gives the model compact text: one numbered line per memory with its date, a mark when a
later statement superseded it, and up to two quotations from its sources. Set
modelOutput: 'json' to give the model the API's JSON instead. Source metadata is never put in the
text.
ingest gives each call a new source id, and uses the same value as the idempotency key, so a
retry of the same call never makes a second data point. The result carries sourceId, which
deleteDataPoint() takes later. A new memory becomes readable after a short processing time.
Recall needs an identity: the person whose memory it reads. Ingest names the identity as the
author, and creates it the first time it sees it. Your code sets the identity, from your own
session or user record. The model never chooses it. No tool schema has an identity field, so
a prompt cannot make the model read or write another person's memory. Make the tools for each
request, with the signed-in user's id.
pastTools({
identity: user.id, // required
apiKey: '...', // default: PAST_API_KEY
apiHost: 'https://...', // default: PAST_API_HOST, then https://api.past.dev
audience: 'team-a', // the audience slug of each ingest; omit it for the whole project
metadata: { app: 'chat' }, // your JSON, stored with each ingest; the model never sees it
limit: 10, // the maximum number of memories in a recall (default 10)
level: 'low', // the evidence budget: low, medium, high, extra-high
maxTokens: 4000, // or an explicit budget in tokens (not with level)
modelOutput: 'text', // or 'json'
descriptions: { recall: '...', ingest: '...' },
fetch, // a custom fetch
});apiHost is for a past dev deployment in your own cloud.
The middleware recalls with the last user message as the query, before each model call, and adds the memories as one system message after your own system messages. It keeps the prompt small:
- at most
limitmemories (default 5) from an evidence budget ofmaxTokens(default 1,000); - at most
maxCharscharacters of memory text (default 4,000), dropping the lowest-ranked memories to fit; - no superseded memories, unless you set
skipSuperseded: false; - control characters removed, and the block marked as notes, not instructions.
It adds nothing when the message has no text, when nothing matches, or when the recall fails. A
failed recall goes to onError (default: one console warning with the error code), and the model
call goes on. Inside a tool loop, the same query within one minute reuses the first answer.
import { gateway, streamText, wrapLanguageModel } from 'ai';
import { pastRecallMiddleware } from '@pastdotdev/ai-sdk';
const model = wrapLanguageModel({
model: gateway('openai/gpt-5-mini'),
middleware: pastRecallMiddleware({ identity: user.id }),
});
const result = streamText({ model, prompt: 'What did we decide about the launch date?' });The middleware only reads. Use the ingest tool, or createPastClient().ingest() in your own
code, to write.
import { createPastClient } from '@pastdotdev/ai-sdk';
const past = createPastClient(); // PAST_API_KEY, PAST_API_HOST
await past.listAudiences(); // free: a good connection test
await past.ingest({ content: 'Order 1042 shipped.', identity: user.id, id: 'order-1042' });
await past.recall({ query: 'order 1042', identity: user.id });
await past.deleteDataPoint('order-1042'); // the data point and the memory derived from it
await past.listIdentities({ limit: 100 }); // paged with hasMore and endCursorAn API error is a PastApiError with code, status, debugMessage and requestId (the
X-Request-Id header). Inside a tool, the AI SDK gives the error to the model as a tool error, and
the loop goes on. A 429 is tried again at most twice, after the Retry-After time (at most 10
seconds). No other error is tried again: 402 out-of-credits and every 400 fail at once.
Every ingest spends credits, and so does a recall on plans that price one. The middleware recalls once per new user message. See pricing for the figures.
npm ci
npm run check # typecheck, build, testsThe tests use a fake fetch and the AI SDK mock model, so they spend nothing. One live smoke test
(test/live.test.ts) runs only when PAST_API_KEY is set. It makes four calls: a free connection
test, one ingest, one recall, and one delete.
Questions: louis@past.dev.
MIT