piagent_
← ~/guides

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 条命令,覆盖提示类(promptsteerfollow_upabort)、状态类(get_stateget_messagesset_modelcompactswitch_sessionfork)、shell 类(bashabort_bash),以及 UI 原语(selectconfirminputeditornotifysetStatus)。JSON 模式发出与 RPC 相同的事件但不附带请求/响应关联,且 message_update事件只携带增量——用于流式输出,不要用它组装完整消息。三种模式都跳过交互式信任提示;用defaultProjectTrust–approve/–no-approve` 一次性覆写来控制项目信任。

本指南是官方 RPC 模式JSON 事件流模式 文档的深度配套。它把 packages/coding-agent/docs/rpc.mdjson.mdusage.mdenvironment-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.jsondefaultProjectTrust——"ask"(默认)与 "never" 会忽略项目资源,"always" 则信任它们。用 --approve / -a--no-approve / -na 为单次运行覆写项目信任。

打印模式:最简入口

pi -p "summarize the open issues labeled 'bug'"
echo "exit=$?"

回复流式输出到 stdout;非零退出码表示失败。配套选项:

  • --provider <name>——设置 LLM provider(anthropicopenaigoogle 等)
  • --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_startcompaction_end(覆盖手动与自动压缩)。基础消息类型来自 packages/ai/src/types.tsUserMessageAssistantMessageToolResultMessage);扩展类型在 packages/coding-agent/src/core/messages.tsBashExecutionMessageCustomMessageBranchSummaryMessage)。

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)作为记录分隔符。客户端必须遵守三条规则:

  1. 只在 \n 上切分记录。
  2. 输入端允许 \r\n,去掉尾随的 \r
  3. 不要使用把 Unicode 分隔符也当换行的通用行读取器。

特别是 Node 自带的 readline 不符合 RPC 协议——它会按 U+2028U+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 交互。

steerfollow_up 是专门的队列原语。steer 不允许扩展命令(那种情况用 prompt);Skill//template 展开同样作用于 steer。用 set_steering_modeset_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_sessionfork 遵守 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_IDPI_SESSION_FILEPI_PROVIDERPI_MODELPI_REASONING_LEVEL),让 LLM 可调用的 shell 能发现当前跑的是哪个模型——比解析系统提示靠谱得多。

UI 原语

RPC 还暴露一组 UI 原语:当 RPC 由交互式父进程驱动时,这些原语会冒泡到 TUI;否则就是 no-op 或被回传:selectconfirminputeditornotifysetStatussetWidgetsetTitle。把 pi 嵌入自定义前端时这组原语很有用——把它们透传,前端用户就能拿到 TUI 提供的同一种会话内 UI 体验。

非交互模式下的项目信任

-p--mode json--mode rpc 启动时 pi 不弹信任提示。解析顺序:

  1. ~/.pi/agent/trust.json 中针对该目录或父目录的已保存决策。
  2. ~/.pi/agent/settings.json 中的 defaultProjectTrust
  3. 单次覆写 --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 offminimallowmediumhighxhighmax 之一

被问到「现在跑的是什么模型?」时,用这些变量查,不要从系统提示猜:

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-scriptspublish-npmannounce-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

延伸阅读