Three complete starter integrations. The Built-in chat UI tab is the fastest path to a working agent. The two headless tabs show how to render your own UI on top of theDocumentation Index
Fetch the complete documentation index at: https://docs.mindset.ai/llms.txt
Use this file to discover all available pages before exploring further.
<mindset-agent> element.
- Built-in chat UI
- Headless: vanilla HTML
- Headless: React
This is the minimum integration. The element renders Mindset AI’s built-in chat UI inside a shadow DOM. Your users see a chat panel; you write no UI code.Replace the agent and app UIDs with yours, set up the token endpoint, and you have a working chat panel.Common patterns:Deep-linking to a specific thread:Programmatic priming. Silently prime the agent with context when the user does something on the page:The agent receives the message and can respond, but no user-side bubble appears in the chat.Call Attributes are read once at render time. Use the JS method if you need to update values dynamically.The runtime, state machine, auth flow, and DOM events stay the same. The only thing that changes is whether the element renders its own UI. Switch to the Headless: vanilla HTML or Headless: React tab above for what that looks like end-to-end.
Complete page
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Mindset AI agent</title>
<style>
body { margin: 0; font-family: system-ui, sans-serif; }
main { max-width: 800px; margin: 0 auto; padding: 2rem; }
mindset-agent { display: block; height: 600px; border: 1px solid #ddd; border-radius: 8px; }
</style>
</head>
<body>
<main>
<h1>Support agent</h1>
<p>Ask anything about your account, orders, or our products.</p>
<mindset-agent
agent-uid="agt-your-agent-uid"
></mindset-agent>
</main>
<script src="MINDSET-SERVER-URL/mindset-sdk3.umd.js"></script>
<script>
window.mindset.init({
appUid: 'your-app-uid',
fetchAuthentication: async () => {
// Your application's API - validates current user and then calls Mindset AI's API to create
const r = await fetch('/api/mindset-token', { credentials: 'include' });
const { authToken } = await r.json();
return authToken;
},
});
</script>
</body>
</html>
What you get
- Chat input and message history
- Thread switcher (the user can start, switch, rename, and delete threads)
- Streaming responses with typing indicator
- Tool widgets rendered automatically (cards, carousels, custom UIs)
- Quick reply chips and follow-up question buttons
- Citation display when the agent uses retrieval
- Theme that matches the page’s color scheme (configurable)
Sizing the element
<mindset-agent> is display: block by default. Size it like any other block element:mindset-agent {
display: block;
height: 600px;
width: 100%;
border: 1px solid #ddd;
border-radius: 8px;
}
- For full-page chat, use
height: 100vh; width: 100%. - For a sidebar, use
height: 100%; width: 360px. - For an embedded panel, use a fixed height like the example above.
Reacting to events
Even with the built-in UI, you can hook into events to integrate the agent with the rest of your page.const agent = document.querySelector('mindset-agent');
// Sync the active thread to the URL
agent.addEventListener('mindset:thread-changed', (e) => {
if (e.detail.threadUid) {
history.replaceState({}, '', `?thread=${e.detail.threadUid}`);
}
});
// Track turn completions in your analytics
agent.addEventListener('mindset:complete', (e) => {
analytics.track('agent_turn_complete', {
threadId: e.detail.threadId,
responseLength: e.detail.response.length,
});
});
// Show your own loading state outside the chat panel
agent.addEventListener('mindset:agent-busy', () => updateStatusIndicator('thinking'));
agent.addEventListener('mindset:agent-idle', () => updateStatusIndicator('ready'));
Driving the agent from outside
You can call methods on the element from anywhere on your page. Useful for:Quick-action buttons that send a pre-written message:<button id="ask-status">Where's my order?</button>
<script>
document.getElementById('ask-status').addEventListener('click', () => {
document.querySelector('mindset-agent').sendMessage("Where's my order?");
});
</script>
const params = new URLSearchParams(location.search);
const threadUid = params.get('thread');
document.querySelector('mindset-agent').addEventListener('mindset:agent-idle', async function once() {
if (threadUid) {
await this.switchThread(threadUid);
}
this.removeEventListener('mindset:agent-idle', once);
}, { once: false });
document.querySelector('.cart-button').addEventListener('click', () => {
const agent = document.querySelector('mindset-agent');
agent.sendMessage('the user just opened the cart panel', { silent: true });
});
Adding page tools
Page tools let the agent call functions on your page during a turn. Configure them withagent.setPageTools():const agent = document.querySelector('mindset-agent');
agent.setPageTools([
{
name: 'lookup_order',
description: 'Look up the current user\'s order by ID',
parameters: {
type: 'object',
properties: {
orderId: { type: 'string', description: 'The order ID' },
},
required: ['orderId'],
},
handler: async ({ orderId }) => {
const r = await fetch(`/api/orders/${orderId}`);
return await r.json();
},
},
]);
setPageTools once the element has fired mindset:agent-idle. See When the element is ready to call.Configuration via attributes
For static setup that’s known at render time, you can pass values as attributes on the element instead of calling JS methods. See the<mindset-agent> reference for the full list.<mindset-agent
agent-uid="agt-..."
initial-question="What can I help you with today?"
situational-awareness='{"currentPage":"/products","userPlan":"enterprise"}'
></mindset-agent>
Switching to headless later
If you outgrow the built-in UI and want to render your own, add theheadless attribute and start listening for events:<mindset-agent agent-uid="agt-..." headless></mindset-agent>
This example builds a complete chat interface in plain HTML and JavaScript. The Render markdown in agent replies. Pipe Handle interrupts. Some turns pause for input. The agent calls a UI-interrupt tool (e.g. Persist the active thread in the URL. Sync
<mindset-agent> element runs the agent and dispatches DOM events; you handle rendering. No framework, no build step.What this example covers
- Streaming text rendering as it arrives
- Sending messages and managing busy state
- Rendering tool widgets when the agent calls tools
- Switching threads and showing thread history
- Quick reply chips and follow-up question buttons
Complete page
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Custom agent UI</title>
<style>
body { margin: 0; font-family: system-ui, sans-serif; display: grid; grid-template-columns: 240px 1fr; height: 100vh; }
aside { background: #f5f5f5; padding: 1rem; overflow-y: auto; }
aside h2 { font-size: 0.9rem; text-transform: uppercase; color: #666; }
aside ul { list-style: none; padding: 0; }
aside button { display: block; width: 100%; text-align: left; padding: 0.5rem; border: none; background: transparent; cursor: pointer; border-radius: 4px; }
aside button:hover { background: #e0e0e0; }
aside button.active { background: #d0d0ff; }
main { display: flex; flex-direction: column; padding: 1rem; }
#messages { flex: 1; overflow-y: auto; }
.message { margin-bottom: 1rem; padding: 0.75rem; border-radius: 8px; }
.message.user { background: #e0e8ff; margin-left: 2rem; }
.message.agent { background: #f0f0f0; margin-right: 2rem; }
.quick-replies { display: flex; gap: 0.5rem; flex-wrap: wrap; margin: 0.5rem 0; }
.quick-replies button { padding: 0.4rem 0.8rem; border: 1px solid #ccc; background: white; border-radius: 16px; cursor: pointer; }
form { display: flex; gap: 0.5rem; padding-top: 1rem; border-top: 1px solid #ddd; }
form textarea { flex: 1; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; resize: vertical; min-height: 2.5rem; }
form button { padding: 0.5rem 1rem; }
form button:disabled { opacity: 0.5; cursor: not-allowed; }
</style>
</head>
<body>
<aside>
<button id="new-thread">+ New thread</button>
<h2>History</h2>
<ul id="thread-list"></ul>
</aside>
<main>
<div id="messages"></div>
<form id="form">
<textarea id="input" placeholder="Ask anything…" required></textarea>
<button type="submit" id="send">Send</button>
</form>
</main>
<!-- Headless: no UI rendered by the element itself -->
<mindset-agent
id="agent"
agent-uid="agt-your-agent-uid"
headless
></mindset-agent>
<script src="MINDSET-SERVER-URL/mindset-sdk3-headless.umd.js"></script>
<script>
const agent = document.getElementById('agent');
const messagesEl = document.getElementById('messages');
const inputEl = document.getElementById('input');
const sendBtn = document.getElementById('send');
const formEl = document.getElementById('form');
const threadListEl = document.getElementById('thread-list');
const newThreadBtn = document.getElementById('new-thread');
// Render state
let currentReplyEl = null;
let currentReplyText = '';
// ---- Initialize the SDK ----
window.mindset.init({
appUid: 'your-app-uid',
fetchAuthentication: async () => {
const r = await fetch('/api/mindset-token', { credentials: 'include' });
const { authToken } = await r.json();
return authToken;
},
});
// ---- Listen for lifecycle events ----
agent.addEventListener('mindset:agent-idle', () => {
sendBtn.disabled = false;
refreshThreadList();
});
agent.addEventListener('mindset:agent-busy', () => {
sendBtn.disabled = true;
// Start a new agent message bubble
currentReplyText = '';
currentReplyEl = appendMessage('agent', '');
});
agent.addEventListener('mindset:agent-error', (e) => {
appendMessage('agent', `[error: ${e.detail.message}]`);
sendBtn.disabled = false;
});
// ---- Listen for streaming output ----
agent.addEventListener('mindset:text-delta', (e) => {
currentReplyText += e.detail.content;
if (currentReplyEl) currentReplyEl.textContent = currentReplyText;
});
// ---- Tool widgets ----
agent.addEventListener('mindset:tool-end', (e) => {
if (e.detail.widget && currentReplyEl) {
const widgetEl = renderWidget(e.detail.widget);
currentReplyEl.appendChild(widgetEl);
}
});
// ---- Quick replies ----
agent.addEventListener('mindset:quick-replies', (e) => {
if (!currentReplyEl) return;
const wrapper = document.createElement('div');
wrapper.className = 'quick-replies';
// Optional: render the framing question above the chips
if (e.detail.question) {
const q = document.createElement('div');
q.textContent = e.detail.question;
q.style.fontSize = '0.85rem';
q.style.color = '#666';
currentReplyEl.appendChild(q);
}
e.detail.options.forEach((option) => {
const btn = document.createElement('button');
btn.textContent = option;
btn.addEventListener('click', () => {
inputEl.value = option;
formEl.requestSubmit();
});
wrapper.appendChild(btn);
});
currentReplyEl.appendChild(wrapper);
});
// ---- Thread management ----
// mindset:thread-changed fires when a thread persists server-side
// (after the first user message of a new thread, or when switching to
// an existing one). It does NOT fire immediately on agent.newThread().
// A fresh thread has no uid until the user engages with it.
agent.addEventListener('mindset:thread-changed', () => {
refreshThreadList();
});
newThreadBtn.addEventListener('click', async () => {
await agent.newThread();
// newThread() resets the agent's local state but doesn't persist a thread
// server-side. That happens after the user's first message. Clear the
// message list now so the UI matches; the sidebar will pick up the new
// thread once it persists (via mindset:thread-changed).
messagesEl.innerHTML = '';
refreshThreadList(); // refresh anyway in case a previous thread should now appear
});
async function refreshThreadList() {
const { threads } = await agent.listThreads({ pageSize: 20 });
threadListEl.innerHTML = '';
threads.forEach((t) => {
const li = document.createElement('li');
const btn = document.createElement('button');
btn.textContent = t.title || '(untitled)';
if (t.uid === agent.threadUid) btn.classList.add('active');
btn.addEventListener('click', () => agent.switchThread(t.uid));
li.appendChild(btn);
threadListEl.appendChild(li);
});
}
// ---- Send a message ----
// sendWhenIdle handles the case where the agent is still busy with its
// icebreaker turn (an automatic first turn some agents are configured to
// fire on startup). If the agent is idle right now, it sends straight
// away; if it's busy, it waits for the next mindset:agent-idle and
// recurses. Safe to call at any time.
function sendWhenIdle(text) {
if (!agent.isAgentBusy()) {
agent.sendMessage(text);
} else {
agent.addEventListener('mindset:agent-idle', () => sendWhenIdle(text), { once: true });
}
}
formEl.addEventListener('submit', (e) => {
e.preventDefault();
const text = inputEl.value.trim();
if (!text) return;
appendMessage('user', text);
sendWhenIdle(text);
inputEl.value = '';
});
// ---- Helpers ----
function appendMessage(role, text) {
const el = document.createElement('div');
el.className = `message ${role}`;
el.textContent = text;
messagesEl.appendChild(el);
el.scrollIntoView({ block: 'end' });
return el;
}
function renderWidget(widget) {
// Replace this with your own widget rendering (cards, carousels, custom components).
const el = document.createElement('pre');
el.style.background = '#fafafa';
el.style.padding = '0.5rem';
el.style.borderRadius = '4px';
el.style.fontSize = '0.85rem';
el.textContent = JSON.stringify(widget, null, 2);
return el;
}
</script>
</body>
</html>
How it works
The element runs the agent. You write the rest. Three event handlers do most of the work:mindset:agent-busystarts a new agent message bubble in the UImindset:text-deltaappends streaming text to the current bubblemindset:agent-idlere-enables the send button. The turn is done.
sendWhenIdle helper covers a subtle case: if your agent is configured with an icebreaker (a turn that fires automatically as the agent settles after init), the agent goes idle once after init completes, then immediately busy again while the icebreaker streams, then idle a second time when the icebreaker finishes. A user clicking “Send” during that first window would otherwise hit a busy throw. sendWhenIdle re-checks busy state on each mindset:agent-idle, so it queues automatically until the agent is actually free.Common adjustments
Show a typing indicator. Replace the empty agent message with a ”…” while waiting for the first text delta:agent.addEventListener('mindset:agent-busy', () => {
currentReplyText = '';
currentReplyEl = appendMessage('agent', '…'); // visible while streaming starts
});
agent.addEventListener('mindset:text-delta', (e) => {
currentReplyText += e.detail.content;
if (currentReplyEl) currentReplyEl.textContent = currentReplyText; // overwrite the …
});
currentReplyText through your favorite markdown library before setting it on the element. Re-render on every delta:import { marked } from 'https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js';
agent.addEventListener('mindset:text-delta', (e) => {
currentReplyText += e.detail.content;
if (currentReplyEl) currentReplyEl.innerHTML = marked.parse(currentReplyText);
});
present_choices) and waits. Listen for mindset:interrupt, render UI based on e.detail.args, then drive the next turn with agent.sendMessage() once the user selects:agent.addEventListener('mindset:interrupt', (e) => {
// e.detail.interruptType identifies the tool that paused the turn.
// e.detail.args carries the data your UI needs to render.
const userSelection = await promptUserForChoice(e.detail.args);
agent.sendMessage(userSelection);
});
A dedicated programmatic-resume API on the element is on the roadmap. For now, treat interrupts as turn-ending and drive the next turn with
sendMessage().agent.threadUid to the URL so reloads land on the same thread. Because uids only surface after a thread persists server-side, the URL won’t get a value until the user’s first message. That’s the correct behavior, since a local-only thread can’t be reloaded anyway.agent.addEventListener('mindset:thread-changed', (e) => {
if (e.detail.threadUid) {
history.replaceState({}, '', `?thread=${e.detail.threadUid}`);
} else {
// threadUid went null (between threads or after newThread() reset).
history.replaceState({}, '', location.pathname);
}
});
// On page load, after the agent reaches idle:
agent.addEventListener('mindset:agent-idle', async function once() {
const threadUid = new URLSearchParams(location.search).get('thread');
if (threadUid) {
try {
await agent.switchThread(threadUid);
} catch (err) {
// The uid in the URL no longer exists (deleted, expired, or stale).
// Clear the URL and continue with a fresh thread.
console.warn('Stored thread no longer exists, starting fresh:', err.message);
history.replaceState({}, '', location.pathname);
}
}
agent.removeEventListener('mindset:agent-idle', once);
}, { once: false });
<mindset-agent> is a custom element, which means it works directly as a JSX tag with no wrapper component. This example shows the patterns React developers want: refs, useEffect for subscription, cleanup on unmount, and TypeScript types for the events.Initialization
Callmindset.init() once at app startup. Don’t put it inside a component’s render. Call it once in your entry file or a top-level effect:// src/lib/mindset-init.ts
declare global {
interface Window {
mindset: {
init(config: {
appUid: string;
fetchAuthentication: () => Promise<string>;
}): Promise<void>;
};
}
}
let initialized = false;
export function initializeMindset() {
if (initialized) return;
initialized = true;
window.mindset.init({
appUid: import.meta.env.VITE_MINDSET_APP_UID,
fetchAuthentication: async () => {
const r = await fetch('/api/mindset-token', { credentials: 'include' });
const { authToken } = await r.json();
return authToken;
},
});
}
initializeMindset() in your app’s entry point (e.g. main.tsx):import { initializeMindset } from './lib/mindset-init';
initializeMindset();
createRoot(document.getElementById('root')!).render(<App />);
index.html before the app boots:<script src="MINDSET-SERVER-URL/mindset-sdk3-headless.umd.js"></script>
TypeScript types for the element and events
There are no shipped TypeScript declarations yet (planned). Declare the types you need locally:// src/types/mindset.d.ts
declare namespace JSX {
interface IntrinsicElements {
'mindset-agent': React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement> & {
'agent-uid': string;
headless?: boolean | '';
'initial-question'?: string;
'initial-thread-id'?: string;
},
HTMLElement
>;
}
}
interface MindsetAgentElement extends HTMLElement {
setPageTools(tools: PageToolDefinition[]): void;
setSituationalAwareness(sa: Record<string, string>): void;
setPassthroughParams(params: Record<string, string>): void;
sendMessage(text: string, options?: { silent?: boolean }): void;
isAgentBusy(): boolean;
newThread(): Promise<void>;
switchThread(threadUid: string): Promise<void>;
deleteThread(threadUid: string): Promise<boolean>;
renameThread(threadUid: string, title: string): Promise<boolean>;
listThreads(options?: { pageSize?: number; pageToken?: string }): Promise<{
threads: Array<{
uid: string;
title: string;
createdAt: string;
updatedAt: string;
messageCount: number;
}>;
nextPageToken: string | null;
hasMore: boolean;
}>;
readonly threadUid: string | null;
}
// Event detail shapes. See events reference for the full list.
interface MindsetEventMap {
'mindset:agent-registered': CustomEvent<{ headless: boolean }>;
'mindset:agent-initializing': CustomEvent<{}>;
'mindset:agent-idle': CustomEvent<{}>;
'mindset:agent-busy': CustomEvent<{}>;
'mindset:agent-error': CustomEvent<{ code: string; message: string }>;
'mindset:thread-changed': CustomEvent<{ threadUid: string | null; previous: string | null }>;
'mindset:text-delta': CustomEvent<{ content: string }>;
'mindset:tool-start': CustomEvent<{
toolName: string;
toolCallId: string;
runId: string;
silent: boolean;
args?: Record<string, unknown>;
}>;
'mindset:tool-end': CustomEvent<{
toolName: string;
toolCallId: string;
runId: string;
durationMs: number;
silent: boolean;
widget?: unknown;
canvas?: unknown;
llmToolCallId?: string;
output?: string;
}>;
'mindset:complete': CustomEvent<{ response: string; threadId: string }>;
// …add more as you use them
}
interface PageToolDefinition {
name: string;
description: string;
parameters: Record<string, unknown>;
handler: (args: any) => Promise<unknown>;
}
A reusable hook
A small custom hook handles the common pattern: get a typed ref, subscribe to events, clean up on unmount.// src/hooks/useMindsetAgent.ts
import { useEffect, useRef } from 'react';
export function useMindsetAgent() {
const ref = useRef<MindsetAgentElement>(null);
function on<K extends keyof MindsetEventMap>(
type: K,
handler: (e: MindsetEventMap[K]) => void,
) {
useEffect(() => {
const el = ref.current;
if (!el) return;
el.addEventListener(type as string, handler as EventListener);
return () => el.removeEventListener(type as string, handler as EventListener);
}, [handler]);
}
return { ref, on };
}
A chat component
Putting it together:// src/components/AgentChat.tsx
import { useState, useCallback } from 'react';
import { useMindsetAgent } from '../hooks/useMindsetAgent';
interface Message {
role: 'user' | 'agent';
text: string;
}
interface Props {
agentUid: string;
}
export function AgentChat({ agentUid }: Props) {
const { ref, on } = useMindsetAgent();
const [messages, setMessages] = useState<Message[]>([]);
const [busy, setBusy] = useState(false);
const [input, setInput] = useState('');
const onBusy = useCallback(() => {
setBusy(true);
setMessages((prev) => [...prev, { role: 'agent', text: '' }]);
}, []);
const onIdle = useCallback(() => {
setBusy(false);
}, []);
const onDelta = useCallback((e: MindsetEventMap['mindset:text-delta']) => {
setMessages((prev) => {
const next = [...prev];
const last = next[next.length - 1];
if (last && last.role === 'agent') {
next[next.length - 1] = { ...last, text: last.text + e.detail.content };
}
return next;
});
}, []);
const onComplete = useCallback((e: MindsetEventMap['mindset:complete']) => {
console.log('Turn done in thread', e.detail.threadId);
}, []);
on('mindset:agent-busy', onBusy);
on('mindset:agent-idle', onIdle);
on('mindset:text-delta', onDelta);
on('mindset:complete', onComplete);
// sendWhenIdle handles the case where the agent is still busy with its
// icebreaker turn (an automatic first turn some agents are configured to
// fire on startup). If the agent is idle, it sends straight away; if it's
// busy, it waits for the next mindset:agent-idle and recurses. Safe to call
// at any time.
function sendWhenIdle(agent: MindsetAgentElement, text: string) {
if (!agent.isAgentBusy()) {
agent.sendMessage(text);
} else {
agent.addEventListener(
'mindset:agent-idle',
() => sendWhenIdle(agent, text),
{ once: true },
);
}
}
function send(e: React.FormEvent) {
e.preventDefault();
const text = input.trim();
if (!text || !ref.current) return;
setMessages((prev) => [...prev, { role: 'user', text }]);
sendWhenIdle(ref.current, text);
setInput('');
}
return (
<>
<mindset-agent
ref={ref}
agent-uid={agentUid}
headless
/>
<div className="messages">
{messages.map((m, i) => (
<div key={i} className={`message message-${m.role}`}>
{m.text}
</div>
))}
</div>
<form onSubmit={send}>
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Ask anything…"
/>
<button type="submit" disabled={busy}>
Send
</button>
</form>
</>
);
}
How it works
A few things make this React pattern straightforward:<mindset-agent>is a real custom element, so React recognizes it as a JSX tag and forwards the ref to the underlying DOM node. No wrapper component needed.- Events come from the element, not from React. Subscribe in
useEffect, return a cleanup function that callsremoveEventListener. The custom hook does this in one line. - State setters and
sendMessageare sync, so call them directly from event handlers and form submit handlers. Noawaitneeded. - Thread CRUD is async.
newThread,switchThread, and the rest return Promises. Handle them like any other API call. sendWhenIdlecovers the icebreaker race. If your agent is configured with an icebreaker (a turn that fires automatically as the agent settles after init), the lifecycle isagent-idle(init done), thenagent-busy(icebreaker streaming), thenagent-idle(icebreaker done). A user clicking Send during that first window would otherwise hit a busy throw. The recursivesendWhenIdlere-checks busy state on each idle event, so it queues automatically until the agent is actually free.
Configuring page tools
Set page tools when the component mounts. UseuseEffect to wait for the element ref:useEffect(() => {
const el = ref.current;
if (!el) return;
el.setPageTools([
{
name: 'lookup_order',
description: 'Look up the current user\'s order by ID',
parameters: {
type: 'object',
properties: { orderId: { type: 'string' } },
required: ['orderId'],
},
handler: async ({ orderId }: { orderId: string }) => {
const r = await fetch(`/api/orders/${orderId}`);
return await r.json();
},
},
]);
}, []);
mindset.init() has completed (or before the SDK script has loaded), setPageTools won’t exist on the element yet. Gate the call on a mindset:agent-idle listener (or wait for it inside the effect). See When the element is ready to call for the pattern.Updating situational awareness when state changes
Pass page state to the agent so it can personalize responses without the user having to repeat themselves:const { user, currentPage, cart } = useAppState();
useEffect(() => {
ref.current?.setSituationalAwareness({
currentPage,
userPlan: user.plan,
cartItems: String(cart.items.length),
});
}, [currentPage, user.plan, cart.items.length]);
Related
Methods reference
Every method on the element with signatures and behaviors.
Events reference
Every event the element fires, with payload shapes.
Integration walkthrough
The end-to-end setup, from token endpoint to working agent.
Data channels overview
How your application talks to the agent.