Spaces:
Running
Running
| /** | |
| * AI Town β Memory System (Core of the Agent Architecture) | |
| * ========================================================== | |
| * Implements the Stanford "Generative Agents" memory stream: | |
| * - Store observations with importance + embedding | |
| * - Retrieve by: recency Γ importance Γ relevance (cosine similarity) | |
| * - Trigger reflection when cumulative importance > threshold | |
| */ | |
| import { v } from "convex/values"; | |
| import { mutation, query, action, internalAction } from "./_generated/server"; | |
| import { internal } from "./_generated/api"; | |
| // βββ STORE NEW MEMORY βββ | |
| export const addMemory = mutation({ | |
| args: { | |
| characterId: v.id("characters"), | |
| description: v.string(), | |
| type: v.string(), | |
| importance: v.number(), | |
| embedding: v.array(v.float64()), | |
| participants: v.optional(v.array(v.string())), | |
| }, | |
| handler: async (ctx, args) => { | |
| const now = Date.now(); | |
| return await ctx.db.insert("memories", { | |
| characterId: args.characterId, | |
| description: args.description, | |
| type: args.type, | |
| importance: args.importance, | |
| embedding: args.embedding, | |
| participants: args.participants, | |
| createdAt: now, | |
| lastAccessed: now, | |
| }); | |
| }, | |
| }); | |
| // βββ RETRIEVE RELEVANT MEMORIES (RAG) βββ | |
| export const retrieveMemories = action({ | |
| args: { | |
| characterId: v.id("characters"), | |
| queryEmbedding: v.array(v.float64()), | |
| limit: v.optional(v.number()), | |
| }, | |
| handler: async (ctx, args) => { | |
| const limit = args.limit || 10; | |
| // Vector search for semantically relevant memories | |
| const results = await ctx.vectorSearch("memories", "by_embedding", { | |
| vector: args.queryEmbedding, | |
| limit: limit * 2, // Get extra for scoring | |
| filter: (q) => q.eq("characterId", args.characterId), | |
| }); | |
| // Score each memory: recency Γ importance Γ relevance | |
| const now = Date.now(); | |
| const scored = results.map((result) => { | |
| const hoursSinceAccess = (now - result.lastAccessed) / (1000 * 60 * 60); | |
| const recencyScore = Math.pow(0.995, hoursSinceAccess); | |
| const importanceScore = result.importance / 10; | |
| const relevanceScore = result._score; // cosine similarity from vector search | |
| return { | |
| ...result, | |
| finalScore: recencyScore + importanceScore + relevanceScore, | |
| }; | |
| }); | |
| // Sort by combined score and return top N | |
| scored.sort((a, b) => b.finalScore - a.finalScore); | |
| const topMemories = scored.slice(0, limit); | |
| // Update lastAccessed for retrieved memories | |
| for (const mem of topMemories) { | |
| await ctx.runMutation(internal.memory.touchMemory, { memoryId: mem._id }); | |
| } | |
| return topMemories; | |
| }, | |
| }); | |
| // Internal: update lastAccessed | |
| export const touchMemory = mutation({ | |
| args: { memoryId: v.id("memories") }, | |
| handler: async (ctx, { memoryId }) => { | |
| await ctx.db.patch(memoryId, { lastAccessed: Date.now() }); | |
| }, | |
| }); | |
| // βββ GET RECENT MEMORIES (short-term buffer) βββ | |
| export const getRecentMemories = query({ | |
| args: { | |
| characterId: v.id("characters"), | |
| limit: v.optional(v.number()), | |
| }, | |
| handler: async (ctx, args) => { | |
| return await ctx.db | |
| .query("memories") | |
| .withIndex("by_character", (q) => q.eq("characterId", args.characterId)) | |
| .order("desc") | |
| .take(args.limit || 20); | |
| }, | |
| }); | |
| // βββ CHECK REFLECTION THRESHOLD βββ | |
| export const shouldReflect = query({ | |
| args: { characterId: v.id("characters") }, | |
| handler: async (ctx, { characterId }) => { | |
| // Get memories since last reflection | |
| const lastReflection = await ctx.db | |
| .query("memories") | |
| .withIndex("by_character_type", (q) => | |
| q.eq("characterId", characterId).eq("type", "reflection") | |
| ) | |
| .order("desc") | |
| .first(); | |
| const since = lastReflection?.createdAt || 0; | |
| const recentMemories = await ctx.db | |
| .query("memories") | |
| .withIndex("by_character", (q) => q.eq("characterId", characterId)) | |
| .order("desc") | |
| .take(100); | |
| const newMemories = recentMemories.filter((m) => m.createdAt > since); | |
| const importanceSum = newMemories.reduce((sum, m) => sum + m.importance, 0); | |
| // Threshold: 150 (from Stanford paper) | |
| return { shouldReflect: importanceSum >= 150, importanceSum, memoryCount: newMemories.length }; | |
| }, | |
| }); | |
| // βββ GET MEMORY COUNT βββ | |
| export const getMemoryCount = query({ | |
| args: { characterId: v.id("characters") }, | |
| handler: async (ctx, { characterId }) => { | |
| const memories = await ctx.db | |
| .query("memories") | |
| .withIndex("by_character", (q) => q.eq("characterId", characterId)) | |
| .collect(); | |
| return memories.length; | |
| }, | |
| }); | |