Mindset AI

Docs / SDK reference / Headless

The UI-less SDK

The same agent and conversation with no interface at all — typed events in, commands out.

The same agent, the same conversation, with no interface at all. You get typed events as the agent works and commands to drive it, and you render every pixel yourself.

It is the same underlying code for our agent, but designed so you can build your own components around it.

Use it when the conversation has to live inside an interface you already have, when the agent's output drives something other than a chat transcript, or when your design system won't tolerate a component you don't control.

If our chat interface is fine, use the drop-in element instead. It's a thin shell over exactly this contract.

Loading it

The SDK is an ES module served from your Mindset AI host. There's no package to install.

const { createAgentConversation } = await import(
  "https://YOUR-MINDSET-HOST/sdk/mindset-agent-uiless.js"
);

Creating a conversation

const chat = createAgentConversation({
  agent: "support-bot",
  getSession: async () => {
    const r = await fetch("/api/session"); // from your application back end
    if (!r.ok) throw new Error("Could not start a session");
    const { session } = await r.json();
    return session;
  },
});
Option Type What it does
agent string Required. Which agent this conversation talks to, by handle
getSession function The credential provider. Returns the opaque session your backend created
conversationId string Resume an existing conversation. Leave it out for a fresh one

getSession is a function, not a string. The SDK calls it, decodes the credential, presents it on every call, and renews the session itself. When the session's outer lifetime is spent it calls your function again for a fresh one. You write no refresh logic.

A static string can't be renewed, so it isn't accepted. An organization API key is dropped rather than sent, and the call will fail with a 401 rather than leaking your key into a browser-reachable header.

Because the credential carries the organization, the Environment and the platform origin, org and baseUrl are both unnecessary on a normal integration.

Driving the conversation

Command What it does
send(text) A user-typed turn. Resolves to the agent's reply
sendMessage(text, options) A turn your application triggered rather than the user. Pass { silent: true } to keep the trigger out of the displayed transcript while the agent still receives it
widgetAction(action) Send a rendered widget's action payload back into the conversation. Framed as text and driven as a silent turn, so the agent reasons about the interaction without a stray user message appearing
stop() Cancel the turn in flight. The model call is cut and the spend stops
retry() Re-run the last turn, replacing its reply. Does nothing if there isn't one
reset() Clear all history and start fresh on the same agent

send and sendMessage both resolve to the reply as a string, so you can await them if that suits your interface. Most interfaces render from events instead, since awaiting means waiting for the whole turn.

stop() ends the turn with a run_error carrying aborted: true. That's a cancellation, not a failure. Render it as "stopped".

Listening

const unsubscribe = chat.on((event) => {
  switch (event.type) {
    case "text_delta":
      append(event.content);
      break;
    case "complete":
      finish(event.response);
      break;
    default:
      break; // ignore anything you don't recognize
  }
});

on() returns a function that unsubscribes.

Two families of event arrive here: the runtime's vocabulary, and the SDK's own events about the conversation. Both are covered in the events reference. Keep a default arm in your switch, because the vocabulary is additive and new types will appear.

Reading state

What it gives you
messages() What the model sees. Includes silent triggers, the assistant turns that called tools, and the tool results they produced
transcript() What you should display. User and assistant turns, without silent triggers and without tool rows, plus any rendered widgets on .widget
conversationId The ID this conversation's turns are grouped under. Read it to correlate with your own systems
envelope() The session envelope, fetched once and cached for this instance

Two of these need care.

transcript() is not free. It re-parses every widget envelope in the conversation on each call. Fine to call when state changes, but hold the result rather than calling it on every render. If you're painting restored history, the history_settled event hands you the same list already built.

messages() and transcript() are two different lists, not two views of one. If you render from messages() your users will see silent triggers and raw tool traffic they were never meant to.

Talking to the agent from your page

Three commands connect your application's state and capabilities to the agent.

Command What it does
setPageTools(tools) Functions in your page the agent may call. Replaces the whole set
setSituationalAwareness(entries) Facts about what the user is currently looking at, folded into the agent's context each turn

These are covered properly in "Host and agent channels". One thing belongs here, because it's the single most consequential fact about this SDK.

Page tool handlers run under adversarial influence

The model decides whether to call your tool and what arguments to pass it. The model is influenceable by anything in its context, which includes your knowledge base and the output of other tools.

Two consequences people get wrong.

The jsonSchema you declare is not an input filter. It's guidance to the model about what to send. Nothing validates the arguments against it before your handler runs. Validate them yourself.

There is no server-side re-authorization. An in-page function can't be re-authorized by us, which is exactly what makes it a page tool. Nothing checks that this user is allowed to do this thing at the moment the handler runs.

So treat every argument as attacker-controlled, and put nothing behind a page tool that you wouldn't expose to a hostile caller. No mutations, no payments, no reading personal data, unless your handler does its own authorization first.

What limits the damage is that you choose what to expose. Your organization API key never reaches the browser, and privileged operations still re-authorize on our servers.