Cloudflare Agents 持久化会话存储实战:基于 Durable Object 的 Sessions 流式历史、分支与压缩覆盖

发布时间:2026/9/18 23:32:48
Cloudflare Agents 持久化会话存储实战:基于 Durable Object 的 Sessions 流式历史、分支与压缩覆盖 Cloudflare Agents 持久化会话存储实战基于 Durable Object 的 Sessions 流式历史、分支与压缩覆盖【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agents本文以仓库中 examples/next/sessions 示例为主线讲解如何在普通 Durable Object 上安装agents/sessions能力实现树形消息结构、流式历史读取、分支导航与非破坏性压缩覆盖compaction overlay并深入行分块row chunking与大消息存储的底层原理。读完本文你将能独立搭建一个服务端会话存储示例、用 curl 完整演练全部 HTTP 端点并理解为何 没有一条消息会因过大而无法存储。一、Sessions 是什么消息存储而非文件存储agents/sessions是 Cloudflare Agents 提供的持久化会话历史能力它的定位非常明确Sessions 存储的是消息MESSAGES不是文件。消息的 JSON 存放在 Durable Object SQLite 中当某条消息的序列化 JSON 超过单行预算1.5 MiB时它会被拆分到多行续行continuation rows中读取时再重新拼接因此往返是字节精确的——不截断、不丢失也没有任何一条消息会大到无法存储。续行与它所属的消息存放在同一个 Durable Object中所以一个 Durable Object 的 10 GB 存储上限才是单条会话真正能容纳内容量的边界Sessions 自身不设置任何每消息上限。这也带来一条重要的工程建议处理文件的应用程序应当把文件放在文件存储中而在消息里只保存引用。需要特别说明的是Sessions 是experimental能力agents/sessions导出的 API 在稳定之前可能随版本变化。二、快速运行示例示例位于 examples/next/sessions是一个纯服务端server-only示例。安装依赖并启动本地开发服务器pnpm install pnpm run devwrangler dev默认将服务运行在http://localhost:8787对应 wrangler 默认端口随后即可对命名对象demo执行一系列 HTTP 调用。写入根消息root message# Append a root message. curl -X POST http://localhost:8787/agents/session-object/demo/messages \ -H content-type: application/json \ -d {id:m1,role:user,parts:[{type:text,text:hello}]}追加子消息省略 parent 时自动挂到当前活动叶子节点# Append a child. Omit parent to use the active leaf automatically. curl -X POST http://localhost:8787/agents/session-object/demo/messages?parentm1 \ -H content-type: application/json \ -d {id:m2,role:assistant,parts:[{type:text,text:hi}]}流式读取活动路径的历史NDJSON一行一条消息# Stream the active path as newline-delimited JSON. curl -N http://localhost:8787/agents/session-object/demo/history流式读取以某个叶子节点结尾的路径# Stream the path ending at a chosen leaf. curl -N http://localhost:8787/agents/session-object/demo/history?leafm2列出某条消息的所有子节点分支导航# List children of m1. curl http://localhost:8787/agents/session-object/demo/branches/m1添加非破坏性压缩覆盖compaction overlay# Add a non-destructive compaction overlay. curl -X POST http://localhost:8787/agents/session-object/demo/compactions \ -H content-type: application/json \ -d {summary:The user greeted the assistant.,fromMessageId:m1,toMessageId:m2}compactions端点接收summary摘要文本、fromMessageId与toMessageId覆盖的消息区间。所谓非破坏性是指原始消息行不会被删除只是在路径读取时被摘要覆盖详见第六节。三、示例代码结构examples/next/sessions/ ├── src/ │ └── index.ts # 唯一的 Worker 入口SessionObject fetch 路由 ├── README.md # 使用说明本文的主体依据 ├── env.d.ts # wrangler types 生成的类型声明 ├── package.json # 脚本与依赖 ├── tsconfig.json # 继承 agents/tsconfig └── wrangler.jsonc # Worker 与 Durable Object 配置整个示例只有一个源码文件 src/index.ts这正是在普通 Durable Object 上安装 Sessions这一思路的极简体现。四、核心实现解析一个普通 Durable Object 上的 Sessions示例的核心类是SessionObject它继承自DurableObjectEnv与 Cloudflare 的普通 Durable Object 并无区别只是通过 Lifecycle 安装use了Sessions能力import { DurableObject } from cloudflare:workers; import { routeAgentRequest } from agents; import { Lifecycle } from agents/lifecycle; import { Sessions, type SessionMessage } from agents/sessions; /** A plain Durable Object with durable conversation history. */ export class SessionObject extends DurableObjectEnv { readonly sessions new Sessions(); readonly lifecycle Lifecycle.install(this).use(this.sessions); readonly session this.sessions.session(); // ... }三个关键字段各司其职sessions new Sessions()创建会话能力实例。从源码 packages/agents/src/sessions/sessions.ts 可以看到Sessions是一个LifecycleCapability它只依赖 storage 与 events 两项标准能力服务不需要 alarm因此也能运行在没有独立 alarm 槽位的 facets 上。lifecycle Lifecycle.install(this).use(this.sessions)把 Sessions 注册进生命周期onStart()会在启动时执行建表与旧数据迁移源码中通过cf_agents:sessions_schema_version键记录当前 schema 版本见 sessions.ts。session this.sessions.session()获取默认会话ID 为空字符串的句柄。在一个 Durable Object 只持有单条会话的模型下空 ID 是主路径。4.1 路由与 HTTP 端点映射onRequest把routeAgentRequest分派不到的路由按路径后缀和 HTTP 方法逐一处理端点与底层Session句柄 API 的映射关系如下HTTP 端点方法底层调用handle 方法说明/messagesPOSTsession.appendMessage追加消息?parent指定父节点/historyGETsession.history({ leafId, signal })以 NDJSON 流式返回路径/messages/:idGETsession.getMessage(id)按 ID 读取单条消息/branches/:idGETsession.getBranches(id)返回某消息的全部子节点/compactionsPOSTsession.addCompaction存储压缩覆盖/messagesDELETEsession.clearMessages()清空会话/searchGETsession.search(q)全文搜索索引按需构建/兜底GETsession.getHistoryRowStats()返回对象名、消息数与叶子 ID全部实现都在 src/index.ts。兜底分支还顺便演示了getHistoryRowStats()返回活动分支路径上每行的存储字节数与写入时盖章的 token 估算值且不加载消息内容。4.2 消息校验parseMessageparseMessage是写入前的结构校验器它要求一条消息必须具备id: string、role: string与parts: array且每个 part 都必须带有type: string。校验通过后以value as SessionMessage形式返回part 上的其它字段text、mediaType、url、filename等对 Sessions 保持透明原样往返round-trip为 JSON。非法消息返回400 Invalid SessionMessage。这一结构定义与 Vercel AI SDK 的UIMessage/UIMessagePart结构兼容——你可以直接传入UIMessage对象而无需转换见 packages/agents/src/sessions/types.ts。4.3 流式响应historyResponse 与 NDJSONhistoryResponse把session.history()返回的AsyncGeneratorSessionMessage包装成ReadableStreamUint8Array逐条序列化为JSON.stringify(message) \n响应头为content-type: application/x-ndjson。pull在迭代完成后关闭流、出错时controller.errorcancel时调用history.return()及时终止底层迭代器——这正是curl -N能即时看到逐条消息的原因。4.4 追加语义appendMessage 与 parentId从 handle.ts 的实现看追加消息遵循如下parentId语义省略 /undefined自动挂到当前活动叶子latest leafnull创建一条没有父节点的根消息字符串挂到指定父节点若该父节点不属于本会话则回退到根。写入流程是一条固定管线source为client时先剥离保留的 metadata 键reservedMetadataKeys在构造Sessions时配置随后在一次同步 SQLite 事务里提交消息行及其续行。appendMessage以消息 ID 幂等重复追加相同 ID 会返回已存储的行并派发inserted: false的append变更事件。示例中对result.inserted分别返回201与200状态码正对应这一语义。五、行分块1.5 MiB 预算与字节精确拆分这是示例 README 最强调的机制。Sessions 在 packages/agents/src/sessions/chunking.ts 中定义行预算常量export const MAX_INLINE_ROW_BYTES 1536 * 1024; // 1.5 MiBSQLite 对单个 Durable Object 行的字节数有上限因此序列化 JSON不超过 1.5 MiB的绝大多数消息只占一行计费也正好是一次行写入超过预算的消息被splitContent切成若干片切片 0 放在消息行其余成为编号从 1 开始的续行存放在cf_agents_session_message_chunks表读取时把切片拼接回原始字符串。拆分按UTF-8 字节边界进行SQLite 的限制是字节数而一个字符最多占 4 字节并且绝不会把边界切在代理对surrogate pair的中间——因为单独的高代理不是合法 UTF-8无法无损往返。splitContent(s).join()恒等于s所以一条塞满 emoji 或 CJK 文本的消息与纯 ASCII 消息一样字节精确。对使用者而言这一切完全透明没有指针、没有重建模式、也没有任何读取选项。文档 docs/agents/sessions.md 给出过一个直观的例子——一条 5 MB 的消息等于 1 个消息行加 3 个续行读取回来逐字节一致没有需要捕获的错误也没有需要配置的东西。值得强调的两点代价与边界拆分不是压缩续行与消息在同一个 Durable Object、同一个 10 GB 上限之内。计费按写入的行数而非字节数——500 KB 的消息与一条极小的消息同样只计一行2 MB 的消息则计两行。Sessions 没有消息上限是刻意设计因此对不可信输入的大小限制是应用程序自己的责任。appendMessage(message, { source: client })只做 metadata 净化与保留键剥离不会限制大小——如果客户端可以直接写入会话请在 append 之前检查载荷尺寸。六、分支与压缩覆盖树形消息与不删行的摘要Sessions 的消息是树形结构一条消息可以有多个子节点。getBranches(parentId)返回某条消息的所有子节点要读取一条根到叶的路径只需在读取时指定其叶子节点如/history?leafm2。压缩覆盖compaction overlay则解决长会话的上下文问题它在读取时用摘要替换一段区间但绝不删除原始行。底层Session句柄还提供了更完整的编程式 API见 handle.tssession .onCompaction(createCompactFunction({ summarize: async (prompt) summarize(prompt), keepRecentTokens: 20_000 })) .compactAfter(80_000);onCompaction注册摘要函数compactAfter(threshold)在追加后当累计 token 估算越过阈值时自动压缩。Sessions 在写入每行时盖章一个 token 估算值所以compactAfter只读取 O(1) 的聚合值做判断从不读取完整对话来决定是否压缩。自动压缩失败是非致命的记录日志、派发session:error能力事件对话内容保持原样。createCompactFunction恰好接受两个选项summarize用 prompt 调模型并返回文本与keepRecentTokens保留原文的最近 token 预算默认 20,000。前三条消息作为头部保留原文、至少最近两条作为尾部保留且边界对齐到工具调用与它的结果不会被拆开。addCompaction(summary, fromMessageId, toMessageId)则直接存储一条已生成的摘要覆盖这正是示例POST /compactions端点背后的调用。七、文件 part内联 data: URL 与类型而非大小的规则示例 README 给出了携带文件的示例消息。文件 part 以内联data:URL 形式携带作为消息内容与其他 part 一样参与拆分与回读{ id: image-1, role: user, parts: [ { type: text, text: inspect this image }, { type: file, mediaType: image/png, filename: screen.png, url: data:image/png;base64,... } ] }按 ID 读回这条消息curl http://localhost:8787/agents/session-object/demo/messages/image-1从 docs/agents/sessions.md 的说明可以确认这里存在一套基于类型而非大小的附件机制声明非文本mediaType且内联携带字节的 part图片、音频、PDF其载荷会被移到消息行之外、按内容哈希去重存放part 保留自身形状与mediaType仅载荷被替换为attachment:sha256:hex指针读取时再放回去因此从外部看仍是逐字节一致的。图片无论 8 KB 还是 8 MB 都走这条路径文本无论多大都走续行拆分——两条机制互不干扰因为媒体在测量消息行之前就已被移出。需要明确成本媒体不进消息行并非免费。一张 200 KB 的图片计费约 4 次行写入消息 一个载荷块 元数据 一个引用而内联只需 1 次2 MiB 的图片约 5 次。得到的回报是消息行无论载荷多大都保持在几百字节且SessionRowStat.bytes仍把指向的载荷计入每消息计费因此传给getRecentHistory()的字节预算依然能约束水合hydration实际占用的内存。八、配置与部署wrangler.jsonc 中的 Durable Object 声明示例的 wrangler.jsonc 完整展示了运行前提——Sessions 依赖 Durable Object SQLite必须显式声明绑定与迁移{ $schema: ./node_modules/wrangler/config-schema.json, name: next-sessions, main: src/index.ts, compatibility_date: 2026-06-11, compatibility_flags: [nodejs_compat], durable_objects: { bindings: [ { name: SessionObject, class_name: SessionObject } ] }, migrations: [ { tag: v1, new_sqlite_classes: [SessionObject] } ], observability: { enabled: true } }要点durable_objects.bindings将SessionObject类暴露为绑定migrations中new_sqlite_classes声明这是一个使用 SQLite 存储的新Durable Object 类——Sessions 的cf_agents_session_*表正是建在这份 SQLite 上。compatibility_flags: [nodejs_compat]提供 Node.js 兼容运行时TextEncoder、crypto.randomUUID()等 API 依赖它。compatibility_date与nodejs_compat标志请以你的 wrangler/运行时版本为准此处为当前仓库示例的实际值。env.d.ts由wrangler types生成声明了SessionObject命名空间与Env接口保证this.lifecycle、this.session等字段的类型推导。package.json 脚本package.json 提供四个脚本脚本命令用途devwrangler dev本地开发运行deploywrangler deploy部署到 Cloudflaretypechecktsc --noEmit类型检查typeswrangler types env.d.ts --include-runtime false重新生成env.d.ts依赖仅有agents一个运行时包wrangler与cloudflare/workers-types为开发依赖。九、进阶阅读与延伸能力本文示例只是 Sessions 能力的一个切面。仓库内 docs/agents/sessions.md 是完整的官方文档还覆盖了以下值得进一步探索的内容存储经济性Durable Object SQLite 上一次行写入的代价约为行读取的 1000 倍因此 schema 被设计成一次逻辑写入 一次计费行写入。所有 Sessions 表都是WITHOUT ROWID 复合主键、无二级索引活动叶子就是会话中最大seq的行token 总量来自逐行盖章的估算值——状态是推导出来的而不是维护出来的。流式读取的字节预算history()先读取不含内容的 ID 行大小路径再按 50 行 / 4 MiB 的有界窗口抓取内容newestFirst: true时最新内容窗口先取提前跳出循环就不会读取更旧的行。getRecentHistory(maxContentBytes)是启动期水合的内存硬上限SessionRowStat.bytes计整条消息含续行所以预算真正约束水合内存但没有消息数量下限——它返回能装下的最长近期后缀且至少包含最新一条。变更订阅Sessions.subscribe()提供本地缓存一致性变更流append/update/delete/clear/compact/compaction/import适合宿主镜像缓存。多会话sessions.session(support)可在同一个 Durable Object 内创建多个句柄句柄有缓存但面向用户的聊天应用更推荐每会话一个 Durable Object以隔离存储与故障域。Think与AIChatAgent在内部使用 SessionsThink 用分支、压缩、搜索与变更流AIChatAgent 用默认句柄作为线性链旧的assistant_*表会被自动迁移后丢弃。提示词组装context blocks、skills 等属于 docs/agents/context.md 中描述的agents/context能力它组合使用会话句柄而不是塞在 Sessions 内部。十、结语examples/next/sessions以不到 150 行代码演示了 Cloudflare Agents 会话持久化的完整闭环树形消息、流式 NDJSON 历史、分支导航、压缩覆盖、全文搜索与按需迁移全部跑在一个普通 Durable Object 上。它的设计哲学可以概括为三句话Sessions 只存消息不存文件行有预算但消息无上限读取永远字节精确。理解行分块与附件存储的代价模型是正确估算会话存储成本、设计文件外置方案的前提——需要处理文件的场景记得把文件交给 R2 或文件存储消息里只留引用。【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agents创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考