Conversation state and memory·续
async load(key) { const row = await this.db .prepare("SELECT content FROM skills WHERE key = ?") .bind(key) .first(); return row ? row.content : null; }
async set(key, content, description) { await this.db .prepare( "INSERT INTO skills (key, content, description) VALUES (?, ?, ?) " + "ON CONFLICT(key) DO UPDATE SET content = ?, description = ?", ) .bind(key, content, description ?? null, content, description ?? null) .run(); } }
```ts
import type { SkillProvider } from "agents/experimental/memory/session";
class DatabaseSkillProvider implements SkillProvider {
private db: D1Database;
constructor(db: D1Database) {
this.db = db;
}
async get(): Promise<string | null> {
const rows = await this.db
.prepare("SELECT key, description FROM skills ORDER BY key")
.all();
if (rows.results.length === 0) return null;
return rows.results
.map((r) => `- ${r.key}${r.description ? `: ${r.description}` : ""}`)
.join("\n");
}
async load(key: string): Promise<string | null> {
const row = await this.db
.prepare("SELECT content FROM skills WHERE key = ?")
.bind(key)
.first();
return row ? (row.content as string) : null;
}
async set(key: string, content: string, description?: string): Promise<void> {
await this.db
.prepare(
"INSERT INTO skills (key, content, description) VALUES (?, ?, ?) " +
"ON CONFLICT(key) DO UPDATE SET content = ?, description = ?",
)
.bind(key, content, description ?? null, content, description ?? null)
.run();
}
}
The Session detects the load() method via duck-typing and generates the appropriate tools automatically.
Skills vs other memory types
| Aspect | Skills | Writable context | Searchable context |
|---|---|---|---|
| In system prompt | Metadata listing only | Full content | Summary count |
| Access pattern | Load whole document by key | Always visible | Search by query |
| Best for | Large documents, reference material | Short notes, preferences | Large collections of small entries |
| Context cost | Low (until loaded) | Proportional to content | Low (until searched) |
| Agent writes? | Optional (if set implemented) |
Yes (via set_context) |
Yes (via set_context) |
The key distinction: skills are lazy. They cost nearly nothing in the system prompt until the agent decides it needs one. This makes them ideal for large reference material where only a subset is relevant to any given conversation.
How agents interact with memory
The Session automatically generates tools based on the provider types of your context blocks. You pass these tools to your LLM alongside your own application-specific tools:
const sessionTools = await session.tools();
const allTools = { ...sessionTools, ...myApplicationTools };
const result = streamText({
model: myModel,
system: await session.freezeSystemPrompt(),
messages: await convertToModelMessages(await session.getHistory()),
tools: allTools,
});
const sessionTools = await session.tools();
const allTools = { ...sessionTools, ...myApplicationTools };
const result = streamText({
model: myModel,
system: await session.freezeSystemPrompt(),
messages: await convertToModelMessages(await session.getHistory()),
tools: allTools,
});
Generated tools
The Session generates tools dynamically based on what provider types are present:
| Tool | Generated when | What it does |
|---|---|---|
set_context |
Any writable, skill, or search block exists | Writes content to a named block. For writable blocks, replaces or appends. For skill/search blocks, writes a keyed entry. |
load_context |
Any skill block exists | Loads full content of a document by key into the agent's context. |
unload_context |
Any skill block exists | Frees context space by removing a previously loaded document. The document remains available for re-loading. |
search_context |
Any search block exists | Full-text search within a searchable block. Returns the top results ranked by relevance. |
session_search |
Using SessionManager |
Searches across all sessions (cross-conversation search). |
The tools include descriptions and parameter schemas that tell the LLM which blocks are available and what they are for. The agent decides when and how to use them based on the conversation.
For the full tool signatures and all Session methods, refer to the Session API reference.
The system prompt
Context blocks are assembled into a structured system prompt with clear headers and metadata. Each block gets a labeled section with tags indicating its type and capacity:
══════════════════════════════════════════════
SOUL (Identity) [readonly]
══════════════════════════════════════════════
You are a helpful coding assistant who speaks concisely.
══════════════════════════════════════════════
MEMORY (Important facts) [45% — 495/1100 tokens] [writable]
══════════════════════════════════════════════
User prefers dark mode.
User's project uses React and TypeScript.
══════════════════════════════════════════════
KNOWLEDGE (Searchable knowledge base) [searchable]
══════════════════════════════════════════════
12 entries indexed.
══════════════════════════════════════════════
SKILLS [loadable]
══════════════════════════════════════════════
- api-ref: API Reference documentation
- style-guide: Company style guide
The tags ([readonly], [writable], [searchable], [loadable]) tell the LLM what kind of interaction is possible with each block. Token budgets show the agent how much space remains in writable blocks, helping it manage its own memory.
Gotchas
Prompt caching
LLM providers (Anthropic, OpenAI, and others) cache the system prompt prefix. When consecutive requests share the same system prompt, the provider can skip re-processing that prefix, reducing both latency and cost. Breaking the cache (by changing the system prompt) throws away this benefit.
The Session API is designed to work with prompt caching:
freezeSystemPrompt()renders the system prompt from all context blocks on the first call, then returns the cached value on subsequent calls. The prompt does not change between turns, even when the agent writes to memory viaset_context.withCachedPrompt()persists the frozen prompt to storage, so it survives Durable Object hibernation and eviction. When the agent wakes up, it loads the same prompt without re-fetching from all providers.
When the agent uses set_context to update a writable block, the underlying provider is updated immediately (the data is saved), but the frozen system prompt is not re-rendered. The LLM sees the update on its next turn only if you explicitly call refreshSystemPrompt(), which you typically do between conversation turns, not mid-turn.
This means the system prompt stays stable throughout a multi-step tool-use turn, preserving the provider's prefix cache across every step.
const session = Session.create(this)
.withContext("soul", {
provider: { get: async () => "You are a helpful assistant." },
})
.withContext("memory", { description: "Learned facts", maxTokens: 1100 })
.withCachedPrompt(); // Persist the frozen prompt across hibernation
// During a conversation turn:
const system = await session.freezeSystemPrompt(); // Same value every call
const tools = await session.tools();
// ... agent calls set_context to update memory ...
// The frozen prompt is NOT changed, prefix cache stays warm
// Between turns (optional, if you want the agent to see its own updates):
await session.refreshSystemPrompt();
const session = Session.create(this)
.withContext("soul", {
provider: { get: async () => "You are a helpful assistant." },
})
.withContext("memory", { description: "Learned facts", maxTokens: 1100 })
.withCachedPrompt(); // Persist the frozen prompt across hibernation
// During a conversation turn:
const system = await session.freezeSystemPrompt(); // Same value every call
const tools = await session.tools();
// ... agent calls set_context to update memory ...
// The frozen prompt is NOT changed, prefix cache stays warm
// Between turns (optional, if you want the agent to see its own updates):
await session.refreshSystemPrompt();
Compaction
Long conversations eventually exceed the LLM's context window. Compaction addresses this at two levels: macro-compaction summarizes ranges of older messages, and micro-compaction truncates individual messages that are too large.
Macro-compaction
Macro-compaction summarizes older messages, but it never deletes the originals.
It uses overlays: a summary is stored in a separate table, keyed by the message range it covers. When getHistory() is called, overlays are applied transparently at read time. The compacted range is replaced by a synthetic summary message. The underlying messages remain in SQLite, preserving the full conversation for audit, search, and branching.
Messages: [1] [2] [3] [4] [5] [6] [7] [8] [9] [10]
↓ compaction ↓
Overlay: [1] [2] [SUMMARY of 3-7] [8] [9] [10]
↑ tail protected
The key points:
- Non-destructive, original messages are never deleted. The full conversation is always available in the database.
- Iterative, when the conversation grows again and triggers another compaction, the existing summary is passed to the LLM to update, not replaced from scratch.
- Boundary-aware, compaction boundaries are shifted to avoid splitting tool call / tool result pairs.
- Configurable,
protectHeadpreserves the first N messages (usually the system context), andtailTokenBudgetkeeps the most recent messages intact.
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,
}),
)
.compactAfter(100_000); // Auto-compact when token estimate exceeds threshold
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,
}),
)
.compactAfter(100_000); // Auto-compact when token estimate exceeds threshold
Auto-compaction triggers after appendMessage() when the estimated token count exceeds the threshold. Compaction failure is non-fatal, the message is already saved.
Micro-compaction
Micro-compaction works at the individual message level rather than across ranges. It handles two problems:
Read-time truncation: truncateOlderMessages() shortens tool outputs and long text in older messages before sending them to the LLM. Recent messages (last 4 by default) are kept intact. This operates on a copy, stored messages are not mutated.
import { truncateOlderMessages } from "agents/experimental/memory/utils";
const history = await session.getHistory();
const truncated = truncateOlderMessages(history);
// Pass truncated history to the LLM
import { truncateOlderMessages } from "agents/experimental/memory/utils";
const history = await session.getHistory();
const truncated = truncateOlderMessages(history);
// Pass truncated history to the LLM
Row size enforcement: when a message is persisted (typically an assistant message with large tool outputs), it is checked against the SQLite row size limit. Oversized tool outputs are replaced with a preview and a note suggesting the tool be re-run. This prevents individual messages from exceeding storage limits while preserving the conversation flow.
Related
Session API reference
Full API reference for Session, covering messages, context blocks, compaction, search, tools, and custom providers.
Store and sync state
setState() for simpler key-value persistence and real-time sync.
Think
Opinionated chat agent with built-in Session integration via configureSession().
Was this helpful?
YesNo
On this page
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/agents/concepts/conversation-state-and-memory/#page","headline":"Conversation state and memory","description":"How agents store and recall information, including read-only context, writable short-form memory, searchable knowledge, and on-demand skills.","url":"https://developers.cloudflare.com/agents/concepts/conversation-state-and-memory/","inLanguage":"en","image":"https://developers.cloudflare.com/agents/concepts/conversation-state-and-memory/og.png?v=52ce156c479b4e08","dateModified":"2026-06-03","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["AI"]}
description: Understand how tools are exposed to models, where tools execute, and how MCP connects external tools to Cloudflare Agents. title: Tools image: https://developers.cloudflare.com/agents/concepts/tools/og.png?v=95e31e7f8e5f6bfc
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.
Docs