我平时同时使用 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 怎样才能配合起来。

项目放在 GitHub,可以通过 npm 接入;源码中也保留了测试和配置范例。