Skip to main content
Every agent that spans more than one request needs a place to remember things — a user’s name, the last tool result, a running total. Anvil’s MemoryStore gives you a typed, namespaced key-value store attached directly to the request context so you never have to thread raw store handles through your handler code.

How it works

MemoryStore wraps any StateStore backend and scopes every key to a namespace, so two sessions never collide. The withMemory middleware creates a fresh MemoryStore for each incoming request and writes it to ctx.state.memory. Your handler reads and writes memory without knowing anything about the underlying store.
Request → withMemory middleware → ctx.state.memory (MemoryStore) → your handler
The default backend is SQLite (via SqliteStateStore). You can swap in any implementation of the StateStore interface — see Custom backends below.

Quick start

1

Open (or create) a SQLite state store

import { SqliteStateStore } from 'anvil/store';

const store = await SqliteStateStore.open(); // writes to .anvil/state.db
2

Register the withMemory middleware

Apply withMemory to any router or route that needs per-session memory.
import { withMemory } from 'anvil/memory';
import { createApp } from 'anvil';

const app = createApp();
app.use(withMemory(store));
3

Read and write from your handler

Access the MemoryStore through ctx.state.memory (or use the getMemory helper for typed access).
import { getMemory } from 'anvil/memory';

app.post('/chat', async (ctx) => {
  const memory = getMemory(ctx)!;

  // Remember the user's name the first time they introduce themselves
  const name = await memory.get<string>('userName');
  if (!name) {
    await memory.set('userName', 'Alice');
  }

  // Append to a conversation log
  await memory.append('history', { role: 'user', text: ctx.body.message });
});

Namespacing

Each MemoryStore instance is scoped to a namespace so that different sessions or users never read each other’s data. By default withMemory derives the namespace from the x-session-id request header, falling back to 'default' when the header is absent. Pass a namespace function to compute a namespace from any request property:
// Use the authenticated user ID as the namespace
app.use(withMemory(store, (ctx) => ctx.state.userId ?? 'anonymous'));

// Use a query parameter
app.use(withMemory(store, (ctx) => ctx.query.get('sessionId') ?? 'default'));
Use a stable, user-scoped identifier as the namespace when you want memory to persist across browser sessions or devices.

MemoryStore API

memory.get<T>(key)

Returns the stored value cast to T, or undefined if the key does not exist.
const name = await memory.get<string>('userName');
// → 'Alice' | undefined

memory.set(key, value)

Stores any JSON-serializable value under key. Overwrites any existing value.
await memory.set('preferences', { theme: 'dark', language: 'en' });

memory.append<T>(key, item)

Pushes item onto the end of the array stored at key. If the key does not exist, creates a new single-element array. Returns the new array length.
const length = await memory.append<ChatMessage>('history', {
  role: 'user',
  text: 'Hello!',
});
// → 1 (first message in the array)

memory.delete(key)

Removes the key from the namespace. No-op if the key does not exist.
await memory.delete('temporaryContext');

memory.keys()

Returns all keys registered under the current namespace. Useful for inspecting or clearing state.
const keys = await memory.keys();
// → ['userName', 'history', 'preferences']

Full example: remembering a user’s name

The following route greets a returning user by name and saves new users for future requests.
import { createApp } from 'anvil';
import { withMemory, getMemory } from 'anvil/memory';
import { SqliteStateStore } from 'anvil/store';

const store = await SqliteStateStore.open();
const app = createApp();

// Derive namespace from x-session-id header (the default behaviour)
app.use(withMemory(store));

app.post('/greet', async (ctx) => {
  const memory = getMemory(ctx)!;
  const { name } = await ctx.json<{ name?: string }>();

  if (name) {
    await memory.set('userName', name);
  }

  const stored = await memory.get<string>('userName');
  const greeting = stored
    ? `Welcome back, ${stored}!`
    : 'Hello, stranger! Tell me your name.';

  return ctx.json({ greeting });
});

export default app;
Pass the same x-session-id header with every request from a client to keep memory consistent across the conversation.

Custom backends

MemoryStore works with any object that satisfies the StateStore interface. You can provide Redis, Postgres, or any other persistence layer:
import type { StateStore } from 'anvil/store';

class RedisStateStore implements StateStore {
  async get<T>(key: string): Promise<T | undefined> {
    const raw = await redis.get(key);
    return raw ? (JSON.parse(raw) as T) : undefined;
  }

  async set(key: string, value: unknown): Promise<void> {
    await redis.set(key, JSON.stringify(value));
  }

  async delete(key: string): Promise<void> {
    await redis.del(key);
  }

  async list(prefix?: string): Promise<string[]> {
    return redis.keys(prefix ? `${prefix}*` : '*');
  }
}

app.use(withMemory(new RedisStateStore()));
The StateStore interface requires four methods:
MethodSignatureDescription
getget<T>(key): Promise<T | undefined>Retrieve a value by key
setset(key, value): Promise<void>Persist any JSON-serializable value
deletedelete(key): Promise<void>Remove a key
listlist(prefix?): Promise<string[]>List keys, optionally filtered by prefix
For development and tests, use the zero-dependency MemoryStateStore from anvil/store. It stores values in-process and requires no native dependencies.