---
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-5716)
* [  TypeScript ](#tab-panel-5717)

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-5702)
* [  TypeScript ](#tab-panel-5703)

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-5712)
* [  TypeScript ](#tab-panel-5713)

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-5710)
* [  TypeScript ](#tab-panel-5711)

JavaScript

```

// Append — auto-parents to the latest leaf unless parentId is specified

await session.appendMessage(message);

await session.appendMessage(message, parentId);


// Update an existing message (matched by message.id)

await session.updateMessage(message);


// Delete specific messages

await session.deleteMessages(["msg-1", "msg-2"]);


// Clear all messages and skill state

await session.clearMessages();


```

TypeScript

```

// Append — auto-parents to the latest leaf unless parentId is specified

await session.appendMessage(message);

await session.appendMessage(message, parentId);


// Update an existing message (matched by message.id)

await session.updateMessage(message);


// Delete specific messages

await session.deleteMessages(["msg-1", "msg-2"]);


// Clear all messages and skill state

await 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-5718)
* [  TypeScript ](#tab-panel-5719)

JavaScript

```

// Linear history from root to the latest leaf

const messages = await session.getHistory();


// History to a specific leaf (for branching)

const branch = await session.getHistory(leafId);


// Get a single message

const msg = await session.getMessage("msg-1");


// Get the newest message

const latest = await session.getLatestLeaf();


// Count messages in path

const count = await session.getPathLength();


```

TypeScript

```

// Linear history from root to the latest leaf

const messages = await session.getHistory();


// History to a specific leaf (for branching)

const branch = await session.getHistory(leafId);


// Get a single message

const msg = await session.getMessage("msg-1");


// Get the newest message

const latest = await session.getLatestLeaf();


// Count messages in path

const 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-5704)
* [  TypeScript ](#tab-panel-5705)

JavaScript

```

// Get all child messages that branch from messageId

const branches = await session.getBranches(messageId);


```

TypeScript

```

// Get all child messages that branch from messageId

const 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-5706)
* [  TypeScript ](#tab-panel-5707)

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-5708)
* [  TypeScript ](#tab-panel-5709)

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-5714)
* [  TypeScript ](#tab-panel-5715)

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-5720)
* [  TypeScript ](#tab-panel-5721)

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-5726)
* [  TypeScript ](#tab-panel-5727)

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 it

session.removeContext("extension-notes");


// Rebuild the system prompt to reflect changes

await 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 it

session.removeContext("extension-notes");


// Rebuild the system prompt to reflect changes

await session.refreshSystemPrompt();


```

Note

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

### Reading and writing context

* [  JavaScript ](#tab-panel-5728)
* [  TypeScript ](#tab-panel-5729)

JavaScript

```

// Read a single block

const block = session.getContextBlock("memory");

// { label, description?, content, tokens, maxTokens?, writable, isSkill, isSearchable }


// Read all blocks

const blocks = session.getContextBlocks();


// Replace content entirely

await session.replaceContextBlock("memory", "User likes coffee.");


// Append content

await session.appendContextBlock("memory", "\nUser prefers dark roast.");


```

TypeScript

```

// Read a single block

const block = session.getContextBlock("memory");

// { label, description?, content, tokens, maxTokens?, writable, isSkill, isSearchable }


// Read all blocks

const blocks = session.getContextBlocks();


// Replace content entirely

await session.replaceContextBlock("memory", "User likes coffee.");


// Append content

await 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-5724)
* [  TypeScript ](#tab-panel-5725)

JavaScript

```

// Freeze — first call renders and persists; subsequent calls return cached value

const prompt = await session.freezeSystemPrompt();


// Refresh — re-render from current block state and persist

const updated = await session.refreshSystemPrompt();


```

TypeScript

```

// Freeze — first call renders and persists; subsequent calls return cached value

const prompt = await session.freezeSystemPrompt();


// Refresh — re-render from current block state and persist

const 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-5722)
* [  TypeScript ](#tab-panel-5723)

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-5736)
* [  TypeScript ](#tab-panel-5737)

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-5730)
* [  TypeScript ](#tab-panel-5731)

JavaScript

```

const result = await session.compact();


// Or manage overlays directly

await 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 directly

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

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-5740)
* [  TypeScript ](#tab-panel-5741)

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-5734)
* [  TypeScript ](#tab-panel-5735)

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-5748)
* [  TypeScript ](#tab-panel-5749)

JavaScript

```

// Create a new session

const info = await manager.create("My Chat");


// Create with metadata

const 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();


// Rename

await manager.rename(sessionId, "New Name");


// Delete (clears messages too)

await manager.delete(sessionId);


```

TypeScript

```

// Create a new session

const info = await manager.create("My Chat");


// Create with metadata

const 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();


// Rename

await manager.rename(sessionId, "New Name");


// Delete (clears messages too)

await manager.delete(sessionId);


```

### Accessing sessions

* [  JavaScript ](#tab-panel-5732)
* [  TypeScript ](#tab-panel-5733)

JavaScript

```

// Get or create the Session instance for an ID

// Lazy — creates on first access, caches for subsequent calls

const session = manager.getSession(sessionId);


```

TypeScript

```

// Get or create the Session instance for an ID

// Lazy — creates on first access, caches for subsequent calls

const session = manager.getSession(sessionId);


```

### Message convenience methods

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

* [  JavaScript ](#tab-panel-5750)
* [  TypeScript ](#tab-panel-5751)

JavaScript

```

// Append a single message

await 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 history

const history = await manager.getHistory(sessionId, leafId);


// Message count

const count = await manager.getMessageCount(sessionId);


// Clear messages

await manager.clearMessages(sessionId);


// Delete specific messages

await manager.deleteMessages(sessionId, ["msg-1"]);


```

TypeScript

```

// Append a single message

await 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 history

const history = await manager.getHistory(sessionId, leafId);


// Message count

const count = await manager.getMessageCount(sessionId);


// Clear messages

await manager.clearMessages(sessionId);


// Delete specific messages

await 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-5738)
* [  TypeScript ](#tab-panel-5739)

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-5746)
* [  TypeScript ](#tab-panel-5747)

JavaScript

```

// Add a compaction overlay

await manager.addCompaction(sessionId, summary, fromId, toId);


// Get overlays

const compactions = await manager.getCompactions(sessionId);


// Compact and split — marks old session as ended, creates a continuation

const continuation = await manager.compactAndSplit(

  sessionId,

  summary,

  "Continued Chat",

);


```

TypeScript

```

// Add a compaction overlay

await manager.addCompaction(sessionId, summary, fromId, toId);


// Get overlays

const compactions = await manager.getCompactions(sessionId);


// Compact and split — marks old session as ended, creates a continuation

const 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-5742)
* [  TypeScript ](#tab-panel-5743)

JavaScript

```

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


```

TypeScript

```

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


```

### Cross-session search

* [  JavaScript ](#tab-panel-5744)
* [  TypeScript ](#tab-panel-5745)

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-5752)
* [  TypeScript ](#tab-panel-5753)

JavaScript

```

// 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),

};


```

TypeScript

```

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

* [  JavaScript ](#tab-panel-5754)
* [  TypeScript ](#tab-panel-5755)

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-5700)
* [  wrangler.toml ](#tab-panel-5701)

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-5756)
* [  TypeScript ](#tab-panel-5757)

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":"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"}}]}
```
