Docs / SDK reference / Both paths
Events reference
Every event the agent emits while a turn runs, and after it finishes.
Everything the agent tells you while a turn runs, and after it finishes.
Two vocabularies, one channel
Two different sets of events arrive on the same listener, and they come from different places.
Runtime events are what the agent runtime emits as it works: text arriving, tools running, the turn finishing. They're the bulk of this page.
SDK events are about the conversation itself rather than the turn: which conversation this is, and what history was restored. The runtime knows nothing about stored conversations, so the SDK owns these.
You handle both in the same listener. The distinction matters when you're looking something up, because they're documented separately, and it matters if you're using the drop-in element, because only one of the two crosses into your DOM.
How you receive them
With the drop-in element, runtime events arrive as mindset:runtime-event DOM events, with the runtime event as detail.
element.addEventListener("mindset:runtime-event", (e) => {
const event = e.detail;
if (event.type === "text_delta") append(event.content);
});
With the headless client, both vocabularies arrive on on().
chat.on((event) => {
if (event.type === "text_delta") append(event.content);
});
Configuration and transport failures are a separate channel. On the element they're mindset:error, and they carry their own codes rather than being runtime events.
The rule that keeps your integration working
Switch on type and ignore anything you don't recognize.
This vocabulary is additive only. New event types will appear, and existing ones won't be renamed or reshaped. A consumer that handles what it knows and skips the rest keeps working across updates. A consumer that throws on an unfamiliar type will break on one.
The shape of a turn
run_started is the first event of a run. run_finished or run_error ends it. complete carries the final answer.
What happens in between varies by turn and by model. Don't build logic that assumes a fixed order beyond those brackets.
Runtime events
type |
detail |
What it means |
|---|---|---|
run_started |
{ runId? } |
The turn has started |
run_finished |
{ runId? } |
The turn finished cleanly |
run_error |
{ message, runId?, aborted?, reason?, effects?, stepLimit? } |
The turn ended in an error. Terminal. See abort semantics below |
text_delta |
{ content } |
A chunk of the reply. Concatenate content across all of these for the full text. Each one may be a fragment |
thinking_delta |
{ content } |
A fragment of the model's summarized reasoning, while it thinks |
stream_flush |
{} |
Token streaming ended for the current chunk. Buffering displays flush here. Not the same as complete, which means the turn is done |
tool_announce |
{ toolName, toolCallId } |
The model has named a tool it's about to call, while its arguments are still streaming |
tool_start |
{ toolName, toolCallId, args? } |
A tool is about to execute. toolCallId pairs with the matching tool_end |
tool_end |
{ toolName, toolCallId, durationMs, output?, isError? } |
A tool finished. isError marks a failed call, with the error in output |
widget-payload |
{ doc, kind?, summary?, toolCallId? } |
A renderable payload produced during the turn. Read the note below before using it |
citation_applied |
{ enrichedText } |
The reply with citation markers inserted |
quick_replies |
{ question, options } |
Two to four tappable canned responses under a short prompt |
follow_up_questions |
{ questions } |
Two or three open-ended prompts to continue with |
references |
{ records } |
The turn used retrieval and produced source references |
conversation_title |
{ title } |
A generated short title for the conversation |
complete |
{ response, messages, threadId?, bound? } |
The final answer. response is the agent's text |
Three that need care
thinking_delta is display only. It never becomes part of the answer and it isn't stored with the conversation. Models that don't think never emit it, so tolerate its complete absence rather than waiting for it.
tool_announce is a timing hint, not a guarantee. It only fires when the model adapter can see the provider's stream boundaries. tool_start always fires when execution actually begins, correlated by the same toolCallId. Build on tool_start.
widget-payload needs you to branch on kind. Don't hand doc straight to a renderer.
For the presentational kinds — chart, callout, table, card, badge, stat and widget — doc is a widget tree you render through our widget renderer.
For the interactive kinds — quick_replies and capability_cards — doc is a different shape entirely, carrying replies or cards. These are meant to be drawn as tappable things that send the chosen option. Passing them to the renderer loses the interaction.
Like thinking_delta, this one is display only and won't appear on every turn.
Abort semantics
When someone stops a turn, you get run_error with aborted: true.
That marks a cancelled run rather than a failure: the caller's abort signal fired, the model call was cut, and the loop exited. It's a requested outcome. Render it as "stopped", not as an error.
It's a marker on run_error rather than its own event type so that consumers switching exhaustively over the vocabulary don't have to change.
SDK events
These arrive on on() alongside the runtime vocabulary when you're using the headless client.
conversation_id
Which conversation this is. Fires once, either at mount when you supplied the ID or on the first completed turn when the SDK started a fresh one.
Store it against whoever is looking and hand it back on their next visit. That round trip is the whole capability.
It's a correlation key, not a credential. It grants nothing without the session credential.
The drop-in element re-emits this one to your DOM as mindset:conversation.
history_settled
The restored transcript. This is how a custom interface paints a returning user's history.
Fires once per conversation, after hydration settles, and it always arrives. Restored, empty, or failed, you get the event, so you can safely hold your composer closed until it lands. The read carries a fifteen-second bound, and a connection that stalls is dropped to a cold start rather than hanging forever.
That bound is a liveness guarantee, not a display policy. It's generous on purpose and shouldn't fire on a working connection. Set your own shorter hold for how long you'll keep the interface waiting.
The payload is display-safe as it arrives, and frozen, so one listener can't corrupt what another sees. It's the prose turns plus any rendered widgets those turns displayed, with tool traffic filtered out.
Each message carries an optional widget of { doc, summary, kind }. You can ignore it. A rendered turn's content is that widget's own plain-text summary, so reading { role, content } alone still paints an honest transcript. Read widget only if you want to redraw the original.
Two things it can't tell you. It doesn't distinguish "this user has no history" from "we couldn't load it", because a failed read is a silent cold start. So you can't render "couldn't load your history" from this event.
And it's deliberately not a DOM event. The drop-in element consumes it internally and doesn't rebroadcast it, because DOM events cross the shadow boundary into your document where any script could read them, and this is the user's private conversation content. If you're using the element you won't see this event, and you don't need to: the element has already painted the history.
Don't build display for these yet. Do tolerate them, under the same rule as any unrecognized type.
Note that interrupt is not used for cancellation, despite the name. A cancelled run is run_error with aborted: true, as above.