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." }

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.

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" }

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

{ "kind": "done" }
  • Exactly one user_prompt starts the stream.
  • Zero or more tool_call / tool_result pairs 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.