Streaming events
The same set of events is delivered as NDJSON lines on POST /conversations/{conversation_id}/turns/stream and as JSON frames on the realtime WebSocket endpoint. Every event carries a type discriminator.
The WebSocket transport adds four transport-only events — conversation.created, conversation.resumed, conversation.settings.updated and ping — that are not part of the HTTP NDJSON stream. Those are documented under Realtime (WebSocket).
Message events
conversation.user_message.created
Confirms that the user message was accepted and a turn is in progress. user_message.content is the post-redaction text.
| Field | Type | Description |
|---|---|---|
user_message | UserMessage | The accepted user message. |
conversation.assistant_message.delta
Incremental text for an in-progress assistant message.
| Field | Type | Description |
|---|---|---|
assistant_message | AssistantMessageDelta | In-progress delta chunk. |
conversation.assistant_message.ui_widget
Transient structured widget for an in-progress assistant message. Emitted only when ui_widgets_enabled is true.
| Field | Type | Description |
|---|---|---|
assistant_message | AssistantMessageUiWidgetEventPayload | Widget scoped to one assistant message segment. |
AssistantMessageUiWidgetEventPayload:
| Field | Type | Description |
|---|---|---|
id | UUID | Assistant message this widget belongs to. |
turn_id | UUID | Turn producing the message. |
widget | UIWidget | Widget payload to append to the message UI. |
conversation.assistant_message.completed
Canonical full representation of a finished assistant message.
| Field | Type | Description |
|---|---|---|
assistant_message | AssistantMessage | Final assistant message for this segment. |
Conversation-level events
conversation.assessment.updated
Replace the client's local assessment state with this payload.
| Field | Type | Description |
|---|---|---|
assessment | Assessment | Current assessment state. |
conversation.handoff.requested
Emitted when the turn produced a handoff record. Only emitted while at least one detector listed in ConversationSettings.handoff_policies is enabled.
| Field | Type | Description |
|---|---|---|
handoff | Handoff | Handoff record produced by the turn. |
conversation.completed
Lifecycle marker — the conversation reached a terminal recommendation. The recommendation itself is already present in the preceding conversation.assessment.updated payload.
| Field | Type | Description |
|---|---|---|
conversation_id | UUID | Conversation that has completed. |
conversation.terminated
Emitted when the turn left the conversation permanently unable to accept further messages, whatever the cause — for example an underage patient or a confirmed age or sex change. Further turn calls fail with conversation_terminated; the client must start a new conversation.
| Field | Type | Description |
|---|---|---|
conversation_id | UUID | Conversation that has terminated. |
The event is a lifecycle marker only. Why the conversation ended, and what the user should do about it, arrive in that turn's assistant messages — render those, not a client-side reason mapping.
Terminal turn events
Exactly one of these ends each turn; they are mutually exclusive.
conversation.turn.completed
| Field | Type | Description |
|---|---|---|
turn_id | UUID | The turn that has completed. |
conversation.turn.interrupted
Emitted when the turn was interrupted by a newer accepted input.
| Field | Type | Description |
|---|---|---|
turn_id | UUID | The interrupted turn. |
conversation.turn.failed
| Field | Type | Description |
|---|---|---|
turn_id | UUID | The failed turn. |
error
Recoverable protocol or processing error. Both transports use the same payload shape. A WebSocket connection stays open after error unless the server also closes the socket; an HTTP NDJSON stream closes after a terminal turn event.
| Field | Type | Description |
|---|---|---|
error | Error | Error code and message; see Error codes. |
Successful turn order
A successful non-terminal turn produces:
conversation.user_message.created- Then, for each assistant message the turn produces, in order:
conversation.assistant_message.delta— one or moreconversation.assistant_message.ui_widget— zero or more, only whenui_widgets_enabledistrue; may appear before, between, or after deltas for the sameassistant_message.idconversation.assistant_message.completed
conversation.assessment.updatedconversation.handoff.requested— only when a handoff was recorded this turnconversation.turn.completed
A single turn commonly produces several assistant messages of different kinds — for example a short acknowledgement followed by the intro that carries the actual question. Each one runs its own delta/completed cycle, distinguished by assistant_message.id. Treat the terminal turn event, not the first conversation.assistant_message.completed, as the end of assistant output for the turn.
Variations:
- A turn that reaches a recommendation inserts
conversation.completedbetween the handoff step, if any, and the finalconversation.turn.completed. - A turn that leaves the conversation unable to continue emits
conversation.terminatedbetween the optional handoff event and the terminal turn event. The explanation for the user arrives earlier in the turn, onconversation.assistant_message.completed, like any other assistant message. - A terminating turn never emits
conversation.handoff.requested: the two are mutually exclusive, and terminating clears any handoff recorded earlier in the conversation.
Transient widget events
conversation.assistant_message.ui_widget is transient: it is delivered live to connected clients but is not individually persisted or replayed on reconnect. When ui_widgets_enabled is true, the server may emit one or more widget events while a message is still streaming, so clients can render structured UI before conversation.assistant_message.completed.
On turn completion, the same widgets appear in assistant_message.content_parts on the completed event, and in blocking Turn.assistant_messages. Treat content_parts as the durable source of truth and transient widget events purely as progressive rendering.
After a reconnect, transient events in the gap between snapshot creation and live subscription may be missed. Reconcile from the next durable boundary — conversation.turn.completed, conversation.turn.interrupted, or conversation.turn.failed — and from refreshed conversation snapshots. If the first post-reconnect transient is not at the start of a new turn, ignore further transients for that in-flight turn until a terminal turn event.