Session API 为 Agent 提供持久化对话存储,支持树形消息(灵感来自 Pi ↗)、上下文块、压缩、全文搜索与 AI 可控工具。默认使用 Durable Object SQLite。需要共享数据库访问、分析或跨 Durable Object 查询的应用也可使用外部 Postgres 存储。
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
}
}使用可链式 builder 的 Session.create(agent)。未显式设置 provider 选项的上下文提供方会自动连接到 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);如需完全控制提供方:
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 方法返回 this 以便链式调用。顺序无关紧要——提供方在首次使用时延迟解析。
| 方法 | 描述 |
|---|---|
Session.create(agent) |
静态工厂。agent 为任意带 sql 模板标签方法的对象(你的 Agent 或 Durable Object)。 |
.forSession(sessionId) |
按 ID 为此会话划分命名空间。未使用 SessionManager 时,多会话隔离必需。 |
.withContext(label, options?) |
添加上下文块。见 上下文块。 |
.withCachedPrompt(provider?) |
启用系统提示词持久化。提示词在首次使用时冻结,并在休眠与驱逐后仍然保留。 |
.onCompaction(fn) |
注册压缩函数。见 压缩。 |
.compactAfter(tokenThreshold, options?) |
当估计 token 数超过阈值时自动压缩。需要 .onCompaction()。传入 { tokenCounter } 控制阈值测量方式。 |
.onCompactionError(handler) |
处理自动压缩错误。handler 失败会被吞掉,使消息写入保持非致命。 |
消息使用 SessionMessage 类型——包含 id、role、parts 与可选 createdAt 的最小结构。AI SDK 的 UIMessage 结构兼容,可直接传入。会话通过 parent_id 以树形结构存储消息,支持分支对话。
// 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();// 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();消息形成树形结构。当 appendMessage 使用的 parentId 已有子节点时,会创建分支。使用 getBranches() 获取从某点分支出去的所有子消息:
// 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);这支持响应重新生成等功能——传入 user message ID 可同时获得原始与重新生成的响应。getHistory(leafId) 遍历所选路径。
使用 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 支持的会话使用带 porter 词干提取与 unicode 分词的 FTS5。Postgres 支持的会话使用提供商的 Postgres 全文索引。若会话提供商不支持搜索,search() 会抛出。
上下文块是注入系统提示词的持久化键值分区。每个块有 标签、可选 描述,以及决定行为的 提供商。
通过鸭子类型检测四种提供方类型:
| Provider | 接口 | 行为 | AI tool |
|---|---|---|---|
| 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 |
AgentContextProvider — SQLite 支持的可写上下文。builder 未显式提供提供方时为默认。
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 存储桶。技能以元数据形式列在系统提示词中;模型通过 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 可搜索上下文。条目被索引,模型可通过 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),
});Block 可在初始化后动态添加与移除:
// 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();// 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.");系统提示词由所有上下文块以及页眉与元数据构建:
══════════════════════════════════════════════
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();启用 withCachedPrompt() 时,已冻结的 prompt 可在 Durable Object 休眠与驱逐后保留。
Session 会根据上下文块的提供方类型自动生成工具。将这些工具与你自己的工具一起传给 LLM。
const tools = await session.tools();
const allTools = { ...tools, ...myTools };const tools = await session.tools();
const allTools = { ...tools, ...myTools };当存在任何可写块时生成。写入常规块、技能块(按键)或搜索块(按键)。强制执行 maxTokens 限制。
当存在任何技能块时生成。从 SkillProvider 按键加载完整内容。
与 load_context 一起生成。通过卸载先前加载的技能释放上下文空间。技能仍可重新加载。
当存在任何搜索块时生成。在可搜索上下文块内进行全文搜索。按 FTS5 排名返回前 10 条结果。
仅在 SessionManager 上可用。跨所有会话搜索。
压缩会摘要较旧消息,以保持对话在 token 限制内。原始消息保留在 SQLite 中——摘要是读取时应用的非破坏性叠加层。
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);- 保护头部(Protect head) — 前 N 条消息永不压缩(默认 3)
- 保护尾部(Protect tail) — 从末尾向前遍历,累积 token 直至预算(默认 20K tokens)
- 对齐边界(Align boundaries) — 调整边界以避免拆分工具调用/结果对
- 摘要中间段(Summarize middle) — 将中间段发送给 LLM,使用结构化格式(主题、要点、当前状态、待办项)
- 存储叠加层(Store overlay) — 保存在
assistant_compactions表中,以fromMessageId与toMessageId为键 - 迭代(Iterative) — 后续压缩时,将现有摘要传给 LLM 更新而非替换
调用 getHistory() 时,压缩叠加层会透明应用——被压缩的范围由合成的摘要消息替换。
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();设置 .compactAfter(threshold) 时,每次写入后 appendMessage() 会检查估计 token 数。若超过阈值,会自动调用 compact()。自动压缩失败是非致命的——消息已保存。
默认情况下,估计值包括已存储消息部分以及 Session 管理的冻结系统提示词,因此 Session 管理的上下文块与缓存提示词会计入阈值。不包括 Session 之外发生的框架特定提示词追加或工具 schema 序列化。
有两个 token 计数决策:
.compactAfter(threshold, { tokenCounter })控制写入后何时触发自动压缩。createCompactFunction({ tokenCounter })控制哪些尾部消息受保护免于摘要。当工具密集历史远大于 Workers 安全启发式可估计时使用。
通常只需配置一个计数器。当未显式提供 createCompactFunction({ tokenCounter }) 时,.compactAfter() 的计数器也会流入 createCompactFunction 的边界遍历(通过 CompactContext),因此单个计数器同时驱动「是否压缩?」与「压缩什么?」。
当你有模型报告的用量或自己的分词器时,使用自定义计数器:
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);
});SessionManager 是单个 Durable Object 内多个命名会话的注册表。它提供生命周期管理、便捷方法与跨会话搜索。
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");通过管理器创建的会话会传播上下文块、prompt 缓存与压缩设置。提供方键会按会话 ID 自动划分命名空间。
| 方法 | 描述 |
|---|---|
SessionManager.create(agent) |
静态工厂。 |
.withContext(label, options?) |
为所有会话添加上下文块模板。 |
.withCachedPrompt(provider?) |
为所有会话启用提示词持久化。 |
.onCompaction(fn) |
为所有会话注册压缩函数。 |
.compactAfter(tokenThreshold, options?) |
所有会话的自动压缩阈值。支持与 Session 相同的 tokenCounter 选项。 |
.onCompactionError(handler) |
处理托管会话的自动压缩错误。 |
.withSearchableHistory(label) |
添加跨会话可搜索的历史块。模型可搜索任意会话的历史对话。 |
// 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);// 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);这些方法委托给底层 Session 并更新会话的 updated_at 时间戳:
// 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"]);在指定消息处分叉会话——将该点之前的历史复制到新会话:
const forked = await manager.fork(sessionId, atMessageId, "Forked Chat");
// forked.parent_session_id === sessionIdconst forked = await manager.fork(sessionId, atMessageId, "Forked Chat");
// forked.parent_session_id === sessionId// 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() 创建带摘要消息的新会话,而不是原地叠加。原会话标记为 end_reason: "compaction"。
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();实现四种提供方接口之一以接入自有存储:
// 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),
};// 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),
};也可实现 SessionProvider 以完全替换 SQLite 存储:
const myStorage = {
async getMessage(id) {
/* ... */
},
async getHistory(leafId) {
/* ... */
},
async getLatestLeaf() {
/* ... */
},
async getBranches(messageId) {
/* ... */
},
async getPathLength(leafId) {
/* ... */
},
async appendMessage(message, parentId) {
/* ... */
},
async updateMessage(message) {
/* ... */
},
async deleteMessages(messageIds) {
/* ... */
},
async clearMessages() {
/* ... */
},
async addCompaction(summary, fromId, toId) {
/* ... */
},
async getCompactions() {
/* ... */
},
async searchMessages(query, limit) {
/* ... */
},
};const myStorage: SessionProvider = {
async getMessage(id) {
/* ... */
},
async getHistory(leafId?) {
/* ... */
},
async getLatestLeaf() {
/* ... */
},
async getBranches(messageId) {
/* ... */
},
async getPathLength(leafId?) {
/* ... */
},
async appendMessage(message, parentId?) {
/* ... */
},
async updateMessage(message) {
/* ... */
},
async deleteMessages(messageIds) {
/* ... */
},
async clearMessages() {
/* ... */
},
async addCompaction(summary, fromId, toId) {
/* ... */
},
async getCompactions() {
/* ... */
},
async searchMessages(query, limit) {
/* ... */
},
};默认情况下,Session 存储使用 Durable Object SQLite 并延迟创建表。若需要在外部 Postgres 数据库中存储会话数据以进行跨 Agent 查询、分析或共享存储,请使用 PostgresSessionProvider、PostgresContextProvider 与 PostgresSearchProvider。
这些提供方通过 Hyperdrive 与 Postgres 兼容数据库配合,实现连接池。
为 Postgres 数据库创建 Hyperdrive 配置:
npx wrangler hyperdrive create my-session-db \
--connection-string="postgresql://user:password@host:port/dbname"然后在 wrangler.jsonc 中添加 Hyperdrive 绑定:
{
"$schema": "./node_modules/wrangler/config-schema.json",
"compatibility_flags": [
"nodejs_compat"
],
"hyperdrive": [
{
"binding": "HYPERDRIVE",
"id": "<your-hyperdrive-id>"
}
],
"placement": {
"mode": "smart"
}
}compatibility_flags = ["nodejs_compat"]
[[hyperdrive]]
binding = "HYPERDRIVE"
id = "<your-hyperdrive-id>"
[placement]
mode = "smart"若已知数据库 region,可将 placement 配置在靠近数据库的位置以降低查询延迟。
Postgres 用户可能没有运行时建表权限。请在数据库控制台中运行一次架构:
CREATE TABLE IF NOT EXISTS assistant_messages (
id TEXT NOT NULL,
session_id TEXT NOT NULL DEFAULT '',
parent_id TEXT,
role TEXT NOT NULL,
content TEXT NOT NULL,
text_content TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ DEFAULT NOW(),
content_tsv TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', text_content)) STORED,
PRIMARY KEY (session_id, id)
);
CREATE INDEX IF NOT EXISTS idx_assistant_msg_parent
ON assistant_messages (parent_id);
CREATE INDEX IF NOT EXISTS idx_assistant_msg_session
ON assistant_messages (session_id);
CREATE INDEX IF NOT EXISTS idx_assistant_msg_fts
ON assistant_messages USING GIN (content_tsv);
CREATE TABLE IF NOT EXISTS assistant_compactions (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL DEFAULT '',
summary TEXT NOT NULL,
from_message_id TEXT NOT NULL,
to_message_id TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS cf_agents_context_blocks (
label TEXT PRIMARY KEY,
content TEXT NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS cf_agents_search_entries (
label TEXT NOT NULL,
key TEXT NOT NULL,
content TEXT NOT NULL,
content_tsv TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', content)) STORED,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (label, key)
);
CREATE INDEX IF NOT EXISTS idx_search_entries_fts
ON cf_agents_search_entries USING GIN (content_tsv);安装 pg,然后从 Hyperdrive 连接字符串创建客户端并传给 Postgres 提供方:
npm i pgyarn add pgpnpm add pgbun add pgimport { Agent } from "agents";
import {
PostgresContextProvider,
PostgresSearchProvider,
PostgresSessionProvider,
Session,
} from "agents/experimental/memory/session";
import { Client } from "pg";
export class MyAgent extends Agent {
session;
pgClient;
async onStart() {
const client = new Client({
connectionString: this.env.HYPERDRIVE.connectionString,
});
await client.connect();
this.pgClient = client;
const sessionId = this.ctx.id.toString();
this.session = Session.create(
new PostgresSessionProvider(client, sessionId),
)
.withContext("soul", {
provider: {
get: async () => "You are a helpful assistant.",
},
})
.withContext("memory", {
description: "Short facts",
maxTokens: 1100,
provider: new PostgresContextProvider(client, `memory_${sessionId}`),
})
.withContext("knowledge", {
description: "Searchable knowledge base",
provider: new PostgresSearchProvider(client),
})
.withCachedPrompt(
new PostgresContextProvider(client, `_prompt_${sessionId}`),
);
}
}import { Agent } from "agents";
import {
PostgresContextProvider,
PostgresSearchProvider,
PostgresSessionProvider,
Session,
} from "agents/experimental/memory/session";
import { Client } from "pg";
export class MyAgent extends Agent<Env> {
private session?: Session;
private pgClient?: Client;
async onStart(): Promise<void> {
const client = new Client({
connectionString: this.env.HYPERDRIVE.connectionString,
});
await client.connect();
this.pgClient = client;
const sessionId = this.ctx.id.toString();
this.session = Session.create(
new PostgresSessionProvider(client, sessionId),
)
.withContext("soul", {
provider: {
get: async () => "You are a helpful assistant.",
},
})
.withContext("memory", {
description: "Short facts",
maxTokens: 1100,
provider: new PostgresContextProvider(client, `memory_${sessionId}`),
})
.withContext("knowledge", {
description: "Searchable knowledge base",
provider: new PostgresSearchProvider(client),
})
.withCachedPrompt(
new PostgresContextProvider(client, `_prompt_${sessionId}`),
);
}
}当 Session.create() 接收 SessionProvider 而非 SQLite 支持的提供方时,会跳过 SQLite 自动接线:
- 上下文块需要显式提供方。 每个应持久化数据的
withContext()调用都需要provider选项。 withCachedPrompt()需要显式提供方。 传入PostgresContextProvider以持久化已冻结的系统提示词。- Session 方法为异步。 读写时使用
await,使相同代码在本地 SQLite 与外部存储上都能工作。 - 跳过 Broadcaster 支持。 会话事件的 WebSocket 状态广播仅适用于 SQLite 支持的会话。
freezeSystemPrompt() 从存储返回缓存的 prompt。首次调用时从提供方加载上下文块、渲染 prompt 并持久化。后续调用直接返回存储值,不重新渲染。
使用 refreshSystemPrompt() 强制重新加载上下文块、重新渲染 prompt 并更新存储值。
默认存储在 Durable Object SQLite 中,表在首次使用时延迟创建。Postgres 支持的会话使用 Postgres 提供方章节所示的外部表。
| 表 | 用途 |
|---|---|
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 tokenization) |
assistant_sessions |
会话注册表(仅 SessionManager),含 name、parent_session_id、model、source、token/成本计数器 |
cf_agents_context_blocks |
持久化上下文块存储(AgentContextProvider) |
cf_agents_search_entries / cf_agents_search_fts |
可搜索上下文条目与 FTS5 索引(AgentSearchProvider) |
- Session 的树形消息灵感来自 Pi ↗。
- 上下文块灵感来自 Letta AI memory blocks ↗。
- Block 格式化灵感来自 Hermes Agent ↗。