Skip to content

AgentEvent stream

appctl emits structured AgentEvents from the agent loop. They are the single source of truth for UIs (CLI renderer, VS Code panel, web UI) and for the WebSocket and POST /run transports.

Each event is a JSON object with a kind discriminator:

{ "kind": "<variant>", /* fields */ }

Emitted once when the user submits a prompt.

{ "kind": "user_prompt", "text": "list widgets" }

Incremental assistant text. Emitted by providers that support streaming; absent otherwise.

{ "kind": "assistant_delta", "text": "Looking " }

A complete assistant message. Always emitted at the end of an assistant turn.

{ "kind": "assistant_message", "text": "Here are your widgets." }

Incremental reasoning text from providers that expose a separate thought stream. Clients should render this separately from the public answer.

{ "kind": "assistant_thought_delta", "text": "Inspecting available tools..." }

Complete reasoning text for the turn. This is separate from assistant_message and should not be treated as the answer.

{ "kind": "assistant_thought", "text": "Inspecting available tools..." }

The agent chose a tool and is about to call it.

{
"kind": "tool_call",
"id": "call_01HV...",
"name": "list_widgets",
"arguments": { "limit": 10 }
}

id correlates with the matching tool_result.

Emitted immediately before appctl applies a safety gate that may need terminal input, such as confirming a mutating CLI tool call.

{ "kind": "awaiting_input" }

The tool returned.

{
"kind": "tool_result",
"id": "call_01HV...",
"result": { "items": [ /* ... */ ] },
"status": "ok",
"duration_ms": 120
}

status is ok or error.

Unrecoverable error during the loop.

{ "kind": "error", "message": "max iterations reached" }

The active in-process chat transcript for HTTP or WebSocket clients.

{
"kind": "session_state",
"session_id": "9f8...",
"transcript_len": 4,
"resumed": true
}

Informational context management message, for example when older turns were trimmed by behavior.history_limit.

{ "kind": "context_notice", "message": "Trimmed 2 older message(s) from model context." }

Loop finished. No more events will be emitted for this turn.

{ "kind": "done" }
  • Exactly one user_prompt starts the stream.
  • Zero or more session_state, context_notice, assistant_thought_delta / assistant_thought, awaiting_input, and tool_call / tool_result events interleave with assistant_delta/assistant_message.
  • Every tool_call is followed by a tool_result with the same id (unless the loop errors first).
  • Exactly one done terminates the stream.

The response body buffers every event in the events array plus a final result object. Good for synchronous callers.

Frames stream live. Good for UIs.

appctl chat and appctl run use the same stream internally. The terminal renderer is in crates/appctl/src/term.rs if you want to see how they are formatted.