11-集成方式-SDK-JSON-RPC
11 集成方式:SDK、CLI、JSON 与 RPC
读这一篇的价值:Pi 不只是一个 TUI。同一套 agent、会话、资源、工具可以用四种方式驱动:交互式 TUI、打印、JSON 事件流、RPC 子进程;进程外还能用 SDK 直接内嵌。这一篇把边界、协议帧格式和消息类型完整说清楚,也是后续写自定义客户端/IDE 插件的依据。
1. 四种模式怎么选
| 模式 | 接口 | 生命周期 | 适用 |
|---|---|---|---|
| 交互式 | 终端 UI | 到用户退出 | 人直接用 |
打印 --print | stdout 上的最终文本 | 单次调用 | 脚本只要最终回答 |
JSON --mode json | stdout 上的 JSONL 事件 | 单次调用 | 进程需要结构化过程输出 |
RPC --mode rpc | JSONL 命令 / 响应 / 事件 | 长驻 | 进程需要双向控制 |
- 四种模式共用同一套 agent、会话、资源与工具;模式只决定输入怎么进、输出怎么出、进程是否留下来继续接命令。
- CLI 选项(工作目录、模型、工具、资源、会话持久化)与模式正交。
- SDK 不是一种 CLI 模式:它把 agent 直接内嵌进 Node.js / Bun 进程。
- 未显式选模式时,stdin 或 stdout 非 TTY 也走打印模式(管道输入/输出不用加
--print)。
2. 打印模式
pi --print "Summarize the changes in this repository"
- 跑完给的 prompt,把最终 assistant 文本写到 stdout,退出。中间事件不暴露。
- 错误写 stderr;最终回答的 stop reason 是
error或aborted时退出码非零。
3. JSON 事件流
pi --mode json "Review this repository" > events.jsonl
注意:这是结构化事件输出,既不是单个 JSON 结果,也不约束模型回答的格式。所有 prompt 在进程启动时给定,跑完即退出,不接受后续命令。
帧格式
- 严格 JSONL:每条记录一个 JSON 对象,以 LF (
\n) 结尾。只能按 LF 切分,可选地剥掉前面的 CR。Unicode 的行分隔符 / 段落分隔符(U+2028/U+2029)在 JSON 字符串内合法,不是记录边界。 - 不要用 Node.js
readline读这个流:它会把上面两个 Unicode 分隔符也当换行。用 byte / UTF-8 流解码器自己按 LF 切。 - 要持续读 stdout:读端停住会在管道缓冲满时把 Pi 堵住。stdout 只留 JSONL,诊断与日志写 stderr。
会话头
JSON 模式的第一条记录是会话头(session-format.md#sessionheader):
{"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path"}
RPC 模式不发这条(它是双向长驻协议),要当前会话 ID 与文件用 get_state。
事件序列
{"type":"agent_start"}
{"type":"turn_start"}
{"type":"message_start","message":{"role":"user","content":"Review this repository","timestamp":1733234401000}}
{"type":"message_end","message":{"role":"user","content":"Review this repository","timestamp":1733234401000}}
{"type":"message_start","message":{"role":"assistant","content":[],"stopReason":"pending","...":"..."}}
{"type":"message_update","usage":{"...":"..."},"assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":"Hello"}}
{"type":"message_end","message":{"role":"assistant","...":"..."}}
{"type":"turn_end","message":{"role":"assistant","...":"..."},"toolResults":[]}
{"type":"agent_end","messages":[{"...":"..."}],"willRetry":false}
{"type":"agent_settled"}
agent_end 不是终点:自动重试、溢出恢复、压缩重试、steering、follow-up 都可能继续。agent_settled 才表示 Pi 在这一层没有剩余自动工作了。
事件总表
agent 与 turn:
| 事件 | 字段 | 含义 |
|---|---|---|
agent_start | 无 | 一次低层 agent 运行开始 |
agent_end | messages, willRetry | 该低层运行结束;messages 是本次产生的消息 |
agent_settled | 无 | Pi 不会再因重试 / 压缩恢复 / 排队消息自动继续 |
turn_start | 无 | 一个 assistant turn 开始 |
turn_end | message, toolResults | 一次 assistant 响应及其工具调用结束 |
一个 turn = 一次 assistant 响应 + 这次响应产生的工具调用与工具结果。
消息:
| 事件 | 字段 | 含义 |
|---|---|---|
message_start | message | 消息开始 |
message_update | usage, assistantMessageEvent | assistant 消息产出内容块更新 |
message_end | message | 消息完成,这是权威最终消息 |
工具执行:
| 事件 | 字段 | 含义 |
|---|---|---|
tool_execution_start | toolCallId, toolName, args | 工具开始执行 |
tool_execution_update | toolCallId, toolName, args, partialResult | 工具报告部分结果 |
tool_execution_end | toolCallId, toolName, result, isError | 工具执行结束 |
partialResult 是工具给的最新部分结果;它是替换还是追加早先的更新取决于该工具的结果契约。
队列与状态:
| 事件 | 字段 | 含义 |
|---|---|---|
queue_update | steering, followUp | 待处理的 steering / follow-up 队列变化(两个字段都是完整当前队列) |
entry_appended | entry | 扩展通过 pi.appendEntry() 追加了自定义会话条目 |
session_info_changed | name | 会话显示名变化;name 缺失表示被清除 |
thinking_level_changed | level | 当前 thinking 级别变化 |
压缩:
{"type":"compaction_start","reason":"threshold"}
reason 为 "manual" / "threshold" / "overflow"。成功时 compaction_end 带 result(含 summary、firstKeptEntryId、tokensBefore、estimatedTokensAfter、usage、details)与 aborted、willRetry。被中止时无 result 且 aborted: true;失败时无 result、aborted: false、有 errorMessage。溢出恢复成功会把 willRetry 置 true 然后再试一次 prompt。
重试:
{"type":"auto_retry_start","attempt":1,"maxAttempts":3,"delayMs":2000,"errorMessage":"529 overloaded"}
{"type":"auto_retry_end","success":true,"attempt":2}
最终失败时 auto_retry_end 的 success: false 并带 finalError 字符串。压缩与分支摘要有另一组:summarization_retry_scheduled / summarization_retry_attempt_start(带 source,"compaction" 或 "branchSummary")/ summarization_retry_finished。
RPC 独有:bash 命令每块输出发一条 bash_execution_update(id 与命令 ID 对应,流的是全部输出,而最终命令响应可能被截断);扩展处理器抛异常时发 extension_error(extensionPath、event、error)。扩展 UI 记录是另一个子协议,不是 AgentSessionEvent。
流式消息重建(重要陷阱)
message_update 在线上是纯增量的:它省略了 SDK 事件里累积的 message 字段和每个 assistantMessageEvent.partial 快照,以保证流大小线性增长。嵌套事件类型:
| 类型 | 额外字段 | 含义 |
|---|---|---|
start | 无 | 供应商流开始(线上移除了累积 partial) |
text_start | contentIndex | 文本块开始 |
text_delta | contentIndex, delta | 追加文本 |
text_end | contentIndex, content | 文本块结束,带权威内容 |
thinking_start | contentIndex | 思考块开始 |
thinking_delta | contentIndex, delta | 追加思考文本 |
thinking_end | contentIndex, content | 思考块结束,带权威内容 |
toolcall_start | contentIndex, id, toolName | 工具调用块开始 |
toolcall_delta | contentIndex, delta | 追加序列化参数 |
toolcall_end | contentIndex, toolCall | 工具调用结束,带完整 ToolCall |
done | reason, message | 供应商流成功结束 |
error | reason, error | 供应商流以错误或中止结束 |
- 正常 agent 循环把供应商级
start/done/error转成message_start/message_end,不作为message_update发出。 contentIndex用来标识内容块;用delta缓冲做实时显示,但到text_end/thinking_end/toolcall_end时必须换成权威内容;message_end.message到达时替换整个部分消息。- 顶层
usage是这条 assistant 响应最新累积的供应商用量报告;供应商不在流式过程中报告用量时,它可能一直是 0 直到完成。
TypeScript 类型
SDK 的 AgentSessionEvent 带累积流快照;JSON / RPC 只改写 message_update:
type WithoutPartial<T> = T extends { partial: unknown } ? Omit<T, "partial"> : T;
type JsonAssistantMessageEvent<T> = T extends { type: "toolcall_start"; partial: unknown }
? WithoutPartial<T> & { id: string; toolName: string }
: WithoutPartial<T>;
type JsonAgentSessionEvent =
| Exclude<AgentSessionEvent, { type: "message_update" }>
| { type: "message_update"; usage: Usage; assistantMessageEvent: JsonAssistantMessageEvent<AssistantMessageEvent> };
实现见仓库 packages/coding-agent/src/modes/json-event.ts。
快捷用法:
pi --mode json "List files" 2>/dev/null | jq -c 'select(.type == "message_end")'
4. RPC 模式
pi --mode rpc --no-session
作为长驻子进程,用 stdin / stdout 上的 JSON 记录控制。适合跨语言集成、进程隔离、IDE 与自定义 UI。TypeScript 宿主优先用导出的 RpcClient(拉起子进程、关联响应、提供带类型的命令方法、把事件派发给监听器)。
RPC 模式拒绝 @file prompt 参数,prompt 一律走 prompt 命令。
四类记录
| 方向 | 记录 | 作用 |
|---|---|---|
| stdin | 命令 | 让 Pi 提问、查状态、改配置、管理会话 |
| stdout | response | 报告一条命令是否成功并返回数据 |
| stdout | 会话事件 | 流式报告运行、消息、工具、队列、压缩、重试活动 |
| 双向 | 扩展 UI 记录 | 转发支持的扩展交互 |
命令关联与帧格式
- 每条命令接受可选字符串
id,对应响应原样回传:
{"id":"req-1","type":"get_state"}
{"id":"req-1","type":"response","command":"get_state","success":true,"data":{"...":"..."}}
- 命令处理是异步的:可能有多条命令在飞,客户端必须按 ID 关联,不能按响应顺序。
- 会话事件一般没有命令 ID(它们描述会话活动);
bash_execution_update是例外:发起它的bash命令有 ID 时,输出事件重复该 ID。 extension_ui_response用它对应的extension_ui_request的 ID,不产生普通命令响应。- 帧格式与 JSON 模式相同(严格 JSONL、只按 LF 切、要持续读、stdout 只留协议记录)。
运行生命周期
{"id":"req-2","type":"prompt","message":"Review this repository"}
{"id":"req-2","type":"response","command":"prompt","success":true,"data":{"disposition":"started"}}
prompt 成功只表示 prompt 被接受 / 排队 / 处理,不表示模型工作完成。data.disposition 为 "handled" 时没有起运行,不要等 agent_settled。之后要继续消费事件,agent_settled 才代表 Pi 不会再自动继续。
发 prompt 前先订阅事件,否则可能错过快速完成。RpcClient.promptAndWait() 内部就是这么做的。
错误
失败命令返回一条 success: false 的响应:
{"id":"req-3","type":"response","command":"set_model","success":false,"error":"Model not found: invalid/model"}
JSON 解析失败产生一条没有请求 ID 的响应:{"type":"response","command":"parse","success":false,"error":"Failed to parse command: ..."}。
关键区分:success 只覆盖命令处理;prompt 被接受之后发生的供应商失败或中止出现在消息流与事件流里,不会为同一个请求 ID 再发第二条响应。客户端还要自己处理子进程启动失败、意外退出、stderr 诊断、取消与自己设的超时——不要把 stderr 当协议数据解析。
关闭
关掉子进程的 stdin 请求有序关闭,Pi 会先 dispose 活动运行时再退出。扩展也能通过自己的 context 请求关闭;Pi 在当前命令之后、或活动运行发出 agent_settled 之后完成关闭。
命令清单
提示类:prompt(可带 images,流式期间必须给 streamingBehavior)、steer、follow_up、abort、clear_queue。
- 流式期间发
prompt不带streamingBehavior会报错。"steer"= 当前 assistant turn 执行完工具调用后、下一次 LLM 调用前送达;"followUp"= 等 agent 停下才送达。扩展命令(如/mycommand)即使在流式中也立即执行;skill 命令(/skill:name)与提示模板(/template)在发送/排队前先展开。 steer/follow_up会展开 skill 与模板,但不允许扩展命令(要用prompt)。prompt的data.disposition:"handled"(扩展命令或输入处理器消费了它)、"queued"(运行中被排队)、"started"(接受并起了一次运行)。- 实现交互式 Esc 的标准做法:先
clear_queue拿回待发文本,再abort,然后把文本还原到客户端编辑器(abort会继续处理仍留在会话里的排队消息)。
状态:get_state(model / thinkingLevel / isStreaming / isCompacting / steeringMode / followUpMode / sessionFile / sessionId / sessionName / autoCompactionEnabled / messageCount / pendingMessageCount)、get_messages。
模型:set_model(provider + modelId,响应是完整 Model 对象)、cycle_model、get_available_models。
思考:set_thinking_level(off / minimal / low / medium / high / xhigh / max;后两档只在所选模型支持时暴露)、cycle_thinking_level、get_available_thinking_levels(不支持推理的模型返回 ["off"])。
队列模式:set_steering_mode 与 set_follow_up_mode,各支持 "all" 与 "one-at-a-time"(默认)。
压缩与重试:compact(可带 customInstructions;estimatedTokensAfter 是压缩后重建上下文的启发式估计,不是供应商精确 token)、set_auto_compaction、set_auto_retry、abort_retry。
Shell:bash(excludeFromContext: true 时输出存进会话但在下次 prompt 时不进模型上下文)、abort_bash。
bash 结果怎么到模型(容易踩坑):bash 立即执行并返回 BashResult,内部创建一条 BashExecutionMessage 存进 agent 消息状态;下次 prompt 时 Pi 才把它转成 user 消息(Ran \ls -la`+ 代码块)发给模型。所以:① 输出在**下一个 prompt** 才被模型看到,不是立刻;② 一个 prompt 之前可以跑多条 bash,只要没设excludeFromContext`,每条都会带进去。
会话管理:get_session_stats、export_html(可带 outputPath)、new_session(可带 parentSession)、switch_session、fork(带 entryId,返回被 fork 的消息文本)、clone、get_fork_messages、get_entries(带 since 游标,只取严格在其之后的条目;包含压缩前的历史与被丢弃的分支,所以能当持久游标用)、get_tree、get_last_assistant_text、set_session_name。
new_session/switch_session/fork/clone都可能被扩展的session_before_switch/session_before_fork处理器取消,响应里表现为data.cancelled: true。get_entries的响应带leafId(当前叶子,空会话为null),客户端一次往返就能知道活动分支是否变了;since不匹配任何条目 ID 时success: false。get_tree返回数组(导航 API 可能造出多个根),节点是{entry, children, label?, labelTimestamp?};父链断裂的孤儿条目也作为根出现。
可发现命令:get_commands 返回扩展命令、提示模板、技能。每项有 name、description?、source("extension" / "prompt" / "skill")、sourceInfo(path、source 如 local/auto/cli、scope 为 user/project/temporary、origin 为 top-level/package、baseDir?)。内置 TUI 命令(/settings、/hotkeys 等)不在里面:它们只在交互模式处理,用 prompt 发不会执行。
Model 对象(成本为美元 / 百万 token):
{
"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 }
}
最小客户端
Python 例(用二进制管道读取器,只按 LF 切,不把 Unicode 分隔符当边界):
import json, subprocess
process = subprocess.Popen(["pi", "--mode", "rpc", "--no-session"],
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
command = {"id": "prompt-1", "type": "prompt", "message": "Hello"}
process.stdin.write(json.dumps(command).encode("utf-8") + b"\n")
process.stdin.flush()
while line := process.stdout.readline():
record = json.loads(line)
if record.get("type") == "message_update":
update = record["assistantMessageEvent"]
if update["type"] == "text_delta":
print(update["delta"], end="", flush=True)
elif record.get("type") == "agent_settled":
print()
break
process.stdin.close()
process.wait()
扩展 UI 子协议
扩展通过 ctx.ui 请求交互,RPC 模式下分两类:
- 对话框方法(
select、confirm、input、editor):在 stdout 发extension_ui_request,并阻塞等待客户端在 stdin 回一条 ID 相同的extension_ui_response。 - 单发方法(
notify、setStatus、setWidget、setTitle、set_editor_text):只发请求,不期待响应,客户端可显示或忽略。
对话框方法带 timeout 时由 agent 侧到期自动用默认值解决(select/input/editor 用 undefined,confirm 用 false),客户端不用自己计时。
请求形状:
{"type":"extension_ui_request","id":"uuid-1","method":"select","title":"Allow dangerous command?","options":["Allow","Block"],"timeout":10000}
{"type":"extension_ui_request","id":"uuid-2","method":"confirm","title":"Clear session?","message":"All messages will be lost."}
{"type":"extension_ui_request","id":"uuid-3","method":"input","title":"Enter a value","placeholder":"type something..."}
{"type":"extension_ui_request","id":"uuid-4","method":"editor","title":"Edit some text","prefill":"Line 1\nLine 2"}
{"type":"extension_ui_request","id":"uuid-5","method":"notify","message":"Command blocked by user","notifyType":"warning"}
{"type":"extension_ui_request","id":"uuid-6","method":"setStatus","statusKey":"my-ext","statusText":"Turn 3 running..."}
{"type":"extension_ui_request","id":"uuid-7","method":"setWidget","widgetKey":"my-ext","widgetLines":["Line 1","Line 2"],"widgetPlacement":"aboveEditor"}
notifyType为info/warning/error,缺省info。setStatus/setWidget用statusText: undefined(或省略)、widgetLines: undefined清除。widgetPlacement缺省aboveEditor;RPC 模式只支持字符串数组,组件工厂被忽略。
响应(只对四种对话框方法):
{"type":"extension_ui_response","id":"uuid-1","value":"Allow"}
{"type":"extension_ui_response","id":"uuid-2","confirmed":true}
{"type":"extension_ui_response","id":"uuid-3","cancelled":true}
取消时扩展收到 undefined(select/input/editor)或 false(confirm)。
RPC 模式下不支持或降级的能力(都要求真实终端):custom() 返回 undefined;onTerminalInput() 返回空 unsubscribe;setWorkingMessage / setWorkingVisible / setWorkingIndicator / setHiddenThinkingLabel / setFooter / setHeader / addAutocompleteProvider / setEditorComponent / setToolsExpanded 都是 no-op;getEditorText() 返回 "",getEditorComponent() 返回 undefined,getToolsExpanded() 返回 false;pasteToEditor() 退化为 setEditorText();getAllThemes() 返回 [],getTheme() 返回 undefined,setTheme() 返回 { success: false, error: "Theme switching not supported in RPC mode" }。
注意:RPC 模式下 ctx.mode === "rpc" 且 ctx.hasUI === true(对话框与单发方法可用);要守 TUI 专属能力(如 custom())应该判 ctx.mode === "tui"。
5. SDK:把 Pi 嵌进进程
import { createAgentSession } from "@earendil-works/pi-coding-agent";
const { session } = await createAgentSession();
try {
await session.prompt("What files are in the current directory?");
console.log(session.getLastAssistantText());
} finally {
session.dispose();
}
默认使用当前工作目录、自动发现的资源、已存设置与已配凭据。prompt() 在运行结束时 resolve。
会话生命周期
createAgentSession() 创建 AgentSession:一个会话拥有一段对话、它的模型与工具、排队消息、压缩状态与扩展运行时。
- 读状态:
session.messages、session.model、session.thinkingLevel、session.systemPrompt、session.getActiveToolNames()。 session.systemPrompt只读,返回当前生效的系统提示词(含尚未发给模型的改动);工具变更会在下一次请求前声明给模型。- 持久化由
SessionManager负责:它拥有持久或内存的条目树,并跟踪活动叶子;分支只改叶子,不删废弃分支。重建模型上下文时,manager 选活动分支并施加压缩。 SessionManager是最终模型上下文的权威。要恢复外部历史,就用包含那些条目的 manager 构造会话;直接给session.agent.state.messages赋值不会替换持久化上下文。- 不需要会话文件时用内存 manager:
const { session } = await createAgentSession({ sessionManager: SessionManager.inMemory() });
cwd决定工作区:项目资源发现、上下文文件、会话分组、内置工具路径。目标不是process.cwd()时显式传。session.dispose()会中止活动工作、失效扩展上下文、断开 agent 连接、移除事件监听——不再需要会话时必须调用。AgentSessionRuntime在其上加newSession()、switchSession()、fork()、importFromJsonl();每次操作都会替换活动AgentSession并为目标工作目录重建服务。替换之后旧的订阅属于旧AgentSession,必须重新绑定。
提问
prompt() 会先处理扩展命令、展开基于文件的提示模板,然后普通用户消息才进入 agent。被接受的运行会在跑完(含自动重试)后 resolve。
- 会话正在流式时再发 prompt 必须指明是 steering 还是 follow-up,不指定会 reject 而不是猜。
steer()/followUp()直接暴露这两种行为,返回"queued"(已排队,含被扩展改写后)或"handled"(被扩展消费)。abort()停止活动操作并等会话空闲;waitForIdle()只等不中止。
订阅事件
const unsubscribe = session.subscribe((event) => {
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
process.stdout.write(event.assistantMessageEvent.delta);
}
});
try { await session.prompt("Explain this repository"); } finally { unsubscribe(); }
- 事件覆盖消息更新、工具执行、队列、压缩、重试、运行生命周期。
message_end带权威完成消息;agent_end只标记一次低层 agent 运行结束,之后仍可能有自动恢复或排队工作;需要知道「Pi 不会再自动继续」时用agent_settled。
可替换的边界
默认工厂会创建 ModelRuntime、文件后端 SettingsManager、持久 SessionManager、DefaultResourceLoader 与配置好的默认工具。每一层都能显式替换:
modelRuntime、model、thinkingLevel、scopedModels:模型访问与选择。settingsManager:合并设置或内存配置。sessionManager:持久或内存对话历史。resourceLoader:扩展、技能、提示模板、主题、上下文文件。tools、noTools、excludeTools、customTools:活动工具集。
想用标准发现 + 局部覆盖就用 DefaultResourceLoader;宿主完全自己管资源存储与发现时提供自定义 ResourceLoader。
内联扩展通过 DefaultResourceLoader 提供:需要诊断与启动输出里有稳定名字时才给 InlineExtension 命名。命名的内联扩展若标 replaceable: true,当另一个扩展在加载期间注册了同名工具 / 命令 / flag 时它会被让位而不是双双冲突加载——CLI 内置的 codemode、tool search、MCP 扩展都是 replaceable。标了 builtin: true 的命名条目不是内联扩展:它提供 builtin:<name> 扩展的代码,像配置文件扩展一样加载,默认加载、在 pi config 里列出、可用 extensions 设置里的 -builtin:<name> 或 noExtensions 禁用,additionalExtensionPaths: ["builtin:<name>"] 可显式加载;它在项目信任解析之后加载,所以不能处理 project_trust。
SDK 不加载内置扩展:CLI 把 codemode、tool_search、MCP 当内置扩展加载,SDK 会话不会。要在 SDK 里加它们,把 createCodemodeExtension()、createToolSearchExtension()、createMcpExtension() 加进 DefaultResourceLoader 的 extensionFactories。codemode 与 tool_search 注册时是非活动的:用 defaultTools 设置启用(["+codemode", "+tool_search"] 保留其他默认工具),或让 MCP 扩展激活(有 codemode 曝光服务器时激活 codemode,有 deferred 时激活 tool_search)。MCP 扩展在 session_start 时连服务器,所以要调 session.bindExtensions()。
官方 SDK 示例(仓库 examples/sdk/,全部参与类型检查):
| 示例 | 用途 |
|---|---|
01-minimal.ts | 创建、提问、观察、dispose |
02-custom-model.ts | 选模型与 thinking 级别 |
03-custom-prompt.ts | 替换或追加系统提示词 |
04-skills.ts | 发现、过滤、新增技能 |
05-tools.ts | 选内置工具与其工作目录 |
06-extensions.ts | 加载文件型与内联扩展 |
07-context-files.ts | 增加或替换项目说明 |
08-prompt-templates.ts | 加文件风格提示模板 |
09-api-keys-and-oauth.ts | 凭据与模型存储 |
10-settings.ts | 文件或内存设置 |
11-sessions.ts | 会话持久化与恢复 |
12-full-control.ts | 替换默认发现与状态服务 |
13-session-runtime.ts | 安全替换活动会话 |
14-codemode-mcp.ts | 加 codemode、tool_search、MCP 扩展 |
6. 消息类型(共享词汇表)
AgentMessage 出现在 SDK 状态、生命周期事件、RPC 响应与持久化会话条目里。消息时间戳是 Unix 毫秒,与会话条目上的 ISO 8601 时间戳不同。
内容块
interface TextContent { type: "text"; text: string; textSignature?: string }
interface ImageContent { type: "image"; data: string; mimeType: string } // data 是 base64
interface ThinkingContent { type: "thinking"; thinking: string; thinkingSignature?: string; redacted?: boolean }
interface ToolCall { type: "toolCall"; id: string; name: string; arguments: Record<string, any>;
thoughtSignature?: string; namespace?: string }
textSignature、thinkingSignature、thoughtSignature都是供应商专属元数据,当作不透明字符串。被 redact 的思考块可能没有可见文本,但在thinkingSignature里保留加密负载。namespace标识 OpenAI Responses 里动态加载或带命名空间的工具。
Usage
interface Usage {
input: number; output: number; cacheRead: number; cacheWrite: number; cacheWrite1h?: number;
reasoning?: number; totalTokens: number;
cost: { input: number; output: number; cacheRead: number; cacheWrite: number; total: number };
}
assistant 消息总是带 usage;工具结果在它做了嵌套模型工作时也可能带。reasoning 存在时已经包含在 output 里,不要再加一次;cacheWrite1h 是 cacheWrite 中按 1 小时保留写入的子集。
基础消息
interface SystemMessage {
role: "system"; content: string | TextContent[];
sections?: Record<string, string | null>;
toolsAdded?: Tool[]; toolsRemoved?: ToolReference[];
replace?: boolean; timestamp: number;
}
interface UserMessage { role: "user"; content: string | (TextContent | ImageContent)[]; timestamp: number }
interface AssistantMessage {
role: "assistant"; content: (TextContent | ThinkingContent | ToolCall)[];
api: string; provider: string; model: string; responseModel?: string; responseId?: string;
providerThinkingLevel?: string; diagnostics?: AssistantMessageDiagnostic[];
usage: Usage;
stopReason: "pending" | "stop" | "length" | "toolUse" | "error" | "aborted" | "deferred";
deferred?: DeferredHandle; errorMessage?: string; rawStopReason?: string;
endTurn?: boolean; timestamp: number;
}
interface ToolResultMessage<TDetails = any> {
role: "toolResult"; toolCallId: string; toolName: string;
content: (TextContent | ImageContent)[]; details?: TDetails; usage?: Usage;
isError: boolean; timestamp: number;
}
要点:
- 开头的 system 消息声明初始提示词与工具;后续 system 消息可以追加指令、替换或移除命名提示词分段、增删工具,按序重放就得到当前状态;带
replace: true的消息丢弃先前状态、建立完整新基线。 "pending"用于流式中的部分 assistant 消息;message_end里的完成消息一定有终止 stop reason,且 Pi 不把"pending"的 assistant 消息写进会话 JSONL。"deferred"响应带DeferredHandle(provider、modelId、api、id、expiresAt?、pollAfterMs?、data?),用于稍后取回结果。ToolResultMessage.usage报告该工具做的嵌套模型工作,计入整会话统计,但不算主模型调用的 usage。
coding-agent 追加的四种角色
interface BashExecutionMessage {
role: "bashExecution"; command: string; output: string; exitCode: number | undefined;
cancelled: boolean; truncated: boolean; fullOutputPath?: string;
excludeFromContext?: boolean; timestamp: number;
}
interface CustomMessage<T = unknown> {
role: "custom"; customType: string; content: string | (TextContent | ImageContent)[];
display: boolean; details?: T; timestamp: number;
}
interface BranchSummaryMessage { role: "branchSummary"; summary: string; fromId: string | null; timestamp: number }
interface CompactionSummaryMessage { role: "compactionSummary"; summary: string; tokensBefore: number; timestamp: number }
BashExecutionMessage由直接 shell 命令(含 RPCbash)创建,不是 LLM 工具结果;除非excludeFromContext为 true,Pi 会在下次模型请求前把它转成 user 角色文本。CustomMessage由扩展发上下文消息创建;内容被转成 user 消息进入模型,display只控制终端渲染,details不发给模型。BranchSummaryMessage/CompactionSummaryMessage由持久化的branch_summary/compaction条目生成。
联合类型:
type AgentMessage =
| SystemMessage | UserMessage | AssistantMessage | ToolResultMessage
| BashExecutionMessage | CustomMessage | BranchSummaryMessage | CompactionSummaryMessage;
在更底层的 agent 包里 AgentMessage 是 Message | CustomAgentMessages[keyof CustomAgentMessages],应用可以用 TypeScript 声明合并添加角色——接受来自被扩展宿主的消息时要容忍未知的自定义角色。
来源文件:packages/ai/src/types.ts(供应商侧消息与内容块)、packages/agent/src/types.ts(可扩展 AgentMessage 联合)、packages/coding-agent/src/core/messages.ts(coding-agent 角色)。
7. 换名/换配置目录(源码 fork)
package.json 里可以改 CLI 名与配置目录:
{ "piConfig": { "name": "my-agent", "configDir": ".my-agent" } }
顶层 bin 字段决定可执行文件名。这些设置影响 CLI 横幅、配置路径与派生出的环境变量名。
8. 选型速查
- 只要最终文本、在 shell 里用 → 打印模式。
- 要结构化进度、一次性、结果落文件 → JSON 模式(记得只按 LF 切、持续读)。
- 要跨语言 / IDE / 进程隔离 / 双向控制 → RPC 模式(TypeScript 用
RpcClient)。 - 就在 Node.js / Bun 进程里、要完整 API 访问 → SDK(注意 SDK 不加载内置扩展,
SessionManager才是上下文权威,agent_settled才代表结束)。
溯源
- 官方
docs/sdk.md、docs/cli-integration.md、docs/json.md、docs/rpc.md、docs/rpc-commands.md、docs/rpc-extension-ui.md、docs/message-types.md