oh-my-pi:把编码智能体做成工程化 Harness
oh-my-pi 给自己的定位很直接:A coding agent with the IDE wired in.。如果只看 README,这句话很容易被理解成又一个“能调工具、能跑命令、能接模型”的 AI 编码 CLI;但沿着 packages/coding-agent/DEVELOPMENT.md、tools/index.ts、session-manager.ts、internal-urls/router.ts、hashline 与 benchmark 包一路读下去,就会发现它真正有意思的地方不是“功能多”,而是把工具边界、会话边界、资源边界与验证边界都做成了运行时协议[1][2]。
这也是我读完整个仓库后的核心判断:oh-my-pi 最值得研究的,不是它支持多少模型、多少 provider,甚至也不只是 hashline 这个编辑格式本身,而是它在尝试把“编码智能体为什么不可靠”这件事,拆成一组可工程化处理的 harness 问题。
1. 为什么说它不是普通 AI CLI
README 首页给出了一串很抓眼球的 headline:40+ providers、32 built-in tools、13 lsp ops、27 dap ops、~27k lines of Rust core,并把 LSP wired into every write、Drives a real debugger、Time-traveling stream rules、First-class subagents、GitHub is just another filesystem、Hashline: edit by content hash 放在同一层能力面上[1:1]。
这类描述当然带有产品叙事色彩,但它并不是空喊口号。仓库顶层同时存在 crates/、packages/、python/、docs/、scripts/ 等多套子系统;根 package.json 显示它是一个以 bun 为核心的 monorepo,而根 Cargo.toml 又表明它维护了一整套 Rust workspace,并把 release profile 调到了偏重原生性能的配置,例如 lto = "fat"、codegen-units = 1、panic = "abort"[3][4]。
这意味着 oh-my-pi 的真实形态不是“一个 TypeScript 程序顺手调一下 shell”,而是:
- 上层用
coding-agent组织 CLI、session、tool、plan、task、MCP、LSP、debug 等运行时。 - 中层用 internal URL、artifact、history、snapshot 等协议把资源表面统一起来。
- 下层把搜索、AST、shell、highlight、token 计算、PTY、图像处理等热路径持续下沉到 Rust / native 层。[1:2][4:1]
如果把这个项目只当成“另一个 AI CLI”,其实会错过它最有研究价值的部分。
2. 从 Pi fork 到工程化 runtime
README 明确说它是 Pi 的 fork,但目标不是做一个通用聊天壳,而是构造一个 coding-first surface[1:3]。packages/coding-agent/DEVELOPMENT.md 更进一步,把这个 surface 拆成了非常清晰的运行时结构:
cli.ts/main.ts/index.ts负责启动入口。modes/负责 interactive、print、rpc 三种模式。session/负责AgentSession、持久化、compaction、artifacts。tools/负责内建工具与统一输出协议。task/负责 subagent / task orchestration。mcp/负责 MCP transport、manager、loader 与 tool bridge。lsp/负责 language server client 与 runtime integration。internal-urls/负责scheme://风格的内部资源路由。[2:1]
用一句话概括:它不是在“命令行里塞很多工具”,而是在搭一个围绕编码任务组织起来的 runtime。
下面这张图更接近我读源码后对其结构的理解:
这个图最重要的一点在于:task、LSP、DAP、artifact、internal URL 都不处在“插件附件层”,而是一级运行时组件。
3. Tool Registry 才是它的心脏
如果只读 README,你会觉得 read、bash、edit、search、web_search 这些是常见工具;但一旦展开 packages/coding-agent/src/tools/index.ts,会发现 ToolSession 已经厚到像一份运行时内核接口:里面除了 cwd、hasUI 外,还有 workspaceTree、skills、promptTemplates、rules、artifactManager、asyncJobManager、mcpManager、agentRegistry、enableLsp、snapshotStore、noopLoopGuard、budget 与 usage 统计等上下文字段[5]。
换句话说,工具并不是“函数调用列表”,而是整个 agent session 的执行表面。
同一个文件里还能直接读到当前快照的 BUILTIN_TOOLS 与 HIDDEN_TOOLS。BUILTIN_TOOLS 能直接数到 29 个键,包括 read、bash、edit、ast_grep、ast_edit、render_mermaid、debug、task、lsp、browser、web_search、todo、memory_edit、retain、recall、reflect 等;HIDDEN_TOOLS 则额外包含 yield、report_finding、report_tool_issue、resolve、goal[5:1]。
这件事带来一个很有意思的阅读结论:README 对外说 32 built-in tools,但当前源码注册表并不等于一个机械的 32。更稳妥的说法应该是:
- README 提供的是外部能力口径。[1:4]
- 当前快照中,源码可直接数到
29个BUILTIN_TOOLS。 - 此外还有 hidden tool 与按 settings / runtime 条件自动注入的能力面。[5:2]
packages/coding-agent/test/tools/index.test.ts 也进一步证明这不是静态列表:默认工具集里会有 eval、bash、read、edit、write、search、find、lsp、task、todo、web_search、resolve,但 ask 是否暴露取决于 hasUI,find / search / ast_grep / browser / inspect_image 等又都可以被 settings 关掉[6]。
因此,oh-my-pi 的工具层更像一个动态裁剪的 agent capability registry,而不是“README 贴了一张大工具表”。
4. Session 不是聊天记录,而是 append-only 事件树
packages/coding-agent/src/session/session-manager.ts 是另一个非常值得认真读的文件。它最重要的一句注释是:Manages conversation sessions as append-only trees stored in JSONL files.[7]
这句话背后有两层含义。
第一层,session 文件里存的不只是对话消息。源码里直接定义了 SessionMessageEntry、CompactionEntry、BranchSummaryEntry、MCPToolSelectionEntry、SessionInitEntry、ModeChangeEntry、CustomMessageEntry 等多种 entry 类型[7:1]。这说明 transcript 在这里不是单纯“聊天记录”,而是运行时事件流。
第二层,branch 不是 UI 假象。branch(branchFromId: string) 会移动 leaf pointer,branchWithSummary(...) 会追加 branch_summary entry,createBranchedSession(leafId) 则能把从根到某个 leaf 的路径导出成新 session,并保留 parentSession 关系[7:2]。这意味着 oh-my-pi 把“会话分支”做成了底层存储模型的一部分,而不是简单在前端上做个切页。
DEVELOPMENT.md 还提示了另一个很容易被忽略的细节:HistoryStorage 是独立的 SQLite prompt recall,而不是 session transcript 本身[2:2]。这使得 session、history、memory 三类概念被明确分层,而不是全塞进一个文件里。
如果说很多 agent 工具把“上下文管理”理解成 prompt engineering,那么 oh-my-pi 在这里已经明显走向了存储结构 engineering。
5. Internal URL 与 Artifact:把资源边界做成协议
我认为这是整个项目最像 “harness engineering” 的地方。
packages/coding-agent/src/internal-urls/router.ts 明确实现了一个进程级 InternalUrlRouter,为 omp://、agent://、artifact://、memory://、local://、vault://、skill://、rule://、mcp://、issue://、pr:// 等 scheme 注册独立 handler,并通过统一的 resolve() 路径分发[8]。
这件事的意义非常大。它把原本散落在不同子系统里的“资源入口”统一成了同一种语义表面:
artifact://指向工具输出工件。agent://指向子智能体或相关会话产物。pr://、issue://把 GitHub 讨论对象视作可读资源。local://、vault://、memory://、skill://则对应不同存储域。[8:1][1:5]
这不是 URL 花活,而是 runtime 资源建模。tools/bash.md 与系统 prompt 甚至会明确要求不要用 head / tail 人工截断输出,因为 harness 自己会把完整输出存进 artifact://<id>,用户界面只显示被截断后的可见部分[9]。换句话说,它在制度化地要求 agent:不要伪造边界,边界由 runtime 来管理。
这比“给大模型多几个工具”深得多,因为它实际在解决三个经典问题:
- 输出太长时如何保真。
- 子任务结果如何可追踪。
- 外部资源与内部资源如何用统一协议访问。
6. Hashline:不是 patch 语法,而是编辑 runtime
hashline 是 oh-my-pi 最具代表性的工程创新之一,但我更愿意把它写成“编辑 runtime”,而不是“新的 diff 格式”。
packages/hashline/README.md 的定义非常直接:Hashline is a diff format designed for LLM-driven file edits. 它用 [PATH#TAG] 作为文件段头,其中 TAG 是 SnapshotStore 记录的短内容哈希;当 patch 应用时,系统会先核验 live file 与 recorded snapshot 是否一致,不一致就拒绝或进入 recovery 路径[10]。
它支持的语法也不是传统 unified diff,而是围绕编辑意图设计的动作语义,例如:
replace A..B:replace block A:delete A..Binsert before/after/head/tail
更关键的是,hashline 并没有停留在一个 README 说明里。当前仓库里至少能看到三层实现证据:
packages/hashline/是独立 package,并维护自己的 changelog。[10:1]packages/coding-agent/src/edit/hashline/下存在filesystem.ts、execute.ts、diff.ts、block-resolver.ts、noop-loop-guard.ts等完整执行栈。[1:6]packages/hashline/bench/recovery-session-chain.ts还专门 benchmark 了Recovery.tryRecover -> applyEditsToSnapshot -> replaySessionChainOnCurrent -> verifyAnchorContent这条恢复链路。[11]
这说明 hashline 并不是一次性的 benchmark trick,而是一套围绕快照、锚点、恢复与空操作防护持续演化的编辑协议。
7. Benchmark 真正说明了什么
这是整篇文章里最需要谨慎写的部分。
作者在自己的文章《The Harness Problem》中提出了一个非常强的论点:Only the Harness Changed,更具体地说,“only the edit tool changed”[12]。文中还给出了几组非常醒目的数字,例如 Grok Code Fast 1 从 6.7% 提升到 68.3%,Grok 4 Fast 的输出 token 下降 61%,以及 MiniMax 的 2.1x 改善[12:1]。
如果只看宣传,这里很容易落入两种误区:
- 误以为这些数字已经被当前仓库 HEAD 逐项复核。
- 误以为 benchmark 只是在给
hashline做 marketing。
这套 benchmark 的真正可贵之处,不在于某一个数字多漂亮,而在于它试图隔离变量。作者在文中拿 apply_patch、str_replace、Cursor 的神经 apply、JetBrains 的 Diff-XYZ 与 EDIT-Bench 等一起做背景比较,实际上是在说明同一个事实:编辑表面并不是模型外的细节,而是结果分布的一部分[12:2][13][14]。
7.1 当前 HEAD 的确存在可运行 benchmark 基础设施
packages/typescript-edit-benchmark/ 在当前快照中完整存在,包含 package.json、all_models_results.json、src/index.ts、src/runner.ts、src/generate.ts、src/tasks.ts、src/report.ts、src/verify.ts 与配套测试[15][16]。
从源码可以确认,这套 benchmark:
- 目标是测试
edit precision,而不是bug-finding ability。[17] - 支持
replace、patch、hashline、vim、atom、apply_patch、auto多种edit variant对比。[16:1] - 会统计成功率、token、tool 使用、ghost run、timeout、retries,以及
hashline的子类型统计。[18] - 会导出 conversation dump 与 session artifact,因此它不只留最终分数,也保留运行轨迹。[18:1]
也就是说,当前 HEAD 里确实有一套真实、可执行、偏方法学自觉的 edit benchmark 子系统。
7.2 作者文章中的 headline 数字并不等于当前 HEAD 的直接结果
all_models_results.json 当前能直接读到的是一些聚合结果,例如 haiku-4.5 为 90.0、gemini-3f 为 80.0、minimax-m2.5 为 75.0 等,但它并没有直接出现作者文章里的 6.7% -> 68.3%、-61% output tokens、2.1x 这些 headline 数字。[19]
因此,更严谨的写法应该是:
- 这些 headline 数字有作者文章作为一手来源。[12:3]
- 当前 HEAD 有现存 benchmark 实现与结果聚合文件。[15:1][19:1]
- 但本文调研到的仓库快照并未把作者文章中的所有数字逐条对应回同一份现存结果文件。
7.3 react-edit-benchmark 的路径存在时间差
作者文章点名的路径是 packages/react-edit-benchmark[12:4]。而当前 HEAD 中现存的是 packages/typescript-edit-benchmark/。这两者之间并非完全无关:Git 历史已经证明 react-edit-benchmark 在过去确实长期存在并频繁演化,只是当前快照中不再以该路径出现。
所以最稳妥的判断是:作者文章中的 React benchmark 与当前 HEAD 的 TypeScript benchmark 高度相关,极可能属于同一条演化线,但在没有显式迁移说明之前,不应把它们写成严格一一同构。
7.4 这套 benchmark 的真正价值
它真正可贵的地方,不在于某一个数字多漂亮,而在于它试图隔离变量:
- mutation 任务强调“精确施加编辑”,而不是“找出 bug”。[17:1]
- edit variant 被显式枚举并可切换。[16:2]
- failure category 被细分为
range-continuation、unified-diff、no-change、hash-mismatch、other。[18:2]
这和作者文章想证明的方向是一致的:在 coding agent 里,模型并不是唯一变量,edit surface 本身就是结果变量。
8. Provider、LSP、DAP:要把 README 口径和源码口径分开读
oh-my-pi 还有一个特别值得学习的地方:它让你看到产品口径与实现口径之间并不总是完全一一对应。
8.1 40+ providers 基本能被源码量级支撑
packages/catalog/src/provider-models/descriptors.ts 里有一个 CATALOG_PROVIDERS 数组,当前快照能直接读到大量 provider descriptor,例如 anthropic、deepseek、google、groq、openai、openrouter、ollama、github-copilot、gitlab-duo、minimax、moonshot、xai、zai、多组 xiaomi-token-plan-* 等[20]。与此同时,model-registry.ts 还给出了 full bundled catalog (thousands of models, ~50 providers) 的注释口径[21]。
因此,README 的 40+ providers 至少在量级上是能被当前源码结构支撑的。[1:7][20:1]
8.2 13 lsp ops 与 27 dap ops 则存在计数口径差异
packages/coding-agent/src/lsp/types.ts 中,lspSchema 当前可直接数到 14 个 action:diagnostics、definition、references、hover、symbols、rename、rename_file、code_actions、type_definition、implementation、status、reload、capabilities、request[22]。
而 packages/coding-agent/src/tools/debug.ts 的 debugSchema 当前则可数到 28 个动作,包括 launch、attach、set_breakpoint、continue、evaluate、stack_trace、threads、variables、read_memory、write_memory、custom_request、terminate、sessions 等[23]。
这与 README 的 13 lsp ops、27 dap ops 不完全一致。比较合理的解释是:README 采用的是偏对外展示的统计口径,而当前 schema 把像 request 或 custom_request 这样的通用 escape hatch 也算了进去。但无论如何,写技术文章时都不该把 README headline 直接当成“源码硬事实”。[1:8][22:1][23:1]
9. 我对 oh-my-pi 的三个关键洞察
9.1 它真正解决的不是“会不会写代码”,而是“工具边界是否可信”
很多 agent 项目把焦点放在 prompt、模型路由、上下文截断或多 agent 协作上;oh-my-pi 则明显把大量工程力气花在:读写边界怎么表达、输出怎么保真、分支怎么持久化、内部资源怎么寻址、恢复链路怎么收口。这种关注点非常像作者文章里的那句话:真正决定可靠性的,往往不是“更大的模型”,而是 harness 本身[12:5]。
9.2 它把本来容易散掉的能力做成了一套统一协议
tool registry、artifact://、agent://、pr://、issue://、local://PLAN.md、append-only session tree,这些看上去彼此无关的机制,其实共同在做一件事:让 agent 的外部操作、内部状态与验证产物,都有稳定的协议边界。[5:3][8:2][7:3]
这比“工具很多”更重要,因为 agent 一旦进入长周期、多步骤、多工件任务,真正先崩的常常不是模型,而是边界管理。
9.3 它最难复制的部分可能不是模型接入,而是长期演化的工程细节
packages/coding-agent/CHANGELOG.md 显示这个项目在高频修补各种边界条件:artifact 输出截断、background task 竞态、subagent loop、no-op edit、权限绕过、LSP / DAP 稳定性、shell minimizer、memory runtime 等[24]。这说明所谓 harness,并不是一个漂亮架构图,而是一长串“只有踩过坑才会写进去”的工程补丁与协议细化。
换句话说,oh-my-pi 最像护城河的地方,不一定是 headline feature,而是这些持续积累出来的运行时手感。
10. 局限性与注意事项
这次研究也有几个必须明确说出的边界。
第一,本文对 benchmark 的处理是分层的:typescript-edit-benchmark、hashline recovery benchmark、tool / session / URL runtime 都属于当前源码已能直接验证的实现;而作者文章中的 6.7% -> 68.3%、-61% tokens、2.1x 等 headline 数字,则属于作者展示的 benchmark 结果,本文没有把它们逐项复核回同一份现存结果文件。[11:1][19:2][12:6]
第二,README 中的 32 built-in tools、13 lsp ops、27 dap ops 更适合作为对外口径来理解;当前 HEAD 的源码计数存在差异,写作时需要主动说明,而不是省略这种不一致。[1:9][5:4][22:2][23:2]
第三,虽然 40+ providers 的量级主张与当前源码 catalog 相符,但如果要给出严格精确的 provider 计数,仍应做一次专门去重统计,而不是直接把 descriptor 目测数量当最终答案。[20:2]
11. 结语
如果要我用一句话总结 oh-my-pi,我会说:它不是把大模型塞进终端,而是在把“编码智能体为什么容易失控”拆解成一套可演化的工程协议。
hashline、append-only session tree、internal URL router、artifact 协议、动态 tool registry、LSP / DAP / subagent 的一等集成,共同构成了这个项目最值得看的部分。它提醒我们一个很重要的事实:在 coding agent 时代,真正的竞争力往往不在模型名字,而在 harness 是否足够厚、足够稳、足够可恢复。
作者在《The Harness Problem》里试图证明“只改 harness,就足以让结果产生数量级变化”[12:7]。无论你是否完全接受那些 headline benchmark 数字,至少从当前仓库快照来看,oh-my-pi 确实把这个命题认真做成了代码,而不只是做成了宣传文案。
参考文献
can1357/oh-my-pi仓库README.md。https://github.com/can1357/oh-my-pi/blob/main/README.md ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎can1357/oh-my-pi仓库packages/coding-agent/DEVELOPMENT.md。https://github.com/can1357/oh-my-pi/blob/main/packages/coding-agent/DEVELOPMENT.md ↩︎ ↩︎ ↩︎can1357/oh-my-pi仓库根package.json。https://github.com/can1357/oh-my-pi/blob/main/package.json ↩︎can1357/oh-my-pi仓库根Cargo.toml。https://github.com/can1357/oh-my-pi/blob/main/Cargo.toml ↩︎ ↩︎can1357/oh-my-pi仓库packages/coding-agent/src/tools/index.ts。https://github.com/can1357/oh-my-pi/blob/main/packages/coding-agent/src/tools/index.ts ↩︎ ↩︎ ↩︎ ↩︎ ↩︎can1357/oh-my-pi仓库packages/coding-agent/test/tools/index.test.ts。https://github.com/can1357/oh-my-pi/blob/main/packages/coding-agent/test/tools/index.test.ts ↩︎can1357/oh-my-pi仓库packages/coding-agent/src/session/session-manager.ts。https://github.com/can1357/oh-my-pi/blob/main/packages/coding-agent/src/session/session-manager.ts ↩︎ ↩︎ ↩︎ ↩︎can1357/oh-my-pi仓库packages/coding-agent/src/internal-urls/router.ts。https://github.com/can1357/oh-my-pi/blob/main/packages/coding-agent/src/internal-urls/router.ts ↩︎ ↩︎ ↩︎can1357/oh-my-pi仓库packages/coding-agent/src/prompts/tools/bash.md。https://github.com/can1357/oh-my-pi/blob/main/packages/coding-agent/src/prompts/tools/bash.md ↩︎can1357/oh-my-pi仓库packages/hashline/README.md。https://github.com/can1357/oh-my-pi/blob/main/packages/hashline/README.md ↩︎ ↩︎can1357/oh-my-pi仓库packages/hashline/bench/recovery-session-chain.ts。https://github.com/can1357/oh-my-pi/blob/main/packages/hashline/bench/recovery-session-chain.ts ↩︎ ↩︎can1357,《The Harness Problem》(2026-02-12)。https://blog.can.ac/2026/02/12/the-harness-problem/ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Diff-XYZ 论文摘要页。https://arxiv.org/abs/2510.12487 ↩︎
EDIT-Bench 论文摘要页。https://arxiv.org/abs/2511.04486 ↩︎
can1357/oh-my-pi仓库packages/typescript-edit-benchmark/package.json。https://github.com/can1357/oh-my-pi/blob/main/packages/typescript-edit-benchmark/package.json ↩︎ ↩︎can1357/oh-my-pi仓库packages/typescript-edit-benchmark/src/index.ts。https://github.com/can1357/oh-my-pi/blob/main/packages/typescript-edit-benchmark/src/index.ts ↩︎ ↩︎ ↩︎can1357/oh-my-pi仓库packages/typescript-edit-benchmark/src/generate.ts。https://github.com/can1357/oh-my-pi/blob/main/packages/typescript-edit-benchmark/src/generate.ts ↩︎ ↩︎can1357/oh-my-pi仓库packages/typescript-edit-benchmark/src/runner.ts。https://github.com/can1357/oh-my-pi/blob/main/packages/typescript-edit-benchmark/src/runner.ts ↩︎ ↩︎ ↩︎can1357/oh-my-pi仓库packages/typescript-edit-benchmark/all_models_results.json。https://github.com/can1357/oh-my-pi/blob/main/packages/typescript-edit-benchmark/all_models_results.json ↩︎ ↩︎ ↩︎can1357/oh-my-pi仓库packages/catalog/src/provider-models/descriptors.ts。https://github.com/can1357/oh-my-pi/blob/main/packages/catalog/src/provider-models/descriptors.ts ↩︎ ↩︎ ↩︎can1357/oh-my-pi仓库packages/coding-agent/src/config/model-registry.ts。https://github.com/can1357/oh-my-pi/blob/main/packages/coding-agent/src/config/model-registry.ts ↩︎can1357/oh-my-pi仓库packages/coding-agent/src/lsp/types.ts。https://github.com/can1357/oh-my-pi/blob/main/packages/coding-agent/src/lsp/types.ts ↩︎ ↩︎ ↩︎can1357/oh-my-pi仓库packages/coding-agent/src/tools/debug.ts。https://github.com/can1357/oh-my-pi/blob/main/packages/coding-agent/src/tools/debug.ts ↩︎ ↩︎ ↩︎can1357/oh-my-pi仓库packages/coding-agent/CHANGELOG.md。https://github.com/can1357/oh-my-pi/blob/main/packages/coding-agent/CHANGELOG.md ↩︎