When running aictrl run --format json, the CLI emits newline-delimited JSON (NDJSON) events to stdout. Each line is a self-contained JSON object with the following base shape:
{
"type": "<event_type>",
"timestamp": 1741500000000,
"sessionID": "session_01abc..."
}The schema is versioned via session_start.schemaVersion. This document describes schema version "1". Consumers should pin to this version and treat unknown fields as forward-compatible additions.
Emitted once when the session begins.
{
"type": "session_start",
"schemaVersion": "1",
"model": "anthropic/claude-sonnet-4-20250514",
"agent": "default",
"permissions": [
{ "permission": "bash", "pattern": "*", "action": "allow" },
{ "permission": "write", "pattern": "*", "action": "ask" }
]
}schemaVersion(string, required) — pinned contract version. Currently"1".permissions(array, required) — the fully resolved permission ruleset for the session (merge of agent + session rules). In headless mode, any permission not matching anallowrule is auto-rejected.
Emitted once per session, immediately after session_start and before the first model turn (present even for zero-turn or immediately-failing runs). Lists every tool and skill that was resolved for the session so consumers can structurally verify tool exposure without string-matching the model's prose.
{
"type": "tool_catalog",
"sessionID": "ses_…",
"timestamp": 1741500000000,
"tools": [
{ "name": "record_finding", "source": "mcp", "server": "aictrl" },
{ "name": "record_review_completed", "source": "mcp", "server": "aictrl" },
{ "name": "bash", "source": "builtin" },
{ "name": "read", "source": "builtin" }
],
"skills": [
{ "name": "code-review", "version": "1.4.0" },
{ "name": "fullstack-code-review", "version": null }
]
}name(string, required) — the tool/function name as exposed to the model. Mirrors upstreamToolListItem.id. For MCP tools this is{serverName}_{toolName}(both sanitised to[a-zA-Z0-9_-]).source(string, required) —"builtin"for tools built into the CLI;"mcp"for tools provided by a connected MCP server.server(string, optional) — the MCP server (client name from config) that provides this tool. Present only whensourceis"mcp".
description and parameters are intentionally omitted to keep the event lean. The primary consumer only needs name + source to verify tool exposure.
name(string, required) — skill name fromSKILL.mdfrontmatter.version(string or null, required) — version string from theversionfield inSKILL.mdfrontmatter.nullwhen no version is declared.
Use case: The server-side completion gate (aictrl-dev/aictrl #3216) uses
tools[]to verify thatrecord_findingandrecord_review_completedwere actually in the model's function list at dispatch time, andskills[]to record which skill version produced the review. Detects the "silent success" failure mode where a missing MCP server lets a run complete green without ever having the review tools available.Note on builtin tool filtering: The
source: "builtin"entries intools[]reflect the instance-level superset registered inToolRegistry. Per-model filters (e.g.apply_patchonly on gpt-*,codesearch/websearchonly for aictrl provider) are applied at dispatch time insideresolveToolsand are not reflected here. Consumers should treat the presence of a builtin tool name as "registered and potentially available", not as "guaranteed to appear in the model's function list". The strong structural guarantee (tool present ↔ tool in dispatch list) applies only tosource: "mcp"entries.
Emitted once when the session ends (success or failure).
{
"type": "session_complete",
"durationMs": 12345,
"error": null
}error is null on success, or a string describing the failure.
Deprecated in v1. Prefer the structured
session_errorevent for new consumers.session_complete.errorremains populated for back-compat and will be removed in a future schema version.Note:
session_erroris only emitted when the session terminates abnormally (provider/auth/rate-limit/timeout/OOM). Non-fatal errors that accumulate during an otherwise-successful run surface as individualerrorevents and may also appear concatenated insession_complete.errorwithout a precedingsession_error. Alerting logic should key onsession_error, not onsession_complete.error.
Emitted immediately before session_complete when the session terminates abnormally.
{
"type": "session_error",
"reason": "rate_limit",
"code": "429",
"message": "Rate limit exceeded"
}reason(string, required) — one ofrate_limit,auth,timeout,oom,provider,unknown.code(string, optional) — provider HTTP status code or error code when available.message(string, required) — human-readable error message.
Emitted when an assistant message finishes (one per LLM turn).
{
"type": "message_complete",
"modelID": "claude-sonnet-4-20250514",
"providerID": "anthropic",
"agent": "default",
"cost": { "input": 0.003, "output": 0.012, "cache": { "read": 0, "write": 0 } },
"tokens": {
"input": 1024,
"output": 512,
"reasoning": 0,
"cache": { "read": 8800, "write": 1024 }
},
"context": { "used": 10848, "limit": 200000, "ratio": 0.05424 },
"finish": "tool-calls"
}finish values: "tool-calls" (model wants to call tools), "end_turn" (model is done), "max_tokens" (output truncated).
tokens (5-way breakdown, mirrors upstream LLM.Usage):
input(number) — raw input tokens billed at the standard input rate.output(number) — output (completion) tokens.reasoning(number) — extended-thinking / reasoning tokens (0 when thinking is off).cache.read(number) — tokens served from the prompt cache (billed at cache-read rate). Distinguishes cache hits from fresh input.cache.write(number) — tokens written to the prompt cache (billed at cache-write rate).
These fields are non-overlapping: a token is counted in exactly one bucket.
context (context-window utilization):
used(number) — tokens occupying the model's context window this turn:input + cache.read + cache.write.limit(number) — the model's total context-window size in tokens, sourced from the models.dev registry (model.limit.context).ratio(number) —used / limit(≥0; may exceed 1 if usage exceeds the model's registered limit). A value approaching or exceeding 1 signals context-exhaustion risk.null— emitted when the model's context limit is not known (e.g. unregistered custom endpoint).
Emitted when a text block from the assistant is complete.
{
"type": "text",
"part": {
"type": "text",
"text": "Here is the result...",
"time": { "start": 1741500000, "end": 1741500001 }
}
}sequenceNum(number, required) — monotonic per-session counter shared acrosstext,reasoning, andtool_useevents. Use it to render a correctly-ordered trace without relying on timestamp ties. Subagent sessions have their own independent counters keyed onpart.sessionID. Note: in JSON mode onlytool_useevents are emitted for subagent sessions, so subagent counters increment only on tool_use.
Emitted when extended thinking content is complete (requires --thinking flag).
{
"type": "reasoning",
"part": {
"type": "reasoning",
"text": "Let me think about this...",
"time": { "start": 1741500000, "end": 1741500001 }
}
}sequenceNum(number, required) — monotonic per-session counter shared acrosstext,reasoning, andtool_useevents. Use it to render a correctly-ordered trace without relying on timestamp ties. Subagent sessions have their own independent counters keyed onpart.sessionID. Note: in JSON mode onlytool_useevents are emitted for subagent sessions, so subagent counters increment only on tool_use.- One event is emitted per complete reasoning block.
part.texthas no size cap; consumers must accept arbitrarily large strings.
Emitted when a tool call completes (success or error). This includes tools executed within subagent sessions.
{
"type": "tool_use",
"part": {
"type": "tool",
"tool": "bash",
"sessionID": "session_01abc...",
"state": {
"status": "completed",
"input": { "command": "ls" },
"metadata": { "exit": 0, "output": "..." }
}
}
}sequenceNum(number, required) — monotonic per-session counter shared acrosstext,reasoning, andtool_useevents. Use it to render a correctly-ordered trace without relying on timestamp ties. Subagent sessions have their own independent counters keyed onpart.sessionID. Note: in JSON mode onlytool_useevents are emitted for subagent sessions, so subagent counters increment only on tool_use.
state.status is "completed" or "error". On error, state.error contains the error message.
For tools executed inside a subagent, part.sessionID will differ from the top-level sessionID — compare the two to identify subagent tool calls.
Emitted at step boundaries during multi-step tool use.
{ "type": "step_start", "part": { "type": "step-start" } }
{ "type": "step_finish", "part": { "type": "step-finish" } }Skills are loaded progressively. The model first sees skill names and descriptions in the tool schema. Full skill content only enters context when the model explicitly invokes the skill tool.
Emitted per skill when the skill tool is initialized and skill descriptions are registered in the tool schema. This happens at the start of each LLM turn.
{
"type": "skill_discovered",
"name": "commit",
"description": "Create well-structured git commits",
"location": "/home/user/.claude/skills/commit/SKILL.md"
}Emitted when the model invokes the skill tool and the full SKILL.md content enters context.
{
"type": "skill_loaded",
"name": "commit",
"location": "/home/user/.claude/skills/commit/SKILL.md"
}Emitted when the model reads a file that belongs to a skill directory (e.g., a script, template, or reference file bundled with the skill).
{
"type": "skill_resource_loaded",
"skillName": "commit",
"filePath": "/home/user/.claude/skills/commit/templates/conventional.md"
}Emitted when a child session (subagent) is spawned.
{
"type": "subagent_start",
"subagentSessionID": "session_01xyz...",
"parentSessionID": "session_01abc...",
"title": "Research codebase"
}Emitted when a child session completes.
{
"type": "subagent_complete",
"subagentSessionID": "session_01xyz...",
"parentSessionID": "session_01abc..."
}Emitted when a session error occurs.
{
"type": "error",
"error": {
"name": "Unknown",
"data": { "message": "Something went wrong" }
},
"sourceSessionID": "session_01abc..."
}Emitted when a permission request is auto-rejected (headless mode has no user to approve).
{
"type": "permission_rejected",
"callID": "call_01xyz...",
"tool": "bash",
"permission": "bash",
"patterns": ["rm -rf /"],
"input": { "command": "rm -rf /" }
}tool(string, required) — the tool that requested the permission. Falls back to thepermissionstring when the request did not originate from a tool execution.permission(string, required) — permission category (e.g.bash,write).patterns(string[], required) — matched patterns.input(any, required) — the arguments the tool tried to invoke.nullwhen unavailable (e.g. subagent task permission checks).callID(string, optional) — tool call id; present when the permission originated from a tool execute closure.sessionID— present on the base envelope; identifies the session that made the request (may be a subagent's session id).
Emitted when a permission request resolves to allow (either by matching an allow rule or a cached always approval). Same shape as permission_rejected.
{
"type": "permission_granted",
"callID": "call_01xyz...",
"tool": "bash",
"permission": "bash",
"patterns": ["ls"],
"input": { "command": "ls" }
}