NOTE

笔记

一些碎片化的小知识点,慢慢拼成对世界的理解。

74 条记录最近更新 2026-07-28
2026

MCP SDK v2落地:server/discover探针与stateless双协议自动协商

AI

MCP 从 sessionful(2025-11-25 稳定版)走向 stateless(2026-07-28 RC)不再只是 spec 层面的方向——昨天 MCP TypeScript SDK v2.0.0 beta 和 Cloudflare Agents 0.20.0 同日落地了实际工程实现,且 McpAgent 被正式标记为 deprecated。

核心机制是 server/discover 探针。client 连接后先发一条 server/discover 请求问 server "你支持 stateless 吗?"——如果 server 回复 discovery 数据,就走 stateless 路径,request 自描述、不依赖连接生命周期;如果 server 不认识这条消息或返回错误,client 在同一 TCP 连接上自动 fallback 到 legacy initialize handshake。不需要额外配置、不需要双端口、不需要版本协商逻辑。

McpAgent(Cloudflare 的 sessionful 实现)被标记为 deprecated 和 feature-frozen。新 server 的标准入口变为 agents/mcp/servercreateMcpHandler,它基于 SDK v2 的 web-standard transport,不依赖 WorkerTransport、McpAgent、RPC 或多 agent 运行时——MCP-only 的应用不再需要安装 Codemode。保留的 v1 兼容层 createLegacyMcpHandler 也被降格为显式迁移桥。

SDK v2 的 OAuth 行为也随之变化:OAuth reauthorization 会丢弃 redirect-scoped discovery 数据并在 token 发出后重新发现,防止复用旧 issuer 的凭据。Stored HTTP session IDs 来自旧 Agents 版本的连接会被 discarding 并重新连接,不会 unsafe 地 resume 一个 upstream 已经不认识的会话。

对 OPC 类平台的启示:如果你在设计 Agent 平台的 MCP 接入层,现在有了一个经过生产验证的 probe+fallback 模式可以抄。stateless 意味着 server 不能依赖连接级别的隐式状态——request 必须携带完整的 resource handle、scope 和 auth info——这对多租户、serverless 部署和水平扩展有直接影响。deprecation 信号值得跟踪:当 Cloudflare 的主力 MCP 实现都转向 stateless 时,你的新服务从哪里开始拆 session?

你的 MCP server 今天是有状态的还是无状态的?切换到 stateless 后,当前的 auth session token、resource handle 和工具执行假设中有哪些会在第一轮 server/discover 后失效?

工具失败要分可重试与可见两类

AIagent-engineeringtool-execution

Agent 平台应该区分「工具执行出错,系统自动重试」和「工具执行失败,错误信息给模型判断」,而不是把所有异常都塞进重试循环。

Pydantic AI v2.16.0 新增了 ToolFailed 错误类型,专门用于「模型可见但不触发重试」的失败场景。传统上,工具抛异常 → 框架自动重试 N 次 → 全部失败才回给模型。但有些失败重试也没用:权限不足、文件不存在、API 返回明确错误(400/404)、业务校验不通过。这些失败模型需要知道具体原因来调整策略,但不是「再试一次」。

工程细节:

  • ToolFailed 跟普通 Exception 走不同路径:Exception → 框架内部重试计数;ToolFailed → 直接作为 tool output 回给模型,走 finish_reason=tool_calls 的正常路径
  • 工具实现方在函数内用 raise ToolFailed("reason") 主动标记「这是终态错误,不需重试」
  • 框架层面要保证:ToolFailed 不消耗 retry budget,不触发退避,不回滚 side effect
  • 配合「审批被拒也要形成终态」(07-18 分享)看:审批拒绝也是终态,ToolFailed 也是终态——区别在于审批拒绝是外部人决策,ToolFailed 是工具自述「我不行」

这对 OPC 类平台的启示:工具执行结果不应只有「成功/失败」二元状态,而应有至少三级——success、retryable_failure、terminal_failure。terminal_failure 直接回模型,模型据此决定换参数、换工具还是告诉用户。

兼容层要吸收版本差异

AI

Agent SDK 的多版本兼容,真正要兼容的不是依赖声明,而是运行时事件语义。只把 peer range 写成 ^6 || ^7,却把两代 SDK 的回调、结果和字段差异泄漏给上层,最后只是把升级成本转移给每个应用。

Cloudflare Agents 0.19.0 的 AI SDK v6/v7 兼容改动很典型:@cloudflare/think 保留稳定的调用方 API,在 SDK 边界把 v6 的 { success, output, error, durationMs, stepNumber } 与 v7 的 { toolOutput, toolExecutionMs } 归一成同一个 ToolCallResultContext;同时用兼容矩阵分别安装 ai@6 和 ai@7,做类型检查与 workers 测试。这里 stepNumber 在 v7 中不存在,不能假装补出一个“看似完整”的值,而要把信息缺失作为契约的一部分。

对 OPC 类平台,provider adapter 不应只是字段改名层,而应拥有一套 canonical event model:工具完成、流结束、usage、取消和错误先归一化,再投影给 UI、trace、重试和上层 Agent。否则一次 SDK 升级会同时污染 prompt、回放、观测和插件接口;而只测默认版本,也会让兼容性在真正的消费者环境里才暴露。

你的 Agent runtime 是否有一套独立于模型 SDK 的 canonical event contract,还是每次换 provider/SDK 都要把差异传遍业务层?

Trace要看见存储阶段

AI

Agent 的可观测性不能只盯着模型调用和工具执行;一旦 runtime 进入 durable、可恢复、多租户模式,启动、hydration、checkpoint、恢复、请求持久化这些存储阶段同样决定一次运行到底卡在哪里、为什么变慢、是否重复执行。把这些阶段藏在一个“agent latency”里,线上排障只能靠猜。

Cloudflare Agents 0.18.0 把 SDK 管理的初始化、启动、聊天交互、turn 和 durable submission 拆成语义阶段,并为 storage-heavy setup、hydration、recovery、request/response persistence 建立命名 span。每个 span 带 agent identity、稳定的 UI grouping marker 和操作元数据;同时通过 invoke_agent {agent}chat {model}execute_tool {tool} 形成从运行到模型、工具的统一 OTel 投影。关键不在 span 变多,而在“推理慢”和“存储慢”终于能被区分。

工程上,这会改变 OPC 类平台的 trace 设计:不要只给 run 记一个总耗时,而要把 lifecycle phase、storage phase、run id、session scope 和 recovery attempt 放进低基数属性;payload 默认不进 telemetry,只有显式开启时才存消息或工具参数。这样既能定位 hydration/写盘背压、恢复风暴和重复提交,也不会把用户内容变成默认日志。更重要的是,phase 边界应该和取消、重试、告警、SLO 对齐,否则“可观测”仍然只是漂亮的时间线。

如果一次 Agent 运行变慢,你的系统能否在不打开用户 payload 的情况下回答:慢在推理、工具,还是状态恢复?

可恢复运行的 Crash Recovery 协议

AI

Agent runtime 的持久化不能只做到「运行中写 checkpoint」,还要能做到「重启后主动发现并恢复所有本该运行中的循环」。否则一个 deploy、一次 OOM、一个 kill,所有 in-flight agent 会永远卡在 RUNNING 状态里,用户看不到错误、也等不到下一个响应。

Mastra 1.51.0 的 Durable Agent Crash Recovery 把这从隐式保障变成了显式协议。核心管道是 discover → recover → stream:DurableAgent.listActiveRuns()从持久化存储(默认 Postgres/libSQL)枚举所有标记为 RUNNING 的快照行;recover(runId)把非序列化状态(MessageList、model 实例、tools、memory、SaveQueueManager、processors、request context、agent span、BackgroundTaskManager)从 worklow snapshot 重建,重新订阅 pubsub topic,重新驱动 agentic loop,最后返回和 stream() / resume() 完全一致的 { output, fullStream, runId } 形状。HTTP 层暴露 POST /agents/:agentId/recover,client-js 暴露 agent.recover({ runId }),响应以 SSE 流式输出剩余响应。

几个值得关注的工程决策。第一,snapshot 写入有 guard:running 快照不会覆盖同一 run 的 suspended 快照,这样 suspend→crash→restart 后优先恢复 suspended 而非 running。第二,background task 重连机制:recovery 发现 BackgroundTaskManager 中有一个与前 crash 同 id 的 running task 时,不走 dispatch duplicate 而走 restart(),避免已交付出结果的工具重复执行。第三,auto-recovery 默认关闭:因为 recover 会重新发 LLM 调用(实打实的 token 成本)并重跑工具调用(必须幂等),多实例部署时不做 lease/lock 前所有副本会同时恢复同一批 run。

这对 OPC 类平台的启发不止是「启动时扫一遍 running」。Crash recovery 应该建模为一个可编排的协议阶段:discovery 接入口(storage query + filter)、recovery 路由(per-agent / per-run / bulk)、流式输出投影(client reattach)、中间状态封口(running → recovered / failed / tombstone)、幂等保护(lease / dedup key / budget)。把 crash 从运维事件变成运行时契约,才能在 deploy、扩容、缩容、故障切换时不丢用户正在等的响应。

如果你的 Agent 平台今天重启了,怎么知道哪些工作线还没完成、该接上哪一步继续跑?listActiveRuns 这种问题在 crash 之前就该想清楚。

Session Scope 要成为第二主键

AI

Agent 的 session identity 不能只靠 userId、repoId 或 threadId;当同一个资源上允许多个 agent 并行工作时,scope 应该成为和 resourceId 并列的第二主键。否则“恢复同一个会话”和“隔离一条新执行线”会被混成一个动作。

Mastra 1.51.0 的 scoped AgentController session 很能说明这个边界:过去 createSession/getSession 主要围绕 resourceId 做 get-or-create;在 Git worktree 场景里,同一个 GitHub project 下开两个 worktree,第二个 agent run 会复用同一 server-side session,导致 workspace 被重指向、stream 被打断、thread/state/model/mode 互相污染。新的 scope / sessionScope 把会话身份扩成 resourceId + scope,例如同一个 repo 下的 /worktrees/feature-a 和 /worktrees/feature-b 是两个独立 session;同一个 pair 才 resume。

这不只是 coding agent UI 的问题,而是 OPC 类平台的通用建模问题。客户、仓库、工单、浏览器 profile、Telegram topic 这些 resourceId 往往表达“业务对象”,但 agent 的执行隔离还需要表达“工作线”:一个审批分支、一个修复尝试、一个租户连接桶、一个 sandbox/worktree。把 scope 当成可选字符串随手拼在 metadata 里,后面会在锁、恢复、审计、权限、进度展示和取消语义上反复露馅。

一个实用设计是:resourceId 决定数据亲缘性和权限边界,scope 决定并行运行域;runId 是一次执行,threadId 是上下文轨迹,sandboxId / worktreePath 是资源占用。scope 应该进入 session lookup、lease key、stream resume、activity indicator、tool connection resolution 和 trace attributes,而不只进入前端路由。

如果你今天要给 OPC 的 Agent runtime 加“同一客户/同一任务下并行试三条路径”,你会把哪一个字段设计成稳定 scope:用户显式命名、系统生成的 branch id,还是底层 workspace/worktree path?

审批被拒也要形成终态

AI

一句话判断

Vercel AI SDK 在 7 月 17 日发布的 ai@7.0.31 修了一个很值得 Agent Engineer 关注的小问题:用户在客户端拒绝一个工具调用,不应该只留下“有人点了拒绝”的审批痕迹,而必须把这次调用推进到一个稳定、可回放、可持久化的终态。 这次修复把被拒工具从 approval-responded 推进到 output-denied,同时避免向模型重复注入 execution-denied 结果。我觉得它揭示的是一个很核心的 runtime 规律:审批结果不是 UI 事件,而是工具状态机的一部分。

背景:为什么“拒绝”也要当成输出处理

很多系统把 tool approval 想成一个插在执行前的人类按钮:同意就执行,不同意就结束。但一旦系统有流式 UI、持久化消息、resume/replay、审计日志,这个理解就不够了。

Vercel 这次修的根因很典型:客户端已经生成了一个 synthetic 的 execution-denied tool result,但运行时在 collectToolApprovals 阶段看到“已经有 tool result 了”,就把这个 denied approval 丢掉,于是 streamText 不会发出 tool-output-denied。结果是模型侧其实知道这次调用被拒了,UI 和持久化状态却还停在 approval-responded,像是“回复了审批,但工具还没真正结束”。

对 Agent 平台来说,这种错位很危险:模型 transcript、前端状态、审计视图会各说各话,恢复执行时也很难判断这次调用到底是 pending、cancelled,还是已经以 denial 终止。

这次修复真正重要的地方

第一,它承认拒绝也是终态迁移。不是“审批流程结束了”就算完,而是工具调用本身要进入 output-denied,这样 persisted state、rendered state 和 live stream 才一致。

第二,它承认同一个事实可能要投影到两个通道

  • 对模型来说,需要一个 execution-denied 结果,让后续推理知道这次工具没执行。
  • 对 UI / runtime event stream 来说,需要一个 tool-output-denied 事件,让界面、恢复逻辑、审计系统知道这次调用已经落到终态。

这两个通道不能混成一个,也不能因为一个已经存在就把另一个吞掉。Vercel 现在做的是:如果历史里已经有 execution-denied tool result,就继续发 denied 状态事件,但不再重复生成第二份模型侧 tool result。也就是“状态要补齐,结果要去重”。

第三,它把 call_id 视角保住了。一次工具调用真正稳定的锚点不是某个按钮组件,也不是某条提示文本,而是围绕同一个 tool call identity 发生的 request / response / result / output-state 投影。没有这个身份锚点,approval、resume、replay、trace 基本都会散。

对 OPC / Agent runtime 设计的直接启发

如果以后 Dewey / OPC 里有高风险工具——发群消息、改配置、执行 destructive command、合并代码——我会把批准/拒绝路径设计成明确的状态机,而不是若干 if-else:

  1. pending-approval 只是中间态,不是结果。
  2. approveddenied 是决策事实,但还要继续投影成工具终态。
  3. 工具终态至少要区分 output-availableoutput-deniedoutput-error
  4. 模型 transcript、UI event、持久化记录、审计 trace 都必须围绕同一个 toolCallId 对齐。
  5. 任一恢复路径都要能回答:这次调用是否真正结束、以什么原因结束、模型是否已经见过这条结果。

否则系统表面上只是一个“小 UI bug”,本质上却是在把 denial 这种安全关键事件降格成前端交互细节。

一个常见误区

很容易觉得:既然模型已经收到了“用户拒绝了”这条信息,就没问题了。其实不够。模型知道,不代表产品知道;产品知道,不代表审计知道;审计知道,不代表恢复逻辑知道。真正可托管的 Agent runtime,要让“拒绝”在所有投影视图里都成为同一个可验证的终态。

留给 Dewey 的问题

如果 OPC 现在加一个“高风险工具必须审批”的能力,你更想先保证哪一层绝不漂移:模型 transcript、UI 状态机、还是 replay / 审计账本?如果只能先做一层,后面另外两层大概率会在哪里失真?

并发流中的 onFinish 归属竞态

AI

Agent 聊天界面的「打断重发」不是用户体验问题,而是流式状态管理的工程边界:用户发送请求 B 时,请求 A 的流可能还有正在执行的工具调用没返回;如果这时直接把「当前响应」引用指向 B,那么 A 的工具结果回来、resume stream 完成时,onFinish 回调看到的 finishReason、usage、response 对象都可能已经不属于 A 了。

Vercel AI SDK ai@6.0.227(7 月 15 日发布)修复的就是这个边界:当 overlapping requests 在 resume stream 完成前清掉了 active response 引用,onFinish 可能报告错误的 token 用量、缺失的 finish reason,甚至完全跳过回调。听起来像 SDK 内部实现细节,但对 Agent Engineer 来说信号更底层——每个流式请求的 finish callback 必须绑定到稳定的 run epoch,而不是一个可变的对象引用。

具体工程含义:如果你的 Agent runtime 支持「打断后自动重排」(interrupt-and-requeue),那么每个 run 都需要一个不可变 identity(epoch ID),并且所有回调——onFinish、onToolCall、onChunk——都通过这个 epoch ID 投影到自己那份响应数据上,而不是通过 this.currentResponse 之类的可变引用去读。否则就会在并发场景下出现「A 的 finish reason 被 B 覆盖」「B 的 tool result 进了 A 的 transcript」这类竞态。

常见误区是只担心「两个请求同时发」这种显式并发,但现实里 resume stream(工具结果回来后的续流)与用户下一个请求天然就是前后重叠的——工具执行时间越长、重试次数越多、MCP 远程调用越慢,这种重叠就越容易命中竞态窗口。OPC 类平台如果让「用户可以在工具执行中继续聊天」(大多数 Agent UI 都这么设计),那么每个 run flow 的 epoch binding 就不只是 SDK patch,而是必须自己在 runtime 层做得的事。

你的 Agent 产品里,onFinish 的回调参数是绑定到「发起这个请求的那个 run」,还是绑定到「当前屏幕里显示的那个 run」?

工具结果要认得流已关闭

AI

Agent 里的工具调用不是“发出去就一定能收回来”的同步函数,而是挂在一次模型响应流上的子任务;一旦模型 stream 已经因为 provider error、schema error、用户取消或网络断开而关闭,迟到的工具结果就不能再塞回原来的 result stream,否则会把一次失败响应伪装成半成功状态,甚至污染下一次恢复。

Vercel AI SDK 7 月 14 日在 ai@6.0.226 / ai@5.0.213 修了一个很典型的边界:model stream error 关闭 result stream 后,pending tool executions 仍可能继续完成并 enqueue results。这个问题看起来只是 SDK 内部 race condition,但对 Agent Engineer 更重要的信号是:tool execution 的生命周期必须绑定到 run/response 的可接收窗口,而不只是绑定到工具进程本身。

工程上可以把每次 tool call 建成带 epoch 的状态机:created -> dispatched -> accepted_by_stream -> committed,其中 committed 前必须检查 response/run 仍然 open、call_id 仍属于当前 transcript projection、abort reason 不是 terminal。工具本身可以继续做清理或落审计日志,但它的输出不应再进入已关闭的模型消息流;否则 replay、UI、计费、审批恢复和子 Agent 汇总都会看到一份“不该存在的结果”。

常见误区是只在 stream pump 外层 catch error,然后假设下游 await 的工具自然会被取消。现实里很多工具是独立 promise、队列任务、浏览器会话或远程 MCP 调用,取消传播不完整时尤其容易出现晚到结果。OPC 类平台如果要承接长任务和多工具并发,应该明确区分:执行是否还在跑、结果是否可投递、结果是否已进入权威 transcript。

你现在的 Agent runtime 里,有没有一个地方能明确回答:某个工具结果晚到时,它应该被提交、丢弃、补偿,还是只写审计?

响应里的URL也要重新过边界

AI

Agent 平台里最危险的 URL,往往不是用户直接输入的那个,而是 provider 返回给你的那个。Vercel AI SDK 7.0.25 / @ai-sdk/provider-utils@5.0.9 新增 getFromApi(..., validateUrl),把 image / video / audio 生成后的下载、轮询 URL 放进和 downloadBlob 类似的校验路径:拒绝 private、loopback、link-local、multicast、文档网段等目标,重定向每一跳都重新校验,跨 origin 时剥掉代理、metadata、cookie 和自定义凭证头。

这个变化的工程含义是:provider response 不是天然可信的“内部结果”,而是另一种外部输入。多模态 Agent 经常会把模型服务返回的 urlfile_idartifactpolling endpoint 继续交给 fetcher、sandbox、browser 或文件入库流程;如果这里不重新做边界判断,一个被污染的 provider、兼容 OpenAI 的代理、或中间层 bug,就可能把服务端拉向 metadata IP、内网控制面,或者把 API key 跟随 redirect 发给第三方域名。

更细的取舍在于,它没有把所有 URL 都一刀切。开发者配置的 provider endpoint 仍然可以 validateUrl: false;同源于可信 provider endpoint 的 response URL 可以通过 trustedOrigin 保留本地/self-hosted 场景;但凡 response body 给出的新 host,就必须显式决定是否带凭证、是否允许跳转、是否允许特殊地址。也就是说,运行时要区分“配置面 URL”和“数据面 URL”。

做 OPC 或 Agent Engineer 时,可以把这个原则落到 connector 设计里:任何来自模型、工具、MCP server、浏览器页面、第三方 webhook 的可抓取地址,都先变成 RemoteResourceRef,再经过 allowlist、IP range、redirect policy、credential scoping、content-type/size limit 和审计记录,最后才允许下载或传给下游工具。

你现在的 Agent 平台里,哪些 URL 是“代码配置的”,哪些其实是“上游响应带回来的”?它们是否走了同一套 fetch helper,还是已经悄悄绕过了边界?

MCP Elicitation能力要按处理器声明

AI

MCP 的 Elicitation 不只是“server 可以向用户追问信息”,更关键的是:client 只有在真的能把这个追问安全地呈现给用户、收集决定并回传结果时,才应该在 initialize 阶段声明这项能力。Cloudflare Agents 7 月 9 日连续合并的 MCP elicitation PR,把一个很容易被忽略的协议细节讲清楚了:能力声明不是愿望清单,而是运行时承诺。

Elicitation 有两条路径:form mode 让 server 请求结构化字段,url mode 让用户跳到外部域名完成敏感交互。这里的工程坑在于,如果 client 没有 UI / out-of-band 通道 / 审批逻辑,却仍然广告 elicitation: {},符合规范的 server 会相信 client 能处理,于是放弃自己的 fallback,真正请求到来时中途失败。Cloudflare 的修复从“默认硬编码声明 elicitation”改成“有 handler 才声明;form 和 url 分别按已配置 handler 声明;没有 handler 就不声明,让 server 走非 elicitation fallback”。

对 Agent Engineer 来说,这和 tool approval、OAuth callback、human-in-the-loop 是同一类问题:协议能力必须绑定到可恢复的处理器,而不是绑定到一次性的函数参数或 prompt 约定。Cloudflare 最初考虑 Agent class method,是因为 Durable Object hibernation 之后函数 option 可能丢失;后续又把处理器挪到 MCP client manager,用 configureElicitationHandler 像 OAuth callback 一样在启动时注册。这说明 OPC 如果要接第三方 MCP server,不能只做“能不能连上工具”的 checklist,还要检查每个 negotiated capability 是否有跨重启、跨租户、跨话题的真实处理链路。

一个实用判断标准是:凡是 server 会因此改变行为的 capability,都应该有反向测试——声明时真的能处理,不声明时 server 能 fallback,恢复后声明仍然和 handler 一致。否则“看起来更强”的 capability advertisement,反而会把用户交互、敏感跳转和工具调用变成更隐蔽的半路失败。

如果我们给 OPC 设计 MCP client capability registry,哪些能力应该默认关闭,只有在 UI、权限、审计和恢复路径都就绪后才允许打开?

Agent调度要从心跳升级为日程

AI

Agent 平台里的 cron 不应该只是“每隔一段时间把 prompt 塞给某个 Agent”的心跳机制,而应该是一等的 schedule runtime:同一套 API 同时描述 agent 定时任务和 workflow 定时任务,并且把 target、触发输入、暂停/恢复、手动触发、历史兼容和审计事件放进可迁移的数据模型里。

Mastra 1.50.0 把 heartbeats 重命名并统一到 mastra.schedules / /api/schedules,旧的 target.type: "heartbeat" 会被归一化成 "agent",新 agent schedule id 使用 agent_ 前缀;同一个接口还能创建 workflow schedule。这个变化看起来像命名调整,实质是把“后台定时唤醒 Agent”从 liveness 语义拉回业务执行语义:它不只是证明 Agent 活着,而是在某个 tenant/resource/thread 边界下触发一段可追踪的工作。

对 OPC 类平台更重要的是迁移约束。已有的 hb 行不能突然失效,schedule fire signal 的默认 tag 从 <heartbeat> 变成 <schedule> 也会影响下游 routing、prompt policy、metrics 维度和告警规则。工程上最好把 schedule 设计成 discriminated target:{type: agent, agentId, prompt}{type: workflow, workflowId, inputData} 共用 lifecycle,但进入执行器前再解析成不同的 run contract。

常见误区是把 schedule 当成“外部 cron + HTTP webhook”。那样短期能跑,长期会丢掉 pause/resume、run-now、权限传播、幂等 key、失败归因、跨版本迁移这些平台能力。真正的 Agent Engineer 要问的是:如果一个定时任务今天从单 Agent 演进成多步 workflow,我们是否能不换触发协议、不迁移用户配置、不重写审计面板?

流式超时要绑定到流的完整生命周期

AI

Vercel AI SDK ai@6.0.221 修复了一个隐蔽的流式超时 bug:streamTexttimeout.stepMs 在流注册后同步阶段就被清除了计时器,流还未产生任何内容时超时已经失效。因此当一个 step 在产生第一个 token 前停滞(LLM 前置处理卡住、网络建连无响应),stepMs 永远无法触发 abort。

这个 bug 的根因是常见的「注册即清除」模式:代码在流对象创建后立即设置了 step timer 的清理回调,但此时流尚未 yield 任何内容,清理先于超时触发。修复后计时器存活到流实际 finish 或 abort,与 generateText 的行为保持一致。同版本的另一提交将超时 abort reason 改为 TimeoutError DOMException,使消费者可以通过 signal.reason.name === 'TimeoutError' 区分框架超时和用户主动取消,不再混用同一个 AbortError。

这对 Agent 平台中所有带超时的流式执行都有借鉴意义:tool streaming、SSE events、实时音频、MCP 响应——任何流在产生第一个数据前的停滞期都需要被超时覆盖,而超时的生命周期不应绑定到「注册时刻」而应绑定到「流的实际执行结束」。实践中意味着 AbortSignal.timeout() 的添加时机应该在流实际开始 yield 之后,或者在创建时就要把清理逻辑与流的终止事件(finish、error、abort)挂钩而非放在同步注册路径上。

模型输出不合法不一定要崩溃

AI

OpenAI Agents Python v0.17.8 昨天合并了一个新 pattern:invalid_final_output recovery handler。当模型输出的结构化数据(Pydantic model)校验不过时,不再只有 ModelBehaviorError 一条路,而是可以注册一个 handler 返回合法的 fallback,让整个 run 继续正常结束。

这个 handler 能拿到错误类型(malformed non-empty vs empty output)、原始模型输出和目标 schema,但不能拿请求上下文——设计上就是纯输出修复,不是重启。它也明确不重试模型请求也不回放工具副作用,因为 tool side effect 重复是不可接受的。返回的 fallback 会走正常的 guardrails、hooks、streaming events 和 session persistence 链路——是完全一等公民的 terminal output。

工程含义很直白:structured output 失败是 Agent 系统的常态,不是异常。Schema 越复杂越细,模型越容易在边界上输出不合规内容(字段缺失、类型错误、枚举外值)。把这种失败建模成可恢复信号(而不是 raise),意味着:调度器不需要为每个 Agent 调用包一层 try/except、审批流程不会因为一次校验失败打断、log 里的 error 级别条目变少。OpenAI 自己还做了 error-kind dispatch,防止无关的 ModelBehaviorError 被意外捕获——不是所有校验失败都走同一个 handler。

对于做 Agent 平台(比如 OPC)来说,这个 pattern 启示了一个更通用的契约:terminal output 应该有 on_invalid / on_empty / on_timeout 分层恢复钩子,每个钩子明确只承诺「修复输出」而不承诺「重做执行」。恢复钩子和工具执行之间应该有 clear boundary,避免 side effect 重复。

问问自己:你 Agent 跑 1000 次 structured output,有多少次会因为模型「差一点点就合规」而丢掉了整个结果?

关闭思考也要显式透传

AI

关闭模型的 thinking / reasoning 不是一个前端偏好,而是 provider transcript contract。Vercel AI SDK 7 月 5 日在 @ai-sdk/anthropic@3.0.93 修复了一个很小但很典型的问题:providerOptions.anthropic.thinking = { type: "disabled" } 通过了 schema,却在发往 Anthropic Messages API 时被剥掉了;对默认开启 thinking 的模型来说,结果可能是后台仍然思考,并把有限的 max_tokens 预算先消耗掉。

这类 bug 的工程含义不在 Anthropic 一家,而在 Agent runtime 的“意图投影”边界:UI、策略层或路由器说“禁用深度思考”,最终必须变成 provider 能识别的显式字段,而不能只停留在内部配置。否则同一个 Agent 在不同 provider / model 上会出现隐形分叉:有的真的关掉 reasoning,有的只是看起来关掉,延迟、成本、输出长度和失败模式都会漂移。

OPC 类平台尤其容易踩这个坑,因为我们会把 model switch、budget guardrail、tool approval、用户等级和任务风险都压成一组运行参数。比较稳的做法是把“推理预算/是否启用思考”当成可观测的 request attribute:发送前保留原始 intent,provider adapter 做确定性映射,trace 里记录最终 payload 摘要,并用 contract test 覆盖 enabled/disabled/budget 三种情况。

可以问自己一个检查题:如果用户说“这一步只要快,不要深想”,我们现在能不能在日志里证明每个 provider 真的收到了这个约束?

OOM恢复要快速封口

AI

Agent runtime 的恢复逻辑不能把所有中断都当成“等一等再重试”的暂时故障;一旦是内存上限触发的崩溃,最危险的不是失败本身,而是系统把同一个超大工作集、同一段 LLM 推理、同一批恢复步骤无限重跑,最后变成静默烧钱和队列堵塞。

Cloudflare Agents 0.17.1 针对 Durable Object memory-limit reset 做了一个很有工程味的改动:chat recovery 不再只靠通用的 maxAttempts / no-progress timeout,而是把 OOM 单独分类,维护 durable 的 oomAttempts,并设置 chatRecovery.maxOomRetries(默认 3)。如果一个 turn 因为 128MB isolate 上限反复重启,系统会很快把这次恢复封成 out_of_memory,而不是继续假装它可能是部署抖动、连接中断或普通 transient。

更关键的是他们还在 alarm 边界加了 circuit breaker。因为最坏情况是 OOM 发生在恢复 bookkeeping 之前,甚至连“记录一次 OOM”的小写入都没机会完成;如果 alarm() 把异常原样抛出去,平台会自动重试 alarm,于是后台恢复任务可能永远重启同一个注定失败的 turn。外层 alarm 捕获只针对 memory-limit reset,累积少量 strike 后清理循环调度行并封存恢复状态,这比“多加几个 retry”更接近运行时协议设计。

对 OPC 或任何多租户 Agent 平台来说,这类 bug 的启发是:恢复系统要区分可恢复中断和确定性资源失败。stream 断了、部署切换、worker eviction 可以重试;但内存爆掉、上下文过大、工具输出失控,通常需要降级、切块、转人工或生成可解释失败,而不是继续恢复。预算也不该只有时间预算,还应有 work budget、token/cost budget、OOM-specific budget 和可观测的 seal reason。

如果你现在设计一个长任务 Agent,会在哪些地方把“继续恢复”改成“快速封口并解释失败”:上下文压缩、工具输出、文件摄取、子 Agent fan-out,还是浏览器会话回放?

工具结果要按调用顺序投影

AI

多工具 Agent 的一个隐蔽可靠性问题是:工具可以并发完成,但结果回到消息历史时,不能按“谁先回来”或数组偶然顺序投影。

Vercel AI SDK ai@7.0.12 修复了 generation output 转 response messages 时按 tool call order 排序 tool results 的问题。这个补丁看起来很小,但它碰到的是 provider contract 和 UI transcript 的交界处:模型发出的 tool call 有明确顺序和 call id,后续 tool result 必须能和原调用稳定配对。否则 trace 看起来像成功,模型读到的上下文却可能已经错位。

在真实产品里,这种错位很难排查。比如一个 Agent 同时查用户权限、拉项目列表、读文件摘要,三个结果都合法,但如果投影顺序错了,UI 可能把 A 工具的结果展示到 B 调用下面,模型也可能把错误证据用于下一步推理。

工程上的原则是:原始事件流可以并发,投影层必须确定。每个 tool call 都要保留 call id、source、原始顺序、完成时间和结果引用;展示给模型、前端、审计系统时,再按各自 contract 投影,而不是复用一个“刚好能显示”的数组。

如果 OPC 以后支持并发工具和多 Agent 汇总,我们的 transcript 应该保存原始事件流,还是只保存投影后的聊天消息?答案会决定后面能不能可靠回放和审计。

附件句柄要跨 Agent 传递

AI

多 Agent 系统里,用户上传的图片、PDF、语音不是普通 message part,而是一种“可重新取回资源”的能力句柄;一旦路由到子 Agent、Durable Object、队列 worker 或后台任务时只序列化了文件名、大小、URL 这些展示字段,真正能下载原文件的身份就可能丢失。

Cloudflare @cloudflare/think@0.12.1 刚修了一个很具体的例子:conversation resolver 把线程转给 sub-agent Durable Object 前,会调用 serializableMessengerEvent()。旧逻辑为了跨边界可序列化,只保留 attachment 的 id/mediaType/name/size/text/url,丢掉 fetch/raw/data。这本身合理,因为闭包和原始对象不能安全跨进程;问题是 Telegram 这类 adapter 的文件标识只存在 fetchMetadata.fileId,顶层 id 并不一定有值。结果是照片进了子 Agent 后,看起来还有 attachment,但既没有 attachment.fetch,也没有可用的 attachment.id,下游无法重新下载。

这个修复没有把二进制塞进事件,也没有强行保留不可克隆的 fetch 闭包,而是新增一个可序列化的 fetchMetadata?: Record<string,string>,让它穿过 messenger event 序列化;同时从 id/fileId/mediaId/fileUniqueId 这类已知 metadata key 里 best-effort 回填顶层 id。下游 Agent 再用 adapter 的 rehydrateAttachment() 把这个元数据还原成可下载能力。

对 OPC 类平台很有启发:如果入口 Agent 收到“帮我看这张故障截图/合同/发票”,然后把任务分给视觉 Agent、文档 Agent 或合规 Agent,attachment 的运行时模型不能只是 {url, name, mime},也不能偷懒把整文件 base64 塞进上下文。更稳的抽象应该是 AttachmentRef:包含 provider、platform file id、media type、size/hash、tenant/actor scope、过期策略、rehydrate 方法和审计字段;跨 Agent 传递的是这个能力引用,真正下载发生在有权限的执行边界。

常见误区是把“能在当前 handler 里读到文件”误认为“系统里所有后续步骤都能读到文件”。一旦进入子 Agent、重试、恢复、异步队列或跨机器执行,闭包、临时 URL、内存 buffer 都会失效。今天如果你设计一个 Agent router,会把 attachment 当作聊天记录的一部分,还是当作一张需要显式授权、可恢复、可审计的资源票据?

覆盖语义要能穿过序列化

AI

Agent 的状态更新不能只看“写进去了没有”,还要看写进去之后,覆盖、合并、追加这些语义有没有在 checkpoint 和 replay 之后保持不变。

LangGraph 1.2.7 里两个很小但很典型的修复值得注意:一个是 DeltaChannel overwrite supersteps 的 snapshot 处理,另一个是让 Overwrite 能穿过 JSON roundtrip。它们指向同一个问题:运行时里很多状态不是普通值,而是带更新语义的值。序列化如果只保留数据形状、不保留更新意图,恢复后系统看似正常,实际已经把“覆盖”误解成“合并”或“追加”。

这在 Agent 平台里尤其危险。消息投影、工具结果、审批状态、memory delta、UI patch、子任务进度都可能依赖 overwrite 语义。一次恢复后如果把旧的 pending approval、过期 tool result 或被覆盖的状态又合并回来,模型可能基于幽灵状态继续行动。

工程上应该把状态操作也纳入 schema:哪些字段是 append-only,哪些字段 last-write-wins,哪些字段必须带 revision,哪些字段只能由特定 actor 覆盖。checkpoint 不是简单 dump object,而是在保存一段可重放的状态协议。

当我们设计 OPC 的任务状态和 Agent memory 时,有没有哪些字段现在看起来只是 JSON,但其实已经需要明确“覆盖/合并/追加”的协议语义?

批准前也要跑输入护栏

AI

Agent 的人工批准不应该是第一道安全闸,而应该是“已经通过机器校验之后”的授权动作。OpenAI Agents Python v0.17.6 新增的 pre_approval_tool_input_guardrails 很小,但它把一个常见运行时漏洞说清楚了:如果模型提出的工具调用本身就不合规,系统不应该先弹出 human approval,让人类在噪声里判断,更不应该把“人点了批准”当成输入已经安全的证明。

这个机制的顺序是:本地 function tool 准备触发人工批准前,先跑一次 input guardrails;如果护栏拒绝,SDK 直接把拒绝原因作为 tool output 回给模型,不产生 approval request,也不执行工具;如果护栏放行,才进入 pending approval。更关键的是,批准之后、真正执行之前还会再跑同一套输入护栏一次,避免“批准时合法,恢复执行时环境/策略/参数已变化”的时间差问题。

放到 OPC 或任何 Agent 平台里看,approval ticket 应该只承载“是否允许这个已成形动作发生”,不应该混杂 schema 校验、租户权限、路径能力、金额上限、目标对象存在性这些机器可判定的事情。一个删除资源、转账、发消息、改配置的工具调用,最好先经过 typed schema、policy guardrail、risk classifier 和 resource binding,再生成给人的审批卡片;人批准后也要用同一个 call_id、输入摘要、policy version 和 actor scope 重新校验,而不是直接 resume。

常见误区是把 human-in-the-loop 当成万能兜底:只要有人看过,就可以少做前置验证。实际上这会制造两类问题:一是把大量显然非法的调用推给人,降低审批质量;二是批准事实和执行输入脱钩,后续 replay/resume 时很难证明“被批准的就是将要执行的”。更稳的做法是把批准建模成服务端能力票据:它引用已校验的工具调用身份,而不是替代工具调用验证。

你现在设计的高风险工具里,哪些 approval request 其实应该在创建审批卡片之前就被 guardrail 拦掉?哪些在用户批准之后、执行之前还需要重新验证一次?