> ## Documentation Index
> Fetch the complete documentation index at: https://developers.firmly.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# SDK Reference

> Every export from @firmly/ask-agent-sdk, plus the GET /api/embed/capabilities endpoint

<Info>
  This page documents the exact public surface of `@firmly/ask-agent-sdk` — every symbol it exports
  and the one backend endpoint (`/api/embed/capabilities`) the SDK's `fetchAskAgentConfig` calls.
  Nothing here is illustrative; every name, parameter, and field matches the source.
</Info>

## Capabilities

### `defineCapabilities({ components })`

Declares which UI components/capabilities your host can render. Resolves the declared list down to
the concrete tool names that ride on every chat request.

<ParamField body="components" type="string[]" required>
  Capability keys your host supports (e.g. `'product-card'`, `'followups'`, `'checkout'`,
  `'data-hooks'`). Throws if `components` isn't an array, or if any key isn't a recognized capability.
</ParamField>

**Returns** a frozen object:

<ResponseField name="components" type="string[]">
  Copy of the capability keys you declared.
</ResponseField>

<ResponseField name="supportedTools" type="string[]">
  Tool names resolved from `components` — this is what you pass as `supportedTools` to
  `createAskAgentTransport`.
</ResponseField>

### `allCapabilities()`

Returns every capability key a host may declare, as a fresh array. Useful for docs or a "support
everything" declaration.

**Returns:** `string[]`

## Transport

### `createAskAgentTransport(options)`

Returns an AI SDK v6 `ChatTransport` that posts to the Ask Agent chat endpoint and injects
`pageContext`, `shopperContext`, and `supportedTools` into the request body on every turn. Drop it
straight into `@ai-sdk/svelte`'s `Chat` or React's `useChat`.

<ParamField body="api" type="string" default="/api/chat">
  Chat endpoint, origin-relative on the merchant's own site.
</ParamField>

<ParamField body="merchant" type="string">
  Merchant domain; appended to `api` as `?m=` so the backend resolves scope.
</ParamField>

<ParamField body="supportedTools" type="string[]" default="[]">
  Resolved from `defineCapabilities(...).supportedTools`.
</ParamField>

<ParamField body="getPageContext" type="() => object | Promise<object>">
  Returns the current page payload, called fresh on every turn. Omitted from the request body if not
  provided.
</ParamField>

<ParamField body="getShopperContext" type="() => object | Promise<object>">
  Returns `{ user, orders }` (or your own shopper-shaped payload), called fresh on every turn.
  Omitted from the request body if not provided.
</ParamField>

<ParamField body="credentials" type="RequestCredentials" default="include">
  Sent with every request so the first-party session cookie/JWT rides along.
</ParamField>

<ParamField body="headers" type="HeadersInit">
  Extra headers merged into every chat request.
</ParamField>

<ParamField body="fetch" type="typeof fetch">
  Custom fetch implementation — e.g. one that attaches a bearer token.
</ParamField>

<Note>
  The `ai` package is an **optional peer dependency**, lazily imported (`import('ai')`) the first
  time the returned transport actually sends a message — not when the factory is called or the
  module is imported. This lets a host that only needs `defineCapabilities`/`createToolDispatcher`
  import the SDK without installing `ai` at all.
</Note>

**Returns** a `ChatTransport`-shaped object: `{ prepareSendMessagesRequest, sendMessages, reconnectToStream }`.

## Tool dispatcher

### `createToolDispatcher({ handlers })`

Builds the function you wire into your chat hook's `onToolCall`. Routes each incoming tool call to
the right handler based on the tool's kind in the catalog.

<ParamField body="handlers.runDataHook" type="(ctx: { name: string, input: any }) => any | Promise<any>">
  Executes a merchant data-hook tool (`is_user_logged_in`, `get_orders`, `get_order_details`,
  `get_loyalty_points`, or a merchant's `custom_hook_*`) and returns its JSON-serializable result.
</ParamField>

<ParamField body="handlers.onCheckout" type="(input: any) => void">
  Called when the agent triggers `proceed_to_checkout`. The server tool already resolved, so this
  handler does **not** call `addToolResult`.
</ParamField>

<ParamField body="handlers.onBuyNow" type="(input: any) => any | Promise<any>">
  Resolves a `buy_now` call — variant lookup plus the cart/checkout handoff — on the client, and
  returns output fed back to the model.
</ParamField>

**Returns** `dispatch({ toolCall, addToolResult })`, an async function:

<ParamField body="toolCall" type="object" required>
  The tool call from your chat hook's `onToolCall` (AI SDK v6 shape — static or dynamic tool).
</ParamField>

<ParamField body="addToolResult" type="(r: { tool?: string, toolCallId: string, output: any }) => any" required>
  Your chat instance's `addToolResult`, used to feed a handler's output back to resume the turn.
</ParamField>

**Routing:**

* `proceed_to_checkout` → calls `handlers.onCheckout`, then returns (no `addToolResult`).
* `buy_now` → calls `handlers.onBuyNow` and feeds its output back via `addToolResult`. If
  `handlers.onBuyNow` isn't provided, dispatch calls `addToolResult` with
  `{ error: 'buy_now_unsupported' }`.
* Any data-hook tool (`isHookTool(name)` is true) → calls `handlers.runDataHook` and feeds its output
  back via `addToolResult`. If `handlers.runDataHook` isn't provided, dispatch calls `addToolResult`
  with `{ error: 'data_hook_unsupported' }`.
* All other UI tools (`recommend_add_to_cart`, `show_product_picks`, `scroll_to_section`,
  `suggest_followups`) → no-op; the server tool already returned and the component renders from the
  streamed part.

<Warning>
  If a handler throws, dispatch catches it and calls `addToolResult` with `{ error: String(err.message ?? err) }`
  rather than propagating — a broken hook implementation degrades to an error result, it doesn't
  crash the chat.
</Warning>

### `shouldResumeAfterHook(messages)`

Predicate for your chat hook's `sendAutomaticallyWhen`: resumes the agent turn once every data-hook
tool call in the latest assistant message has a result. This is the **hooks-only baseline** — hosts
that also intercept custom UI components or `buy_now` outside the dispatcher may need a richer
predicate.

<ParamField body="messages" type="Array<{ role?: string, parts?: any[] }>" required>
  The chat's current message list.
</ParamField>

**Returns:** `boolean` — `true` only if the last message is from the assistant, contains at least one
data-hook tool-call part, and every such part already has a result.

## Config

### `fetchAskAgentConfig({ merchant, api?, fetch? })`

Fetches the merchant's effective Ask Agent configuration from the backend.

<ParamField body="merchant" type="string" required>
  Merchant domain. Throws if omitted.
</ParamField>

<ParamField body="api" type="string" default="/api/embed/capabilities">
  Capabilities endpoint path.
</ParamField>

<ParamField body="fetch" type="typeof fetch">
  Custom fetch implementation. Defaults to `globalThis.fetch`; throws if neither is available.
</ParamField>

Sends the request with `credentials: 'include'` and `Accept: application/json`. Throws if the
response isn't `ok`.

**Returns** a `Promise` resolving to the config object — see [Capabilities endpoint](#capabilities-endpoint)
below for the exact field list, which this function returns verbatim.

## Session

### `createSession({ merchant, serverOrigin?, storage?, fetch?, now? })`

First-party browser-session bootstrap. Mints/refreshes the Firmly browser-session JWT by POSTing to
the merchant's own origin, so the shopper's existing first-party session cookie rides along — no API
keys in the browser.

<ParamField body="merchant" type="string" required>
  Merchant domain, used for the `?m=` scope. Throws if omitted.
</ParamField>

<ParamField body="serverOrigin" type="string" default="''">
  Origin of the Ask Agent backend. Empty string means same-origin.
</ParamField>

<ParamField body="storage" type="Storage | null">
  Defaults to `globalThis.localStorage` (falls back to `null` if unreachable, e.g. SSR). Pass your
  own for testing or a non-browser storage strategy.
</ParamField>

<ParamField body="fetch" type="typeof fetch">
  Defaults to `globalThis.fetch`.
</ParamField>

<ParamField body="now" type="() => number">
  Epoch-seconds clock, injectable for tests. Defaults to `Math.floor(Date.now() / 1000)`.
</ParamField>

**Returns** `{ ensureSession, getSession }`:

<ResponseField name="ensureSession" type="() => Promise<object>">
  Reads the stored session; if it's missing, expired, or expiring within 300 seconds, mints/refreshes
  it (coalescing concurrent calls into one in-flight request) and persists the result to storage. On
  mint failure, warns and falls back to returning the current (possibly stale) session.
</ResponseField>

<ResponseField name="getSession" type="() => { accessToken: string | null, deviceId: string | null, expiresAt: number | null }">
  Read-only synchronous snapshot of the current stored session, without triggering a refresh.
</ResponseField>

## Catalog helpers

Low-level helpers backing `defineCapabilities`/the dispatcher — reach for these if you need the raw
tool catalog directly.

<ResponseField name="isHiddenTool(name)" type="(name: string) => boolean">
  Is this tool `kind: 'hidden'` — server/metadata only, never rendered?
</ResponseField>

<ResponseField name="isComponentTool(name)" type="(name: string) => boolean">
  Is this a merchant "custom component" tool (name starts with `custom_component_`)?
</ResponseField>

<ResponseField name="isHookTool(name)" type="(name: string) => boolean">
  Is this a client-executing merchant data-hook — one of `STANDARD_HOOK_TOOLS`, a `custom_hook_*`
  name, or catalog `kind: 'client-executing'`?
</ResponseField>

<ResponseField name="getTool(name)" type="(name: string) => ToolDescriptor | undefined">
  Look up a tool's full catalog descriptor by name.
</ResponseField>

<ResponseField name="toolsForCapabilities(capabilities)" type="(capabilities: string[]) => string[]">
  Resolve capability keys to their backing tool names (sorted, de-duplicated). Throws on any unknown
  capability key.
</ResponseField>

<ResponseField name="allCapabilities()" type="() => string[]">
  Same function re-exported from `capabilities.js` — every capability key a host may declare.
</ResponseField>

**Constants:**

<ResponseField name="TOOL_CATALOG" type="ToolDescriptor[]">
  The full tool catalog — every tool's `name`, `kind`, `capability`, `component`, `description`,
  `input`, `hostHandlers`, and `terminator` metadata.
</ResponseField>

<ResponseField name="STANDARD_HOOK_TOOLS" type="string[]">
  `['is_user_logged_in', 'get_orders', 'get_order_details', 'get_loyalty_points']` — the standard
  merchant data-hook tool names.
</ResponseField>

<ResponseField name="DATA_HOOKS_CAPABILITY" type="string">
  `'data-hooks'` — the capability key that unlocks the standard data-hook tools.
</ResponseField>

<ResponseField name="ALL_CAPABILITIES" type="string[]">
  Every host-declarable capability key, including `data-hooks`.
</ResponseField>

<ResponseField name="ALWAYS_ON_TOOLS" type="string[]">
  Tool names never gated by the host allowlist — every `progress` and `hidden` tool in the catalog.
</ResponseField>

***

## Capabilities endpoint

`GET /api/embed/capabilities`

Returns the merchant's effective Ask Agent capability set — the same endpoint `fetchAskAgentConfig`
calls. Client-safe only.

<ParamField query="m" type="string" required>
  Merchant domain (leading `www.` is stripped server-side).
</ParamField>

### Response — enabled

<ResponseField name="merchant" type="string">
  The merchant domain echoed back.
</ResponseField>

<ResponseField name="enabled" type="boolean">
  `true` when Ask Agent is configured and enabled for this merchant.
</ResponseField>

<ResponseField name="enabledComponents" type="string[]">
  Capability keys this merchant has turned on. `followups` is always included; `product-card`,
  `product-carousel`, and `buy-now` depend on `show_product_card`; `scroll-to-section` depends on
  `enable_page_navigation`; `checkout` depends on `checkout_mode !== 'merchant'`; `data-hooks` is
  included only if the merchant has at least one enabled hook.
</ResponseField>

<ResponseField name="enabledTools" type="string[]">
  Concrete tool names backing `enabledComponents`, plus this merchant's actual data-hook tool names
  (standard hooks and/or `custom_hook_*`).
</ResponseField>

<ResponseField name="enabledHooks" type="string[]">
  Names of the merchant's enabled data-hook tools (standard and/or `custom_hook_*`).
</ResponseField>

<ResponseField name="accentColor" type="string | null">
  The merchant's configured accent color, or `null` if unset.
</ResponseField>

<ResponseField name="welcomeMessage" type="string | null">
  The merchant's configured welcome message, or `null` if unset.
</ResponseField>

<ResponseField name="checkoutMode" type="string">
  `'firmly'` or `'merchant'` — mirrors whether `checkout` is in `enabledComponents`.
</ResponseField>

<ResponseField name="voiceEnabled" type="boolean">
  Whether the merchant has voice turned on.
</ResponseField>

<ResponseExample>
  ```json 200 enabled theme={null}
  {
    "merchant": "your-store.com",
    "enabled": true,
    "enabledComponents": ["followups", "product-card", "product-carousel", "buy-now", "checkout"],
    "enabledTools": [
      "buy_now",
      "proceed_to_checkout",
      "recommend_add_to_cart",
      "show_product_picks",
      "suggest_followups"
    ],
    "enabledHooks": [],
    "accentColor": "#1a1a1a",
    "welcomeMessage": "Hi! How can I help you shop today?",
    "checkoutMode": "firmly",
    "voiceEnabled": false
  }
  ```
</ResponseExample>

<Accordion title="200 — Ask Agent disabled for this merchant">
  When the merchant's config exists but Ask Agent isn't enabled, the endpoint still returns `200`
  with a minimal body — no component/tool/presentation fields:

  ```json theme={null}
  { "merchant": "your-store.com", "enabled": false }
  ```
</Accordion>

<Accordion title="400 — missing_merchant">
  Returned when the `m` query parameter is missing:

  ```json theme={null}
  { "error": "missing_merchant" }
  ```
</Accordion>

<Warning>
  **Never returned (server-only):** the system prompt, custom-instruction text, and data-hook snippet
  code. This endpoint mirrors the same gating the chat route applies when building the tool list, so
  the capability set a host sees always matches what the agent will actually be given — but it never
  leaks the implementation behind a hook or the merchant's prompt customization.
</Warning>
