Skip to content

第 15 章 · RPC 模式与进程集成

本章目标:理解 RPC 模式的架构与 JSONL 协议,掌握核心命令与事件流,处理扩展 UI 子协议与常见错误,并实现一个极简的编辑器集成客户端。

15.1 架构:宿主进程 + pi 子进程

RPC 模式让 pi 无头运行:启动 pi --mode rpc 得到一个通过 stdin/stdout 通信的子进程,任何语言都能驱动它。这是 IDE 插件、桌面 GUI、CI 编排器集成 pi 的标准姿势。

bash
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_treeentries 是追加树,entry id 可当持久游标
模型set_model / cycle_model / get_available_models / set_thinking_levelthinking 级别 off→max
压缩/重试compact / set_auto_compaction / set_auto_retry / abort_retry
Bashbash / abort_bash输出经 bash_execution_update 流式返回;结果在下一次 prompt 时才进入 LLM 上下文
会话new_session / switch_session / fork / clone / export_html / set_session_name / get_session_stats
json
{"id": "req-1", "type": "prompt", "message": "修复登录 bug"}
{"type": "prompt", "message": "先跑测试再改", "streamingBehavior": "steer"}
{"type": "compact", "customInstructions": "重点保留代码改动"}

15.3 事件流:从增量到终态

text
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

json
{"type": "extension_ui_request", "id": "u1", "method": "confirm",
 "title": "危险命令", "message": "允许 rm -rf 吗?", "timeout": 10000}
json
{"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 客户端)

typescript
// 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_entriessince 游标在重启后同步历史。

本章小结

  • 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() 会怎样?扩展应如何判断环境?

🛠️ 动手实践

  1. 用 Python subprocess 重写 15.5 的最小客户端(官方文档提供了同构示例),对比两种语言处理 JSONL 分帧的差异。
  2. 给客户端增加 get_state 轮询:每 2 秒打印一次 isStreamingcontextUsage.percent,观察一次多工具任务的完整生命周期。
  3. 安装第 11 章的 test-guard 扩展后在 RPC 模式启动 pi,触发它的 confirm 对话框,为你的客户端补上真正的交互式应答逻辑。

本批次到此结束。后续章节将继续深入 Print/JSON 自动化与团队工作流。