RPC 模式
RPC 模式通过 stdin/stdout 上的 JSON 协议使编码助手能够在无头模式下运行。这对于将助手嵌入到其他应用程序、IDE 或自定义界面中非常有用。
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 模式
pi --mode rpc [选项]常用选项:
--provider <名称>:设置 LLM 提供商(anthropic、openai、google 等)--model <模式>:模型模式或 ID(支持provider/id和可选的:<thinking>)--no-session:禁用会话持久性--session-dir <路径>:自定义会话存储目录
协议概述
- 命令:通过 stdin 发送的 JSON 对象,每行一个
- 响应:带有
type: "response"的 JSON 对象,表示命令成功/失败 - 事件:作为 JSON 行流式传输到 stdout 的代理事件
所有命令都支持可选的 id 字段,用于请求/响应关联。如果提供了 id,相应的响应将包含相同的 id。
帧封装
RPC 模式使用严格的 JSONL 语义,LF (\n) 是唯一的记录分隔符。
这对客户端很重要:
- 仅按
\n分割记录 - 通过剥离尾随的
\r接受可选的\r\n输入 - 不要使用将 Unicode 分隔符视为换行符的通用行读取器
特别是,Node 的 readline 对于 RPC 模式来说不符合协议规范,因为它也会在 U+2028 和 U+2029 处分割,而这些字符在 JSON 字符串中是有效的。
命令
提示
prompt
向代理发送用户提示。命令响应在提示被接受、排队或处理后发出。接受后事件会继续异步流式传输。
{"id": "req-1", "type": "prompt", "message": "Hello, world!"}带图片:
{"type": "prompt", "message": "这张图片里有什么?", "images": [{"type": "image", "data": "base64-编码的数据", "mimeType": "image/png"}]}在流式传输期间:如果代理已经在流式传输,您必须指定 streamingBehavior 来排队消息:
{"type": "prompt", "message": "新指令", "streamingBehavior": "steer"}"steer":在代理运行时排队消息。它在当前助手轮次完成工具调用之后、下一次 LLM 调用之前传递。"followUp":等待代理完成。消息仅在代理停止时传递。
如果代理在流式传输且未指定 streamingBehavior,命令将返回错误。
扩展命令:如果消息是扩展命令(例如 /mycommand),它即使在流式传输期间也会立即执行。扩展命令通过 pi.sendMessage() 自行管理 LLM 交互。
输入扩展:技能命令(/skill:名称)和提示模板(/template)在发送/排队前会被扩展。
响应:
{"id": "req-1", "type": "response", "command": "prompt", "success": true}success: true 表示提示已被接受、排队或立即处理。success: false 表示提示在接收入之前被拒绝。接受后发生的失败将通过正常的事件和消息流报告,而不是作为同一请求 id 的第二个 response。
images 字段是可选的。每个图片使用 ImageContent 格式:{"type": "image", "data": "base64-编码的数据", "mimeType": "image/png"}。
steer
在代理运行时排队一条转向消息。它在当前助手轮次完成工具调用之后、下一次 LLM 调用之前传递。技能命令和提示模板会被扩展。不允许扩展命令(使用 prompt 代替)。
{"type": "steer", "message": "停止并做这个"}带图片:
{"type": "steer", "message": "看这个代替", "images": [{"type": "image", "data": "base64-编码的数据", "mimeType": "image/png"}]}images 字段是可选的。每个图片使用与 prompt 相同的 ImageContent 格式。
响应:
{"type": "response", "command": "steer", "success": true}参见 set_steering_mode 以控制转向消息的处理方式。
stop
终止当前会话。
响应:
{"id": "req-1", "type": "response", "command": "stop", "success": true}set_steering_mode
设置转向模式。转向消息如何被处理取决于此设置。
{"type": "set_steering_mode", "mode": "queued"}可能的值:
"queued":转向消息被排队,在下一个 LLM 调用前处理"immediate":转向消息被立即处理,中断当前 LLM 调用
响应:
{"type": "response", "command": "set_steering_mode", "success": true}session_info
返回当前会话信息。
{"type": "session_info"}响应:
{"type": "response", "command": "session_info", "success": true, "data": {
"sessionId": "uuid-1234",
"cwd": "/path/to/project",
"provider": "anthropic",
"model": "claude-3-5-sonnet",
"thinkingLevel": "high"
}}事件
事件是代理向客户端发出的异步消息。
model_change
模型更改事件。
{"type": "event", "name": "model_change", "data": {"provider": "openai", "modelId": "gpt-4o"}}thinking_level_change
思考级别更改事件。
{"type": "event", "name": "thinking_level_change", "data": {"thinkingLevel": "high"}}user_message
用户消息事件。
{"type": "event", "name": "user_message", "data": {"role": "user", "content": "你好", "timestamp": 1734512345678}}assistant_message
助手消息事件。
{"type": "event", "name": "assistant_message", "data": {
"role": "assistant",
"content": [{"type": "text", "text": "你好!"}],
"provider": "anthropic",
"model": "claude-3-5-sonnet",
"usage": {...},
"stopReason": "stop",
"timestamp": 1734512346789
}}tool_result
工具结果事件。
{"type": "event", "name": "tool_result", "data": {
"role": "toolResult",
"toolCallId": "call_abc123",
"toolName": "bash",
"content": [{"type": "text", "text": "输出..."}],
"details": {...},
"isError": false,
"timestamp": 1734512347890
}}bash_execution
Bash 执行事件。
{"type": "event", "name": "bash_execution", "data": {
"role": "bashExecution",
"command": "ls -la",
"output": "输出内容\n",
"exitCode": 0,
"cancelled": false,
"truncated": false,
"timestamp": 1734512348901
}}compaction
压缩事件。
{"type": "event", "name": "compaction", "data": {
"summary": "用户讨论了 X、Y、Z...",
"tokensBefore": 50000,
"timestamp": 1734512349012
}}custom
自定义扩展消息事件。
{"type": "event", "name": "custom", "data": {
"role": "custom",
"customType": "my-extension",
"content": "扩展注入的内容",
"display": true,
"timestamp": 1734512350123
}}branch_summary
分支摘要事件。
{"type": "event", "name": "branch_summary", "data": {
"summary": "分支探索了方法 A...",
"fromId": "prev-entry-id",
"timestamp": 1734512351234
}}扩展 UI 协议
扩展可以通过 RPC 与 TUI 进行交互。
set_working_indicator
设置工作指示器(在流式响应时显示)。
{"type": "set_working_indicator", "data": {
"frames": ["○", "●", "•"],
"intervalMs": 100
}}或隐藏指示器:
{"type": "set_working_indicator", "data": {"frames": []}}set_status
设置状态文本(显示在页脚)。
{"type": "set_status", "data": {"text": "正在处理...", "key": "my-ext-status"}}清除:
{"type": "set_status", "data": {"text": "", "key": "my-ext-status"}}set_footer
替换页脚组件。
{"type": "set_footer", "data": {
"render": {
"width": 80,
"text": "扩展自定义页脚"
}
}}set_editor_component
设置自定义编辑器组件。
{"type": "set_editor_component", "data": {
"factory": "my-editor-factory"
}}工厂函数接收 (tui, theme, keybindings) 并返回组件。
register_entry_renderer
注册自定义条目渲染器。
{"type": "register_entry_renderer", "data": {
"customType": "my-extension",
"render": {
"width": 80,
"text": "自定义渲染"
}
}}notify
显示通知。
{"type": "notify", "data": {"text": "操作完成", "level": "info"}}错误处理
错误通过响应返回。
success: false表示命令被拒绝error字段包含错误信息
常见错误:
- 无效命令
- 缺少必需参数
- 代理正在流式传输且未指定
streamingBehavior - 会话不存在(对某些命令)
类型
PromptRequest
{
id?: string,
type: "prompt",
message: string | ({type: "text"; text: string} | {type: "image"; data: string; mimeType: string})[]
}SteerRequest
{
id?: string,
type: "steer",
message: string | ({type: "text"; text: string} | {type: "image"; data: string; mimeType: string})[]
}SetSteeringModeRequest
{
id?: string,
type: "set_steering_mode",
mode: "queued" | "immediate"
}SessionInfoRequest
{
type: "session_info"
}Response
{
type: "response",
command: string,
success: boolean,
data?: any
}示例
基本客户端(Python)
import subprocess
import json
import sys
def rpc_call(cmd: dict) -> dict:
proc = subprocess.Popen(
[sys.executable, "-m", "pi", "--mode", "rpc"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
text=True
)
stdin = proc.stdin
stdout = proc.stdout
stdin.write(json.dumps(cmd) + "\n")
stdin.flush()
for line in stdout:
line = line.rstrip("\r\n")
if not line:
continue
event = json.loads(line)
if event["type"] == "event":
print(f"Event: {event['name']}")
elif event["type"] == "response":
if event.get("success"):
return event.get("data", {})
else:
raise ValueError(event.get("error", "Unknown error"))
# 发送提示
result = rpc_call({
"type": "prompt",
"message": "Hello, RPC world!"
})
print(f"Session info: {result}")交互式客户端(Node.js)
import * as readline from "readline";
import { spawn } from "child_process";
const rpc = (cmd: object) => {
return new Promise((resolve, reject) => {
const proc = spawn("pi", ["--mode", "rpc"]);
const rl = readline.createInterface({ input: proc.stdin, output: proc.stdout });
rl.on("line", (line) => {
const evt = JSON.parse(line);
if (evt.type === "response" && evt.success) {
resolve(evt.data);
} else if (evt.type === "response" && !evt.success) {
reject(new Error(evt.error || "failed"));
}
});
rl.close();
proc.stdin.write(JSON.stringify(cmd) + "\n");
proc.on("error", reject);
proc.on("close", () => proc.kill("SIGTERM"));
});
};
(async () => {
await rpc({ type: "prompt", message: "What's the weather like?" });
})();