# Events reference

> Every event an embedded agent sends while a turn runs and after it finishes, with each payload and how to handle it.

Use this page to handle everything an embedded agent tells your code: text as it streams, tools starting and finishing, the end of a turn, and which conversation is running.

## Two kinds of event

**Runtime events** come from the agent runtime as a turn runs: text arriving, tools running, the turn ending. Most of this page is about them.

**SDK events** are about the conversation rather than the turn: which conversation this is, what history was restored, and whether the session works. The SDK owns these, because the runtime knows nothing about stored conversations.

## How you receive them

| | Runtime events | SDK events |
|---|---|---|
| Drop-in element | `mindset:runtime-event`, with the event as `detail` | `conversation_id` as `mindset:conversation`. A `session_error` becomes `mindset:error` with code `invalid-auth`. `history_settled` is not passed on |
| UI-less client | On `chat.on()` | On `chat.on()`. Use `isSdkEvent(event)` to tell the two kinds apart |

```javascript
// Drop-in element
document.querySelector("mindset-agent").addEventListener("mindset:runtime-event", (e) => {
  const event = e.detail;
  if (event.type === "text_delta") console.log(event.content);
});
```

```javascript
// UI-less client
chat.on((event) => {
  if (event.type === "text_delta") console.log(event.content);
});
```

The element's own configuration and transport failures arrive separately, as `mindset:error`. See [error codes](https://docs4.mindset.ai/docs/sdk/the-mindset-agent-element#error-codes).

## The rule that keeps your integration working

Switch on `type` and ignore anything you don't recognize. The vocabulary is additive only. New event types and new fields can appear. Existing ones keep their names and shapes. Code that throws on an unfamiliar type will break one day.

## The shape of a turn

`run_started` is the first event of a turn. `run_finished` or `run_error` ends it. `complete` carries the final answer.

What happens in between varies by turn and by model. Don't rely on any order beyond those brackets.

## Runtime events

The fields sit on the event object itself, next to `type`.

| `type` | Fields | 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. See [run_error](#run-error) |
| `text_delta` | `content` | A piece of the reply. Join `content` across these for the full text |
| `thinking_delta` | `content` | A piece of the model's summarized reasoning while it thinks |
| `stream_flush` | none | Streaming paused for now. Flush any buffered text. The turn isn't over until `complete` |
| `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 run |
| `tool_end` | `toolName`, `toolCallId`, `durationMs`, `output?`, `isError?`, `refused?` | A tool finished. `isError` marks a failed call, with the error in `output`. `refused` marks a call that never ran because the agent wasn't allowed it |
| `widget-payload` | `doc`, `kind?`, `summary?`, `toolCallId?` | Something to render, produced during the turn. See [widget-payload](#widget-payload) |
| `citation_applied` | `enrichedText` | The reply with citation markers such as `[1]` inserted |
| `quick_replies` | `question`, `options` | Two to four short replies the user can tap, under a short question |
| `follow_up_questions` | `questions` | Two or three open questions to continue with |
| `references` | `records` | The turn used retrieval. `records` are the sources |
| `conversation_title` | `title` | A short generated title for the conversation |
| `complete` | `response`, `messages`, `conversationId?`, `bound?` | The final answer. `response` is the agent's text. See [complete](#complete) |

### thinking_delta

Display only. It never joins the answer and isn't stored with the conversation. Models that don't think never send it, so don't wait for it.

### tool_announce and tool_start

`tool_announce` is a timing hint and may not arrive at all. `tool_start` always fires when the tool actually runs, with the same `toolCallId`. Build on `tool_start`, and pair it with `tool_end` by `toolCallId`.

### widget-payload

Branch on `kind`. Don't pass `doc` straight to a renderer.

| `kind` | What `doc` is |
|---|---|
| `chart`, `callout`, `table`, `card`, `badge`, `stat`, `widget` | A widget tree to draw |
| `quick_replies`, `capability_cards` | A different shape, carrying `replies` or `cards`. Draw these as tappable options that send the chosen one |

`summary` is a plain-text description of the widget. On the UI-less client, where no Mindset renderer is served, showing `summary` is a reasonable fallback. To send a widget interaction back to the agent, use `widgetAction()`.

Display only, and many turns don't produce one.

### run_error

`reason`, when present, says which kind of failure it was:

| `reason` | Meaning |
|---|---|
| `cancelled` | Someone stopped the turn. Always comes with `aborted: true` |
| `step_limit` | The run used its step budget mid-work. `stepLimit` is the budget |
| `model_error` | A model call failed. Trying again may work |
| `tool_schema` | A tool definition was refused by the model provider. It fails the same way until the tool is fixed in AMS |
| `unavailable` | The agent can't run as asked, for example it has no version in force |
| `stale_configuration` | The agent's active version changed since this conversation loaded it |
| `failed` | Anything else |

`effects`, when present, lists tool calls that changed something and finished before the run failed. Check it before you offer a retry, so a retry doesn't repeat a booking or a payment.

### Stopping a turn

When someone stops a turn, you get `run_error` with `aborted: true` and `reason: "cancelled"`. The model call was cut and the turn ended as requested. Show it as "stopped", not as an error.

### complete

`messages` is the full message list at the end of the turn. `conversationId` is the conversation the turn belonged to.

`bound`, when present, means the turn stopped at a limit before the job was done. `bound.reason` is `step_limit`, `deadline`, `spend_limit` or `repetition`, and `bound.limit` is the limit's value. The turn is still complete and can be continued.

No `bound` means no limit was hit. It doesn't prove the agent finished the job.

## SDK events

These arrive on the UI-less client's `on()` alongside runtime events.

### conversation_id

`{ type: "conversation_id", conversationId, replacedConversationId?, replacedBecause? }`

Which conversation this is. It fires when the ID first becomes known: at once if you supplied a `conversationId`, otherwise after the first completed turn. It fires again only when the conversation changes:

- `replacedBecause: "discarded"`: the ID you supplied couldn't be used, so a fresh conversation started. This arrives before any turn.
- `replacedBecause: "switched"`: `switchConversation()` moved to another conversation. The old ID still works.

Store the latest `conversationId` against the user and hand it back next visit. It is a correlation key, not a credential. The element re-emits this event to your page as `mindset:conversation`.

### history_settled

`{ type: "history_settled", messages }`

The restored transcript, so a custom interface can draw a returning user's history. It fires once per conversation, every time: with restored messages, with an empty list, or with an empty list after a failed or stalled read (the read gives up after 15 seconds). You can keep your input closed until it arrives.

Each message has `role` and `content`, and optionally `widget` as `{ doc, summary, kind }`. When a turn showed a widget, its `content` is that widget's summary, so drawing `role` and `content` alone still gives an honest transcript. The list is frozen.

It can't tell "no history" from "history couldn't be loaded". Both arrive as an empty list.

The element doesn't pass this event to your page, because it contains the user's private conversation and DOM events are readable by every script on the page. The element draws the history itself.

### session_error

`{ type: "session_error", code, message, context }`

The value your `getSession` returned can't be used. It arrives straight away, when `getSession` first answers, not on the first turn.

| `code` | Meaning | Fix |
|---|---|---|
| `invalid-session-credential` | The SDK couldn't read what came back | Return your endpoint's parsed JSON body unchanged |
| `expired-session-credential` | It was readable but already used up | Create a new session instead of reusing a cached one |

`message` says what to return. `context` names the shape that came back, such as `{ received: "Response" }`, never the value. More codes can be added.

If `getSession` throws instead, there's no `session_error`. The SDK asks again later.

On the element, this arrives as `mindset:error` with code `invalid-auth`.

## Reserved event types

The runtime defines `interrupt`, `guard_intercept`, `guard_passed`, `exit_confirmation` and `plan_update`, but doesn't send them today. Don't build for them yet. Your default branch already ignores them.
