Sessions·续2

// Read-only context
const myProvider = {
    get: async () => "Static content here",
};

// Writable context (enables set_context tool)
const myWritable = {
    get: async () => fetchFromMyDB(),
    set: async (content) => saveToMyDB(content),
};

// Skill provider (enables load_context tool)
const mySkills = {
    get: async () => "- api-ref: API Reference\n- guide: User Guide",
    load: async (key) => fetchDocument(key),
    set: async (key, content, description) =>
        saveDocument(key, content, description),
};

// Search provider (enables search_context tool)
const mySearch = {
    get: async () => "42 entries indexed",
    search: async (query) => searchMyIndex(query),
    set: async (key, content) => indexContent(key, content),
};
// Read-only context
const myProvider: ContextProvider = {
    get: async () => "Static content here",
};

// Writable context (enables set_context tool)
const myWritable: WritableContextProvider = {
    get: async () => fetchFromMyDB(),
    set: async (content) => saveToMyDB(content),
};

// Skill provider (enables load_context tool)
const mySkills: SkillProvider = {
    get: async () => "- api-ref: API Reference\n- guide: User Guide",
    load: async (key) => fetchDocument(key),
    set: async (key, content, description) =>
        saveDocument(key, content, description),
};

// Search provider (enables search_context tool)
const mySearch: SearchProvider = {
    get: async () => "42 entries indexed",
    search: async (query) => searchMyIndex(query),
    set: async (key, content) => indexContent(key, content),
};

You can also implement SessionProvider to replace the SQLite storage entirely:

const myStorage = {
    async getMessage(id) {
        /* ... */
    },
    async getHistory(leafId) {
        /* ... */
    },
    async getLatestLeaf() {
        /* ... */
    },
    async getBranches(messageId) {
        /* ... */
    },
    async getPathLength(leafId) {
        /* ... */
    },
    async appendMessage(message, parentId) {
        /* ... */
    },
    async updateMessage(message) {
        /* ... */
    },
    async deleteMessages(messageIds) {
        /* ... */
    },
    async clearMessages() {
        /* ... */
    },
    async addCompaction(summary, fromId, toId) {
        /* ... */
    },
    async getCompactions() {
        /* ... */
    },
    async searchMessages(query, limit) {
        /* ... */
    },
};
const myStorage: SessionProvider = {
    async getMessage(id) {
        /* ... */
    },
    async getHistory(leafId?) {
        /* ... */
    },
    async getLatestLeaf() {
        /* ... */
    },
    async getBranches(messageId) {
        /* ... */
    },
    async getPathLength(leafId?) {
        /* ... */
    },
    async appendMessage(message, parentId?) {
        /* ... */
    },
    async updateMessage(message) {
        /* ... */
    },
    async deleteMessages(messageIds) {
        /* ... */
    },
    async clearMessages() {
        /* ... */
    },
    async addCompaction(summary, fromId, toId) {
        /* ... */
    },
    async getCompactions() {
        /* ... */
    },
    async searchMessages(query, limit) {
        /* ... */
    },
};

Postgres providers

By default, Session storage uses Durable Object SQLite and creates tables lazily. If you need session data in an external Postgres database for cross-agent queries, analytics, or shared storage, use PostgresSessionProvider, PostgresContextProvider, and PostgresSearchProvider.

These providers work with Postgres-compatible databases through Hyperdrive for connection pooling.

1. Create a Hyperdrive config

Create a Hyperdrive config for your Postgres database:

npx wrangler hyperdrive create my-session-db \
    --connection-string="postgresql://user:password@host:port/dbname"

Then add the Hyperdrive binding to wrangler.jsonc:

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "compatibility_flags": [
    "nodejs_compat"
  ],
  "hyperdrive": [
    {
      "binding": "HYPERDRIVE",
      "id": "<your-hyperdrive-id>"
    }
  ],
  "placement": {
    "mode": "smart"
  }
}
compatibility_flags = ["nodejs_compat"]

[[hyperdrive]]
binding = "HYPERDRIVE"
id = "<your-hyperdrive-id>"

[placement]
mode = "smart"

If you know your database region, configure placement close to the database to reduce query latency.

2. Create the tables

The Postgres user may not have permission to create tables at runtime. Run the schema once in your database console:

CREATE TABLE IF NOT EXISTS assistant_messages (
    id TEXT NOT NULL,
    session_id TEXT NOT NULL DEFAULT '',
    parent_id TEXT,
    role TEXT NOT NULL,
    content TEXT NOT NULL,
    text_content TEXT NOT NULL DEFAULT '',
    created_at TIMESTAMPTZ DEFAULT NOW(),
    content_tsv TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', text_content)) STORED,
    PRIMARY KEY (session_id, id)
);

CREATE INDEX IF NOT EXISTS idx_assistant_msg_parent
    ON assistant_messages (parent_id);
CREATE INDEX IF NOT EXISTS idx_assistant_msg_session
    ON assistant_messages (session_id);
CREATE INDEX IF NOT EXISTS idx_assistant_msg_fts
    ON assistant_messages USING GIN (content_tsv);

CREATE TABLE IF NOT EXISTS assistant_compactions (
    id TEXT PRIMARY KEY,
    session_id TEXT NOT NULL DEFAULT '',
    summary TEXT NOT NULL,
    from_message_id TEXT NOT NULL,
    to_message_id TEXT NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS cf_agents_context_blocks (
    label TEXT PRIMARY KEY,
    content TEXT NOT NULL,
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS cf_agents_search_entries (
    label TEXT NOT NULL,
    key TEXT NOT NULL,
    content TEXT NOT NULL,
    content_tsv TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', content)) STORED,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),
    PRIMARY KEY (label, key)
);

CREATE INDEX IF NOT EXISTS idx_search_entries_fts
    ON cf_agents_search_entries USING GIN (content_tsv);

3. Wire it up

Install pg, then create a client from the Hyperdrive connection string and pass it to the Postgres providers:

npmyarnpnpmbun

npm i pg
yarn add pg
pnpm add pg
bun add pg
import { Agent } from "agents";
import {
    PostgresContextProvider,
    PostgresSearchProvider,
    PostgresSessionProvider,
    Session,
} from "agents/experimental/memory/session";
import { Client } from "pg";

export class MyAgent extends Agent {
    session;
    pgClient;

async onStart() {
        const client = new Client({
            connectionString: this.env.HYPERDRIVE.connectionString,
        });
        await client.connect();
        this.pgClient = client;

const sessionId = this.ctx.id.toString();
        this.session = Session.create(
            new PostgresSessionProvider(client, sessionId),
        )
            .withContext("soul", {
                provider: {
                    get: async () => "You are a helpful assistant.",
                },
            })
            .withContext("memory", {
                description: "Short facts",
                maxTokens: 1100,
                provider: new PostgresContextProvider(client, `memory_${sessionId}`),
            })
            .withContext("knowledge", {
                description: "Searchable knowledge base",
                provider: new PostgresSearchProvider(client),
            })
            .withCachedPrompt(
                new PostgresContextProvider(client, `_prompt_${sessionId}`),
            );
    }
}
import { Agent } from "agents";
import {
    PostgresContextProvider,
    PostgresSearchProvider,
    PostgresSessionProvider,
    Session,
} from "agents/experimental/memory/session";
import { Client } from "pg";

export class MyAgent extends Agent<Env> {
    private session?: Session;
    private pgClient?: Client;

async onStart(): Promise<void> {
        const client = new Client({
            connectionString: this.env.HYPERDRIVE.connectionString,
        });
        await client.connect();
        this.pgClient = client;

const sessionId = this.ctx.id.toString();
        this.session = Session.create(
            new PostgresSessionProvider(client, sessionId),
        )
            .withContext("soul", {
                provider: {
                    get: async () => "You are a helpful assistant.",
                },
            })
            .withContext("memory", {
                description: "Short facts",
                maxTokens: 1100,
                provider: new PostgresContextProvider(client, `memory_${sessionId}`),
            })
            .withContext("knowledge", {
                description: "Searchable knowledge base",
                provider: new PostgresSearchProvider(client),
            })
            .withCachedPrompt(
                new PostgresContextProvider(client, `_prompt_${sessionId}`),
            );
    }
}

Behavior differences

When Session.create() receives a SessionProvider instead of a SQLite-backed provider, it skips SQLite auto-wiring:

  • Context blocks need explicit providers. Each withContext() call that should persist data needs a provider option.
  • withCachedPrompt() needs an explicit provider. Pass a PostgresContextProvider to persist the frozen system prompt.
  • Session methods are async. Use await for reads and writes so the same code works with local SQLite and external storage.
  • Broadcaster support is skipped. WebSocket status broadcasts for Session events only work with SQLite-backed sessions.

System prompt lifecycle

freezeSystemPrompt() returns the cached prompt from storage. On first call, it loads context blocks from providers, renders the prompt, and persists it. Subsequent calls return the stored value without re-rendering.

Use refreshSystemPrompt() to force reload context blocks, re-render the prompt, and update the stored value.

Storage tables

By default, storage is in Durable Object SQLite and tables are created lazily on first use. Postgres-backed sessions use the external tables shown in the Postgres providers section.

Table Purpose
assistant_messages Tree-structured messages with id, session_id, parent_id, role, content (JSON), created_at
assistant_compactions Compaction overlays with summary, from_message_id, to_message_id
assistant_fts FTS5 virtual table for message search (porter stemming, unicode tokenization)
assistant_sessions Session registry (SessionManager only) with name, parent_session_id, model, source, token/cost counters
cf_agents_context_blocks Persistent context block storage (AgentContextProvider)
cf_agents_search_entries / cf_agents_search_fts Searchable context entries and FTS5 index (AgentSearchProvider)

Acknowledgments

  • Think — opinionated chat agent that uses Session for conversation storage via configureSession()
  • Chat agents — AIChatAgent with its own message persistence layer
  • Store and sync state — setState() for simpler key-value persistence

Was this helpful?

YesNo

On this page

Sessions·续2 - 图1Docs

{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/agents/runtime/lifecycle/sessions/#page","headline":"Sessions","description":"Persistent conversation storage with tree-structured messages, context blocks, compaction, full-text search, and AI-controllable tools.","url":"https://developers.cloudflare.com/agents/runtime/lifecycle/sessions/","inLanguage":"en","image":"https://developers.cloudflare.com/agents/runtime/lifecycle/sessions/og.png?v=5d6c7b41bcca63b6","dateModified":"2026-06-05","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["AI"]}


description: Persist and sync Agent state across clients in real time using setState, SQL storage, and bidirectional updates. title: Store and sync state image: https://developers.cloudflare.com/agents/runtime/lifecycle/state/og.png?v=886569493f00f3d5


Skip to content

Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/agents/llms.txt
Use this file to discover all available pages before exploring further.