背景:十个运行时,十套胶水代码
2026 年的编码智能体市场有一个不太被提起的成本项:不是模型账单,而是接入成本。一家想做内部智能体工作台的团队,通常要同时支持三到四个运行时——工程部门用 Claude Code,平台部门用 Codex,某些业务线用 Cursor,还有人用 Kimi Code 或 Qwen Code。这些运行时的启动方式、登录方式、配额查询方式、事件格式、恢复机制、MCP 挂载语法各不相同,于是每个宿主应用都要为每个运行时写一套胶水代码,并在运行时升级后重写一遍。
更麻烦的是语义差异。有的运行时允许在回合进行中插入指令(steer),有的只能排队;有的原生支持撤回尚未开始的输入(withdraw),有的完全没有这个概念;有的能查询账户配额,有的只能靠解析本地配置文件猜。多数宿主的处理方式是「先发出去,看结果」,于是这些差异变成了运行时的隐性行为,直到某个客户环境里出问题才被发现。
OAR 的切入点不是再做一个运行时,而是在运行时与宿主之间加一层统一接口,并把「差异」变成显式的、可枚举的声明。它对自己的定位写得很克制:一个面向编码智能体运行时的、与厂商无关的编程接口,是构建智能体工作台与其他应用的地基。仓库的自述口号是「删掉驾驭层逻辑,专注结果与体验」。
OAR 的形态:接口、CLI 与四层文档
从仓库结构看,OAR 由四块组成:库本体(@botiverse/oar)、命令行工具(@botiverse/oar-cli,可用 npx 直接跑)、一套规范文档(docs/spec)与一套设计文档(docs/design),另有按运行时分页的映射说明(docs/runtimes)与一份与既有项目的逐特性对照(docs/prior-arts)。
规范文档的首页顶部写着「Status: SHIPPED」,并声明下面写的不是设计意图,而是当前 @botiverse/oar 实际会发出的东西,规范性的 TypeScript 定义在 packages/oar/src/contracts/session.ts 与 records.ts。这个措辞值得留意:它把文档当成契约而不是说明,且明确要求「代码改了契约,规范页在同一提交里改,绝不留到以后」。对一个刚开源的库来说,这种自我约束是它想被当成基础设施用的信号。
技术形态上有三条硬约束:仅支持 ESM;Node 入口要求 Node.js 24 及以上;浏览器入口需要支持 Web Crypto 的现代浏览器。许可证为 Apache-2.0。
规范文档还给出了一套证据标记,用来说明每一条事实是从哪来的:厂商源码(标注到具体提交)、二进制符号、观察到的运行时行为、官方文档、以及一份锁定的 ACP 规范克隆。这类标记在开源项目里不常见,但它恰好回应了这层抽象最容易被质疑的地方——你怎么知道某个运行时真的会这么做。
一条有序可续读的记录流:三类记录与一个游标
OAR 的全部设计压在一句话上:契约是一条有序、可续读的记录流。运行时产生的每条原始记录(RawEvent)按「义务」分成三类——frame(运行时自己说的话)、request(期待结果的动作)、response(始终指向某条 request)。每条记录自带归属信息(会话图与 agentPath),整条流上有一个单调递增的 seq 作为游标。
对外的承诺被写成四条:运行时说过的东西全在流里;OAR 没有观察到的东西不在流里;每条记录都知道自己属于谁;流可以从任意位置续读。这四条里第三条最容易被低估。多智能体场景下的经典问题是「这条输出是谁说的」——主智能体的、子智能体的、还是运行时自己的系统消息。OAR 把这个问题的答案放进记录本身,而不是靠调用方去维护多条通道再对账。
规范还定义了四种记录标记,并用「符号加文字」而不是颜色来表达:勾号表示 frame(运行时原话)、实心菱形表示 request(期待结果的动作)、空心菱形表示 response(总是指向一条 request)、圆圈表示「从未被观察到的结果」(诚实的空缺)。最后一项尤其重要:它把「没看到」与「结果是空」区分开,这是很多可观测性实现里长期含糊的地方。
| 记录种类 | 含义 | 典型例子 | 消费者看到的形式 |
|---|---|---|---|
| frame | 运行时自己说的话,逐字记录 | 文本增量、工具调用开始与结束、用量、压缩、重试 | events() 里的 Event,同时保留原始载荷 |
| request | 一个期待结果的动作 | prompt、steer、queue、withdraw、abort、dispose | 被回答为 accepted 或 rejected |
| response | 始终指向某条 request | 控制被驳回并附带类型码、运行时向应用发起的请求与自动应答 | ControlOutcome 与 app_request / app_answered |
| absence | 从未被观察到的结果,诚实空缺 | 运行时没有上报退出码、没有给出完成事件 | 不合成、不猜测,留空 |
值得注意的是:OAR 明确不做多传输通道、不做多控制面与并发提示排队、不提供存储层、不做用量推导。这四条「非目标」每一条都对应一个具体的设计决策文档,后文会展开它们对采用者的含义。
归属是字段不是通道:agentPath 与 seq
在 OAR 里,每个事件都带 agentPath:空数组表示根智能体,子智能体有自己的路径。README 给的示例用了一个很小的技巧来展示这件事——把路径用大于号拼起来作为前缀,于是控制台上会直接显示「parent > child > grandchild」这样的层级。这个细节说明归属是被当作一等字段设计的,而不是事后补的标签。
为什么把归属做成字段而不是通道?如果每个子智能体开一条独立的事件通道,消费者就要自己做多路归并、排序与回溯,而且一旦某个子智能体在另一个进程或另一台机器上启动,通道归并就会变成分布式问题。做成字段之后,流仍然是单条,归属只是每条记录上的一个属性,归并成本转嫁给了生产者而不是消费者。
seq 的作用则是让「续读」有精确定义。rawEvents(observer, sessionId, afterSeq) 与 events(observer, cursor) 都会从指定位置之后重放保留的记录并继续接收实时事件,规范承诺既不丢也不重。游标的有效期被限定在适配器进程的生命期内——这是一处明确的边界,跨进程恢复要靠 SessionOptions.resume 重新打开运行时原生会话,此时新流从 seq 0 开始。
会话图本身只记录「真会话」。外部压缩(compaction)被当作一个新会话处理:新会话的头一条提示携带摘要输入,新旧会话之间不建立会话图边,也不做任何链接。这个处理相当克制——它没有为了图好看而编造一条因果边。后文会提到,「因果链接」正是这套规范里两处主动留白之一。
控制给出诚实答案:接受、驳回与类型码
OAR 的会话控制有五个动作:prompt(发起)、steer(在回合进行中插入)、queue(排队)、withdraw(撤回尚未落地的输入)、abort(中止)。关键不在动词,而在返回值:每个动作都会被回答为接受或驳回,驳回时带一个类型码与一段说明文字,类型码包括 busy、no_active_turn、runtime_exited 等。
更细的一点是:输入到底落在哪里,是从事件里读出来的,不是猜的。规范写道,一条提示请求会「暂时性地」开启一个回合,而原生的排队或开始证据才能确立这个输入真正开始的时刻;结束则以运行时自己的完成事件为准。也就是说,OAR 不承诺「发出去就生效」,它承诺「生效与否会在流里写出来」。
查询也被设计成折叠(fold)而不是状态字段:model、effort、serviceTier、usage、contextUsage、status 都是对 records() 的投影,返回 value 与 seq 两个字段,其中 seq 是这次折叠消费到的最后一条记录(一条记录都没有时为 -1)。这个设计让「这个数字是基于哪些事实算出来的」变成可回答的问题,对一个要做计费或对账的宿主来说含金量不低。
README 里那个示例也顺带展示了控制与查询的组合:先发起一个带十分钟超时的提示,同时从 UI 侧插入一句「别动快照」,最后打印状态、用量与上下文占用,释放会话;之后哪怕换一个进程,也能凭 session.id 把同一段对话接上。
拒绝发生在打开之前
OAR 反复强调的一条原则是:运行时兑现不了的选项,在会话打开之前就被拒绝,绝不静默丢弃。对应两个机制:Runtime.refusedSessionOptions 提前声明该运行时会拒掉哪些会话选项;session() 见到这些选项时抛出 UnsupportedOptionError 而不是忽略。若运行时当下拒绝恢复某段会话(比如对话已归档或在别处打开),则抛出 SessionUnavailableError。
这条原则的工程意义在于把「隐性差异」变成「可枚举的错误」。常规做法下,宿主给三个运行时都传了 effort=high,其中两个生效、一个被忽略,宿主无从判断哪个生效;OAR 的做法是后者在打开阶段就报错,宿主必须显式降级或换运行时。代价是宿主代码里要多写分支,收益是行为可预测。
类似的思路也用在其他能力上:运行时整体做不到的操作表现为「缺这个成员」(比如没有 Session.steer、没有 Session.withdraw、没有 Runtime.accountUsage);运行时如何接收运行中的 MCP 工具列表变更,由 Runtime.mcpToolsListChanged 声明;SessionCapabilities 则声明排队持久性、归属分层与图像输入支持。宿主因此可以只依据声明来挑选运行时,而不必维护一份「哪些运行时支持什么」的手工清单。
还有一处安全相关的细节:会话选项里的 mcpServers 可以为单次会话挂载 stdio 或 streamable HTTP 的 MCP 服务器,但这些服务器配置里的 env 与 headers 值不会进入任何记录、事件或错误信息。对要落审计日志的团队,这一条比听起来重要——它避免了凭据随事件流泄漏到日志系统。
跨运行时子智能体:把活派给别的厂商
OAR 提供的子智能体能力有一个与主流做法不同的地方:派出去的子任务可以跑在另一个运行时上。示例里创建了一个 maxRunning 为 4 的 crew,然后分别派一个 codex 子智能体去审竞态、派一个 pi 子智能体去写变更日志,每份报告回来时唤醒正在等待的父会话。
这件事之所以值得单独说,是因为它把「编排」与「执行」彻底拆开了。过去的多智能体框架通常要求所有智能体跑在同一个框架里,跨框架协作只能在进程外靠文件或消息队列拼。OAR 的做法是:只要两边都被同一个宿主打开,子任务就可以跨运行时流转,报告通过 formatReport 与 reportOrigin 保留来源信息后投递回父会话。
这一层也解释了为什么归属要设计成字段。跨运行时的子智能体天然带来「这条输出来自哪个运行时、哪个子任务」的问题,而 agentPath 与报告来源两个机制共同给出了答案。
宿主自己的工具也能以 MCP 服务器的形式挂到任何运行时上(serveMcpTools),并且可以在会话运行中替换工具列表,前提是 Runtime.mcpToolsListChanged 声明该运行中的会话能看到变更。这让「宿主提供受控工具集」成为一个可移植的能力,而不必为每个运行时各写一遍。
不开会话也能用的一整套运维查询
OAR 里有一组不需要打开会话就能用的能力,覆盖的是运维侧的高频问题:装没装、登没登录、哪个账号、还剩多少配额、能跑哪些模型、有没有更新、装了哪些技能与工具。README 给出的示例是遍历所有内置运行时,对已安装的并发查询用量、模型与更新。
其中几处的措辞能看出作者对边界的在意:installation() 只在本机查找并报告版本,不做账号或网络调用;当它是通过 PATH 找到的,还会列出排在它后面、因此永远不会被执行的那几份副本(shadowed);installPlan() 只说明「安装会执行什么」(厂商自己文档里的安装器,附来源页面)或「为什么没有安装器可跑」,install() 才真正执行,并以安装后的 installation() 结果判定成败;目的地未知时返回 location_unknown 并附上厂商命令与来源。login() 走运行时光自己的登录流程,转达登录 URL 或设备码,人粘贴回来的码只交给运行时,规范明确写「OAR 从不接触 token」。listModels() 在未登录时会给出「未登录」这个答案,而不是返回一个空列表。
这组能力的价值在于:一个仪表盘、一个装机向导或一个配额监控,不必为每个运行时写一份代码。对有多种运行时并存的组织,这类「无聊但必需」的接口往往比智能体本身更早成为刚需。
CLI 暴露了同样一组查询:oar installation、oar login(加 --status 只做报告)、oar logout、oar usage、oar models、oar upgrade --check,以及 oar skills、oar commands、oar mcps、oar tools;另有 oar run claude "..." --record run.jsonl 这样的一行式用法。
十一个图标与九个适配器:口径差在哪里
把仓库的两处清单并排读,会发现一个需要说明的口径差。README 顶部的运行时表格列出了十一个:Antigravity、Claude Code、Codex、CodeBuddy Code、Cursor、Grok Build、Kimi Code、OpenCode、Pi、Qwen Code、Qoder,另有 Pi Durable 走宿主持有的 Harness(含浏览器入口)。而规范文档在描述「契约覆盖范围」时写的是「由每个适配器实现(claude、codex、pi、grok、kimi、opencode、cursor、antigravity、qwen),并由共享行为套件钉住」——这份清单是九个,不含 CodeBuddy Code 与 Qoder。
这不是矛盾,而是两层口径:README 说的是「能驱动哪些运行时」,规范说的是「哪些运行时通过了共享行为套件的钉扎」。对采用者来说,后者才是「行为一致」的保证范围。共享行为套件位于 sea-trial/cases/session.ts,CI 在 mock 后端以及 claude、codex、pi、pi-durable、qwen 的 aimock 后端上运行它,并且可以用 OAR_TEST=运行时 id 在任何已安装的运行时上跑;仓库里还有一个 experiments/live-contract.ts,用真实登录去核对同一份契约,每个运行时页面会写明哪些实况检查已验证。
| 口径 | 数量 | 成员 | 含义 |
|---|---|---|---|
| README 运行时表格 | 11 | Antigravity、Claude Code、Codex、CodeBuddy Code、Cursor、Grok Build、Kimi Code、OpenCode、Pi、Qwen Code、Qoder | 能被同一套接口驱动 |
| 规范声明的适配器 | 9 | claude、codex、pi、grok、kimi、opencode、cursor、antigravity、qwen | 由共享行为套件钉住契约 |
| CI 上的自动化后端 | 6 | mock、claude、codex、pi、pi-durable、qwen(后两者为 aimock) | 每次提交自动核验 |
| 另有独立入口 | 1 | Pi Durable(宿主持有 Harness,含浏览器入口与可移植内核) | 需宿主已打开的 Harness 与 Models |
还有两处需要宿主自己接的部分:Cursor 需要宿主安装并把 SDK 交进来(文档解释了原因),Pi Durable 需要传入宿主已经打开的 Harness 与 Models。也就是说,这十一个入口里有两个不是开箱即用。
DeepSeek Harness 那处引用说明了什么
OAR 在 10 月 29 日进入公众视野时,同时出现的一条线索是:DeepSeek Harness 仓库的实验目录里有一份 packages/experimental/subagent-oar/README,用它来实现子智能体。这个位置值得留意——它在 experimental 目录下,说明是探索性用法,不等于官方集成承诺。
但它传递的信号比集成本身更有意思:一个厂商自己的 harness 在实验性地用一层中立接口去接别的运行时做子智能体,说明「编排层不属于任何单一运行时」这个判断正在被实践检验。若这条路走得通,未来宿主应用只需要接 OAR 一层,就能同时驱动多家的运行时;若走不通,各家仍会回到自建编排。
需要说明的是,这条线索来自公开的代码与社交平台讨论,本文未对其做运行时验证;集成的成熟度、是否在正式版本启用、以及具体的行为边界,目前官方及行业暂未披露更多细节,后续将持续跟进迭代动态。
OAR 卖的不是「一次接入到处运行」,而是「运行时说过的每句话都能自证归属、做不到的每件事都在打开前报错」;真正难复制的部分是跨运行时子智能体。
本文推算:五条原文没有直说的判断
其之一:被契约钉住的比例约为十一个里的九个。README 的运行时图标是十一个,规范声明由共享行为套件钉住的是九个,两者相差 CodeBuddy Code 与 Qoder 两个。按此折算,通过一致性套件钉扎的覆盖率约为 81.8%,其余约 18.2% 属于「可驱动但未钉住」。对宿主来说这意味着:在这两个运行时上,接口能跑,但行为一致性没有自动化证据支撑,需要自建回归测试。
其之二:自动化一致性后端覆盖约为三分之二。CI 上跑共享行为套件的后端是六个(mock 与 claude、codex、pi、pi-durable、qwen,后两者为 aimock),规范声明的适配器是九个,折算覆盖率约为 66.7%。也就是说,约三分之一的适配器依赖 OAR_TEST 手动跑或实况实验来验证。这不是缺陷,而是新项目的常态,但它决定了「升级之后有没有回归」这个问题的答案强度在不同运行时上并不相同。
其之三:「打开前拒绝」把运行时差异从隐性变成可枚举。设一个宿主同时用三个运行时,每个运行时平均有 k 个兑现不了的会话选项。在静默丢弃的语义下,宿主无从知道某次调用里有哪些选项没生效,问题只会在客户环境里以「行为不一致」的形式暴露;在 OAR 的语义下,宿主必须在打开阶段处理 3k 个分支。代价是确定的开发量,收益是差异不再泄漏到运行期。对要交付给外部客户的宿主,这笔交换通常是划算的。
其之四:跨运行时子智能体是这套设计里最难复制的部分。把归属做成字段、把记录流做成单一事实来源,这些是设计选择,别人也能做;但「派给 codex 一个子任务、派给 pi 另一个子任务、两份报告回来唤醒父会话」这件事,需要有人真的把九到十一个运行时的原生协议都摸清并映射过来。这层工作量既是护城河,也是风险:任何一个上游运行时改协议,对应的适配器就要跟着改。
其之五:事件种类的密度决定了 UI 能做多细。README 在描述 events() 时列举了原生事实约十七类——原生用户消息回显、文本、推理、带后续输入的工具调用、进度、结束、运行时上报的退出码、后台任务、回合、用量、模型、推理强度、压缩、重试、应用请求、控制驳回、进程退出;规范另有七类由请求与响应记录读出的派生事件(回合开始、输入被撤回、控制被驳回、应用请求、应用应答、释放、退出)。两者合计约二十四类。这个密度意味着宿主不必为了「显示子智能体正在做什么」而额外打点,但也意味着事件解析器本身有维护成本。
三处非目标与两处未决
规范明确列出四项非目标,其中三项对采用者影响直接:其一,不提供存储层——记录流的持久化、检索与归档由宿主负责;其二,不做用量推导——累计、分周期、按边界切分的视图属于消费者的业务,OAR 只给出原始用量与消费到哪条记录的 seq;其三,不做多控制面与并发提示排队——一个会话同一时间只走一条控制路径。
另有两处作者主动留白。其一是记录之间的因果链接:规范说迄今没有消费者场景需要一条 causedBy 式的指针,是否添加保持开放。其二是「归属之外的能力声明」:整体做不到某个操作用「缺成员」表达,某个选项做不到用拒绝表达,MCP 工具列表变更用声明表达,但还有哪些事实值得被声明(比如远端执行环境)尚未定论。
尚未披露的部分还包括:九个适配器各自的归属分层(attribution tier)具体落点、Cursor 与 Pi Durable 之外还有哪些运行时需要宿主自带依赖、每个运行时页面的实况检查覆盖到什么程度、共享行为套件的用例数量与断言清单、Pi Durable 在浏览器环境下的资源与权限边界,以及是否有稳定的长期维护安排与版本路线图。目前官方及行业暂未披露更多细节,后续将持续跟进迭代动态。
采用前六问
其一,你真正需要的是「一层接口」还是「一个运行时」?如果你的团队只用一个运行时,OAR 带来的主要是事件规范化与运维查询,价值有限;如果在三个以上运行时之间切换或并存,收益才明显。
其二,你的宿主是否已经有自己的事件模型?如果有,要评估的是「把现有模型映射到记录流」的成本,而不是「要不要用 OAR」。
其三,持久化方案准备好了吗?OAR 不做存储层,会话的跨进程恢复依赖运行时原生会话与 session id,自己的记录流归档要自建。
其四,计费归属怎么算?OAR 给出原始用量与 seq,但按项目、按人、按任务分摊仍是宿主的事。只有 OpenCode 等少数运行时的成本是精确账单,其余多为按自家 token 计数估算,这一点在 kw-bench 等基准里也有体现。
其五,你能接受「打开就报错」吗?refusedSessionOptions 的语义要求宿主为每个运行时准备降级路径,这会增加代码量,也会让某些「默认能跑」的场景变成显式失败。
其六,子智能体要不要跨运行时?如果答案是肯定的,OAR 是目前少见的现成选项;如果答案是否定的,那么它的这部分能力对你来说是超配。
冷静判断:它改变了什么,没改变什么
OAR 改变的是「接入成本」这一项的结构。它把一个随运行时数量线性增长、且每次上游升级都要重写的成本,压缩成「维护适配器」这一件事,并把适配器之外的部分固定下来。对做智能体工作台、内部开发门户、多运行时编排的团队,这个交换是清晰的。
它没有改变的是运行时的能力本身。OAR 不提升任何一个模型的表现,也不让某个运行时具备它本来没有的特性——它的哲学恰恰相反:做不到的就说做不到,并在打开之前说出来。这意味着采用 OAR 之后,你的系统在「能力上限」上不会有任何提升,但在「行为可预测性」上会有明显改善。
还有一处需要克制看待:这是一个刚在 10 月 29 日进入公众视野的项目,规范文档写着 SHIPPED,但九个适配器的一致性覆盖约为三分之二,且两处设计问题仍主动留白。把它用在生产关键路径上之前,值得先在你自己的两三个运行时上用 OAR_TEST 跑一遍共享行为套件,看契约在你关心的那几个运行时上是否真的成立。综合来看,OAR 属于那种「把惯例固化成契约」的基础设施,价值集中在证据完整性与差异显式化两处;对已经在多运行时胶水代码上投入较多的团队,它值得作为对照对象认真评估;对只用单一运行时、尚未遇到接入痛点的团队,它解决的是后半段的问题。