Almost every guide to agent memory is a guide to which store to bolt on — a vector database, Mem0, Letta, or Zep, a SQLite file vs a service. All of them assume the same shape: the agent is one thing, its memory is another thing somewhere else, and you wire them together.

Cloudflare's Agents SDK quietly offers a different shape. Because each agent instance is a Durable Object — a single-threaded stateful micro-server with compute and its own SQLite database attached — memory doesn't live somewhere else. It lives inside the agent, on the same machine that runs its logic. There's nothing to provision. This walkthrough takes you from an empty Worker to an agent that remembers across sessions, and flags the one thing this shape can't do.

The mental model: two tiers of memory in one object#

A Cloudflare agent has two places to put memory, and picking the right one is the whole skill:

Small and always-needed goes in state. Large or queried-on-demand goes in SQL. That's the same core-vs-archival split MemGPT made famous — except here both tiers live in the same object, not across a network.

1. Define the agent and its working state#

Install the SDK (npm install agents) and extend the Agent class. initialState seeds the fast tier:

import { Agent, callable } from "agents";

type MemoryState = {
  displayName?: string;
  recent: string[];        // last few turns, kept small on purpose
};

export class MemoryAgent extends Agent<Env, MemoryState> {
  initialState: MemoryState = { recent: [] };

  @callable()
  setName(name: string) {
    this.setState({ ...this.state, displayName: name });  // persists + syncs to clients
    return this.state.displayName;
  }
}

this.setState survives requests and hibernation, so the next time this user's agent wakes, this.state.displayName is still there — no load step, no cache to warm.

2. Create the durable table (once)#

The SQLite database ships with the object; you just declare your schema. Do it in onStart() so it's ready before any request touches it:

  async onStart() {
    this.sql`
      CREATE TABLE IF NOT EXISTS messages (
        id      INTEGER PRIMARY KEY AUTOINCREMENT,
        role    TEXT NOT NULL,
        content TEXT NOT NULL,
        ts      INTEGER NOT NULL
      )`;
  }

No connection string, no pool, no migration service. The IF NOT EXISTS makes it idempotent across cold starts.

3. Write and read the long-term log#

this.sql is a tagged template — interpolations are bound as parameters, not string-concatenated, so it's injection-safe by construction:

  @callable()
  remember(role: string, content: string) {
    this.sql`
      INSERT INTO messages (role, content, ts)
      VALUES (${role}, ${content}, ${Date.now()})`;

    // keep the fast tier small: mirror only the last 8 turns into state
    const recent = [...this.state.recent, content].slice(-8);
    this.setState({ ...this.state, recent });
  }

  @callable()
  history(limit = 50) {
    return this.sql`
      SELECT role, content, ts FROM messages
      ORDER BY ts DESC LIMIT ${limit}`;   // returns an array of rows
  }

The pattern that keeps agents fast: the working set lives in synced state and is always in hand; the full record lives in SQL and is fetched only when you need to look back. That directly avoids the failure mode where an agent's memory rots because everything is crammed into the context window.

4. Let the agent maintain itself#

Because the object has compute, it can act on its own memory on a schedule — prune old rows, summarize a long session into a compact note, expire stale facts:

  @callable()
  scheduleNightlyCompaction() {
    this.schedule("0 3 * * *", "compact");   // cron: 03:00 daily
  }

  async compact() {
    const old = this.sql`SELECT count(*) AS n FROM messages`;
    // ...summarize + delete rows older than N days...
  }

this.schedule supports one-time, recurring, and cron tasks, and they survive hibernation — the agent wakes to run them and goes back to sleep.

The one thing this shape can't do#

A Durable Object gives you exact recall — by key, id, timestamp, or a LIKE filter — and nothing more. There is no built-in vector search. The question every long-running agent eventually asks — "what did this user tell me about their deploy setup three weeks ago?" — is a semantic query, and SQL can't answer it by meaning.

So the honest framing is: **Durable Objects are an excellent place to keep an agent's memory and a poor place to search it by meaning without help.** When you need similarity recall, you add embeddings yourself — store vectors in Cloudflare Vectorize, or, for a tiny per-agent set, keep a brute-force cosine scan inside the object. If semantic recall is the primary access pattern across many users, that's exactly when a bolt-on layer like Mem0 or a shared vector service earns its keep instead.

When to reach for it#

Same rule as always with state: don't run a server until you have to. Durable Objects let you not run one at all.

One caveat before you build on it: the Agents SDK is a fast-moving 0.x. Pin your version, read the changelog before you upgrade, and expect the surface to shift month to month. The shape is right; the API isn't frozen yet.