Agent 需要内存才能在时间上持续有用。没有内存,每次对话都从零开始——Agent 会忘记用户是谁、学到了什么、正在做什么。内存将无状态 LLM 调用变为持久、上下文感知的 Agent。
Session API 为基于 Cloudflare Agents SDK 构建的 Agent 提供内存层。它管理两类内存:对话历史(构成会话的消息与工具调用)与上下文内存(注入系统提示词的持久块,Agent 可读、写、搜索与加载)。
当你需要的不只是简单同步状态或扁平聊天历史时使用本页。小型 UI 状态用存储与同步状态。基础聊天持久化由 AIChatAgent 替你存储消息。有明确主张的长期内存由 Think 基于 Session 与上下文块构建。
最基本的内存类型是对话本身:用户与 Agent 之间的消息、Agent 发起的 tool 调用及其收到的结果。Session 将所有内容存入由 Session Provider 支持的树形消息历史,默认为 SQLite。
import { Session } from "agents/experimental/memory/session";
// Append messages as the conversation progresses
await session.appendMessage({
id: `user-${crypto.randomUUID()}`,
role: "user",
parts: [{ type: "text", text: "What's the status of the deployment?" }],
});
// Read the full conversation history
const history = await session.getHistory();import { Session } from "agents/experimental/memory/session";
// Append messages as the conversation progresses
await session.appendMessage({
id: `user-${crypto.randomUUID()}`,
role: "user",
parts: [{ type: "text", text: "What's the status of the deployment?" }],
});
// Read the full conversation history
const history = await session.getHistory();对话历史在 Durable Object 休眠与驱逐后仍保留。Agent 唤醒时完整历史可在 SQLite 中获取,无需重放或重建。
消息通过 parent_id 以树结构存储,支持分支对话。对已有子节点的 parentId 调用 appendMessage 会创建分支,适用于响应重新生成等功能。getHistory(leafId) 沿树中任选路径遍历。
Session 还提供对话历史的全文搜索:
const results = await session.search("deployment Friday", { limit: 10 });const results = await session.search("deployment Friday", { limit: 10 });对话变长时,压缩 会摘要较早消息,在保留底层数据的同时控制上下文窗口。
上下文内存是注入系统提示词的持久信息,与对话历史分离。它让 Agent 在每一轮次都能访问身份、指令、已学事实、知识库与参考资料。
Session API 支持四类上下文内存,各适用于不同信息。类型由支撑上下文块的 提供商 决定。Session 自动检测提供商能力。
这是传统系统提示词:Agent 的身份、个性与指令。可直接写在代码库、从 R2 的 SOUL.md 加载,或从 API 获取。内容注入系统提示词,Agent 无法修改。
编程助手可有定义个性与约束的 soul:
import { Session } from "agents/experimental/memory/session";
const session = Session.create(this).withContext("soul", {
provider: {
get: async () =>
"You are a senior TypeScript engineer. You write concise, " +
"well-tested code. You prefer composition over inheritance. " +
"When you are unsure, you say so rather than guessing.",
},
});import { Session } from "agents/experimental/memory/session";
const session = Session.create(this).withContext("soul", {
provider: {
get: async () =>
"You are a senior TypeScript engineer. You write concise, " +
"well-tested code. You prefer composition over inheritance. " +
"When you are unsure, you say so rather than guessing.",
},
});或从 R2 加载,以便在不重新部署的情况下更新 Agent 个性:
const session = Session.create(this).withContext("soul", {
provider: {
get: async () => {
const obj = await env.CONFIG_BUCKET.get("soul.md");
return obj ? obj.text() : "You are a helpful assistant.";
},
},
});const session = Session.create(this).withContext("soul", {
provider: {
get: async () => {
const obj = await env.CONFIG_BUCKET.get("soul.md");
return obj ? obj.text() : "You are a helpful assistant.";
},
},
});只读块通过仅提供带 get() 方法的对象定义。不生成工具。内容出现在系统提示词中,Agent 无法更改。
可将其视为 Agent 自用的 scratchpad,用于记下需要记住的内容——类似 Claude Code 维护待办列表,或客服 Agent 在对话中跟踪对用户的了解。
const session = Session.create(this)
.withContext("memory", {
description: "Important facts learned during conversation",
maxTokens: 1100,
})
.withContext("todos", {
description: "Task list, track what needs to be done and what is complete",
maxTokens: 2000,
});const session = Session.create(this)
.withContext("memory", {
description: "Important facts learned during conversation",
maxTokens: 1100,
})
.withContext("todos", {
description: "Task list, track what needs to be done and what is complete",
maxTokens: 2000,
});构建器中省略 provider 时,Session 自动连接到 SQLite 支持的可写提供商。Agent 获得 set_context 工具,可替换或追加这些块的内容。强制执行 token 限制,Agent 不能超出 maxTokens 预算。
系统提示词渲染可写块时会显示 token 使用指示,让 Agent 知道剩余空间:
══════════════════════════════════════════════
MEMORY (Important facts learned during conversation) [45% — 495/1100 tokens] [writable]
══════════════════════════════════════════════
User prefers dark mode.
User's project uses React and TypeScript.
Deployment target is Cloudflare Workers.
══════════════════════════════════════════════
TODOS (Task list) [12% — 240/2000 tokens] [writable]
══════════════════════════════════════════════
- [x] Set up project scaffolding
- [ ] Add authentication middleware
- [ ] Write integration tests内容跨消息持久化并在休眠后存活。始终在系统提示词中可见,Agent 每轮次都能看到,无需额外获取。
当有大量信息(知识库、文档、日志、累积笔记)不宜全部塞入系统提示词时,可搜索上下文在系统提示词中保留摘要(例如「已索引 42 条」),并在需要时让 Agent 检索具体条目。
你提供带 search() 方法的提供商。搜索实现完全由你决定:全文搜索、通过 Vectorize 的向量搜索、调用外部 API 等。Session 只关心提供商是否有 search() 方法。
内置 AgentSearchProvider 默认使用 Durable Object SQLite 与 FTS5:
import { AgentSearchProvider } from "agents/experimental/memory/session";
const session = Session.create(this).withContext("knowledge", {
description:
"Searchable knowledge base, search for relevant information before answering",
provider: new AgentSearchProvider(this),
});import { AgentSearchProvider } from "agents/experimental/memory/session";
const session = Session.create(this).withContext("knowledge", {
description:
"Searchable knowledge base, search for relevant information before answering",
provider: new AgentSearchProvider(this),
});也可实现由任意搜索机制支持的自定义提供商:
const session = Session.create(this).withContext("knowledge", {
description: "Searchable knowledge base",
provider: {
get: async () => "Product documentation and FAQs",
search: async (query) => {
// Use Vectorize, an external API, whatever you need
const results = await env.VECTORIZE_INDEX.query(
await generateEmbedding(query),
{ topK: 5 },
);
return results.matches.map((m) => m.metadata.text).join("\n\n");
},
set: async (key, content) => {
// Index new content
},
},
});const session = Session.create(this).withContext("knowledge", {
description: "Searchable knowledge base",
provider: {
get: async () => "Product documentation and FAQs",
search: async (query) => {
// Use Vectorize, an external API, whatever you need
const results = await env.VECTORIZE_INDEX.query(
await generateEmbedding(query),
{ topK: 5 },
);
return results.matches.map((m) => m.metadata.text).join("\n\n");
},
set: async (key, content) => {
// Index new content
},
},
});Agent 获得用于查询的 search_context 工具与用于索引新条目的 set_context 工具。由 Agent 决定搜什么,由你决定如何搜索。
当 Agent 需从大型集合中查找具体信息而非加载整份文档时,这是合适选择。
技能是大型上下文(完整文档、参考指南、运行手册、模板),Agent 可按需发现与加载。可将其视为书架上的参考资料:Agent 看到标题与描述列表,选取与当前任务相关者,加载、使用,完成后卸载。
与从大型集合检索小块的可搜索上下文不同,技能设计为整份加载。Agent 加载技能时,整份文档进入其上下文窗口。
技能由 SkillProvider 接口支持。技能提供商有三个方法:
get()返回出现在系统提示词中的元数据列表(标题与描述)load(key)获取特定技能的完整内容set(key, content, description?)写入或更新技能条目(可选)
系统提示词将可用技能显示为列表。[loadable] 标签告诉 LLM 这些条目不是内联的,需要使用工具访问完整内容:
══════════════════════════════════════════════
SKILLS [loadable]
══════════════════════════════════════════════
- api-ref: API Reference documentation
- style-guide: Company style guide
- deploy-checklist: Production deployment checklistAgent 看到标题,决定哪个技能与当前任务相关,并使用 load_context 将完整内容拉入工作上下文。完成后使用 unload_context 释放空间。当技能提供商实现 set() 时,Agent 也可写回,更新现有技能或创建新技能。
Agent sees: "- deploy-checklist: Production deployment checklist"
User asks: "Walk me through a production deployment"
Agent calls: load_context({ block: "skills", key: "deploy-checklist" })
→ Full checklist content is loaded into the agent's working context内置 R2SkillProvider 将技能存储在 Cloudflare R2 存储桶中。每个技能是一个 R2 对象,可选自定义元数据用于描述。
import { Session, R2SkillProvider } from "agents/experimental/memory/session";
const session = Session.create(this)
.withContext("soul", {
provider: {
get: async () =>
[
"You are a helpful assistant with access to skills.",
"When a user asks you to do something, check the SKILLS section",
"for a relevant skill and use load_context to load it.",
].join("\n"),
},
})
.withContext("memory", {
description: "Learned facts",
maxTokens: 1100,
})
.withContext("skills", {
provider: new R2SkillProvider(env.SKILLS_BUCKET, { prefix: "skills/" }),
})
.withCachedPrompt();import { Session, R2SkillProvider } from "agents/experimental/memory/session";
const session = Session.create(this)
.withContext("soul", {
provider: {
get: async () =>
[
"You are a helpful assistant with access to skills.",
"When a user asks you to do something, check the SKILLS section",
"for a relevant skill and use load_context to load it.",
].join("\n"),
},
})
.withContext("memory", {
description: "Learned facts",
maxTokens: 1100,
})
.withContext("skills", {
provider: new R2SkillProvider(env.SKILLS_BUCKET, { prefix: "skills/" }),
})
.withCachedPrompt();prefix 选项将提供商限定到存储桶中的子目录。元数据列表中的技能键显示时不带前缀,因此 skills/api-ref 在系统提示词中显示为 api-ref。
使用 keys 为 get() 与 load() 允许列表指定相对前缀的技能:
new R2SkillProvider(env.SKILLS_BUCKET, {
prefix: "skills/",
keys: ["deploy-checklist", "api-ref"],
});new R2SkillProvider(env.SKILLS_BUCKET, {
prefix: "skills/",
keys: ["deploy-checklist", "api-ref"],
});在 Wrangler 配置中添加 R2 存储桶绑定:
{
"r2_buckets": [
{
"binding": "SKILLS_BUCKET",
"bucket_name": "my-agent-skills"
}
]
}[[r2_buckets]]
binding = "SKILLS_BUCKET"
bucket_name = "my-agent-skills"技能是普通 R2 对象。通过任意 R2 接口(Wrangler CLI、仪表板或 Workers API)上传:
# Upload a skill from a file
wrangler r2 object put my-agent-skills/skills/style-guide --file ./docs/style-guide.md --content-type text/markdown要添加描述(显示在元数据列表中),在 R2 对象上设置自定义元数据:
await env.SKILLS_BUCKET.put("skills/api-ref", content, {
customMetadata: { description: "API Reference documentation" },
});await env.SKILLS_BUCKET.put("skills/api-ref", content, {
customMetadata: { description: "API Reference documentation" },
});通过实现 SkillProvider 接口,可用任意存储支撑技能:
class DatabaseSkillProvider {
db;
constructor(db) {
this.db = db;
}
async get() {
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) {
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();
}
}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();
}
}Session 通过鸭子类型检测 load() 方法并自动生成相应工具。
| 方面 | 技能 | 可写上下文 | 可搜索上下文 |
|---|---|---|---|
| 在系统提示词中 | 仅元数据列表 | 完整内容 | 摘要计数 |
| 访问模式 | 按键加载整份文档 | 始终可见 | 按查询搜索 |
| 适用场景 | 大型文档、参考资料 | 短笔记、偏好 | 大量小条目集合 |
| 上下文成本 | 低(加载前) | 与内容成正比 | 低(搜索前) |
| Agent 可写? | 可选(若实现 set) |
是(通过 set_context) |
是(通过 set_context) |
关键区别:技能是惰性的。在 Agent 决定需要某个技能之前,系统提示词中几乎不占成本。这使它们非常适合大型参考资料,其中仅子集与任意对话相关。
Session 根据上下文块的提供商类型自动生成工具。将这些工具与自有应用特定工具一并传给 LLM:
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,
});Session 根据存在的提供商类型动态生成工具:
| 工具 | 生成条件 | 作用 |
|---|---|---|
set_context |
存在任意可写、技能或搜索块 | 向命名块写入内容。可写块替换或追加。技能/搜索块写入带键条目。 |
load_context |
存在技能块 | 按键将文档完整内容加载到 Agent 上下文。 |
unload_context |
存在技能块 | 移除先前加载的文档以释放上下文空间。文档仍可重新加载。 |
search_context |
存在搜索块 | 在可搜索块内全文搜索。返回按相关性排序的前若干结果。 |
session_search |
使用 SessionManager |
跨所有会话搜索(跨对话搜索)。 |
这些工具包含描述与参数 schema,告诉 LLM 哪些块可用及其用途。Agent 根据对话决定何时及如何使用。
完整工具签名与所有 Session 方法请参阅 Session API 参考。
上下文块组装为带清晰标题与元数据的结构化系统提示词。每个块获得带标签的分区,标签指示其类型与容量:
══════════════════════════════════════════════
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标签([readonly]、[writable]、[searchable]、[loadable])告诉 LLM 每个块可进行的交互类型。Token 预算显示可写块中剩余空间,帮助 Agent 管理自身内存。
LLM 提供商(Anthropic、OpenAI 等)缓存系统提示词前缀。连续请求共享相同系统提示词时,提供商可跳过重新处理该前缀,降低延迟与成本。破坏缓存(更改系统提示词)会丧失此收益。
Session API 设计为与提示词缓存配合:
freezeSystemPrompt()在首次调用时从所有上下文块渲染系统提示词,后续调用返回缓存值。即使 Agent 通过set_context写入内存,提示词在轮次之间也不变。withCachedPrompt()将冻结的提示词持久化到存储,使其在 Durable Object 休眠与驱逐后存活。Agent 唤醒时加载相同提示词,无需从所有提供商重新获取。
当 Agent 使用 set_context 更新可写块时,底层提供商立即更新(数据已保存),但冻结的系统提示词 不会重新渲染。LLM 仅在显式调用 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();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();长对话最终会超出 LLM 的上下文窗口。压缩在两个层面解决:宏观压缩摘要较旧消息范围,微观压缩截断过大的单条消息。
宏观压缩摘要较旧消息,但从不删除原文。
它使用 覆盖层:摘要在单独表中存储,以其覆盖的消息范围为键。调用 getHistory() 时,覆盖层在读取时透明应用。被压缩的范围由合成摘要消息替换。底层消息保留在 SQLite 中,保留完整对话供审计、搜索与分支。
Messages: [1] [2] [3] [4] [5] [6] [7] [8] [9] [10]
↓ compaction ↓
Overlay: [1] [2] [SUMMARY of 3-7] [8] [9] [10]
↑ tail protected要点:
- 非破坏性,原始消息从不删除。完整对话始终在数据库中可用。
- 迭代式,对话再次增长并触发另一次压缩时,现有摘要传给 LLM 更新,而非从头替换。
- 边界感知,压缩边界会偏移,避免拆分工具调用 / 工具结果对。
- 可配置,
protectHead保留前 N 条消息(通常是系统上下文),tailTokenBudget保持最近消息完整。
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 thresholdimport { 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自动压缩在 appendMessage() 后、估计 token 数超过阈值时触发。压缩失败非致命 — 消息已保存。
微观压缩在单条消息层面而非跨范围工作。它处理两个问题:
读取时截断:truncateOlderMessages() 在发送给 LLM 前缩短较旧消息中的工具输出与长文本。最近消息(默认最后 4 条)保持完整。这在副本上操作,不修改已存储消息。
import { truncateOlderMessages } from "agents/experimental/memory/utils";
const history = await session.getHistory();
const truncated = truncateOlderMessages(history);
// Pass truncated history to the LLMimport { truncateOlderMessages } from "agents/experimental/memory/utils";
const history = await session.getHistory();
const truncated = truncateOlderMessages(history);
// Pass truncated history to the LLM行大小强制:持久化消息时(通常是带大型工具输出的 assistant 消息),检查 SQLite 行大小限制。过大的工具输出替换为预览与建议重新运行工具的说明。这防止单条消息超出存储限制,同时保留对话流程。