21 RPC 模式介绍如何通过 stdin/stdout 上的 JSON 协议以无头方式驱动 pi 编码智能体。
RPC 模式让你能够通过 stdin/stdout 上的 JSON 协议以无头(headless)方式运行编码智能体。它适合把智能体嵌入其他应用、IDE 或自定义 UI。
Node.js/TypeScript 用户请注意 :如果你在构建 Node.js 应用,可以考虑直接使用 @earendil-works/pi-coding-agent 中的 AgentSession,而不是启动子进程。API 参见 src/core/agent-session.ts 。基于子进程的 TypeScript 客户端参见 src/modes/rpc/rpc-client.ts 。
启动 RPC 模式 常用选项:
--provider <name>:设置 LLM 提供商(anthropic、openai、google 等)--model <pattern>:模型模式或 ID(支持 provider/id 以及可选的 :<thinking>)--name <name> / -n <name>:启动时设置会话显示名称--no-session:禁用会话持久化--session-dir <path>:自定义会话存储目录协议概览 命令 :发送到 stdin 的 JSON 对象,每行一个响应 :带有 type: "response" 的 JSON 对象,表示命令执行成功或失败事件 :以 JSON 行形式流式输出到 stdout 的智能体事件所有命令都支持可选的 id 字段,用于请求/响应关联。如果提供了 id,对应的响应会包含相同的 id。bash_execution_update 事件也会包含其来源 bash 命令的 id。
分帧 RPC 模式采用严格的 JSONL 语义,仅以 LF(\n)作为记录分隔符。
这一点对客户端很重要:
只按 \n 拆分记录 接受可选的 \r\n 输入,方式是去掉末尾的 \r 不要使用会把 Unicode 分隔符当作换行符的通用行读取器 尤其要注意,Node 的 readline 并不符合 RPC 模式协议的要求,因为它还会在 U+2028 和 U+2029 处拆分,而这两个字符在 JSON 字符串中是合法的。
命令 发送提示 prompt 向智能体发送一条用户提示。命令响应在提示被接受、入队或处理完毕后发出;接受之后,事件仍会继续异步流式输出。
{ "id" : "req-1" , "type" : "prompt" , "message" : "Hello, world!" } 带图片:
{ "type" : "prompt" , "message" : "What's in this image?" , "images" : [{ "type" : "image" , "data" : "base64-encoded-data" , "mimeType" : "image/png" }]} 流式输出期间 :如果智能体已在流式输出中,你必须指定 streamingBehavior 才能把消息入队:
{ "type" : "prompt" , "message" : "New instruction" , "streamingBehavior" : "steer" } "steer":在智能体运行期间把消息加入队列。当前助手回合执行完其工具调用之后、下一次 LLM 调用之前,消息会被投递。"followUp":等到智能体完成。只有在智能体停止后,消息才会被投递。如果智能体正在流式输出而未指定 streamingBehavior,命令会返回错误。
扩展命令 :如果消息是扩展命令(例如 /mycommand),即使在流式输出期间也会立即执行。扩展命令通过 pi.sendMessage() 自行管理其 LLM 交互。
输入展开 :技能命令(/skill:name)和提示词模板(/template)会在发送/入队之前先被展开。
响应:
{ "id" : "req-1" , "type" : "response" , "command" : "prompt" , "success" : true } success: true 表示提示已被接受、入队或立即处理;success: false 表示提示在接受之前即被拒绝。接受之后的失败会通过正常的事件和消息流报告,而不会针对同一请求 id 再发出一个 response。
images 字段可选。每张图片采用 ImageContent 格式:{"type": "image", "data": "base64-encoded-data", "mimeType": "image/png"}。
steer 在智能体运行期间把一条转向消息入队。当前助手回合执行完其工具调用之后、下一次 LLM 调用之前,消息会被投递。技能命令和提示词模板会被展开。不允许扩展命令(请改用 prompt)。
{ "type" : "steer" , "message" : "Stop and do this instead" } 带图片:
{ "type" : "steer" , "message" : "Look at this instead" , "images" : [{ "type" : "image" , "data" : "base64-encoded-data" , "mimeType" : "image/png" }]} images 字段可选。每张图片采用 ImageContent 格式(与 prompt 相同)。
响应:
{ "type" : "response" , "command" : "steer" , "success" : true } 关于如何控制转向消息的处理方式,参见 set_steering_mode 。
follow_up 把一条后续消息入队,待智能体完成后再处理。只有当智能体不再有工具调用或转向消息时,消息才会被投递。技能命令和提示词模板会被展开。不允许扩展命令(请改用 prompt)。
{ "type" : "follow_up" , "message" : "After you're done, also do this" } 带图片:
{ "type" : "follow_up" , "message" : "Also check this image" , "images" : [{ "type" : "image" , "data" : "base64-encoded-data" , "mimeType" : "image/png" }]} images 字段可选。每张图片采用 ImageContent 格式(与 prompt 相同)。
响应:
{ "type" : "response" , "command" : "follow_up" , "success" : true } 关于如何控制后续消息的处理方式,参见 set_follow_up_mode 。
abort 中止当前操作,并等待会话进入空闲状态后再返回响应。
响应:
{ "type" : "response" , "command" : "abort" , "success" : true } clear_queue 移除已排队的转向和后续消息,并返回它们的文本。
响应:
{
"type" : "response" ,
"command" : "clear_queue" ,
"success" : true ,
"data" : {
"steering" : [ "Change direction" ],
"followUp" : [ "Summarize when finished" ]
}
} 要实现交互式的 Esc 行为,可以先发送 clear_queue 再发送 abort,然后把返回的文本恢复到客户端编辑器里。若排队消息仍保留在会话中,abort 会继续处理它们。
new_session 开启一个全新会话。该操作可以被 session_before_switch 扩展事件处理器取消。
带可选的父会话跟踪:
{ "type" : "new_session" , "parentSession" : "/path/to/parent-session.jsonl" } 响应:
{ "type" : "response" , "command" : "new_session" , "success" : true , "data" : { "cancelled" : false }} 如果被扩展取消:
{ "type" : "response" , "command" : "new_session" , "success" : true , "data" : { "cancelled" : true }} 状态 get_state 获取当前会话状态。
响应:
{
"type" : "response" ,
"command" : "get_state" ,
"success" : true ,
"data" : {
"model" : { ... },
"thinkingLevel" : "medium" ,
"isStreaming" : false ,
"isCompacting" : false ,
"steeringMode" : "all" ,
"followUpMode" : "one-at-a-time" ,
"sessionFile" : "/path/to/session.jsonl" ,
"sessionId" : "abc123" ,
"sessionName" : "my-feature-work" ,
"autoCompactionEnabled" : true ,
"messageCount" : 5 ,
"pendingMessageCount" : 0
}
} model 字段是完整的 Model 对象或 null。sessionName 字段是通过 set_session_name 设置的显示名称,未设置时省略。
get_messages 获取会话中的所有消息。
响应:
{
"type" : "response" ,
"command" : "get_messages" ,
"success" : true ,
"data" : { "messages" : [ ... ]}
} 消息为 AgentMessage 对象(参见消息类型 )。
模型 set_model 切换到特定模型。
{ "type" : "set_model" , "provider" : "anthropic" , "modelId" : "claude-sonnet-4-20250514" } 响应包含完整的 Model 对象:
{
"type" : "response" ,
"command" : "set_model" ,
"success" : true ,
"data" : { ... }
} cycle_model 切换到下一个可用模型。如果只有一个可用模型,则返回 null 数据。
响应:
{
"type" : "response" ,
"command" : "cycle_model" ,
"success" : true ,
"data" : {
"model" : { ... },
"thinkingLevel" : "medium" ,
"isScoped" : false
}
} model 字段是完整的 Model 对象。
get_available_models 列出所有已配置的模型。
{ "type" : "get_available_models" } 响应包含一个由完整 Model 对象组成的数组:
{
"type" : "response" ,
"command" : "get_available_models" ,
"success" : true ,
"data" : {
"models" : [ ... ]
}
} 思考 set_thinking_level 为支持该功能的模型设置推理/思考级别。
{ "type" : "set_thinking_level" , "level" : "high" } 级别:"off"、"minimal"、"low"、"medium"、"high"、"xhigh"、"max"
"xhigh" 和 "max" 仅在所选模型支持时才会暴露。部分模型(包括 GPT-5.6)两者都支持。
响应:
{ "type" : "response" , "command" : "set_thinking_level" , "success" : true } cycle_thinking_level 在可用的思考级别之间循环切换。模型不支持思考时返回 null 数据。
{ "type" : "cycle_thinking_level" } 响应:
{
"type" : "response" ,
"command" : "cycle_thinking_level" ,
"success" : true ,
"data" : { "level" : "high" }
} get_available_thinking_levels 列出当前模型支持的思考级别。不支持推理的模型返回 ["off"]。
{ "type" : "get_available_thinking_levels" } 响应:
{
"type" : "response" ,
"command" : "get_available_thinking_levels" ,
"success" : true ,
"data" : {
"levels" : [ "off" , "minimal" , "low" , "medium" , "high" ]
}
} 队列模式 set_steering_mode 控制转向消息(来自 steer)的投递方式。
{ "type" : "set_steering_mode" , "mode" : "one-at-a-time" } 模式:
"all":当前助手回合执行完其工具调用之后,投递所有转向消息"one-at-a-time":每个完成的助手回合投递一条转向消息(默认)响应:
{ "type" : "response" , "command" : "set_steering_mode" , "success" : true } set_follow_up_mode 控制后续消息(来自 follow_up)的投递方式。
{ "type" : "set_follow_up_mode" , "mode" : "one-at-a-time" } 模式:
"all":智能体完成时投递所有后续消息"one-at-a-time":每次智能体完成时投递一条后续消息(默认)响应:
{ "type" : "response" , "command" : "set_follow_up_mode" , "success" : true } 上下文压缩 compact 手动压缩会话上下文,以减少 token 用量。
使用自定义指令:
{ "type" : "compact" , "customInstructions" : "Focus on code changes" } 响应:
{
"type" : "response" ,
"command" : "compact" ,
"success" : true ,
"data" : {
"summary" : "Summary of conversation..." ,
"firstKeptEntryId" : "abc123" ,
"tokensBefore" : 150000 ,
"estimatedTokensAfter" : 32000 ,
"usage" : {
"input" : 32000 ,
"output" : 1200 ,
"cacheRead" : 0 ,
"cacheWrite" : 0 ,
"totalTokens" : 33200 ,
"cost" : { "input" : 0.01 , "output" : 0.02 , "cacheRead" : 0 , "cacheWrite" : 0 , "total" : 0.03 }
},
"details" : {}
}
} estimatedTokensAfter 是对压缩后立即重建的消息上下文的启发式估算,并不是提供商给出的精确 token 计数。usage 报告的是生成摘要的那一次或多次 LLM 调用,自定义压缩处理器可能会省略该字段。
set_auto_compaction 在上下文即将占满时启用或禁用自动压缩。
{ "type" : "set_auto_compaction" , "enabled" : true } 响应:
{ "type" : "response" , "command" : "set_auto_compaction" , "success" : true } 重试 set_auto_retry 在出现瞬时错误(过载、限流、5xx)时启用或禁用自动重试。
{ "type" : "set_auto_retry" , "enabled" : true } 响应:
{ "type" : "response" , "command" : "set_auto_retry" , "success" : true } abort_retry 中止正在进行的重试(取消等待并停止重试)。
响应:
{ "type" : "response" , "command" : "abort_retry" , "success" : true } Bash bash 执行 shell 命令并把输出加入会话上下文。命令运行期间,输出以 bash_execution_update 事件流式输出;响应中包含最终结果。
{ "id" : "req-1" , "type" : "bash" , "command" : "ls -la" } 包含 id 可以把流式的 bash_execution_update 事件与该命令关联起来。
响应:
{
"id" : "req-1" ,
"type" : "response" ,
"command" : "bash" ,
"success" : true ,
"data" : {
"output" : "total 48\ndrwxr-xr-x ..." ,
"exitCode" : 0 ,
"cancelled" : false ,
"truncated" : false
}
} 如果输出被截断,还会包含 fullOutputPath:
{
"type" : "response" ,
"command" : "bash" ,
"success" : true ,
"data" : {
"output" : "truncated output..." ,
"exitCode" : 0 ,
"cancelled" : false ,
"truncated" : true ,
"fullOutputPath" : "/tmp/pi-bash-abc123.log"
}
} bash 结果如何进入 LLM:
bash 命令立即执行并返回一个 BashResult。在内部,会创建一个 BashExecutionMessage 并存入智能体的消息状态。
当下一个 prompt 命令发送时,所有消息(包括 BashExecutionMessage)都会在发送给 LLM 之前先被转换。BashExecutionMessage 会被转换成如下格式的 UserMessage:
Ran `ls -la`
```
total 48
drwxr-xr-x ...
```这意味着:
bash 输出在下一次 prompt 时才进入 LLM 上下文,而不是立即进入 在一次 prompt 之前可以执行多条 bash 命令;所有输出都会被包含 abort_bash 中止正在运行的 bash 命令。
响应:
{ "type" : "response" , "command" : "abort_bash" , "success" : true } 会话 get_session_stats 获取 token 用量、成本统计以及当前上下文窗口的使用情况。
{ "type" : "get_session_stats" } 响应:
{
"type" : "response" ,
"command" : "get_session_stats" ,
"success" : true ,
"data" : {
"sessionFile" : "/path/to/session.jsonl" ,
"sessionId" : "abc123" ,
"userMessages" : 5 ,
"assistantMessages" : 5 ,
"toolCalls" : 12 ,
"toolResults" : 12 ,
"totalMessages" : 22 ,
"tokens" : {
"input" : 50000 ,
"output" : 10000 ,
"cacheRead" : 40000 ,
"cacheWrite" : 5000 ,
"total" : 105000
},
"cost" : 0.45 ,
"contextUsage" : {
"tokens" : 60000 ,
"contextWindow" : 200000 ,
"percent" : 30
}
}
} tokens 和 cost 涵盖整个会话中的助手消息、工具报告的用量,以及压缩和分支摘要的生成。contextUsage 包含用于压缩和底部状态栏显示的实际当前上下文窗口估算。
当没有可用模型或上下文窗口时,contextUsage 会被省略。刚完成压缩后,直到新的压缩后助手响应提供有效用量数据之前,contextUsage.tokens 和 contextUsage.percent 均为 null。
export_html 将会话导出为 HTML 文件。
使用自定义路径:
{ "type" : "export_html" , "outputPath" : "/tmp/session.html" } 响应:
{
"type" : "response" ,
"command" : "export_html" ,
"success" : true ,
"data" : { "path" : "/tmp/session.html" }
} switch_session 加载另一个会话文件。该操作可以被 session_before_switch 扩展事件处理器取消。
{ "type" : "switch_session" , "sessionPath" : "/path/to/session.jsonl" } 响应:
{ "type" : "response" , "command" : "switch_session" , "success" : true , "data" : { "cancelled" : false }} 如果切换被扩展取消:
{ "type" : "response" , "command" : "switch_session" , "success" : true , "data" : { "cancelled" : true }} fork 从活动分支上较早的某条用户消息处创建新的分叉。该操作可以被 session_before_fork 扩展事件处理器取消。返回作为分叉起点的那条消息的文本。
{ "type" : "fork" , "entryId" : "abc123" } 响应:
{
"type" : "response" ,
"command" : "fork" ,
"success" : true ,
"data" : { "text" : "The original prompt text..." , "cancelled" : false }
} 如果分叉被扩展取消:
{
"type" : "response" ,
"command" : "fork" ,
"success" : true ,
"data" : { "text" : "The original prompt text..." , "cancelled" : true }
} clone 把当前活动分支在当前位置复制为一个新会话。该操作可以被 session_before_fork 扩展事件处理器取消。
响应:
{
"type" : "response" ,
"command" : "clone" ,
"success" : true ,
"data" : { "cancelled" : false }
} 如果克隆被扩展取消:
{
"type" : "response" ,
"command" : "clone" ,
"success" : true ,
"data" : { "cancelled" : true }
} get_fork_messages 获取可用于分叉的用户消息。
{ "type" : "get_fork_messages" } 响应:
{
"type" : "response" ,
"command" : "get_fork_messages" ,
"success" : true ,
"data" : {
"messages" : [
{ "entryId" : "abc123" , "text" : "First prompt..." },
{ "entryId" : "def456" , "text" : "Second prompt..." }
]
}
} get_entries 按追加顺序获取所有会话条目(不含会话头)。会话是一棵具有稳定 id 的只追加条目树,因此条目 id 可以充当持久游标:把你已见过的最后一个条目 id 作为 since 传入,即可只获取严格位于其后的条目,即使客户端重启也一样有效。与 get_messages 不同,它包含压缩前的历史和被放弃的分支。
使用游标:
{ "type" : "get_entries" , "since" : "abc123" } 响应:
{
"type" : "response" ,
"command" : "get_entries" ,
"success" : true ,
"data" : {
"entries" : [
{ "type" : "message" , "id" : "def456" , "parentId" : "abc123" , "timestamp" : "..." , "message" : { "role" : "user" , "..." : "..." }}
],
"leafId" : "def456"
}
} leafId 是当前叶子条目的 id(空会话时为 null),客户端一次往返即可判断活动分支是否移动。如果 since 不匹配任何条目 id,响应为 success: false。
get_tree 以条目树的形式获取会话。每个节点为 {entry, children, label?, labelTimestamp?}。格式良好的会话只有一个根;孤儿条目(父链断裂的条目)也会作为根出现。
响应:
{
"type" : "response" ,
"command" : "get_tree" ,
"success" : true ,
"data" : {
"tree" : [
{
"entry" : { "type" : "message" , "id" : "abc123" , "parentId" : null , "..." : "..." },
"children" : [
{ "entry" : { "type" : "message" , "id" : "def456" , "parentId" : "abc123" , "..." : "..." }, "children" : []}
]
}
],
"leafId" : "def456"
}
} get_last_assistant_text 获取最后一条助手消息的文本内容。
{ "type" : "get_last_assistant_text" } 响应:
{
"type" : "response" ,
"command" : "get_last_assistant_text" ,
"success" : true ,
"data" : { "text" : "The assistant's response..." }
} 如果没有助手消息,则返回 {"text": null}。
set_session_name 为当前会话设置显示名称。该名称会出现在会话列表中,便于识别会话。
{ "type" : "set_session_name" , "name" : "my-feature-work" } 响应:
{
"type" : "response" ,
"command" : "set_session_name" ,
"success" : true
} 当前会话名称可通过 get_state 的 sessionName 字段获取。要在启动 RPC 模式时设置初始名称,请为 pi --mode rpc 进程传入 --name <name> 或 -n <name>。
可用命令 get_commands 获取可用命令(扩展命令、提示词模板和技能)。这些命令可以通过 prompt 命令调用,只需加上 / 前缀。
响应:
{
"type" : "response" ,
"command" : "get_commands" ,
"success" : true ,
"data" : {
"commands" : [
{ "name" : "session-name" , "description" : "Set or clear session name" , "source" : "extension" , "path" : "/home/user/.pi/agent/extensions/session.ts" },
{ "name" : "fix-tests" , "description" : "Fix failing tests" , "source" : "prompt" , "location" : "project" , "path" : "/home/user/myproject/.pi/agent/prompts/fix-tests.md" },
{ "name" : "skill:brave-search" , "description" : "Web search via Brave API" , "source" : "skill" , "location" : "user" , "path" : "/home/user/.pi/agent/skills/brave-search/SKILL.md" }
]
}
} 每条命令包含:
name:命令名(用 /name 调用)description:人类可读的描述(扩展命令可选)source:命令的种类:"extension":由扩展中的 pi.registerCommand() 注册"prompt":从提示词模板 .md 文件加载"skill":从技能目录加载(名称带 skill: 前缀)location:加载来源(可选,扩展没有该字段):"user":用户级(~/.pi/agent/)"project":项目级(./.pi/agent/)"path":通过 CLI 或设置显式指定的路径path:命令源文件的绝对路径(可选)注意 :不包含内置 TUI 命令(/settings、/hotkeys 等)。它们只在交互模式下处理,如果通过 prompt 发送并不会执行。
事件 事件在智能体运行期间以 JSON 行的形式流式输出到 stdout。事件通常不包含 id 字段;当来源 bash 命令提供了 id 时,bash_execution_update 会包含该 id。
事件类型 agent_start 当智能体开始处理一条提示时发出。
agent_end 当一次低层智能体运行完成时发出。包含这次运行期间生成的所有消息。如果 willRetry 为 true,随后会进行自动重试。
{
"type" : "agent_end" ,
"messages" : [ ... ],
"willRetry" : false
} agent_settled 在完整的会话级运行稳定之后发出。此时,pi 不会再通过重试、压缩重试或排队的后续消息自动继续。
{ "type" : "agent_settled" } turn_start / turn_end 一个回合(turn)由一次助手响应及其产生的工具调用和结果组成。
{
"type" : "turn_end" ,
"message" : { ... },
"toolResults" : [ ... ]
} message_start / message_end 在消息开始和完成时发出。message 字段包含一个 AgentMessage。
{ "type" : "message_start" , "message" : { ... }}
{ "type" : "message_end" , "message" : { ... }} message_update(流式) 在助手消息流式输出期间发出。包含一个增量(delta)事件,不含累积的消息快照。
{
"type" : "message_update" ,
"usage" : {
"input" : 100 ,
"output" : 1 ,
"cacheRead" : 0 ,
"cacheWrite" : 0 ,
"totalTokens" : 101 ,
"cost" : { "input" : 0 , "output" : 0 , "cacheRead" : 0 , "cacheWrite" : 0 , "total" : 0 }
},
"assistantMessageEvent" : {
"type" : "text_delta" ,
"contentIndex" : 0 ,
"delta" : "Hello "
}
} assistantMessageEvent 字段包含以下增量类型之一:
流式输出文本响应的示例:
{ "type" : "message_update" , "usage" :{ ... }, "assistantMessageEvent" :{ "type" : "text_start" , "contentIndex" : 0 }}
{ "type" : "message_update" , "usage" :{ ... }, "assistantMessageEvent" :{ "type" : "text_delta" , "contentIndex" : 0 , "delta" : "Hello" }}
{ "type" : "message_update" , "usage" :{ ... }, "assistantMessageEvent" :{ "type" : "text_delta" , "contentIndex" : 0 , "delta" : " world" }}
{ "type" : "message_update" , "usage" :{ ... }, "assistantMessageEvent" :{ "type" : "text_end" , "contentIndex" : 0 , "content" : "Hello world" }} 顶层的 usage 字段包含提供商报告的最新累计用量。如果提供商在流式输出期间不报告用量,该字段可能一直为零,直到完成。
启动工具调用的示例:
{ "type" : "message_update" , "usage" :{ ... }, "assistantMessageEvent" :{ "type" : "toolcall_start" , "contentIndex" : 1 , "id" : "call_abc123" , "toolName" : "write" }} message_update 有意省略了以往的累积 message 字段和 assistantMessageEvent.partial。需要实时部分消息的客户端必须利用 contentIndex,从 message_start 及后续事件自行组装。应以 message_end.message 为准。对于工具调用,toolcall_start 会提供调用 id 和 toolName;请为参数缓冲 toolcall_delta.delta。toolcall_end.toolCall 包含已完成的调用。
bash_execution_update 直接 bash 命令的每个输出块发出一次。id 与命令的 id 匹配,客户端可据此将输出关联到正确的命令。
命令运行期间,事件会流式输出全部输出,即使最终 bash 响应的 output 被截断。
{
"type" : "bash_execution_update" ,
"id" : "req-1" ,
"delta" : "total 48\n"
} 在工具开始执行、流式输出进度以及执行完成时发出。
{
"type" : "tool_execution_start" ,
"toolCallId" : "call_abc123" ,
"toolName" : "bash" ,
"args" : { "command" : "ls -la" }
} 执行期间,tool_execution_update 事件会流式输出部分结果(例如随到随发的 bash 输出):
{
"type" : "tool_execution_update" ,
"toolCallId" : "call_abc123" ,
"toolName" : "bash" ,
"args" : { "command" : "ls -la" },
"partialResult" : {
"content" : [{ "type" : "text" , "text" : "partial output so far..." }],
"details" : { "truncation" : null , "fullOutputPath" : null }
}
} 完成时:
{
"type" : "tool_execution_end" ,
"toolCallId" : "call_abc123" ,
"toolName" : "bash" ,
"result" : {
"content" : [{ "type" : "text" , "text" : "total 48\n..." }],
"details" : { ... }
},
"isError" : false
} 使用 toolCallId 关联事件。tool_execution_update 中的 partialResult 包含到目前为止累积的输出(而不只是增量),客户端可以在每次更新时直接替换自己的显示。
queue_update 每当待处理的转向或后续队列发生变化时发出。
{
"type" : "queue_update" ,
"steering" : [ "Focus on error handling" ],
"followUp" : [ "After that, summarize the result" ]
} compaction_start / compaction_end 在压缩运行时发出,无论是手动还是自动。
{ "type" : "compaction_start" , "reason" : "threshold" } reason 字段为 "manual"、"threshold" 或 "overflow"。
{
"type" : "compaction_end" ,
"reason" : "threshold" ,
"result" : {
"summary" : "Summary of conversation..." ,
"firstKeptEntryId" : "abc123" ,
"tokensBefore" : 150000 ,
"estimatedTokensAfter" : 32000 ,
"usage" : {
"input" : 32000 ,
"output" : 1200 ,
"cacheRead" : 0 ,
"cacheWrite" : 0 ,
"totalTokens" : 33200 ,
"cost" : { "input" : 0.01 , "output" : 0.02 , "cacheRead" : 0 , "cacheWrite" : 0 , "total" : 0.03 }
},
"details" : {}
},
"aborted" : false ,
"willRetry" : false
} 如果 reason 为 "overflow" 且压缩成功,则 willRetry 为 true,智能体会自动重试该提示。
如果压缩被中止,result 为 null 且 aborted 为 true。
如果压缩失败(例如 API 配额超限),result 为 null、aborted 为 false,errorMessage 包含错误描述。
auto_retry_start / auto_retry_end 在瞬时错误(过载、限流、5xx)之后触发自动重试时发出。
{
"type" : "auto_retry_start" ,
"attempt" : 1 ,
"maxAttempts" : 3 ,
"delayMs" : 2000 ,
"errorMessage" : "529 {\"type\":\"error\",\"error\":{\"type\":\"overloaded_error\",\"message\":\"Overloaded\"}}"
}
{
"type" : "auto_retry_end" ,
"success" : true ,
"attempt" : 2
} 最终失败(超过最大重试次数)时:
{
"type" : "auto_retry_end" ,
"success" : false ,
"attempt" : 3 ,
"finalError" : "529 overloaded_error: Overloaded"
} summarization_retry_scheduled / summarization_retry_attempt_start / summarization_retry_finished 当压缩或分支摘要生成在瞬时提供商错误后重试时发出。这些事件使用与助手回合自动重试相同的重试设置。
{
"type" : "summarization_retry_scheduled" ,
"attempt" : 1 ,
"maxAttempts" : 3 ,
"delayMs" : 2000 ,
"errorMessage" : "terminated"
}
{
"type" : "summarization_retry_attempt_start" ,
"source" : "compaction" ,
"reason" : "threshold"
} 对于分支摘要,source 为 "branchSummary",且没有 reason 字段。
{
"type" : "summarization_retry_finished"
} extension_error 当扩展抛出错误时发出。
{
"type" : "extension_error" ,
"extensionPath" : "/path/to/extension.ts" ,
"event" : "tool_call" ,
"error" : "Error message..."
} 扩展 UI 协议 扩展可以通过 ctx.ui.select()、ctx.ui.confirm() 等方法请求用户交互。在 RPC 模式中,这些调用会被转换为一个构建在基础命令/事件流之上的请求/响应子协议。
扩展 UI 方法分为两类:
对话框方法 (select、confirm、input、editor):在 stdout 上发出一个 extension_ui_request,并阻塞等待客户端在 stdin 上发回带有匹配 id 的 extension_ui_response。即发即弃方法 (notify、setStatus、setWidget、setTitle、set_editor_text):在 stdout 上发出 extension_ui_request,但不期待响应。客户端可以显示这些信息,也可以忽略。如果对话框方法带有 timeout 字段,超时到期时智能体一侧会自动以默认值解决该请求。客户端不需要跟踪超时。
一些 ExtensionUIContext 方法在 RPC 模式中不受支持或被降级,因为它们需要直接访问 TUI:
custom() 返回 undefinedsetWorkingMessage()、setWorkingIndicator()、setFooter()、setHeader()、setEditorComponent()、setToolsExpanded() 是空操作getEditorText() 返回 ""getToolsExpanded() 返回 falsepasteToEditor() 委托给 setEditorText()(不做粘贴/收起处理)getAllThemes() 返回 []getTheme() 返回 undefinedsetTheme() 返回 { success: false, error: "..." }注意:RPC 模式下 ctx.mode 为 "rpc",且 ctx.hasUI 为 true,因为对话框方法和即发即弃方法都能通过扩展 UI 子协议正常工作。要保护 custom() 这类需要真实终端的 TUI 专属功能,请使用 ctx.mode === "tui" 判断。
扩展 UI 请求(stdout) 所有请求都带有 type: "extension_ui_request"、唯一的 id 和 method 字段。
select 提示用户从列表中选择。带 timeout 字段的对话框方法会包含以毫秒为单位的超时值;如果客户端没有及时响应,智能体会自动以 undefined 作为结果。
{
"type" : "extension_ui_request" ,
"id" : "uuid-1" ,
"method" : "select" ,
"title" : "Allow dangerous command?" ,
"options" : [ "Allow" , "Block" ],
"timeout" : 10000
} 预期响应:带有 value(选中的选项字符串)或 cancelled: true 的 extension_ui_response。
confirm 提示用户进行是/否确认。
{
"type" : "extension_ui_request" ,
"id" : "uuid-2" ,
"method" : "confirm" ,
"title" : "Clear session?" ,
"message" : "All messages will be lost." ,
"timeout" : 5000
} 预期响应:带有 confirmed: true/false 或 cancelled: true 的 extension_ui_response。
提示用户输入自由格式的文本。
{
"type" : "extension_ui_request" ,
"id" : "uuid-3" ,
"method" : "input" ,
"title" : "Enter a value" ,
"placeholder" : "type something..."
} 预期响应:带有 value(输入的文本)或 cancelled: true 的 extension_ui_response。
editor 打开一个多行文本编辑器,可选预填内容。
{
"type" : "extension_ui_request" ,
"id" : "uuid-4" ,
"method" : "editor" ,
"title" : "Edit some text" ,
"prefill" : "Line 1\nLine 2\nLine 3"
} 预期响应:带有 value(编辑后的文本)或 cancelled: true 的 extension_ui_response。
notify 显示一条通知。即发即弃,不期待响应。
{
"type" : "extension_ui_request" ,
"id" : "uuid-5" ,
"method" : "notify" ,
"message" : "Command blocked by user" ,
"notifyType" : "warning"
} notifyType 字段为 "info"、"warning" 或 "error"。省略时默认为 "info"。
setStatus 在底部/状态栏中设置或清除一个状态项。即发即弃。
{
"type" : "extension_ui_request" ,
"id" : "uuid-6" ,
"method" : "setStatus" ,
"statusKey" : "my-ext" ,
"statusText" : "Turn 3 running..."
} 发送 statusText: undefined(或省略该字段)即可清除该键的状态项。
设置或清除显示在编辑器上方或下方的小组件(widget,多行文本块)。即发即弃。
{
"type" : "extension_ui_request" ,
"id" : "uuid-7" ,
"method" : "setWidget" ,
"widgetKey" : "my-ext" ,
"widgetLines" : [ "--- My Widget ---" , "Line 1" , "Line 2" ],
"widgetPlacement" : "aboveEditor"
} 发送 widgetLines: undefined(或省略该字段)即可清除小组件。widgetPlacement 字段为 "aboveEditor"(默认)或 "belowEditor"。RPC 模式只支持字符串数组;组件工厂会被忽略。
setTitle 设置终端窗口/标签页标题。即发即弃。
{
"type" : "extension_ui_request" ,
"id" : "uuid-8" ,
"method" : "setTitle" ,
"title" : "pi - my project"
} set_editor_text 在输入编辑器中设置文本。即发即弃。
{
"type" : "extension_ui_request" ,
"id" : "uuid-9" ,
"method" : "set_editor_text" ,
"text" : "prefilled text for the user"
} 扩展 UI 响应(stdin) 只有对话框方法(select、confirm、input、editor)需要发送响应。id 必须与请求匹配。
{ "type" : "extension_ui_response" , "id" : "uuid-1" , "value" : "Allow" } 确认响应(confirm)
{ "type" : "extension_ui_response" , "id" : "uuid-2" , "confirmed" : true } 取消响应(任意对话框) 关闭任意对话框方法。扩展会收到 undefined(select/input/editor)或 false(confirm)。
{ "type" : "extension_ui_response" , "id" : "uuid-3" , "cancelled" : true } 错误处理 失败的命令会返回 success: false 的响应:
{
"type" : "response" ,
"command" : "set_model" ,
"success" : false ,
"error" : "Model not found: invalid/model"
} 解析错误:
{
"type" : "response" ,
"command" : "parse" ,
"success" : false ,
"error" : "Failed to parse command: Unexpected token..."
} 类型 源码文件:
Model
{
"id" : "claude-sonnet-4-20250514" ,
"name" : "Claude Sonnet 4" ,
"api" : "anthropic-messages" ,
"provider" : "anthropic" ,
"baseUrl" : "https://api.anthropic.com" ,
"reasoning" : true ,
"input" : [ "text" , "image" ],
"contextWindow" : 200000 ,
"maxTokens" : 16384 ,
"cost" : {
"input" : 3.0 ,
"output" : 15.0 ,
"cacheRead" : 0.3 ,
"cacheWrite" : 3.75
}
} UserMessage
{
"role" : "user" ,
"content" : "Hello!" ,
"timestamp" : 1733234567890 ,
"attachments" : []
} content 字段可以是字符串,也可以是 TextContent/ImageContent 块的数组。
AssistantMessage
{
"role" : "assistant" ,
"content" : [
{ "type" : "text" , "text" : "Hello! How can I help?" },
{ "type" : "thinking" , "thinking" : "User is greeting me..." },
{ "type" : "toolCall" , "id" : "call_123" , "name" : "bash" , "arguments" : { "command" : "ls" }}
],
"api" : "anthropic-messages" ,
"provider" : "anthropic" ,
"model" : "claude-sonnet-4-20250514" ,
"usage" : {
"input" : 100 ,
"output" : 50 ,
"cacheRead" : 0 ,
"cacheWrite" : 0 ,
"cost" : { "input" : 0.0003 , "output" : 0.00075 , "cacheRead" : 0 , "cacheWrite" : 0 , "total" : 0.00105 }
},
"stopReason" : "stop" ,
"timestamp" : 1733234567890
} 停止原因:"stop"、"length"、"toolUse"、"error"、"aborted"
{
"role" : "toolResult" ,
"toolCallId" : "call_123" ,
"toolName" : "bash" ,
"content" : [{ "type" : "text" , "text" : "total 48\ndrwxr-xr-x ..." }],
"usage" : {
"input" : 100 ,
"output" : 50 ,
"cacheRead" : 0 ,
"cacheWrite" : 0 ,
"totalTokens" : 150 ,
"cost" : { "input" : 0.0003 , "output" : 0.00075 , "cacheRead" : 0 , "cacheWrite" : 0 , "total" : 0.00105 }
},
"isError" : false ,
"timestamp" : 1733234567890
} usage 为可选字段,报告由工具执行的嵌套 LLM 工作。存在时,它会计入会话的 token 和成本总计。
BashExecutionMessage 由 bash RPC 命令创建(而不是由 LLM 工具调用创建):
{
"role" : "bashExecution" ,
"command" : "ls -la" ,
"output" : "total 48\ndrwxr-xr-x ..." ,
"exitCode" : 0 ,
"cancelled" : false ,
"truncated" : false ,
"fullOutputPath" : null ,
"timestamp" : 1733234567890
} Attachment
{
"id" : "img1" ,
"type" : "image" ,
"fileName" : "photo.jpg" ,
"mimeType" : "image/jpeg" ,
"size" : 102400 ,
"content" : "base64-encoded-data..." ,
"extractedText" : null ,
"preview" : null
} 示例:基础客户端(Python)
import subprocess
import json
proc = subprocess . Popen (
[ "pi" , "--mode" , "rpc" , "--no-session" ],
stdin = subprocess . PIPE ,
stdout = subprocess . PIPE ,
text = True
)
def send ( cmd ):
proc . stdin . write ( json . dumps ( cmd ) + " \n " )
proc . stdin . flush ()
def read_events ():
for line in proc . stdout :
yield json . loads ( line )
# Send prompt
send ({ "type" : "prompt" , "message" : "Hello!" })
# Process events
for event in read_events ():
if event . get ( "type" ) == "message_update" :
delta = event . get ( "assistantMessageEvent" , {})
if delta . get ( "type" ) == "text_delta" :
print ( delta [ "delta" ], end = "" , flush = True )
if event . get ( "type" ) == "agent_end" :
print ()
break 示例:交互式客户端(Node.js) 完整的交互式示例参见 test/rpc-example.ts ,类型化客户端实现参见 src/modes/rpc/rpc-client.ts 。
处理扩展 UI 协议的完整示例参见 examples/rpc-extension-ui.ts ,它与扩展 examples/extensions/rpc-demo.ts 配套使用。
const { spawn } = require ( "child_process" );
const { StringDecoder } = require ( "string_decoder" );
const agent = spawn ( "pi" , [ "--mode" , "rpc" , "--no-session" ]);
function attachJsonlReader ( stream , onLine ) {
const decoder = new StringDecoder ( "utf8" );
let buffer = "" ;
stream . on ( "data" , ( chunk ) => {
buffer += typeof chunk === "string" ? chunk : decoder . write ( chunk );
while ( true ) {
const newlineIndex = buffer . indexOf ( "\n" );
if ( newlineIndex === - 1 ) break ;
let line = buffer . slice ( 0 , newlineIndex );
buffer = buffer . slice ( newlineIndex + 1 );
if ( line . endsWith ( "\r" )) line = line . slice ( 0 , - 1 );
onLine ( line );
}
});
stream . on ( "end" , () => {
buffer += decoder . end ();
if ( buffer . length > 0 ) {
onLine ( buffer . endsWith ( "\r" ) ? buffer . slice ( 0 , - 1 ) : buffer );
}
});
}
attachJsonlReader ( agent . stdout , ( line ) => {
const event = JSON . parse ( line );
if ( event . type === "message_update" ) {
const { assistantMessageEvent } = event ;
if ( assistantMessageEvent . type === "text_delta" ) {
process . stdout . write ( assistantMessageEvent . delta );
}
}
});
// Send prompt
agent . stdin . write ( JSON . stringify ({ type : "prompt" , message : "Hello" }) + "\n" );
// Abort on Ctrl+C
process . on ( "SIGINT" , () => {
agent . stdin . write ( JSON . stringify ({ type : "abort" }) + "\n" );
});