一、一个听起来很对的错误答案
我平时用 pi 当主力驾驶,但它有个让我越来越烦的问题:有些脏活——改个组件、跑个调研、写段样板——一旦塞进同一段上下文,既烧 token,又让主线变脏。我手头本来就有 CodeBuddy 的命令行,于是冒出一个念头:把它变成一个我能调度、能查进度、能喊停的"下属"。
第一反应很自然:"用 ACP 的方式接入 CodeBuddy。"ACP(Agent Client Protocol)是 Zed 推的"编辑器↔agent"通信协议,registry 里甚至就有 codebuddy-code 条目,启动命令是 npx @tencent-ai/codebuddy-code --acp。听起来正是为这种事生的。
但 pi 真去调研了一遍,把这个美丽的误会拆穿了:
- CodeBuddy 确实支持 ACP,但它是 ACP 的 server 端(被编辑器调用的那一侧);
- pi 既不是编辑器,也不原生支持 ACP——它不能当 ACP client 去拉别的 agent,接模型走的是 HTTP API,不是 ACP 的 stdio JSON-RPC;
- 所以"用 ACP 接入"在我的场景里,等价于"让 pi 当 ACP client 去调 CodeBuddy 这个 ACP server"——一个 agent 套另一个 agent,还得自己写桥接层,工程量大、语义也别扭。
我学到的最重要的一课,恰恰是被这个"标准答案"带偏了。 当一样东西有官方协议、有 registry、有文档,人很容易跳过思考直接伸手。但协议是给"编辑器"设计的,我的主角是 pi,不是编辑器。硬套,反而最绕。
二、真正的钥匙,在 CodeBuddy 自己手里
退一步,从第一性原理看我要的到底是什么:① 异步启动一个任务;② 查它跑到哪了;③ 它卡住时我有权喊停。带着这三个需求去翻 CodeBuddy CLI 的原语,发现它自己就有一整套后台任务生命周期,根本不需要 ACP:
-p+--output-format stream-json:非交互模式,实时吐结构化事件流——这正是"查进度"要的实时性;-y(bypassPermissions):子代理无人值守,遇到要授权的操作不会卡住等输入,而是自动拒绝并继续;--max-turns:给轮数封顶,防死循环;--tools白名单:配着-y用,最小权限,抵消安全风险;daemon/ps/kill:后台会话管理。
这里还有个值得记的坑。我一度想走"daemon + --bg 后台会话"的轻量路线,实测却发现:不开 codebuddy daemon start,--bg 启动的任务就是"孤立"的——能跑,但 ps/logs/kill 全报 not found;而且 --bg 的 log 文件只有在任务结束后才有内容,跑着的时候读是空的,等于看不到中间过程。为了"实时进度"和"不依赖常驻 daemon",我最终选了方案 2:pi 扩展里自己 spawn codebuddy -p stream-json 子进程,自己读 stdout buffer、自己管看门狗、自己杀进程树。复杂一点,但进度是真·实时,可控性也最高。
三、一个薄扩展,三个工具
最终落地的是一个 pi 扩展(~/.pi/agent/extensions/codebuddy-subagent.ts),全局自动发现,pi 启动即加载。它只注册了三个工具:
codebuddy_start(task, name?, max_turns?, tools?)——spawn 子进程,从 stdin 传 prompt(避免命令行引号转义地狱),立刻返回 PID + 名字;codebuddy_status(name?)——返回进程是否活着 + 最近 N 行实时事件,顺带做卡住检测;codebuddy_stop(name)——杀掉整个进程树。
看门狗、白名单、-y、stdin 传参,这些都不是拍脑袋,是每一步都先真跑一遍验证出来的:先验证 daemon 链路,再验证 stdin prompt,再独立测 spawn/解析/kill,最后端到端跑 start → status → stop。三关全过,才敢说"能用"。
四、结果怎么回来:别阻塞,发通知
子代理干完了,结果怎么交还给我?这里我给 pi 出了个难题,也暴露了我对"委派"的真正要求。
我不要它把 CodeBuddy 的输出直接塞进上下文再自己总结一遍——那等于我既付出了 CodeBuddy 的 token,又付出了 pi 再思考一遍的 token,还慢。两个方案里我选了后者:用 sendMessage 发一条"任务完成"通知,并借助 steer 机制。更关键的是加了个判断——如果 pi 当前自己正忙着一个任务,通知就 triggerTurn: false 先挂着;等它手头空了,才 triggerTurn: true 去读结果继续。还要支持一种场景:我不在的时候,pi 也要能拿到这条完成通知,忙完当前任务后自动接着干。 这本质上是在把子代理当后台任务/异步消息用,而不是同步函数调用。
五、最上层:软引导,而不是硬开关
工具有了,但每次都显式调 codebuddy_start 太重。我最后把"delegate"收敛成一个模式开关:平时 pi 自己干,我切一下 delegate,输入就软引导(系统提示词层面)代理到 CodeBuddy;显式派发我直接用 prompt 指定就行,不用专门造指令。开关状态还显式挂在状态栏上。底下那层硬扩展不动,上面这层只是"用嘴说"的引导——也就是"最开始的那种加系统提示词的方案"。
为了让委派过程不黑盒,我还让 CodeBuddy 的执行过程能实时展示出来(像 Claude Code 展示子代理任务那样切换查看),并给用户输入块加了边框和背景色——我每次回看历史,总得花半天找"哪句是我说的"。最后用 codebuddy_start/status/stop 跑了端到端测试和一次 load test,确认能启动、能查、能终止、能抗压。
六、回看:这件事暴露了我的几条价值观
一篇"AI 驱动"的文章不能只记操作,得从选择里提炼价值观。这次我的几个选择,其实都指向同一类偏好——
1. 可控的委派,远大于全自动。 我反复拦住 pi"把所有任务都丢给 CodeBuddy"的冲动,只要"结果可见、过程可查、可停止"。我要的是手下,不是替身。掌控感比省事重要。
2. 异步非阻塞,是一种尊重。 从 sendMessage 通知到 triggerTurn 检测,我一直在避免"调用即阻塞"。子代理该像后台任务,不该像同步函数。这背后是同一个判断:我的注意力是串联的,工具的执行不该卡住我。
3. 可观测性即信任。 实时展示、用户输入加边框,看似是界面洁癖,实则是在说:我愿意把活交出去,但我必须随时能看懂"刚才发生了什么"。看不懂的委派,我不会用第二次。
4. 务实,且警惕"标准答案"。 从 ACP 的诱惑里抽身,选最朴素的 stream-json 薄包装;先验证握手、记忆问题先挂起、不追求一步到位。不过度工程,是我对"优雅"的重新定义——能跑通的薄一层,好过听起来正确的重协议。
说到底,我没有把 CodeBuddy 变成"另一个我",而是变成了一个我随时能收回控制权的手。工具是手段,我的注意力才是稀缺资源。而这次最值钱的教训,是那个被我亲手拆穿的"ACP 误会":当一个标准协议听起来恰如其分时,先问一句——它到底是给谁设计的?
