npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-headless

react (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-headless and drop @smartdatahq/embedded-agent from your package.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 labels

Notifications

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:

  1. browserToolsRegistration: Define available tools with their schemas
  2. onBrowserToolCall: Handle when the agent wants to use a tool
  3. 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 when selectionMode is "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_question is 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

  1. Clear Descriptions: Provide clear, detailed descriptions for tools and parameters
  2. Validation: Use JSON Schema constraints to validate inputs
  3. Error Handling: Always handle errors gracefully
  4. Response Format: Maintain consistent response format with success/error indicators
  5. Async Operations: Handle asynchronous operations properly
  6. 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

  1. Signal in: onCheckoutInitiated(config) fires on AgentProvider. The config contains the Adyen clientKey, sessionId, sessionData, and environment.
  2. Render: Mount your payment UI (Adyen Drop-in or any compatible flow) using that config.
  3. 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: respondToCheckout must be called from inside an <AgentProvider> subtree (it's exposed via useAgentSend()). If your payment surface lives outside the provider, lift the onCheckoutInitiated payload into shared state and render the payment component as a child of AgentProvider.


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