Sessions·续1

AI tools

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

const tools = await session.tools();
const allTools = { ...tools, ...myTools };
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.

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

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

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

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

Caution

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:

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

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

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

// Get or create the Session instance for an ID
// Lazy — creates on first access, caches for subsequent calls
const session = manager.getSession(sessionId);
// 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:

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

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

Compaction helpers

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

await manager.addUsage(sessionId, inputTokens, outputTokens, cost);
await manager.addUsage(sessionId, inputTokens, outputTokens, cost);
// 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();
// 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: