一个 pi 命令,四种运行方式
沿源码还原 interactive、print、JSON、RPC 四个 AppMode 的判定优先级、I/O 合同和最终分派,解释它们为何共用会话却不能混为一种协议。
pi 没有四个 binary。四种运行方式来自同一次参数解析后的 AppMode:interactive、print、json、rpc。它们共享前面的会话组装,却有不同的输入来源、输出格式和退出条件。
源码里有两个容易混淆的类型。CLI 的 Mode 只有 text | json | rpc,另有独立的 print 布尔参数;内部 AppMode 才包含四个值。因此 --mode interactive 和 --mode print 都不是合法写法。
模式不是只由参数决定
resolveAppMode() 的优先级很具体:显式 RPC 最高,其次是 JSON;设置 -p,或者 stdin/stdout 任一端不是 TTY,进入 print;剩下才是 interactive。这也留下一个反直觉细节:在正常双 TTY 终端里,--mode text 不会强制 print,因为它既不命中 RPC/JSON,也没有设置 parsed.print。需要一次性文本输出时,应使用 -p。
| AppMode | 常见入口 | stdin 的含义 | stdout 的主要合同 | 生命周期 |
|---|---|---|---|---|
interactive | 双 TTY 下直接运行 pi | 终端按键与编辑器输入 | TUI 差分渲染 | 持续到用户退出 |
print | pi -p "..." 或管道输入 | 可作为一次性 prompt | 最后一个 assistant 文本 | prompt 完成后退出 |
json | pi --mode json "..." | 可作为一次性 prompt | session header 与逐事件 JSONL | prompt 完成后退出 |
rpc | pi --mode rpc | 一行一条 JSON-RPC 命令 | response、event 与 UI request | 等待 shutdown 或 EOF |
非 RPC 模式会读取管道内容;如果原本判为 interactive,却真的读到了 stdin,代码会把它改为 print。RPC 特意跳过这一步,因为 stdin 已经属于协议,不能先被当成普通 prompt 消耗。
JSON 和 RPC 的差别不在花括号
json 与 print 最终都进入 runPrintMode()。JSON 分支订阅会话事件并逐行输出,文本分支只取最后一个 assistant 消息的文本内容。两者都会处理给定 prompt,随后清理 runtime 并返回退出码。
RPC 是另一份 runner。它从 stdin 接收带 type 和可选关联 id 的 JSON 对象,向 stdout 发送 response、event 和扩展 UI request;函数签名返回 Promise<never>,说明正常路径不是“一次 prompt 后返回”。
最终分派也把这个边界写得很直白:RPC 进入 runRpcMode,interactive 构造 InteractiveMode,其余两个 AppMode 一起进入 runPrintMode。
下面的命令只展示固定 tag 上的模式判定和最终分支,适合核对表格,不会启动 Pi:
repo="${PI_SOURCE_DIR:-/tmp/pi-handbook-qbQTcA}"
git -C "$repo" show v0.83.0:packages/coding-agent/src/main.ts |
nl -ba | sed -n '109,124p;818,825p;868,914p'

固定源码中的实际分支只有三种 runner:RPC 进入 runRpcMode,interactive 构造
InteractiveMode,text 与 JSON 共用 runPrintMode
。这张图只定位分派点,不会启动模型请求,也不代表三种 runner 的输出协议相同。
四种模式到这里已经分开,但它们为什么能共用同一个会话?答案藏在 I/O runner 之前的启动阶段。下一章沿 main() 继续往下,看看 runtime 出现以前到底组装了什么。