@smartdatahq/embedded-agent-headless
v0.3.7
Published
Headless engine for the SmartData Embedded Agent: the AgentProvider and hooks for teams building their own chat UI
Readme
@smartdatahq/embedded-agent-headless
The engine behind the SmartData Embedded Agent, without the UI. It gives you an AgentProvider and a set of hooks that handle the WebSocket connection, message parsing, streaming, conversation state, browser tools, checkout signals and audio, so you can build a chat interface in your own components and design system.
Looking for the drop-in chat UI instead? Use @smartdatahq/embedded-agent, which is built on top of this package.
Installation
npm install @smartdatahq/embedded-agent-headless
# or
yarn add @smartdatahq/embedded-agent-headlessreact (16.8 or newer) is a peer dependency. The only runtime dependencies are zustand and uuid.
Migrating from
@smartdatahq/embedded-agent/headless? That subpath still works and re-exports this package, but it drags the pre-built UI's dependency tree into your install. Switch the import to@smartdatahq/embedded-agent-headlessand drop@smartdatahq/embedded-agentfrom yourpackage.json.
Table of Contents
Headless Agent (Custom UI)
The headless agent gives you full control over the UI while the package handles WebSocket connections, message parsing, streaming, conversation state, and audio.
No CSS import needed:
import { AgentProvider, useAgent } from "@smartdatahq/embedded-agent-headless";Quick Start
Wrap your component tree with AgentProvider and use the useAgent() convenience hook:
import { AgentProvider, useAgent } from "@smartdatahq/embedded-agent-headless";
function ChatUI() {
const {
messages,
sendMessage,
isBotThinking,
isStreaming,
isConnected,
conversationId,
configLoading,
configError,
} = useAgent();
if (configLoading) return <p>Loading agent...</p>;
if (configError) return <p>Failed to load agent configuration.</p>;
return (
<div>
{messages.map((msg, i) => (
<div key={i} className={msg.user === "user" ? "user-msg" : "bot-msg"}>
{msg.message}
</div>
))}
{isBotThinking && <p>Thinking...</p>}
<input
type="text"
onKeyDown={(e) => {
if (e.key === "Enter") {
sendMessage(e.currentTarget.value);
e.currentTarget.value = "";
}
}}
disabled={!isConnected}
/>
</div>
);
}
export default function App() {
return (
<AgentProvider config={{ identifier: "YOUR_IDENTIFIER" }}>
<ChatUI />
</AgentProvider>
);
}Message Format
Bot messages are returned in Markdown format. When building your own UI, use a Markdown renderer (e.g. react-markdown, marked, etc.) to display them properly. Each message also carries a time (its server timestamp, used for replies and feedback) and, on rated bot messages, a feedback field (see Message Feedback):
import ReactMarkdown from "react-markdown";
{messages.map((msg, i) => (
<div key={i}>
{msg.user === "bot" ? (
<ReactMarkdown>{msg.message}</ReactMarkdown>
) : (
<p>{msg.message}</p>
)}
</div>
))}Using Individual Hooks
For more granular control, use the composable hooks instead of useAgent():
import {
useAgentConnection,
useAgentMessages,
useAgentSend,
useAgentConversation,
useAgentAudio,
useAgentConfig,
} from "@smartdatahq/embedded-agent-headless";
function MyChat() {
// Connection state
const { isConnected, reconnect } = useAgentConnection();
// Read-only message state
const { messages, isBotThinking, isStreaming, botActivity } = useAgentMessages();
// Send actions
const { sendMessage, sendReply, sendFeedback, respondToBrowserToolCall } = useAgentSend();
// Conversation lifecycle
const { conversationId, startNewConversation, deleteConversation, downloadChatHistory, getChatUrl } =
useAgentConversation();
// Audio (voice input)
const audio = useAgentAudio();
// Remote agent configuration + loading state
const { agentConfig, configLoading, configError } = useAgentConfig();
// ... build your UI
}Sending Messages
const { sendMessage, sendReply, sendFeedback, sendMessageFeedback } = useAgentSend();
// Send a text message
sendMessage("Hello, how can you help me?");
// Reply to a specific message (by its timestamp)
sendReply("Thanks, that's helpful!", "2025-01-15T10:30:00.000Z");
// Submit the conversation-level feedback survey (stars)
sendFeedback({ rating: 5, comments: "Great experience!" });
// Rate a single bot message (thumbs) — see Message Feedback below
sendMessageFeedback({ timestamp: msg.time, value: "positive" });Message Feedback (Thumbs)
Per-message thumbs up / thumbs down uses the MESSAGE_FEEDBACK protocol. Call sendMessageFeedback() with the bot message's time; the package builds the frame, applies the server caps (10 topics, 500-char comment), and updates the message's feedback field only once the server acknowledges it.
const { messages, sendMessageFeedback, agentConfig } = useAgent();
// Thumbs up
sendMessageFeedback({ timestamp: msg.time, value: "positive" });
// Thumbs down with optional topics and comment
sendMessageFeedback({
timestamp: msg.time,
value: "negative",
topics: ["Wrong source"],
comment: "didn't cite a source",
});
// Clear (e.g. user re-clicks the active thumb)
sendMessageFeedback({ timestamp: msg.time, value: null });Render the thumbs from message.feedback?.value — do not update optimistically. The value is set when the feedback_ack frame lands and is restored automatically from history on reload / reconnect:
{messages.map((msg) => {
const value = msg.feedback?.value; // "positive" | "negative" | undefined
return (
<div key={msg.time}>
<ReactMarkdown>{msg.message}</ReactMarkdown>
{msg.user === "bot" && (
<>
<button
aria-pressed={value === "positive"}
onClick={() =>
sendMessageFeedback({
timestamp: msg.time,
value: value === "positive" ? null : "positive",
})
}
>
👍
</button>
<button
aria-pressed={value === "negative"}
onClick={() =>
value === "negative"
? sendMessageFeedback({ timestamp: msg.time, value: null })
: openThumbsDownModal(msg) // collect topics / comment, then send "negative"
}
>
👎
</button>
</>
)}
</div>
);
})}Topic labels for the thumbs-down UI are per-agent and localized in the agent config as feedback_options (e.g. ["Incomplete", "Wrong source", "Wrong wording", "Didn't understand me", "Other"]). Read them from agentConfig?.feedback_options. The server stores whatever strings you send, but sticking to the configured vocabulary keeps analytics comparable across agents.
State model per message is positive / negative / none. Switching thumbs replaces the previous value (last write wins). sendFeedback() (the star-rating survey) is a separate, conversation-level mechanism.
File Uploads
File uploads work through the same sendMessage function. Pass a file object instead of a string:
const { sendMessage } = useAgentSend();
function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
sendMessage({
file: file,
type: file.type,
name: file.name,
lastModified: file.lastModified,
size: file.size,
});
}
// In your JSX:
<input type="file" onChange={handleFileChange} />The file is read as base64 and sent over the WebSocket automatically.
Connection Management
const { isConnected, reconnect } = useAgentConnection();
// Check connection status
if (!isConnected) {
// Messages sent while disconnected are queued
// and delivered automatically on reconnect
reconnect();
}Conversation Management
const {
conversationId,
startNewConversation,
deleteConversation,
downloadChatHistory,
getChatUrl,
} = useAgentConversation();
// Start fresh
startNewConversation();
// Delete current conversation
deleteConversation();
// Trigger chat history download
downloadChatHistory();
// Get a shareable URL for the current conversation
const url = getChatUrl();Audio / Voice
const audio = useAgentAudio();
if (audio) {
// Voice input is available
const { isVoiceActive, activateVoice, deactivateVoice } = audio;
return (
<button onClick={isVoiceActive ? deactivateVoice : activateVoice}>
{isVoiceActive ? "Disable Voice" : "Enable Voice"}
</button>
);
}Agent Configuration
The remote agent configuration (theme, welcome message, etc.) is fetched automatically by AgentProvider. Access loading state and the config via useAgentConfig():
const { agentConfig, configLoading, configError } = useAgentConfig();
if (configLoading) return <Spinner />;
if (configError) return <p>Failed to load: {configError}</p>;
// agentConfig contains the remote configuration:
// welcome_message, icon, default_language, on_prem, feedback_options, etc.
console.log(agentConfig?.welcome_message);
console.log(agentConfig?.feedback_options); // thumbs-down topic labelsNotifications
By default, internal notifications (e.g. microphone permission errors) are logged to the console. You can provide your own handler:
import { AgentProvider } from "@smartdatahq/embedded-agent-headless";
import type { NotifyFn } from "@smartdatahq/embedded-agent-headless";
const myNotify: NotifyFn = (message, type, options) => {
// type is "success" | "info" | "warning" | "error"
toast[type](message); // e.g. using react-hot-toast, sonner, etc.
};
<AgentProvider config={{ identifier: "YOUR_IDENTIFIER", onNotify: myNotify }}>
<ChatUI />
</AgentProvider>Full Headless Example
A complete custom chat UI using only headless hooks:
import { AgentProvider, useAgent } from "@smartdatahq/embedded-agent-headless";
function CustomChatUI() {
const {
messages,
sendMessage,
isBotThinking,
isStreaming,
botActivity,
isConnected,
reconnect,
conversationId,
startNewConversation,
deleteConversation,
downloadChatHistory,
getChatUrl,
audio,
configLoading,
configError,
} = useAgent();
const inputRef = useRef<HTMLInputElement>(null);
if (configLoading) return <p>Loading...</p>;
if (configError) return <p>Error: {configError}</p>;
const handleSend = () => {
const val = inputRef.current?.value?.trim();
if (!val) return;
sendMessage(val);
inputRef.current!.value = "";
};
return (
<div>
{/* Connection status */}
<div>
{isConnected ? "Connected" : "Disconnected"}
{!isConnected && <button onClick={() => reconnect()}>Reconnect</button>}
</div>
{/* Messages */}
<div style={{ height: 400, overflowY: "auto" }}>
{messages.map((msg, i) => (
<div key={i} style={{ textAlign: msg.user === "user" ? "right" : "left" }}>
<span>{msg.message}</span>
<small>
{msg.user === "user" ? "You" : "Bot"} - {new Date(msg.time).toLocaleTimeString()}
</small>
</div>
))}
{isBotThinking && <p>Thinking...</p>}
{isStreaming && botActivity && <p>{botActivity}</p>}
</div>
{/* Input */}
<input
ref={inputRef}
onKeyDown={(e) => e.key === "Enter" && handleSend()}
disabled={!isConnected}
placeholder="Type a message..."
/>
<button onClick={handleSend} disabled={!isConnected}>Send</button>
{/* Actions */}
<div>
<button onClick={startNewConversation}>New Chat</button>
<button onClick={deleteConversation}>Delete</button>
<button onClick={downloadChatHistory}>Download</button>
<button onClick={() => navigator.clipboard.writeText(getChatUrl())}>Copy URL</button>
{audio && (
<button onClick={audio.isVoiceActive ? audio.deactivateVoice : audio.activateVoice}>
{audio.isVoiceActive ? "Disable Voice" : "Enable Voice"}
</button>
)}
</div>
</div>
);
}
export default function App() {
return (
<AgentProvider config={{ identifier: "YOUR_IDENTIFIER" }}>
<CustomChatUI />
</AgentProvider>
);
}Browser Tools
Overview
Browser tools integration consists of three main components:
- browserToolsRegistration: Define available tools with their schemas
- onBrowserToolCall: Handle when the agent wants to use a tool
- browserToolCallResponse (Embedded) / respondToBrowserToolCall (Headless): Send the tool execution result back to the agent
Browser Tools with Headless Agent
With the headless agent, browser tools are configured via AgentProvider props and handled using useAgentSend():
import { AgentProvider, useAgent, useAgentSend } from "@smartdatahq/embedded-agent-headless";
import type { BrowserToolCallData, AskQuestionArgs, AskQuestionAnswer } from "@smartdatahq/embedded-agent-headless";
function ToolAwareChat() {
const { messages, sendMessage, isBotThinking, isConnected } = useAgent();
const { respondToBrowserToolCall } = useAgentSend();
// The onBrowserToolCall callback is set on AgentProvider config,
// so handle it there. Use respondToBrowserToolCall to send results back.
return (
<div>
{messages.map((msg, i) => (
<div key={i}>{msg.message}</div>
))}
{/* ... your UI */}
</div>
);
}
function App() {
const [shoppingCart, setShoppingCart] = useState([]);
return (
<AgentProvider
config={{
identifier: "YOUR_IDENTIFIER",
browserTools: [
{
scope: "agent",
title: "add_to_cart",
description: "Add a product to the shopping cart",
type: "object",
properties: {
product_sku: { type: "string", description: "Product SKU" },
quantity: { type: "integer", minimum: 1, default: 1 },
},
required: ["product_sku"],
},
],
onBrowserToolCall: (data) => {
if (data.name === "add_to_cart") {
setShoppingCart((prev) => [
...prev,
{ sku: data.args.product_sku, qty: data.args.quantity || 1 },
]);
// Respond to the agent from inside a component using respondToBrowserToolCall,
// or handle it here with any state management approach.
}
},
}}
>
<ToolAwareChat />
</AgentProvider>
);
}Handling Internal Tool Calls (ask_question)
The agent may invoke an internal ask_question tool to present the user with questions and options. If you are using the Embedded Agent (pre-built UI), this is handled automatically — no extra work needed. This section only applies if you are using the Headless Agent and ask_question has been activated for your agent. In headless mode, you handle it via the onBrowserToolCall callback and render your own UI.
You receive all questions upfront, present them however you like, and send one respondToBrowserToolCall() when all answers are collected. The headless engine automatically adds the questions and answers to chat history — you don't need to manage messages yourself.
Tool call data shape:
{
name: "ask_question",
type: "tool_call",
id: "call_abc123", // Use this as the response ID
args: {
questions: [
{
id: "contact_methods",
title: "How can we reach you?",
selectionMode: "multiple", // optional — defaults to "single"
minSelections: 1, // optional — defaults to 1 for multi-select
maxSelections: 2, // optional — defaults to all options
options: [
{ id: "email", label: "Email" },
{ id: "phone", label: "Phone" },
{ id: "sms", label: "SMS" },
],
},
{
id: "best_time",
title: "What time works best for you?",
options: [
{ id: "morning", label: "Morning" },
{ id: "afternoon", label: "Afternoon" },
{ id: "evening", label: "Evening" },
],
},
],
},
}Each question supports:
selectionMode:"single"(default) or"multiple".minSelections/maxSelections: Only apply whenselectionModeis"multiple".
Response shape (always send via respondToBrowserToolCall):
type AskQuestionToolOutput = {
answers: AskQuestionAnswer[];
};
type AskQuestionAnswer = {
questionId: string;
question: string;
selectedOptions: Array<{ id: string; label: string }>;
// Single-select: selectedOptions.length === 1
// Multi-select: selectedOptions.length >= minSelections
};Example:
import { AgentProvider, useAgent, useAgentSend } from "@smartdatahq/embedded-agent-headless";
import type {
BrowserToolCallData,
AskQuestionAnswer,
AskQuestionToolOutput,
} from "@smartdatahq/embedded-agent-headless";
function App() {
const [pendingQuestions, setPendingQuestions] = useState<BrowserToolCallData | null>(null);
return (
<AgentProvider
config={{
identifier: "YOUR_IDENTIFIER",
onBrowserToolCall: (data: BrowserToolCallData) => {
if (data.name === "ask_question") {
// Store questions to render in your UI
setPendingQuestions(data);
}
},
}}
>
<MyChatUI
pendingQuestions={pendingQuestions}
onQuestionsAnswered={() => setPendingQuestions(null)}
/>
</AgentProvider>
);
}
function MyChatUI({ pendingQuestions, onQuestionsAnswered }) {
const { messages } = useAgent();
const { respondToBrowserToolCall } = useAgentSend();
const handleSubmitAnswers = (answers: AskQuestionAnswer[]) => {
const output: AskQuestionToolOutput = { answers };
// Send the response — chat history is updated automatically
respondToBrowserToolCall({
id: pendingQuestions.id,
output,
});
onQuestionsAnswered();
};
return (
<div>
{messages.map((msg, i) => (
<div key={i}>{msg.message}</div>
))}
{pendingQuestions && (
<MyQuestionForm
questions={pendingQuestions.args.questions}
onComplete={handleSubmitAnswers}
/>
)}
</div>
);
}Example MyQuestionForm component:
The component below steps through each question one at a time. Single-select questions submit on click; multi-select questions let the user toggle options and confirm with a Continue button. It calls onComplete with the full answers array once the last question is answered:
import { useEffect, useState } from "react";
import type { AskQuestionAnswer, AskQuestionItem } from "@smartdatahq/embedded-agent-headless";
function MyQuestionForm({
questions,
onComplete,
}: {
questions: AskQuestionItem[];
onComplete: (answers: AskQuestionAnswer[]) => void;
}) {
const [currentIndex, setCurrentIndex] = useState(0);
const [collectedAnswers, setCollectedAnswers] = useState<AskQuestionAnswer[]>([]);
const [selectedIds, setSelectedIds] = useState<Set<string>>(() => new Set());
const current = questions[currentIndex];
const isMultiple = current.selectionMode === "multiple";
const minSelections = current.minSelections ?? 1;
const maxSelections = current.maxSelections ?? current.options.length;
useEffect(() => {
setSelectedIds(new Set());
}, [currentIndex]);
const submitAnswer = (selectedOptions: { id: string; label: string }[]) => {
const answer: AskQuestionAnswer = {
questionId: current.id ?? `question-${currentIndex + 1}`,
question: current.title,
selectedOptions,
};
const updatedAnswers = [...collectedAnswers, answer];
if (currentIndex + 1 < questions.length) {
setCollectedAnswers(updatedAnswers);
setCurrentIndex((i) => i + 1);
return;
}
onComplete(updatedAnswers);
};
const toggleOption = (option: { id: string; label: string }) => {
setSelectedIds((currentIds) => {
const next = new Set(currentIds);
if (next.has(option.id)) {
next.delete(option.id);
return next;
}
if (next.size >= maxSelections) {
return currentIds;
}
next.add(option.id);
return next;
});
};
const submitMultiple = () => {
const selectedOptions = current.options.filter((option) =>
selectedIds.has(option.id),
);
if (selectedOptions.length < minSelections) return;
submitAnswer(selectedOptions);
};
return (
<div style={{ padding: 12, background: "#f5f5f5", borderRadius: 8 }}>
<p style={{ fontWeight: 600, marginBottom: 8 }}>
{current.title}
{questions.length > 1 && (
<span style={{ fontWeight: 400, fontSize: 12, color: "#666", marginLeft: 6 }}>
({currentIndex + 1}/{questions.length})
</span>
)}
</p>
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
{current.options.map((opt) => (
<button
key={opt.id}
onClick={() =>
isMultiple ? toggleOption(opt) : submitAnswer([opt])
}
style={{
padding: "6px 12px",
borderRadius: 6,
border: `1px solid ${selectedIds.has(opt.id) ? "#15803d" : "#007bff"}`,
background: selectedIds.has(opt.id) ? "#ecfdf3" : "white",
color: selectedIds.has(opt.id) ? "#15803d" : "#007bff",
cursor: "pointer",
textAlign: "left",
}}
>
{opt.label}
</button>
))}
</div>
{isMultiple && (
<button
onClick={submitMultiple}
disabled={selectedIds.size < minSelections}
style={{ marginTop: 8, padding: "6px 12px" }}
>
Continue
</button>
)}
</div>
);
}Note: If you are using the Embedded Agent (pre-built UI),
ask_questionis rendered automatically — you do not need to implement any of the above.
Tool Schema Format
Browser tools use JSON Schema format for defining parameters. Each tool should include:
- title: Unique identifier for the tool
- scope: Either "agent" or "conversation". This registers the tool for either the current conversation or globally for the agent.
- description: Clear description of what the tool does and when to call it. Describe under what conditions the AI should use the tool and explain the goal of calling the tool. For example, if it's for adding a product to the cart, then explain that it should be called once the user has confirmed that he wishes to buy a product.
- type: Should be "object" for complex tools
- properties: Object defining all parameters
- required: Array of required parameter names
Parameter Types
You can use various JSON Schema types and constraints:
// String with enum constraints
{
type: "string",
enum: ["option1", "option2", "option3"],
description: "Choose from predefined options"
}
// Number with range constraints
{
type: "number",
minimum: 0,
maximum: 100,
description: "Value between 0 and 100"
}
// Complex nested objects
{
type: "object",
properties: {
nested_field: {
type: "string",
description: "Nested parameter"
}
},
required: ["nested_field"]
}
// Arrays with item constraints
{
type: "array",
items: {
type: "string",
enum: ["item1", "item2"]
},
description: "Array of predefined items"
}Error Handling
Always include error handling in your tool call handler:
const handleBrowserToolCall = (data) => {
try {
// Your tool logic here
setToolResponse({
id: data.id,
output: { success: true, result: "..." },
});
} catch (error) {
setToolResponse({
id: data.id,
output: {
success: false,
error: error.message,
},
});
}
};Best Practices
- Clear Descriptions: Provide clear, detailed descriptions for tools and parameters
- Validation: Use JSON Schema constraints to validate inputs
- Error Handling: Always handle errors gracefully
- Response Format: Maintain consistent response format with success/error indicators
- Async Operations: Handle asynchronous operations properly
- State Management: Update your application state based on tool results
Checkout Integration (Headless)
When the agent decides the user is ready to pay, it emits a checkout_initiated signal carrying an Adyen session payload. The Embedded Agent renders the payment UI for you — no extra wiring needed. With the Headless Agent you own the UI, so you receive the signal via the onCheckoutInitiated callback and ship the outcome back via respondToCheckout from useAgentSend().
Checkout Flow
- Signal in:
onCheckoutInitiated(config)fires onAgentProvider. Theconfigcontains the AdyenclientKey,sessionId,sessionData, andenvironment. - Render: Mount your payment UI (Adyen Drop-in or any compatible flow) using that config.
- Signal out: When the payment finishes, call
respondToCheckout({ status: "completed" | "failed", resultCode })so the agent can continue the conversation accordingly.
import type { CheckoutConfig, CheckoutResult } from "@smartdatahq/embedded-agent-headless";
// CheckoutConfig — what you receive
{
clientKey: string;
sessionId: string;
sessionData: string;
environment: "test" | "live" | "live-us" | "live-au" | "live-apse" | "live-in" | (string & {});
}
// CheckoutResult — what you send back
{ status: "completed"; resultCode: string } | { status: "failed"; resultCode: string }Adyen Drop-in Example
import { useEffect, useRef, useState } from "react";
import {
AgentProvider,
useAgent,
useAgentSend,
} from "@smartdatahq/embedded-agent-headless";
import type { CheckoutConfig } from "@smartdatahq/embedded-agent-headless";
import { AdyenCheckout, Dropin, Card } from "@adyen/adyen-web";
import "@adyen/adyen-web/dist/es/adyen.css";
function CheckoutSurface({ config }: { config: CheckoutConfig }) {
const containerRef = useRef<HTMLDivElement>(null);
const { respondToCheckout } = useAgentSend();
useEffect(() => {
if (!containerRef.current) return;
let dropin: InstanceType<typeof Dropin> | null = null;
(async () => {
const checkout = await AdyenCheckout({
environment: config.environment,
clientKey: config.clientKey,
session: { id: config.sessionId, sessionData: config.sessionData },
onPaymentCompleted: (result) => {
respondToCheckout({ status: "completed", resultCode: result.resultCode });
},
onPaymentFailed: (result) => {
respondToCheckout({ status: "failed", resultCode: result?.resultCode ?? "ERROR" });
},
});
dropin = new Dropin(checkout, { paymentMethodComponents: [Card] }).mount(
containerRef.current!,
);
})();
return () => {
dropin?.unmount();
};
}, [config, respondToCheckout]);
return <div ref={containerRef} />;
}
function ChatWithCheckout({
checkoutConfig,
onClose,
}: {
checkoutConfig: CheckoutConfig | null;
onClose: () => void;
}) {
const { messages } = useAgent();
return (
<>
{messages.map((msg, i) => (
<div key={i}>{msg.message}</div>
))}
{checkoutConfig && (
<Modal onClose={onClose}>
<CheckoutSurface config={checkoutConfig} />
</Modal>
)}
</>
);
}
function App() {
const [checkoutConfig, setCheckoutConfig] = useState<CheckoutConfig | null>(null);
return (
<AgentProvider
config={{
identifier: "YOUR_IDENTIFIER",
onCheckoutInitiated: (config) => setCheckoutConfig(config),
}}
>
<ChatWithCheckout
checkoutConfig={checkoutConfig}
onClose={() => setCheckoutConfig(null)}
/>
</AgentProvider>
);
}Note:
respondToCheckoutmust be called from inside an<AgentProvider>subtree (it's exposed viauseAgentSend()). If your payment surface lives outside the provider, lift theonCheckoutInitiatedpayload into shared state and render the payment component as a child ofAgentProvider.
API Reference
AgentProvider Props (AgentConfig)
These props are passed via the config prop on <AgentProvider>:
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| identifier | string | Yes | The identifier of the agent. Required to connect to the server. |
| conversationId | string | No | Optional conversation ID. Auto-generated UUID if omitted. |
| language | string | No | Language code (ISO 639-1 with country, e.g. "en-US"). |
| browserTools | Array<{ scope: "agent" \| "conversation"; ... }> | No | Browser tools to register with the agent. |
| onError | (error: string) => void | No | Called when an error occurs during agent processing. |
| onBrowserToolCall | (data: BrowserToolCallData) => void | No | Called when a browser tool call is made by the agent. Also receives internal browser tools like ask_question in headless mode — see Handling Internal Tool Calls. |
| onToolCall | (data: { toolName: string; conversationId: string }) => void | No | Called when an internal tool is invoked. Useful for analytics. |
| onUserMessage | (data: { conversationId: string }) => void | No | Called when a user message is sent. Useful for analytics. |
| onCheckoutInitiated | (config: CheckoutConfig) => void | No | Called when the agent initiates a checkout flow. Render your own payment UI and reply via respondToCheckout — see Checkout Integration. |
| onResponseStreamStart | () => void | No | Called when the assistant starts streaming a response. |
| onResponseStreamed | () => void | No | Called when the assistant finishes streaming a response. |
| onNewConversation | () => void | No | Called when a new conversation is started. |
| onNotify | NotifyFn | No | Custom notification handler. Falls back to console logging. |
| shouldConnect | boolean | No | Controls whether the agent opens its WebSocket connection. Defaults to true. Set to false to defer connecting (e.g. while gating on auth, feature flags, or a user gesture); flip back to true to connect. Messages sent while disconnected are queued and flushed on connect. |
| useFraios | boolean | No | Routes requests to the Fraios-hosted agent: the isFraios flag is forwarded on agent-config, WebSocket, and file-upload requests. Defaults to true. Set to false when the agent is not hosted in Fraios. |
Headless Hooks
| Hook | Returns | Description |
|------|---------|-------------|
| useAgent() | All fields below combined | Convenience hook composing all primitives. |
| useAgentConnection() | { isConnected, reconnect } | WebSocket connection state and reconnect action. |
| useAgentMessages() | { messages, isBotThinking, isStreaming, botActivity } | Read-only message and streaming state. |
| useAgentSend() | { sendMessage, sendReply, sendFeedback, sendMessageFeedback, respondToBrowserToolCall, respondToCheckout } | Actions for sending messages, replies, the survey, per-message thumbs feedback, tool responses, and checkout outcomes. |
| useAgentConversation() | { conversationId, startNewConversation, deleteConversation, downloadChatHistory, getChatUrl } | Conversation lifecycle management. |
| useAgentAudio() | { isVoiceActive, activateVoice, deactivateVoice } \| null | Audio/voice input controls. null if unavailable. |
| useAgentConfig() | { agentConfig, configLoading, configError } | Remote agent configuration and loading state. |
