All skills
Skillintermediate
Research: Pi NDJSON Stream Format vs Claude stream-json
Pi's `--mode json` NDJSON format and Claude's `--output-format stream-json` have fundamentally different schemas. A new parser is required; `ClaudeStreamParser` cannot be reused.
Claude Code Knowledge Pack7/10/2026
Overview
Research: Pi NDJSON Stream Format vs Claude stream-json
Summary
Pi's --mode json NDJSON format and Claude's --output-format stream-json have fundamentally different schemas. A new parser is required; ClaudeStreamParser cannot be reused.
Claude's stream-json Format
Claude emits 3-4 event types per session:
{"type":"system","session_id":"abc123","model":"claude-opus","tools":[]}
{"type":"assistant","message":{"content":[{"type":"text","text":"Hello"},{"type":"tool_use","id":"id1","name":"Bash","input":{"command":"ls"}}]},"usage":{"input_tokens":100,"output_tokens":50}}
{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"id1","content":"output"}]}}
{"type":"result","duration_ms":1500,"total_cost_usd":0.01,"num_turns":3,"is_error":false}
Key traits:
- Coarse-grained: Each
assistantevent contains the FULL message with all content blocks - Flat structure:
typediscriminator at top level withsystem,assistant,user,result - Usage per assistant turn:
usageobject withinput_tokens,output_tokens - Session summary:
resultevent at end with duration, cost, turns
Pi's --mode json Format
Pi emits fine-grained streaming events:
{"type":"session","version":3,"id":"...","timestamp":"...","cwd":"..."}
{"type":"agent_start"}
{"type":"turn_start"}
{"type":"message_start","message":{"role":"user","content":[...],"timestamp":...}}
{"type":"message_end","message":{"role":"user","content":[...],"timestamp":...}}
{"type":"message_start","message":{"role":"assistant","content":[],...}}
{"type":"message_update","assistantMessageEvent":{"type":"text_start","contentIndex":0,...},"message":{...}}
{"type":"message_update","assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":"Hello "},"message":{...}}
{"type":"message_update","assistantMessageEvent":{"type":"text_end","contentIndex":0,"content":"Hello"},"message":{...}}
{"type":"message_update","assistantMessageEvent":{"type":"thinking_start",...},"message":{...}}
{"type":"message_update","assistantMessageEvent":{"type":"thinking_delta","contentIndex":1,"delta":"..."},"message":{...}}
{"type":"message_update","assistantMessageEvent":{"type":"thinking_end",...},"message":{...}}
{"type":"message_update","assistantMessageEvent":{"type":"toolcall_start",...},"message":{...}}
{"type":"message_update","assistantMessageEvent":{"type":"toolcall_delta",...},"message":{...}}
{"type":"message_update","assistantMessageEvent":{"type":"toolcall_end",...},"message":{...}}
{"type":"message_end","message":{...}}
{"type":"tool_execution_start","toolCallId":"...","toolName":"bash","args":{"command":"ls"}}
{"type":"tool_execution_update","toolCallId":"...","toolName":"bash","partialResult":{...}}
{"type":"tool_execution_end","toolCallId":"...","toolName":"bash","result":{"content":[{"type":"text","text":"..."}]},"isError":false}
{"type":"turn_end","message":{...},"toolResults":[...]}
{"type":"agent_end","messages":[...]}
Key traits:
- Fine-grained: Individual delta events for text, thinking, tool calls
- Nested
assistantMessageEvent: Streaming events wrapped inmessage_updatewith sub-types - Full message snapshots: Each
message_updateincludes the full accumulatedmessageobject - Lifecycle events:
agent_start/end,turn_start/end,message_start/end - Usage in message:
usageobject insidemessagewithinput,output,cacheRead,cacheWrite,cost - Cost breakdown:
costobject withinput,output,cacheRead,cacheWrite,total - No session summary event: Cost/usage data is embedded in the final message's
usagefield
Mapping Pi Events to StreamHandler
| StreamHandler method | Claude source | Pi source |
|---|---|---|
on_text(text) | assistant.message.content[].text | message_update where assistantMessageEvent.type == "text_delta" → use delta field |
on_tool_call(name, id, input) | assistant.message.content[].tool_use | tool_execution_start → toolName, toolCallId, args |
on_tool_result(id, output) | user.message.content[].tool_result | tool_execution_end → toolCallId, result.content[0].text |
on_error(error) | result.is_error | message_update where assistantMessageEvent.type == "error" |
on_complete(result) | result event | agent_end → synthesize from accumulated usage data |
Key Differences
- Text extraction: Claude gives complete text blocks; pi gives deltas that must be concatenated
- Tool calls: Claude embeds tool_use in assistant content; pi has separate
tool_execution_start/endevents - Cost tracking: Claude has a dedicated
resultevent; pi embeds cost in per-messageusagefields - Thinking: Claude doesn't expose thinking in stream-json; pi streams thinking deltas (can be ignored)
- Event volume: Pi emits ~10x more events for the same interaction (every delta is an event)
Recommendation
Create a PiStreamParser (analogous to ClaudeStreamParser) that:
- Parses pi's NDJSON events into a
PiStreamEventenum - Has a
dispatch_pi_stream_event()function that maps toStreamHandlercalls - Accumulates cost/usage data across events for the
on_complete()call - Extracts text content for
extracted_text(used by event loop for LOOP_COMPLETE detection)
The OutputFormat enum should get a new PiStreamJson variant (or reuse StreamJson since the dispatch logic is in the executor, not the format enum).