---
title: Sessions
description: Persistent conversation storage with tree-structured messages, context blocks, compaction, full-text search, and AI-controllable tools.
image: https://developers.cloudflare.com/dev-products-preview.png
---

> 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. 

[Skip to content](#%5Ftop) 

# Sessions

The Session API provides persistent conversation storage for agents, with tree-structured messages (inspired by [Pi ↗](https://pi.dev)), context blocks, compaction, full-text search, and AI-controllable tools. By default, it uses Durable Object SQLite. External Postgres storage is also available for apps that need shared database access, analytics, or cross-Durable Object queries.

Experimental

The Session API is under `agents/experimental/memory/session`. The API surface is stable but may evolve before graduating to the main package.

## Quick start

* [  JavaScript ](#tab-panel-6557)
* [  TypeScript ](#tab-panel-6558)

JavaScript

```
import { Agent } from "agents";import { Session } from "agents/experimental/memory/session";
class MyAgent extends Agent {  session = Session.create(this)    .withContext("soul", {      provider: { get: async () => "You are a helpful assistant." },    })    .withContext("memory", {      description: "Learned facts about the user",      maxTokens: 1100,    })    .withCachedPrompt();
  async onMessage(message) {    await this.session.appendMessage(message);    const history = await this.session.getHistory();    const system = await this.session.freezeSystemPrompt();    const tools = await this.session.tools();    // Pass history, system prompt, and tools to your LLM  }}
```

TypeScript

```
import { Agent } from "agents";import { Session } from "agents/experimental/memory/session";
class MyAgent extends Agent {  session = Session.create(this)    .withContext("soul", {      provider: { get: async () => "You are a helpful assistant." },    })    .withContext("memory", {      description: "Learned facts about the user",      maxTokens: 1100,    })    .withCachedPrompt();
  async onMessage(message: unknown) {    await this.session.appendMessage(message);    const history = await this.session.getHistory();    const system = await this.session.freezeSystemPrompt();    const tools = await this.session.tools();    // Pass history, system prompt, and tools to your LLM  }}
```

## Creating a session

### Builder API (recommended)

Use `Session.create(agent)` with a chainable builder. Context providers without an explicit `provider` option are auto-wired to SQLite.

* [  JavaScript ](#tab-panel-6543)
* [  TypeScript ](#tab-panel-6544)

JavaScript

```
const session = Session.create(this)  .withContext("soul", { provider: { get: async () => "You are helpful." } })  .withContext("memory", { description: "Learned facts", maxTokens: 1100 })  .withCachedPrompt()  .onCompaction(myCompactFn)  .compactAfter(100_000);
```

TypeScript

```
const session = Session.create(this)  .withContext("soul", { provider: { get: async () => "You are helpful." } })  .withContext("memory", { description: "Learned facts", maxTokens: 1100 })  .withCachedPrompt()  .onCompaction(myCompactFn)  .compactAfter(100_000);
```

### Direct constructor

For full control over providers:

* [  JavaScript ](#tab-panel-6553)
* [  TypeScript ](#tab-panel-6554)

JavaScript

```
import {  Session,  AgentSessionProvider,  AgentContextProvider,} from "agents/experimental/memory/session";
const session = new Session(new AgentSessionProvider(this), {  context: [    {      label: "memory",      description: "Notes",      maxTokens: 500,      provider: new AgentContextProvider(this, "memory"),    },    { label: "soul", provider: { get: async () => "You are helpful." } },  ],});
```

TypeScript

```
import {  Session,  AgentSessionProvider,  AgentContextProvider,} from "agents/experimental/memory/session";
const session = new Session(new AgentSessionProvider(this), {  context: [    {      label: "memory",      description: "Notes",      maxTokens: 500,      provider: new AgentContextProvider(this, "memory"),    },    { label: "soul", provider: { get: async () => "You are helpful." } },  ],});
```

### Builder methods

All builder methods return `this` for chaining. Order does not matter — providers are resolved lazily on first use.

| Method                                  | Description                                                                                                                                              |
| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Session.create(agent)                   | Static factory. agent is any object with a sql tagged template method (your Agent or Durable Object).                                                    |
| .forSession(sessionId)                  | Namespace this session by ID. Required for multi-session isolation when not using SessionManager.                                                        |
| .withContext(label, options?)           | Add a context block. Refer to [Context blocks](#context-blocks).                                                                                         |
| .withCachedPrompt(provider?)            | Enable system prompt persistence. The prompt is frozen on first use and survives hibernation and eviction.                                               |
| .onCompaction(fn)                       | Register a compaction function. Refer to [Compaction](#compaction).                                                                                      |
| .compactAfter(tokenThreshold, options?) | Auto-compact when estimated token count exceeds the threshold. Requires .onCompaction(). Pass { tokenCounter } to control how the threshold is measured. |
| .onCompactionError(handler)             | Handle errors from automatic compaction. Handler failures are swallowed so message writes remain non-fatal.                                              |

## Messages

Messages use the `SessionMessage` type — a minimal shape with `id`, `role`, `parts`, and optional `createdAt`. The AI SDK's `UIMessage` is structurally compatible and can be passed directly. The session stores messages in a tree structure via `parent_id`, enabling branching conversations.

* [  JavaScript ](#tab-panel-6551)
* [  TypeScript ](#tab-panel-6552)

JavaScript

```
// Append — auto-parents to the latest leaf unless parentId is specifiedawait session.appendMessage(message);await session.appendMessage(message, parentId);
// Update an existing message (matched by message.id)await session.updateMessage(message);
// Delete specific messagesawait session.deleteMessages(["msg-1", "msg-2"]);
// Clear all messages and skill stateawait session.clearMessages();
```

TypeScript

```
// Append — auto-parents to the latest leaf unless parentId is specifiedawait session.appendMessage(message);await session.appendMessage(message, parentId);
// Update an existing message (matched by message.id)await session.updateMessage(message);
// Delete specific messagesawait session.deleteMessages(["msg-1", "msg-2"]);
// Clear all messages and skill stateawait session.clearMessages();
```

Note

Session methods are async. SQLite-backed sessions are usually fast, but external providers may perform network I/O, and `appendMessage()` may also trigger auto-compaction.

### Reading history

* [  JavaScript ](#tab-panel-6559)
* [  TypeScript ](#tab-panel-6560)

JavaScript

```
// Linear history from root to the latest leafconst messages = await session.getHistory();
// History to a specific leaf (for branching)const branch = await session.getHistory(leafId);
// Get a single messageconst msg = await session.getMessage("msg-1");
// Get the newest messageconst latest = await session.getLatestLeaf();
// Count messages in pathconst count = await session.getPathLength();
```

TypeScript

```
// Linear history from root to the latest leafconst messages = await session.getHistory();
// History to a specific leaf (for branching)const branch = await session.getHistory(leafId);
// Get a single messageconst msg = await session.getMessage("msg-1");
// Get the newest messageconst latest = await session.getLatestLeaf();
// Count messages in pathconst count = await session.getPathLength();
```

### Branching

Messages form a tree. When you `appendMessage` with a `parentId` that already has children, you create a branch. Use `getBranches()` to get all child messages branching from a given point:

* [  JavaScript ](#tab-panel-6545)
* [  TypeScript ](#tab-panel-6546)

JavaScript

```
// Get all child messages that branch from messageIdconst branches = await session.getBranches(messageId);
```

TypeScript

```
// Get all child messages that branch from messageIdconst branches = await session.getBranches(messageId);
```

This powers features like response regeneration — pass the user message ID to get both the original and regenerated responses. `getHistory(leafId)` walks the chosen path.

## Search

Full-text search over the conversation history using SQLite FTS5:

* [  JavaScript ](#tab-panel-6547)
* [  TypeScript ](#tab-panel-6548)

JavaScript

```
const results = await session.search("deployment Friday", { limit: 10 });// Returns: Array<{ id, role, content, createdAt? }>
```

TypeScript

```
const results = await session.search("deployment Friday", { limit: 10 });// Returns: Array<{ id, role, content, createdAt? }>
```

SQLite-backed sessions use FTS5 with porter stemming and unicode tokenization. Postgres-backed sessions use the provider's Postgres full-text index. `search()` throws if the session provider does not support search.

## Context blocks

Context blocks are persistent key-value sections injected into the system prompt. Each block has a **label**, optional **description**, and a **provider** that determines its behavior.

### Provider types

There are four provider types, detected by duck-typing:

| Provider                    | Interface                   | Behavior                                                                                     | AI tool                                      |
| --------------------------- | --------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------- |
| **ContextProvider**         | get()                       | Read-only block in system prompt                                                             | —                                            |
| **WritableContextProvider** | get() \+ set()              | Writable via AI                                                                              | set\_context                                 |
| **SkillProvider**           | get() \+ load() \+ set?()   | On-demand keyed documents. get() returns a metadata listing; load(key) fetches full content. | load\_context, unload\_context, set\_context |
| **SearchProvider**          | get() \+ search() \+ set?() | Full-text searchable entries. get() returns a summary; search(query) runs FTS5.              | search\_context, set\_context                |

### Built-in providers

**`AgentContextProvider`** — SQLite-backed writable context. This is the default when using the builder without an explicit provider.

* [  JavaScript ](#tab-panel-6549)
* [  TypeScript ](#tab-panel-6550)

JavaScript

```
import { AgentContextProvider } from "agents/experimental/memory/session";
new AgentContextProvider(this, "memory");
```

TypeScript

```
import { AgentContextProvider } from "agents/experimental/memory/session";
new AgentContextProvider(this, "memory");
```

**`R2SkillProvider`** — Cloudflare R2 bucket for on-demand document loading. Skills are listed in the system prompt as metadata; the model loads full content on demand via `load_context`.

* [  JavaScript ](#tab-panel-6555)
* [  TypeScript ](#tab-panel-6556)

JavaScript

```
import { R2SkillProvider } from "agents/experimental/memory/session";
Session.create(this).withContext("skills", {  provider: new R2SkillProvider(env.SKILLS_BUCKET, { prefix: "skills/" }),});
```

TypeScript

```
import { R2SkillProvider } from "agents/experimental/memory/session";
Session.create(this).withContext("skills", {  provider: new R2SkillProvider(env.SKILLS_BUCKET, { prefix: "skills/" }),});
```

**`AgentSearchProvider`** — SQLite FTS5 searchable context. Entries are indexed and searchable by the model via `search_context`.

* [  JavaScript ](#tab-panel-6561)
* [  TypeScript ](#tab-panel-6562)

JavaScript

```
import { AgentSearchProvider } from "agents/experimental/memory/session";
Session.create(this).withContext("knowledge", {  description: "Searchable knowledge base",  provider: new AgentSearchProvider(this),});
```

TypeScript

```
import { AgentSearchProvider } from "agents/experimental/memory/session";
Session.create(this).withContext("knowledge", {  description: "Searchable knowledge base",  provider: new AgentSearchProvider(this),});
```

### Adding and removing context at runtime

Blocks can be added and removed dynamically after initialization:

* [  JavaScript ](#tab-panel-6567)
* [  TypeScript ](#tab-panel-6568)

JavaScript

```
// Add a new block (auto-wires to SQLite if no provider given)await session.addContext("extension-notes", {  description: "From extension X",  maxTokens: 500,});
// Remove itsession.removeContext("extension-notes");
// Rebuild the system prompt to reflect changesawait session.refreshSystemPrompt();
```

TypeScript

```
// Add a new block (auto-wires to SQLite if no provider given)await session.addContext("extension-notes", {  description: "From extension X",  maxTokens: 500,});
// Remove itsession.removeContext("extension-notes");
// Rebuild the system prompt to reflect changesawait session.refreshSystemPrompt();
```

Note

`addContext` and `removeContext` do not automatically update the frozen system prompt. Call `refreshSystemPrompt()` afterward.

### Reading and writing context

* [  JavaScript ](#tab-panel-6569)
* [  TypeScript ](#tab-panel-6570)

JavaScript

```
// Read a single blockconst block = session.getContextBlock("memory");// { label, description?, content, tokens, maxTokens?, writable, isSkill, isSearchable }
// Read all blocksconst blocks = session.getContextBlocks();
// Replace content entirelyawait session.replaceContextBlock("memory", "User likes coffee.");
// Append contentawait session.appendContextBlock("memory", "\nUser prefers dark roast.");
```

TypeScript

```
// Read a single blockconst block = session.getContextBlock("memory");// { label, description?, content, tokens, maxTokens?, writable, isSkill, isSearchable }
// Read all blocksconst blocks = session.getContextBlocks();
// Replace content entirelyawait session.replaceContextBlock("memory", "User likes coffee.");
// Append contentawait session.appendContextBlock("memory", "\nUser prefers dark roast.");
```

### System prompt

The system prompt is built from all context blocks with headers and metadata:

```
══════════════════════════════════════════════SOUL (Identity) [readonly]══════════════════════════════════════════════You are a helpful assistant.
══════════════════════════════════════════════MEMORY (Learned facts) [45% — 495/1100 tokens]══════════════════════════════════════════════User likes coffee.User prefers dark roast.
```

* [  JavaScript ](#tab-panel-6565)
* [  TypeScript ](#tab-panel-6566)

JavaScript

```
// Freeze — first call renders and persists; subsequent calls return cached valueconst prompt = await session.freezeSystemPrompt();
// Refresh — re-render from current block state and persistconst updated = await session.refreshSystemPrompt();
```

TypeScript

```
// Freeze — first call renders and persists; subsequent calls return cached valueconst prompt = await session.freezeSystemPrompt();
// Refresh — re-render from current block state and persistconst updated = await session.refreshSystemPrompt();
```

The frozen prompt survives Durable Object hibernation and eviction when `withCachedPrompt()` is enabled.

## AI tools

Session automatically generates tools based on the provider types of your context blocks. Pass these to your LLM alongside your own tools.

* [  JavaScript ](#tab-panel-6563)
* [  TypeScript ](#tab-panel-6564)

JavaScript

```
const tools = await session.tools();const allTools = { ...tools, ...myTools };
```

TypeScript

```
const tools = await session.tools();const allTools = { ...tools, ...myTools };
```

### set\_context

Generated when any writable block exists. Writes to regular blocks, skill blocks (keyed), or search blocks (keyed). Enforces `maxTokens` limits.

### load\_context

Generated when any skill block exists. Loads full content by key from a `SkillProvider`.

### unload\_context

Generated alongside `load_context`. Frees context space by unloading a previously loaded skill. The skill remains available for re-loading.

### search\_context

Generated when any search block exists. Full-text search within a searchable context block. Returns top 10 results by FTS5 rank.

### session\_search

Available on `SessionManager` only. Searches across all sessions.

## Compaction

Compaction summarizes older messages to keep conversations within token limits. Original messages are preserved in SQLite — the summary is a non-destructive overlay applied at read time.

### Setup

* [  JavaScript ](#tab-panel-6575)
* [  TypeScript ](#tab-panel-6576)

JavaScript

```
import { createCompactFunction } from "agents/experimental/memory/utils/compaction-helpers";
const session = Session.create(this)  .withContext("memory", { maxTokens: 1100 })  .onCompaction(    createCompactFunction({      summarize: (prompt) =>        generateText({ model: myModel, prompt }).then((r) => r.text),      protectHead: 3,      tailTokenBudget: 20000,      minTailMessages: 2,      tokenCounter: async (messages) => estimateWithYourTokenizer({ messages }),    }),  )  .compactAfter(100_000);
```

TypeScript

```
import { createCompactFunction } from "agents/experimental/memory/utils/compaction-helpers";
const session = Session.create(this)  .withContext("memory", { maxTokens: 1100 })  .onCompaction(    createCompactFunction({      summarize: (prompt) =>        generateText({ model: myModel, prompt }).then((r) => r.text),      protectHead: 3,      tailTokenBudget: 20000,      minTailMessages: 2,      tokenCounter: async (messages) => estimateWithYourTokenizer({ messages }),    }),  )  .compactAfter(100_000);
```

### How compaction works

1. **Protect head** — first N messages are never compacted (default 3)
2. **Protect tail** — walk backward from the end, accumulating tokens up to a budget (default 20K tokens)
3. **Align boundaries** — shift boundaries to avoid splitting tool call/result pairs
4. **Summarize middle** — send the middle section to an LLM with a structured format (Topic, Key Points, Current State, Open Items)
5. **Store overlay** — saved in the `assistant_compactions` table, keyed by `fromMessageId` and `toMessageId`
6. **Iterative** — on subsequent compactions, the existing summary is passed to the LLM to update rather than replace

When `getHistory()` is called, compaction overlays are applied transparently — the compacted range is replaced by a synthetic summary message.

### Manual compaction

* [  JavaScript ](#tab-panel-6571)
* [  TypeScript ](#tab-panel-6572)

JavaScript

```
const result = await session.compact();
// Or manage overlays directlyawait session.addCompaction("Summary of messages 1-50", "msg-1", "msg-50");const overlays = await session.getCompactions();
```

TypeScript

```
const result = await session.compact();
// Or manage overlays directlyawait session.addCompaction("Summary of messages 1-50", "msg-1", "msg-50");const overlays = await session.getCompactions();
```

### Auto-compaction

When `.compactAfter(threshold)` is set, `appendMessage()` checks the estimated token count after each write. If it exceeds the threshold, `compact()` is called automatically. Auto-compaction failure is non-fatal — the message is already saved.

Note

Auto-compaction is checked **between turns** (on each `appendMessage()`), not within a turn. A single long, tool-heavy turn can grow past the model context window mid-flight, before the next check. `@cloudflare/think` adds opt-in mid-turn recovery on top of this — refer to [Context-window overflow recovery](https://developers.cloudflare.com/agents/harnesses/think/recovery/#context-window-overflow-recovery).

By default, the estimate includes stored message parts plus the Session-managed frozen system prompt, so context blocks and cached prompts managed by `Session` contribute to the threshold. It does not include framework-specific prompt additions or tool schema serialization that happen outside `Session`.

There are two token-counting decisions:

* `.compactAfter(threshold, { tokenCounter })` controls **when** automatic compaction is triggered after writes.
* `createCompactFunction({ tokenCounter })` controls **which** tail messages are protected from summarization. Use this when tool-heavy histories are much larger than the Workers-safe heuristic can estimate.

You usually only need to configure one counter. The `.compactAfter()` counter also flows into `createCompactFunction`'s boundary walk (via `CompactContext`) when no explicit `createCompactFunction({ tokenCounter })` is given, so a single counter drives both "should we compact?" and "what should we compact?".

Warning

The flowed counter is invoked **per message** during the boundary walk. A tokenizer-style counter budgets accurately; a usage-only counter that returns a fixed whole-prompt total (for example `usage.inputTokens` regardless of which messages are passed) degrades the tail budget to `minTailMessages` — compaction still runs and context stays bounded, but the byte budget is effectively ignored. Pass an explicit per-message `createCompactFunction({ tokenCounter })` for precise tail budgeting.

Use a custom counter when you have model-reported usage or your own tokenizer:

* [  JavaScript ](#tab-panel-6581)
* [  TypeScript ](#tab-panel-6582)

JavaScript

```
const session = Session.create(this)  .onCompaction(myCompactFn)  .compactAfter(100_000, {    tokenCounter: async ({ messages, systemPrompt, contextBlocks }) => {      return estimateWithYourTokenizer({        messages,        systemPrompt,        contextBlocks,      });    },  })  .onCompactionError((err) => {    console.warn("Auto-compaction failed", err);  });
```

TypeScript

```
const session = Session.create(this)  .onCompaction(myCompactFn)  .compactAfter(100_000, {    tokenCounter: async ({ messages, systemPrompt, contextBlocks }) => {      return estimateWithYourTokenizer({        messages,        systemPrompt,        contextBlocks,      });    },  })  .onCompactionError((err) => {    console.warn("Auto-compaction failed", err);  });
```

Note

The default token estimation is heuristic (not tiktoken). It uses `max(chars/4, words*1.3)` with 4 tokens per-message overhead, and also applies the string heuristic to the Session-managed system prompt. Tiktoken would add 80–120 MB heap overhead, which exceeds Cloudflare Workers' 128 MB limit.

## SessionManager

`SessionManager` is a registry for multiple named sessions within a single Durable Object. It provides lifecycle management, convenience methods, and cross-session search.

### Creating a SessionManager

* [  JavaScript ](#tab-panel-6577)
* [  TypeScript ](#tab-panel-6578)

JavaScript

```
import { SessionManager } from "agents/experimental/memory/session";
const manager = SessionManager.create(this)  .withContext("soul", { provider: { get: async () => "You are helpful." } })  .withContext("memory", { description: "Learned facts", maxTokens: 1100 })  .withCachedPrompt()  .onCompaction(myCompactFn)  .compactAfter(100_000)  .withSearchableHistory("history");
```

TypeScript

```
import { SessionManager } from "agents/experimental/memory/session";
const manager = SessionManager.create(this)  .withContext("soul", { provider: { get: async () => "You are helpful." } })  .withContext("memory", { description: "Learned facts", maxTokens: 1100 })  .withCachedPrompt()  .onCompaction(myCompactFn)  .compactAfter(100_000)  .withSearchableHistory("history");
```

Context blocks, prompt caching, and compaction settings are propagated to all sessions created through the manager. Provider keys are automatically namespaced by session ID.

### Builder methods

| Method                                  | Description                                                                                             |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| SessionManager.create(agent)            | Static factory.                                                                                         |
| .withContext(label, options?)           | Add context block template for all sessions.                                                            |
| .withCachedPrompt(provider?)            | Enable prompt persistence for all sessions.                                                             |
| .onCompaction(fn)                       | Register compaction function for all sessions.                                                          |
| .compactAfter(tokenThreshold, options?) | Auto-compact threshold for all sessions. Supports the same tokenCounter option as Session.              |
| .onCompactionError(handler)             | Handle automatic compaction errors for managed sessions.                                                |
| .withSearchableHistory(label)           | Add a cross-session searchable history block. The model can search past conversations from any session. |

### Session lifecycle

* [  JavaScript ](#tab-panel-6589)
* [  TypeScript ](#tab-panel-6590)

JavaScript

```
// Create a new sessionconst info = await manager.create("My Chat");
// Create with metadataconst info2 = await manager.create("My Chat", {  parentSessionId: "parent-id",  model: "claude-sonnet-4-20250514",  source: "web",});
// Get session metadata (null if not found)const session = await manager.get(sessionId);
// List all sessions (ordered by updated_at DESC)const sessions = await manager.list();
// Renameawait manager.rename(sessionId, "New Name");
// Delete (clears messages too)await manager.delete(sessionId);
```

TypeScript

```
// Create a new sessionconst info = await manager.create("My Chat");
// Create with metadataconst info2 = await manager.create("My Chat", {  parentSessionId: "parent-id",  model: "claude-sonnet-4-20250514",  source: "web",});
// Get session metadata (null if not found)const session = await manager.get(sessionId);
// List all sessions (ordered by updated_at DESC)const sessions = await manager.list();
// Renameawait manager.rename(sessionId, "New Name");
// Delete (clears messages too)await manager.delete(sessionId);
```

### Accessing sessions

* [  JavaScript ](#tab-panel-6573)
* [  TypeScript ](#tab-panel-6574)

JavaScript

```
// Get or create the Session instance for an ID// Lazy — creates on first access, caches for subsequent callsconst session = manager.getSession(sessionId);
```

TypeScript

```
// Get or create the Session instance for an ID// Lazy — creates on first access, caches for subsequent callsconst session = manager.getSession(sessionId);
```

### Message convenience methods

These delegate to the underlying Session and update the session's `updated_at` timestamp:

* [  JavaScript ](#tab-panel-6591)
* [  TypeScript ](#tab-panel-6592)

JavaScript

```
// Append a single messageawait manager.append(sessionId, message, parentId);
// Add or update (upsert)await manager.upsert(sessionId, message, parentId);
// Batch append (auto-chains parent IDs)await manager.appendAll(sessionId, messages, parentId);
// Read historyconst history = await manager.getHistory(sessionId, leafId);
// Message countconst count = await manager.getMessageCount(sessionId);
// Clear messagesawait manager.clearMessages(sessionId);
// Delete specific messagesawait manager.deleteMessages(sessionId, ["msg-1"]);
```

TypeScript

```
// Append a single messageawait manager.append(sessionId, message, parentId);
// Add or update (upsert)await manager.upsert(sessionId, message, parentId);
// Batch append (auto-chains parent IDs)await manager.appendAll(sessionId, messages, parentId);
// Read historyconst history = await manager.getHistory(sessionId, leafId);
// Message countconst count = await manager.getMessageCount(sessionId);
// Clear messagesawait manager.clearMessages(sessionId);
// Delete specific messagesawait manager.deleteMessages(sessionId, ["msg-1"]);
```

### Forking

Fork a session at a specific message — copies history up to that point into a new session:

* [  JavaScript ](#tab-panel-6579)
* [  TypeScript ](#tab-panel-6580)

JavaScript

```
const forked = await manager.fork(sessionId, atMessageId, "Forked Chat");// forked.parent_session_id === sessionId
```

TypeScript

```
const forked = await manager.fork(sessionId, atMessageId, "Forked Chat");// forked.parent_session_id === sessionId
```

### Compaction helpers

* [  JavaScript ](#tab-panel-6587)
* [  TypeScript ](#tab-panel-6588)

JavaScript

```
// Add a compaction overlayawait manager.addCompaction(sessionId, summary, fromId, toId);
// Get overlaysconst compactions = await manager.getCompactions(sessionId);
// Compact and split — marks old session as ended, creates a continuationconst continuation = await manager.compactAndSplit(  sessionId,  summary,  "Continued Chat",);
```

TypeScript

```
// Add a compaction overlayawait manager.addCompaction(sessionId, summary, fromId, toId);
// Get overlaysconst compactions = await manager.getCompactions(sessionId);
// Compact and split — marks old session as ended, creates a continuationconst continuation = await manager.compactAndSplit(  sessionId,  summary,  "Continued Chat",);
```

`compactAndSplit()` creates a new session with a summary message instead of an in-place overlay. The original session is marked with `end_reason: "compaction"`.

### Usage tracking

* [  JavaScript ](#tab-panel-6583)
* [  TypeScript ](#tab-panel-6584)

JavaScript

```
await manager.addUsage(sessionId, inputTokens, outputTokens, cost);
```

TypeScript

```
await manager.addUsage(sessionId, inputTokens, outputTokens, cost);
```

### Cross-session search

* [  JavaScript ](#tab-panel-6585)
* [  TypeScript ](#tab-panel-6586)

JavaScript

```
// Search across all sessions (FTS5)const results = await manager.search("deployment Friday", { limit: 20 });
// Get tools for the model (includes session_search)const tools = await manager.tools();
```

TypeScript

```
// Search across all sessions (FTS5)const results = await manager.search("deployment Friday", { limit: 20 });
// Get tools for the model (includes session_search)const tools = await manager.tools();
```

## Custom providers

Implement any of the four provider interfaces to plug in your own storage:

* [  JavaScript ](#tab-panel-6593)
* [  TypeScript ](#tab-panel-6594)

JavaScript

```
// Read-only contextconst 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),};
```

TypeScript

```
// Read-only contextconst 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:

* [  JavaScript ](#tab-panel-6595)
* [  TypeScript ](#tab-panel-6596)

JavaScript

```
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) {    /* ... */  },};
```

TypeScript

```
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](https://developers.cloudflare.com/hyperdrive/) for connection pooling.

### 1\. Create a Hyperdrive config

Create a Hyperdrive config for your Postgres database:

Terminal window

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

Then add the Hyperdrive binding to `wrangler.jsonc`:

* [  wrangler.jsonc ](#tab-panel-6541)
* [  wrangler.toml ](#tab-panel-6542)

JSONC

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

TOML

```
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:

 npm  yarn  pnpm  bun 

```
npm i pg
```

```
yarn add pg
```

```
pnpm add pg
```

```
bun add pg
```

* [  JavaScript ](#tab-panel-6597)
* [  TypeScript ](#tab-panel-6598)

JavaScript

```
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}`),      );  }}
```

TypeScript

```
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

* Session's tree-structured messages are inspired by [Pi ↗](https://pi.dev).
* Context blocks are inspired by [Letta AI memory blocks ↗](https://www.letta.com/blog/memory-blocks).
* Formatting of blocks is inspired by [Hermes Agent ↗](https://github.com/nousresearch/hermes-agent).

## Related

* [Think](https://developers.cloudflare.com/agents/harnesses/think/) — opinionated chat agent that uses Session for conversation storage via `configureSession()`
* [Chat agents](https://developers.cloudflare.com/agents/communication-channels/chat/chat-agents/) — `AIChatAgent` with its own message persistence layer
* [Store and sync state](https://developers.cloudflare.com/agents/runtime/lifecycle/state/) — `setState()` for simpler key-value persistence

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/agents/runtime/lifecycle/sessions/#page","headline":"Sessions · Cloudflare Agents docs","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/dev-products-preview.png","dateModified":"2026-06-05","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["AI"]}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/runtime/","name":"Runtime"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/runtime/lifecycle/","name":"Lifecycle"}},{"@type":"ListItem","position":5,"item":{"@id":"/agents/runtime/lifecycle/sessions/","name":"Sessions"}}]}
```
