- Published on
Pi 源码解读(七):执行过程如何变成界面上的文字和状态?
- Authors

- Name
- Stone
本篇目录
假设你不想在终端里使用 Pi,而是想做一个 Vue 聊天页面:能逐字显示回答,也能看到“正在读文件”“修改失败”和“处理完成”。
你需要重写 Agent Loop 吗?不需要先从这里开始。你首先需要读懂 Pi 发出的事件。
NOTE
运行时报告发生了什么,界面决定怎么显示。
但“收到请求”“一条消息结束”“这次运行彻底收尾”,是三个不同的状态。
源码基准:Pi v0.84.1。本文分析真实的事件与 RPC 实现;Vue 页面是教学接入方案,不是 Pi 已经内置的页面。
一、别把“接口成功”当成“任务完成”
你在网页里发送:
把 App.vue 的页面标题改成你好。
假如只做一个普通请求,收到成功响应就关掉加载动画,可能会过早显示“已完成”。
Pi 的 RPC 文档明确区分两件事:prompt 的成功响应表示输入已被接受、排队或立即处理;后续运行进度与失败,继续通过事件报告。这张“受理回执”,不是任务做完的证明。1
这里的 RPC,可以先理解成“其他程序用消息指挥 Pi”。本课版本通过标准输入接收 JSON 命令,通过标准输出发送 JSON 行;它不是一个默认就能被浏览器直连的 HTTP 服务。2
一种可行的接入设计是:Vue 页面 ↔ 你自己的后端 ↔ Pi 进程。 后端负责转发命令与事件,也负责鉴权和运行环境;不能把浏览器界面直接当成本地文件执行权限。
二、先认识几类事件,就能画出界面状态
继续沿用修改标题的任务。Pi 可能先输出一句说明,再读取文件、修改文件,最后回答用户。
| 事件 | 含义 | 界面可以怎么处理 |
|---|---|---|
message_update | 助手消息正在更新,内部可能是文字、思考或工具请求的增量。 | 遇到文字增量,追加到对应消息。 |
message_end | 一条消息完成,不等于整个任务完成。 | 用最终内容校准这条消息。 |
tool_execution_start / update / end | 一个工具请求进入处理、提供进度、产生最终处理结果。 | 按调用编号更新工具卡片,检查结果是否出错。 |
agent_end | 一次底层 Agent 运行结束,应用层可能还要继续处理。 | 不要仅凭它宣布整个会话任务完成。 |
agent_settled | 会话级运行已收尾,没有剩余的自动重试、压缩重试或排队续跑。 | 结束这次忙碌状态,再根据结果显示成功、失败或取消。 |
前面的消息和工具事件来自通用 Agent;agent_settled 是 Coding Agent 会话层补充的事件。它们不是同一层抽象。34
还有一个源码细节:tool_execution_start 在执行前检查之前就会发出。因此,它不证明工具函数已经真正运行;请求可能随后被扩展拦截。界面可以显示“正在处理工具请求”,但不能据此记录“文件已经被修改”。5
三、Pi 真的是怎样把事件传出去的?
先看进程内部:Agent Loop 接收模型流式输出,把文字和工具请求的变化包装成 message_update,交给事件处理链路。完成一条响应时,再发送 message_end。6
再看 RPC 出口:runRpcMode 会订阅 Session,将事件交给 toJsonEvent,然后序列化成 JSON 行写到标准输出。不是去识别终端上打印的“正在读取……”文字,而是接收明确的数据事件。7
这里有一个很具体的 Pi 设计:RPC 的消息更新会去掉累计的消息快照,主要保留增量。 message_start 提供开始状态,中间的增量拼出内容,message_end 提供最终权威消息。这个转换就在 json-event.ts,不是所有接入方式拿到的字段都完全一样。8
例如界面先收到“我先”,再收到“读取文件”,就拼成“我先读取文件”。如果把每次增量都当成整条消息替换,就可能只剩最后几个字;反过来,把累计快照反复追加,又会造成重复。
先确认拿到的是增量还是快照,再决定“追加”还是“替换”。
四、为什么 agent_end 后还不能马上当成空闲?
因为底层运行结束后,会话层还可能需要处理可重试错误、上下文压缩,以及结束处理器新加入的消息。
源码中,AgentSession._runAgentPrompt 等待底层 Agent 后,会通过 _handlePostAgentRun 检查是否需要继续;全部处理完,才调用 _emitAgentSettled。因此,agent_end 更像一个阶段的结束,agent_settled 才是会话层本次运行的收尾通知。9
还有一层区别:底层 Agent.waitForIdle() 会等当前运行以及被等待的事件监听器完成,而不是一发出 agent_end 就立即认为收尾结束。这里说的是程序内部的等待关系,不是保证远端网页已经渲染完成。1011
下面是一个简化的网页处理思路,不是 Pi 的原始实现:
发送需求,等待受理结果
受理成功:显示已受理,继续监听事件
收到消息开始:建立消息
收到文字增量:追加到对应位置
收到消息结束:用最终消息校准内容
收到工具事件:按调用编号更新状态和错误
收到 agent_end:不要立即停止监听
收到 agent_settled:结束忙碌状态,再展示实际结果
五、界面上的“结束”,也需要保留证据边界
工具卡片结束,不一定是成功。 检查 isError 和返回内容,不能把所有 tool_execution_end 都画成绿色对勾。12
会话收尾,也不证明业务目标达成。 Pi 的 _runAgentPrompt 在收尾路径里发出 agent_settled;它表达的是生命周期,而不是“测试全部通过”这类业务结论。13
连接断开,更不应自动显示成功。 这是接入设计要处理的情况:如果只是后端连接或 Pi 进程意外中断,界面应保留“中断或状态未知”,而不是替运行时编造完成结果。
对照 Codex:相似的问题,不同的协议
Codex App Server 的官方协议也区分文本增量、工作项结束和轮次结束,例如 item/agentMessage/delta、item/completed 和 turn/completed;结束通知还要结合 turn.status 区分完成、中断和失败。14
可以对照它们如何支持“边做边显示”,但不要把 Codex 的事件名、Turn 含义直接套到 Pi 上。这里比较的是官方公开接入协议,不是在声称两者内部代码相同。对照资料查阅于 2026-09-25。
六、回到源码和面试
第一遍只跟这条线:模型增量 → Agent 事件 → Session 订阅 → JSON 转换 → 客户端显示。
需要定位时,再展开三个入口
agent-loop.ts 的 streamAssistantResponse:看模型输出怎样变成消息事件。
json-event.ts 的 toJsonEvent:看 RPC 怎样移除累计快照。
rpc-mode.ts 的 runRpcMode:看订阅与输出怎样连接。
需要验证收尾,再读 agent-session.ts 的 _runAgentPrompt。
面试时,可以这样讲
我跟过 Pi 从模型流式输出到 RPC 的事件链路。运行时产生消息和工具事件,RPC 订阅 Session,再把事件转换成 JSON 发出去,所以界面不需要自己实现 Agent Loop。我特别注意了两个细节:RPC 的文字更新使用增量,不能按完整快照处理;agent_end 后也可能继续重试,要区分底层结束和会话级的 agent_settled。
一个不用写代码的小练习
网页收到 prompt 的成功响应,随后收到 agent_end,但还没收到 agent_settled。此时能否显示“代码已全部修改并验证通过”?
想过之后再看答案
不能。成功响应只表示输入被受理;agent_end 也不代表会话层已经没有后续处理。即使之后收到 agent_settled,仍要根据真实工具结果和测试证据判断任务是否成功。
这一课记住:用事件驱动界面,但不要把消息完成、运行收尾和业务成功当成同一件事。
参考源码
Footnotes
Pi v0.84.1 ·
packages/coding-agent/src/modes/rpc/rpc-mode.ts,第 47–58 行 ↩Pi v0.84.1 ·
packages/coding-agent/src/core/agent-session.ts,第 135–150 行 ↩Pi v0.84.1 ·
packages/coding-agent/src/modes/rpc/rpc-mode.ts,第 330–340 行 ↩Pi v0.84.1 ·
packages/coding-agent/src/modes/json-event.ts↩Pi v0.84.1 ·
packages/coding-agent/src/core/agent-session.ts,第 995–1035 行 ↩Pi v0.84.1 ·
packages/coding-agent/src/core/agent-session.ts,第 995–1006 行 ↩
