并发流中的 onFinish 归属竞态
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」?