第 15 章 · RPC 模式与进程集成
本章目标:理解 RPC 模式的架构与 JSONL 协议,掌握核心命令与事件流,处理扩展 UI 子协议与常见错误,并实现一个极简的编辑器集成客户端。
15.1 架构:宿主进程 + pi 子进程
RPC 模式让 pi 无头运行:启动 pi --mode rpc 得到一个通过 stdin/stdout 通信的子进程,任何语言都能驱动它。这是 IDE 插件、桌面 GUI、CI 编排器集成 pi 的标准姿势。
pi --mode rpc [options]
# 常用选项:
# --provider anthropic 指定 provider
# --model provider/id:high 模型 + 思考级别
# --no-session 关闭会话持久化
# -n 名称 会话显示名协议三条规则:
- 命令:JSON 对象写入子进程 stdin,每行一个;
- 响应:带
type: "response"的行,表示对应命令成败(可选id字段做请求关联); - 事件:agent 运行过程中持续输出到 stdout 的 JSON 行。
分帧细节
严格 JSONL:只用 \n 分帧(容忍结尾 \r\n)。Node 的 readline 不合规——它还会按 Unicode 分隔符 U+2028/U+2029 切行,而这两字符可以合法出现在 JSON 字符串里,会导致解析错乱。
15.2 核心命令速查
| 类别 | 命令 | 说明 |
|---|---|---|
| 提问 | prompt / steer / follow_up / abort | 流式中 prompt 必须带 streamingBehavior: "steer"|"followUp" |
| 状态 | get_state / get_messages / get_entries(可传 since 游标) / get_tree | entries 是追加树,entry id 可当持久游标 |
| 模型 | set_model / cycle_model / get_available_models / set_thinking_level | thinking 级别 off→max |
| 压缩/重试 | compact / set_auto_compaction / set_auto_retry / abort_retry | |
| Bash | bash / abort_bash | 输出经 bash_execution_update 流式返回;结果在下一次 prompt 时才进入 LLM 上下文 |
| 会话 | new_session / switch_session / fork / clone / export_html / set_session_name / get_session_stats |
{"id": "req-1", "type": "prompt", "message": "修复登录 bug"}
{"type": "prompt", "message": "先跑测试再改", "streamingBehavior": "steer"}
{"type": "compact", "customInstructions": "重点保留代码改动"}15.3 事件流:从增量到终态
agent_start ── turn_start ── message_update(text_delta/toolcall_delta...)
── tool_execution_start/update/end ── turn_end ── agent_end
最终:agent_settled(自动重试/压缩重试/排队消息全部消化完毕)关键语义:
message_update只含增量(assistantMessageEvent.delta),要拼实时画面就自己累积,以message_end.message为准;tool_execution_update.partialResult.content是累计输出,客户端直接整体替换显示即可;agent_end可能后面还跟着自动重试——状态类 UI 请等agent_settled;- 失败响应形如
{"success": false, "error": "..."},解析失败则command: "parse"。
15.4 扩展 UI 子协议
扩展里的 ctx.ui.select()/confirm() 在 RPC 模式下不会消失,而是转成 stdout 上的 extension_ui_request,由宿主回答 extension_ui_response:
{"type": "extension_ui_request", "id": "u1", "method": "confirm",
"title": "危险命令", "message": "允许 rm -rf 吗?", "timeout": 10000}{"type": "extension_ui_response", "id": "u1", "confirmed": true}- 对话类(select/confirm/input/editor)必须回包,否则阻塞(带 timeout 则到点自动按默认值解决);
- 通知类(notify/setStatus/setWidget/setTitle/set_editor_text)发完即忘,无需回应;
- TUI 专属能力在 RPC 下降级:
custom()返回undefined、主题 API 返回空等。注意此模式下ctx.hasUI === true,扩展判断要用ctx.mode === "tui"。
15.5 实战:极简编辑器集成(Node 客户端)
// rpc-client-mini.ts —— 用法:node rpc-client-mini.ts "修复登录 bug"
import { spawn } from "node:child_process";
const agent = spawn("pi", ["--mode", "rpc", "--no-session"]);
// 合规的 JSONL 读取器:只按 \n 分帧、剥掉 \r
function attachReader(stream: NodeJS.ReadableStream, onLine: (l: string) => void) {
let buf = "";
stream.on("data", (chunk: string) => {
buf += chunk;
let i: number;
while ((i = buf.indexOf("\n")) !== -1) {
let line = buf.slice(0, i);
buf = buf.slice(i + 1);
if (line.endsWith("\r")) line = line.slice(0, -1);
if (line) onLine(line);
}
});
}
attachReader(agent.stdout, (line) => {
const evt = JSON.parse(line);
if (evt.type === "extension_ui_request") {
// 极简策略:所有确认自动拒绝、选择选第一项
const resp = evt.method === "confirm"
? { type: "extension_ui_response", id: evt.id, confirmed: false }
: { type: "extension_ui_response", id: evt.id, value: evt.options?.[0] ?? "" };
agent.stdin.write(JSON.stringify(resp) + "\n");
return;
}
if (evt.type === "response" && !evt.success) {
console.error(`[命令失败] ${evt.command}: ${evt.error}`);
}
if (evt.type === "message_update" &&
evt.assistantMessageEvent?.type === "text_delta") {
process.stdout.write(evt.assistantMessageEvent.delta);
}
if (evt.type === "agent_settled") {
console.log("\n[完成] agent 已完全空闲");
agent.kill();
}
});
agent.stdin.write(JSON.stringify({
id: "req-1", type: "prompt", message: process.argv[2] ?? "你好",
}) + "\n");
process.on("SIGINT", () => {
agent.stdin.write(JSON.stringify({ type: "abort" }) + "\n");
});真实编辑器插件只需在此骨架上补三件事:把 text_delta 接进编辑器的输出面板、把 extension_ui_request 映射成原生对话框、用 get_entries 的 since 游标在重启后同步历史。
本章小结
- RPC = 无头 pi 子进程 + stdin/stdout 上的严格 JSONL 协议(只认
\n,readline 有坑); - 命令覆盖提问/模型/压缩/会话/bash 全套;流式中 prompt 必须声明 steer/followUp;
- 事件是增量流:拼装靠 contentIndex/message_end,UI 终态等
agent_settled; - 扩展 UI 转为 request/response 子协议,对话类必须应答,通知类即发即忘;
- Node 同进程优先 SDK;跨语言、需隔离时 RPC 是官方推荐路径。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. 为什么官方说 Node 的 readline 不能用于解析 RPC 输出?
2. agent 正在流式执行时发送不带 streamingBehavior 的 prompt 命令,结果是?
3. 关于 bash RPC 命令的结果何时进入 LLM 上下文,正确的是?
4. RPC 模式下扩展调用 ctx.ui.custom() 会怎样?扩展应如何判断环境?
🛠️ 动手实践
- 用 Python
subprocess重写 15.5 的最小客户端(官方文档提供了同构示例),对比两种语言处理 JSONL 分帧的差异。 - 给客户端增加
get_state轮询:每 2 秒打印一次isStreaming与contextUsage.percent,观察一次多工具任务的完整生命周期。 - 安装第 11 章的 test-guard 扩展后在 RPC 模式启动 pi,触发它的 confirm 对话框,为你的客户端补上真正的交互式应答逻辑。
本批次到此结束。后续章节将继续深入 Print/JSON 自动化与团队工作流。