Sessions
Session API 为 agent 提供持久化的会话存储,带有树状结构消息(灵感来自 Pi ↗)、上下文块、压缩、全文搜索以及 AI 可控工具。它完全运行在 Durable Object SQLite 上 — 无需外部数据库。
实验性
Session API 位于 agents/experimental/memory/session 下。API 接口稳定,但在毕业到主包之前可能会演变。
快速开始
JavaScript
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 = this.session.getHistory();
const system = await this.session.freezeSystemPrompt();
const tools = await this.session.tools();
// Pass history, system prompt, and tools to your LLM
}
}
Explain Code
TypeScript
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 = this.session.getHistory();
const system = await this.session.freezeSystemPrompt();
const tools = await this.session.tools();
// Pass history, system prompt, and tools to your LLM
}
}
Explain Code
创建会话
Builder API(推荐)
使用带有可链式 builder 的 Session.create(agent)。没有显式 provider 选项的上下文 provider 会自动连接到 SQLite。
JavaScript
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
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);
直接构造
为了完全控制 provider:
JavaScript
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." } },
],
});
Explain Code
TypeScript
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." } },
],
});
Explain Code
Builder 方法
所有 builder 方法都返回 this 以支持链式调用。顺序无关紧要 — provider 在首次使用时才会被解析。
| 方法 | 描述 |
|---|---|
| Session.create(agent) | 静态工厂。agent 是任何带有 sql 标记模板方法的对象(你的 Agent 或 Durable Object)。 |
| .forSession(sessionId) | 通过 ID 命名此会话。当不使用 SessionManager 时,多会话隔离需要此项。 |
| .withContext(label, options?) | 添加上下文块。请参阅 Context blocks。 |
| .withCachedPrompt(provider?) | 启用系统提示持久化。提示在首次使用时被冻结,并在休眠和驱逐后保留。 |
| .onCompaction(fn) | 注册压缩函数。请参阅 Compaction。 |
| .compactAfter(tokenThreshold) | 当估计的 token 数超过阈值时自动压缩。需要 .onCompaction()。 |
消息
消息使用 SessionMessage 类型 — 一个最小的形状,带有 id、role、parts 和可选的 createdAt。Vercel AI SDK 的 UIMessage 在结构上兼容,可以直接传递。会话通过 parent_id 在树状结构中存储消息,启用分支会话。
JavaScript
// 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)
session.updateMessage(message);
// Delete specific messages
session.deleteMessages(["msg-1", "msg-2"]);
// Clear all messages and skill state
session.clearMessages();
Explain Code
TypeScript
// 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)
session.updateMessage(message);
// Delete specific messages
session.deleteMessages(["msg-1", "msg-2"]);
// Clear all messages and skill state
session.clearMessages();
Explain Code
注意
appendMessage() 是 async 的,因为它可能触发自动压缩。底层的 SQLite 写入是同步的。所有其他写入方法(updateMessage、deleteMessages、clearMessages)是同步的。
读取历史
JavaScript
// Linear history from root to the latest leaf
const messages = session.getHistory();
// History to a specific leaf (for branching)
const branch = session.getHistory(leafId);
// Get a single message
const msg = session.getMessage("msg-1");
// Get the newest message
const latest = session.getLatestLeaf();
// Count messages in path
const count = session.getPathLength();
Explain Code
TypeScript
// Linear history from root to the latest leaf
const messages = session.getHistory();
// History to a specific leaf (for branching)
const branch = session.getHistory(leafId);
// Get a single message
const msg = session.getMessage("msg-1");
// Get the newest message
const latest = session.getLatestLeaf();
// Count messages in path
const count = session.getPathLength();
Explain Code
分支
消息形成一棵树。当你使用已有子项的 parentId 调用 appendMessage 时,你创建了一个分支。使用 getBranches() 获取从给定点分支的所有子消息:
JavaScript
// Get all child messages that branch from messageId
const branches = session.getBranches(messageId);
TypeScript
// Get all child messages that branch from messageId
const branches = session.getBranches(messageId);
这驱动了如响应重新生成等功能 — 传入用户消息 ID,即可同时获取原始响应和重新生成的响应。getHistory(leafId) 走选择的路径。
搜索
使用 SQLite FTS5 在会话历史上进行全文搜索:
JavaScript
const results = session.search("deployment Friday", { limit: 10 });
// Returns: Array<{ id, role, content, createdAt? }>
TypeScript
const results = session.search("deployment Friday", { limit: 10 });
// Returns: Array<{ id, role, content, createdAt? }>
使用 porter stemming 和 unicode 分词。搜索覆盖会话中的所有消息。
上下文块
上下文块是注入到系统提示中的持久化键值段。每个块都有一个 label、可选的 description 和决定其行为的 provider。
Provider 类型
通过 duck-typing 检测出四种 provider 类型:
| Provider | 接口 | 行为 | AI 工具 |
|---|---|---|---|
| ContextProvider | get() | 系统提示中的只读块 | — |
| WritableContextProvider | get() + set() | 通过 AI 可写 | set_context |
| SkillProvider | get() + load() + set?() | 按需键控文档。get() 返回元数据列表;load(key) 获取完整内容。 | load_context, unload_context, set_context |
| SearchProvider | get() + search() + set?() | 全文可搜索条目。get() 返回摘要;search(query) 运行 FTS5。 | search_context, set_context |
内置 provider
AgentContextProvider — SQLite 支持的可写上下文。这是在不使用显式 provider 时使用 builder 的默认值。
JavaScript
import { AgentContextProvider } from "agents/experimental/memory/session";
new AgentContextProvider(this, "memory");
TypeScript
import { AgentContextProvider } from "agents/experimental/memory/session";
new AgentContextProvider(this, "memory");
R2SkillProvider — 用于按需文档加载的 Cloudflare R2 bucket。Skill 在系统提示中作为元数据列出;模型通过 load_context 按需加载完整内容。
JavaScript
import { R2SkillProvider } from "agents/experimental/memory/session";
Session.create(this).withContext("skills", {
provider: new R2SkillProvider(env.SKILLS_BUCKET, { prefix: "skills/" }),
});
TypeScript
import { R2SkillProvider } from "agents/experimental/memory/session";
Session.create(this).withContext("skills", {
provider: new R2SkillProvider(env.SKILLS_BUCKET, { prefix: "skills/" }),
});
AgentSearchProvider — SQLite FTS5 可搜索上下文。条目被索引并可由模型通过 search_context 搜索。
JavaScript
import { AgentSearchProvider } from "agents/experimental/memory/session";
Session.create(this).withContext("knowledge", {
description: "Searchable knowledge base",
provider: new AgentSearchProvider(this),
});
TypeScript
import { AgentSearchProvider } from "agents/experimental/memory/session";
Session.create(this).withContext("knowledge", {
description: "Searchable knowledge base",
provider: new AgentSearchProvider(this),
});
在运行时添加和删除上下文
块可以在初始化后动态添加和删除:
JavaScript
// 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();
Explain Code
TypeScript
// 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();
Explain Code
注意
addContext 和 removeContext 不会自动更新冻结的系统提示。之后调用 refreshSystemPrompt()。
读取和写入上下文
JavaScript
// 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.");
Explain Code
TypeScript
// 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.");
Explain Code
系统提示
系统提示由所有上下文块构建,带有标题和元数据:
══════════════════════════════════════════════
SOUL (Identity) [readonly]
══════════════════════════════════════════════
You are a helpful assistant.
══════════════════════════════════════════════
MEMORY (Learned facts) [45% — 495/1100 tokens]
══════════════════════════════════════════════
User likes coffee.
User prefers dark roast.
Explain Code
JavaScript
// 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
// 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();
启用 withCachedPrompt() 时,冻结的提示在 Durable Object 休眠和驱逐后仍然保留。
AI 工具
Session 自动基于上下文块的 provider 类型生成工具。将这些工具与你自己的工具一起传递给你的 LLM。
JavaScript
const tools = await session.tools();
const allTools = { ...tools, ...myTools };
TypeScript
const tools = await session.tools();
const allTools = { ...tools, ...myTools };
set_context
当存在任何可写块时生成。写入到普通块、skill 块(键控)或搜索块(键控)。强制执行 maxTokens 限制。
load_context
当存在任何 skill 块时生成。从 SkillProvider 按 key 加载完整内容。
unload_context
与 load_context 一起生成。通过卸载先前加载的 skill 来释放上下文空间。skill 仍然可用于重新加载。
search_context
当存在任何搜索块时生成。在可搜索的上下文块内进行全文搜索。返回按 FTS5 排名的前 10 个结果。
session_search
仅在 SessionManager 上可用。在所有会话中搜索。
压缩
压缩通过总结较旧的消息以将会话保持在 token 限制内。原始消息保留在 SQLite 中 — 摘要是在读取时应用的非破坏性叠加。
设置
JavaScript
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);
Explain Code
TypeScript
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);
Explain Code
压缩工作原理
- 保护头部 — 前 N 条消息从不被压缩(默认 3)
- 保护尾部 — 从末尾向后走,累积 token 直到预算(默认 20K token)
- 对齐边界 — 移动边界以避免拆分工具调用/结果对
- 总结中间 — 使用结构化格式(Topic, Key Points, Current State, Open Items)将中间部分发送给 LLM
- 存储叠加 — 保存在
assistant_compactions表中,以fromMessageId和toMessageId为键 - 迭代 — 在后续压缩中,现有摘要被传递给 LLM 进行更新而不是替换
调用 getHistory() 时,压缩叠加会被透明地应用 — 被压缩的范围被替换为合成的摘要消息。
手动压缩
JavaScript
const result = await session.compact();
// Or manage overlays directly
session.addCompaction("Summary of messages 1-50", "msg-1", "msg-50");
const overlays = session.getCompactions();
TypeScript
const result = await session.compact();
// Or manage overlays directly
session.addCompaction("Summary of messages 1-50", "msg-1", "msg-50");
const overlays = session.getCompactions();
自动压缩
设置了 .compactAfter(threshold) 时,appendMessage() 会在每次写入后检查估计的 token 数。如果超过阈值,compact() 会被自动调用。自动压缩失败是非致命的 — 消息已经被保存。
注意
Token 估算是启发式的(不是 tiktoken)。它使用 max(chars/4, words*1.3),每条消息有 4 token 的开销。Tiktoken 会增加 80–120 MB 的堆开销,这超过了 Cloudflare Workers 128 MB 的限制。
SessionManager
SessionManager 是单个 Durable Object 中多个命名会话的注册表。它提供生命周期管理、便捷方法和跨会话搜索。
创建 SessionManager
JavaScript
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
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");
上下文块、提示缓存和压缩设置会传播到通过 manager 创建的所有会话。Provider 键自动按会话 ID 命名空间化。
Builder 方法
| 方法 | 描述 |
|---|---|
| SessionManager.create(agent) | 静态工厂。 |
| .withContext(label, options?) | 为所有会话添加上下文块模板。 |
| .withCachedPrompt(provider?) | 为所有会话启用提示持久化。 |
| .onCompaction(fn) | 为所有会话注册压缩函数。 |
| .compactAfter(tokenThreshold) | 所有会话的自动压缩阈值。 |
| .withSearchableHistory(label) | 添加跨会话可搜索的历史块。模型可以从任何会话搜索过去的会话。 |
会话生命周期
JavaScript
// Create a new session
const info = manager.create("My Chat");
// Create with metadata
const info2 = manager.create("My Chat", {
parentSessionId: "parent-id",
model: "claude-sonnet-4-20250514",
source: "web",
});
// Get session metadata (null if not found)
const session = manager.get(sessionId);
// List all sessions (ordered by updated_at DESC)
const sessions = manager.list();
// Rename
manager.rename(sessionId, "New Name");
// Delete (clears messages too)
manager.delete(sessionId);
Explain Code
TypeScript
// Create a new session
const info = manager.create("My Chat");
// Create with metadata
const info2 = manager.create("My Chat", {
parentSessionId: "parent-id",
model: "claude-sonnet-4-20250514",
source: "web",
});
// Get session metadata (null if not found)
const session = manager.get(sessionId);
// List all sessions (ordered by updated_at DESC)
const sessions = manager.list();
// Rename
manager.rename(sessionId, "New Name");
// Delete (clears messages too)
manager.delete(sessionId);
Explain Code
访问会话
JavaScript
// Get or create the Session instance for an ID
// Lazy — creates on first access, caches for subsequent calls
const session = manager.getSession(sessionId);
TypeScript
// Get or create the Session instance for an ID
// Lazy — creates on first access, caches for subsequent calls
const session = manager.getSession(sessionId);
消息便捷方法
这些方法委托给底层 Session,并更新会话的 updated_at 时间戳:
JavaScript
// 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 = manager.getHistory(sessionId, leafId);
// Message count
const count = manager.getMessageCount(sessionId);
// Clear messages
manager.clearMessages(sessionId);
// Delete specific messages
manager.deleteMessages(sessionId, ["msg-1"]);
Explain Code
TypeScript
// 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 = manager.getHistory(sessionId, leafId);
// Message count
const count = manager.getMessageCount(sessionId);
// Clear messages
manager.clearMessages(sessionId);
// Delete specific messages
manager.deleteMessages(sessionId, ["msg-1"]);
Explain Code
Forking
在特定消息处 fork 会话 — 将历史复制到该点的新会话:
JavaScript
const forked = await manager.fork(sessionId, atMessageId, "Forked Chat");
// forked.parent_session_id === sessionId
TypeScript
const forked = await manager.fork(sessionId, atMessageId, "Forked Chat");
// forked.parent_session_id === sessionId
用量跟踪
JavaScript
manager.addUsage(sessionId, inputTokens, outputTokens, cost);
TypeScript
manager.addUsage(sessionId, inputTokens, outputTokens, cost);
跨会话搜索
JavaScript
// Search across all sessions (FTS5)
const results = manager.search("deployment Friday", { limit: 20 });
// Get tools for the model (includes session_search)
const tools = manager.tools();
TypeScript
// Search across all sessions (FTS5)
const results = manager.search("deployment Friday", { limit: 20 });
// Get tools for the model (includes session_search)
const tools = manager.tools();
自定义 provider
实现四个 provider 接口中的任一个,即可插入你自己的存储:
JavaScript
// 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),
};
Explain Code
TypeScript
// 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),
};
Explain Code
你也可以实现 SessionProvider 来完全替换 SQLite 存储:
JavaScript
const myStorage = {
getMessage(id) {
/* ... */
},
getHistory(leafId) {
/* ... */
},
getLatestLeaf() {
/* ... */
},
getBranches(messageId) {
/* ... */
},
getPathLength(leafId) {
/* ... */
},
appendMessage(message, parentId) {
/* ... */
},
updateMessage(message) {
/* ... */
},
deleteMessages(messageIds) {
/* ... */
},
clearMessages() {
/* ... */
},
addCompaction(summary, fromId, toId) {
/* ... */
},
getCompactions() {
/* ... */
},
searchMessages(query, limit) {
/* ... */
},
};
Explain Code
TypeScript
const myStorage: SessionProvider = {
getMessage(id) { /* ... */ },
getHistory(leafId?) { /* ... */ },
getLatestLeaf() { /* ... */ },
getBranches(messageId) { /* ... */ },
getPathLength(leafId?) { /* ... */ },
appendMessage(message, parentId?) { /* ... */ },
updateMessage(message) { /* ... */ },
deleteMessages(messageIds) { /* ... */ },
clearMessages() { /* ... */ },
addCompaction(summary, fromId, toId) { /* ... */ },
getCompactions() { /* ... */ },
searchMessages(query, limit) { /* ... */ },
};
Explain Code
存储表
所有存储都在 Durable Object SQLite 中。表在首次使用时延迟创建。
| 表 | 用途 |
|---|---|
| assistant_messages | 树状结构消息,带有 id、session_id、parent_id、role、content (JSON)、created_at |
| assistant_compactions | 压缩叠加,带有 summary、from_message_id、to_message_id |
| assistant_fts | 用于消息搜索的 FTS5 虚表(porter stemming, unicode 分词) |
| assistant_sessions | 会话注册表(仅 SessionManager),带有 name、parent_session_id、model、source、token/cost 计数器 |
| cf_agents_context_blocks | 持久化的上下文块存储(AgentContextProvider) |
| cf_agents_search_entries / cf_agents_search_fts | 可搜索的上下文条目和 FTS5 索引(AgentSearchProvider) |
鸣谢
- Session 的树状结构消息灵感来自 Pi ↗。
- 上下文块灵感来自 Letta AI memory blocks ↗。
- 块的格式化灵感来自 Hermes Agent ↗。
相关
- Think — 通过
configureSession()使用 Session 进行对话存储的有主张的 chat agent - Chat agents —
AIChatAgent带有自己的消息持久化层 - Store and sync state —
setState()用于更简单的键值持久化