Memory
Two contracts, not one. ChatMemory persists conversation history. VectorMemory stores embeddings for semantic recall. Different problems, different backends.
Memory in AgentsKit is two contracts, deliberately split:
- ChatMemory β the ordered history of a session. Append to it, load it back. Redis is great at this.
- VectorMemory β embeddings indexed for semantic retrieval. The substrate for RAG. pgvector, Pinecone, Qdrant are great at this.
Plus a third primitive β EmbedFn β for turning text into vectors.
LangChain unifies these. The result is implementations that half-fulfill both. We split them so a backend implements only what it does well.
#ChatMemory
import type { ChatMemory } from '@agentskit/core'
export interface ChatMemory {
load: () => MaybePromise<Message[]>
save: (messages: Message[]) => MaybePromise<void>
clear?: () => MaybePromise<void>
}save is replace-all β not append. Controllers append in memory and flush the whole state at the end of a turn. This removes the "did I dedupe?" branching that haunts append-based stores.
import { sqliteChatMemory } from '@agentskit/memory'
const memory = sqliteChatMemory({ path: './sessions/user-42.db' })
createRuntime({ adapter, memory, /* ... */ })The runtime calls load() at the start of each run() and save() after a successful run. Aborted or failed runs do not save β atomicity comes for free.
#VectorMemory
import type { VectorMemory, VectorDocument } from '@agentskit/core'
export interface VectorMemory {
store: (docs: VectorDocument[]) => MaybePromise<void>
search: (
embedding: number[],
options?: { topK?: number; threshold?: number },
) => MaybePromise<RetrievedDocument[]>
delete?: (ids: string[]) => MaybePromise<void>
}store is upsert by id. Re-indexing the same document is cheap and idempotent β no duplicate-id errors.
import { fileVectorMemory } from '@agentskit/memory'
import { openaiEmbedder } from '@agentskit/adapters'
const store = fileVectorMemory({ path: './embeddings.json' })
const embed = openaiEmbedder({ apiKey: KEY, model: 'text-embedding-3-small' })
await store.store([
{ id: 'doc-1', content: 'AgentsKit core is 10KB gzipped.', embedding: await embed('AgentsKit core is 10KB gzipped.') },
])
const hits = await store.search(await embed('how big is the core?'), { topK: 3 })Search results come descending by score. threshold is exclusive from below (> 0.7, not >= 0.7).
#EmbedFn
export type EmbedFn = (text: string) => Promise<number[]>A pure function. Same input + same model = same output. No randomness allowed.
Built-in embedders:
import { openaiEmbedder, ollamaEmbedder, geminiEmbedder } from '@agentskit/adapters'
const embed = openaiEmbedder({ apiKey: KEY, model: 'text-embedding-3-small' })#Built-in backends
| Backend | ChatMemory | VectorMemory |
|---|---|---|
fileChatMemory | β | β |
fileVectorMemory | β | β |
sqliteChatMemory | β | β |
redisChatMemory | β | β |
redisVectorMemory | β | β |
More coming in Phase 3 (pgvector, Pinecone, Qdrant, Chroma, Weaviate, Turso, Cloudflare Vectorize, Upstash). Each is a small adapter implementing six or eight invariants.
#When to write your own
- Your storage backend isn't covered yet. A new ChatMemory backend is ~50 lines. A new VectorMemory backend is ~100. See the existing implementations as templates.
- You need encryption at rest. Wrap an existing backend with an encrypting proxy β neither contract changes.
- You're testing. A pure in-memory
ChatMemoryis twelve lines and instantly replayable.
#Common pitfalls
| Pitfall | What to do instead |
|---|---|
Implementing save as append-with-dedup | Replace-all (CM2). Trust the consumer to send the correct full state. |
Returning null from load() on empty | Return []. Empty state is success, not absence. |
| Mixing embedding dimensions in one VectorMemory | Pick one model per store; reject mismatches at construction. |
Padding search results to reach topK | Return fewer documents β topK is an upper bound, not a floor. |
| Random embeddings or non-deterministic embedders | Cache by input; never inject randomness. EmbedFn must be stable. |
#Going deeper
The full list of invariants (six for ChatMemory, eight for VectorMemory, three for EmbedFn) is in ADR 0003 β Memory contract.
Explore nearby
- PeerConcepts
Six contracts. Every AgentsKit package is an implementation of one of them.
- PeerMental model
The six concepts every AgentsKit user should know.
- PeerAdapter
The seam between AgentsKit and an LLM provider. The same interface for OpenAI, Anthropic, Gemini, Ollama, or anything that streams tokens.