返回课程首页

32

使用 RPC 隔离 Agent

设计桌面进程与 Pi 子进程之间的事件协议。

源码基线:Pi v0.82.0 · commit 518855d

同进程 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.tsjsonl.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 后:

  1. 启动同版本 Worker;
  2. 恢复相同 CWD/Trust/Environment;
  3. 打开 Session File;
  4. get_state/get_messages Rehydrate UI;
  5. Reject/Cancel/Clear 旧 Pending Command、UI Dialog 与 Steer/Follow-up Queue;
  6. 把可能已执行但未确认的动作标为 Indeterminate,通过 Audit/Backend Query 对账;
  7. 不自动重放不确定是否已执行的 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. 自测

  1. RPC 的三类 stdout 消息是什么?
  2. Prompt success 为什么不代表完成?
  3. JSONL 为什么必须按 LF Split?
  4. Agent Event 是否都有 Command ID?
  5. Extension Confirm 怎样往返桌面 UI?
  6. Worker Crash 后为什么不能盲目重发 Prompt?
  7. 怎样 Rehydrate UI?
  8. SDK 与 RPC 的核心取舍是什么?
  9. 为什么连续写入两条 Command 不代表它们会按完成顺序执行?