Pi 的 RPC 与 JSON 模式
最后更新: 2026年8月18日
$ pi –rpc
TL;DR。 Pi 提供三种非交互入口:-p / --print 做一次性 prompt → 回复,--mode json 用同样形态把完整会话事件流以 JSONL 输出到 stdout,--mode rpc 通过 stdio 上的 JSON-RPC 做有状态的多回合会话。RPC 使用严格的 LF 分隔 JSONL(不要用 Node readline**——它会按 U+2028/U+2029 切分),通过可选的 id 关联请求与响应,并暴露约 40 条命令,覆盖提示类(prompt、steer、follow_up、abort)、状态类(get_state、get_messages、set_model、compact、switch_session、fork)、shell 类(bash、abort_bash),以及 UI 原语(select、confirm、input、editor、notify、setStatus)。JSON 模式发出与 RPC 相同的事件但不附带请求/响应关联,且 message_update事件只携带增量——用于流式输出,不要用它组装完整消息。三种模式都跳过交互式信任提示;用defaultProjectTrust与–approve/–no-approve` 一次性覆写来控制项目信任。
本指南是官方 RPC 模式 与 JSON 事件流模式 文档的深度配套。它把 packages/coding-agent/docs/rpc.md、json.md、usage.md、environment-variables.md 中的规则汇总到一处,并加上 CI 任务或脚本宿主真正需要的实用旋钮。上游文档如有变更,本指南会滞后——动手前请核对官方版本。
三种非交互模式
| 模式 | 形态 | 适用场景 |
|---|---|---|
-p / --print |
一次性 prompt → stdout 回复 | 快速 CI 任务、git hooks、shell 流水线 |
--mode json |
一次性 prompt + 完整会话事件流以 JSONL 输出 | 想记录每个事件但不需要多回合控制的工具 |
--mode rpc |
stdio 上有状态的多回合 JSON-RPC | IDE、自定义 UI、需要在流中途 steer 的 agent 循环、会话编排 |
三种模式都跳过交互式信任提示。在没有适用的已保存信任决策时,回退到 ~/.pi/agent/settings.json 的 defaultProjectTrust——"ask"(默认)与 "never" 会忽略项目资源,"always" 则信任它们。用 --approve / -a 或 --no-approve / -na 为单次运行覆写项目信任。
打印模式:最简入口
pi -p "summarize the open issues labeled 'bug'"
echo "exit=$?"
回复流式输出到 stdout;非零退出码表示失败。配套选项:
--provider <name>——设置 LLM provider(anthropic、openai、google等)--model <pattern>——模型 pattern 或provider/id,可选:<thinking>后缀--name <name>/-n <name>——启动时设置会话显示名--no-session——禁用会话持久化(不写 JSONL)--session-dir <path>——自定义会话存储目录--approve/--no-approve——单次项目信任覆写--thinking/--no-thinking——控制推理--system-prompt/--append-system-prompt——替换或扩展默认系统提示--no-extensions/--no-skills——关闭扩展/技能层--tools/--exclude-tools/--no-tools——控制工具集@file——把@path/to/file参数展开为文件内容
CI 任务里的典型写法:
pi -p "..." --no-session --approve --provider anthropic --model claude-opus-5
JSON 模式:事件流
pi --mode json "Your prompt" > session.jsonl
把每个会话事件以 JSON 行输出到 stdout。线上类型是 JsonAgentSessionEvent,与 AgentSessionEvent 相同,只是流式 message_update 事件不再携带累计的 partial 快照,只保留增量(assistantMessageEvent)与 usage。增量用于实时流式;不要试图从 message_update 拼出完整 assistant 消息——用 message_end 拿完整消息。
基础事件类型:
type AgentEvent =
// Agent 生命周期
| { type: "agent_start" }
| { type: "agent_end"; messages: AgentMessage[] }
// 回合生命周期
| { type: "turn_start" }
| { type: "turn_end"; message: AgentMessage; toolResults: ToolResultMessage[] }
// 消息生命周期
| { type: "message_start"; message: AgentMessage }
| { type: "message_update"; message: AgentMessage; assistantMessageEvent: AssistantMessageEvent }
| { type: "message_end"; message: AgentMessage }
// 工具执行
| { type: "tool_execution_start"; toolCallId: string; toolName: string; args: any }
| { type: "tool_execution_update"; toolCallId: string; toolName: string; args: any; partialResult: any }
| { type: "tool_execution_end"; toolCallId: string; toolName: string; result: any; isError: boolean };
此外还有 queue_update(steering / follow-up 队列变化时发出完整内容)、compaction_start、compaction_end(覆盖手动与自动压缩)。基础消息类型来自 packages/ai/src/types.ts(UserMessage、AssistantMessage、ToolResultMessage);扩展类型在 packages/coding-agent/src/core/messages.ts(BashExecutionMessage、CustomMessage、BranchSummaryMessage)。
用 jq 从 JSON 流里取出最终 assistant 文本:
pi --mode json "explain the regex" \
| jq -c 'select(.type == "message_end") | .message.content[]? | select(.type == "text") | .text'
RPC 模式:完整协议
帧规则
RPC 模式使用 严格的 JSONL,仅以 LF(\n)作为记录分隔符。客户端必须遵守三条规则:
- 只在
\n上切分记录。 - 输入端允许
\r\n,去掉尾随的\r。 - 不要使用把 Unicode 分隔符也当换行的通用行读取器。
特别是 Node 自带的 readline 不符合 RPC 协议——它会按 U+2028 与 U+2029 切分,而这两者在 JSON 字符串里是合法的。请使用仅按 LF 切的 JSONL 读取器。
请求/响应关联
每条命令都接受可选的 id 字段。提供 id 时,对应响应会带上同样的 id,方便客户端匹配回请求。bash_execution_update 事件也会带上触发它的 bash 命令的 id。不传 id 时命令仍然能工作,但响应无法关联——一次性场景没问题,复杂客户端就难了。
最小请求与响应:
{"id": "req-1", "type": "prompt", "message": "Hello, world!"}
{"id": "req-1", "type": "response", "command": "prompt", "success": true}
success: true 表示 prompt 已被接受、加入队列或立即处理;success: false 表示在接收之前就被拒绝。接收之后的失败会通过正常的事件/消息流上报,不会再为同一个请求 id 发第二条 response。
提示类命令
prompt 发送用户 prompt,响应在 prompt 被接受、入队或立即处理后立即返回;事件继续异步流式输出。
{"id": "req-1", "type": "prompt", "message": "Hello, world!"}
带图片(每张用 ImageContent 格式):
{"type": "prompt", "message": "What's in this image?", "images": [{"type": "image", "data": "base64...", "mimeType": "image/png"}]}
如果 agent 已在流式输出,必须指定 streamingBehavior 来排队:
{"type": "prompt", "message": "New instruction", "streamingBehavior": "steer"}
"steer"——运行时排队;在当前 assistant 回合执行完工具调用后、下一次 LLM 调用前投递。"followUp"——等 agent 停止;只在 agent 停下时投递。
agent 在流式时若不指定 streamingBehavior,命令会报错。Skill 命令(/skill:name)与 prompt 模板(/template)会在发送/排队前展开;扩展命令(如 /mycommand)即使在流式中也会立即执行,因为它们通过 pi.sendMessage() 自管 LLM 交互。
steer 与 follow_up 是专门的队列原语。steer 不允许扩展命令(那种情况用 prompt);Skill//template 展开同样作用于 steer。用 set_steering_mode 与 set_follow_up_mode 控制两个队列的处理方式;用 abort 丢掉待发消息。
状态类命令
| 命令 | 作用 |
|---|---|
get_state |
快照完整会话状态 |
get_messages |
迄今为止的所有消息 |
get_session_stats |
token 用量、回合数等 |
get_entries / get_tree |
会话条目列表与会话树 |
get_last_assistant_text |
上一条 assistant 文本(便捷命令) |
get_available_models / get_available_thinking_levels |
枚举已配置的 provider / 模型 / 推理级别 |
set_model / cycle_model |
选择或步进模型 |
set_thinking_level / cycle_thinking_level |
选择或步进推理级别 |
set_steering_mode / set_follow_up_mode |
配置两个队列的处理方式 |
set_session_name |
重命名会话 |
get_commands |
列出已注册命令 |
compact |
触发压缩 |
set_auto_compaction |
启用/禁用自动压缩 |
set_auto_retry / abort_retry |
控制重试行为 |
new_session |
开新会话 |
switch_session / fork / clone |
在树中跳转或分支(switch_session 与 fork 遵守 session_before_switch / session_before_fork 扩展事件用于取消) |
get_fork_messages |
查看分叉点的消息 |
export_html |
把会话渲染为 HTML |
bash 命令
bash 在 agent 会话里跑 shell 命令,结果通过 BashExecutionMessage 注入到下一条 prompt,而不是当前这一条:
{"type": "bash", "command": "ls -la"}
abort_bash 终止正在进行的 bash。bash 工具会暴露会话变量(PI_SESSION_ID、PI_SESSION_FILE、PI_PROVIDER、PI_MODEL、PI_REASONING_LEVEL),让 LLM 可调用的 shell 能发现当前跑的是哪个模型——比解析系统提示靠谱得多。
UI 原语
RPC 还暴露一组 UI 原语:当 RPC 由交互式父进程驱动时,这些原语会冒泡到 TUI;否则就是 no-op 或被回传:select、confirm、input、editor、notify、setStatus、setWidget、setTitle。把 pi 嵌入自定义前端时这组原语很有用——把它们透传,前端用户就能拿到 TUI 提供的同一种会话内 UI 体验。
非交互模式下的项目信任
-p、--mode json、--mode rpc 启动时 pi 不弹信任提示。解析顺序:
~/.pi/agent/trust.json中针对该目录或父目录的已保存决策。~/.pi/agent/settings.json中的defaultProjectTrust。- 单次覆写
--approve/-a或--no-approve/-na。
"ask"(默认)与 "never" 完全忽略项目资源(不加载 .pi/settings.json、项目扩展、项目技能);"always" 则加载。多数 CI 任务用 --approve,让项目自己的 context 文件与已 pin 的扩展生效;沙箱环境用 --no-approve,把运行锁在干净的可信基线上。
环境变量
Pi 设置几个脚本宿主和子进程需要知道的变量:
PI_OFFLINE=1——禁用启动时的网络访问(无更新检查、无 telemetry ping)。PI_SKIP_VERSION_CHECK=1——启动时跳过版本检查。PI_TELEMETRY=0——关闭 telemetry。PI_CODING_AGENT_DIR——覆写 agent 主目录(默认~/.pi/agent/)。PI_CODING_AGENT_SESSION_DIR——覆写会话存储目录。AI_AGENT=pi——通用标记,任何工具都可以据此识别 pi 是启动 agent。PI_CODING_AGENT=true——pi 专属标记,子进程用此判断自己跑在 pi 里。
这些标记会被子进程自动继承;它们不是 session-specific 的,且通过 SDK 嵌入 pi 时不会设置(SDK 是不同的进程树)。
bash 工具在每条命令开始时设置一组额外的变量——回合间切换模型或推理级别会影响下一次 bash 调用,无需重启 pi:
| 变量 | 值 |
|---|---|
PI_SESSION_ID |
当前会话 ID |
PI_SESSION_FILE |
当前会话 JSONL 的绝对路径;ephemeral 会话未设置 |
PI_PROVIDER |
当前选中的 provider |
PI_MODEL |
当前选中的模型 ID |
PI_REASONING_LEVEL |
off、minimal、low、medium、high、xhigh、max 之一 |
被问到「现在跑的是什么模型?」时,用这些变量查,不要从系统提示猜:
printf '%s/%s\n' "$PI_PROVIDER" "$PI_MODEL"
printf 'reasoning=%s session=%s\n' "$PI_REASONING_LEVEL" "$PI_SESSION_ID"
CI 集成模式
最小可用的 GitHub Actions 步骤:以打印模式运行 pi 并透出退出码:
- uses: actions/setup-node@v7
with:
node-version: 22
- run: npm ci --ignore-scripts
- name: Run pi
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
set -e
pi -p "label this issue" \
--no-session \
--approve \
--provider anthropic \
--model claude-opus-5
要点:
npm ci --ignore-scripts与项目自身 CI 一致;lifecycle 脚本默认关闭,AGENTS.md规则要求除非用户要求否则不要开启。- 如果 runner 离线,加
PI_OFFLINE=1。 - 想要确定性,把
--model钉到精确 ID,而不是依赖 provider 默认值。 - 在 CI 里处理信任时,倾向于
--no-approve(跑在干净基线上),除非你确实想让项目本地设置生效。
pi 项目自己在 .github/workflows/ 里也用同款写法——actions/setup-node@v7 + Node 22 + npm ci --ignore-scripts;publish-npm 与 announce-pi-dev-release 任务里接 npm Trusted Publishing OIDC(id-token: write),而不是用长效 NPM_TOKEN secret。
自写 RPC 客户端
Node 宿主首选 @earendil-works/pi-coding-agent 的进程内 AgentSession,而不是 fork 子进程;要参考子进程版 TypeScript 客户端实现,见 packages/coding-agent/src/modes/rpc/rpc-client.ts。下面的 Python 草图展示任何语言实现都必须遵守的规则:仅按 LF 切、逐行 JSON parse、可选 id 关联、增量流式事件。
import json
import subprocess
import sys
import threading
proc = subprocess.Popen(
["pi", "--mode", "rpc"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
text=True, # 文本模式;我们自己控制行切分
bufsize=1,
)
def reader():
for line in proc.stdout: # 只在 '\n' 上切——RPC 的硬性要求
line = line.rstrip("\n")
if not line:
continue
try:
event = json.loads(line)
except json.JSONDecodeError:
continue
# 在这里区分 response 与 event
print(event, flush=True)
threading.Thread(target=reader, daemon=True).start()
req = {"id": "req-1", "type": "prompt", "message": "Hello"}
proc.stdin.write(json.dumps(req) + "\n")
proc.stdin.flush()
任何语言实现都必须遵守四条规则:仅按 LF 切、不要按 U+2028/U+2029 切、逐行 JSON parse/serialize、需要时用 id 关联。流式 message_update 事件只携带增量;想要完整消息就等匹配的 message_end。
延伸阅读
- RPC mode——上游官方文档,本指南的主要来源
- JSON event stream mode——完整的事件类型 schema
- Usage——每条 CLI 选项及其搭配
- Environment variables——
PI_*旋钮与 bash 工具的会话变量 - Settings——
defaultProjectTrust的位置 - Security——非交互信任跳过的实际含义
packages/coding-agent/src/modes/rpc/rpc-client.ts——参考用 TypeScript RPC 客户端- Custom tools and SDK integration——进程内 SDK 路径,Node 宿主的正确选择