跳至内容

RPC 模式

创建于 2025-07-16·更新于 2026-07-16

RPC 模式通过 stdin/stdout 上的 JSON 协议使编码代理能够以无头模式运行。这对于将代理嵌入其他应用程序、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 模式 Starting RPC Mode

pi --mode rpc [options]

常用选项:

  • --provider <name>: 设置 LLM 提供商(anthropic、openai、google 等)
  • --model <pattern>: 模型模式或 ID(支持 provider/id 和可选的 :<thinking>
  • --name <name> / -n <name>: 设置启动时的会话显示名称
  • --no-session: 禁用会话持久化
  • --session-dir <path>: 自定义会话存储目录

协议概述 Protocol Overview

  • 命令 (Commands): 发送到 stdin 的 JSON 对象,每行一个
  • 响应 (Responses): type: "response" 的 JSON 对象,指示命令成功/失败
  • 事件 (Events): 代理事件以 JSON 行形式流式输出到 stdout

所有命令都支持可选的 id 字段用于请求/响应关联。如果提供了该字段,相应的响应将包含相同的 id

帧格式 Framing

RPC 模式使用严格的 JSONL 语义,仅使用 LF(\n)作为记录分隔符。

这对客户端很重要:

  • 仅以 \n 分割记录
  • 通过去除尾随的 \r 来接受可选的 \r\n 输入
  • 不要使用将 Unicode 分隔符视为换行符的通用行读取器

特别是,Node 的 readline 不符合 RPC 模式的协议要求,因为它还会在 U+2028U+2029 处分割,而这些字符在 JSON 字符串中是合法的。

命令 Commands

提示 Prompting

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": "abort"}

响应:

{"type": "response", "command": "abort", "success": true}

new_session

启动一个新会话。可被 session_before_switch 扩展事件处理器取消。

{"type": "new_session"}

带可选的父会话追踪:

{"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}}

状态 State

get_state

获取当前会话状态。

{"type": "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 对象或 nullsessionName 字段是通过 set_session_name 设置的显示名称,如果未设置则省略。

get_messages

获取会话中的所有消息。

{"type": "get_messages"}

响应:

{
  "type": "response",
  "command": "get_messages",
  "success": true,
  "data": {"messages": [...]}
}

消息是 AgentMessage 对象(参见消息类型)。

模型 Model

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": "cycle_model"}

响应:

{
  "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": [...]
  }
}

思考 Thinking

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"}
}

队列模式 Queue Modes

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}

压缩 Compaction

compact

手动压缩对话上下文以减少 token 用量。

{"type": "compact"}

带自定义指令:

{"type": "compact", "customInstructions": "Focus on code changes"}

响应:

{
  "type": "response",
  "command": "compact",
  "success": true,
  "data": {
    "summary": "对话摘要...",
    "firstKeptEntryId": "abc123",
    "tokensBefore": 150000,
    "estimatedTokensAfter": 32000,
    "details": {}
  }
}

estimatedTokensAfter 是基于压缩后重建消息上下文的启发式估算,并非提供商精确的 token 计数。

set_auto_compaction

启用或禁用上下文接近满时的自动压缩。

{"type": "set_auto_compaction", "enabled": true}

响应:

{"type": "response", "command": "set_auto_compaction", "success": true}

重试 Retry

set_auto_retry

启用或禁用临时错误(过载、速率限制、5xx)的自动重试。

{"type": "set_auto_retry", "enabled": true}

响应:

{"type": "response", "command": "set_auto_retry", "success": true}

abort_retry

中止正在进行的重试(取消延迟并停止重试)。

{"type": "abort_retry"}

响应:

{"type": "response", "command": "abort_retry", "success": true}

Bash

bash

执行 shell 命令并将输出添加到对话上下文中。

{"type": "bash", "command": "ls -la"}

响应:

{
  "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": "截断的输出...",
    "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 ...
```

这意味着:

  1. bash 输出会在下一次 prompt 时包含在 LLM 上下文中,而不是立即包含
  2. 可以在一次 prompt 之前执行多个 bash 命令;所有输出都会被包含
  3. BashExecutionMessage 本身不会发出事件

abort_bash

中止正在运行的 bash 命令。

{"type": "abort_bash"}

响应:

{"type": "response", "command": "abort_bash", "success": true}

会话 Session

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 包含当前会话状态下助手的累计用量。contextUsage 包含用于压缩和页脚展示的实际当前上下文窗口估算值。

当没有模型或上下文窗口可用时,省略 contextUsage。在压缩之后、新的压缩后助手响应提供有效用量数据之前,contextUsage.tokenscontextUsage.percentnull

export_html

将会话导出为 HTML 文件。

{"type": "export_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": "原始提示文本...", "cancelled": false}
}

如果扩展取消了分支:

{
  "type": "response",
  "command": "fork",
  "success": true,
  "data": {"text": "原始提示文本...", "cancelled": true}
}

clone

将当前活动分支复制到当前位置的新会话中。可被 session_before_fork 扩展事件处理器取消。

{"type": "clone"}

响应:

{
  "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": "第一条提示..."},
      {"entryId": "def456", "text": "第二条提示..."}
    ]
  }
}

get_entries

按追加顺序获取所有会话条目(不包括会话头部)。会话是一个仅追加的条目树,具有稳定的 ID,因此条目 ID 可用作持久化游标:将你已看到的最后一个条目 ID 作为 since 传入,即可仅获取其之后的条目,即使客户端重启也有效。与 get_messages 不同,这包括压缩前的历史记录和被废弃的分支。

{"type": "get_entries"}

带游标:

{"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": "get_tree"}

响应:

{
  "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": "助手的回复..."}
}

如果不存在助手消息,则返回 {"text": null}

set_session_name

为当前会话设置显示名称。该名称会出现在会话列表中,有助于识别会话。

{"type": "set_session_name", "name": "my-feature-work"}

响应:

{
  "type": "response",
  "command": "set_session_name",
  "success": true
}

当前会话名称可通过 get_statesessionName 字段获取。要在启动 RPC 模式时设置初始名称,请向 pi --mode rpc 进程传递 --name <name>-n <name>

命令 Commands

get_commands

获取可用命令(扩展命令、提示模板和技能)。这些命令可以通过在 prompt 命令前加 / 前缀来调用。

{"type": "get_commands"}

响应:

{
  "type": "response",
  "command": "get_commands",
  "success": true,
  "data": {
    "commands": [
      {"name": "session-name", "description": "设置或清除会话名称", "source": "extension", "path": "/home/user/.pi/agent/extensions/session.ts"},
      {"name": "fix-tests", "description": "修复失败的测试", "source": "prompt", "location": "project", "path": "/home/user/myproject/.pi/agent/prompts/fix-tests.md"},
      {"name": "skill:brave-search", "description": "通过 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 发送则不会执行。

事件 Events

事件在代理运行期间以 JSON 行形式流式输出到 stdout。事件包含 id 字段(只有响应包含)。

事件类型 Event Types

事件 (Event)描述 (Description)
agent_start代理开始处理
agent_end一个底层代理运行完成(仍可能后跟重试、压缩或排队继续)
agent_settled代理运行完全稳定;没有自动重试、压缩重试或排队继续剩余
turn_start新轮次开始
turn_end轮次完成(包括助手消息和工具结果)
message_start消息开始
message_update流式更新(文本/思考/工具调用增量)
message_end消息完成
tool_execution_start工具开始执行
tool_execution_update工具执行进度(流式输出)
tool_execution_end工具完成
queue_update待处理的引导/后续队列已变更
compaction_start压缩开始
compaction_end压缩完成
auto_retry_start自动重试开始(在临时错误之后)
auto_retry_end自动重试完成(成功或最终失败)
extension_error扩展抛出错误

agent_start

代理开始处理提示时发出。

{"type": "agent_start"}

agent_end

一个底层代理运行完成时发出。包含此运行期间生成的所有消息。如果 willRetry 为 true,将进行自动重试。

{
  "type": "agent_end",
  "messages": [...],
  "willRetry": false
}

agent_settled

在整个会话级别运行完全稳定后发出。此时 Pi 将不会通过重试、压缩重试或排队后续消息自动继续。

{"type": "agent_settled"}

turn_start / turn_end

一个轮次由一次助手响应以及由此产生的任何工具调用和结果组成。

{"type": "turn_start"}
{
  "type": "turn_end",
  "message": {...},
  "toolResults": [...]
}

message_start / message_end

消息开始和完成时发出。message 字段包含一个 AgentMessage

{"type": "message_start", "message": {...}}
{"type": "message_end", "message": {...}}

message_update(流式)

助手消息流式传输过程中发出。包含部分消息和流式增量事件。

{
  "type": "message_update",
  "message": {...},
  "assistantMessageEvent": {
    "type": "text_delta",
    "contentIndex": 0,
    "delta": "Hello ",
    "partial": {...}
  }
}

assistantMessageEvent 字段包含以下增量类型之一:

类型 (Type)描述 (Description)
start消息生成开始
text_start文本内容块开始
text_delta文本内容块片段
text_end文本内容块结束
thinking_start思考块开始
thinking_delta思考内容块片段
thinking_end思考块结束
toolcall_start工具调用开始
toolcall_delta工具调用参数片段
toolcall_end工具调用结束(包含完整 toolCall 对象)
done消息完成(原因:"stop""length""toolUse"
error发生错误(原因:"aborted""error"

文本响应流式传输示例:

{"type":"message_update","message":{...},"assistantMessageEvent":{"type":"text_start","contentIndex":0,"partial":{...}}}
{"type":"message_update","message":{...},"assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":"Hello","partial":{...}}}
{"type":"message_update","message":{...},"assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":" world","partial":{...}}}
{"type":"message_update","message":{...},"assistantMessageEvent":{"type":"text_end","contentIndex":0,"content":"Hello world","partial":{...}}}

tool_execution_start / tool_execution_update / tool_execution_end

工具开始执行、流式传输进度和完成执行时发出。

{
  "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": "部分输出..."}],
    "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": ["专注于错误处理"],
  "followUp": ["之后总结结果"]
}

compaction_start / compaction_end

压缩运行时发出,无论是手动还是自动。

{"type": "compaction_start", "reason": "threshold"}

reason 字段为 "manual""threshold""overflow"

{
  "type": "compaction_end",
  "reason": "threshold",
  "result": {
    "summary": "对话摘要...",
    "firstKeptEntryId": "abc123",
    "tokensBefore": 150000,
    "estimatedTokensAfter": 32000,
    "details": {}
  },
  "aborted": false,
  "willRetry": false
}

如果 reason"overflow" 且压缩成功,则 willRetrytrue,代理将自动重试提示。

如果压缩被中止,resultnullabortedtrue

如果压缩失败(例如 API 配额超限),resultnullabortedfalse,且 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"
}

extension_error

扩展抛出错误时发出。

{
  "type": "extension_error",
  "extensionPath": "/path/to/extension.ts",
  "event": "tool_call",
  "error": "错误信息..."
}

扩展 UI 协议 Extension UI Protocol

扩展可以通过 ctx.ui.select()ctx.ui.confirm() 等方式请求用户交互。在 RPC 模式下,这些操作被转换为基于基础命令/事件流之上的请求/回复子协议。

扩展 UI 方法分为两类:

  • 对话框方法 (selectconfirminputeditor):在 stdout 上发出 extension_ui_request,并阻塞等待客户端通过 stdin 发送带有匹配 idextension_ui_response
  • 即发即弃方法 (notifysetStatussetWidgetsetTitleset_editor_text):在 stdout 上发出 extension_ui_request,但不期望回复。客户端可以显示该信息或忽略它。

如果对话框方法包含 timeout 字段,代理端会在超时到期时自动使用默认值解析。客户端无需跟踪超时。

某些 ExtensionUIContext 方法在 RPC 模式下不受支持或功能受限,因为它们需要直接访问 TUI:

  • custom() 返回 undefined
  • setWorkingMessage()setWorkingIndicator()setFooter()setHeader()setEditorComponent()setToolsExpanded() 为无操作
  • getEditorText() 返回 ""
  • getToolsExpanded() 返回 false
  • pasteToEditor() 委托给 setEditorText()(无粘贴/折叠处理)
  • getAllThemes() 返回 []
  • getTheme() 返回 undefined
  • setTheme() 返回 { success: false, error: "..." }

注意:在 RPC 模式下 ctx.mode"rpc"ctx.hasUItrue,因为对话框和即发即弃方法通过扩展 UI 子协议是可用的。使用 ctx.mode === "tui" 来保护需要真实终端的 TUI 特定功能(如 custom())。

扩展 UI 请求(stdout 输出)

所有请求的 type"extension_ui_request",包含唯一的 idmethod 字段。

select

提示用户从列表中选择。包含 timeout 字段的对话框方法包含以毫秒为单位的超时时间;如果客户端未及时响应,代理会自动解析为 undefined

{
  "type": "extension_ui_request",
  "id": "uuid-1",
  "method": "select",
  "title": "允许危险命令?",
  "options": ["允许", "阻止"],
  "timeout": 10000
}

期望的响应:包含 value(所选选项字符串)或 cancelled: trueextension_ui_response

confirm

提示用户进行是/否确认。

{
  "type": "extension_ui_request",
  "id": "uuid-2",
  "method": "confirm",
  "title": "清除会话?",
  "message": "所有消息将丢失。",
  "timeout": 5000
}

期望的响应:包含 confirmed: true/falsecancelled: trueextension_ui_response

input

提示用户输入自由格式文本。

{
  "type": "extension_ui_request",
  "id": "uuid-3",
  "method": "input",
  "title": "输入一个值",
  "placeholder": "输入内容..."
}

期望的响应:包含 value(输入的文本)或 cancelled: trueextension_ui_response

editor

打开一个多行文本编辑器,可选预填内容。

{
  "type": "extension_ui_request",
  "id": "uuid-4",
  "method": "editor",
  "title": "编辑一些文本",
  "prefill": "第1行\n第2行\n第3行"
}

期望的响应:包含 value(编辑后的文本)或 cancelled: trueextension_ui_response

notify

显示通知。即发即弃,不期望回复。

{
  "type": "extension_ui_request",
  "id": "uuid-5",
  "method": "notify",
  "message": "命令已被用户阻止",
  "notifyType": "warning"
}

notifyType 字段为 "info""warning""error"。如果省略,默认为 "info"

setStatus

设置或清除页脚/状态栏中的状态条目。即发即弃。

{
  "type": "extension_ui_request",
  "id": "uuid-6",
  "method": "setStatus",
  "statusKey": "my-ext",
  "statusText": "第3轮运行中..."
}

发送 statusText: undefined(或省略该字段)以清除该键对应的状态条目。

setWidget

设置或清除编辑器上方或下方显示的小部件(文本行块)。即发即弃。

{
  "type": "extension_ui_request",
  "id": "uuid-7",
  "method": "setWidget",
  "widgetKey": "my-ext",
  "widgetLines": ["--- 我的小部件 ---", "第1行", "第2行"],
  "widgetPlacement": "aboveEditor"
}

发送 widgetLines: undefined(或省略该字段)以清除小部件。widgetPlacement 字段为 "aboveEditor"(默认)或 "belowEditor"。RPC 模式仅支持字符串数组;组件工厂将被忽略。

setTitle

设置终端窗口/标签标题。即发即弃。

{
  "type": "extension_ui_request",
  "id": "uuid-8",
  "method": "setTitle",
  "title": "pi - 我的项目"
}

set_editor_text

设置输入编辑器中的文本。即发即弃。

{
  "type": "extension_ui_request",
  "id": "uuid-9",
  "method": "set_editor_text",
  "text": "为用户预填的文本"
}

扩展 UI 响应(stdin 输入)

仅针对对话框方法(selectconfirminputeditor)发送响应。id 必须与请求匹配。

值响应(select、input、editor)

{"type": "extension_ui_response", "id": "uuid-1", "value": "允许"}

确认响应(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}

错误处理 Error Handling

失败的命令返回 success: false 的响应:

{
  "type": "response",
  "command": "set_model",
  "success": false,
  "error": "未找到模型: invalid/model"
}

解析错误:

{
  "type": "response",
  "command": "parse",
  "success": false,
  "error": "无法解析命令: 意外的标记..."
}

类型 Types

源文件:

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"

ToolResultMessage

{
  "role": "toolResult",
  "toolCallId": "call_123",
  "toolName": "bash",
  "content": [{"type": "text", "text": "total 48\ndrwxr-xr-x ..."}],
  "isError": false,
  "timestamp": 1733234567890
}

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)Example: Basic Client (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({"type": "prompt", "message": "Hello!"})

# 处理事件
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)Example: Interactive Client (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);
        }
    }
});

// 发送提示
agent.stdin.write(JSON.stringify({ type: "prompt", message: "Hello" }) + "\n");

// Ctrl+C 中止
process.on("SIGINT", () => {
    agent.stdin.write(JSON.stringify({ type: "abort" }) + "\n");
});