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

> Documentation Index  
> Fetch the complete documentation index at: https://edgetunnel-b2h.pages.dev/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-6847)
* [  TypeScript ](#tab-panel-6848)

**JavaScript**

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

```ts
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-6833)
* [  TypeScript ](#tab-panel-6834)

**JavaScript**

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

```ts
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-6843)
* [  TypeScript ](#tab-panel-6844)

**JavaScript**

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

```ts
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-6841)
* [  TypeScript ](#tab-panel-6842)

**JavaScript**

```js
// 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**

```ts
// 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-6849)
* [  TypeScript ](#tab-panel-6850)

**JavaScript**

```js
// 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**

```ts
// 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-6835)
* [  TypeScript ](#tab-panel-6836)

**JavaScript**

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

**TypeScript**

```ts
// 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-6837)
* [  TypeScript ](#tab-panel-6838)

**JavaScript**

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

**TypeScript**

```ts
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-6839)
* [  TypeScript ](#tab-panel-6840)

**JavaScript**

```js
import { AgentContextProvider } from "agents/experimental/memory/session";


new AgentContextProvider(this, "memory");
```

**TypeScript**

```ts
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-6845)
* [  TypeScript ](#tab-panel-6846)

**JavaScript**

```js
import { R2SkillProvider } from "agents/experimental/memory/session";


Session.create(this).withContext("skills", {
  provider: new R2SkillProvider(env.SKILLS_BUCKET, { prefix: "skills/" }),
});
```

**TypeScript**

```ts
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-6851)
* [  TypeScript ](#tab-panel-6852)

**JavaScript**

```js
import { AgentSearchProvider } from "agents/experimental/memory/session";


Session.create(this).withContext("knowledge", {
  description: "Searchable knowledge base",
  provider: new AgentSearchProvider(this),
});
```

**TypeScript**

```ts
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-6857)
* [  TypeScript ](#tab-panel-6858)

**JavaScript**

```js
// 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**

```ts
// 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-6859)
* [  TypeScript ](#tab-panel-6860)

**JavaScript**

```js
// 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**

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

```txt
══════════════════════════════════════════════
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-6855)
* [  TypeScript ](#tab-panel-6856)

**JavaScript**

```js
// 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**

```ts
// 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-6853)
* [  TypeScript ](#tab-panel-6854)

**JavaScript**

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

**TypeScript**

```ts
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-6865)
* [  TypeScript ](#tab-panel-6866)

**JavaScript**

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

```ts
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-6861)
* [  TypeScript ](#tab-panel-6862)

**JavaScript**

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

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

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://edgetunnel-b2h.pages.dev/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-6871)
* [  TypeScript ](#tab-panel-6872)

**JavaScript**

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

```ts
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-6867)
* [  TypeScript ](#tab-panel-6868)

**JavaScript**

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

```ts
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-6879)
* [  TypeScript ](#tab-panel-6880)

**JavaScript**

```js
// 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**

```ts
// 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-6863)
* [  TypeScript ](#tab-panel-6864)

**JavaScript**

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

**TypeScript**

```ts
// 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-6881)
* [  TypeScript ](#tab-panel-6882)

**JavaScript**

```js
// 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**

```ts
// 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-6869)
* [  TypeScript ](#tab-panel-6870)

**JavaScript**

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

**TypeScript**

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

### Compaction helpers

* [  JavaScript ](#tab-panel-6877)
* [  TypeScript ](#tab-panel-6878)

**JavaScript**

```js
// 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**

```ts
// 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-6873)
* [  TypeScript ](#tab-panel-6874)

**JavaScript**

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

**TypeScript**

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

### Cross-session search

* [  JavaScript ](#tab-panel-6875)
* [  TypeScript ](#tab-panel-6876)

**JavaScript**

```js
// 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**

```ts
// 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-6883)
* [  TypeScript ](#tab-panel-6884)

**JavaScript**

```js
// 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**

```ts
// 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-6885)
* [  TypeScript ](#tab-panel-6886)

**JavaScript**

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

```ts
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://edgetunnel-b2h.pages.dev/hyperdrive/) for connection pooling.

### 1\. Create a Hyperdrive config

Create a Hyperdrive config for your Postgres database:

```sh
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-6831)
* [  wrangler.toml ](#tab-panel-6832)

**JSONC**

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

**TOML**

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

```sql
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-6887)
* [  TypeScript ](#tab-panel-6888)

**JavaScript**

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

```ts
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://edgetunnel-b2h.pages.dev/agents/harnesses/think/) — opinionated chat agent that uses Session for conversation storage via `configureSession()`
* [Chat agents](https://edgetunnel-b2h.pages.dev/agents/communication-channels/chat/chat-agents/) — `AIChatAgent` with its own message persistence layer
* [Store and sync state](https://edgetunnel-b2h.pages.dev/agents/runtime/lifecycle/state/) — `setState()` for simpler key-value persistence

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://edgetunnel-b2h.pages.dev/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://edgetunnel-b2h.pages.dev/agents/runtime/lifecycle/sessions/","inLanguage":"en","image":"https://edgetunnel-b2h.pages.dev/dev-products-preview.png","dateModified":"2026-06-05","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://edgetunnel-b2h.pages.dev/#website","name":"Cloudflare Docs","url":"https://edgetunnel-b2h.pages.dev/"},"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"}}]}
```
