Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
227 lines
11 KiB
Markdown
227 lines
11 KiB
Markdown
# 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> = 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`](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")'
|
|
```
|