Claude
Skills
Sign in
Back

ai-memory

Included with Lifetime
$97 forever

Cross-session memory with context injection. Persists user preferences and facts to Postgres, auto-injects relevant memories into the system prompt, and exposes save/recall tools. Use this skill when the user says "add memory", "remember things", "persistent context", or "cross-session memory".

Backend & APIs

What this skill does


# AI Memory Skill

Persists user preferences and facts across chat sessions. Memories are stored in Postgres via Drizzle, auto-injected into the system prompt as context, and exposed as `saveMemory` and `recallMemory` tools the AI can invoke.

## Prerequisites

- `ai-chat` skill applied (provides `route.ts` with comment slots, DB setup with Drizzle)
- `ai-tools` skill applied (provides tool registration pattern in `route.ts`)
- `ai-core` skill applied (provides `getModel()`)
- PostgreSQL running (via Docker from the `docker` skill)

## Installation

No additional packages required. Uses `drizzle-orm` (already installed by `ai-chat`) and `tool`/`z` from `ai`/`zod` (already installed by `ai-core`).

## Environment Variables

No additional environment variables required. Uses the existing `DATABASE_URL` from the `docker` skill.

## What Gets Created

```
src/
├── db/
│   └── schema/
│       └── memory.ts              # memory table (userId, key, value, category)
├── lib/
│   └── ai/
│       ├── memory.ts              # getRelevantMemories() + saveMemoryRecord() helpers
│       └── tools/
│           └── memory.ts          # saveMemory + recallMemory tool definitions
```

Plus modifications to:

```
src/app/api/ai/chat/route.ts      # MODIFIED — inject memories + spread memory tools
src/db/schema/index.ts             # MODIFIED — re-export memory schema
```

## Comment Slots

- **route.ts**: `// [ai-memory]: append memory context here` — injects relevant memories into the system prompt
- **route.ts**: `// [ai-memory]: add saveMemory, recallMemory` — spreads memory tools into the tools object

## Setup Steps

### Step 1: Create `src/db/schema/memory.ts`

```typescript
import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";

export const memory = pgTable("memory", {
  id: uuid("id").defaultRandom().primaryKey(),
  userId: text("user_id").notNull(),
  key: text("key").notNull(),
  value: text("value").notNull(),
  category: text("category").notNull().default("general"),
  createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
  updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
});

export type Memory = typeof memory.$inferSelect;
export type NewMemory = typeof memory.$inferInsert;
```

### Step 2: Modify `src/db/schema/index.ts`

Add the memory schema export to the barrel file.

Find this in `src/db/schema/index.ts`:

```typescript
export * from "./chat";
```

Replace with:

```typescript
export * from "./chat";
export * from "./memory";
```

### Step 3: Create `src/lib/ai/memory.ts`

```typescript
import { eq, and, ilike, or, desc } from "drizzle-orm";
import { db } from "@/db";
import { memory, type Memory } from "@/db/schema/memory";

interface MemoryRecord {
  key: string;
  value: string;
  category: string;
}

export async function getRelevantMemories(
  userId: string,
  query: string,
): Promise<MemoryRecord[]> {
  const words = query
    .toLowerCase()
    .split(/\s+/)
    .filter((w) => w.length > 2);

  if (words.length === 0) {
    const recentMemories = await db
      .select()
      .from(memory)
      .where(eq(memory.userId, userId))
      .orderBy(desc(memory.updatedAt))
      .limit(10);

    return recentMemories.map((m) => ({
      key: m.key,
      value: m.value,
      category: m.category,
    }));
  }

  const searchConditions = words.map((word) =>
    or(
      ilike(memory.key, `%${word}%`),
      ilike(memory.value, `%${word}%`),
      ilike(memory.category, `%${word}%`),
    ),
  );

  const results = await db
    .select()
    .from(memory)
    .where(
      and(
        eq(memory.userId, userId),
        or(...searchConditions),
      ),
    )
    .orderBy(desc(memory.updatedAt))
    .limit(20);

  return results.map((m) => ({
    key: m.key,
    value: m.value,
    category: m.category,
  }));
}

export async function saveMemoryRecord(
  userId: string,
  key: string,
  value: string,
  category: string = "general",
): Promise<Memory> {
  const existing = await db
    .select()
    .from(memory)
    .where(
      and(
        eq(memory.userId, userId),
        eq(memory.key, key),
      ),
    )
    .limit(1);

  if (existing.length > 0) {
    const [updated] = await db
      .update(memory)
      .set({
        value,
        category,
        updatedAt: new Date(),
      })
      .where(eq(memory.id, existing[0].id))
      .returning();

    return updated;
  }

  const [created] = await db
    .insert(memory)
    .values({
      userId,
      key,
      value,
      category,
    })
    .returning();

  return created;
}

export async function deleteMemoryRecord(
  userId: string,
  key: string,
): Promise<boolean> {
  const result = await db
    .delete(memory)
    .where(
      and(
        eq(memory.userId, userId),
        eq(memory.key, key),
      ),
    )
    .returning();

  return result.length > 0;
}

export async function listMemories(
  userId: string,
  category?: string,
): Promise<MemoryRecord[]> {
  const conditions = [eq(memory.userId, userId)];
  if (category) {
    conditions.push(eq(memory.category, category));
  }

  const results = await db
    .select()
    .from(memory)
    .where(and(...conditions))
    .orderBy(desc(memory.updatedAt))
    .limit(50);

  return results.map((m) => ({
    key: m.key,
    value: m.value,
    category: m.category,
  }));
}
```

### Step 4: Create `src/lib/ai/tools/memory.ts`

```typescript
import { tool } from "ai";
import { z } from "zod";
import {
  saveMemoryRecord,
  getRelevantMemories,
  deleteMemoryRecord,
  listMemories,
} from "@/lib/ai/memory";

interface SaveMemoryResult {
  key: string;
  value: string;
  category: string;
  success: true;
}

interface RecallMemoryResult {
  query: string;
  memories: { key: string; value: string; category: string }[];
  count: number;
  success: true;
}

interface DeleteMemoryResult {
  key: string;
  deleted: boolean;
  success: true;
}

interface ListMemoriesResult {
  category: string | undefined;
  memories: { key: string; value: string; category: string }[];
  count: number;
  success: true;
}

export function createMemoryTools(userId: string) {
  const saveMemory = tool({
    description:
      "Save a fact, preference, or piece of information about the user for future reference. " +
      "Use this when the user shares personal info, preferences, or anything worth remembering across sessions. " +
      "The key should be a short, descriptive label. The value is the actual information. " +
      "Categories: 'personal' (name, location), 'preference' (likes, dislikes), 'work' (job, projects), 'general' (other).",
    inputSchema: z.object({
      key: z
        .string()
        .describe("Short descriptive label for this memory, e.g. 'name', 'favorite_color', 'job_title'"),
      value: z
        .string()
        .describe("The information to remember, e.g. 'Matt', 'blue', 'software engineer'"),
      category: z
        .enum(["personal", "preference", "work", "general"])
        .default("general")
        .describe("Category for organizing memories"),
    }),
    execute: async ({ key, value, category }): Promise<SaveMemoryResult> => {
      await saveMemoryRecord(userId, key, value, category);
      return { key, value, category, success: true };
    },
  });

  const recallMemory = tool({
    description:
      "Search the user's saved memories for relevant information. " +
      "Use this when the user asks about something they previously told you, " +
      "or when you need context about their preferences, personal info, or past conversations.",
    inputSchema: z.object({
      query: z
        .string()
        .describe("Search query to find relevant memories, e.g. 'name', 'favorite', 'work'"),
    }),
    execute: async ({ query }): Promise<RecallMemoryResult> => {
      const memories = await getRelevantMemories(userId, query);
      return {
        query,
        memories,
        count: memories.length,
        success: tru
Files: 1
Size: 13.5 KB
Complexity: 23/100
Category: Backend & APIs

Related in Backend & APIs