omp作者:AI编程工具外壳是系统软件,不是while循环
原文:《The Harness Playbook — Stencil》|作者:Can Bölük(@_can1357),omp(Oh My Pi)作者,Stencil Labs 创始人|项目:github.com/can1357/oh-my-pi(MIT 协议,26k+ stars,上游为 Mario Zechner 的 pi-mono)
导语
2026 年,AI Coding Agent 的“外壳”——Harness——正在经历一场集体重构。OpenCode、Pi、OpenClaw、omp 不约而同地推倒重来。为什么?
omp 作者 Can Bölük 发布的《Harness Playbook》给出了迄今最系统的回答。这篇长文既是对 omp/Pi 这一代 Harness 的复盘(postmortem),也是其重写版 omp² 的架构蓝图:9 章正文 + 2 个附录,覆盖状态、运行时、控制面、推理层、工具面、界面与技术栈。
它的核心命题只有一句话:
Agent Harness 是系统软件,不是“一个 while 循环套一个 fetch”。
核心命题:复杂性必须有人拥有
文章开篇援引两位先哲,确立全文基调:
Dijkstra 的“简单是可靠的前提”被业界误用为“简单好、复杂坏”的借口。其本意是帮助实现者推理,而非免除实现者推理的责任。
Ousterhout 补上另一半:模块作者应当“拥抱痛苦”(embrace suffering)——把复杂性压进模块内部,让少数实现者承担,而不是让每个调用者各自背负一份略有差异的副本。
作者继而给出一个精妙的类比:Harness 的职责清单与游戏引擎几乎完全重合——维护权威世界状态、记录变更日志、运行不可信动作、向多视图复制状态、调度 actor、解释命令、适配不兼容协议、渲染实时界面。而游戏引擎已经拥有这些复杂性数十年的成熟解法。全文因此大量借用 Source 引擎(Valve)的设计作为参照系。
这一方法论贯穿全文:从真实失败证据出发,找到该拥有复杂性的那一层,让错误在表达层面不可能发生。
设计包线:四个“架构测试”
在设计任何子系统之前,作者要求先想象四种产品同时依赖这个 Harness:
这些不是用户画像,而是架构压力测试。只为第一种场景设计的系统,会把控制器偷运进 TUI、把状态藏进闭包、让扩展跑在引擎进程里。能同时活过四种场景的设计,被迫产生五条架构推论,构成全文的结缔组织:
唯一权威会话——rewind、fork、resume、复制、检视全部派生自同一份日志化状态;
可信控制面——策略留在宿主,沙箱只接收有界的执行请求;
有界工作——一切工具调用、子 Agent、后台任务都是可取消的流,有统一限额与可观测性;
显式兼容性——模型与厂商的怪癖是结构化知识,而非散落在调用点的分支;
视图即投影——TUI、Web、远程客户端渲染同一份状态,而不是各自成为新的权威。
逐章解读
3.1 状态:唯一权威,否则 rewind 就是谎言
问题。Pi/omp 的日志只覆盖消息树,而权威状态(todo、重试计数、子 Agent 注册表、流式标志等)活在日志之外——存在两个真相来源,rewind/fork/resume 全部“说谎”。这违反了事件溯源的第一原则:状态必须能仅从事件推导。
证据。官方 78 个扩展示例中,60 个无状态;17 个有状态的里只有 2 个是正确的。失败模式包括:闭包计数器在 rewind 后错乱、死分支的存档复活、checkpoint 在 fork 前被清空、切换会话误提交工作区等。作者的结论很硬:文档修不好这种 bug 分布。Source 引擎的正确性不来自开发者自觉,而来自“不可重放的状态在表达层面不存在”这一约束。
omp² 方案。整个会话物化为一棵 DOM 树(XML 表示),日志是属性变更的 patch 流。运行时对象可以缓存或索引它,但不构成第二个真相所在地。由此,一串难题被归约为同一操作:
Rewind = DOM diff:消失的 <Bash> 元素即终止,出现的即恢复,diff 本身就是完整的生命周期工作清单;
系统提示词 = 投影:直接查询同一棵树,不再有 100 行的状态对象传入模板;
远程复制 = 订阅:远程客户端消费 patch 流,无需独立的状态管道;
渲染 = 投影:组件注册表从同一份元素状态渲染任何工具;
控制器与 actor 彻底分离:检视子 Agent 只是把同一个 actor 指向子状态。
增加一个有状态的功能,永远不需要给 rewind、fork、resume 或复制增加新的调用点。
3.2 运行时:沙箱只执行,不决策
作者用一段归谬推演说明:把执行器放进 VM,会导致工具被迫分裂、需要双向网关;把驱动应用放进 VM,又会泄露提示词与源码。唯一干净的边界是——宿主拥有状态、推理、策略、审批、限额与日志;沙箱里只放一个“愚忠的 stub”,且所有回流数据流必须有界(防止一次误用的 Read 返回 2GB 撑爆宿主)。
其余关键决策:
子 Agent 走同一边界:用写时复制文件系统视图(APFS/btrfs/ZFS/overlayfs)隔离,子 Agent 拿到视图、返回 diff,不共享父级的可变权威;
执行即状态流:旧契约 renderCall / execute / renderResult 把一次操作劈成三个互不相识的阶段,导致重复 IO、重复计算、结果反序列化。omp² 中一次调用是一个带结构化子元素(input/result/diag/usage)的 DOM 元素,执行器边跑边改它,模型、用户、日志、远程客户端看的是同一状态的不同投影;
限额是原语的一部分:输出截断默认开启、显式 notrunc 才能关闭;阻塞时长与后台化统一收敛到一个 stdio 形状的作业原语(signal + stdin + stdout + 退出码)——后台 shell、子 Agent、守护进程、远程函数全是同一个对象;
取消需要“击杀边界”:AbortSignal 这类协作式取消靠不住,必须有进程/worker 级别的可强制终止单元,其死亡不能带走会话权威;
扩展用 Python:AST 自省能力让 @remote 装饰器把本地样子的函数变成 RPC,抹平“两个文件系统”的痛苦;内嵌运行时同时让 Eval 工具开箱可靠。
3.3 控制面:Convar 管值,Director 管行为
值 → Convar。直接移植 Source 引擎的 convar 体系:每个设置在声明处一次性写清类型、默认值、帮助文本与标志位(REPLICATED / ARCHIVE / SESSION……),持久化、作用域、复制、脏跟踪全部内生于声明,不再有 god object 转手 setter。子 Agent 默认继承父会话的全部值;想钉死就写一行 subagent.cfg。bind、toggle、alias 也是控制台命令,键位绑定不再需要专门 schema。cfg 文件、控制台输入、远程管理、日志回放说同一种语言——定制化不再繁殖一次性 schema。
行为 → Director 栈。Plan 模式、Goal 模式、/force、todo 提醒都想“拥有循环”,各自实现必然互撞(作者实测:两个最流行的同类插件无法共存;私有 mutex 只能在同一作者的插件群内奏效)。omp² 的答案是:候选 yield 沿 Director 栈流动,每个 Director 可:
Pass——交给下一个 Director;
Continue——消费 yield,再跑一轮;
Yield——真正交还给用户;
Push——在自己顶上压入子 Director;
Done——弹出自身,把同一候选 yield 交还父级;
Fail——带错误弹出。
栈本身是会话 DOM 的子树——于是 rewind 自动移除 Director,resume 自动恢复,远程检视器能看到当前谁拥有 yield。Plan 模式被完整重写为该原语上的一次普通组合,而非特例。
3.4 推理层:兼容性是结构化知识
omp v1 曾有 880 行的 OpenAI 兼容文件,靠 isCerebras、isKimiModel 等布尔量层层嵌套——每个分支都修过真实 bug,但同一知识被编码在五六个地方。omp² 改为三层声明式结构(taxonomy 识别型号 → classes 陈述血统事实 → providers 声明宿主差异,用 KDL 书写),关键在编译器:未知指令报错、同等优先级规则冲突报错、无匹配规则返回 unknown 而非 false。收益不是怪癖变少,而是每个事实有唯一主人、优先级显式、未知可被表达。
其余要点:
Provider 不止 stream:token 计数、联网搜索、embeddings、OAuth 刷新、模型发现……留给扩展等于保证出现 N 份各有缺陷的实现;
强制工具调用的三级策略:无条件注入软提示 → 厂商原生 flag 仅在无副作用时启用 → 模型不服从时有界重试,最终升级为代价性硬约束。这正是 Director 在推理层的对偶;
对模型的“方言”要宽容:模型可能用别家 harness 的 schema 调用工具(如 Codex 把数组发成分号分隔字符串),库应校验并修复,而非裸跑 JSON Schema;
严格采样是共享预算:厂商对 strict schema 数量有限额,语法方言因厂商而异,必须由推理层统一管理;
矫正性推理:修复畸形 JSON、检测重复循环、把泄露成文本的工具调用解析回结构化块——适配器“能开流”不算完成,“其余层收到一个规范化 turn”才算;
压缩是调度而非触发:在触及上限前约 10% 投机性启动压缩分支,主分支继续工作,完成后拼接——用户不必在最投入的时刻等待全会话最大的一次请求;
本地小模型做内部杂务:分类、起标题、TTS/STT,省下前沿模型的延迟与成本。
3.5 工具面:每个 schema 都对每一轮征税
实测数据:工具名册砍到 5 个,wall-clock 从落后 Codex 近 2 倍反超(36.6s vs 42.2s)——工具语法会实质影响 token 生成过程。动态工具发现虽省税,但换名册即缓存失效,不可取。
长尾藏在稳定表面之后:dyn CLI 给模型一个稳定的发现协议(搜索 → --help 查看合成自 JSON Schema 的用法 → 经 Bash/Eval 调用,支持 @file 与 stdin 传大输入);对开放式 API(浏览器、桌面)则暴露代码表面而非 schema 枚举。有界操作集用 schema,开放操作集用代码表面,两者都不需要改动常驻名册。
内建工具要“深”:
Read 一个工具顶别家 20 个:目录、notebook、Office 文档、SQLite、归档包、图片、性能 profile、HTTP 资源、统一 URL 体系(pr://、skill://、ssh://、mcp://……)、灵活区间语法、结构摘要。“Read 复杂,所以读取不复杂”——复杂性有唯一主人;
Bash 不简单 exec:内置 bash 解析器/解释器/核心工具集,保留模型的肌肉记忆(grep 自动路由到 ripgrep 引擎)、跨平台、会话状态延续;更重要的是审批粒度从“整个不可读的 shell 字符串”变为能力边界——执行到 ln 才询问、写目录外才询问;
AutoQA:给 Agent 一条“报 bug”的通道,自主收集它对工具的困惑与不满,过滤噪声后是改进工具面的高价值信号。
3.6 界面:字符串不是渲染原语
性能剖析显示渲染器吃掉会话 CPU 的大头——仅“判断某行是否为图片”的 .includes 就占 20%。同一契约还导致扩展 UI 风格失控,以及未消毒外部输入可用 ANSI 转义覆写整个终端的安全漏洞。性能、安全、一致性问题的共同根源是:一个已渲染的字符串同时被当作布局树、样式树、内容、传输格式和终端程序。
omp² 的三层解法:
单遍 RichText 流原语:渲染时间从 267 秒降到 90 毫秒,ANSI 解析、测量、分配从所有中间层消失;
类型化组件模型:(Element, Props, Children) + 布局引擎,语义化颜色/图标/边框由渲染器统一解析,扩展作者只描述结构与语义;
转录协议 + 形式化验证:把“块”的生命周期(active → finalized → committed)、可变 vs 仅追加两种模式、逻辑历史与物理 scrollback 的分离、resize 三策略全部写成精确规则,并用 TLA+ 建模验证(附录 B 给出完整 ElasticSlots.tla 规约)。改协议时模型检查器直接给出反例,告别靠 fuzzer 撞稳定性的日子。
另一个务实建议:为 UI 预先定义机器可读的“验证协议”(非破坏、离屏、可多实例的调试接口)。否则 Agent 会自行编造一个“看起来在测试”的旁路,悄悄拉低成功的定义。
3.7 技术栈:语言选择即架构
在大量代码由 Agent 产出的时代,语言的默认值、标准库、规范项目形态、编译器反馈构成生成代码的“先验”。作者尖锐批评 TypeScript:二十种同样“正常”的局部风格,意味着模型在碰到产品问题前要先做二十个决定(Zod 还是 Typebox、ESM 还是 CJS、Array 还是 T[]……)。一个允许二十种局部风格的语言,是在要求模型先做二十次风格决策。
结论:核心用 Rust(std + serde 生态 + 编译器安全网),扩展用 Python(Agent 写得好、AST 自省支撑 @remote、内嵌运行时让 Eval 可靠)。
评价与启示
这篇文章的价值,在于它把“Agent Harness 工程”从经验之谈提升为有名字、有边界、可推理的学科问题:状态权威、信任边界、有界执行、兼容性知识、视图投影——每一章都遵循同一方法论,且辅以扎实的实证材料(78 个扩展示例仅 2 个状态正确、渲染 CPU 剖析、wall-clock 基准),远超一般的架构布道文。
可持保留之处:
DOM/XML 作为会话权威格式、convar/cfg 体系的全面移植、自研 bash 解释器等选择都相当激进,长期维护成本有待 omp² 落地验证;
对 TypeScript 的否定带有明显的个人偏好色彩;
omp² 本身“部分已建成、部分仍在推演”(官方在 issue #9820 中确认暂无公开发布日期),Playbook 中不少承诺尚无第三方验证。
对从业者的启示:即便你不打算构建自己的 Harness,文中的多数原则——状态必须有唯一权威、限额应内建于原语、兼容性知识应显式建模、视图只是投影——同样适用于任何需要长时间运行、人机协作、容错恢复的 Agent 应用设计。
结语
《Harness Playbook》最终回答的,是开篇那句“but why?”:
Harness 的每个子系统,都是有着数十年先例的软件品类——复制、沙箱、配置、调度、协议兼容、实时渲染、语言与运行时设计。
简单性不能靠分摊复杂性来伪装。每一类不可避免的复杂性,都必须有一个明确的所有者,并被压进能强制执行其不变量的那一层。