跳转到内容
搜索文档

会话

最后更新 查看 MarkdownAgent 设置

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 API(推荐)

使用可链式 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 方法

所有 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 类型——包含 idroleparts 与可选 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 休眠与驱逐后保留。

AI 工具

Session 会根据上下文块的提供方类型自动生成工具。将这些工具与你自己的工具一起传给 LLM。

const tools = await session.tools();
const allTools = { ...tools, ...myTools };
const tools = await session.tools();
const allTools = { ...tools, ...myTools };

set_context

当存在任何可写块时生成。写入常规块、技能块(按键)或搜索块(按键)。强制执行 maxTokens 限制。

load_context

当存在任何技能块时生成。从 SkillProvider 按键加载完整内容。

unload_context

load_context 一起生成。通过卸载先前加载的技能释放上下文空间。技能仍可重新加载。

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

压缩工作原理

  1. 保护头部(Protect head) — 前 N 条消息永不压缩(默认 3)
  2. 保护尾部(Protect tail) — 从末尾向前遍历,累积 token 直至预算(默认 20K tokens)
  3. 对齐边界(Align boundaries) — 调整边界以避免拆分工具调用/结果对
  4. 摘要中间段(Summarize middle) — 将中间段发送给 LLM,使用结构化格式(主题、要点、当前状态、待办项)
  5. 存储叠加层(Store overlay) — 保存在 assistant_compactions 表中,以 fromMessageIdtoMessageId 为键
  6. 迭代(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

SessionManager 是单个 Durable Object 内多个命名会话的注册表。它提供生命周期管理、便捷方法与跨会话搜索。

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

通过管理器创建的会话会传播上下文块、prompt 缓存与压缩设置。提供方键会按会话 ID 自动划分命名空间。

Builder 方法

方法 描述
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"]);

Fork

在指定消息处分叉会话——将该点之前的历史复制到新会话:

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

压缩辅助方法

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

自定义 provider

实现四种提供方接口之一以接入自有存储:

// 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) {
		/* ... */
	},
};

Postgres 提供方

默认情况下,Session 存储使用 Durable Object SQLite 并延迟创建表。若需要在外部 Postgres 数据库中存储会话数据以进行跨 Agent 查询、分析或共享存储,请使用 PostgresSessionProviderPostgresContextProviderPostgresSearchProvider

这些提供方通过 Hyperdrive 与 Postgres 兼容数据库配合,实现连接池。

1. 创建 Hyperdrive 配置

为 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 配置在靠近数据库的位置以降低查询延迟。

2. 创建表

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

3. 接入

安装 pg,然后从 Hyperdrive 连接字符串创建客户端并传给 Postgres 提供方:

npm i pg
import { 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 树形消息,含 idsession_idparent_idrolecontent(JSON)、created_at
assistant_compactions 压缩叠加层,含 summaryfrom_message_idto_message_id
assistant_fts 用于消息搜索的 FTS5 虚拟表(porter stemming、unicode tokenization)
assistant_sessions 会话注册表(仅 SessionManager),含 nameparent_session_idmodelsource、token/成本计数器
cf_agents_context_blocks 持久化上下文块存储(AgentContextProvider
cf_agents_search_entries / cf_agents_search_fts 可搜索上下文条目与 FTS5 索引(AgentSearchProvider

致谢

相关

  • Think — 通过 configureSession() 使用 Session 存储对话的约定式聊天 agent
  • 聊天 Agent — 自带消息持久化层的 AIChatAgent
  • 存储与同步状态 — 更简单的键值持久化用 setState()

这篇文档对您有帮助吗?