Mindset AI

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 widgetdoc is a widget tree you render through our widget renderer.

For the interactive kinds — quick_replies and capability_cardsdoc 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.