Sessions

Last updated Jun 5, 2026 The Session API provides persistent conversation storage for agents, with tree-structured messages (inspired by Pi ↗︎), 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

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

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

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);
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:

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." } },
    ],
});
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.
.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.
.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.

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

// 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();
// 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:

// Get all child messages that branch from messageId
const branches = await session.getBranches(messageId);
// 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.

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

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

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

new AgentContextProvider(this, "memory");
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.

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

Session.create(this).withContext("skills", {
    provider: new R2SkillProvider(env.SKILLS_BUCKET, { prefix: "skills/" }),
});
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.

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

Session.create(this).withContext("knowledge", {
    description: "Searchable knowledge base",
    provider: new AgentSearchProvider(this),
});
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:

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

// 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.");
// 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.
// 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();
// 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.