Chapter 15 · RPC Mode and Process Integration
Chapter goals: Understand the architecture and JSONL protocol of RPC mode, master core commands and event streams, handle the extension UI sub-protocol and common errors, and implement a minimal editor integration client.
15.1 Architecture: Host Process + pi Subprocess
RPC mode lets pi run headless: launching pi --mode rpc gives a subprocess communicating via stdin/stdout; any language can drive it. This is the standard posture for IDE plugins, desktop GUIs, and CI orchestrators integrating pi.
pi --mode rpc [options]
# Common options:
# --provider anthropic specify provider
# --model provider/id:high model + thinking level
# --no-session disable session persistence
# -n name session display nameThree protocol rules:
- Commands: JSON objectswritten to the subprocess stdin, one per line;
- Responses: lines with
type: "response"indicate the对应 command's success/failure (optionalidfield for request correlation); - Events: JSON lines持续输出 to stdout during agent execution.
Framing Detail
Strict JSONL: frame only with \n (trailing \r\n tolerated). Node's readline is non-compliant — it also splits on Unicode separators U+2028/U+2029, which can legitimately appear inside JSON strings, causing parsing乱序.
15.2 Core Commands Quick Reference
| Category | Command | Description |
|---|---|---|
| Prompt | prompt / steer / follow_up / abort | Must带 streamingBehavior: "steer"|"followUp" when streaming |
| State | get_state / get_messages / get_entries(with since cursor) / get_tree | entries是追加树, entry id可当持久游标 |
| Model | set_model / cycle_model / get_available_models / set_thinking_level | thinking levels off→max |
| Compaction/Retry | compact / set_auto_compaction / set_auto_retry / abort_retry | |
| Bash | bash / abort_bash | Output streams via bash_execution_update; result enters LLM context only on the next prompt |
| Session | new_session / switch_session / fork / clone / export_html / set_session_name / get_session_stats |
{"id": "req-1", "type": "prompt", "message": "Fix the login bug"}
{"type": "prompt", "message": "Run tests first then改", "streamingBehavior": "steer"}
{"type": "compact", "customInstructions": "重点保留代码改动"}15.3 Event Stream: From Incremental to Terminal State
agent_start ── turn_start ── message_update(text_delta/toolcall_delta...)
── tool_execution_start/update/end ── turn_end ── agent_end
Final: agent_settled (auto-retry/compaction-retry/pending messages all消化完毕)Key semantics:
message_updatecontains only increments (assistantMessageEvent.delta); toassemble a实时画面, accumulate yourself, withmessage_end.messageas the authority;tool_execution_update.partialResult.contentis cumulative output; the client直接整体替换显示即可;agent_endmay be followed by auto-retry — state类 UI请等agent_settled;- Failed responses look like
{"success": false, "error": "..."}; parse failure则command: "parse".
15.4 Extension UI Sub-Protocol
ctx.ui.select()/confirm() in extensions don't消失 in RPC mode; instead they转 to extension_ui_request on stdout, answered by the host with extension_ui_response:
{"type": "extension_ui_request", "id": "u1", "method": "confirm",
"title": "Dangerous command", "message": "Allow rm -rf?", "timeout": 10000}{"type": "extension_ui_response", "id": "u1", "confirmed": true}- Dialog类 (select/confirm/input/editor) must回包, else block (with timeout auto-resolve to default);
- Notification类 (notify/setStatus/setWidget/setTitle/set_editor_text) fire-and-forget,无需回应;
- TUI-exclusive capabilities degrade in RPC:
custom()returnsundefined, theme API returns empty, etc. Note:ctx.hasUI === truein this mode; extensions判断要用ctx.mode === "tui".
15.5 Walkthrough: Minimal Editor Integration (Node Client)
// rpc-client-mini.ts — Usage: node rpc-client-mini.ts "Fix the login bug"
import { spawn } from "node:child_process";
const agent = spawn("pi", ["--mode", "rpc", "--no-session"]);
// Compliant JSONL reader: frames only on \n, strips trailing \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") {
// Minimal strategy: auto-reject all confirms, pick第一项 for selects
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(`[command failed] ${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[done] agent is fully idle");
agent.kill();
}
});
agent.stdin.write(JSON.stringify({
id: "req-1", type: "prompt", message: process.argv[2] ?? "Hello",
}) + "\n");
process.on("SIGINT", () => {
agent.stdin.write(JSON.stringify({ type: "abort" }) + "\n");
});A real editor plugin只需在此骨架上补三件事: wire text_delta into the editor's output panel, map extension_ui_request to native dialogs, and use get_entries with since cursor to sync历史 after重启.
Chapter Summary
- RPC = headless pi subprocess + strict JSONL protocol on stdin/stdout (only
\n, readline has坑); - Commands cover prompt/model/compaction/session/bash全套; must declare steer/followUp for streaming prompts;
- Events are incremental streams: assemble靠 contentIndex/message_end; wait
agent_settledfor UI final state; - Extension UI转为 request/response sub-protocol; dialog类 must应答; notification类即发即忘;
- Node same-process优先 SDK; cross-language or needing isolation → RPC is the官方推荐 path.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. Why does the official say Node's readline cannot be used to parse RPC output?
2. Sending a prompt命令 without streamingBehavior while the agent is already streaming results in?
3. Regarding when bash RPC command results enter the LLM context, which is correct?
4. What happens when an extension calls ctx.ui.custom() in RPC mode? How should the extension判断 the environment?
🛠️ Hands-on Practice
- Rewrite the 15.5 minimal client in Python
subprocess(the official doc provides an isomorphic example); compare how the两种 languages handle JSONL framing. - Add
get_statepolling to the client: printisStreamingandcontextUsage.percentevery 2 seconds; observe the full lifecycle of a multi-tool task. - After installing the test-guard extension from Chapter 11, start pi in RPC mode; trigger its confirm dialog;补上 your client with真正的 interactive应答 logic.
This batch ends here. Subsequent chapters继续深入 Print/JSON automation and team workflows.