senpi --mode json "Your prompt"Outputs all session events as JSON lines to stdout. Useful for integrating senpi into other tools or custom UIs.
Wire events use JsonAgentSessionEvent. It matches
AgentSessionEvent
except that streaming message updates omit cumulative snapshots:
type AgentSessionEvent =
// All AgentEvent variants; agent_end additionally carries willRetry: boolean
| AgentEvent
| { type: "queue_update"; steering: readonly string[]; followUp: readonly string[] }
| { type: "compaction_start"; reason: "manual" | "threshold" | "overflow" }
| { type: "compaction_progress"; reason: "manual" | "threshold" | "overflow"; delta?: string; text?: string }
| { type: "compaction_end"; reason: "manual" | "threshold" | "overflow"; result: CompactionResult | undefined; aborted: boolean; willRetry: boolean; requestId?: string; accepted?: boolean; rejectionCause?: "cancelled-by-extension" | "external-owner" | "would-overflow" | "circuit-breaker" | "per-turn-cap"; errorMessage?: string }
| { type: "session_info_changed"; name: string | undefined }
| { type: "thinking_level_changed"; level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" }
| { type: "thinking_level_clamped"; provider: string; modelId: string; requestedLevel: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; appliedLevel: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; reason: "model-not-reasoning" | "level-unsupported" }
| SystemPromptChangeEvent // type: "system_prompt_change"
| ExtensionToolHookLifecycleEvent // type: "tool_hook_status"
| { 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" };external-owner means senpi declined the compaction because the active provider owns compaction for the session.
The wire format strips cumulative snapshots from streaming updates:
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>;
resolvedToolName?: string; // toolcall_start and toolcall_end only
};queue_update emits the full pending steering and follow-up queues whenever they change. compaction_start, compaction_progress, and compaction_end cover both manual and automatic compaction. session_info_changed fires when the session display name changes, thinking_level_changed when the thinking level changes, system_prompt_change (see SystemPromptChangeEvent in extensions/types.ts) when a model switch changes the active system prompt, and tool_hook_status (see ExtensionToolHookLifecycleEvent in extensions/runner.ts) for extension tool hook start/end phases.
thinking_level_clamped fires once per model and requested level when an explicit thinking request (such as --thinking, /thinking, or a remembered per-model level) runs at a lower level than asked. A level that only comes from the global defaultThinkingLevel does not count as a request. In RPC mode, the thinkingSelection carried by thinking_level_changed and get_state then also has requested and clampReason.
Other base events come from
AgentEvent:
type AgentEvent =
// Agent lifecycle
| { type: "agent_start" }
| { type: "agent_end"; messages: AgentMessage[] } // in JSON mode also: willRetry: boolean; abortCause?: AgentAbortCause (a run stopped by a session replacement)
// Turn lifecycle
| { type: "turn_start" }
| { type: "turn_end"; message: AgentMessage; toolResults: ToolResultMessage[] }
// Message lifecycle
| { type: "message_start"; message: AgentMessage }
| { type: "message_update"; message: AgentMessage; 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 };Base messages from packages/ai/src/types.ts:
UserMessageAssistantMessageToolResultMessage
Extended messages from packages/coding-agent/src/core/messages.ts:
BashExecutionMessageCustomMessageBranchSummaryMessageCompactionSummaryMessage
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","contentIndex":0,"delta":"Hello"}}
{"type":"message_end","message":{...}}
{"type":"turn_end","message":{...},"toolResults":[]}
{"type":"agent_end","messages":[...]}message_update records are delta-only. They omit both the cumulative message field and
assistantMessageEvent.partial to keep stream size linear. The top-level usage field contains
the latest cumulative provider-reported usage and may remain zero when a provider only reports
usage at completion. Use contentIndex and delta to assemble live text, thinking, or tool-call
arguments if needed. A toolcall_start event also includes the constant-sized id and toolName
fields. toolcall_start and toolcall_end records carry a top-level resolvedToolName: the tool
the call will run. It differs from the requested name when senpi resolves a gateway-namespaced or
recased name (mcp__<id>__Edit runs edit), and equals it otherwise, including when nothing resolves. message_end contains the final authoritative message.
senpi --mode json "List files" 2>/dev/null | jq -c 'select(.type == "message_end")'