pi 事件流 字段参考
本文是 pi -p --mode json 输出的事件流的字段参考:每种事件何时出现、有哪些字段、字段的类型与含义、典型运行的顺序、错误与退出码。写给要为这条流写解析器的人,不依赖 agentrun 的其他文档。依据是 pi 1.0.1(npm 包 @earendil-works/pi-coding-agent)在 Linux 容器里的实测输出,以及随包安装的 docs/json.md、docs/message-types.md、docs/session-format.md、docs/cli-integration.md 与 CHANGELOG.md。字段表中标出了每一项的来源。
读这条流的三条规则:第一行是
session头,之后每行一个事件,按type分发,不认识的类型跳过;message_end里的message是权威的完整消息,message_update只用于实时显示;成败看最后一个 assistant 消息的stopReason,退出码只区分启动失败与被中断,模型调用失败时退出码仍是 0。
代码位置
| 位置 | 内容 |
|---|---|
src/runtime/pi.rs | Pi(launch、translate、failure) |
tests/fixtures/pi/ | 本页样本的原始输出 *.raw.jsonl 与对应的统一事件 *.events.jsonl,格式见测试与发布 |
tests/pi.rs | 回放这些样本的 replay_* 测试与用假 pi 脚本做的适配器测试 |
怎样产生这条流
pi -p --mode json \
--no-session --no-extensions --no-skills --no-prompt-templates --no-themes --no-context-files \
--offline --tools read,bash,edit,write,grep,find,ls \
-- "提示词"
--mode json 选择逐行 JSON 输出,处理完命令行给出的全部提示词后退出,运行中不接受新的输入。-p(--print)在 --mode json 下可以省略,加上它的效果相同。提示词也可以从标准输入管道传入。标准输出里只有 JSON 行,诊断信息写到标准错误。
agentrun 的 Pi::launch 生成的命令以本节开头的命令为基础。
以下选项改变流的内容:
| 选项 | 对流的影响 |
|---|---|
--model <pattern>、--provider <name> | 决定 assistant 消息的 provider、model、api 字段。模型名不存在时进程启动失败,见「错误、中断与退出码」 |
--thinking <level> | off、minimal、low、medium、high、xhigh、max。决定 assistant 消息的 thinkingLevel,以及是否出现 thinking 内容块。也可以写成 --model 名字:级别 |
--tools <list> | 替换整个工具列表,决定 system 消息的 toolsAdded 与模型能调用的工具名。默认是 read、bash、edit、write |
--session-id <id> | 决定 session 头的 id。不给时 pi 生成一个 UUID v7 |
--no-session | 不把会话写到磁盘。流的内容不变,仍然输出 session 头 |
--no-extensions、--no-skills、--no-prompt-templates、--no-themes、--no-context-files | 不加载对应的扩展与配置。关闭扩展后不会出现扩展产生的事件(entry_appended、自定义消息角色);关闭上下文文件与技能后 system 消息的 sections 更短 |
--offline | 不检查更新、不刷新模型目录。流的内容不变 |
--system-prompt、--append-system-prompt | 改变 system 消息 sections 的内容。未实测 |
模型 API 的凭据通过环境变量(例如 DEEPSEEK_API_KEY、ANTHROPIC_API_KEY)、pi auth 保存的登录信息或 --api-key 提供。本文样本使用 deepseek provider 的 deepseek-flash 模型,走 openai-completions API;其他 provider 的 api 值、rawStopReason 值与 thinking 块的签名形式会不同。
每行的格式
每行是一个 JSON 对象,以 LF 结尾。文档要求按 LF 切行,去掉行尾可选的 CR,不要把 Unicode 行分隔符(U+2028、U+2029)当作行界,它们可以合法地出现在 JSON 字符串里。每个对象都有 type。除 type 之外没有所有事件共有的字段:事件本身没有 id、时间戳和会话 id,时间戳在 message 对象上(timestamp,Unix 毫秒整数),会话 id 只在第一行的 session 头里。
读取方必须持续消费标准输出。文档说明管道缓冲区满时 pi 会阻塞。
有哪些事件
按用途分为五类,越靠前越需要处理。「来源」一列:「观测」表示在 1.0.1 的样本中出现过;「文档」表示只在 docs/json.md 中有描述。
消息与结果
解析器必须处理的事件。message_end 给出每条消息的完整内容,最后一个 assistant 消息的 stopReason 决定成败。
| type | 何时出现 | 来源 |
|---|---|---|
session | 第一行,一次 | 观测 |
message_end | 每条消息完成,message 为最终内容 | 观测 |
message_start | 每条消息开始:system、user、assistant、toolResult | 观测 |
message_update | assistant 消息流式输出中的每个片段 | 观测 |
工具执行
每次工具调用的开始、部分输出与结束,用 toolCallId 关联。
| type | 何时出现 | 来源 |
|---|---|---|
tool_execution_start | 每次工具执行开始 | 观测 |
tool_execution_update | 工具报告部分结果。样本中只有 bash 工具产生 | 观测 |
tool_execution_end | 每次工具执行结束 | 观测 |
运行与轮次的边界
标出一次运行与每次模型调用的开始和结束,本身不带新内容。
| type | 何时出现 | 来源 |
|---|---|---|
agent_start | 一次 agent 运行开始。正常运行一次;自动重试时每次尝试一次 | 观测 |
turn_start | 每次模型调用之前 | 观测 |
turn_end | 一次模型调用及其工具结果全部完成 | 观测 |
agent_end | 一次 agent 运行结束 | 观测 |
agent_settled | 没有剩余的自动工作(重试、压缩恢复、排队消息),正常运行的最后一行 | 观测 |
重试与压缩
模型调用失败或上下文过长时出现。
| type | 何时出现 | 来源 |
|---|---|---|
auto_retry_start、auto_retry_end | 模型调用失败后自动重试前后 | 文档 |
compaction_start、compaction_end | 上下文压缩开始与结束 | 文档 |
summarization_retry_scheduled、summarization_retry_attempt_start、summarization_retry_finished | 压缩或分支摘要失败后的重试 | 文档 |
JSON 模式下通常不出现
依赖输入通道、扩展或 RPC 模式,解析器跳过即可。
| type | 何时出现 | 来源 |
|---|---|---|
queue_update | steering 或 follow-up 队列变化。JSON 模式没有输入通道,正常情况不会出现 | 文档 |
entry_appended | 扩展通过 pi.appendEntry() 追加自定义条目 | 文档 |
session_info_changed | 会话显示名变化 | 文档 |
thinking_level_changed | thinking 级别变化 | 文档 |
bash_execution_update、extension_error | 只在 RPC 模式出现,JSON 模式没有 | 文档 |
一次运行的顺序
下面是一次「运行一条命令、回答结果」的运行。图中每个方框是流里的一行,从上到下按出现顺序排列,按产生它的一方分为四列。提示词作为 role user 的消息出现在流里。写成 message_start/end 的方框表示紧挨着的两行。工具调用与工具结果用同一个 toolCallId 对应(图中的 C)。
要点:
- system 与 user 消息的
message_start与message_end内容相同,紧挨着出现。system 消息只在第一次turn_start之后出现一次。 - 一次模型调用返回多个工具调用时,所有
tool_execution_start先出现,然后是各自的tool_execution_update与tool_execution_end,再是每个工具一对 toolResult 的message_start与message_end。样本中两个工具调用的tool_execution_start连续出现,说明工具是并行执行的。用toolCallId关联。 turn_end.message与前一个 assistant 消息的message_end.message相同;turn_end.toolResults与这一轮各 toolResult 消息相同。agent_end.messages包含本次运行产生的所有消息,从 system 消息开始。因此这些字段里的内容在流里会重复出现三次,只需在一处解析。agent_settled是正常运行的最后一行。文档说明agent_end之后可能还有自动重试、溢出恢复、压缩重试,这时会再次出现agent_start,直到agent_settled。
类型总览
从本节开始,事件的字段用 TypeScript 类型定义描述。写法约定:? 表示字段可能缺失;字符串字面量的并集是观测到的全部取值;没有标注来源的定义都来自 1.0.1 的样本,注释里以「文档」开头的内容只有随包安装的 docs/ 作依据,没有样本。
type PiLine = SessionHeader | PiEvent; // 第一行是 SessionHeader,之后每行一个 PiEvent
type PiEvent =
| AgentStart | TurnStart | MessageStart | MessageUpdate | MessageEnd
| ToolExecutionStart | ToolExecutionUpdate | ToolExecutionEnd
| TurnEnd | AgentEnd | AgentSettled
| AutoRetryStart | AutoRetryEnd // 文档
| CompactionStart | CompactionEnd // 文档
// 文档
| SummarizationRetryScheduled | SummarizationRetryAttemptStart | SummarizationRetryFinished
// 文档
| QueueUpdate | EntryAppended | SessionInfoChanged | ThinkingLevelChanged;
session 头
与会话文件的第一行相同。
interface SessionHeader {
type: "session";
version: 3; // 会话文件格式版本
id: string; // 会话 id,等于 --session-id 的值,否则是 pi 生成的 UUID
// ISO 8601,UTC,毫秒。message 对象上的时间戳是 Unix 毫秒整数,格式不同
timestamp: string;
cwd: string; // 工作目录的绝对路径
parentSession?: string; // 文档:fork 或 clone 出来的会话指向原会话文件的路径
}
{
"type": "session",
"version": 3,
"id": "SESSION_ID",
"timestamp": "2026-10-03T16:15:48.217Z",
"cwd": "/work/project"
}
agent 与 turn 事件
interface AgentStart { type: "agent_start"; } // 一次 agent 运行开始;自动重试时每次尝试一次
interface TurnStart { type: "turn_start"; } // 一次模型调用开始
interface TurnEnd {
type: "turn_end";
message: AssistantMessage; // 本轮的 assistant 消息,与 message_end.message 相同
toolResults: ToolResultMessage[]; // 本轮的 toolResult 消息,没有工具调用时为 []
}
interface AgentEnd {
type: "agent_end";
messages: Message[]; // 本次运行产生的全部消息,按出现顺序,从 system 消息开始
// true 表示 pi 将自动重试,之后会有新的 agent_start。观测值都是 false。
willRetry: boolean;
}
interface AgentSettled { type: "agent_settled"; } // pi 不会再自动继续,正常运行的最后一行。
{
"type": "turn_end",
"message": { "role": "assistant", "content": ["..."], "stopReason": "toolUse", "...": "..." },
"toolResults": [
{
"role": "toolResult",
"toolCallId": "call_01A",
"toolName": "bash",
"content": [{ "type": "text", "text": "200" }],
"isError": false,
"timestamp": 1791044150015
}
]
}
{
"type": "agent_end",
"messages": [
{ "role": "system", "...": "..." },
{ "role": "user", "...": "..." },
{ "role": "assistant", "...": "..." },
{ "role": "toolResult", "...": "..." },
{ "role": "assistant", "...": "..." }
],
"willRetry": false
}
{ "type": "agent_settled" }
message 事件
message_start 与 message_end 只有一个字段 message。message.role 决定对象的其余字段。观测到四种角色:system、user、assistant、toolResult。文档另外定义了 bashExecution(RPC 的直接 shell 命令)、custom(扩展注入的上下文)、branchSummary 与 compactionSummary(压缩产生的摘要),解析器应容忍未知角色。agentrun 中处理这两种事件的是 Pi::translate 里 type 为 message_start 与 message_end 的分支。
interface MessageStart { type: "message_start"; message: Message; }
interface MessageEnd { type: "message_end"; message: Message; } // message 是权威的完整消息
type Message =
| SystemMessage | UserMessage | AssistantMessage | ToolResultMessage
// 文档
| BashExecutionMessage | CustomMessage | BranchSummaryMessage | CompactionSummaryMessage;
// 文档:以下四种角色没有样本
interface BashExecutionMessage {
role: "bashExecution";
command: string; output: string; exitCode: number | undefined;
cancelled: boolean; truncated: boolean; fullOutputPath?: string; excludeFromContext?: boolean;
timestamp: number;
}
interface CustomMessage {
role: "custom";
customType: string; content: string | (TextContent | ImageContent)[]; display: boolean; details?: unknown;
timestamp: number;
}
interface BranchSummaryMessage { role: "branchSummary"; summary: string; fromId: string | null; timestamp: number; }
interface CompactionSummaryMessage { role: "compactionSummary"; summary: string; tokensBefore: number; timestamp: number; }
system 消息
第一次 turn_start 之后出现一次,声明系统提示与工具定义。message_start 与 message_end 的内容相同。
interface SystemMessage {
role: "system";
content: string | TextContent[]; // 观测值为空字符串 "",正文在 sections 里
// 观测到 preamble、tools、rules、docs、cwd 五节,按顺序拼接得到系统提示
sections?: { [name: string]: string | null };
toolsAdded?: ToolDefinition[]; // 模型可用的工具定义
toolsRemoved?: unknown[]; // 文档:后续 system 消息移除的工具引用
// 文档:true 表示丢弃之前的 system 状态,以这条消息为新的基线
replace?: boolean;
timestamp: number; // Unix 毫秒
}
interface ToolDefinition {
name: string;
description: string;
parameters: object; // JSON Schema
constrainedSampling: { type: "json_schema"; strict: "prefer" };
}
{
"type": "message_start",
"message": {
"role": "system",
"content": "",
"sections": {
"preamble": "You are an expert coding assistant operating inside pi, ...",
"tools": "<tools>\n- read: Read file contents\n...",
"rules": "<rules>\n...",
"docs": "...",
"cwd": "Current working directory: /work/project"
},
"timestamp": 1791044136946,
"toolsAdded": [
{
"name": "read",
"description": "Read the contents of a file. ...",
"parameters": { "type": "object", "properties": { "...": "..." }, "required": ["path"] },
"constrainedSampling": { "type": "json_schema", "strict": "prefer" }
}
]
}
}
user 消息
interface UserMessage {
role: "user";
// 观测值是 [{ type: "text", text: "提示词" }];用 @文件 传入图片时,
// 是一个 text 元素加上每张图片一个 image 元素(1.2.0 实测)。文档也允许字符串
content: string | (TextContent | ImageContent)[];
timestamp: number;
}
{
"type": "message_start",
"message": {
"role": "user",
"content": [
{ "type": "text", "text": "Run this shell command and reply with only the HTTP status code it prints: curl -sS -o /dev/null -w '%{http_code}' https://example.com/" }
],
"timestamp": 1791044148273
}
}
assistant 消息
message_start 里的 assistant 消息是空壳:content 为 [],usage 全为 0,stopReason 为 pending,没有 responseId、rawStopReason 与 thinkingLevel。message_end、turn_end.message 与 agent_end.messages 里是完整消息。
interface AssistantMessage {
role: "assistant";
content: (TextContent | ThinkingContent | ToolCall)[]; // 模型调用失败时为 []
api: string; // provider 的 API 协议,观测值 "openai-completions"
provider: string; // 观测值 "deepseek"
model: string; // 请求的模型 id,观测值 "deepseek-flash"
responseModel?: string; // 文档:provider 实际返回的模型,与 model 不同时才有
// provider 的响应 id;pending 与 error 状态下缺失。
responseId?: string;
providerThinkingLevel?: string; // 文档
diagnostics?: unknown[]; // 文档:provider 或运行时诊断
usage: Usage; // 这一次模型调用的用量
// 流式输出中、文字回答结束、要求调用工具、模型调用失败
stopReason: "pending" | "stop" | "toolUse" | "error"
| "length" | "aborted" | "deferred"; // 文档
// stopReason 为 "error" 时的错误文本,观测值以 HTTP 状态码开头,例如 "401: {...}"
errorMessage?: string;
// provider 原始的结束原因,观测值 "stop"、"tool_calls";pending 与 error 状态下缺失。
rawStopReason?: string;
// 本次调用的 thinking 级别,观测值 "high";pending 状态下缺失。
thinkingLevel?: string;
deferred?: { // 文档:stopReason 为 "deferred" 时取回结果所需的句柄
provider: string; modelId: string; api: string; id: string;
expiresAt?: number; pollAfterMs?: number; data?: unknown;
};
endTurn?: boolean; // 文档
timestamp: number; // message_start 与 message_end 里的值相同,是消息开始的时间
}
{
"type": "message_start",
"message": {
"role": "assistant",
"content": [],
"api": "openai-completions",
"provider": "deepseek",
"model": "deepseek-flash",
"usage": {
"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0, "totalTokens": 0,
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0, "total": 0 }
},
"stopReason": "pending",
"timestamp": 1791044148299
}
}
{
"type": "message_end",
"message": {
"role": "assistant",
"content": [
{
"type": "thinking",
"thinking": "The user wants me to run a shell command.",
"thinkingSignature": "reasoning_content"
},
{
"type": "toolCall",
"id": "call_01A",
"name": "bash",
"arguments": { "command": "curl -sS -o /dev/null -w '%{http_code}' https://example.com/", "timeout": 30 }
}
],
"api": "openai-completions",
"provider": "deepseek",
"model": "deepseek-flash",
"usage": {
"input": 1746,
"output": 94,
"cacheRead": 512,
"cacheWrite": 0,
"reasoning": 15,
"totalTokens": 2352,
"cost": { "input": 0.0005238, "output": 0.0001128, "cacheRead": 0.000003072, "cacheWrite": 0, "total": 0.000639672 }
},
"stopReason": "toolUse",
"timestamp": 1791044148299,
"responseId": "RESPONSE_ID",
"rawStopReason": "tool_calls",
"thinkingLevel": "high"
}
}
toolResult 消息
每次工具执行结束后一对 message_start 与 message_end,内容相同。
interface ToolResultMessage {
role: "toolResult";
toolCallId: string; // 对应 assistant 消息里 ToolCall 的 id
toolName: string;
content: (TextContent | ImageContent)[]; // 返回给模型的内容,观测值都是一个 text 块
details?: object; // 工具自定义的附加数据,观测值为 {},只在部分结果上出现
usage?: Usage; // 文档:工具内部调用模型的用量,不计入主模型调用
isError: boolean;
timestamp: number;
}
{
"type": "message_end",
"message": {
"role": "toolResult",
"toolCallId": "call_01A",
"toolName": "write",
"content": [{ "type": "text", "text": "EROFS: read-only file system, open '/root/x'" }],
"details": {},
"isError": true,
"timestamp": 1791074931228
}
}
toolResult 消息的 content 与 tool_execution_end 的 result.content 相同;tool_execution_end 里的 result.structuredContent 不会出现在 toolResult 消息里。
message_update
只在 assistant 消息流式输出时出现。
interface MessageUpdate {
type: "message_update";
// provider 到目前为止报告的累计用量。观测中 provider 在流结束时才报告,所以直到 *_end
// 之前都是 0,之后等于 message_end.message.usage
usage: Usage;
assistantMessageEvent: AssistantMessageEvent; // 一个内容块的增量
}
type AssistantMessageEvent =
| { type: "thinking_start"; contentIndex: number }
| { type: "thinking_delta"; contentIndex: number; delta: string }
| { type: "thinking_end"; contentIndex: number; content: string } // content 是完整文本
| { type: "text_start"; contentIndex: number }
| { type: "text_delta"; contentIndex: number; delta: string }
| { type: "text_end"; contentIndex: number; content: string }
// 这时已经知道调用 id 与工具名
| { type: "toolcall_start"; contentIndex: number; id: string; toolName: string }
// 序列化的参数片段,观测中可能是空字符串
| { type: "toolcall_delta"; contentIndex: number; delta: string }
| { type: "toolcall_end"; contentIndex: number; toolCall: ToolCall } // 完整的 ToolCall
// 文档:正常 agent 循环把它翻译成 message_start,不作为 message_update 输出
| { type: "start" }
// 文档:同上,翻译成 message_end
| { type: "done"; reason: string; message: AssistantMessage }
| { type: "error"; reason: string; error: AssistantMessage }; // 文档:同上
contentIndex 是内容块在 message.content 数组中的下标。实时显示时按 contentIndex 累积 delta,收到 *_end 时用其中的完整内容替换累积结果,收到 message_end 时用整条消息替换。1.0.1 的 message_update 不含累计的 message 对象,也不含 assistantMessageEvent.partial,见「版本说明」。
{
"type": "message_update",
"usage": {
"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0, "totalTokens": 0,
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0, "total": 0 }
},
"assistantMessageEvent": { "type": "thinking_start", "contentIndex": 0 }
}
{
"type": "message_update",
"usage": { "...": "..." },
"assistantMessageEvent": { "type": "thinking_delta", "contentIndex": 0, "delta": "The" }
}
{
"type": "message_update",
"usage": { "...": "..." },
"assistantMessageEvent": { "type": "toolcall_start", "contentIndex": 1, "id": "call_01A", "toolName": "bash" }
}
{
"type": "message_update",
"usage": { "...": "..." },
"assistantMessageEvent": { "type": "toolcall_delta", "contentIndex": 1, "delta": "" }
}
{
"type": "message_update",
"usage": {
"input": 1746, "output": 94, "cacheRead": 512, "cacheWrite": 0, "reasoning": 15, "totalTokens": 2352,
"cost": { "...": "..." }
},
"assistantMessageEvent": { "type": "thinking_end", "contentIndex": 0, "content": "The user wants me to run a shell command." }
}
{
"type": "message_update",
"usage": { "...": "..." },
"assistantMessageEvent": {
"type": "toolcall_end",
"contentIndex": 1,
"toolCall": { "type": "toolCall", "id": "call_01A", "name": "bash", "arguments": { "command": "pwd", "timeout": 10 } }
}
}
注意顺序:观测中 thinking 块的 thinking_end 出现在 toolcall 块的 toolcall_start 与 toolcall_delta 之后,也就是说一个块的结束事件可能晚于下一个块的开始事件。解析器应按 contentIndex 归属事件,不按出现顺序。
内容块
出现在 assistant 消息的 content、toolcall_end.toolCall、user 与 toolResult 消息的 content 里。
interface TextContent {
type: "text";
text: string;
textSignature?: string; // 文档:provider 专有的元数据,不透明
}
interface ThinkingContent {
type: "thinking";
thinking: string; // 推理文本,观测中正文可读
// provider 专有的回放数据,不透明。deepseek 的观测值是字符串 "reasoning_content"
thinkingSignature?: string;
redacted?: boolean; // 文档:true 表示正文被 provider 隐去,只有签名
}
interface ToolCall {
type: "toolCall";
id: string; // 调用 id,deepseek 的观测值形如 "call_00_..."
name: string; // 工具名,与 system 消息 toolsAdded[].name 对应
// 键由工具的 parameters schema 决定。观测中模型会显式传 null,例如 read 的 offset: null
arguments: { [key: string]: unknown };
thoughtSignature?: string; // 文档:provider 专有
namespace?: string; // 文档:OpenAI Responses 的工具命名空间
}
interface ImageContent { // 文档:只出现在 user 与 toolResult 消息
type: "image";
data: string; // base64
mimeType: string;
}
usage 对象
出现在 assistant 消息的 usage 与 message_update.usage,都是单次模型调用的量,流里没有整次运行的累计值,需要读取方把每个 assistant message_end 的 usage 相加。
interface Usage {
// 未命中缓存的输入 token。观测中 input + cacheRead + output = totalTokens
input: number;
output: number; // 输出 token,已包含 reasoning
cacheRead: number; // 命中缓存的输入 token
cacheWrite: number; // 写入缓存的 token,deepseek 观测值为 0
cacheWrite1h?: number; // 文档:cacheWrite 中按一小时保留写入的部分
// 推理 token,是 output 的子集,不要重复相加;pending 状态与错误消息的 usage 里没有。
reasoning?: number;
totalTokens: number;
cost: { // 单位美元,按 pi 模型目录里的单价计算
input: number;
output: number;
cacheRead: number;
cacheWrite: number;
total: number;
};
}
tool_execution 事件
agentrun 中处理这两种事件的是 Pi::translate 里 type 为 tool_execution_start 与 tool_execution_end 的分支。
interface ToolExecutionStart {
type: "tool_execution_start";
toolCallId: string; // 对应 ToolCall 的 id
toolName: string;
args: { [key: string]: unknown }; // 与 ToolCall.arguments 相同
}
interface ToolExecutionUpdate {
type: "tool_execution_update";
toolCallId: string;
toolName: string;
args: { [key: string]: unknown };
// bash 工具的第一次更新 content 为 [],之后 content[0].text 是到目前为止的全部输出,
// 是累计值,用它替换之前的内容。文档说明替换还是追加由各工具决定
partialResult: {
content: TextContent[];
details?: object;
};
}
interface ToolExecutionEnd { // 没有 args
type: "tool_execution_end";
toolCallId: string;
toolName: string;
result: {
content: TextContent[]; // 返回给模型的内容块
details?: object; // read、write 成功时没有,失败时为 {}
structuredContent?: BashStructuredContent; // 只有 bash 工具有
};
isError: boolean; // 错误文本在 result.content[0].text
}
interface BashStructuredContent {
output: string;
truncated: boolean;
exit_code: number;
wall_time_seconds: number;
}
{
"type": "tool_execution_start",
"toolCallId": "call_01A",
"toolName": "bash",
"args": { "command": "pwd", "timeout": 10 }
}
{
"type": "tool_execution_update",
"toolCallId": "call_01A",
"toolName": "bash",
"args": { "command": "pwd", "timeout": 10 },
"partialResult": { "content": [] }
}
{
"type": "tool_execution_update",
"toolCallId": "call_01A",
"toolName": "bash",
"args": { "command": "pwd", "timeout": 10 },
"partialResult": { "content": [{ "type": "text", "text": "/work/project\n" }], "details": {} }
}
{
"type": "tool_execution_end",
"toolCallId": "call_01A",
"toolName": "bash",
"result": {
"content": [{ "type": "text", "text": "/work/project\n" }],
"structuredContent": { "output": "/work/project\n", "truncated": false, "exit_code": 0, "wall_time_seconds": 0 }
},
"isError": false
}
{
"type": "tool_execution_end",
"toolCallId": "call_01B",
"toolName": "write",
"result": {
"content": [{ "type": "text", "text": "EROFS: read-only file system, open '/root/x'" }],
"details": {}
},
"isError": true
}
命令退出码非 0 的情况在样本里没有出现,按 bash 工具的结构推断它会出现在 structuredContent.exit_code,而 isError 是否为 true 未经确认。
文档中有、样本中没有的事件
auto_retry_start、auto_retry_end
模型调用失败后,pi 按 delayMs 延迟后重新开始一次 agent 运行,最后由 auto_retry_end 报告重试结果。1.0.1 的样本中 401 认证错误没有触发重试,直接结束运行,说明重试只针对部分错误类型。
// 以下定义来自文档
interface AutoRetryStart {
type: "auto_retry_start";
attempt: number; // 第几次重试,从 1 开始
maxAttempts: number;
delayMs: number; // 重试前的等待毫秒数
errorMessage: string; // 触发重试的错误
}
interface AutoRetryEnd {
type: "auto_retry_end";
success: boolean;
attempt: number; // 结束时的尝试序号
finalError?: string; // success 为 false 时的最终错误
}
compaction_start、compaction_end 与摘要重试
上下文接近上限时 pi 自动压缩历史。溢出恢复成功后 willRetry 为 true,pi 会重新发送提示词。压缩完成后的历史里会出现 compactionSummary 角色的消息。以下定义全部来自文档。
type CompactionReason = "manual" | "threshold" | "overflow";
interface CompactionStart { type: "compaction_start"; reason: CompactionReason; }
interface CompactionEnd {
type: "compaction_end";
reason: CompactionReason;
result?: { // 成功时有
summary: string;
firstKeptEntryId: string;
tokensBefore: number;
estimatedTokensAfter: number;
usage: Usage;
details: object;
};
aborted: boolean; // 被中止时为 true,没有 result
errorMessage?: string; // 失败时有,没有 result,aborted 为 false
willRetry: boolean;
}
interface SummarizationRetryScheduled {
type: "summarization_retry_scheduled";
attempt: number; maxAttempts: number; delayMs: number; errorMessage: string;
}
interface SummarizationRetryAttemptStart {
type: "summarization_retry_attempt_start";
source: "compaction" | "branchSummary";
reason?: CompactionReason; // 分支摘要没有
}
interface SummarizationRetryFinished { type: "summarization_retry_finished"; }
队列与状态事件
由交互操作或扩展触发,在没有扩展的 JSON 模式一次运行里不会出现。定义来自文档。
interface QueueUpdate { type: "queue_update"; steering: unknown[]; followUp: unknown[]; } // 各为完整的当前队列
interface EntryAppended { type: "entry_appended"; entry: object; } // 会话文件的条目对象
interface SessionInfoChanged { type: "session_info_changed"; name?: string; } // 缺失表示清除
interface ThinkingLevelChanged { type: "thinking_level_changed"; level: string; }
错误、中断与退出码
agentrun 的 Pi::failure 读最后一条 assistant 消息的 stopReason,并结合模型检查与退出码判断成败,完整的判定顺序见运行中 pi 一节的「失败判定」。
| 情形 | 退出码 | 流里的表现 |
|---|---|---|
| 正常完成 | 0 | 最后一个 assistant 消息的 stopReason 为 stop,最后一行是 agent_settled |
| 工具失败(写只读路径等) | 0 | tool_execution_end.isError 与 toolResult 消息的 isError 为 true,模型继续,运行正常结束 |
| 模型调用失败(401 认证错误) | 0 | assistant 消息的 message_start 之后直接 message_end,没有 message_update;content 为 [],stopReason 为 error,errorMessage 为 401: {...}。之后 turn_end、agent_end、agent_settled 照常出现。标准错误为空 |
| 模型名不存在 | 1 | 标准输出为空,标准错误 Error: Model "xxx" not found. Use --list-models to see available models. |
| 未知命令行选项 | 1 | 标准输出为空,标准错误 Error: Unknown option: --xxx |
| SIGINT | 130 | 流停在当前位置,没有收尾事件。样本中一次停在 tool_execution_start 之后,一次停在 turn_start 之后。标准错误为空 |
判定规则:退出码非 0 一定失败;退出码为 0 时,找到最后一个 role 为 assistant 的 message_end,stopReason 为 stop 或 length 算成功,error 或 aborted 算失败;没有 agent_settled 时按被中断处理。docs/cli-integration.md 说明 JSON 模式下失败或中止的 assistant 响应本身不产生非 0 退出码,只有进程抛出异常时才非 0;print 模式(-p 不加 --mode json)则相反,error 或 aborted 会产生非 0 退出码。收到 SIGTERM 时 pi 以 143 退出,最后一个 assistant 的 message_start 之后没有 message_end 与 agent_end;pi 1.2.0 在 Linux 上用 bwrap 包裹与不包裹相同,pi 1.0.2 在 macOS 上用 Seatbelt 包裹与不包裹相同。
{
"type": "message_end",
"message": {
"role": "assistant",
"content": [],
"api": "openai-completions",
"provider": "deepseek",
"model": "deepseek-flash",
"usage": {
"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0, "totalTokens": 0,
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0, "total": 0 }
},
"stopReason": "error",
"timestamp": 1791044157763,
"errorMessage": "401: {\"message\":\"Authentication Fails, Your api key: **** is invalid (request_id: REQUEST_ID)\",\"type\":\"authentication_error\",\"param\":null,\"code\":\"invalid_request_error\"}",
"thinkingLevel": "high"
}
}
{ "type": "turn_end", "message": { "...": "..." }, "toolResults": [] }
{ "type": "agent_end", "messages": ["..."], "willRetry": false }
{ "type": "agent_settled" }
样本范围
- 本文的 1.0.1 样本只覆盖 deepseek provider 与
read、bash、write三个工具的执行;edit、grep、find、ls的result.details形状、其他 provider 的api与签名字段、tool 结果里的图片内容块、length、aborted、deferred三种stopReason、压缩与扩展相关事件均未实测。 - pi 在
--mode json下的输出同时也是 RPC 模式的事件格式(RPC 没有session头,另有bash_execution_update与extension_error),以及 SDK 的AgentSessionEvent去掉累计快照后的形状。TypeScript 类型JsonAgentSessionEvent由@earendil-works/pi-coding-agent导出。