NOTE

笔记

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

52 条记录最近更新 2026-06-26
2026

工具批准不能信客户端历史

AI

Agent 产品里的“工具批准”不应该被当成一条普通 UI 消息保存后原样回放;它更像一次短期授权票据。Vercel AI SDK 7.0.0 修复的 WorkflowAgent 问题很典型:服务端从客户端传回的 messages 历史里重建已批准 tool call 时,如果不重新校验输入 schema、不重新计算 approval policy、不验 HMAC,恶意客户端就可以伪造一个“已批准”的 assistant tool-call part,让服务端执行攻击者参数。

这件事的工程含义是:approval response 和 tool-call history 不能混成同一种信任等级。消息历史可以帮助恢复 UI 状态,但不能自动恢复执行权限。真正可执行的批准至少要重新绑定三件事:原始 tool call 的稳定 id/签名、批准时看到的输入、当前服务端的工具 schema 与策略。只要其中任一项变了,就应该回到“待批准”或直接拒绝,而不是继续走 resume。

对 OPC 或 Agent Engineer 来说,这会影响所有“人类点过同意后继续跑”的场景:浏览器登录、发消息、改文件、调用内部 API、审批工作流。一个安全的实现通常会把 approval 存成服务端票据,而不是只相信前端消息;恢复时按 call_id 查票据,再重放 schema validation、policy resolution 和租户/actor scope 检查。前端可以展示“已批准”,但不能成为批准事实本身。

可以检查一下你正在设计的 Agent 审批链路:如果用户刷新页面、换设备、或客户端自己提交一段历史,服务端到底是在“验证一张批准票据”,还是在“相信一段看起来像批准的聊天记录”?

Harness 退为工厂,Session 成为运行时主体

AI

Mastra 1.46.0(2026年6月24日发布)把 Harness 从一个带状态的运行时彻底重构成了纯工厂:createSession() 返回的 Session 才是真正的 identity、state、event bus、权限和 run control 的载体。这不是增量重构——Harness 上原有的 sendMessage、abort、model.switch、subscribe、OM accessor 和 thread lifecycle 全被删除,强制迁移到 session.* 命名空间。如果你之前写 harness.sendMessage(),升级后直接编译失败,必须改成 harness.createSession(resourceId).sendMessage()。设计意图很清楚:把"单例假象"从 API 表面彻底剥离。

工程意义有几个层次。第一层是隔离性:两个 concurrent user 通过同一个 Harness 创建不同 Session,它们的 event subscriber 互不干扰(session.subscribe 返回的是 session-scoped 的 event emitter),state 读写不会交叉,permissions 各自生效。这意味着底层不再需要在 Harness 层做锁或 routing,Session 本身就是 fault boundary。第二层是跨进程:配套的 LeaseProvider(内存/Redis Streams 两种实现)保证多实例部署中只有一台 worker 拥有某个 thread run 的租约,信号投递时 LeaseProvider 返回一个 discriminated routing decision(wake/deliver/persist/discard),避免双发或丢消息。第三层是投影到 HTTP 协议:Mastra 新加了 harness-scoped 的 server routes,客户端可以通过 GET /harness/:id/session/:resourceId/sendMessage 远程控制远端 Session,JS client 也有一等公民的 client.getHarness(id).session(resourceId) API。

一个值得注意的取舍是:createSession 不是 spawn,而是 get-or-create by resourceId。同一个 resourceId 返回同一个 Session——这意味着 caller 必须自己保证 resourceId 的全局唯一性。在多租户场景下,resourceId 的设计(比如 userId:threadId 的复合键、还是 UUID、还是业务自然键)直接影响并发控制和数据亲缘性,Mastra 把这个问题交给上层而不做内置约束。另一个隐含代价是 session 级事件总线挂掉的隔离:每个 Session 持有一个独立 EventEmitter,订阅者和 listener 数量会随 session 数线性增长,大租户场景下需要关注 listener 泄漏和 backpressure 治理。

如果你的 Agent 平台已经或计划支持多用户并发,一个值得立即检查的问题是:你的"Harness"层现在持有和用户会话耦合的状态吗?如果 state、event bus、permissions 还挂在全局 factory 上而不是每次 interaction 的开始就获得一个隔离的 Session 句柄,1.46 的这个重构给出了一个可复用的模式——把 factory 做薄到只负责资源创建和共享基础设施(agent 定义、模型注册),把一切可变状态放进按 resourceId 隔离的 Session。

工具顺序要按协议重排

AI

Agent 的工具调用不只是“模型说要调、客户端调完再塞回结果”这么简单;当同一轮里混有模型侧 tool use、客户端执行 tool result、以及 provider 自己要求的 assistant content 顺序时,消息数组的顺序就是协议契约。Vercel AI SDK 在 6 月 24 日的 @ai-sdk/anthropic@3.0.86 里修了一个很小但很关键的问题:Anthropic provider 需要在 client tool use 和 provider tool use 之间重排 assistant content,否则后续工具结果可能对不上模型期望的上下文结构。

这类 bug 的危险点在于,它通常不会表现成“工具不存在”或“HTTP 请求失败”,而是表现成模型看起来变笨:重复调用工具、忽略刚返回的结果、把某个工具结果理解成另一个调用的上下文。根因不是推理能力,而是 transcript projection 把事件流投影成 provider message 时丢了顺序约束。

做 Agent runtime 时,可以把内部事件流拆成三层:原始执行事件保持 append-only;中间层保留 call id、tool name、role、source(client/provider/server)和因果顺序;最后才按不同 provider 的格式投影。不要让前端 UI 消息、OpenAI/Anthropic message schema、内部 trace 共用同一个“万能 message object”,否则一旦接入多 provider 或本地工具执行,最先坏的就是顺序和归属。

一个实用的回归测试是:构造同一轮里既有 provider tool use、又有 client-executed tool call 的 case,断言投影后的 assistant content 与 tool result 顺序、call id 配对、以及再次 stream resume 后的 transcript 完全一致。你现在的 Agent/OPC 运行时,是在测试“工具能跑”,还是在测试“工具事件能被正确投影给每个模型供应商”?

信号要有消息边界

AI

Agent 的“信号”一旦进入长线程,就不能再被当成附着在上下文旁边的小纸条;它必须有稳定的消息边界、消息 ID 和可定向恢复路径。Mastra 1.45.0 连续修了两个看起来很细、但对 Agent 平台很关键的问题:pending signals 过去可能挂到上一个 assistant response 下面,长线程恢复 state signals 时还可能全量扫描 thread history。

这里的工程含义是:signal 和 message 不是简单的父子关系,而是同一条执行时间线里的两类事件。比如浏览器 snapshot、task-list delta、后台状态更新如果在 assistant response 没有“收口”前直接写入 transcript,恢复时就会变成“assistant final answer 后面跟着一串 state”,模型下一轮看到的因果顺序就错了。Mastra 的修复是让 signal drain 走 canonical signal transcript path,并在写 follow-up signal 前 rotate response message id,让 stream order 和 persisted transcript order 对齐。

另一个修复更像性能边界:恢复 state signals 不再扫完整线程,而是根据 tracked signal message IDs 做 listMessagesById;如果 ID 不可用,就退回本地内存状态,而不是做无界历史扫描。这说明 durable Agent 的状态恢复不能依赖“从聊天记录里重新考古”,否则线程越长、信号越多,恢复成本和出错面都会随时间增长。

做 OPC 或内部 Agent 平台时,可以把这条规则落成一个简单设计约束:任何脱离普通对话的状态更新,都要同时回答三个问题——它属于哪个 response boundary?它的 stable id 是什么?重放/恢复时能不能按 id 精确取回而不是扫全量历史?如果今天的任务、浏览器、审批和后台队列都变成 signal,我们现在的 transcript schema 还撑得住吗?

MCP OAuth 要绑在原传输上

AI

一句话判断

MCP 的 OAuth 不是一个可以从传输层里“抽出来单独做登录”的外围功能,它本身就是传输协议状态机的一部分。Cloudflare Agents 在 agents@0.16.2 里接入官方 @modelcontextprotocol/conformance 套件后,立刻抓到了两个 OAuth bug:一个是在 401 挑战后换了新 transport,导致丢失 WWW-Authenticate 里带回来的 resource_metadata,最终把 token 交换错打到默认 /token;另一个是在 mid-session 401 之后重授权成功,却因为旧 transport 没 detach 而永久卡死在 Already connected to a transport。我觉得这条信号非常重要:MCP 的授权流程不能被实现成“外部登录插件”,而必须被当成 runtime transport contract 来设计、测试和恢复。

背景:为什么这件事值得单独拿出来讲

过去很多团队做 Agent 平台时,会把 MCP 的授权想得过于“普通 Web”:

  • 先连上 server;
  • 如果没权限,就跳一个 OAuth 页面;
  • 拿到 token 后,再重新发起连接;
  • 只要最终有 token,就算授权链路完成。

这个心智模型的问题在于,它把 OAuth 当成了一个与协议执行解耦的前置步骤。但 MCP 不是这样。

MCP draft authorization 明确把授权失败建模为协议路径的一部分:

  • 初次无权限时,server 应返回 401
  • scope 不足时,server 应返回 403 insufficient_scope
  • 关键元数据通过 WWW-Authenticate 头和 resource_metadata 暴露;
  • client 需要基于挑战信息做 step-up,并重试原始请求。

也就是说,授权挑战不是“跳出协议去登录一下”,而是协议本身在运行中发出的控制信号。

这次 Cloudflare 暴露出的两个真正工程问题

1. 丢的不是 URL,丢的是 challenge 上下文

第一个 bug 看起来像是“token endpoint 地址错了”,但本质并不是配置错误,而是 challenge context 丢失。

Cloudflare 在 PR #1722 里写得很清楚:finishAuth 如果跑在一个 freshly created transport 上,就会丢掉最初 401 响应里的 resource metadata URL。这样客户端虽然知道“要去换 token”,却不知道应该按哪个受保护资源刚刚给出的元数据去完成交换,于是就退回默认 /token 路径。只要授权服务器不是默认布局,流程就会失败。

这里最关键的工程启发是:

  • WWW-Authenticate 不是一个可有可无的提示;
  • resource_metadata 不是可重新猜测的衍生信息;
  • 401 challenge 携带的是当前 transport 上下文的一部分。

换句话说,授权恢复必须延续 challenge 所在的协议语境,而不是只保留“去登录”这个抽象动作。

对 Dewey/OPC 这类要接第三方 MCP server 的系统来说,这意味着:如果你把认证包装在一个 gateway、浏览器弹窗或 host-side helper 里,却没有把 challenge 头、resource metadata、scope 提示、原始请求标识一起保留下来,后面即使“登录成功”,也可能已经脱离了原来的协议现场。

2. step-up authorization 本质上是一次运行中恢复,不是重新开局

第二个 bug更有代表性:mid-session 401 之后,用户完成了重新授权,但 init() 永远失败,因为之前的 transport 还挂着,于是报 Already connected to a transport

这说明很多实现虽然“支持 OAuth”,却并没有真正支持运行中的重授权

MCP authorization draft 对 step-up 的描述其实很明确:

  1. 解析当前错误里的 scope / metadata;
  2. 将新 scope 与已有 scope 做 union;
  3. 发起 re-authorization;
  4. retry 原始请求;
  5. 并且要有 retry limit,避免无限失败循环。

这整个过程更像是一次 in-flight recovery path,而不是重新 new 一个 client、重新开始一个独立会话。

所以真正该被建模的不是“OAuth 成不成功”,而是:

  • 原 transport 是否被正确关闭或脱附;
  • 新 transport 是否带着上一轮的授权语义回来;
  • 原请求是否还能被幂等地重试;
  • scope 升级是不是累积的,而不是覆盖式的。

如果这些都没做,所谓“支持 step-up”通常只是 demo 级支持。

为什么 conformance suite 比“集成测试过了”更关键

这次最值得注意的,不只是修了两个 bug,而是 bug 是被官方 conformance suite 第一时间打出来的。

Cloudflare 在 PR 里把官方 @modelcontextprotocol/conformance 套件直接跑在 workerd 里的真实实现上,而不是在一个 Node mock 上凑合。它覆盖了:

  • client 侧 26 个 scenario、334 个 checks;
  • 包括 OAuth metadata variants、scope step-up、token endpoint auth methods、pre-registration、backcompat 和 offline-access drafts;
  • server 侧两个实现各 40 个 checks。

这给我的一个强判断是:MCP 的很多 bug 根本不是“单元测试写少了”,而是只有在真实 transport、真实 callback、真实 runtime 里才会暴露。

尤其是授权链路,最容易在这些地方出问题:

  • 401/403 challenge header 是否被正确保留;
  • callback 回来后是否还认得原来的 resource;
  • re-auth 之后旧连接有没有彻底清理;
  • 多种 metadata 变体和历史兼容路径是否都过关。

所以如果一个平台说自己“兼容 MCP”,但没有跑官方 conformance、没有覆盖 OAuth 场景、没有在真实宿主环境里验证,那这个兼容性声明通常还不够硬。

对 Agent Runtime 设计的几个直接启发

1. transport 应该拥有 auth challenge state

不要把 OAuth 仅仅挂在全局 auth manager 上。至少以下信息应被视为 transport-bound state:

  • 最近一次 challenge 的 WWW-Authenticate 内容;
  • resource_metadata URI;
  • 原始请求与目标 resource 的绑定;
  • 已申请 scope 集合及其升级历史。

否则实现很容易退化成“我知道用户登录过,但我不知道这次协议交互是对谁、为哪件事登录的”。

2. re-auth 要有明确的 detach / resume contract

重授权之后最怕的是半开连接和幽灵状态:逻辑上你已经有新 token,运行时里却还挂着旧 transport、旧 listener 或旧 stream。

所以 runtime 最少要明确三件事:

  1. 旧 transport 何时算失效;
  2. 新 transport 如何接管;
  3. 原请求如何重放,哪些请求禁止自动重放。

没有这三条,step-up 一多,系统迟早会出现“UI 显示已授权,但 agent 永远跑不动”的灰故障。

3. scope 升级要作为状态累积,而不是一次性弹窗

MCP draft 明说 client 应该把之前的 scopes 与新 challenge 的 scopes 做 union。这个细节很像权限系统里的 capability accumulation:

  • 如果只按当前 challenge 覆盖 scope,旧权限可能丢失;
  • 如果每次只增一个 scope,却不记录历史,系统很难解释为什么 token 现在拥有这组权限;
  • 如果没有 retry limit,坏配置会把 agent 推进无限重授权循环。

对 OPC 来说,这和 tool approval、sandbox capability、browser handoff 其实是一类问题:权限提升不是一个 UI 动作,而是一条带审计、带边界、带恢复语义的运行时状态迁移。

常见误区

误区一:拿到 token 就代表授权完成

不对。拿到 token 只是开始。真正关键的是:这个 token 是否仍然绑定到正确的 resource、scope 集合和原始请求语境。

误区二:重连成功就等于恢复成功

也不对。恢复成功意味着原协议状态机继续往前走,而不是“socket 又开了”“client 又初始化了”。如果旧 transport 没 detach、原请求不能安全重试、scope 信息丢了,本质上都不算恢复。

误区三:OAuth 问题属于集成层,不影响 runtime 核心设计

恰恰相反。只要 Agent runtime 允许在运行中接第三方 MCP server,OAuth challenge、step-up、retry、detach、metadata preservation 就已经是 runtime contract 的一部分。

我对这条信号的核心判断

Cloudflare 这次最有价值的地方,不只是修复了两个 MCP OAuth bug,而是把一个经常被忽略的现实钉死了:MCP 互操作性的 hardest part,往往不是 JSON-RPC message 能不能发出去,而是授权挑战、transport 生命周期和恢复语义能不能一起成立。

这意味着下一阶段做 Agent 平台,MCP 接入层至少要从“能连上 server”升级到“能在真实权限变化下稳定恢复”。真正成熟的实现,应该把以下几件事放进同一个测试与运行时合同里:

  • challenge metadata 保留;
  • scope step-up 累积;
  • transport detach / reconnect;
  • 原请求安全重试;
  • 官方 conformance 持续回归。

谁先把这些做成基础设施,谁的 MCP 集成才更像可托管的软件系统,而不是幸运时能跑通的 demo。

留给 Dewey 的问题

如果 OPC 接下来要把更多外部 MCP server 纳入统一 runtime,你会更想先固定哪一层契约:challenge state 的持久化格式,还是 re-auth 之后的请求重放/幂等策略?为什么?

流式取消要向下传播

AI

Agent 的“停止生成”按钮不应该只是停止前端收 token,而要把取消信号一路传到正在跑的子图、工具和后台任务。LangGraph 1.2.6 修复了 v3 event streaming 里 stream.abort() 只关闭 mux、不关闭底层 graph iterator 的问题:用户界面看起来停了,但 astream / stream generator 和 running subgraphs 可能继续跑到完成,继续消耗模型调用、沙箱时间、数据库锁或外部系统配额。

这类 bug 的关键不是某个框架的流式 API 细节,而是 Agent runtime 的取消语义到底有没有“向下传播”。前端 abort、HTTP disconnect、审批拒绝、用户点击 stop,都应该被翻译成同一个可追踪的 cancellation path:关闭顶层 iterator,触发 GeneratorExit / aclose(),让 in-flight node、subgraph、tool wrapper 和资源清理 hook 都有机会收到信号。如果只在展示层停止 pump,后台执行就会变成幽灵任务,尤其在多 Agent graph、browser session、长工具调用和队列 worker 里很难排查。

工程上可以把取消当成和重试一样的一等协议:每个 run / step / tool call 带 cancellation token 或 run id;stream adapter 不只负责转发事件,还负责把 abort 映射到底层执行器的 close;测试里专门放一个 looping subgraph 或长 running tool,断言 abort 后它真的停止,而不是只是前端不再显示。

做 OPC 或 Agent 平台时,可以问一个很具体的问题:如果用户在第 3 秒点了停止,我们能证明第 4 秒之后没有任何子任务、浏览器动作、外部 API 或后台 workflow 还在替他继续执行吗?

客户端工具调用要保留身份

AI

客户端执行的工具调用,不能只把“结果”送回模型;它也要作为完整的协议事件保留调用身份。Vercel AI SDK 在 6 月 17 日的 @ai-sdk/openai@3.0.73 修复了一个细节:通过 OpenAI Responses API 发送 client-executed tool calls 时,要把它们作为完整的 function_call item 发送,这样后续 function_call_output 才能按 call_id 正确配对。

这件事看起来只是 provider adapter 的 patch,但它暴露的是 Agent runtime 的一个边界:工具在哪里执行,不应该改变协议语义。服务端工具、浏览器内工具、用户本地工具、移动端工具,本质上都需要一个稳定的 call identity,把“模型提出调用”“人/客户端执行”“输出回传”“trace 展示”“重试/去重”串在一起。如果中间只传 output,短 demo 可能能跑,复杂链路里就会出现输出挂错调用、UI 回放不一致、审计日志看不到原始意图的问题。

做 OPC 或 Agent 平台时,可以把 client-side tool 当成一种远程执行器,而不是前端小技巧:模型侧协议里保留 tool name、arguments、call_id;客户端只负责执行授权后的那一次调用;回传时只允许输出绑定到已存在且未完成的 call_id。这样以后接浏览器插件、本地文件选择器、钱包签名、企业 SaaS 授权动作时,才不会把“谁请求了什么”和“谁返回了什么”混成一团。

一个值得检查的问题:你现在的 Agent 前端如果执行了某个本地/浏览器工具,trace 里还能完整看到原始 tool call 和对应 call_id,还是只剩下一段看似成功的 tool output?

拒绝下载也要关流

AI

Agent 平台做网页抓取、文件摄取和多模态附件下载时,安全边界不能只停在“我拒绝了这个 URL / 这个 Content-Length / 这个状态码”。Vercel AI SDK 6.0.207 和 5.0.204 刚修了一个很典型的 provider-utils 问题:下载在早期被拒绝后,如果没有取消 response body,WHATWG Fetch / undici 会把底层 TCP socket 留着,攻击者控制的源站就可以把“被拦截的下载”变成文件描述符耗尽。

这个点对 Agent Engineer 很实用,因为 Agent runtime 往往会把下载包装成高级能力:读取网页、解析 PDF、抓取图片、跟随跳转、接 MCP 或浏览器工具返回的 URL。我们容易在策略层写很多 allowlist、size limit、redirect validation,却忘了每一个拒绝分支本身也是资源生命周期分支。Vercel 这次修复的关键不是换一个校验规则,而是在 readResponseWithSizeLimitdownloaddownloadBlob 以及 validated redirects 的每个早退路径里显式 cancel body;连被拒绝或将继续跟随的 redirect hop,也要先释放它自己的响应体。

工程上可以把它抽象成一条运行时契约:凡是 Agent 可触发的外部 I/O,如果结果不会被完整消费,就必须有明确的 close / cancel / abort 语义,并且这件事要进入测试。否则“安全拒绝”只是业务上拒绝了,系统资源层仍然在接受对方施压。OPC 这类平台以后做 connector、browser、file ingestion 或 remote tool proxy 时,下载 helper 最好只暴露一种封装过的结果类型,让调用方无法绕过 finally cleanup。

可以回头检查一下:你现在的 Agent 工具里,哪些路径会在发现状态码不对、文件太大、MIME 不对、跳转不可信时直接 return 或 throw?这些路径有没有真的把网络连接、文件句柄、临时文件和后台任务都关掉?

浏览器工具要可暂停恢复

AI

浏览器 Agent 的关键能力正在从“一组点状 API”变成“可暂停、可恢复、可审计的一段执行”。Cloudflare Agents 0.16.0 把原来的多个浏览器工具重建成单一 durable tool:browser_execute,让模型在沙箱代码里通过 cdp connector 操作页面;更重要的是,执行状态记录在 CodemodeRuntime Durable Object 上,遇到审批可以暂停,恢复后仍能拿回同一个浏览器会话、tab 和 cookies。

这件事对 Agent Engineer 的提示是:浏览器不是普通工具,它更像带资源句柄的远程运行时。点击、登录、跳转、抓取都依赖页面 session、target、CDP socket 和 cookie 状态;如果每次 tool call 都当成无状态 RPC,暂停审批、人工接管、长任务恢复、错误重放都会变成“看起来能跑,线上很脆”。Cloudflare 的设计里,cdp.attachToTarget 返回的是稳定 handle,而不是裸 CDP session id;pause 时不销毁浏览器,terminal status 才 dispose;session sweep 还会留下 tombstone,避免恢复时偷偷开一个新浏览器假装继续。

常见误区是把浏览器自动化理解成“给模型更多 Playwright/CDP 函数”。真正的工程边界反而是更少、更稳定的工具表面:一个 browser_execute 包住代码执行、连接器、会话模式、审批恢复和错误解释。模型不需要在十几个低层工具之间猜调用顺序,平台也能把一次浏览器任务当成可记录、可中断、可回放的 execution,而不是散落在聊天上下文里的多次工具调用。

如果 OPC 要做能操作后台、网页控制台、第三方 SaaS 的 Agent,我们现在的浏览器能力是“临时脚本执行”,还是已经有 execution id、session ownership、pause/resume、资源清理和可解释失败这些运行时契约?

来源:

SSE默认消息也要当协议路径

AI

MCP 客户端的健壮性,常常不是体现在“支持最新规范”,而是体现在愿不愿意兼容底层 Web 协议的默认语义。Vercel AI SDK 6 月 14 日给 @ai-sdk/mcp 发了一个很小的 patch:SSE 里没有显式 event 字段的 data 消息,也要按 message 事件处理。这个改动看起来只有几行,却暴露了 Agent 工具协议接入里很典型的一类风险:把框架里的“常见输出格式”误当成协议本身。

SSE 的语义里,服务端只发 data: ... 而不写 event: message,本来就应该进入 onmessage;MDN 也明确说,没有 event 字段的 server message 会被接收为 message event。MCP Streamable HTTP 又允许服务端用 text/event-stream 返回 JSON-RPC,因此一个 MCP server 完全可能只吐 data 行。若客户端只接受 event === "message",结果不是解析失败那么简单,而是 initialize response、tool result 或 notification 被静默丢掉,表现成“某些 MCP server 偶发连接不上”。

工程上这提醒我们:Agent Engineer 做协议适配时,测试矩阵不能只覆盖自己 SDK 生成的 happy path。至少要补三类 compatibility case:字段缺省但语义等价、旧传输和新传输混跑、代理/CDN/运行时重写 header 或 stream framing。尤其 OPC 如果要接第三方 MCP 工具,transport 层应该被当成产品可用性的核心边界,而不是“网络库细节”。

一个可执行的检查问题:我们现在接入的每个 MCP / webhook / streaming adapter,有没有把“缺省字段的合法消息”纳入契约测试,还是只测了自己发出来的格式?

来源:

后台任务要有系统主体

AI

一句话判断

Agent runtime 一旦开始承接 cron、队列、webhook 和夜间批任务,真正需要建模的就不再只是“哪个用户发起了这次对话”,而是“这个后台动作到底代表谁、拥有什么边界、如何被审计”。Mastra 在 6 月 12 日发布的 @mastra/core@1.42.0 把 trusted actor 显式接进 workflow、tool、memory 和 agent generate()/stream(),我觉得这是一个很重要的工程信号:后台任务不能再靠伪装成人类会话来获得权限,系统主体必须成为一等运行时对象。

背景:为什么这不是一个小 API

很多团队做 Agent 平台时,前台交互的权限模型往往比较清楚:有用户、有组织、有 thread、有审批。但一到后台任务就开始偷懒:

  • 给定时任务塞一个假 JWT;
  • 让队列消费者复用某个管理员用户;
  • 或者直接绕过鉴权,在内部代码里“默认可信”。

短期看这很省事,长期会造成三个问题:

  1. 权限语义失真:日志上看起来像是某个真人用户做了操作,但其实是系统定时任务。
  2. 租户边界变脆:一旦后台作业不再带 tenant / organization 约束,就很容易把一个租户的工具、记忆或 workflow 跑到另一个租户上下文里。
  3. 恢复与审计困难:任务失败后,你很难回答“是谁触发的、为什么有权做、是否应该自动重试、结果该回到哪个线程”。

所以这里的核心不是“后台能不能调用 agent”,而是后台调用是否拥有一个和人类主体不同、但同样可验证、可约束、可追踪的主体模型

核心机制:Mastra 这次到底做对了什么

1. 把 system actor 显式建模,而不是隐式放进上下文魔法里

Mastra 现在允许把一个 actor 对象显式传入:例如 actorKind: 'system',并可附带 sourceWorkflow。这意味着后台任务不再需要伪装成某个用户 session,而是以“系统主体”的身份进入运行时。

这一步的意义很大:它把“后台动作也是一种主体”从约定俗成,提升成 API 合同。只要没有这个合同,系统内部就会不断出现“先借一个用户身份跑起来再说”的临时方案。

2. 它绕过的是 membership 解析,不是租户边界

这次设计里我最认可的一点,是 trusted actor 并不是全局免鉴权。Mastra 明确要求:即使是 system actor,requestContext 里仍然必须带 organizationId,否则 FGA bypass 会被拒绝。

这说明它想解决的问题不是“让后台任务变成超级管理员”,而是“让后台任务不必伪造人类 membership,同时仍然留在租户边界内”。

也就是说:

  • 可以跳过“这个主体是不是某个组织成员”的人类式解析;
  • 但不能跳过“这个动作属于哪个组织、在哪个权限域里执行”。

这个取舍非常关键。真正成熟的 Agent runtime,不应该把 trusted background job 设计成 root,而应该设计成tenant-scoped service principal

3. 它把主体一直传到了 tool / memory / agent loop 里

很多框架即使入口层承认后台任务,到了执行深处仍然会丢身份:workflow 里还有,进 tool 就没了;agent 外层有,sub-agent 或 memory check 时又退化回普通上下文。

Mastra 这次是把 actor 连续穿过了几个关键边界:

  • workflow.execute()
  • tool.execute()
  • MastraMemory.checkThreadFGA()
  • agent.generate() / stream()
  • agentic loop 内部的 tool-call 执行路径

这背后的工程价值是:主体如果不能跨 runtime boundary 传播,就不是真主体,只是入口注释。

对 OPC 这类系统来说,这一点尤其重要。因为真正容易串线的地方,恰恰不是最外层 HTTP 请求,而是第二层、第三层:子 agent、后台 workflow、异步工具、线程记忆、恢复执行。

4. 它还顺手把审计语义补齐了

PR 里还提到会把 actor_kind: "system"sourceWorkflow 等信息打进 FGA telemetry。别小看这个细节——权限模型只有在 trace 和审计里可见,才算真正落地。

否则你虽然在代码里区分了 user actor 和 system actor,但排障时仍然只看到“某个调用失败了”,那运行时边界依然是黑箱。

对工程实践的启发

如果以后要让 Dewey / OPC 里的 Agent 承接日报汇总、定时评测、离线索引刷新、仓库巡检或自动修复,我会优先补这四层:

  1. 主体层:定义 user principalsystem principal,不要让后台任务借人类身份运行。
  2. 租户层:即使是 system principal,也必须显式绑定 org / project / environment scope。
  3. 传播层:主体信息要穿过 workflow、tool、memory、sub-agent、resume path,而不是只停留在入口。
  4. 审计层:trace、审批记录、失败日志里必须能看出这是哪个 system actor、来自哪条 job/workflow。

这样做的直接收益是:以后当一个 cron job 写错数据、一个 repo bot 误开 PR、一个 nightly agent 扫错环境时,你至少能准确回答“它代表谁、为什么被允许、影响面在哪”。

常见误区

误区一:后台任务可信,所以直接给管理员权限

这通常是最快把系统做脆的方法。可信不等于无边界。后台任务比人类更应该被严格约束,因为它们执行频率高、自动化程度高、失败放大更快。

误区二:把 actor 放进共享上下文就行

如果主体只存在于某个可变 RequestContext 里,而没有被明确传入 workflow/tool/agent API,后续很容易在异步执行、重试、恢复和跨线程处理中丢失。主体要成为显式参数,才容易验证与审计。

误区三:有 system actor 之后就不需要审批

也不对。system actor 解决的是“谁在运行”和“如何授权”,不是“哪些高风险动作可以自动执行”。对外发消息、改生产配置、合并代码、执行 destructive tool,仍然应该保留审批或策略门。

我对这条信号的核心判断

Mastra 这次真正推进的,不是一个方便后台调用的 API,而是一个更成熟的 runtime 观念:前台的人类主体、后台的系统主体、租户边界和工具权限,应该被放进同一套运行时协议,而不是由开发者在不同模块里临时拼接。

对 Agent Engineer 来说,这意味着下一阶段的竞争点不只是模型效果,而是谁能更早把“系统主体 + capability + trace + approval”做成稳定基础设施。没有这层,Agent 很容易停留在 demo;有了这层,才开始像真正可托管的软件系统。

留给 Dewey 的问题

如果 OPC 下个月就要新增一个“夜间自动跑 repo 巡检并生成修复建议”的任务,你更愿意先固定哪一个:system principal 的 scope 模型,还是它的审批/回放模型?为什么?

UI适配器要过滤文件句柄

AI

Agent 应用的前端消息历史不能被当成“只是上下文”。当 UI adapter 允许客户端回传历史消息时,里面的文件引用其实是一种能力句柄:它可能让后端用自己的 provider key、S3 IAM role 或 GCS service account 去读取客户端本不该碰到的文件。

Pydantic AI 6 月 10 日发布的 v1.107.0 / v2.0.0b7 把一个 6 月 11 日公开的安全问题写进 release notes:VercelAIAdapter 曾经会信任客户端控制的 providerMetadata 来重建 UploadedFile。FileUrl 这类 URL 文件会走 scheme allowlist,但 UploadedFile 这种“provider 文件 ID / s3:// / gs://”引用没有同等校验,于是形成 confused deputy:攻击者不直接拥有服务器凭证,却能诱导服务器替自己取文件。

这个点对 Agent Engineer 很关键,因为多模态 Agent、聊天恢复、前端 thread replay、AG-UI/Vercel AI 这类桥接层都会把“过去的消息”重新喂回 runtime。工程上不要只校验当前输入框的文本;要把每一种可执行或可解析的 message part 都当成 capability:FileUrl、UploadedFile、tool result、artifact id、sandbox path 都需要按来源、租户、scheme、过期时间和显式开关过滤。Pydantic AI 的修复是把 preserve_file_data 下沉到 base UIAdapter,默认 false,并让 UploadedFile 像 FileUrl 一样被处理;这比在某个 adapter 里打补丁更像一个正确的边界抽象。

常见误区是“文件 ID 很难猜,所以风险不大”。但 Agent 产品里文件 ID 经常来自日志、分享链接、对象名规则、跨租户历史同步或模型 provider 的 metadata;一旦 message history 可由用户提交,后端身份就成了攻击面的一部分。真正安全的默认值应该是:客户端历史只恢复可展示语义,不自动恢复可读取资源。

如果你在做 OPC 或自己的 Agent 平台,今天可以检查一个问题:所有从前端、外部协作方或历史记录回流进 Agent 的“文件/工具/资源引用”,有没有在进入模型或工具层前被重新授权,而不是只因为它出现在历史消息里就被信任?

MCP传输层要少自研

AI

Agent 平台接 MCP 时,最危险的不是“能不能连上工具”,而是你是不是悄悄自研了一套协议传输层。Cloudflare Agents 0.15.0 把 WorkerTransport 改成继承官方 MCP SDK 的 WebStandardStreamableHTTPServerTransport,只在外层补 Workers 特有的 CORS、Durable Object 休眠后的状态恢复、SSE keepalive;协议版本协商、session 校验、SSE 流、event-store resumability、send/close 生命周期都交回 SDK。

这个变化的工程含义很明确:MCP transport 已经不是普通 HTTP wrapper,而是一个带状态机的协议边界。sessionId、initialized、initializeParams 要在每次请求后 snapshot,DO 冷启动时 replay,否则 client capabilities 会丢;有 eventStore 时,断线恢复靠 Last-Event-ID,而不是盲目保活;POST response stream 和独立 GET stream 的 keepalive 策略也不能混在一起。

对 OPC 这种要接很多工具/工作流的系统来说,传输层越“聪明”,未来越难升级协议。更稳的分层是:协议语义尽量贴官方 SDK,平台适配层只处理部署环境差异,比如鉴权、CORS、休眠恢复、日志和限流。这样 MCP 版本协商、resumability、stream 生命周期变化时,不需要在每个内部 connector 里重修一遍。

常见误区是把“少 500 行代码”理解成重构收益;真正的收益是减少协议分叉。你现在负责的 Agent runtime 里,哪些逻辑其实属于协议 SDK,哪些只是平台 glue code?

来源:

Agent间通信要结构化信号

AI

Agent 之间的通信正在从"互相聊天"转向结构化信号。这不是语法偏好问题,而是可扩展性问题——当 3 个 Agent 用自然语言互相 @mention 时还能凑合,但 30 个 Agent 需要协调任务、传递状态变更、触发告警时,靠 LLM 读聊天记录来做决策就开始出错了。

Mastra 在 6 月 3-4 日的 release 里同时推出了两种信号模式:Notification Signals 和 State Signals。前者解决 Agent 到 Agent(或 Agent 到人)的异步通知——agent.sendNotificationSignal() 支持 priority(高优先级即时送达,低优先级批量汇总)、due-date dispatch(到时间自动投递),以及一个持久化的 notification_inbox 工具让 Agent 像读邮件一样 list/read/dismiss/archive。后者解决 Agent 内部各组件之间的状态同步——Processor 通过 computeStateSignal() 发布带 cacheKey 的 snapshot/delta,外部 Producer 通过 sendStateSignal() 写入同一 lane,运行时会自动去重。

这里的关键工程取舍是"信号"和"消息"的区别。消息(message)是对话流的一部分,序列化在 LLM 上下文里,一旦压缩或截断就消失。信号(signal)是独立持久化的记录,有自己的生命周期——创建、投递、已读、归档——不依赖 LLM 上下文窗口。这意味着一个 Agent 可以在不占用 context budget 的前提下感知到"CI 挂了""审批通过了""上游数据就绪了"这些事件。

一个容易被忽视的设计点:Mastra 的 working memory 也开始支持通过 State Signals 投递(useStateSignals: true),而不是像往常一样注入 system prompt。这解决了一个实际痛点——working memory 经常被塞进 system message,导致 prompt 缓存频繁失效。改成 signal 后,working memory 的变更不再影响主 prompt 稳定性。

作为 Agent 工程师,在架构设计时需要明确划分:哪些信息应该进入对话上下文(影响模型推理),哪些应该作为信号旁路投递(触发行为但不占 context budget)。把一切都当聊天消息处理,是 Agent 系统在规模上最容易犯的错误。

工具缺失不一定要崩溃

AI

Agent 调用一个不存在的工具时,不一定要直接崩溃。OpenAI Agents Python v0.17.4 给 RunConfig 加了一个 tool_not_found_behavior 选项:默认仍是 raise_error,但开发者可以显式设成 return_error_to_model——模型调用缺失的工具时,框架不再抛异常,而是把 "Tool 'xxx' not found" 作为一个正常的 function_call_output 喂回给模型,让模型自己换条路走。

这个改动的实现很干净:缺失的工具被建模成 ToolRunFunctionNotFound,和正常工具执行走同一条 output pipeline;错误消息可以通过 tool_error_formatter 自定义,甚至可以按 tool_name 返回不同的话术。总共只加了 347 行、改了 10 个文件,就把"工具缺失"从系统故障变成了可恢复信号。

工程上容易踩两个坑。一是如果设成 return_error_to_model 但没设合理的 max_turns,模型可能反复尝试同一个缺失工具形成死循环——框架只是把错误变成输出,并不保证模型会"聪明地放弃"。二是这个行为只处理本地 agent 不知道的工具;如果是远端 tool search 延迟或 MCP 注册竞态导致的暂时性缺失,可能在上层还需要一层重试或降级。

更深一层看,这个问题本质上是:agent runtime 应该把模型行为的越界当作异常抛给调用方,还是当作输入信号还给模型?前者容错靠外部重试/降级,后者容错靠模型自身纠偏——两条路线的选择会影响整整一层 agent 架构的设计。

状态要先定身份

AI

Agent 的可恢复性,第一步不是把状态存下来,而是先给每条可复用状态一个稳定身份。LangGraph 5 月底修的一个细节很典型:当消息没有 id 时,如果异步 checkpoint 线程先把 id=None 序列化了,后面每次 get_state() / resume 都可能重新生成 UUID;同一条消息在 trace、UI、RemoveMessage 里就变成了“不同对象”。

这不是 LangGraph 独有的问题,而是所有 Agent runtime 都会遇到的顺序问题:reducer、serializer、background writer、UI projection 看起来只是内部实现,但一旦并发或异步写入介入,“什么时候补默认值”就会变成协议边界。默认值如果在 reducer 里补,可能已经晚于持久化;如果在恢复时补,就会破坏 replay;更稳的做法是在进入 durable write path 前,把 message/tool-call/task 这类实体的 identity 固化。

工程上可以把它当成一个检查清单:凡是会被 checkpoint、trace、前端增量渲染、人工干预或删除引用到的对象,都不应该依赖“读取时再生成”的 id。OPC 如果要承载长期 Agent 会话,尤其要把 state identity 当成 schema 的一部分,而不是存储层的附带字段。

你现在的 Agent 状态里,哪些对象一旦重放就必须还是“同一个”?这些 id 是在进入持久化前确定的,还是在某个后处理/reducer/UI 层才补上的?

来源:

  • LangGraph 1.2.2 release:stable IDs before DeltaChannel checkpoint writes
  • PR #7913:assign stable IDs to id=None BaseMessages before DeltaChannel checkpoint writes
  • LangGraph checkpoint 4.1.1 / PR #7892:checkpoint serializer revival 收紧,说明恢复路径也在被当作安全与兼容边界处理

动态沙箱要保连续性

AI

一句话结论

当 Agent runtime 从“固定工作目录”升级成“按请求动态分配沙箱”后,真正决定系统是否可用的,不只是隔离性本身,而是你能不能同时守住三件事:提示词稳定、后台任务连续、沙箱生命周期可追责。Mastra 这周把这三件事一起做进了 Workspace API,我觉得这是一个很强的工程信号:多租户 Agent 的难点,已经从“能不能跑起来”转向“隔离和连续性能不能同时成立”。

背景:为什么动态沙箱现在变重要了

过去很多 coding agent / tool agent 默认只有一个静态执行环境:同一个 workspace、同一套进程管理器、同一个文件系统视图。这样做实现简单,但一旦场景变成多租户、多项目、多权限级别,就会立刻碰到两个问题:

  1. 不同用户共用一个执行环境,隔离边界很脆。
  2. 一旦你把沙箱按请求动态分配,长任务、后台进程、流式会话又容易丢连续性。

Mastra @mastra/core@1.41.0 这次的更新很有代表性:Workspace.sandbox 不再只能是静态实例,而可以是 ({ requestContext }) => WorkspaceSandbox 这样的 resolver。也就是说,运行时终于承认“这次请求该进哪个沙箱”本身就是一个动态决策,而不是配置文件里一次性写死的东西。

这件事值得关注,不是因为 API 多了一个函数签名,而是因为它把 Agent runtime 的一个真实矛盾显式化了:你想按用户/线程/租户切沙箱,就必须重新设计 prompt、process continuity 和 ownership。

核心机制:不是“每次请求换个目录”这么简单

1. 沙箱选择进入 request context

Mastra 允许你在 resolver 里根据 requestContext 选择具体 sandbox,比如按 user-id 映射工作目录或权限集。这意味着沙箱不再是 agent 的静态附属物,而是一次执行上下文的一部分。

这背后的工程意义是:

  • 隔离边界开始从“哪个 agent 实例”转到“哪个请求主体”。
  • 运行时终于可以把多租户和权限域放进同一层抽象,而不是靠外部网关拼接。
  • 对 OPC 这类内部 Agent 平台来说,sandbox resolver 实际上就是 capability resolver 的执行环境版本。

也就是说,未来真正稳的系统不是“一人一个 agent 进程”,而是“一个 runtime,根据主体和任务路由到不同能力边界”。

2. Prompt 不能因为动态沙箱而失稳

这里最容易被忽略的一点,是 Mastra 明确写了:构造 workspace instructions 时默认不会调用 sandbox resolver。相反,它只放一个稳定 placeholder;如果你真想把具体沙箱信息写进 prompt,要显式开启 instructions.dynamicSandbox

这很关键,因为很多团队一做动态环境,第一反应就是把“当前目录、可写路径、挂载信息、租户上下文”全塞进 system prompt。这样短期看起来更透明,长期却会同时破坏三样东西:

  • prompt cache 命中率下降;
  • 相同任务无法稳定复现;
  • 沙箱构建副作用被 prompt 生成阶段提前触发。

Mastra 的这个默认值其实在表达一个更成熟的 runtime 观念:执行环境可以是动态的,但给模型看的环境描述应该尽量稳定。 具体环境差异,只有在模型真的需要时再按受控方式注入。

对 Dewey/OPC 的启发很直接:如果以后要做 per-user workspace、临时 repo、隔离工具目录,应该先区分“模型需要知道的能力边界”和“运行时内部真正使用的物理环境”。这两者不是一回事。

3. 后台任务连续性必须显式建模

动态沙箱最麻烦的问题,往往不是首轮调用,而是第二轮。比如模型先 execute_command(background=true) 起了一个长任务,下一轮再来 get_process_outputkill_process。如果这两次请求碰巧被路由到不同 sandbox,那个 PID 在逻辑上就“失踪”了。

Mastra 为此新增了 sandboxCacheKey:让 follow-up 请求按一个稳定 key 重新绑定到同一个 sandbox,而不是依赖某个短命的 RequestContext 对象实例。这其实是在说:

  • 长任务的连续性不能依赖连接对象;
  • 也不能依赖“当前正好还在同一台 worker”;
  • 必须依赖一个 runtime 级别、可重建、可审计的 continuity key。

这个设计和最近很多 Agent runtime 信号是一致的:连接会断、进程会迁移、流式响应会恢复,但后台任务必须有 stable identity。

为什么 untilIdle 也值得一起看

同一个版本里,Mastra 还把 streamUntilIdle() 折叠进普通 stream() / resumeStream(),变成一个统一的 untilIdle 选项。表面上这是 API 收敛,实际上它透露了另一个判断:“等待后台任务继续产出”不该被建模成另一套独立接口,而应被视为标准流式执行的一个模式。

这和动态沙箱放在一起看,就更有意思了:

  • sandboxCacheKey 解决“我还能不能回到原来的执行环境”;
  • untilIdle 解决“流要不要为后台任务继续保持可恢复语义”。

两者组合后,runtime 对长任务的抽象才开始完整:同一条任务线不只是能启动,还要能继续观察、继续恢复、继续终止。

常见误区

误区一:动态沙箱只是安全隔离问题

不是。它同时是权限问题、调度问题、状态归属问题、可观测性问题。只做目录隔离,不做 continuity key、ownership 和 trace 绑定,最后会变成“隔离是有了,但任务无法恢复”。

误区二:把真实环境细节都写进 prompt 更安全

这通常更脆。模型知道得越多,不代表控制越强;反而容易把高基数、易漂移、带副作用的信息变成 prompt 噪声。更稳的做法是:prompt 只暴露稳定能力边界,具体物理环境在工具执行层处理。

误区三:后台任务状态跟着 session 走就够了

也不够。session 很可能只是交互容器,不是执行环境标识。真正可靠的连续性通常要绑定 thread、task、tenant 或 principal,而不是某次 HTTP 请求对象、某条 websocket 连接,甚至不是某个瞬时 session id。

如果 OPC 要落地,可以怎么做

我会把“动态沙箱连续性”拆成一个最小运行时合同:

  1. Resolver 层:输入 principal / project / thread / task type,产出 sandbox、工具白名单和权限域。
  2. Prompt 层:默认只注入稳定能力描述,不直接注入高波动的物理路径和挂载细节。
  3. Continuity 层:所有后台任务都拿到 continuity_key,后续读日志、取输出、终止任务都必须带它。
  4. Ownership 层:记录谁创建了这个 sandbox、谁能复用、何时销毁、失败后谁负责清理。
  5. Tracing 层:把 sandbox id / continuity key / tool name / thread id 绑定进 trace,而不是只留“agent step failed”。

如果这五层没分开,系统早晚会在两种失败里选一个:

  • 要么为了连续性而偷共享环境,最后租户串线;
  • 要么为了隔离而每次都新建环境,最后长任务不可恢复。

我对这条信号的核心判断

Mastra 这次更新的价值,不在于“又支持了一种 sandbox 配置方式”,而在于它把下一代 Agent runtime 的三个基本约束摆到台面上了:

  • 隔离必须按请求主体生效;
  • prompt 不能因此失去稳定性;
  • 长任务连续性必须脱离连接对象,进入显式 runtime contract。

这比单纯讨论“本地跑还是远程跑”“一个 agent 一个容器还是一个线程一个容器”更接近真实工程问题。因为团队一旦进入多租户、多人协作、长任务和后台工具并存的阶段,真正难的从来不是启动一个沙箱,而是让它在安全、缓存、可恢复之间不互相打架。

留给 Dewey 的问题

如果 OPC 未来要支持“同一个 Agent runtime 服务多个项目成员,并允许长任务跨轮次继续执行”,你最想先固定下来的 continuity key 是什么:thread、task、principal,还是 project+principal 的组合?为什么?

工具调用要可观测命名

AI

Agent 的工具调用不要只记“调用成功/失败”,而要把每一次工具执行命名成可聚合、低基数、可追责的 span。OpenTelemetry Semantic Conventions v1.41.0 在 GenAI breaking change 里把 gen_ai.tool.name 从推荐提高到必填,并要求工具执行 span 名称形如 execute_tool {gen_ai.tool.name};这不是埋点格式小改,而是在说:Agent 的执行轨迹必须能按“哪个工具”稳定切片。

为什么这对 Agent Engineer 很关键?工具调用通常发生在应用代码里,而不是模型服务里:MCP server、内部 API、检索、代码执行、支付/权限检查,都可能由不同框架接管。如果 trace 里只有一次 generic tool_call,出了问题只能看日志;如果 span name 和属性里有稳定的 tool name、call id、error type,就能回答更工程化的问题:哪个工具拖慢了整体任务?哪个工具的失败导致模型反复重试?同一个工具在不同 agent policy 下成本是否不同?

这里的取舍是:工具名应该是低基数的能力名,比如 search_docsrun_sqlcreate_ticket,不要把用户 query、URL、订单号塞进 span name。参数和结果即使要记录,也应像规范里写的那样保持 opt-in,因为它们常常有敏感信息、体积不可控、基数爆炸。也就是说,可观测性不是“多记一点”,而是把身份字段、诊断字段、敏感载荷分层。

如果 OPC 之后要建设自己的 Agent runtime / MCP 工具层,一个很实用的起点是:先规定每个 tool 的 canonical name、版本、权限域和错误码,再让 runtime 自动为每次工具执行打 span;否则等工具数量上来,再补可观测性,就会发现所有失败都叫“agent step failed”。

你现在负责的 Agent 系统里,有没有一个地方能列出“所有可被模型调用的工具名”,并把线上失败率、耗时、重试次数按这个名字聚合出来?

流式输出要分投影

AI

Agent 产品的流式输出不要再把后端事件日志直接甩给前端,而要先投影成“消息、工具调用、状态变化”这几类稳定视图。最近 LangGraph SDK 0.4.x / LangGraph 1.2.3 把 v3 streaming、SSE/WebSocket transport、messages 和 tool call projections、interleave projections 连续补上;AG-UI 也在 5 月底到 6 月初持续发布 LangGraph 适配包。这说明前端 Agent 体验正在从“显示 token”走向“消费可组合的交互事件”。

机制上,原始 stream 是执行轨迹:节点开始、模型增量、工具参数片段、工具结果、取消、重连、子图事件……这些对调试有用,但不适合作为产品 API。产品真正需要的是投影:当前用户能看到哪条 assistant message、哪个 tool call 正在等待/执行/完成、哪个子 agent 在说话、哪些状态可以被恢复。投影层一旦稳定,后端可以从 SSE 换到 WebSocket,可以把单 agent 换成 RemoteGraph 或子 agent,前端仍然只处理同一套语义事件。

对 OPC 这类系统尤其关键:如果把“订单查询 agent 调了库存工具”直接编码成 UI 特例,后面一加审批、人审、回滚或多 agent 协作,界面就会被执行细节绑死。更好的做法是把流式协议当成产品边界:message projection 负责可读叙事,tool-call projection 负责可观测和权限提示,state projection 负责恢复与审计;raw trace 只进日志和回放。

常见误区是以为流式只是降低首 token 延迟。真正的工程价值是让长任务可解释、可中断、可恢复、可迁移。你现在的 Agent UI 如果明天换掉编排框架,前端消费的是“语义投影”,还是某个框架泄漏出来的事件名?

来源:

  • LangGraph SDK 0.4.0 release:新增 thread stream helpers、WebSocket stream transports、stream reconnect、messages/tool calls 等 stream primitives。
  • LangGraph 1.2.3 release:RemoteGraph v3 streaming、tool-dispatched subagent naming、interleave projections。
  • AG-UI release 2026-06-02:发布 @ag-ui/langgraph 0.0.36、ag-ui-protocol 0.1.19;README 将 AG-UI 定义为连接 agent 与用户应用的轻量事件协议。

能力按需显露

AI

Agent 的工具集开始从“启动时一次性塞满上下文”,转向“运行中按需显露能力”。Pydantic AI 在 2026-06-02 的 v1.105.0 / v2.0.0b5 里合入 on-demand capabilities:能力有稳定 id,初始只给模型一个紧凑目录,模型需要时再调用框架管理的 load_capability,把对应 instructions、tools、model settings、hooks 暴露出来。

这不是单纯省 token。对 Agent Engineer 更重要的是,它把“能力发现”变成了可观测、可回放的状态转换:哪些能力一开始可见、哪个 turn 加载了哪个 capability、恢复会话时 loaded state 如何从 message history 还原,都可以进入 tracing 和评测。OPC 这类多流程、多工具平台如果继续把全部工具描述塞进系统提示词,会同时伤害 prompt cache、模型路由和权限边界。

工程上可以把每个业务域建成 capability:订单、合同、日程、知识库、代码执行、外部发信等。默认只暴露 id、description 和加载入口;加载后才暴露细工具。OpenAI Responses / Anthropic 这类有原生 tool search 的路径还可以保持 provider-visible function-tool array 更稳定,让 prompt-cache 前缀更容易复用;没有原生支持时退回本地 search_tools,仍能减上下文,但缓存稳定性会差一些。

常见误区是把它理解成“动态插件”。真正的设计重点是能力边界:id 必须稳定,description 要能让模型判断何时加载,native tools 的 deferral 可能破坏缓存前缀,敏感 capability 还要接审批、审计和租户权限,而不是只靠模型少看见几个工具。

如果你今天要为 OPC 设计一个 capability catalog,哪些能力应该默认可见,哪些必须按需加载,哪些加载动作本身需要留下审计事件?

Sources