# JSON Event Stream JSON mode emits structured progress for one invocation: ```bash pi --mode json "Review this repository" ``` Pi writes one session header followed by session events, then exits after the supplied prompts finish. RPC mode emits the same session-event shapes but has no session header because it is a bidirectional, long-lived protocol. See [RPC Mode](rpc.md). This page is the canonical reference for events shared by JSON and RPC mode. Message values use the [shared message types](message-types.md). ## Framing and process I/O The stream uses strict JSONL framing. Each record is one JSON object terminated by LF (`\n`). Split records only on LF and strip an optional preceding carriage return. Unicode line and paragraph separators are valid inside JSON strings and are not record boundaries. Node.js `readline` is not suitable for this stream because it also recognizes those Unicode separators. Use a byte or UTF-8 stream decoder and split on LF. Read stdout continuously. A reader that stops consuming records can stall Pi when the pipe buffer fills. Stdout is reserved for JSONL; diagnostics and application logging go to stderr. ## Session header The first JSON-mode record is the current [session header](session-format.md#sessionheader): ```json {"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path"} ``` RPC mode does not emit this record. Use [`get_state`](rpc-commands.md#get_state) for its current session ID and file. ## Event sequence A basic run produces records like these: ```json {"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","aborted":false} ``` `agent_end` closes one low-level agent run. Automatic retry, overflow recovery, compaction retry, steering, or follow-up work can still continue. `agent_settled` means Pi has no remaining automatic work for that session-level run. ## Agent and turn events | Event | Fields | Meaning | |---|---|---| | `agent_start` | None | A low-level agent run started. | | `agent_end` | `messages`, `willRetry` | That low-level run ended. `messages` contains messages generated by the run. | | `agent_settled` | `aborted` | Pi will not continue automatically through retries, compaction recovery, or queued messages. `aborted` is `true` when the run ended because it was aborted. | | `turn_start` | None | One assistant turn started. | | `turn_end` | `message`, `toolResults` | One assistant response and its resulting tool calls finished. | A turn is one assistant response plus any tool calls and tool results produced by that response. ## Message events | Event | Fields | Meaning | |---|---|---| | `message_start` | `message` | A message started. | | `message_update` | `usage`, `assistantMessageEvent` | An assistant message emitted a content-block update. | | `message_end` | `message` | A message completed. This is the authoritative final message. | ### Reconstruct streaming messages Wire `message_update` records are delta-only. They omit the SDK event's cumulative `message` field and every `assistantMessageEvent.partial` snapshot so stream size remains linear. The nested event is one of: | Type | Fields in addition to `type` | Meaning | |---|---|---| | `start` | None | The provider stream started; its cumulative `partial` field is removed on the wire. | | `text_start` | `contentIndex` | A text block started. | | `text_delta` | `contentIndex`, `delta` | Append text to the block. | | `text_end` | `contentIndex`, `content` | The text block ended with authoritative content. | | `thinking_start` | `contentIndex` | A thinking block started. | | `thinking_delta` | `contentIndex`, `delta` | Append thinking text to the block. | | `thinking_end` | `contentIndex`, `content` | The thinking block ended with authoritative content. | | `toolcall_start` | `contentIndex`, `id`, `toolName` | A tool-call block started. | | `toolcall_delta` | `contentIndex`, `delta` | Append serialized argument data. | | `toolcall_end` | `contentIndex`, `toolCall` | The tool call ended with the complete `ToolCall`. | | `done` | `reason`, `message` | The provider stream completed successfully. | | `error` | `reason`, `error` | The provider stream ended with an error or abort message. | The normal agent loop translates provider-level `start`, `done`, and `error` into `message_start` and `message_end` session events rather than emitting them as `message_update`. They remain admitted by the exported `JsonAgentSessionEvent` transformation for callers that construct a matching session event. Use `contentIndex` to identify the content block. Buffer `delta` fields for a live display, but replace reconstructed data with the completed content in `text_end`, `thinking_end`, or `toolcall_end`. Replace the whole partial message with `message_end.message` when it arrives. The top-level `usage` is the latest cumulative provider-reported usage for the assistant response. It can remain zero until completion when a provider does not report usage while streaming. ```json {"type":"message_update","usage":{"input":100,"output":1,"cacheRead":0,"cacheWrite":0,"totalTokens":101,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":"Hello "}} ``` ## Tool execution events | Event | Fields | Meaning | |---|---|---| | `tool_execution_start` | `toolCallId`, `toolName`, `args` | Tool execution started. | | `tool_execution_update` | `toolCallId`, `toolName`, `args`, `partialResult` | The tool reported a partial result. | | `tool_execution_end` | `toolCallId`, `toolName`, `result`, `isError`, `durationMs` | Tool execution finished. `durationMs` is how long the tool's `execute()` took, measured with a monotonic clock; absent when the tool did not run. | Use `toolCallId` to correlate the lifecycle. `partialResult` is the latest partial result supplied by the tool. Whether it replaces or extends an earlier update depends on that tool's result contract. ```json {"type":"tool_execution_start","toolCallId":"call_abc123","toolName":"bash","args":{"command":"ls -la"}} {"type":"tool_execution_update","toolCallId":"call_abc123","toolName":"bash","args":{"command":"ls -la"},"partialResult":{"content":[{"type":"text","text":"partial output"}],"details":{}}} {"type":"tool_execution_end","toolCallId":"call_abc123","toolName":"bash","result":{"content":[{"type":"text","text":"complete output"}],"details":{}},"isError":false} ``` ## Queue and state events | Event | Fields | Meaning | |---|---|---| | `queue_update` | `steering`, `followUp` | The pending steering or follow-up queue changed. Both fields contain the complete current queue. | | `entry_appended` | `entry` | An extension appended a custom session entry through `pi.appendEntry()`. | | `session_info_changed` | `name` | The session display name changed. An absent `name` means it was cleared. | | `thinking_level_changed` | `level` | The active thinking level changed. | The `entry` value uses a persisted [session entry type](session-format.md#entry-types). ## Compaction events `compaction_start` reports why compaction began: ```json {"type":"compaction_start","reason":"threshold"} ``` `reason` is `"manual"`, `"threshold"`, or `"overflow"`. `compaction_end` contains the result when compaction succeeds: ```json { "type": "compaction_end", "reason": "threshold", "result": { "summary": "Summary of conversation...", "firstKeptEntryId": "abc123", "tokensBefore": 150000, "estimatedTokensAfter": 32000, "usage": {"...": "..."}, "details": {} }, "aborted": false, "willRetry": false } ``` If compaction was aborted, `result` is absent and `aborted` is true. If it failed, `result` is absent, `aborted` is false, and `errorMessage` describes the failure. Successful overflow recovery sets `willRetry` to true before Pi retries the prompt. See [Compaction and Branch Summaries](compaction.md) for result semantics. ## Retry events Assistant-turn retry emits: ```json {"type":"auto_retry_start","attempt":1,"maxAttempts":3,"delayMs":2000,"errorMessage":"529 overloaded"} {"type":"auto_retry_end","success":true,"attempt":2} ``` On final failure, `auto_retry_end` has `success: false` and a `finalError` string. Compaction and branch-summary retry emit: ```json {"type":"summarization_retry_scheduled","attempt":1,"maxAttempts":3,"delayMs":2000,"errorMessage":"terminated"} {"type":"summarization_retry_attempt_start","source":"compaction","reason":"threshold"} {"type":"summarization_retry_finished"} ``` For a branch summary, `source` is `"branchSummary"` and `reason` is absent. The `reason` on a compaction retry is `"manual"`, `"threshold"`, or `"overflow"`. ## RPC-only events A direct RPC [`bash`](rpc-commands.md#bash) command emits one `bash_execution_update` for each output chunk. Its optional `id` matches the command ID. The final command response can contain truncated output, but these events stream all output: ```json {"type":"bash_execution_update","id":"req-1","delta":"total 48\n"} ``` RPC also adds `extension_error` when an extension handler throws: ```json {"type":"extension_error","extensionPath":"/path/to/extension.ts","event":"tool_call","error":"Error message"} ``` Extension UI records are a separate RPC subprotocol, not `AgentSessionEvent` values. See [RPC Extension UI](rpc-extension-ui.md). ## TypeScript types The SDK's `AgentSessionEvent` contains cumulative streaming snapshots for in-process consumers. JSON and RPC transform only `message_update`: ```typescript type WithoutPartial = T extends { partial: unknown } ? Omit : T; type JsonAssistantMessageEvent = T extends { type: "toolcall_start"; partial: unknown } ? WithoutPartial & { id: string; toolName: string } : WithoutPartial; type JsonAgentSessionEvent = | Exclude | { type: "message_update"; usage: Usage; assistantMessageEvent: JsonAssistantMessageEvent; }; ``` Use the exported `JsonAgentSessionEvent` type from `@earendil-works/pi-coding-agent`. Its implementation is in [`json-event.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/modes/json-event.ts). ## Example Print completed messages from a one-shot run: ```bash pi --mode json "List files" 2>/dev/null | jq -c 'select(.type == "message_end")' ```