ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

TanStack AI 客户端接入 Cloudflare Agents 共享恢复引擎:tanstack-recovery 通用性 Harness 深度解析

2026/9/18 19:07:25 拓冰建站 浏览量
TanStack AI 客户端接入 Cloudflare Agents 共享恢复引擎:tanstack-recovery 通用性 Harness 深度解析 TanStack AI 客户端接入 Cloudflare Agents 共享恢复引擎tanstack-recovery 通用性 Harness 深度解析【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agents导读本篇文章围绕experimental/tanstack-recovery这套内部通用性验证装置genericity harness讲解如何让一个非 AI-SDK 的客户端传输与工具协议TanStack AI / AG-UI WebSocket 客户端驱动 Cloudflare Agents 仓库中agents/chat的共享会话恢复引擎ChatRecoveryEngine与ResumeHandshake。读完本文你将掌握恢复引擎的词汇无关vocabulary-agnostic编解码接缝设计、{ persist: false }策略下已落定工具结果的保留门settled-tool persist gate以及如何通过wrangler dev SIGKILL 端到端验证流式会话的中断恢复与无缝续传。背景与定位第二个通用性验证装置tanstack-recovery是当前仓库中与 pi-recovery 并列的第二个 genericity harness内部验证装置README明确标注 Experimental test harness — not a product example。它的作用不是演示产品能力而是以最小代价证明共享恢复引擎的通用性边界pi-recovery证明的是共享恢复引擎可以驱动一个非 AI-SDK 的 agent自己的AgentEvent事件词汇tanstack-recovery证明的是共享恢复引擎可以驱动一个非 AI-SDK 的客户端传输 工具协议—— 即tanstack/ai客户端经由 WebSocket 桥接走与cloudflare/ai-chat、cloudflare/think完全相同的ChatRecoveryEngine resume 握手但底层是AG-UIEventType流块词汇而不是 AI-SDK 的UIMessageparts。两份装置的完整设计背景记录在 design/rfc-chat-recovery-foundation.md 的 Phase 5Second harness中。也就是说本文介绍的实验是恢复基础设施通用性这一设计目标的验证证据。架构总览一条 WebSocket 桥接链路整个装置由以下几个源码文件组成一条完整的链路角色文件职责Worker 入口src/server.ts将 WebSocket 连接路由到TanStackAgentDurable Object并提供 HTTP 控制面POST /start、GET /statusDurable Objectsrc/tanstack-agent.ts实现 Agent挂接共享ChatRecoveryEngine、ResumeHandshake、ResumableStream流编解码器src/tanstack-codec.tsTanStackRecoveryCodec把 AG-UIStreamChunk重放为引擎的RecoveryPartial客户端桥接src/ws-bridge.tsRecoveryBridgeConnection把cf_agent_*握手帧翻译为 AG-UIStreamChunk流连接工厂src/ws-adapter.ts生成指向/agents/tan-stack-agent/{session}的 WebSocket URL 并构造桥接连接确定性模型src/faux-model.ts默认的FauxTanStackModel慢速流式输出脚本化 AG-UI 块真实模型src/workers-ai-model.ts可选的真 Workers AI 提供商经cloudflare/tanstack-ai的createWorkersAiChat模型接缝src/model.tsTurnModel/TurnProvider接口定义React 演示src/client.tsx使用useChat的浏览器端演示e2e 主测试e2e/recovery.test.ts真实wrangler dev SIGKILL 的四个场景e2e 真实模型测试e2e/workers-ai.test.tsRUN_WORKERS_AI_E2E1门控的可选 leg编解码器单测src/tanstack-codec.test.ts纯 Node 环境下的 codec 单元测试端到端的数据流大致如下TanStack AI 客户端 (useChat / headless Node) │ subscribe/sendAG-UI StreamChunk 语义 ▼ RecoveryBridgeConnection (ws-bridge) ── 客户端翻译层cf_agent_* 帧 ⇄ AG-UI 块 │ WebSocket (/agents/tan-stack-agent/{session}) ▼ TanStackAgent (Durable Object) │ 共享 ResumeHandshake ChatRecoveryEngine ▼ ResumableStream逐 chunk 持久化 TanStackRecoveryCodec重放为 RecoveryPartialsrc/ws-bridge.ts 的文件头注释点出了整个装置的核心发现共享的ResumeHandshake驱动是帧语义耦合的它发出cf_agent_stream_resuming/cf_agent_stream_resume_none以及 AI-SDK 形状的cf_agent_use_chat_response帧仅responseMessageType可注入而tanstack/ai的SubscribeConnectionAdapter期待的是 AG-UIStreamChunk流。因此需要一个薄客户端翻译层 —— 而这个翻译层足够小一个帧路由器恰好度量出握手协议本身是传输无关的只有帧词汇是耦合的。TanStackRecoveryCodecAG-UI 词汇的编解码接缝共享恢复引擎重建被中断轮次的助理状态方式是把一个持久化流缓冲通过某个ChatRecoveryCodec重放。仓库里目前有三种实现AISDKRecoveryCodec重放 AI-SDK 的 SSE chunkpi 装置重放自己的AgentEvent词汇本装置的TanStackRecoveryCodec重放 AG-UI 的StreamChunk词汇TEXT_MESSAGE_CONTENT增量、TOOL_CALL_*等。三者的共同点在于都向引擎投喂形状完全一致的RecoveryPartial——{ text, parts, hasSettledToolResults }。这正是 src/tanstack-codec.ts 顶部注释强调的接缝设计引擎永远看不到线上词汇chunk 形状差异全部由 codec 承担。重建两半文本与工具 partstoRecoveryPartial(bodies: string[])按最旧优先顺序重放存储的 chunk body同时重建两半内容助手文本累加TEXT_MESSAGE_CONTENT增量到text工具 parts按照 AG-UI 的子协议TOOL_CALL_START → TOOL_CALL_ARGS* → TOOL_CALL_END → TOOL_CALL_RESULT逐个重建TanStackToolPart。重建过程有两个值得注意的容错细节见decodeChunk与parseArgsdecodeChunk用JSON.parse解析每个 body解析失败时返回null并停止重放—— 因为 SIGKILL 可能撕开最后写入的 body。重放就此截断保留已存活的部分一个已经刷出RESULT的工具会读作settledhasOutput true而结果尚未刷出的工具读作unsettled。parseArgs会把累积的TOOL_CALL_ARGS缓冲先尝试JSON.parse失败则退回原始字符串避免参数被破坏时整个恢复失败。重建出来的工具 part 是本装置自己的 AG-UI 原生形状toolCallId、toolName、argsBuffer、input、hasOutput、output绝不是AI-SDK 的UIMessageparts。这是因为引擎接缝RecoveryPartial.parts是不透明的unknown[]codec 无需伪造 AI-SDK parts只需自己决定是否已落定// 来自 experimental/tanstack-recovery/src/tanstack-codec.ts const hasSettledToolResults parts.some((part) part.hasOutput); return { text, parts, hasSettledToolResults };hasSettledToolResults这个布尔值就是引擎的settled-tool persist gate的输入当恢复策略为{ persist: false }时若重建的 parts 中带有已落定的非幂等的工具结果引擎会保留这个 partial —— 与对 AI-SDK 工具的处理完全一致且本 codec 中零 AI-SDK 耦合。进度判定里程碑 vs 流式内容TanStackRecoveryCodec还实现了两个进度语义方法与AISDKRecoveryCodec对应isProgressChunk(type)AG-UI 的进度里程碑——TEXT_MESSAGE_START、TOOL_CALL_START、TOOL_CALL_RESULT这些块总是计入前进进度对应用 AI-SDK 的text-start与已落定工具里程碑isStreamingContentChunk(type)段内的流式内容——TEXT_MESSAGE_CONTENT、TOOL_CALL_ARGS它们经过宿主侧的进度节流StreamProgressCreditThrottle计分保证一个超长段在崩溃之间仍能登记进度而无须逐 chunk 写存储。两个集合是不相交的这正是共享引擎里程碑必计分、流式内容按节流窗口计分规则对 AG-UI 词汇的映射。该计分规则实际由TanStackAgent._runTurn中的shouldCreditStreamProgress({ codec, type, throttle, now })调用与AIChatAgent/Think走的是同一条宿主无关逻辑。ws-bridge客户端侧的手握帧翻译层RecoveryBridgeConnection实现了tanstack/ai-client的SubscribeConnectionAdapter接口subscribe/send/close是唯一触碰网络的部分。它维护了一组观察计数器observationse2e 正是靠这些计数断言外来客户端确实驱动了握手观察项含义e2e 断言resumingFrames收到cf_agent_stream_resuming帧的次数服务端提供可恢复流 0acksSent本客户端回复cf_agent_stream_resume_ack的次数 1握手场景/ 0恢复场景replayResponseFrames带replay: true的响应帧数缓冲 partial 重放 0chunkFrames携带非空 AG-UI chunk body 的响应帧数—accumulatedText累计观察到的TEXT_MESSAGE_CONTENT文本包含tanstack reply to桥接层需要翻译的帧类型字符串与agents/chat的CHAT_MESSAGE_TYPES保持一致本地保留以避免客户端 bundle 引入服务端 barrel包括STREAM_RESUMING cf_agent_stream_resuming STREAM_RESUME_ACK cf_agent_stream_resume_ack STREAM_RESUME_REQUEST cf_agent_stream_resume_request STREAM_RESUME_NONE cf_agent_stream_resume_none USE_CHAT_RESPONSE cf_agent_use_chat_response CHAT_RECOVERING cf_agent_chat_recovering握手时序值得注意两点连接打开后桥接层主动发送STREAM_RESUME_REQUEST避免错过服务端积极通知式的onConnect通知竞态见_ensureSocket中 open 事件的注释对同一id的STREAM_RESUMING桥接层通过_acked集合保证只 ACK 一次防止重复发送对应源码注释里的 #1733 double-send 问题。服务端侧TanStackAgent.onConnect会在存在活动流时调用this._resumeHandshake().notifyStreamResuming(connection)积极通知若无活动流但恢复进行中则重放cf_agent_chat_recovering状态帧让连接中的客户端读到正在工作而不是卡死源码注释对应 #1620。onMessage则把STREAM_RESUME_REQUEST/STREAM_RESUME_ACK交给共享ResumeHandshake处理把tanstack-run帧转成一次startTurn。共享引擎与握手在 AG-UI 词汇下的挂接src/tanstack-agent.ts 是装置的服务端核心一个 Durable Object复用AIChatAgent与Think同款的三件共享基础设施ResumeHandshake通过_resumeHandshakeHost()提供responseMessageType设置为CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE、resumableStream、continuation、pendingResumeConnections、pendingChatTerminal、persistOrphanedStream。注意该 host 的responseMessageType是唯一可注入的部分 —— 其余帧语义与 AI-SDK 实现完全共享。ChatRecoveryEngine通过_adapter()提供resolveConfig、readProgress、incident 读写、scheduleRecovery、setRecovering、resolveRecoveryStream、getPartialStreamText内部调用TanStackRecoveryCodec等回调。chatRecovery配置以类字段形式赋值而非onStart中以保证冷唤醒后 fiber 恢复仍能读到配置的预算。ResumableStream逐 chunk 把JSON.stringify(chunk)持久化。_runTurn中每个 chunk 之后都执行flushBuffer()—— 这是装置特意关闭批量缓冲保证 SIGKILL 落地的瞬间 partial 已可靠落盘。恢复分类与调度_handleInternalFiberRecovery把 fiber 恢复委托给共享引擎_wakeHooks()提供四个关键回调classifyRecoveredTurnpartial.text 非空 →continue为空崩溃发生在第一个 delta 刷出之前→retryinvokeOnChatRecovery从持久化存储读取PERSIST_POLICY_KEY返回{ persist }保证冷唤醒恢复沿用轮次开始时设置的策略dispatchRecoveredTurn按recoveryKind调度_chatRecoveryContinue或_chatRecoveryRetry回调schedule带idempotent语义shouldPersistOrphanedPartialstreamStillActive时把孤儿 partial 持久化为partial助手条目。续传合并continue 的精确数学对确定性 faux 模型_resumeRecoveredTurn做的是精确前缀/后缀切分full replyFor(userText) // 确定性全量回复 prefix partial.text // 已存活的 partial 文本 if (full.startsWith(prefix) prefix.length full.length) → 只生成 full.slice(prefix.length)后缀merge 折叠到前缀上 if (prefix full) → 整条回复已存活直接 _finalizePartial 提升为正式消息 else → 不是干净前缀退回全量重新生成_mergeContinuation把续传后缀与保留 partial 合并merged prefix suffix并把条目从partial提升为已提交消息同时记录恢复摘要via、generatedChars、prefixChars供 e2e 断言。四个 e2e 场景真实 wrangler dev SIGKILLe2e/recovery.test.ts 在一份wrangler dev端口 18902--persist-to .wrangler-tanstack-e2e-state之上跑真实进程级 SIGKILL覆盖 README 中列出的四个场景场景 1流中途的手握重连ResumeHandshake先服务端启动一个慢轮次POST /start等 1.5 秒让若干 chunk 进入缓冲随后一个无头 TanStack 桥接客户端中途接入。onConnect触发STREAM_RESUMING→ 桥接 ACK → 收到缓冲重放 → 继续接收实时尾部。断言包括assistantCount 1、resumingFrames 0、acksSent 1、replayResponseFrames 0、累计文本包含tanstack reply to。这是恢复协议以仅一层薄桥接驱动外来客户端、且零agents改动的干净证明。场景 2SIGKILL 后的续传continue精确数学启动轮次等 3 秒确认 fiber 行存在且无已提交助手消息然后对wrangler dev进程组SIGKILL等待端口释放后重启。桥接客户端重连后访问 DO 唤醒它共享引擎重建孤儿 partial、保留之、调度continue只生成剩余后缀。断言的核心数学recoveredVia continue partialPrefixChars 0 recoveryGeneratedChars 0 partialPrefixChars recoveryGeneratedChars assistantText.length最终还等待 fiber 行被回收fiberRows 0 assistantCount 1。场景 3{ persist: false }下已落定工具结果的保留门这是整套装置最精妙的一个证明。轮次以withTool: true, persist: false启动faux 模型先以 AG-UITOOL_CALL_START → ARGS → END → RESULT子协议落定一个工具调用再流式输出长文本尾。SIGKILL 后TanStackRecoveryCodec从 AG-UI parts 重建出带已落定结果的 partial于是共享引擎的 settled-tool 子句覆盖{ persist: false }保留该 partial恢复走continue。断言partialHadSettledTool true、recoveredVia continue、前缀后缀等于完整回复长度。源码注释指出该行为对应 issue #1631。场景 4{ persist: false }下纯文本 partial 被丢弃重试同样的persist: false策略但没有工具—— partial 不携带任何已落定工作引擎的保留门放行丢弃。断言partialHadSettledTool false、recoveredVia retry、partialPrefixChars 0无合并目标整条重新生成。场景 3 与场景 4 共享同一策略、仅在是否落定了工具上不同因此结果分歧恰好隔离出保留门本身。e2e 基建层面的两个工程细节两处注释均有说明关闭 happy-eyeballs 竞态并吞掉 SIGKILL/重启探测时setTypeOfService的良性 EINVAL用lsof -tiTCP清理端口占用。Faux vs 真实 Workers AI确定性离线 vs 可选线上默认确定性 faux 模型完全离线src/faux-model.ts 的FauxTanStackModel以固定tokensPerSecondTanStackAgent中配置为 4慢速流式输出脚本化回复不调用AI.run()因此整套测试完全离线、可复现。它发出的正是真实提供商同样的 AG-UI 词汇序列RUN_STARTED → TEXT_MESSAGE_START → (可选 TOOL_CALL_*) → TEXT_MESSAGE_CONTENT* → TEXT_MESSAGE_END → RUN_FINISHED两个细节保证了 e2e 的确定性tokenize按空白边界切分text.split(/(?\s)/)切块拼接后字节级还原完整文本 —— codec 的文本重建依赖这一点脚本化工具调用在文本体之前先落定SIGKILL 落在文本尾部时partial 恰好携带已落定工具结果。可选真实 Workers AI lege2e/workers-ai.test.ts 是RUN_WORKERS_AI_E2E1门控的可选 leg端口 18903。它驱动同一套 codec 引擎 ws-bridge但模型换成真实、非确定性的 Workers AItanstack/ai的chat()跑在cloudflare/tanstack-ai的createWorkersAiChat之上模型cf/moonshotai/kimi-k2.7-code绑定到AIbinding。由于wrangler dev的AIbinding 在remote: true时代理到真实 Workers AI该 leg 需要网络与 Cloudflare 账号。两个 provider 共享同一个 src/model.ts 的TurnModel接缝 —— 都是stream(options): AsyncIterableStreamChunk所以换 provider 是纯模型层的恢复 codec / 握手 / 引擎完全无感。针对真实模型的非确定性断言从字节精确的数学放宽为引擎保证的续传不变量recoveredVia continue partialPrefixChars 0 recoveryGeneratedChars 0 partialPrefixChars recoveryGeneratedChars 最终长度 // merge 构造上恒成立真实模型的续传不是精确后缀切分而是助理预填充assistant-prefill_buildModelMessages把存活 partial 作为 assistant 消息追加再附上WORKERS_AI_CONTINUE_NUDGE提示词从你停止的位置继续不要重复此前文本同时系统提示词强制长回复300 词、至少六段把首 token 前的时间窗拉宽到足以被 SIGKILL 击中。测试还用waitForBufferedContent等待实际内容刷入缓冲而非仅 fiber 行存在以避开真实模型的 time-to-first-token 竞态确保幸存 partial 非空。运行方式与 CI按 README 与 package.json 的脚本定义运行命令如下cd experimental/tanstack-recovery # 纯 codec 单元测试普通 Node无需 Workers runtime pnpm test # 真实 wrangler dev SIGKILL e2efaux 模型离线 pnpm test:e2e # 可选的真实 Workers AI leg需要网络 Cloudflare 账号 RUN_WORKERS_AI_E2E1 pnpm test:e2e测试相关的包脚本与依赖test→vitest run覆盖src/tanstack-codec.test.tstest:e2e→vitest run --config e2e/vitest.config.ts覆盖两个 e2e 文件dev→wrangler devdeploy→vite build wrangler deploy依赖tanstack/ai0.38.0、tanstack/ai-client0.19.0、tanstack/ai-react0.16.0、cloudflare/tanstack-ai^0.2.0、agents本仓库 workspace、wrangler^4.115.0。wrangler.jsonc 的关键配置main: src/server.tscompatibility_date: 2026-06-11compatibility_flags: [nodejs_compat]ai.binding AIremote: true即使本地wrangler dev也代理真实 Workers AIfaux e2e 从不调用AI.run()因此保持完全离线Durable Object 绑定TanStackAgentmigrations 使用new_sqlite_classesSQLite 状态存放tanstack_messages表与恢复元数据键。README 还说明faux e2e 与单元测试通过.github/workflows/nightly.yml中的e2e-engine-genericity任务每晚运行。设计启示从翻译层厚度度量耦合度作为验证装置tanstack-recovery 的结论价值集中在两点均可从源码注释与结构推断握手协议是传输无关的只有帧词汇耦合。共享的ResumeHandshake驱动notify → ACK → replay终态经 resume 达成的 #1733/#1645 语义在 AI-SDKuseChat与 TanStack 桥接客户端两种传输下原样复用零引擎改动。差异被压缩在一个客户端翻译层ws-bridge里 —— 该层有多薄正是 RFC 中 Approach A保持词汇耦合、客户端自译与 Approach B把词汇折叠进可注入接缝取舍的决策依据。编解码接缝让已落定工具结果跨词汇可判定。RecoveryPartial.parts对引擎不透明unknown[]codec 自己决定hasSettledToolResults引擎消费的只是一个布尔值因此非幂等工具工作的持久化保护{ persist: false }下的保留门见 #1631天然对所有词汇生效。如果你想继续深入共享引擎本身的实现可以阅读 packages/agents/src/chat/recovery-engine.ts 及其测试 packages/agents/src/chat/tests/recovery-engine.test.ts本装置正是以最小的外来客户端 外来词汇成本为这条共享代码路径提供了第二根通用性支柱。说明本实验目录在README中明确标注 Experimental test harness — not a product example属于内部验证装置而非产品示例文中涉及的 issue 编号#1620、#1631、#1645、#1733均出自源码注释中的引用。【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agents创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考