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.