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 aprovideroption. withCachedPrompt()needs an explicit provider. Pass aPostgresContextProviderto persist the frozen system prompt.- Session methods are async. Use
awaitfor 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
- Session's tree-structured messages are inspired by Pi ↗︎.
- Context blocks are inspired by Letta AI memory blocks ↗︎.
- Formatting of blocks is inspired by Hermes Agent ↗︎.
Related
- Think — opinionated chat agent that uses Session for conversation storage via
configureSession() - Chat agents —
AIChatAgentwith its own message persistence layer - Store and sync state —
setState()for simpler key-value persistence
Was this helpful?
YesNo
On this page
{"@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
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.
Docs