On this page
JSON Event Stream
JSON mode emits structured progress for one invocation:
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.
This page is the canonical reference for events shared by JSON and RPC mode. Message values use the shared message types.
Framing and process I/O
CopiedThe 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
CopiedThe first JSON-mode record is the current session header:
{"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 for its current session ID and file.
Event sequence
CopiedA basic run produces records like these:
{"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 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
Copied| 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 |
None | Pi will not continue automatically through retries, compaction recovery, or queued messages. |
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
Copied| 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
CopiedWire 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.
{"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
Copied| 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.
{"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
Copied| 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.
Compaction events
Copiedcompaction_start reports why compaction began:
{"type":"compaction_start","reason":"threshold"}
reason is "manual", "threshold", or "overflow".
compaction_end contains the result when compaction succeeds:
{
"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 for result semantics.
Retry events
CopiedAssistant-turn retry emits:
{"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:
{"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
CopiedA direct RPC 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:
{"type":"bash_execution_update","id":"req-1","delta":"total 48\n"}
RPC also adds extension_error when an extension handler throws:
{"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.
TypeScript types
CopiedThe SDK's AgentSessionEvent contains cumulative streaming snapshots for in-process consumers. JSON and RPC transform only 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>;
};
Use the exported JsonAgentSessionEvent type from @earendil-works/pi-coding-agent. Its implementation is in json-event.ts.
Example
CopiedPrint completed messages from a one-shot run:
pi --mode json "List files" 2>/dev/null | jq -c 'select(.type == "message_end")'