08. Pi 子进程桥
Pi 路径由两个组件组成:主进程里的 PiAgent 与独立可执行的 pi-agent-server。两者通过 stdin/stdout JSONL 通讯;权限、MCP 连接和产品状态留在父进程,Pi SDK session/agent loop 留在子进程。
主文件:
8.1 为什么要多一个进程
Section titled “8.1 为什么要多一个进程”子进程顶部注释给出直接动机:Pi SDK 是 ESM、依赖较重、打包复杂,隔离能避免 Electron main bundle 冲突。更深层收益还有:
- 故障隔离:Pi runtime 崩溃不直接击穿 SessionManager。
- 版本/模块隔离:可以单独构建
dist/index.js与 runtime resolver。 - 强制终止:abort 无效时父进程可 kill child。
- stdout 协议纪律:只有 JSONL,调试走 stderr,避免 parser 被日志污染。
- 秘密边界:MCP credential/client 留父进程,子进程只拿必要 auth 与 proxy 调用。
代价是要实现一套小型双向 RPC、correlation map、超时、崩溃恢复和协议兼容。
8.2 进程拓扑
Section titled “8.2 进程拓扑”flowchart LR SM["SessionManager"] --> PA["PiAgent\nparent adapter"] PA -->|"stdin JSONL"| PS["pi-agent-server"] PS -->|"stdout JSONL"| PA PS -->|"stderr logs"| LOG["debug ring/file"] PS --> PI["Pi AgentSession"] PA --> CHECK["central pre-tool checks"] PA --> MCP["McpClientPool"] PA --> CB["session/browser callbacks"]PiAgent 仍继承 BaseAgent,所以 SessionManager 看见的接口与 Claude 完全相同。
8.3 JSONL 协议
Section titled “8.3 JSONL 协议”子进程定义 inbound union 于 index.ts,outbound union 于 index.ts。
Parent → subprocess
Section titled “Parent → subprocess”| 消息 | 作用 |
|---|---|
init |
workspace/session/model/auth/system/runtime 初始配置 |
prompt |
用户消息、图片、system prompt、turn 配置 |
register_tools |
完整/增量 proxy tool 定义 |
pre_tool_use_response |
中央检查后的 allow/block/modified input |
tool_execute_response |
MCP/session proxy 执行结果 |
abort |
请求停止当前生成 |
steer |
原生中途转向 |
mini_completion |
小模型文本调用 |
llm_query |
完整 call_llm 请求 |
ensure_session_ready |
branch preflight/拿 session id |
set_model / set_thinking_level |
轻量运行时更新 |
update_runtime_config |
endpoint/provider/model registry 更新 |
compact / set_auto_compaction |
手动/自动压缩控制 |
token_update |
OAuth token 刷新后热替换 |
shutdown |
释放并退出 |
Subprocess → parent
Section titled “Subprocess → parent”| 消息 | 作用 |
|---|---|
ready |
init 完成,包含 session/runtime 信息 |
event |
Pi SDK event,父进程再适配为 AgentEvent |
pre_tool_use_request |
请求中央安全管道决策 |
tool_execute_request |
执行父进程拥有的 proxy tool |
session_tool_completed |
session tool 的产品侧完成信号 |
mini_completion_result / llm_query_result |
correlation response |
ensure_session_ready_result |
branch preflight 结果 |
compact_result / toggle/update result |
管理命令返回 |
session_id |
provider session ID 更新 |
error |
transport/runtime error |
每个 request/response 类型带 request id;父子分别维护 pending maps。
8.4 Spawn 主链
Section titled “8.4 Spawn 主链”PiAgent.ensureSubprocess() 位于 pi-agent.ts,实际 spawn 从约 425 开始:
resolve packaged/dev pi-agent-server path→ resolve Node/Bun runtime→ 组 env(provider auth、debug、session vars)→ 可选 --require network interceptor bundle→ spawn child with pipes→ stdout readline(JSONL)→ stderr 捕获但绝不回 stdout→ exit/error handler→ send init→ wait ready with timeout→ register toolsInterceptor
Section titled “Interceptor”统一网络 interceptor 只注入 Pi subprocess 路径,用于兼容 endpoint/SSE/tool-call 修整等。它必须在模块加载前通过 runtime preload 生效,不能等进程启动后 import。
Ready barrier
Section titled “Ready barrier”父进程不能 spawn 后马上发 prompt;要等待 ready。否则 init/model registry/auth storage 尚未建立,会出现竞态。ensureSubprocess() 把并发调用合并到同一个启动 Promise,避免 title generation 和 chat 同时 spawn 两个 child。
8.5 stdout 必须纯净
Section titled “8.5 stdout 必须纯净”子进程 send() 只做一行 JSON + newline;所有 debug 写 stderr。原因很朴素:任意第三方库 console.log 都会变成无法解析的“协议帧”。
父进程仍应容错:
- 忽略空行;
- JSON parse 错误记录原始片段;
- stderr 保存最近内容,child 异常退出时附到诊断;
- 单个坏帧不把所有 pending promise 永久悬挂。
8.6 子进程 ensureSession()
Section titled “8.6 子进程 ensureSession()”入口:pi-agent-server/index.ts。它负责:
- 初始化 Pi auth/model registry;
- resolve model/custom endpoint;
- 创建 Pi built-in Read/Write/Edit/Bash/Glob/Grep 等工具;
- 加入 web search/fetch;
- 为父进程 proxy tools 构造 wrapper;
- 设置 session manager/continue/fork;
- 应用 thinking、auto-compaction、system prompt override;
- 订阅
AgentSessionEvent。
Pi SDK 在 session.prompt() 时可能重建 system prompt,因此代码使用专门 override 机制,而不是只修改一次 state.systemPrompt。
8.7 Prompt 时序
Section titled “8.7 Prompt 时序”Parent PiAgent.chatImpl() 位于 pi-agent.ts。简化流程:
sequenceDiagram participant SM as SessionManager participant PA as PiAgent participant PS as pi-agent-server participant PI as Pi SDK
SM->>PA: chat(message, attachments) PA->>PA: ensureSubprocess + context split PA->>PS: prompt(systemPrompt, message, images) PS->>PS: waitForCompaction PS->>PI: session.prompt(..., followUp) PI-->>PS: AgentSessionEvent* PS-->>PA: event JSONL* PA-->>SM: AgentEvent* PI-->>PS: agent_end PS-->>PA: event(agent_end) PA-->>SM: complete若 session 已 streaming,子进程用 streamingBehavior: 'followUp',避免 Pi SDK 抛“already processing”。Craft 的外层 queue/steer 仍由 SessionManager 决定,followUp 是 SDK 内部安全调用方式。
8.8 Parent EventQueue
Section titled “8.8 Parent EventQueue”stdout reader 是长期存在的,chat() generator 却按 turn 存在。PiAgent 用 EventQueue 把进程事件送给当前 generator:
event进入队列;- generator await 下一项;
agent_end/terminal error 关闭本 turn;- source activation 等 callback 可能触发额外事件;
- child exit 会以 error 终止所有 pending。
不能直接在 stdout callback 中 yield,EventQueue 是 callback world 到 async iterator world 的桥。
8.9 Event adapter
Section titled “8.9 Event adapter”Pi 子进程把较原生的 AgentSessionEvent 发给父进程,父进程适配:
- message update/delta →
text_delta; - assistant end →
text_complete; - tool execution start/end → tool events;
- agent end → complete/usage;
- compaction/retry 状态 → info/status;
- provider entry id →
pi_turn_anchor。
父进程在 pre-tool request 时捕获的 intent/display metadata 会在后续 tool event 上补回,再交 SessionManager/UI。
8.10 双向工具调用
Section titled “8.10 双向工具调用”Pi 内置工具可在 subprocess 执行,但 source/session/browser 工具由 parent 拥有。
sequenceDiagram participant PI as Pi tool wrapper participant PS as Subprocess participant PA as PiAgent parent participant Pool as MCP/session/browser
PI->>PS: execute(proxyName, args) PS-->>PA: tool_execute_request(id, name, args) PA->>Pool: execute Pool-->>PA: content/isError PA-->>PS: tool_execute_response(id, result) PS-->>PI: tool result父进程的 pending map 把 requestId 对应到 resolver。Abort/shutdown 必须 resolve/reject所有 pending,否则 Pi SDK tool promise 永久挂起。
8.11 PreToolUse 往返
Section titled “8.11 PreToolUse 往返”sequenceDiagram participant PI as Pi tool participant PS as Subprocess hook participant PA as PiAgent participant C as Central checks / UI
PI->>PS: before execute PS-->>PA: pre_tool_use_request(id, tool, input) PA->>C: runPreToolUseChecks C-->>PA: allow/block/modified input PA-->>PS: pre_tool_use_response(id,...) PS-->>PI: execute or deny这保证 safe/ask/allow-all 与 Claude 一致。对 parent proxy tools,某些检查可在父进程直接认定已由 proxy handler保护,但不能绕过需要的 mutation 分类。
8.12 工具热更新
Section titled “8.12 工具热更新”register_tools 把新定义按 name merge。若 Pi session 已存在,不会在 generation 中立即 dispose;只设置 toolsChanged = true,下一次 prompt 前重建 session。
原因:Pi SDK 的 tool set 在 AgentSession 构造时确定,中途更换容易破坏当前 tool call;延迟到 turn boundary 是安全切点。
8.13 原生 steer
Section titled “8.13 原生 steer”PiAgent.redirect() 发送 {type:'steer', message} 并返回 true;子进程调用 piSession.steer(message),见 processMessage。
事件继续从同一个 generator 流出,因此 SessionManager 不应再 abort + 新建 turn。Pi 默认 midstream behavior 为 steer,正是利用这个原生能力。
如果 child/session 尚未 ready,父进程必须返回失败或发 steer_undelivered,不能把“写进 stdin”当作已被模型接收。
8.14 Branch
Section titled “8.14 Branch”Craft 保存每个 assistant message 对应的 Pi entry ID/turn anchor。创建 branch 时:
- 复制 cutoff 前 Craft messages;
- 复制 parent anchor sidecar;
- init 告知 parent session 路径/ID/anchor;
- 子进程的 Pi SessionManager 调 native branch;
ensure_session_ready返回新 session id;- 上层成功后才 announce child。
PiAgent.ensureBranchReady() 位于 pi-agent.ts。
Provider entry ID 是硬截止的依据,不能用 Craft message timestamp 猜。
8.15 Compaction
Section titled “8.15 Compaction”Pi SDK 自己在 overflow 时 _runAutoCompaction 并继续一次。Wrapper 不再平行调用 session.compact(),因为两个 AbortController/compaction 状态会竞态。
手动 /compact
Section titled “手动 /compact”Parent 识别 slash command,发 compact request。子进程:
ensureSession→ waitForCompaction(等待自动压缩排空,带 timeout)→ session.compact(customInstructions)→ 返回 summary / firstKeptEntryId / tokensBefore源码:handleCompact。
压缩必须串行化;它会修改 session entry graph,和 prompt/abort 并发会损坏 SDK 内部状态。
8.16 Token refresh
Section titled “8.16 Token refresh”OAuth token 可能在 parent credential manager 刷新。token_update 将新 credential 写入子进程内的 Pi auth storage,并同步 init config;无需销毁整个 Craft session。
如果某 provider 的 auth client 在构造时捕获 token 而不重新读取 storage,则仍可能要求 runtime restart;热更新能力要以真实 SDK 行为为准。
8.17 Runtime update
Section titled “8.17 Runtime update”update_runtime_config 合并 model/provider/auth/base URL/custom models:
- custom endpoint 可动态注册未知 model;
- active session 通过
setModel()切换; - interceptor 同步 API hints;
- 无 active session 时只保存,下一次构造应用;
- 失败返回
{success:false, updated:false}给 parent 决定重建。
8.18 call_llm 与 speculative prefetch
Section titled “8.18 call_llm 与 speculative prefetch”完整 LLMQueryRequest 通过协议原样传递,子进程注释强调“传递字段”和“queryLlm 真正使用字段”是两个独立不变量。
对无副作用/只读 call_llm,代码可提前 speculative prefetch,等正式 tool call 时命中 cache,减少串行 latency。Cache 必须:
- 只用于确定性足够的匹配 key;
- abort/shutdown 清理;
- mutation tool 禁用;
- 未消费结果不能影响主状态。
8.19 崩溃与清理
Section titled “8.19 崩溃与清理”Parent destroy
Section titled “Parent destroy”- 发 shutdown;
- 短暂等待 graceful exit;
- 超时 kill;
- 关 readline/stdin;
- reject pending maps;
- 清 EventQueue 和 callback。
Subprocess shutdown
Section titled “Subprocess shutdown”- unsubscribe Pi events;
- dispose session;
- stop callback server;
- pending pre-tool 全 block;
- pending tool execution 返回 error;
- exit 0。
非预期 exit
Section titled “非预期 exit”父进程要区分用户 abort、正常 shutdown 和 crash;crash 应携 stderr tail 形成 typed error,并把下一次 chat 置于可冷启动状态,而不是保留一个已死 child 引用。
8.20 子进程方案的评价
Section titled “8.20 子进程方案的评价”优点:
- provider 依赖和崩溃隔离;
- native steer/branch/compaction 得到保留;
- parent 统一权限、MCP、凭据;
- 协议可记录、重放和独立测试。
代价:
- 一次 tool call 多两次序列化与跨进程往返;
- correlation timeout/cleanup 分支多;
- stdout 污染、child crash、版本不匹配都是新故障面;
- tool set 变化可能重建 Pi session;
- parent 与 subprocess 类型定义若不共享生成物,可能漂移。
8.21 可迁移经验
Section titled “8.21 可迁移经验”当第三方 Agent SDK 与宿主 bundler/运行时冲突时,独立进程是很实用的 anti-corruption layer。关键不是“用 JSONL”本身,而是坚持:
- stdout 只承载协议;
- request 全有 correlation 与 timeout;
- parent 保留安全与秘密所有权;
- terminal path 排空 pending;
- turn event 用 queue 桥接到 AsyncGenerator;
- tool/config 变更只在安全边界生效;
- provider-native session anchor 被显式持久化。
8.22 本章小结
Section titled “8.22 本章小结”PiAgent 是一个完整的进程边界适配器:不是把 prompt 发给 CLI,而是在两个进程之间重建了 agent loop 所需的事件、工具、权限、模型配置和生命周期协议。
下一章把 Claude/Pi 的差异再次折叠起来,看统一 AgentEvent 如何变成持久消息、WebSocket push 和 renderer 的一致状态。