本页还没有译文
本页 20 段还没译成Deutsch,下面显示的是英文原文。Custom Adapter
还没翻译译自英文(tanstack.com)本页 20 段都还没译
You have a backend in mind (pgvector, MongoDB, DynamoDB, a hosted memory API) and the
built-in inMemory() / redis() adapters don't fit. A memory adapter is just an object
with two methods, recall and save, so this is a short guide.
First time looking at memory? Start with the Overview for what the contract is and how the middleware uses it.
The contract
// The MemoryAdapter contract, from `@tanstack/ai-memory`:
import type { MemoryAdapter } from '@tanstack/ai-memory'// The shape of the contract, shown for reference.
import type {
MemoryFact,
MemoryScope,
MemorySnapshot,
MemoryTurn,
RecallResult,
SaveReceipt,
} from '@tanstack/ai-memory'
interface MemoryAdapter {
id: string
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)
}Two rules the middleware relies on:
recalldecides relevance. Return a renderedsystemPrompt(empty string when there's nothing), plus optionalfragments,tools, andtoolGuidance. Ranking strategy is entirely yours: lexical, vector, hybrid, or vendor-native.saveowns extraction. Turn the{ user, assistant }turn into whatever you persist. Return oneSaveReceiptper underlying write.
Scope isolation is your responsibility: a recall for one scope must never surface
another scope's data.
Step 1: Scaffold
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, etc.
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 dim exactly (including NULL). Omitted tenant/user
// must not match rows written with a tenant/user set.
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 }
},
}
}The shape generalizes: every method takes a scope, does its backend-specific work,
and keeps scopes isolated. For a backend without native search, load the scope's
records and rank them yourself.
Step 2: Run the contract suite
@tanstack/ai-memory/testkit exports runMemoryAdapterContract. inMemory and redis
run this suite. Point it at a factory that returns a fresh adapter. It verifies the save then recall round-trip, scope
isolation, empty recall, receipt shape, and the optional introspection methods.
// 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 })
})Step 3: Wire it into memoryMiddleware
Once the suite is green, the adapter is interchangeable with the built-ins:
// ignore: imports the `./pgvector` module you wrote in Step 1, and assumes
// `messages` / `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-5.5'),
messages,
middleware: [memoryMiddleware({ adapter: memory, scope })],
})The middleware never inspects the adapter's internals. recall/save is the entire
interface.
Exposing tools (optional)
recall can return tools and toolGuidance to give the model direct control over
memory (this is how the hindsight() adapter exposes retain/recall/reflect tools). The
middleware merges them into the run's tools and injects the guidance ahead of the
recalled prompt. Return tools: [] (or omit it) when your adapter exposes none.
Pitfalls
- Keep scopes isolated. If you serialize scope into a composite key, escape your
delimiter so a
threadId/userId/tenantIdcontaining it can't collide with another scope. recallmust not throw for an empty scope. Return{ systemPrompt: '' }.- Extraction lives in
save. Don't expect the middleware to derive facts. The raw turn is handed to you; store or summarize it however you like.
Where to go next
- Overview: the
recall/savecontract, scope, and how a turn flows - Adapters: the built-in and vendor adapters, with every option
- Quickstart: wire
memoryMiddlewareinto a realchat()call - Operating memory: options, telemetry, devtools events, and failures