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.rsPi(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_updateassistant 消息流式输出中的每个片段观测

工具执行

每次工具调用的开始、部分输出与结束,用 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_updatesteering 或 follow-up 队列变化。JSON 模式没有输入通道,正常情况不会出现文档
entry_appended扩展通过 pi.appendEntry() 追加自定义条目文档
session_info_changed会话显示名变化文档
thinking_level_changedthinking 级别变化文档
bash_execution_update、extension_error只在 RPC 模式出现,JSON 模式没有文档

一次运行的顺序

下面是一次「运行一条命令、回答结果」的运行。图中每个方框是流里的一行,从上到下按出现顺序排列,按产生它的一方分为四列。提示词作为 role user 的消息出现在流里。写成 message_start/end 的方框表示紧挨着的两行。工具调用与工具结果用同一个 toolCallId 对应(图中的 C)。

调用方模型工具pi 状态启动session会话头agent_startturn_startmessage_start/endrole system:系统提示与工具定义message_start/endrole user:提示词第 1 次模型调用:运行一条命令message_startrole assistantmessage_update ×Nthinking、toolcall 片段message_endstopReason toolUsetool_execution_startbash,toolCallId Ctool_execution_update部分输出tool_execution_endresult,isError falsemessage_start/endrole toolResult,toolCallId Cturn_end第 2 次模型调用:回答turn_startmessage_startrole assistantmessage_update ×Nthinking、text 片段message_endstopReason stop:给调用方的回答turn_end结束agent_endmessages 含全部消息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
工具失败(写只读路径等)0tool_execution_end.isError 与 toolResult 消息的 isError 为 true,模型继续,运行正常结束
模型调用失败(401 认证错误)0assistant 消息的 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
SIGINT130流停在当前位置,没有收尾事件。样本中一次停在 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" }

样本范围