Docs / SDK / Build / Errors and resilience
View as MarkdownErrors 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 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 |
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:
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:
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
getSessionagain 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:conversationon the element), withreplacedBecause: "discarded". Store whatever ID arrived last. - History that won't load.
history_settledstill 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 covers what the ceiling covers and what it doesn't.
Related
- Events reference for every event and field.
- The mindset-agent element for the element's error codes.
- Create a session for your users for every create error.