我平时同时使用 Codex 和 Pi。有些任务需要反复讨论目标、判断方案,也有大量读文件、改代码、跑测试的执行工作。我希望继续和 Codex 讨论、规划和审查,把执行交给 Pi,减少 Codex 的额度消耗。
最初的想法很直接:能不能让 Codex 指挥 Pi 干活?后来发现,接通只是开始。结果什么时候取、遇到问题怎么问、上下文怎么复用、等待时谁在调用模型,都需要逐个解决。
我负责提出需求、试用和纠偏,Codex 完成实现与验证,最后形成了 sub-pi-agent。下面按这些问题出现的顺序,记录工具怎么变成现在这样,也记录我对 agent 协作的理解如何变化。文中工具行为对应 0.1.1,通信验证使用的是本地 Codex 0.162.0-alpha.17.2。
先接起来,让 Codex 能把任务交给 Pi
起初我想过给 Codex 自带的 subagent 换一个模型。进一步找方案时,发现已经有 agent-pi,可以通过 MCP 把 Pi 的执行能力暴露给上层。
这就有了一个清晰的分工:Codex 理解需求、拆解任务、检查方向,Pi 读取项目、修改代码、执行验证,再交回结论。Pi 不可用时,Codex 也可以直接兜底。
这里有个容易忽略的细节:Pi 不会自动获得整段 Codex 对话。上层需要传入任务目标、必要背景和工作目录。如果任务适合产出文件,也可以让 Pi 在结论中给出位置,由 Codex 读取审查。
我也希望 Codex 留在对话里。执行中发现目标偏了,它要能纠偏;Pi 交回结果后,它要检查修改是否符合最初的需求。把一段工作委派出去,上层仍需要判断任务是否真正完成。
sub-pi-agent 基于 agent-pi 1.8.0 修改,保留原作者的 MIT 许可。它内置 Pi SDK,读取 Pi 的配置和认证;启动任务时并不再启动一个全局 Pi CLI。
接上以后,为什么又要选一次模型?
初版派发任务需要指定 model,这很快变成了麻烦:我已经在 Pi 中选好了模型,为什么还要让 Codex 再选一次?如果我切换 Pi 默认模型,MCP 会不会继续使用旧配置?
于是把 model 改成可选。省略时,每次 spawn 或 continue 都重新读取 Pi 当前设置;显式填写时才覆盖。正在执行的任务保持启动时的模型。
thinking level 也沿用 Pi:先取该模型的设置,再取 Pi 默认值;都没有配置时,才使用 SDK 的 medium,并按模型支持范围调整。
这样,我在 Pi 中管理模型,Codex 负责派发任务。两边少了一份需要人工同步的配置。
工具能调用,交接却不一定清楚
接下来是工具的输入输出。
最初 steer_agent 只返回一个 ok。看见它,我仍不知道发了什么、是否进入队列、Pi 是否已经处理。后来改成返回发送的纠偏原文和投递状态。这里的 queued 只表示已接受排队,处理进度仍要继续观察。
poll_agent 则一度把累积文字都返回。我想知道的是当前进度和待回答的问题,完整执行记录会占据上层上下文。
get_result 又出现了另一个问题:spawn 后立刻调用,工具就可能一直等,界面上也看不到具体交付。我开始怀疑,它是不是和其他工具不一样。
排查时,我们把“调用是否已经返回”和“结果正文是否进入标准 MCP 返回”分开检查,统一让结果同时进入 content 的文本块和 structuredContent。工具描述也补上了调用顺序,但最终还需要明确各自的职责:
| 工具 | 什么时候用 | 读到什么 |
|---|---|---|
pi_poll_agent | 想了解当前情况 | 最新进度、活动状态、待回答问题 |
pi_get_result | 任务结束后审查交付 | 本轮最后一条 Pi 文字回复 |
get_result 现在不承担等待。提前调用会明确返回 ready: false 和下一步提示。最终回复可以包含结论、验证情况和文件位置,具体交付方式由任务决定;任务失败时,也会交回部分回复和错误,供上层判断如何处理。
我以前主要从人的角度看工具说明,这次开始把它当成给模型的使用说明:spawn 返回 ID 后该做什么,什么状态可以取结果,收到问题用哪个工具回答。返回值里的 next 与说明相互配合,减少让 Codex 猜测下一步的空间。
同一个任务,为什么总要新建 agent?
只有一个临时 ID,也不方便长期使用。我希望能看见 agent 的名字、职责和状态,之后接着找它。
于是 spawn 增加了 name、description,再加入 list_agents,支持按状态和目录筛选。名字帮助辨认,真正继续任务时仍显式指定 session_id,避免同名带来的歧义。
需求也从“本次进程内复用”逐渐变成了“重启 MCP、重开 Codex 后还能继续”。这里要保存的是 Pi 原生会话上下文,包括之前的工具交互。只把上一轮总结塞给一个新 agent,很多执行细节就接不上了。
恢复范围限定为这个 MCP 创建并登记的 Pi 会话。它不会随意接管我在 Pi CLI 中开的其他会话。
生命周期因此分成了两层:任务有没有结束,以及会话资源是否仍加载。
release 释放内存,磁盘中的上下文仍可恢复。运行中的任务不会被闲置回收;MCP 退出会中断执行,之后需要明确继续,不自动重跑中断的命令。不同 MCP 进程也不能同时写入同一个 agent。
为了验证“记住了”不是上层重新提示出来的,我们曾让 Pi 先记住一条约定,中途提问,再卸载会话。恢复时只回答它的问题,不重复那条约定,最后检查它能否同时给出原有信息与新答案。这样的检查,比看见同一个 ID 更有说服力。
Pi 有问题,能不能主动叫回 Codex?
看到 Codex 需要主动取结果,我自然问:MCP 不是双向的吗?Pi 能不能完成后主动通知,甚至像 steer 一样,把问题送进 Codex 当前这一轮?
这里才发现,协议能力、客户端支持和模型收到消息,是三件不同的事。
MCP 的 sampling 允许服务端经客户端请求模型生成,需要客户端声明支持。它也不等同于向现有主对话注入一条消息。当时的本地验证中,Codex 没有声明 sampling,测试请求返回 Method not found;普通 MCP 通知也没有让主模型收到完成事件。
另一条路是 Codex App Server,它提供控制 Codex 会话的接口,包括 turn/steer。但普通 MCP 配置并不会把当前桌面会话的控制连接交给服务端。要走这条路,需要额外的客户端集成或启动层。
我的目标是别人下载后加一段 MCP 配置就能用,因此先保留普通 stdio MCP,暂时放下实时注入。
提问仍然可以做,只是换一种交接方式。Pi 保留原生系统提示词,再追加几行委派说明,大意是:
你是上层委派的执行 agent。完成后简洁报告结论;如果缺少信息或存在阻塞决策,调用
ask_supervisor提出具体问题,然后结束本轮,等待上层继续同一会话。
ask_supervisor 是 Pi 内部工具。MCP 收到结构化问题后记录为 needs_input,Codex 读取问题,再用同一个 ID 调用 continue 给出答案。
未读提醒可以随这个 MCP 的下一次工具返回附送,告诉上层哪个 agent 有问题或已经完成。正文仍通过 poll 或 get_result 读取。这样能减少漏看,但聊天结束后,它不会凭空唤醒 Codex。
提醒也需要有“读过了”的规则:poll 读到问题正文才确认问题已读,get_result 读到交付才确认结果已读。仅仅在列表里看见完成状态,不算已经看过结果。这一点决定了提醒会不会在正文尚未读取时就消失。
进度文字没变,Pi 真的没动吗?
后来一次实际任务里,poll 多次返回同一句:
Now implementing. First, the delivery contract in `bridge.py`:
与此同时,Codex 还去检查了 git diff。我担心两件事:Pi 是不是卡住了?Codex 是不是在等待期间反复调用模型,消耗我原本想省下的额度?
我们对照了两边的 session 日志。两次重复 poll 之间,Pi 实际新增了 9 条 assistant 消息和 9 条工具结果,在持续编辑文件和运行命令,只是没有新的显式文字回复。
这解释了重复输出:poll 选择最近的显式回复,单靠它无法体现工具执行。于是增加 activity,只提供活动元数据:
{
"status": "running",
"output": "开始修改输入校验。",
"activity": {
"phase": "tool_running",
"tool_name": "edit",
"updated_at": 1791970000000
}
}
原始推理和工具结果没有被塞回 poll。时间戳表示活动更新的时间,也不是持续刷新的心跳。
Codex 日志则确认,这段等待过程中确实发生了多次模型生成。不过,紧邻的一次 poll 和 git diff 位于同一个工具脚本,不能据此把每个工具调用都算成一次模型请求。
这让我更明确地意识到:任务交出去以后,等待方式也影响上层模型的调用次数。
所以加入独立的 pi_wait_agent:默认等待 3 分钟,最长 5 分钟。等待期间由 MCP 程序检查状态,Pi 提问、完成、失败或中断时返回;普通进度变化不会提前叫回上层。
这时 Codex 的运行程序在等工具返回,模型没有持续思考。Pi 仍会调用自己的模型、执行工具。wait 超时只结束这次等待,Pi 继续运行;控制权回到 Codex 后,由它决定下一步。
集成测试中,一次默认等待实际持续约 180 秒,没有因状态检查新增模型请求。这验证了等待机制,还不能直接换算成真实任务的额度节省比例。
最后一步,让别人也能接上
到这里,工具已经适合我的日常使用。要分发,还要补上独立入口、配置说明、跨平台检查和安装后的验收。我把它整理成 sub-pi-agent,同时提供 GitHub 源码、Release 和 npm 包。
工具数量也做了一次收敛:移除原先同步执行的 pi_run_task,统一通过异步派发、等待和读取交付完成任务。这样,新任务与后续修改都沿用同一套会话机制,主 agent 不必再判断两种执行入口之间的差别。
发布前还有一次很实际的返工:MCP 握手正常,客户端断开后,进程却没有正常退出。先前的测试会在超时后杀进程,掩盖了这个问题。补上标准输入结束时的清理,再验证真正的退出,才形成 0.1.1。
发布版本通过了 173 项单元测试、18 项使用真实 MCP/Pi SDK 和本机假模型的集成检查,以及 Windows、Linux、macOS 的 Node 22.19/24 CI。另行验证了公开 npm 包的 npx 启动、11 个工具发现和正常退出。
如果你已经配置好 Pi 的模型与认证,准备 Node.js ≥ 22.19,在 ~/.codex/config.toml 中加入:
[mcp_servers.pi]
command = "npx"
args = ["-y", "sub-pi-agent@0.1.1"]
startup_timeout_sec = 120
tool_timeout_sec = 600
Windows 无法启动 npx 时,可改成 npx.cmd。镜像源尚未同步新包时,在 args 中加入 --registry=https://registry.npmjs.org。工具超时要覆盖最长等待时间;这些配置项也可查阅 Codex MCP 文档。
重新加载 MCP 后,可以直接向 Codex 说明工作方式:
这项任务由你负责规划和审查,把代码修改与验证交给 Pi。先看看有没有可复用的 agent,使用项目的绝对工作目录,不指定 model。有独立工作就继续,没有就 wait;遇到问题,在同一个 ID 上回答并继续。
11 个工具速查
展开内容
| 工具 | 用途 |
|---|---|
pi_spawn_agent | 新建有名字和描述的 agent |
pi_continue_agent | 保留上下文,继续同一 ID |
pi_poll_agent | 读当前进度、活动和问题 |
pi_wait_agent | 有时限地等待交接 |
pi_get_result | 读本轮最终交付 |
pi_list_agents | 查找、筛选可复用的 agent |
pi_steer_agent | 向执行中的 agent 发送纠偏 |
pi_followup_agent | 排队追加后续消息 |
pi_terminate_agent | 中断当前任务,保留会话 |
pi_release_agent | 卸载资源,保留可恢复上下文 |
pi_list_models | 查看 Pi 可用模型 |
Pi 的执行工具使用启动 MCP 的本机用户权限,这个服务没有额外沙箱。派发时要明确项目范围,最后仍由上层检查实际交付。
做完这一轮,我开始更留意 agent 之间的交接:谁负责判断,谁保留上下文,谁等待,什么状态值得上层再次介入。最初只是想让 Codex 少做一些执行工作,最后也借这些具体问题,逐渐看清了两个 agent 怎样才能配合起来。
看不懂,大受震撼🤩