Graft

不知道大家有没有用过以 mutiagent 著称的 oh-my-openagent(简称 omo)。我曾经就是被 omo 极其高效率的 muti 编排才骗过去用 opencode,然而使用体验极其糟糕——看起高效率的多 agent 流水线实际上带给你的只有慢,浪费上下文,和被莫名其妙不知所云的提示词污染 context。但我相信 mutiagent,或者说起码 subagent 的设计理念初衷本身是对的:用 fast context 抑或 explore 子代理去读代码肯定比直接 grep/rg 更干练,用不同的 worktree fork 以实现单独的上下文干单独的活更符合开发规范,本质是用来节省用来干活的核心 main session 的上下文,让他在最少的 compact 次数下做最有意义的活。

于是,我想做一个基于 fork 的 git-like 的 session 管理工具。传统的 agent fork 工具在 opencode,pi,claude code 都有实现,可惜他们的实现仅仅是传统 os 里 fork 出去的操作,顶多给 fork 分支打上一个 parent_id 的标签,而 fork 分支并没有实际上与他的父分支的逻辑意义上的管理(pi 其实有,还做了树形可视化和 branch 摘要)。

可问题在于 fork 出去往往是为了节省 main 分支的宝贵上下文,fork 出来干活,吭哧吭哧做了很多的活,然后呢?后续的开发你打算在新 fork 出来的还是在 main 里面?如果是 fork 分支,那除了一个存档或者快照外,和在 main 里直接干没区别;如果是 main,那 fork 分支干的活 main 的上下文并没有加载,如果是 有摘要 main 能读到还好(比如 pi),要是再去读一次 fork 上下文或者读代码更改,那岂不是多浪费一次 token?两头都不舒服,所以这个功能往往一直没啥人用。

如何解决上述问题?其实很简单,fork 这个词原本是操作系统原语,但是大部分人认识它是在 git 管理,我们学 git 把 fork 分支 merge back 回主分支不就行了!不过 merge 什么比较好呢,把 fork 对话上下文直接搬或者 compact 一下再搬都很不经济,不如我们再学一下 git,让 merge 写的东西和交 pr 写的摘要或者 changlog 一样,与 fork 出来的目标对齐即可。至于具体类型,返回的内容以 noReply 类型返回摘要。

所以,我选择 opencode 进行了开发这个改进 fork 插件,取名为 Graft(意思为嫁接,嫁接一段树枝和 merge 一个 fork 很像吧 x)。之所以选择 opencode 其实是路径依赖了,opencode 也没有那么的好用,不过 hooks 点还是蛮适合魔改开发的。不过 opencode 的原生 fork 存在诸如上文提到的众多问题,我用 ai 修改了 opencode 原生的 fork 功能,保留 fork 分支和原生 session 一样的 top-level session 地位的同时,同时用 SessionTree 来维护所有节点(或者说 fork 会话)和 main 分支的逻辑关系。我还做了 sidebar 的 fork tree,可以标识不同子会话的完成进度。

除了 /fork 和 /merge,我还做了 /abadon 和 /rewind,这些都顾名思义。(不过 opencode 的 rewind 是基于快照的,有点蠢)。

如此这样,main 会话就只需要维护项目级别的理解,具体实现工作交给子会话,可以保护宝贵的上下文和注意力来发配干活!

相信会有朋友担心多个分支的并发写入会不会存在问题,我在这一点的实现上是让主会话同步等待 fork,让 fork 分支像等待一个 tool 返回结果一样等待,但是 main 的中断脱离不会级联到 fork 分支上,完成后依然会把结果 noReply 地返回给 main 分支。其实整体来看是不是有点像不线性的线性管理www

不过,说到底我做的只是 git-like 的管理模式,所有的 fork 只是 session 意味上的 fork。我要求 agent 在使用该插件的时候不能对照 fork 分支去开对应的 git 分支,当然你想这么做也可以,我想还是尊重一下不同人开发习惯吧!

下面是我做的小 demo:https://github.com/Tanpinsary/Graft 。自己测试了小半个月看起来还行,啰啰嗦嗦说这么多也算自己一点 agent harness 设计上的小想法,希望大家能喜欢 Graft,如果有想法或者意见欢迎 issue/pr!