JSON Event Stream Mode

September 20, 2026 ยท View on GitHub

atomic --mode json "Your prompt"

Outputs all session events as JSON lines to stdout. Useful for integrating Atomic into other tools or custom UIs.

If a complete saved provider/model default names a provider that remains unsupported after provider registration, JSON mode writes the generic configuration diagnostic to stderr and exits nonzero before sending the prompt. It writes no human diagnostic to stdout, so any stdout records remain valid JSONL. This differs from ordinary supported-provider model or authentication fallback, which retains normal automatic model selection.

Choosing an integration mode

JSON mode is one of three ways to drive Atomic from your own software. Pick the smallest one that does the job.

You wantUseWhy
One prompt, structured output, then exitJSON event stream mode (this page)One process, one prompt, newline-delimited events on stdout. Nothing to keep alive.
A long-lived process you send more input toRPC modeSend further prompts, interrupt, switch models, and answer tool permission prompts on a live process.
Atomic embedded inside a Node.js applicationSDKThe same engine as a library, with programmatic control over extensions, skills, tools, and session storage.

If you need the event contract rather than a walkthrough, go to the RPC protocol or the SDK API reference. Programmatic use compares all three modes side by side.

Event Types

Events are defined in AgentSessionEvent:

type AgentSessionEvent =
  | AgentEvent
  | { type: "queue_update"; steering: readonly string[]; followUp: readonly string[] }
  | { type: "compaction_start"; reason: "manual" | "threshold" | "overflow" }
  | { type: "session_info_changed"; name: string | undefined }
  | { type: "model_changed"; model: Model<Api>; previousModel: Model<Api> | undefined; source: "set" | "cycle" | "restore" }
  | { type: "thinking_level_changed"; level: ThinkingLevel }
  | { type: "compaction_end"; reason: "manual" | "threshold" | "overflow"; result: VerbatimCompactionResult | undefined; aborted: boolean; willRetry: boolean; unresolvedOverflow?: boolean; errorMessage?: string }
  | { type: "auto_retry_start"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string }
  | { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string }
  | { type: "summarization_retry_scheduled"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string }
  | { type: "summarization_retry_attempt_start"; source: "branchSummary" }
  | { type: "summarization_retry_attempt_start"; source: "compaction"; reason: "manual" | "threshold" | "overflow" }
  | { type: "summarization_retry_finished" };

queue_update emits the full pending steering and follow-up queues whenever they change. session_info_changed, model_changed, and thinking_level_changed report interactive session metadata changes. compaction_start and compaction_end cover manual and automatic verbatim line compaction: the model emits deleted ranges and Atomic mechanically reconstructs retained text.

For automatic compaction, compaction_end.willRetry === true means the interrupted turn will retry; AgentSession.prompt() waits for that continuation. This includes overflow and retry-worthy threshold recovery, such as output-token truncation or OpenAI Responses output-budget underflow. Generic invalid_request_body failures still use willRetry: false when threshold compaction is warranted.

If same-model compact-and-retry recovery is exhausted, unresolvedOverflow: true and errorMessage let integrations choose another model rather than treating the prompt as successful.

Check result and errorMessage independently. Both can be present when the boundary was saved but the follow-up request exceeded the provider's hard input limit.

Compaction planning and branch summaries reuse the configured retry policy for transient provider failures. Their summarization_retry_* events expose scheduling, each restarted request (including whether it belongs to branch summarization or a compaction reason), and retry-loop completion to JSON, RPC, SDK, and interactive consumers.

Base events come from AgentEvent in @earendil-works/pi-agent-core (installed as an Atomic dependency):

type AgentEvent =
  // Agent lifecycle
  | { type: "agent_start" }
  | { type: "agent_end"; messages: AgentMessage[] }
  // Turn lifecycle
  | { type: "turn_start" }
  | { type: "turn_end"; message: AgentMessage; toolResults: ToolResultMessage[] }
  // Message lifecycle
  | { type: "message_start"; message: AgentMessage }
  | { type: "message_update"; assistantMessageEvent: AssistantMessageEvent }
  | { type: "message_end"; message: AgentMessage }
  // Tool execution
  | { type: "tool_execution_start"; toolCallId: string; toolName: string; args: any }
  | { type: "tool_execution_update"; toolCallId: string; toolName: string; args: any; partialResult: any }
  | { type: "tool_execution_end"; toolCallId: string; toolName: string; result: any; isError: boolean };

On the wire, each message_update record carries:

  • The streaming delta in assistantMessageEvent, with its cumulative partial field stripped.
  • The latest cumulative provider-reported usage. This may remain zero until completion if the provider reports usage only at the end.
  • endTurn, only when the provider reported it. This is the provider's explicit end-of-turn signal, pi-ai's AssistantMessage.endTurn, for example OpenAI Codex end_turn.

A non-assistant message on this event is a protocol violation. Atomic throws rather than emitting invented zeroed usage.

Message Types

Base messages come from @bastani/pi-ai (installed as an Atomic dependency):

  • UserMessage
  • AssistantMessage
  • ToolResultMessage

Extended messages from packages/coding-agent/src/core/messages.ts:

  • BashExecutionMessage
  • CustomMessage
  • BranchSummaryMessage

Output Format

Each line is a JSON object. The first line is the session header:

{"type":"session","version":3,"id":"uuid","timestamp":"...","cwd":"/path"}

Followed by events as they occur:

{"type":"agent_start"}
{"type":"turn_start"}
{"type":"message_start","message":{"role":"assistant","content":[],...}}
{"type":"message_update","usage":{...},"assistantMessageEvent":{"type":"text_delta","delta":"Hello",...}}
{"type":"message_end","message":{...}}
{"type":"turn_end","message":{...},"toolResults":[]}
{"type":"agent_end","messages":[...]}

Example

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