# Errors and resilience

> Every way an embedded agent can fail, from the session call to a turn, and what your code should do about each one.

After this page your integration handles every failure an embedded agent can report: it tells the user something useful, retries only when a retry can work, and never repeats a payment or a booking by accident.

## Where failures show up

| Where it fails | Drop-in element | UI-less client |
|---|---|---|
| Your server's session create call | An HTTP error your backend receives | The same |
| `getSession` returns something the SDK can't use | `mindset:error` with `invalid-auth` | A `session_error` event |
| `getSession` throws | Nothing at once. The next turn fails with `turn-failed` | Nothing at once. The next turn fails, and its promise rejects |
| Your code calls the element wrongly | `mindset:error` with a configuration code | Not applicable |
| A turn fails | `mindset:error` with `turn-failed`, plus a `run_error` runtime event when the run itself failed | A `run_error` event when the run itself failed, and the promise from `send()` rejects |
| A turn stops at a limit | A `complete` runtime event carrying `bound` | The same, on `on()` |

On the element, runtime events arrive as `mindset:runtime-event`. On the UI-less client, everything arrives on `chat.on()`. The [events reference](https://docs4.mindset.ai/docs/sdk/events-reference) has every field.

## What to do about each one

Branch on the code or reason, never on the message text. Treat a code you don't recognize as a general failure, because new ones can be added.

| Signal | What happened | What to do |
|---|---|---|
| Create: 400 `validation_error` | Your request body is wrong, or the agent has no published version to embed | Fix the request or publish the agent. Retrying the same call won't help |
| Create: 403 `agent_not_available_here` | **Embedded in your own product** isn't ticked for the agent | An org admin ticks it. See [Where your agent can be reached](https://docs4.mindset.ai/docs/ams/where-your-agent-can-be-reached#choose-where-it-can-be-used) |
| Create: 404 `not_found` | Wrong org slug, environment slug or agent; a key without admin scope; a personal agent; or an unknown user without `createUserIfNeeded` | Check each against the agent's **Embed** tab. Retrying won't help |
| Create: 409 `user_not_active` | The user is invited but not yet active, or was removed | Don't retry. Tell the user they don't have access |
| Create: 409 `no_version_in_force` | No configuration version is active | Activate one in AMS |
| Create: a 5xx, or no answer | Mindset or the network failed | Return an error from your endpoint. The SDK calls `getSession` again the next time it needs a session |
| `getSession` throws | Your endpoint failed or was unreachable | Nothing extra. The SDK asks again later. Make sure your endpoint returns a non-2xx status and your function throws, rather than returning an error body |
| `invalid-auth`, or `session_error` with `invalid-session-credential` | `getSession` returned something the SDK can't read | Return your endpoint's parsed JSON body (`r.json()`) unchanged. Not the `Response`, and not a field from inside it |
| `session_error` with `expired-session-credential` | The session was readable but already used up | Create a new session on every `getSession` call. Don't cache one |
| `missing-agent` | `configure()` ran with no agent | Set the `agent` attribute |
| `not-configured` | `send()` or a `set` method ran before `configure()`, or after the element left the page | Call `configure()` first |
| `stale-configuration` | An attribute changed after `configure()` | Call `configure()` again |
| `turn-failed` | A turn failed: network, credential or the run | If a `run_error` runtime event came with it, follow its row below. A network or credential failure (an expired session, a `getSession` that threw) has no `run_error`. Let the user try again |
| `run_error`, `cancelled` | Someone stopped the turn (`aborted: true`) | Show "stopped". It isn't an error |
| `run_error`, `model_error` | A model call failed | Offer a retry, after checking `effects` |
| `run_error`, `step_limit` | The run used its whole step budget | Offer to continue or retry, after checking `effects`. If it keeps happening, the agent needs work in AMS |
| `run_error`, `tool_schema` | The model provider refused one of the agent's tool definitions | Don't retry. It fails the same way until the tool is fixed in AMS |
| `run_error`, `unavailable` | The agent can't run as asked, for example it has no version in force | Don't retry. Tell the user the assistant isn't available |
| `run_error`, `stale_configuration` | The agent's active version changed, and the SDK's own retry didn't get past it | Retry once more. The SDK has already fetched the new configuration |
| `run_error`, `failed` | Anything else | Offer a retry, after checking `effects` |
| `complete` with `bound` | The turn stopped at a limit: `step_limit`, `deadline`, `spend_limit` or `repetition`. It is complete, and can be continued | Show the reply, and let the user continue. `bound.limit` holds the limit's value |

## Check effects before you retry

A turn can fail after the agent has already done something. `run_error.effects` lists the tool calls that changed something and finished before the failure. Each has `toolName`, `toolCallId` and `access`, and may have a `reference` (a receipt the system returned) and `output`.

If `effects` has entries, a plain retry may repeat them: a second refund, a second booking. Tell the user what already happened, and let them decide.

This UI-less handler puts the rows above into code:

```javascript
chat.on((event) => {
  if (event.type !== "run_error") return;

  if (event.aborted) {
    showNotice("Stopped.");
    return;
  }

  const done = event.effects ?? [];
  if (done.length > 0) {
    const list = done.map((e) => e.reference ?? e.toolName).join(", ");
    showNotice(`Something went wrong, but these already happened: ${list}. Check before trying again.`);
    return;
  }

  switch (event.reason) {
    case "tool_schema":
    case "unavailable":
      showNotice("The assistant isn't available right now.");
      break;
    case "model_error":
    case "step_limit":
    case "stale_configuration":
    case "failed":
    default:
      showRetryButton(() => chat.retry().catch(() => {}));
      break;
  }
});
```

`showNotice` and `showRetryButton` stand for your own interface code. `retry()` runs the last turn again and replaces its reply. Its promise rejects if that turn fails too, and the `run_error` event reports it, so the empty `catch` only stops an unhandled rejection.

On the element, listen for `mindset:runtime-event` and read `e.detail` the same way:

```javascript
const agent = document.querySelector("mindset-agent");

agent.addEventListener("mindset:runtime-event", (e) => {
  const event = e.detail;
  if (event.type === "run_error" && !event.aborted && event.effects?.length) {
    console.warn("A turn failed after changing something:", event.effects);
  }
});

agent.addEventListener("mindset:error", (e) => {
  const { code, message } = e.detail;
  if (code === "invalid-auth") console.error("Fix getSession:", message);
  else if (code === "stale-configuration") reconfigure(); // your own function that calls configure() again
  else console.error("Mindset error:", code, message);
});
```

## What the SDK already handles

- **Renewal.** The SDK renews its access token while the page is open, and calls `getSession` again when it can't.
- **A changed agent version.** When the agent's active version changes mid-conversation, the SDK fetches the new configuration and runs the turn again, once, before you see anything. That works while the turn hasn't produced output yet.
- **A conversation ID it can't use.** It starts a fresh conversation and tells you with `conversation_id` (`mindset:conversation` on the element), with `replacedBecause: "discarded"`. Store whatever ID arrived last.
- **History that won't load.** `history_settled` still arrives, with an empty list, within 15 seconds. It can't tell you whether the history was empty or failed to load.

## Spend limits

Each person using an embedded agent has an hourly spend ceiling on their turns, roughly $25 an hour per person on each Mindset server. Past it, their turns are refused until spend in the window drops, and a refused turn fails like any other. [See and control what it costs](https://docs4.mindset.ai/docs/ams/see-and-control-what-it-costs) covers what the ceiling covers and what it doesn't.

## Related

- [Events reference](https://docs4.mindset.ai/docs/sdk/events-reference) for every event and field.
- [The mindset-agent element](https://docs4.mindset.ai/docs/sdk/the-mindset-agent-element#error-codes) for the element's error codes.
- [Create a session for your users](https://docs4.mindset.ai/docs/sdk/create-a-session-for-your-users#what-can-go-wrong) for every create error.
