同进程 SDK 最直接,但模型、Tool 与 Extension 崩溃会更接近 UI 进程。RPC Mode 把 Coding Agent 放进子进程:
flowchart LR
UI["Desktop UI"] <-->|"JSONL stdin/stdout"| W["Pi RPC Worker"]
W --> P["Provider"]
W --> T["Tools/Extensions"]
UI -.-> M["Process Monitor"]
M -.-> W
本章只讲协议和桌面集成,不使用 Pi TUI。
1. 启动
pi --mode rpc --provider <name> --model <id>
常用 Process Options:
- CWD;
- Session/No-session;
- Session Directory;
- Provider/Model;
- Project Trust Override;
- Environment Allowlist。
桌面应用必须持有 stdin、stdout、stderr 和 Exit Status。
2. JSONL Framing
Command、Response、Event 都是一行一个 JSON Object,以 LF \n 分隔。
严格规则:
- 只按 LF Split;
- 输入可去掉 CRLF 中末尾的
\r; - JSON String 内的
U+2028/U+2029不是 Record Delimiter; - 不要使用会把 Unicode Separator 当换行的通用 Line Reader。
Protocol Parser 维护 Byte Buffer,直到发现 LF,再 Decode/Parse 一条。
3. 三类消息
stdin: Command
stdout: Response
stdout: Event / Extension UI Request
stdin: Extension UI Response
Command 可带 id。Response 会回显 ID,用于关联并发请求。
Agent Event 没有“属于某个 Prompt Request ID”的统一父 ID;客户端要按 Session 与 Event Payload 建立状态。
4. Prompt 是“接受确认”,不是完成响应
发送:
{"id":"p1","type":"prompt","message":"分析项目"}
响应:
{
"id":"p1",
"type":"response",
"command":"prompt",
"success":true
}
这只表示 Prompt 被接受、排队或由 Extension 处理。之后的模型/Tool 失败通过 Event 与
Message Stream 报告,不会为 p1 再发第二个 Response。
只有实际启动或加入 Agent Run 的 Prompt,桌面 UI 才以 agent_settled 判断这次 Run
稳定结束。若 Prompt 命中 Extension Command,或 input Handler 直接返回 Handled,它会在
Preflight 成功后返回,却不启动 _runAgentPrompt(),因此不会为这次输入产生新的
agent_settled。这类输入要以 Command Response 和 Extension 自己的 Event/State 为准,
不能统一等待 Settled。
5. Command 没有全局串行顺序
Worker 的 LF Reader 收到一行后以 void handleInputLine(line) 启动处理,不会先等待上一行
完成。多个 Command 可以并发执行,Response 也可能乱序返回。
因此:
- 必须用 ID 关联 Response;
- 不能把 stdin 发送顺序当作 Effect 完成顺序;
set_model → prompt、Session Mutation → Query 等有依赖的操作,必须等待前一个 Response;- Prompt Response 仍只表示 Preflight/接受,真正 Agent Run 在后台继续。
6. Streaming 中提交
Worker 正在 Streaming 时,Prompt 必须指定:
{
"type":"prompt",
"message":"先检查配置",
"streamingBehavior":"steer"
}
- Steer:当前 Assistant Turn 及 Tool Calls 后、下一 LLM Call 前送入;
- Follow-up:Agent 停止后处理;
- 不指定:Response Error。
Extension Command 可通过 Prompt 立即执行;独立 steer/follow_up Command 不接受
Extension Command,但会展开 Skill/Template。
7. State 与 Messages
get_state 返回:
- Model/Thinking;
- Streaming/Compacting;
- Queue Modes;
- Session File/ID/Name;
- Auto-compaction;
- Message/Pending Count。
get_messages 返回当前 Conversation Message Array。
State Query 是 Snapshot;实时 UI 仍要消费 Event,不能高频 Poll 代替 Stream。
8. Command Surface
RPC Type 覆盖:
- Prompt/Steer/Follow-up/Abort;
- Model/Thinking;
- Queue Mode;
- Compaction/Retry;
- User Bash;
- Session New/Switch/Fork/Clone/Tree/Entries;
- Export/Stats;
- Commands/Models/Messages 查询。
客户端应按 RpcCommand/RpcResponse 建立 Discriminated Union,而不是发送任意 JSON。
9. Extension UI Bridge
Extension 调用 Confirm 时,Worker 输出:
{
"type":"extension_ui_request",
"id":"ui-7",
"method":"confirm",
"title":"Database write",
"message":"UPDATE ..."
}
桌面 UI 回:
{
"type":"extension_ui_response",
"id":"ui-7",
"confirmed":false
}
Select/Input/Editor 也需要 Response;Notify/Status/Widget/Title 是单向 UI Action。
10. Approval Routing
维护:
uiRequestId → windowId/sessionId/dialog controller
要求:
- Worker Exit 时 Cancel 全部 Pending Dialog;
- Timeout 后拒绝并丢弃迟到 Response;
- Window Close 时返回 Cancel;
- ID 不可跨 Worker Generation 复用;
- Display 的 Arguments 与最终 Backend Policy 绑定。
RPC 只运输 Dialog,不自动提供 Authorization。
11. 使用 RpcClient
TypeScript 子进程客户端可以参考源码中的 rpc-client.ts 与 jsonl.ts Framing 思路;
但 v0.82.0 的内置 RpcClient 不能原样作为完整桌面 UI Bridge:
extension_ui_request被强转为普通AgentSessionEvent;- 没有公开的
extension_ui_response发送 API; - JSON Parse Error 被静默忽略;
stdin.write()不检查false/drain,没有输入背压。
因此生产客户端需要扩展它,或只复用 Framing 思路并按 rpc-types.ts 建立完整对应类型。
无论语言,都要处理:
- Command ID Map;
- Event Listener;
- UI Request;
- Parse Error;
- Child Exit;
- stdin Backpressure。
12. 进程崩溃检测
stateDiagram-v2
[*] --> Starting
Starting --> Running: ready/state succeeds
Running --> Degraded: protocol error
Running --> Exited: child exit
Degraded --> Exited: terminate
Exited --> Recovering: restart policy
Recovering --> Running: rehydrate
Recovering --> Failed: budget exhausted
记录 Exit Code、Signal、最近有效 Protocol Message 和 stderr Tail,但对 Secret 做 Redaction。
13. 崩溃恢复
Restart 后:
- 启动同版本 Worker;
- 恢复相同 CWD/Trust/Environment;
- 打开 Session File;
get_state/get_messagesRehydrate UI;- Reject/Cancel/Clear 旧 Pending Command、UI Dialog 与 Steer/Follow-up Queue;
- 把可能已执行但未确认的动作标为 Indeterminate,通过 Audit/Backend Query 对账;
- 不自动重放不确定是否已执行的 Tool。
这些 Pending 状态在 Worker 内存中,不能从 Session File 或 get_state 重建成“仍在等待”。
模型文字可以重新生成,高风险 Side Effect 不能盲目重试。
14. Exactly-once 并不存在
RPC Command Response 丢失时可能出现:
UI 发送写操作相关 Prompt
Worker 已执行 Tool
Worker 在 Response/Event Flush 前崩溃
UI 不知道是否完成
解决依赖业务 Idempotency Key、Audit Store 与 Backend Query,不是简单重新发送同一个 Prompt。
15. Backpressure
stdout 可能快速产生 Delta/Tool Update。Client 应:
- 持续 Drain Pipe;
- Parse 与 UI Render 解耦;
- Batch Text Delta;
- 限制 Store Update Frequency;
- 不因一个慢 Window 阻塞 Protocol Reader;
- 对巨大 Event 设上限。
stdin Write 也要尊重 Stream Backpressure,不能无界 Buffer Command。
16. stderr 不是协议
stdout 保持 JSONL Protocol。stderr 用于 Log/Diagnostic。
客户端不要把 stderr 行当 RpcResponse,也不要把 stdout 的未知 JSON 直接打印到终端而破坏 Parser。
17. Version 与 Capability
当前 Protocol Type 由 Worker 版本决定。桌面发布应:
- 固定 Worker Version;
- 在协议外验证 Worker Version;
- 对 Unknown Event Forward-compatible;
- 对 Missing Required Command Fail Fast;
- 保留升级迁移测试。
当前 RpcCommand 没有 Version/Capabilities Handshake,get_state 也不返回协议版本。宿主可把
固定 Worker 随 App 一起打包、启动前执行 pi --version,或使用自己的外层握手。Capability
只能依据宿主版本表,或探测必需 Command 后 Fail Fast,不能假设 Pi 协议已经自动协商。
不要让桌面 App 自动连接任意系统 PATH 中不同版本的 pi。
18. 权限隔离
启动 Worker 时减少:
- Environment;
- Filesystem Mount;
- Network;
- OS Capability;
- Executable PATH。
更强结构:
UI Process
→ RPC Agent Worker(无直接高权限)
→ Tool Broker
→ Sandbox Worker
即使 Extension 在 Agent Worker 运行,敏感动作仍必须经过 Broker。
19. SDK 与 RPC 对照
| 维度 | SDK 同进程 | RPC 子进程 |
|---|---|---|
| 类型安全 | 直接 TS | 协议 Schema |
| 延迟 | 较低 | 序列化/IPC |
| Crash | 可能影响 UI | 可独立重启 |
| 权限 | 同进程 | 可进一步限制 |
| 多语言 | 困难 | 容易 |
| Debug | 直接 Stack | 跨进程 Log |
| Session Replacement | Runtime API | Command |
| Extension UI | Binding | UI Request/Response |
20. 何时选 SDK
- Electron/Node Main Process;
- 希望直接注入 Object/Callback;
- 需要最小延迟;
- 已有 Worker Isolation;
- 团队能维护 TypeScript Runtime。
21. 何时选 RPC
- Swift/Kotlin/Rust/Python Desktop;
- Agent Worker 要独立升级/重启;
- Tool/Extension 风险需要 Process Boundary;
- 多 UI Client 共享协议;
- 希望把 Agent 放入 Container。
22. 常见误区
“Prompt Response Success 代表回答完成”
错误,只代表接受。
“Event 都会带 Prompt ID”
错误。Command Response ID 与 Agent Event 是不同关联层。
“readline 一定能正确解析 JSONL”
不一定;必须遵循 LF-only Framing。
“Worker 崩溃后重发 Prompt就安全”
错误。Tool Side Effect 可能已经发生。
“子进程自动最小权限”
错误。权限取决于 Spawn/OS Policy。
23. 本章小结
- RPC 用 LF-only JSONL 在 stdin/stdout 传 Command、Response 与 Event;
- ID 关联 Command Response,不统一关联所有 Agent Event;
- Prompt Response 只确认接受;实际 Agent Run 看 Event/AgentSettled,Handled Input 不统一产生 Settled;
- Command 可并发且 Response 可乱序,有依赖的命令必须等待前一个 Response;
- Extension Dialog 通过 UI Request/Response 桥接;
- 内置 RpcClient 只是基础实现,完整桌面 Client 还要补 UI Response、Parse Error 与输入背压;
- Client 必须处理 Framing、Backpressure、Exit、Pending Request 与外层版本固定;
- Crash Recovery 通过 Session Rehydrate,不盲目重放 Side Effect;
- Exactly-once 依赖业务 Idempotency/Audit;
- Process Boundary 改善 Crash Isolation,权限仍要由 OS/Broker 限制;
- Node/TS 同进程优先 SDK,跨语言与 Worker Isolation 优先 RPC。
24. 自测
- RPC 的三类 stdout 消息是什么?
- Prompt success 为什么不代表完成?
- JSONL 为什么必须按 LF Split?
- Agent Event 是否都有 Command ID?
- Extension Confirm 怎样往返桌面 UI?
- Worker Crash 后为什么不能盲目重发 Prompt?
- 怎样 Rehydrate UI?
- SDK 与 RPC 的核心取舍是什么?
- 为什么连续写入两条 Command 不代表它们会按完成顺序执行?