Examples
Memory and Events
How agent memory and event stores differ, how each maps to database storage, and how they are used together in production.
Production agents often use both memory and event storage, but they solve different problems. Memory is conversation context for future prompts. Event storage is an operational log of what happened during one streamed run.
Do not merge them into one transcript table. A memory store answers “what should the model remember next time?” An event store answers “what happened during run run_123?”
What Each Store Does
| Store | Key | Contract | Contains | Used for |
|---|---|---|---|---|
| memory | sessionId, plus user or tenant scope |
MemoryStore.load(context): Promise<Message[]> |
ordered Anvia Message[] values |
future prompts, durable conversations, session.messages() |
| event store | runId |
AgentEventStore.load(runId): Promise<AgentEventRecord[]> |
ordered stream events with raw event JSON |
replay, debugging, audit review, operations views |
Memory is loaded before the next run and becomes model context. Event records are not loaded into prompts; they are inspected by humans, eval tooling, dashboards, or replay/debug flows.
Production Layout
Keep these as separate storage paths:
| Data | Typical table | Why it exists |
|---|---|---|
| memory sessions | agent_memory_sessions |
owns product session scope, tenant/user ownership, lifecycle metadata, and cascade cleanup |
| memory messages | agent_memory_messages |
gives the next prompt prior user, assistant, and tool-result messages |
| runtime events | agent_events |
preserves turn starts, deltas, tool calls, tool results, nested agent events, final output, and errors by run id |
| product run record | support_runs or agent_runs |
compact user-facing status, final output, trace id, and usage |
| audit records | audit_logs |
records who requested or performed sensitive product actions |
| traces | tracing backend or trace id fields | connects the run to model/provider/tool observability |
Memory session rows are scoped by product-owned session, user, and tenant values. Memory message rows keep the ordered Anvia Message JSON used as future prompt context. Event rows are scoped by run id and agent metadata. Product run records usually connect both worlds by storing conversationId, runId, traceId, final output, and usage.
Prisma Memory Setup
For a Prisma app, use @anvia/memory-prisma as the first-class memory adapter. It provides the store implementation and a safe schema generator, so the application does not have to design the Anvia memory tables by hand.
pnpm add @anvia/memory-prisma @anvia/core @prisma/client
npx @anvia/memory-prisma init
npx @anvia/memory-prisma init --write
npx prisma validate
npx prisma migrate dev --name add_anvia_memory
Then create the memory store from your Prisma client:
import { createPrismaMemoryStore } from "@anvia/memory-prisma";
import { prisma } from "./db";
export const memoryStore = createPrismaMemoryStore(prisma, {
scope: {
metadataKeys: ["tenantId"],
},
});
The scope option defines the database identity for one memory thread. By default the key includes sessionId and userId; metadataKeys: ["tenantId"] also includes metadata.tenantId. That means conversation_123 for tenant_a and conversation_123 for tenant_b load different memory.
The store persists full Anvia messages. Product-facing conversation tables can still exist, but they should reference or summarize the agent session instead of replacing the memory store.
Typical Flow
import { AgentBuilder } from "@anvia/core";
import { createPrismaMemoryStore } from "@anvia/memory-prisma";
export function createSupportAgent(scope: SupportAgentScope) {
const memoryStore = createPrismaMemoryStore(scope.prisma, {
scope: { metadataKeys: ["tenantId"] },
});
return new AgentBuilder("support", scope.model)
.instructions("Answer support questions using tools and policy context.")
.tools(scope.tools)
.memory(memoryStore, { savePolicy: "turn" })
.eventStore(scope.eventStore, { include: "all" })
.build();
}
export async function runSupportTurn(input: SupportTurnInput) {
const user = await input.auth.requireUser();
const agent = createSupportAgent({
prisma: input.prisma,
model: input.model,
tools: input.tools,
eventStore: input.eventStore,
});
let final;
for await (const event of agent
.session(input.conversationId, {
userId: user.id,
metadata: { tenantId: user.tenantId },
})
.prompt(input.message)
.withTrace({
name: "support-chat",
userId: user.id,
metadata: {
tenantId: user.tenantId,
conversationId: input.conversationId,
},
})
.stream()) {
if (event.type === "final") {
final = event;
}
}
if (final === undefined) {
throw new Error("Agent stream ended without a final event.");
}
await input.runs.record({
conversationId: input.conversationId,
runId: final.runId,
traceId: final.trace?.traceId,
output: final.output,
usage: final.usage,
});
return { output: final.output, runId: final.runId };
}
The memory store loads prior messages before the run and saves the completed turn. The event store records events while the stream is consumed. If you need replayable events, use .stream() and drain it even when your API only returns the final answer. Use .send() when the caller only needs the final response and event replay is not required.
Loading Initial Messages
When the user opens an existing conversation, do not rebuild the transcript from event logs. Load the stored memory thread:
export async function loadSupportConversation(input: LoadConversationInput) {
const user = await input.auth.requireUser();
const agent = createSupportAgent({
prisma: input.prisma,
model: input.model,
tools: input.tools,
eventStore: input.eventStore,
});
const messages = await agent
.session(input.conversationId, {
userId: user.id,
metadata: { tenantId: user.tenantId },
})
.messages();
return { messages };
}
Return those messages from a GET route as core Anvia Message[]. On the React page, convert them with initialMessagesFromMemory(messages) and pass the result to useChat({ initialMessages }). initialMessages is only UI hydration; future prompts should still go through the memory-backed session so the server can load authoritative history.
Choosing The Adapter
Use Prisma as the default memory example when your app already uses Prisma:
| Need | First-class path | Notes |
|---|---|---|
| durable memory with Prisma | @anvia/memory-prisma |
Generates the Prisma models and implements MemoryStore. |
| durable memory with another database layer | @anvia/memory-drizzle, @anvia/memory-postgres, or @anvia/memory-sqlite |
Use the package that matches the app’s storage layer. |
| replayable runtime events with Prisma | Prisma Agent Event Store | Event stores are still app-owned because event indexing and retention usually follow product operations needs. |
| replayable runtime events with another database layer | Drizzle Agent Event Store or Raw SQL Agent Event Store | Keep event storage separate from memory. |
The adapter choice should not change the runtime contract. Memory still returns Message[]; event storage still returns AgentEventRecord[].
Production Checks
- Keep memory, events, audit logs, traces, and product state in separate tables or services.
- Apply tenant and user scope to memory reads and writes.
- Store event payloads as JSON/JSONB and index
runId,agentId,turn, and tool identifiers. - Set retention and redaction policies for both memory and events; event payloads may include prompts, history, tool args, tool results, reasoning, provider metadata, and errors.
- Store a compact product run record for UI status instead of querying event logs for normal user-facing pages.
