JSON Event Stream

JSON Event Stream

#

JSON mode emits structured progress for one invocation:

pig --mode json "Review this repository"

pig 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

#

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 pig 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:

{"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

#

A 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","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 pig has no remaining automatic work for that session-level run.

Agent and turn events

#
EventFieldsMeaning
agent_startNoneA low-level agent run started.
agent_endmessages, willRetryThat low-level run ended. messages contains messages generated by the run.
agent_settledabortedpig will not continue automatically through retries, compaction recovery, or queued messages. aborted is true when the run was aborted.
turn_startNoneOne assistant turn started.
turn_endmessage, toolResultsOne 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

#
EventFieldsMeaning
message_startmessageA message started.
message_updateusage, assistantMessageEventAn assistant message emitted a content-block update.
message_endmessageA 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:

TypeFields in addition to typeMeaning
startNoneThe provider stream started; its cumulative partial field is removed on the wire.
text_startcontentIndexA text block started.
text_deltacontentIndex, deltaAppend text to the block.
text_endcontentIndex, contentThe text block ended with authoritative content.
thinking_startcontentIndexA thinking block started.
thinking_deltacontentIndex, deltaAppend thinking text to the block.
thinking_endcontentIndex, contentThe thinking block ended with authoritative content.
toolcall_startcontentIndex, id, toolNameA tool-call block started.
toolcall_deltacontentIndex, deltaAppend serialized argument data.
toolcall_endcontentIndex, toolCallThe tool call ended with the complete ToolCall.
donereason, messageThe provider stream completed successfully.
errorreason, errorThe 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. RpcEvents still serializes them 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

#
EventFieldsMeaning
tool_execution_starttoolCallId, toolName, argsTool execution started.
tool_execution_updatetoolCallId, toolName, args, partialResultThe tool reported a partial result.
tool_execution_endtoolCallId, toolName, result, isErrorTool execution finished.

Use toolCallId to correlate the lifecycle. Tools called from a codemode script have their own events with parentToolCallId set to the script's call; their toolCallId is <script call id>/<n>. 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

#
EventFieldsMeaning
queue_updatesteering, followUpThe pending steering or follow-up queue changed. Both fields contain the complete current queue.
entry_appendedentryA session entry no message_end carries was written: an extension's custom entry ($pig->appendEntry()), a context edit, or the usage entry of a cache-warming request. Nothing is emitted when the session is not saved.
session_info_changednameThe session display name changed. An absent name means it was cleared.
thinking_level_changedlevelThe active thinking level changed.
model_fallbackfrom, to, errorpig-only: the session moved to the next fallbackModels entry after a quota error. from and to are {provider, id}.

The entry value uses a persisted session entry type.

Compaction events

#

compaction_start reports why compaction began:

{"type":"compaction_start","reason":"threshold"}

reason is "manual", "threshold", or "overflow". pig emits it only when there is something to compact, for manual, threshold, and overflow compaction alike, so a client can draw automatic compactions from these events.

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": {"readFiles": ["src/A.php"], "modifiedFiles": []}
  },
  "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 pig retries the prompt. pig makes one overflow recovery attempt per prompt; a second overflow ends with compaction_end and session_compact_failed.

See Compaction and Branch Summaries for result semantics.

Retry events

#

Assistant-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

#

A 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.php","event":"tool_call","error":"Error message"}

Extension UI records are a separate RPC subprotocol, not AgentSessionEvent values. See RPC Extension UI.

Event serialization

#

The SDK's session events (Pig\Agent\AgentEvent and the session's own events) carry cumulative streaming snapshots for in-process consumers. JSON and RPC serialize them with RpcEvents.php, which transforms only message_update: it drops the cumulative message and every partial snapshot, adds the top-level usage, and gives toolcall_start the tool call's id and toolName. Every other event is written as described above.

Example

#

Print completed messages from a one-shot run:

pig --mode json "List files" 2>/dev/null | jq -c 'select(.type == "message_end")'