Vectorize
Long-term semantic memory. "What did Margaret think about Tom?" — answered by meaning, not by keyword.
What is Vectorize?
Cloudflare Vectorize is a serverless vector database, integrated with Workers.
For AI World Sim, it stores long-term NPC memories as embeddings. When the simulation needs to recall relevant past events for an NPC, it queries by semantic similarity.
Why Vector Memory?
D1 stores memory chronologically. To find "what did Margaret think about Tom?" with D1 alone, you'd have to scan all her diaries.
With Vectorize:
- Embed the query: "Margaret's thoughts about Tom"
- Query Vectorize for top-K most similar memories
- Get back semantically relevant memories instantly
This mirrors how human memory works — we recall by meaning, not by date.
Index Structure
We use one Vectorize index for the entire NPC memory corpus, with metadata filtering per NPC.
Metadata Schema
Each vector has metadata:
interface VectorMetadata {
npcId: string;
type: 'event' | 'relationship' | 'opinion' | 'place';
day: number;
importance: number;
decayFactor: number;
textPreview: string;
}Embedding Model
We use OpenAI's text-embedding-3-small (1536 dimensions) by default. Optionally, Workers AI's @cf/baai/bge-base-en-v1.5 (768 dimensions) for cost-free local embeddings.
When Memory is Embedded
1. Real-time (during tick)
When a high-importance short-term memory is created (importance > 0.7), it's immediately embedded and upserted to Vectorize.
async function indexMemory(memory: ShortTermMemory, env: Env) {
const embedding = await embed(memory.content, env);
await env.VECTORIZE.upsert([{
id: memory.id,
values: embedding,
metadata: {
npcId: memory.npcId,
type: 'event',
day: memory.worldTime.day,
importance: memory.importance,
decayFactor: 0,
textPreview: memory.content.slice(0, 200),
},
}]);
}2. Background consolidation (every 6 hours)
Older daily digests are summarized into 1-2 sentences and embedded.
Retrieval API
Simple query
async function recall(npcId: string, query: string, env: Env, topK = 5) {
const queryEmbedding = await embed(query, env);
const results = await env.VECTORIZE.query(queryEmbedding, {
topK,
filter: { npcId },
returnMetadata: true,
});
return results.matches.map(m => ({
text: m.metadata.textPreview,
importance: m.metadata.importance,
day: m.metadata.day,
score: m.score,
}));
}Weighted query
For decision-making, we apply decay weighting:
function weightedRecall(matches: Match[], currentDay: number) {
return matches
.map(m => ({
...m,
effectiveScore: m.score * m.metadata.importance / (1 + (currentDay - m.metadata.day) * m.metadata.decayFactor),
}))
.sort((a, b) => b.effectiveScore - a.effectiveScore);
}Use Cases
1. Decision Context
When deciding what to do, the NPC queries relevant memories:
const context = await recall(
npcId,
`${npc.name} is deciding whether to go to the pub tonight`,
env
);2. Diary Generation
When writing a diary, relevant past events are surfaced.
3. Relationship Context
When two NPCs interact, each recalls their history with the other.
Decay & Pruning
Memory is not immortal. Two mechanisms:
Decay factor
Every memory has a decayFactor that grows over time. During retrieval, decay reduces effective score.
Active pruning
A daily job queries all memories with decayFactor > 0.9. Re-embeds important ones, deletes the rest.
Failure Modes
| Failure | Handling |
|---|---|
| Embedding API fails | Memory is still stored in D1; we retry embedding later |
| Vectorize query returns empty | Decision module degrades to short-term memory only |
| Index size limit reached | Aggressive pruning + older memories kept only in D1 |
What's Not Stored Here
- ❌ Raw diary text (in D1)
- ❌ Short-term memory (in D1)
- ❌ Current NPC state (in D1)
- ❌ World events (in D1)
Only semantic memory lives in Vectorize.