# The UI-less client

> Reference for driving a Mindset agent from your own interface. Loading, commands, events, state, conversation lists and page tools.

**Skill for your coding agent:** [`mindset-uiless/SKILL.md`](https://docs.mindset.ai/skills/mindset-uiless/SKILL.md)

**Skill for your coding agent:** [`mindset-page-tools/SKILL.md`](https://docs.mindset.ai/skills/mindset-page-tools/SKILL.md)

In the browser, the SDK runs the conversation with Mindset, with your page tools and widgets. It gets its session from your server, which creates it with your org API key. Mindset holds the agent and runs its model and tools. Highlighted: Browser.

The UI-less client gives you the same agent and conversation as the drop-in element, with typed events in and commands out, and no Mindset UI at all.

Choose it when the conversation has to live inside an interface you already have, or when the agent's output drives something other than a chat transcript. If the Mindset chat panel fits, use [the mindset-agent element](https://docs.mindset.ai/docs/the-mindset-agent-element) instead. It is a thin shell over this client.

**Live demo:** [Ask your data](https://docs.mindset.ai/demos/ask-your-data). Ask sales questions and a dashboard draws the answers from what the agent’s functions return.

A scripted agent's greeting, cards, phases and questions appear only in the `<mindset-agent>` element. On this client the agent runs on its configuration and tools, without them. If the agent you're embedding has a script, use the element.

## Load it

The client is an ES module served from your Mindset host. There is nothing to install. Import it from a module script or a dynamic `import()`.

```javascript
import { createAgentConversation } from "https://YOUR-MINDSET-HOST/sdk/mindset-agent-uiless.js";
```

The module also exports `isSdkEvent`, `SDK_EVENT_TYPES`, `readPageMessage`, and `version`, `commit` and `buildTimeMs` for the build you loaded.

## Create a conversation

```javascript
const chat = createAgentConversation({
  agent: "billing-help",
  getSession: async () => {
    const r = await fetch("/api/session"); // your backend endpoint
    if (!r.ok) throw new Error("Could not start a session");
    return r.json();
  },
});
```

| Option            | Type     | What it does                                                                                                                             |
| ----------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `agent`           | string   | Required. The agent's handle or ID                                                                                                       |
| `getSession`      | function | Required. Returns the JSON body your backend got from the [session create call](https://docs.mindset.ai/docs/sessions-and-trust), or a promise of it            |
| `conversationId`  | string   | Resume this conversation. Leave it out for a fresh one                                                                                   |
| `initialMessages` | array    | Earlier messages to seed the agent's memory with, as `{ role, content, silent }` objects. Most integrations use `conversationId` instead |

`createAgentConversation` calls `getSession` straight away, not on the first message. Each call creates a real session on your backend, so create the conversation when the user can actually use it, for example when they open the chat panel.

The SDK reads the session, renews it while the page is open, and calls `getSession` again when it can't renew. You write no refresh logic. The session carries your org, environment and Mindset host, so there's nothing else to configure.

If `getSession` returns something the SDK can't use, you get a `session_error` event straight away. If `getSession` throws (your backend was briefly unreachable), there's no event, and the SDK tries again the next time it needs a session.

## Drive the conversation

| Command                      | What it does                                                                                                                                                                                                    |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `send(text)`                 | A turn the user typed. Resolves to the agent's reply text                                                                                                                                                       |
| `sendMessage(text, options)` | A turn your app sends. Pass `{ silent: true }` to keep it out of the transcript while the agent still reads it, or `{ from: "page" }` to report something that happened on the page. Resolves to the reply text |
| `widgetAction(action)`       | Send a rendered widget's action back to the agent as a silent turn. A string goes as is. Anything else is sent as JSON with a line saying the user interacted with a widget. Resolves to the reply text         |
| `stop()`                     | Cancel the turn in progress. The model call is cut and spend stops. Safe when idle                                                                                                                              |
| `isAgentBusy()`              | Whether a turn is running now                                                                                                                                                                                   |
| `retry()`                    | Run the last turn again, replacing its reply. Does nothing if there isn't one                                                                                                                                   |
| `reset()`                    | Clear the agent's memory of earlier turns. It keeps the same conversation ID, so don't use it to start over. See [start a new conversation](#start-a-new-conversation)                                          |

Turns run one at a time. Sending a new turn while one is running cancels the running one, so check `isAgentBusy()` first if that would cut off an answer.

The promise from `send`, `sendMessage`, `widgetAction` and `retry` rejects when the turn fails or is stopped. Most interfaces render from events and only catch the rejection:

```javascript
chat.send("Why was I charged twice?").catch(() => {
  // The run_error event already told the listener what happened.
});
```

A stopped turn ends with a `run_error` event carrying `aborted: true`. Show it as "stopped", not as an error.

## Listen

```javascript
let reply = "";

const unsubscribe = chat.on((event) => {
  switch (event.type) {
    case "text_delta":
      reply += event.content;
      renderReply(reply);
      break;
    case "complete":
      reply = "";
      break;
    case "run_error":
      showNotice(event.aborted ? "Stopped" : event.message);
      reply = "";
      break;
    case "conversation_id":
      saveConversationId(event.conversationId);
      break;
    default:
      break; // ignore anything you don't recognize
  }
});
```

`renderReply`, `showNotice` and `saveConversationId` stand for your own code.

`on()` returns a function that unsubscribes. A listener added after construction still receives `conversation_id`, `history_settled` and `session_error` if they already fired.

Two kinds of event arrive here: the agent runtime's events and the SDK's own events about the conversation. `isSdkEvent(event)` tells them apart. Both are in the [events reference](https://docs.mindset.ai/docs/events-reference). Keep a `default` branch, because new event types can appear.

## Resume a conversation

The round trip is the same as on the element.

1. After the user's first completed turn, a `conversation_id` event arrives. Store `event.conversationId` against that user in your backend.
2. On their next visit, pass it as `conversationId` to `createAgentConversation`.
3. Wait for `history_settled`, draw `event.messages` as the earlier transcript, then open your input box.

`history_settled` always fires once per conversation: with the restored messages, with an empty list, or with an empty list after a failed or stalled read (it gives up after 15 seconds). So you can safely keep your input closed until it arrives. Set your own shorter limit for how long you make the user wait.

If the ID you supplied can't be used, the SDK starts a fresh conversation and sends `conversation_id` again before any turn, with `replacedConversationId` and `replacedBecause: "discarded"`. Storing whatever arrived last keeps you correct.

## Start a new conversation

There is no "new conversation" command. Stop the current conversation and create another without a `conversationId`:

```javascript
let chat = createConversation();

function createConversation() {
  const c = createAgentConversation({
    agent: "billing-help",
    getSession: async () => {
      const r = await fetch("/api/session");
      if (!r.ok) throw new Error("Could not start a session");
      return r.json();
    },
  });
  c.on(handleEvent); // your own listener, as in Listen above
  return c;
}

function startNewConversation() {
  chat.stop();
  chat = createConversation();
}
```

`handleEvent` stands for your listener. Each `createAgentConversation` calls `getSession`, so a new conversation is also a new session. After its first completed turn, `conversation_id` arrives with the new ID. Store that one.

Don't use `reset()` for this. It clears what the agent remembers in the page but keeps the conversation ID, so later turns are stored in the same conversation, and resuming that ID later doesn't give a clean transcript.

## Show earlier conversations

```javascript
const { conversations, nextCursor } = await chat.listConversations({ limit: 20 });

for (const c of conversations) {
  addPickerRow(c.conversationId, c.title ?? "Untitled", c.updatedAt);
}

async function onPick(conversationId) {
  await chat.switchConversation(conversationId);
}
```

`addPickerRow` and `onPick` stand for your own picker code.

| Command                                | What it does                                                                                                                                                                                                                                        |
| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `listConversations({ limit, cursor })` | This user's conversations with this agent, newest first. Each has `conversationId`, `title` (may be `null`) and `updatedAt`. The server caps `limit`. Pass `nextCursor` back as `cursor` for the next page. A `null` cursor means there are no more |
| `switchConversation(id)`               | Load that conversation's history and continue it. Fires `conversation_id` with `replacedBecause: "switched"`. If the load fails, it throws and the conversation stays where it was. Switching to the current conversation does nothing              |

The list never includes another user's conversations or another agent's. Store the ID from the `conversation_id` event after a switch, or the user's next visit opens the conversation they switched away from.

## Read state

| Member              | What it gives you                                                                                                                                                                                                                                                                                |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `transcript()`      | What to display: user and agent turns, without silent turns and tool traffic. A turn that showed a widget carries it on `.widget` as `{ doc, summary, kind }`, plus `ref` (`{ handle, version }`) when the widget was authored in the Studio, and `block` when a script asked a question with it |
| `messages()`        | What the model sees: silent turns, the agent turns that called tools, and the tool results                                                                                                                                                                                                       |
| `conversationId`    | The ID this conversation's turns are stored under                                                                                                                                                                                                                                                |
| `enabledFeatures()` | Resolves to the release features switched on for this deployment, from your `getSession` response. Empty when there are none, or when `getSession` returned only `{ session }`                                                                                                                   |

A message sent with `{ from: "page" }` keeps a wrapper in `content`. `readPageMessage(content)` returns `{ kind, text }` for it, so you can draw it as a page note rather than as the user's words.

`transcript()` re-reads every widget in the conversation on each call. Call it when state changes and keep the result, rather than calling it on every render.

> **Warning:** Rendering from `messages()` shows users silent turns and raw tool traffic they were never meant to see.

## Give the agent context and tools

Three commands connect your app to the agent. Each replaces its whole set, so pass the full set each time. Passing a shorter set is how you remove an entry. Each is read fresh on every turn.

| Command                            | What it does                                                                                                                                                           |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `setPageTools(tools)`              | Functions in your page the agent may call                                                                                                                              |
| `setSituationalAwareness(entries)` | Facts about what the user is looking at, as `{ key: value }` entries with any JSON values, added to the agent's context on every turn. Up to 10,000 characters of JSON |
| `setPassthroughParams(params)`     | Values a tool needs that the model must never see                                                                                                                      |

[Page tools](https://docs.mindset.ai/docs/page-tools) explains each one, with examples and the safety rules for page tools.

## What isn't possible

- No image attachments on the published client yet.
- No renaming or deleting conversations.
- No Mindset widget renderer is served for the UI-less client. A widget arrives as data (`doc`, `kind` and a plain-text `summary`). Show the summary, or see [Use your own UI](https://docs.mindset.ai/docs/use-your-own-ui) for what you can rely on.

## Stale configuration

When someone activates a new version of the agent in the Studio, an open conversation's configuration goes stale. The SDK fetches the new configuration and runs the turn again, once, without telling you, as long as the turn hasn't produced anything yet. You see a `run_error` with `reason: "stale_configuration"` only when the retry hits the same problem, or when the change lands after the turn has started producing output. [Errors and resilience](https://docs.mindset.ai/docs/errors-and-resilience) covers what to do.
