Memory System

Prisma Setup

Liz uses Prisma as its ORM, supporting both SQLite and PostgreSQL databases. The schema defines the structure for storing memories and tweets.

// prisma/schema.prisma
datasource db {
  provider = "sqlite" // or "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Memory {
  id          String   @id @default(uuid())
  userId      String
  agentId     String
  roomId      String
  content     String   // Stores JSON as string
  type        String
  generator   String   // "llm" or "external"
  createdAt   DateTime @default(now())

  @@index([roomId])
  @@index([userId, agentId])
  @@index([type])
}

model Tweet {
  id             String    @id
  text           String
  userId         String
  username       String
  conversationId String?
  inReplyToId    String?
  createdAt      DateTime  @default(now())
  permanentUrl   String?
  
  @@index([userId])
  @@index([conversationId])
}

Loading Memories

The loadMemories middleware retrieves relevant conversation history for each request:

// src/middleware/load-memories.ts
export function createLoadMemoriesMiddleware(
  options: LoadMemoriesOptions = {}
): AgentMiddleware {
  const { limit = 100 } = options;

  return async (req, res, next) => {
    const memories = await prisma.memory.findMany({
      where: {
        userId: req.input.userId,
      },
      orderBy: {
        createdAt: "desc",
      },
      take: limit,
    });

    req.memories = memories.map((memory) => ({
      id: memory.id,
      userId: memory.userId,
      agentId: memory.agentId,
      roomId: memory.roomId,
      type: memory.type,
      createdAt: memory.createdAt,
      generator: memory.generator,
      content: JSON.parse(memory.content),
    }));

    await next();
  };
}

Creating Memories

The createMemoryFromInput middleware stores new interactions in the database:

// src/middleware/create-memory.ts
export const createMemoryFromInput: AgentMiddleware = async (
  req,
  res,
  next
) => {
  await prisma.memory.create({
    data: {
      userId: req.input.userId,
      agentId: req.input.agentId,
      roomId: req.input.roomId,
      type: req.input.type,
      generator: "external",
      content: JSON.stringify(req.input),
    },
  });

  await next();
};

// Creating LLM response memories
await prisma.memory.create({
  data: {
    userId: req.input.userId,
    agentId: req.input.agentId,
    roomId: req.input.roomId,
    type: "agent",
    generator: "llm",
    content: JSON.stringify({ text: response }),
  },
});

Memory Context

The wrapContext middleware formats memories into a structured context for LLM interactions:

// src/middleware/wrap-context.ts
function formatMemories(memories: Memory[]): string {
  return memories
    .reverse()
    .map((memory) => {
      const content = memory.content;
      if (memory.generator === "external") {
        return `[${memory.createdAt}] User ${memory.userId}: ${content.text}`;
      } else if (memory.generator === "llm") {
        return `[${memory.createdAt}] You: ${content.text}`;
      }
    })
    .join("\n\n");
}

// Final context structure
<PREVIOUS_CONVERSATION>
${memories}
</PREVIOUS_CONVERSATION>

<AGENT_CONTEXT>
${agentContext}
</AGENT_CONTEXT>

<CURRENT_USER_INPUT>
${currentInput}
</CURRENT_USER_INPUT>

Performance Considerations

Memory Limits

  • Default limit of 100 recent memories
  • Configurable through middleware options
  • Consider token limits of your LLM
  • Use indexes for faster queries

Database Tips

  • SQLite for development/small apps
  • PostgreSQL for production/scale
  • Regular database maintenance
  • Monitor memory table growth