- MCP Server ToolsUpdated
- Managed MCP with chat()Updated
- Manual MCPUpdated
- MCP Type GenerationUpdated
- MCP Client InputUpdated
- MCP Server Tools: AdvancedNew
- MCP EventsNew
- MCP AppsUpdated
- Serve Tools over HTTPUpdated
- Serve Resources and PromptsUpdated
- Ask for InputUpdated
- Draft Text from a ToolUpdated
- MCP Server TasksUpdated
- MCP Server AuthUpdated
- MCP Server SessionsUpdated
- MCP Server on stdioUpdated
Persistence (Advanced)
- GenerationsUpdated
- Realtime Voice ChatUpdated
- Text-to-SpeechUpdated
- TranscriptionUpdated
- Transcription: AdvancedNew
- Voice CreationUpdated
- Audio RecordingUpdated
- Audio GenerationUpdated
- Image GenerationUpdated
- Image Generation: AdvancedNew
- Video GenerationUpdated
- Video Generation: AdvancedNew
- Live GenerationUpdated
- World GenerationUpdated
- Generation HooksUpdated
- Build your first harnessUpdated
- Host optionsNew
- Send and show mediaUpdated
- Connect clientsUpdated
- Write a client in any languageNew
- Access and tenantsNew
- Run in the terminalUpdated
- Deploy a harnessUpdated
- Send inputs safelyUpdated
- Run a tool without a model
- Durable sessionsUpdated
- Run background work on workersNew
- List, rename, and fork sessionsUpdated
- Lifecycle and retentionNew
- Run side effects onceUpdated
- Wait for a personNew
- Build on the session logUpdated
- Control how a turn endsUpdated
- Store settings per threadUpdated
- Compact a long sessionUpdated
- Fork and reset a threadUpdated
- One log for a session treeUpdated
- Write a pluginUpdated
- Track usage and costUpdated
- Ask before risky tool callsUpdated
- Give your agent coding toolsUpdated
- Sandbox and extend the coding toolsUpdated
- Run the coding tools for many threadsNew
- Undo a turnUpdated
- Build a coding agentUpdated
- Delegate to coding agentsUpdated
- Switch between agent profilesUpdated
- Auth and connectorsUpdated
- Share a thread between peopleUpdated
- Connect model providersUpdated
- Build your own UIUpdated
- Code mode in a harnessUpdated
- Work until a goal is metUpdated
- Run agents from a harnessUpdated
- Hand off a conversationNew
- Add skillsUpdated
- Use MCP serversUpdated
- Give your agent a computerNew
- Use from any MCP clientUpdated
- Use a harness from an editor (ACP)Updated
- OverviewUpdated
- Quick StartUpdated
- ProvidersUpdated
- HarnessesUpdated
- Harness AuthUpdated
- WorkspaceUpdated
- ToolsUpdated
- PolicyUpdated
- Lifecycle & SnapshotsUpdated
- Portable SnapshotsUpdated
- Keep Files After ReloadUpdated
- Save a Named VersionUpdated
- Branch From a VersionUpdated
- Send a Frozen FileUpdated
- Let the Agent Save and ForkUpdated
- Pick Which Files to KeepUpdated
- What a Snapshot StoresUpdated
- Instance DurabilityUpdated
- Durable RunsUpdated
- EventsUpdated
Sandboxes (Advanced)
- Runtime ContextUpdated
- Bring Your Own KeyUpdated
- Keep Keys on the ServerNew
- Debug LoggingUpdated
- Multimodal ContentUpdated
- Files APIUpdated
- Per-Model Type SafetyUpdated
- Runtime Adapter SwitchingUpdated
- Tree-ShakingUpdated
- Extend AdapterUpdated
- Extend Adapter: AdvancedNew
- Typed Pre-Configured OptionsUpdated
- Test with a Fake ModelUpdated
- Prompt CachingUpdated
- Mid-Conversation ChangesUpdated
- Approval Flow ProcessingUpdated
Custom Adapter
A custom memory adapter is an object with recall and save. Use one for pgvector, MongoDB, DynamoDB, or a hosted memory API.
This page builds a pgvector adapter. The same shape works for any backend.
Quick setup#
1. Write the adapter#
save stores the turn. recall returns the records that match the query, as a prompt block:
// pgvector.ts
import type {
MemoryAdapter,
MemoryScope,
MemoryTurn,
RecallResult,
SaveReceipt,
} from '@tanstack/ai-memory'
// node-postgres' Pool, minimally. In your project, use the real type instead:
// import type { Pool } from 'pg'
type Pool = {
query: (
text: string,
values: Array<unknown>,
) => Promise<{ rows: Array<{ text: string }> }>
}
// Your embedding client. Swap in OpenAI, Cohere, a local model, and more.
type Embed = (text: string) => Promise<Array<number>>
export function pgvectorMemory(options: { pool: Pool; embed: Embed }): MemoryAdapter {
const { pool, embed } = options
return {
id: 'pgvector',
async save(scope: MemoryScope, turn: MemoryTurn): Promise<Array<SaveReceipt>> {
const rows = [
{ role: 'user', text: turn.user },
{ role: 'assistant', text: turn.assistant },
]
for (const row of rows) {
const vector = await embed(row.text)
await pool.query(
`INSERT INTO memory (thread_id, user_id, tenant_id, role, text, embedding)
VALUES ($1, $2, $3, $4, $5, $6)`,
[
scope.threadId,
scope.userId ?? null,
scope.tenantId ?? null,
row.role,
row.text,
JSON.stringify(vector),
],
)
}
return [{ ok: true }]
},
async recall(scope: MemoryScope, query: string): Promise<RecallResult> {
const q = await embed(query)
// Match every isolation field exactly (including NULL). An omitted
// tenant or user must not match rows written with a tenant or user.
const { rows } = await pool.query(
`SELECT text, 1 - (embedding <=> $1::vector) AS score
FROM memory
WHERE thread_id = $2
AND user_id IS NOT DISTINCT FROM $3::text
AND tenant_id IS NOT DISTINCT FROM $4::text
ORDER BY score DESC
LIMIT 6`,
[
JSON.stringify(q),
scope.threadId,
scope.userId ?? null,
scope.tenantId ?? null,
],
)
const fragments = rows.map((r) => ({ text: r.text, source: 'pgvector' }))
const systemPrompt = fragments.length
? `Relevant memory:\n${fragments.map((f) => `- ${f.text}`).join('\n')}`
: ''
return { systemPrompt, fragments }
},
}
}2. Run the contract suite#
runMemoryAdapterContract from @tanstack/ai-memory/testkit is a Vitest suite. Give it a factory that returns a fresh adapter:
// ignore: imports the `../src/pgvector` module you wrote in Step 1.
// tests/pgvector.test.ts
import { runMemoryAdapterContract } from '@tanstack/ai-memory/testkit'
import { pgvectorMemory } from '../src/pgvector'
runMemoryAdapterContract('pgvectorMemory', async () => {
const pool = makeCleanPool() // truncate between tests for a fresh adapter
return pgvectorMemory({ pool, embed })
})The suite checks:
- A saved turn comes back in a later recall.
- recall on an empty scope returns an empty prompt.
- recall never returns data from another threadId, userId, or tenantId.
- save returns at least one receipt with ok: true.
- inspect and listFacts, if you add them, return well-formed data.
inMemory() and redis() run the same suite.
3. Add it to memoryMiddleware#
When the suite passes, use your adapter like a built-in one:
// ignore: imports the `./pgvector` module you wrote in Step 1, and assumes
// `pool`, `embed`, `messages`, and `scope` from your app.
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { memoryMiddleware } from '@tanstack/ai-memory'
import { pgvectorMemory } from './pgvector'
const memory = pgvectorMemory({ pool, embed })
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages,
threadId: 'thread-1',
middleware: [memoryMiddleware({ adapter: memory, scope })],
})Your backend now gives memory to every chat that uses this middleware.
How it works#
The middleware calls only the adapter methods. It never reads your records.
// The shape of the contract, shown for reference.
import type {
MemoryFact,
MemoryScope,
MemorySnapshot,
MemoryTurn,
RecallResult,
SaveReceipt,
} from '@tanstack/ai-memory'
interface MemoryAdapter {
id: string
name?: string // optional label for logs, defaults to id
recall(scope: MemoryScope, query: string): Promise<RecallResult>
save(scope: MemoryScope, turn: MemoryTurn): Promise<Array<SaveReceipt>>
inspect?(scope: MemoryScope): Promise<MemorySnapshot> // optional (devtools)
listFacts?(scope: MemoryScope): Promise<Array<MemoryFact>> // optional (devtools)
}| Member | What it does |
|---|---|
| id | A stable name for logs and devtools. |
| name | Optional. A readable label for logs. The default is id. |
| recall(scope, query) | Decides what is relevant. Returns a rendered systemPrompt (an empty string when there is nothing), plus optional fragments, tools, and toolGuidance. You choose the ranking: lexical, vector, hybrid, or what your vendor gives. |
| save(scope, turn) | Owns extraction. Turns the { user, assistant } turn into what you store. Returns one SaveReceipt per write. |
| inspect(scope) | Optional. A full snapshot for the devtools panel. |
| listFacts(scope) | Optional. A flat fact list for the devtools panel. |
Your adapter owns scope isolation. A recall for one scope must never return data from another scope.
Advanced#
Give the model memory tools#
recall can return tools and toolGuidance. Then the model can read or write memory itself. The hindsight() adapter uses this for its retain, recall, and reflect tools.
- The middleware adds your tools to the tools of the run. It skips a tool when the run already has a tool with the same name.
- The middleware puts toolGuidance in the system prompts before the recalled systemPrompt.
- If your adapter has no tools, return tools: [] or leave it out.
Pitfalls#
- Keep scopes isolated. If you put the scope into one composite key, escape the delimiter. Then a threadId, userId, or tenantId that contains the delimiter cannot match another scope.
- Do not throw for an empty scope. Return { systemPrompt: '' } from recall.
- Do extraction in save. The middleware does not derive facts. It gives you the raw turn. Store it or summarize it as you need.
- Backends without search. Load the records of the scope and rank them in your code.