ReferenceSDKThe UI-less client

Updated 27 Oct 2026View as Markdown

The UI-less client

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

See it running: Halcyon

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 instead. It is a thin shell over this client.

Live demoAsk your data · Northwind AnalyticsAsk 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();
  },
});
OptionTypeWhat it does
agentstringRequired. The agent's handle or ID
getSessionfunctionRequired. Returns the JSON body your backend got from the session create call, or a promise of it
conversationIdstringResume this conversation. Leave it out for a fresh one
initialMessagesarrayEarlier 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#

CommandWhat 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

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. 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.

CommandWhat 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#

MemberWhat 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
conversationIdThe 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.

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.

CommandWhat 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 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 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 covers what to do.