Skip to content

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:

  1. Embed the query: "Margaret's thoughts about Tom"
  2. Query Vectorize for top-K most similar memories
  3. 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:

ts
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.

ts
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 ​

ts
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:

ts
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:

ts
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 ​

FailureHandling
Embedding API failsMemory is still stored in D1; we retry embedding later
Vectorize query returns emptyDecision module degrades to short-term memory only
Index size limit reachedAggressive 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.

Last updated:

Released under the MIT License.