# Introduction Source: https://docs.scenaro.io/introduction/overview/ Scenaro combines a scenario authoring cockpit, a Python voice agent, a public API, and the `@scenaro/sdk` npm package — everything you need to ship a production voice experience on the web. ## Platform map ```mermaid flowchart LR Cockpit[Cockpit] --> API[Platform API] API --> Agent[Voice Agent] SDK["@scenaro/sdk"] --> Transport["Transport session"] Agent --> Transport App[Your Web App] --> SDK ``` | Component | Role | |-----------|------| | **Cockpit** | Author scenarios, configure features, publish to production | | **Platform API** | Identity, sessions, scenario metadata, collections | | **Voice Agent** | Python agent joining the Transport session — STT, LLM, TTS, tool calls | | **`@scenaro/sdk`** | Protocol, API client, React providers and hooks | | **Transport** | Realtime engine carrying audio + data channel — [LiveKit](https://livekit.io) today, see [Transport](/introduction/transport) | ## Three primitives Create and publish scenarios in Cockpit. The API issues JWT identity tokens and Transport session credentials. The Python agent joins the Transport session, runs the conversation, and calls tools — some fulfilled on the frontend via RPC. Headless TypeScript SDK: protocol types, HTTP client, React bindings. Business features stay in your app. ## Why @scenaro/sdk Before unification, each experience vendored its own copy of `scenaro-sdk` with diverging auth and storage behavior. `@scenaro/sdk` is now the **single source of truth** for: 1. **Protocol** — Transport data-channel topics, message formats, RPC contracts 2. **API client** — identify, session, token storage 3. **React bindings** — providers, hooks, feature runtime Business features (product search, cart, comparison) remain **outside the package**. Each app injects its own feature registry. `@scenaro/sdk` talks to an internal `Transport` interface rather than any realtime engine directly — LiveKit is the current implementation. See [Transport](/introduction/transport) for what that means for your code. ## Package structure ``` @scenaro/sdk ├── protocol # Agent ↔ frontend contract (zero dependencies) ├── client # ScenaroClient, JWT, session storage ├── core # ScenaroSession — headless, framework-agnostic └── react # ScenaroProvider, useScenaroSession, FeatureProvider ``` | Import | Contents | Dependencies | |--------|----------|--------------| | `@scenaro/sdk` | `ScenaroSession`, client, protocol (framework-agnostic) | livekit-client (peer) | | `@scenaro/sdk/protocol` | Topics, message types, RPC | none | | `@scenaro/sdk/client` | `createScenaroClient`, auth, storage | none (native fetch) | | `@scenaro/sdk/react` | `ScenaroProvider`, `useScenaroSession`, `FeatureProvider` | react, livekit-client, @livekit/components-react (peers) | `livekit-client` and `@livekit/components-react` are **peer dependencies** because LiveKit is the current [Transport](/introduction/transport) implementation. The SDK does not bundle LiveKit — it builds on top of it. Backend scripts can use `client` + `protocol` without React or a Transport implementation at all. ## Install For the embed path — one provider, one hook — you only need the SDK itself: ```bash npm npm install @scenaro/sdk ``` ```bash pnpm pnpm add @scenaro/sdk ``` ```bash yarn yarn add @scenaro/sdk ``` Current version: **`@scenaro/sdk@0.3.0`** Reaching for the current engine's own components directly — custom visualizers, avatars, multi-participant UI? Install the LiveKit peer dependencies as described in [Advanced: Transport extensions](/guides/advanced-transport-integration). The quickstart below does not need them. ## Next steps Full developer journey — install to production Embed voice on your site in 15 minutes Connect docs to Cursor, Claude, and ChatGPT Understand the identify → session flow Data-channel topics and message contracts --- # Implement an experience Source: https://docs.scenaro.io/introduction/implement-an-experience/ This page is the **developer journey map** for building a voice experience with `@scenaro/sdk`. It follows the same structure as Retell, Vapi, and ElevenLabs docs: install → connect → listen → handle tools → ship. Already have a working quickstart? Use this page as a checklist and jump to the sections you need. ## What you are building ```mermaid flowchart LR App[Your React app] --> SDK["@scenaro/sdk/react"] SDK --> API[Platform API] SDK --> Transport["Transport session"] Agent[Voice agent] --> Transport Cockpit[Cockpit scenario] --> Agent ``` | Layer | Your responsibility | Provided by Scenaro | |-------|---------------------|---------------------| | **Scenario** | Choose publication UUID | Cockpit authoring + agent | | **Session** | Call `start()` / `end()` | Identify, JWT, Transport credentials | | **Transport** | Usually nothing | SDK connects the session + data channel — see [Transport](/introduction/transport) | | **Tools** | Implement frontend handlers | Agent calls tools via RPC | | **UI features** | Registry + components | `FeatureProvider` or local runtime | | **Brand** | CSS, layout, copy | — | ## Prerequisites - A **published scenario UUID** from Cockpit - Node.js 18+ and React 18+ (for the React path) - Microphone access in the browser (for voice mode) ## Step 1 — Install ```bash npm npm install @scenaro/sdk ``` ```bash pnpm pnpm add @scenaro/sdk ``` Current version: **`@scenaro/sdk@0.3.0`** This is all you need for the embed path below. Only install `livekit-client` and `@livekit/components-react` if you reach [Step 7](#step-7-advanced-transport-extensions-optional). ## Step 2 — Choose your integration path `ScenaroProvider` + `useScenaroSession`. Used by all Scenaro experience apps and platform-spa. Start here unless you have a strong reason not to. `ScenaroSession` from `@scenaro/sdk`. Same lifecycle and events, no React bindings. See [ScenaroSession reference](/sdk-reference/session). ### Provider stack (React) ```tsx import { ScenaroProvider, FeatureProvider } from '@scenaro/sdk/react'; export function ScenaroLiveProviders({ children, registry }) { return ( {children} ); } ``` `FeatureProvider` is optional — skip it if you only need session tools (see [Tools and features](/build/tools-and-features)). ## Step 3 — Start a session The SDK handles the full connect flow: identify visitor → start session → connect Transport → config handshake. ```tsx import { useScenaroSession } from '@scenaro/sdk/react'; const PUBLICATION_UUID = 'your-publication-uuid'; export function VoiceControls() { const { status, // 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'ended' agentState, // 'idle' | 'listening' | 'thinking' | 'speaking' start, end, sendText, setMicrophoneEnabled, } = useScenaroSession({ scenario: PUBLICATION_UUID, language: 'en-US', // optional — omit to use scenario default onError: (e) => console.error(e.code), }); return ( <> ); } ``` Auth is **two-hop**: `identifyUser` then `startSession`. You never call these directly in a typical embed — `start()` runs both. See [Authentication](/build/authentication). ## Step 4 — Listen to events Like Retell's `call_started` / Vapi's `call-start` / ElevenLabs' `onConnect`, Scenaro exposes session state through the hook and typed callbacks: | What you need | How | |---------------|-----| | Connection state | `status`, `agentState` | | Transcript / messages | `messages` array + `onMessage` | | Session config | `config` (curated) or `rawConfig` (wire format) | | Errors | `error` + `onError` | | Session ended | `onSessionEnd` | | Cross-component events | `useScenaroEvents()` event bus | ```tsx const { messages, config } = useScenaroSession({ scenario: PUBLICATION_UUID, onMessage: (m) => appendToUI(m), onSessionEnd: (info) => showSummary(info.reason), }); ``` See [Events and listeners](/build/events-and-listener) for the full event catalog. ## Step 5 — Handle frontend tools When the voice agent calls a tool configured for frontend execution, the SDK routes the RPC to your handler and returns the result to the agent. You have **two integration patterns** — pick one per tool: | Pattern | Best for | Doc | |---------|----------|-----| | **Session tools** | Headless logic, no UI panel | `useScenaroSession({ tools })` | | **Feature tools** | Tool opens a UI panel (search, cart…) | `FeatureProvider` registry | ```tsx // Session tool — plain function, SDK handles RPC useScenaroSession({ scenario: PUBLICATION_UUID, tools: { search_products: async (args, context) => { return searchCatalog(args, { signal: context.signal }); }, }, }); ``` Read [Tools and features](/build/tools-and-features) before wiring handlers — the two patterns use **different handler signatures**. ## Step 6 — Build UI features (optional) For rich experiences (product search, comparison, shopping list), register feature components: 1. Define handlers + a React component per feature type 2. Register in a `Map` 3. Mount `FeatureProvider` with that registry See [Feature registry](/build/feature-registry) and the [custom feature recipe](/guides/recipes/custom-feature). Most Scenaro experience apps keep a **local `FeatureManager`** instead of SDK `FeatureProvider`. Both patterns work on top of `useScenaroSession`. See [Tools and features](/build/tools-and-features#local-featuremanager-vs-sdk-featureprovider). ## Step 7 — Advanced: Transport extensions (optional) Most embeds never touch the underlying engine directly. When you need custom audio visualizers, avatars, or participant UI, use `room` from the hook — the current Transport implementation's engine handle: ```tsx const { room } = useScenaroSession({ scenario: PUBLICATION_UUID }); // room: LiveKit Room | null (today) — use @livekit/components-react hooks ``` See [Advanced: Transport extensions](/guides/advanced-transport-integration) and [Transport](/introduction/transport) for the portable-vs-engine-extension distinction. ## Step 8 — Ship to production Before going live: - [ ] Env vars: `NEXT_PUBLIC_API_URL` (or equivalent) points to production API - [ ] Scenario published in Cockpit production environment - [ ] HTTPS + microphone permission UX - [ ] Error handling for `IDENTIFY_FAILED`, `SESSION_START_FAILED`, `MIC_PERMISSION_DENIED` - [ ] Session cleanup on page unload (`end()`) Full checklist: [Production checklist](/guides/production-checklist). ## Recommended reading order [15-minute embed](/introduction/quickstart) — minimal working example [Key concepts](/introduction/concepts) — scenario, auth, protocol [Provider stack](/build/react-integration) — props, hooks, Next.js [Tools and features](/build/tools-and-features) — choose the right pattern Pick a track: [embed](/guides/recipes/embed-voice-website) · [e-commerce](/guides/recipes/ecommerce-embed) · [multi-language](/guides/recipes/multi-language) [SDK reference](/sdk-reference/react) · [Protocol](/voice-agent/protocol/topics) ## Compare with other platforms | Theme | Retell | Vapi | ElevenLabs | Scenaro | |-------|--------|------|------------|---------| | Web SDK install | `retell-client-js-sdk` | `@vapi-ai/web` | `@elevenlabs/react` | `@scenaro/sdk/react` | | Start call | `startCall({ accessToken })` | `start(assistantId)` | `startSession({ agentId })` | `start()` via `useScenaroSession` | | Auth | Server mints token | Public key + assistant ID | Signed URL / token | Two-hop identify + session (automatic) | | Events | `call_started`, `update` | `call-start`, `message` | `onConnect`, `onMessage` | `status`, `agentState`, `messages` | | Client tools | — | `tool-calls` (one-way) | `clientTools` | Session tools + FeatureProvider (two-way RPC) | | Transport | WebRTC audio | WebRTC | WebRTC / WebSocket | Abstracted (LiveKit today) — see [Transport](/introduction/transport) | Scenaro's differentiator: **scenario-driven features** authored in Cockpit, with a typed protocol between agent and frontend carried over Transport. --- # Quickstart Source: https://docs.scenaro.io/introduction/quickstart/ Get a live voice session running on your site in about 15 minutes. This quickstart covers the embed path — a single provider, a single hook, no transport concepts to learn. Building an immersive, highly customized experience (custom audio visualizers, avatars, multi-participant UI)? See [Advanced: Transport extensions](/guides/advanced-transport-integration) once you've got the basics running here. ## Prerequisites - A published scenario UUID from Cockpit - Node.js 18+ and a React 18+ app - Microphone access in the browser ## Step 1 — Install the SDK ```bash npm npm install @scenaro/sdk ``` ```bash pnpm pnpm add @scenaro/sdk ``` ## Step 2 — Wrap your app ```tsx import { ScenaroProvider } from '@scenaro/sdk/react'; export function AppProviders({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` If your API base URL already includes `/v1` (e.g. `NEXT_PUBLIC_API_URL=https://api.scenaro.io/v1`), pass `apiPathPrefix=""` to `ScenaroProvider` to avoid double-prefixing paths. ## Step 3 — Start a session ```tsx import { useScenaroSession } from '@scenaro/sdk/react'; const SCENARIO_UUID = 'your-publication-uuid'; export function VoiceButton() { const { status, start, end } = useScenaroSession({ scenario: SCENARIO_UUID }); return status === 'connected' ? : ; } ``` That's it — `useScenaroSession` handles identification, session start, transport connection, and teardown. No transport object to wire up, nothing to render conditionally around it. ## What happens under the hood The SDK identifies the visitor against the scenario and persists a JWT identity in `localStorage`. A session is created for the scenario and the SDK opens a connection to the voice agent. The SDK receives the scenario's `config` (features, tools, voice stack) and registers your frontend tool handlers automatically. `status` becomes `'connected'`, `agentState` reflects what the agent is doing (`'listening' | 'thinking' | 'speaking'`), and `messages` accumulates the conversation. ## Reading state and handling messages ```tsx const { status, // 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'ended' agentState, // 'idle' | 'listening' | 'thinking' | 'speaking' messages, // ConversationMessage[] error, // ScenaroError | null sendText, setMicrophoneEnabled, } = useScenaroSession({ scenario: SCENARIO_UUID, onMessage: (message) => console.log(message), onError: (error) => console.error(error.code, error.message), }); ``` ## Frontend tools Tools are plain functions — no request IDs, no manual response wrapping: ```tsx useScenaroSession({ scenario: SCENARIO_UUID, tools: { search_products: async ({ query }) => searchCatalog(query), open_page: async ({ url }) => { router.push(url); return { ok: true }; }, }, }); ``` See [Authentication](/build/authentication) for caching, token refresh, and `external_id` behavior. ## Environment variables | Variable | Example | Purpose | |----------|---------|---------| | `VITE_SCENARO_API_URL` | `https://api.scenaro.io` | API base URL (Vite apps) | | `NEXT_PUBLIC_API_URL` | `https://api.scenaro.io/v1` | API base URL (Next.js — adjust `apiPathPrefix`) | ## Next steps - [Implement an experience](/introduction/implement-an-experience) — full developer journey - [Tools and features](/build/tools-and-features) — session tools vs FeatureProvider - [Feature registry](/build/feature-registry) — wire tool calls to UI components - [Embed voice on a website](/guides/recipes/embed-voice-website) — full production recipe --- # Key concepts Source: https://docs.scenaro.io/introduction/concepts/ Before diving into implementation details, understand how Scenaro pieces fit together. ## Scenario and publication A **scenario** is the authored experience in Cockpit (prompt, features, collections, voice settings). A **publication** is the live UUID your frontend embeds. One scenario can have multiple publications (staging vs production). Your app always uses the **publication UUID** — it's the `scenario` option you pass to `useScenaroSession`. ## Two-hop authentication Scenaro separates **identity** from **session**: | Step | Endpoint | Output | |------|----------|--------| | Identify | `POST /v1/public/token` | `user_token` (JWT), `user_id`, `application_id` | | Session | `POST /v1/public/session` | **Transport credentials** — `livekit_token`, `livekit_url` (LiveKit is the current [Transport](/introduction/transport) implementation) | The legacy single-call `generateToken` flow has been removed. Every integration must use identify then session — `useScenaroSession` runs both automatically when you call `start()`. Identification is **idempotent on the client**: if a valid JWT is already in storage, no network call is made. ## Transport data channel Once connected, the voice agent and frontend communicate over the **Transport data channel** — implemented with LiveKit data topics today (see [Transport](/introduction/transport) for the engine-agnostic boundary): - `config` — agent sends feature definitions, collections, restore state - `message:new` — conversation messages (user, assistant, tool, event) - `tool:{toolName}` — dynamic tool response topics - `state:update` — frontend sends experience checkpoints to the agent `useScenaroSession` subscribes to these topics for you — you only see the results (`messages`, `config`, tool calls). See the [Protocol reference](/voice-agent/protocol/topics) if you're building on the raw channel. ## Feature registry The SDK's `FeatureProvider` manages feature state (history, visibility, tool execution) but **does not ship business features**. Your app provides a `registry`: ```tsx ``` Each registry entry maps tool names to React components and handler functions. When the agent calls a tool, the SDK session dispatches the RPC to your handler. For tools without a UI panel, you can skip the registry and pass a plain function via the `tools` option of `useScenaroSession`. ## Event bus `ScenaroProvider` creates a scoped `ScenaroEventBus` (replacing the legacy `window.__` global). Subscribe with `useScenaroEvents()` for config, messages, tool requests, and session events. ## Sub-exports mental model ```mermaid flowchart TB subgraph protocol ["@scenaro/sdk/protocol"] Topics[topics.ts] Messages[messages.ts] RPC[rpc.ts] end subgraph client ["@scenaro/sdk/client"] HTTP[ScenaroClient] Storage[session-store] end subgraph react ["@scenaro/sdk/react"] Provider[ScenaroProvider] Session[useScenaroSession] Features[FeatureProvider] end Agent[Voice Agent Python] --> Topics HTTP --> Agent Session --> HTTP Session --> Topics Features --> Session ``` | Layer | Use when | |-------|----------| | `protocol` | Defining cross-language contracts, agent development, type sharing | | `client` | Node scripts, non-React apps, API calls without UI | | `react` | Browser apps with voice sessions over Transport | Non-React browser apps can use the headless `ScenaroSession` class from `@scenaro/sdk` — see the [Session reference](/sdk-reference/session). ## Session lifecycle 1. User calls `start()` → SDK identifies the visitor (cached JWT reused if valid) 2. SDK starts the session and connects the Transport → `status` becomes `'connecting'` then `'connected'` 3. Agent joins → sends `config` → SDK registers your tool handlers 4. Conversation runs → `messages` accumulates, tool RPCs fire your handlers 5. Agent or user ends → `onSessionEnd` fires, `status` becomes `'ended'` ## What stays in your app - Feature UI components (search, cart, comparison) - Brand styling and layout - Collection-specific API calls - Tool handler implementations (passed via `tools` or the feature registry) --- # Transport Source: https://docs.scenaro.io/introduction/transport/ `@scenaro/sdk` separates two things that most voice SDKs bundle together: the **session contract** your app codes against, and the **realtime engine** that actually moves audio and data. That boundary is called **Transport**. You rarely need this page to ship a voice experience — [`useScenaroSession`](/sdk-reference/react) hides Transport completely for the embed path. Read this if you're deciding whether code belongs in your app or in a Transport-specific extension, or if you're curious how the SDK stays engine-agnostic. ## Why a Transport boundary exists Today, Transport is implemented with [LiveKit](https://livekit.io) — WebRTC audio plus a data channel for the agent ↔ frontend protocol. Tomorrow it could be [Pipecat](https://www.pipecat.ai) or another realtime engine. `ScenaroSession` never calls LiveKit directly — it calls an internal `Transport` interface, and a `LiveKitTransport` implements that interface underneath. ```mermaid flowchart TB subgraph portable ["Contract portable Scenaro"] Session[ScenaroSession] Protocol["Protocol - topics + RPC"] end subgraph transportLayer ["Transport abstraction"] TransportIF[Transport interface] end subgraph engines ["Engine implementations"] LK[LiveKitTransport] PC["PipecatTransport (future)"] end Session --> TransportIF Protocol --> Session TransportIF --> LK TransportIF -.-> PC LK --> LKRoom["engine handle: Room"] PC --> PCHandle["engine handle: TBD"] ``` This mirrors the internal boundary in the SDK source — [`packages/sdk/src/core/transport.ts`](https://github.com/scenaro/projects/blob/main/packages/sdk/src/core/transport.ts): ```ts /** * Internal, non-public boundary between ScenaroSession's business logic and the real-time * engine that carries it (LiveKit today, potentially another engine — e.g. Pipecat — later). */ export interface Transport { connect(url: string, token: string): Promise; disconnect(): Promise; setMicrophoneEnabled(enabled: boolean): Promise; sendText(text: string): Promise; publishData(topic: string, data: Record): Promise; registerToolHandler(name: string, handler: TransportToolRpcHandler): void; onData(handler: (message: TransportDataMessage) => void): () => void; onConnectionStateChange(handler: (state: TransportConnectionState) => void): () => void; /** Non-portable escape hatch: the underlying engine object (e.g. LiveKit's `Room`). */ getEngineHandle(): unknown; } ``` This interface is **not exported** from `@scenaro/sdk` — it's an internal seam, not a plugin API you implement yourself. What it buys you as a developer is a stable contract on top of it. ## Two levels: portable contract vs engine extension | Level | What it is | Stability | Examples | |-------|-----------|-----------|----------| | **Portable contract** | Scenaro's session API, independent of the engine underneath | Stable across future Transport implementations | `status`, `agentState`, `messages`, `config`, `tools`, session events | | **Engine extension** | The current engine's own API surface | Tied to LiveKit today — not guaranteed to carry over | `room`, `RoomContext`, `@livekit/components-react` hooks/components | Code written against the portable contract keeps working if Scenaro ever adds another Transport implementation behind `useScenaroSession`. Code that calls `room.localParticipant...` or uses LiveKit's React hooks is written against the current engine directly — the same trade-off you'd make using that engine standalone. ```tsx const { status, messages, room } = useScenaroSession({ scenario: PUBLICATION_UUID }); status; // portable — survives a future Transport swap messages; // portable room; // engine extension — LiveKit's Room, today ``` See [Advanced: Transport extensions](/guides/advanced-transport-integration) for how to use the current engine's extension surface. ## What "transport session" means in the docs Across the documentation, **transport session** refers to the realtime connection established by `start()` — audio (if enabled) plus the data channel carrying the [protocol](/voice-agent/protocol/topics). The wire response from `POST /v1/public/session` is described as **transport credentials**: today those fields are named `livekit_token` and `livekit_url` (see [`ScenaroClient.startSession`](/sdk-reference/client#startsession)), because LiveKit is the current implementation. The field names are not renamed as part of this documentation alignment — only the prose around them is engine-agnostic. ## Implementations | Engine | Status | Where | |--------|--------|-------| | **LiveKit** | Current, only implementation | `packages/sdk/src/transport/livekit/` | | **Pipecat** | Not implemented — this boundary exists so it could be added without rewriting `ScenaroSession` | — | ## Next steps - [Advanced: Transport extensions](/guides/advanced-transport-integration) — using the LiveKit engine extension surface - [Key concepts](/introduction/concepts) — scenario, auth, protocol - [Protocol reference](/voice-agent/protocol/topics) — the data carried over Transport --- # Coding agent support Source: https://docs.scenaro.io/introduction/coding-agent-support/ Use Scenaro docs as context for AI coding assistants — the same pattern used by LiveKit, Retell, and Vapi. Scenaro's SDK abstracts its realtime engine behind a [Transport](/introduction/transport) boundary — LiveKit is the current implementation. Generated code should prefer the portable `useScenaroSession` contract over engine-specific APIs unless the task explicitly needs [Transport extensions](/guides/advanced-transport-integration). ## Quick links | Resource | URL | |----------|-----| | Documentation index | https://docs.scenaro.io/llms.txt | | Full documentation | https://docs.scenaro.io/llms-full.txt | | MCP server | https://docs.scenaro.io/mcp | | Markdown export | Append `.md` to any page URL | ## Instructions for AI agents > Fetch the complete documentation index at: https://docs.scenaro.io/llms.txt > Use this file to discover all available pages before exploring further. > > For clean Markdown of any page, append `.md` to the page URL. > > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.scenaro.io/mcp ## Install the Scenaro skill npx skills add https://docs.scenaro.io ## Contextual menu Every documentation page includes a contextual menu (top-right) with: - **Copy page** — copies the current page as Markdown - **View as Markdown** — opens the `.md` version - **Open in ChatGPT / Claude / Cursor** — starts a conversation with page context - **MCP** — copies the MCP server URL Press **Cmd+C** (Mac) or **Ctrl+C** (Windows) on any page to copy it as Markdown. ## Cursor setup In Cursor Settings → MCP, add the Scenaro docs server: ``` https://docs.scenaro.io/mcp ``` Or use the contextual menu **Cursor** option on any page. Paste `https://docs.scenaro.io/llms.txt` when you need the agent to discover all pages before drilling into specifics. For large implementation tasks (full SDK integration, protocol work), provide `https://docs.scenaro.io/llms-full.txt` as a single context file. ## Recommended reading order for agents When implementing a Scenaro integration, read these pages in order: 1. [Implement an experience](/introduction/implement-an-experience.md) — full journey map 2. [Quickstart](/introduction/quickstart.md) — minimal working embed 3. [Tools and features](/build/tools-and-features.md) — session tools vs FeatureProvider 4. [Authentication](/build/authentication.md) — identify → session flow 5. [Protocol topics](/voice-agent/protocol/topics.md) — data-channel contract 6. [SDK reference](/sdk-reference/react.md) — `useScenaroSession` API 7. [Transport](/introduction/transport.md) — only if the task touches engine-specific code ## Key facts for code generation - Package: `@scenaro/sdk@0.3.0` on npm - Legacy APIs (`useScenarioSession`, `AgentListener`, `getScenario`) were **removed in 0.3.0** - Auth is **two-hop**: `identifyUser` then `startSession` — never a single token call - Transport is abstracted (LiveKit today) — see [Transport](/introduction/transport). Only install `livekit-client >=2.0.0` and `@livekit/components-react >=2.0.0` for [Transport extensions](/guides/advanced-transport-integration) - Storage keys: `scenaro_session:app:{applicationId}` + `scenaro_scenario_uuid` - Business features are **not** in the SDK — inject via `FeatureProvider` registry - Protocol source of truth: `packages/sdk/src/protocol/` in the monorepo ## Scenaro repositories | Repo | Purpose | |------|---------| | [scenaro/projects](https://github.com/scenaro/projects) | Monorepo — `@scenaro/sdk`, experiences | | [scenaro/platform-api](https://github.com/scenaro/platform-api) | Public and admin HTTP API | | [scenaro/voice-agent](https://github.com/scenaro/voice-agent) | Python voice agent (LiveKit Agents today) | | [scenaro/platform-spa](https://github.com/scenaro/platform-spa) | Cockpit and embed SPA | --- # Changelog Source: https://docs.scenaro.io/changelog/ ## @scenaro/sdk ### 0.3.0 **Legacy API removed** - Removed `useScenarioSession`, `AgentListener`, `useConversationMessages`, UI activity exports - Removed `ScenaroClient.getScenario()` (endpoint never existed server-side) - Added `rawConfig` on `ScenaroSession` / `useScenaroSession` for wire-format config access - `SessionEventBridge` + `useFeatureToolRpc` replace legacy listener plumbing internally - All experiences and platform-spa migrated to `useScenaroSession` ### 0.2.0 **New session contract — `useScenaroSession` and headless `ScenaroSession`** - New single entry point: `useScenaroSession({ scenario, tools, onMessage, onError, onSessionEnd })` — handles identification, session start, transport connection, config handshake, and tool RPC. No `AgentListener` to mount, no manual `RoomContext` wiring. - New framework-agnostic `ScenaroSession` class (`@scenaro/sdk`) with typed events (`status-change`, `agent-state`, `message`, `config`, `session-end`, `error`). - Frontend tools are plain functions `(args, context) => result` — request IDs, response wrapping, and the RPC timeout (`TIMEOUT` + aborted `context.signal`) are handled by the SDK. - Typed errors: `ScenaroError` with stable `code` values. - Curated `SessionConfig` (camelCase) replaces raw wire payload exposure; `Collection` is now fully typed in `@scenaro/sdk/protocol`. - LiveKit isolated behind an internal transport boundary (lint-enforced); `room` stays available as a documented LiveKit extension for advanced integrations. - `ScenaroProvider` injects LiveKit `RoomContext` automatically and disposes the session on unmount. **Breaking changes** - Root entry (`@scenaro/sdk`) is now framework-agnostic: React exports moved exclusively to `@scenaro/sdk/react`. - Removed the empty `@scenaro/sdk/ui` sub-export. - `startSession` no longer defaults `language` to `'fr-FR'` — omitting it uses the scenario's configured language. - `getAppSession`, `LEGACY_KEYS`, and `ApiFetchOptions` are no longer exported from `@scenaro/sdk/client` (internal plumbing). **Deprecations (removal in 0.3.0)** - `useScenarioSession`, `AgentListener`, `useConversationMessages`, ui-activity-tracker exports — see the [migration guide](/guides/migration-from-vendored-sdk). - `ScenaroClient.getScenario()` — the backing endpoint is not implemented server-side. ### 0.1.1 **Simplified session storage** - Identity stored in a single app-scoped blob: `scenaro_session:app:{applicationId}` - Removed redundant flat keys (`scenaro_user_token`, `scenaro_user_id`, etc.) - Automatic migration from legacy storage formats on read - Legacy platform-spa keys (`user_token`, `external_id`) still supported via fallback ### 0.1.0 **Initial npm release** - Package renamed from `@scenaro/embed-sdk` to `@scenaro/sdk` - Sub-exports: `protocol`, `client`, `react`, `ui` - Two-hop auth: `identifyUser` + `startSession` - `ScenaroProvider`, `useScenarioSession`, `AgentListener` - `FeatureProvider` with injectable registry - Scoped `ScenaroEventBus` (replaces `window.__`) - LiveKit peer dependencies (`>=2.0.0`) - Protocol: data-channel topics, message types, RPC contracts ## Documentation ### 2026-07-11 **SDK 0.3.0 documentation alignment** - New [Implement an experience](/introduction/implement-an-experience) journey map - New [Tools and features](/build/tools-and-features) — session tools vs FeatureProvider - Removed stale legacy API references (`useScenarioSession`, `AgentListener`, `getScenario`) - Version pins updated to `@scenaro/sdk@0.3.0` - OpenAPI: removed unimplemented `GET /public/scenarios/{uuid}` **Transport terminology** - New [Transport](/introduction/transport) page — the engine-agnostic boundary behind `ScenaroSession` (LiveKit today, extensible to other engines such as Pipecat) - Renamed `guides/advanced-livekit-integration` to [Advanced: Transport extensions](/guides/advanced-transport-integration) (redirect kept) - Documentation now distinguishes the **portable contract** (`status`, `messages`, `config`, `tools`) from **engine extensions** (`room`, `RoomContext`) everywhere — install instructions, diagrams, and reference tables no longer imply LiveKit is required for the embed path ### 2026-07-10 **Initial public documentation** - Mintlify site at [docs.scenaro.io](https://docs.scenaro.io) - AI-ready: `llms.txt`, `.md` export, MCP server, contextual menu - Guides: quickstart, auth, protocol, feature registry, 5 recipes - SDK reference for `client`, `react`, `protocol`, `session` - Public API OpenAPI spec (initial) ## Reporting issues - SDK bugs: [github.com/scenaro/projects/issues](https://github.com/scenaro/projects/issues) - Documentation: same repo, label `documentation` --- # Cockpit Source: https://docs.scenaro.io/build/cockpit/ Cockpit is Scenaro's authoring environment. Use it to design voice scenarios, configure features and collections, test sessions, and publish to production. ## Workflow Define the agent prompt, voice stack, language, and behavior in Cockpit. Attach features (product search, display, comparison, bucket list, etc.) and link them to tools the agent can call. Bind product or content collections so features can query your catalog data. Use the live test page (`/scenarios/[id]/live`) or the public embed preview to validate voice and tool behavior. Publish the scenario to obtain a **publication UUID** for production embeds. ## What you get after publishing | Artifact | Used by | |----------|---------| | Publication UUID | Frontend `useScenaroSession({ scenario })` | | Feature config | Sent by agent on `config` data topic at session start | | Collections metadata | Passed to feature handlers and UI components | ## Testing before production - **Cockpit live page** — internal test with full feature registry (platform-spa) - **Public embed** — `/public/[uuid]` route for stakeholder review - **Experience apps** — brand-specific Vite apps in the monorepo (`experiences/*`) ## Environment alignment Ensure your test and production frontends point to the correct API: | Environment | API URL | |-------------|---------| | Local dev | `https://localhost:8443` or bootstrap default | | Staging | Your staging API host | | Production | `https://api.scenaro.io` | ## Next steps - [React integration](/build/react-integration) — wire the publication UUID in your app - [Feature registry](/build/feature-registry) — map Cockpit features to frontend components --- # React integration Source: https://docs.scenaro.io/build/react-integration/ This guide covers the standard React integration pattern used across Scenaro experience apps and platform-spa. ## Provider stack ```tsx import { ScenaroProvider, FeatureProvider } from '@scenaro/sdk/react'; export function ScenaroLiveProviders({ children, registry }) { return ( {children} ); } ``` ## ScenaroProvider Owns the session, creates a shared `ScenaroClient` and scoped `ScenaroEventBus`, and injects the current [Transport](/introduction/transport) implementation's `RoomContext` (LiveKit today) silently — no manual `RoomContext.Provider` wiring needed. | Prop | Type | Description | |------|------|-------------| | `client` | `ScenaroClient` | Pre-built client instance (preferred for shared config) | | `apiUrl` | `string` | API base URL (alternative to `client`) | | `apiPathPrefix` | `string` | Path prefix, default `/v1` | | `storagePrefix` | `string` | Prefix for localStorage keys (multi-tenant embeds) | | `storage` | `StorageAdapter \| null` | Custom storage (default: `localStorage`) | Hooks: - `useScenaro()` — full context (`client` + `events`) - `useScenaroClient()` — HTTP client only - `useScenaroEvents()` — event bus only ## useScenaroSession The single entry point for a session. It handles the full lifecycle — identify, start session, Transport connect/disconnect — plus the data channel, config handshake, and tool RPC internally. ```tsx import { useScenaroSession } from '@scenaro/sdk/react'; const { status, // 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'ended' agentState, // 'idle' | 'listening' | 'thinking' | 'speaking' messages, // ConversationMessage[] config, // SessionConfig | null (curated) rawConfig, // ConfigPayload | null (wire format) conversationId, error, // ScenaroError | null start, // (overrides?) => Promise end, // () => Promise sendText, // (text) => Promise setMicrophoneEnabled, } = useScenaroSession({ scenario: 'publication-uuid', onMessage: (m) => console.log(m), onError: (e) => console.error(e.code), onSessionEnd: (info) => {}, }); ``` ### Options | Option | Description | |--------|-------------| | `scenario` | Publication UUID (required, cannot be overridden in `start`) | | `language` | Optional BCP-47 tag; omit to use the scenario's configured language | | `tools` | Frontend tool handlers (see below) | | `onMessage` | Called for each `ConversationMessage` | | `onError` | Called with a `ScenaroError` (has `.code`) | | `onSessionEnd` | Called when the session ends | `start(overrides?)` accepts option overrides for the session being started — everything except `scenario`. ### Frontend tools Tools are plain async functions — no `requestId` plumbing: ```tsx useScenaroSession({ scenario: 'publication-uuid', tools: { search_products: async (args, context) => { // context: { toolName, settings, collections: Collection[], signal: AbortSignal } return await searchCatalog(args, { signal: context.signal }); }, }, }); ``` ### Session config After the config handshake: - `config` — curated `{ features, collections, avatarEnabled, audioDictationEnabled }` - `rawConfig` — full wire payload (`restore_state`, tool wiring, snake_case fields) See [Tools and features](/build/tools-and-features#config-curated-vs-raw) for when to use each. ### Advanced: engine handle access The hook also returns `room` — the current Transport implementation's engine handle (LiveKit `Room | null`) — for advanced use cases. Most apps never need it — see [Advanced: Transport extensions](/guides/advanced-transport-integration). The hook is powered by the headless `ScenaroSession` class from `@scenaro/sdk`, usable outside React. See the [ScenaroSession reference](/sdk-reference/session). ## Next.js Add the SDK to `transpilePackages`: ```js // next.config.js module.exports = { transpilePackages: ['@scenaro/sdk'], }; ``` ## Next steps - [Tools and features](/build/tools-and-features) - [Authentication](/build/authentication) - [Events and listeners](/build/events-and-listener) - [Feature registry](/build/feature-registry) --- # Authentication Source: https://docs.scenaro.io/build/authentication/ Scenaro uses a **two-step authentication flow**. Identity (JWT) is separate from the Transport session credentials. If you use `useScenaroSession` (React) or `ScenaroSession` (headless), both steps run automatically when you call `start()`. This page documents the underlying client flow. ## Flow diagram ```mermaid sequenceDiagram participant App as Your App participant API as Platform API participant Transport as Transport (LiveKit today) App->>API: POST /v1/public/token (identifyUser) API-->>App: user_token, user_id, application_id App->>API: POST /v1/public/session (Bearer user_token) API-->>App: transport credentials (livekit_token, livekit_url) App->>Transport: connect(url, token) ``` ## Step 1 — Identify ```ts import { createScenaroClient } from '@scenaro/sdk/client'; const client = createScenaroClient({ apiUrl: 'https://api.scenaro.io', apiPathPrefix: '/v1', }); const identity = await client.identifyUser(scenarioUUID, externalId); // { user_token, user_id, application_id } ``` **Request body** (`POST /v1/public/token`): ```json { "scenario_uuid": "3d4134b3-d3f9-40b1-98d9-fa4f36dec3f7", "external_id": "guest-abc123" } ``` ### Client-side caching `identifyUser` is **idempotent**: if a non-expired `user_token` with `user_id` and `application_id` exists in storage, it returns the cached identity without a network call. ### external_id If you omit `externalId`, the SDK generates a stable anonymous ID and persists it. This consolidates conversations for returning visitors without login. ## Step 2 — Start session ```ts const session = await client.startSession(scenarioUUID, { inputMode: 'audio', outputMode: 'audio', language: 'en-US', sessionOptions: { resumeConversationId: 'conv-uuid', metadata: { page: '/products' }, }, }); // session.livekit_url / session.livekit_token are the Transport credentials — // named after LiveKit, the current Transport implementation. See /introduction/transport. await transport.connect(session.livekit_url, session.livekit_token); ``` `language` is optional — omit it and the backend applies the scenario's configured language. **Request body** (`POST /v1/public/session`): ```json { "scenario_uuid": "3d4134b3-d3f9-40b1-98d9-fa4f36dec3f7", "input_mode": "audio", "output_mode": "audio", "language": "en-US" } ``` Requires `Authorization: Bearer {user_token}`. ## Token refresh outside sessions Features that call the API outside the Transport session (search, cart) use: ```ts const token = await client.getValidUserToken(); ``` This returns the stored token if valid, otherwise silently re-identifies using persisted `scenario_uuid` and `external_id`. ## JWT expiration Tokens are validated locally with a **90-second buffer** (`EXPIRY_BUFFER_SECONDS`). No server round-trip is needed to check expiration. ```ts import { isTokenExpired, getJWTExpiry } from '@scenaro/sdk/client'; ``` ## Deprecated flow The legacy `generateToken` single-call flow (one request creating user + session) has been removed. Do not implement it in new integrations. ## Clear session ```ts client.clearStoredSession(scenarioUUID); ``` Removes the app-scoped identity blob and scenario pointer. See [Session storage](/build/session-storage) for key details. ## API reference See the [API Reference](/api-reference) tab for full request/response schemas. --- # Session storage Source: https://docs.scenaro.io/build/session-storage/ After `identifyUser`, the SDK persists identity in browser storage using a simplified model introduced in `@scenaro/sdk@0.1.1`. ## Storage keys | Key | Content | |-----|---------| | `scenaro_session:app:{applicationId}` | **Source of truth** — JSON blob `{ user_token, user_id, application_id, external_id }` | | `scenaro_scenario_uuid` | Pointer to the active publication UUID (for token refresh context) | Example after identify: ``` scenaro_session:app:2 → {"external_id":"guest-…","user_token":"eyJ…","user_id":64,"application_id":2} scenaro_scenario_uuid → "3d4134b3-d3f9-40b1-98d9-fa4f36dec3f7" ``` Transport session credentials (LiveKit tokens today) are **ephemeral** — they are not stored. Only the JWT identity is persisted. ## Removed keys (v0.1.1+) The following are no longer written at identify time: - Flat keys: `scenaro_user_token`, `scenaro_user_id`, `scenaro_application_id`, `scenaro_external_id` - Duplicate scenario blob: `scenaro_session:{scenarioUUID}` ## Automatic migration On read, the SDK client consolidates legacy storage formats (flat keys or scenario-scoped blobs) into `scenaro_session:app:{id}` and deletes obsolete keys. Legacy platform-spa keys (`user_token`, `external_id`) are still supported once via fallback. ## Custom storage Inject a `StorageAdapter` for SSR, tests, or non-browser environments: ```ts const client = createScenaroClient({ apiUrl: 'https://api.scenaro.io', storage: myMockStorage, // or null for SSR }); ``` ```ts interface StorageAdapter { getItem(key: string): string | null; setItem(key: string, value: string): void; removeItem(key: string): void; } ``` Default: `localStorage` in browser, `null` on server. ## Multi-embed prefix For multiple Scenaro embeds on one origin, use `storagePrefix`: ```ts createScenaroClient({ apiUrl: 'https://api.scenaro.io', storagePrefix: 'brand_a', }); // Keys become: brand_a_scenaro_session:app:2 ``` ## Reading stored values ```ts client.getStoredUserToken(scenarioUUID?, applicationId?); client.getSessionUserId(); client.getSessionApplicationId(); ``` ## Clear session ```ts // Clear specific scenario context client.clearStoredSession(scenarioUUID); // Clear all Scenaro storage for this prefix client.clearStoredSession(); ``` --- # Tools and features Source: https://docs.scenaro.io/build/tools-and-features/ Scenaro voice agents call **frontend tools** over Transport RPC (LiveKit RPC today — see [Transport](/introduction/transport)). The SDK offers two ways to handle those calls. This page explains when to use each — similar to how Vapi separates [client-side tools](https://docs.vapi.ai/tools/client-side-websdk) (browser effects) from server tools (model needs the result). Do not register the same RPC method name in both patterns — behavior will conflict. ## Decision tree ```mermaid flowchart TD A[Agent calls a frontend tool] --> B{Does the tool open UI?} B -->|No — fetch data, update state silently| C[Session tools] B -->|Yes — search panel, product card, cart| D{Using SDK FeatureProvider?} D -->|Yes| E[FeatureProvider registry] D -->|No — vendored FeatureManager| F[Local AgentListener + FeatureManager] C --> G["useScenaroSession({ tools })"] E --> H["FeatureProvider({ registry })"] F --> I["engine handle registerRpcMethod → executeTool"] ``` ## Pattern 1 — Session tools **Use when:** the handler returns data to the agent and does not need a feature panel, history, or overlay UI. **Registration:** pass `tools` to `useScenaroSession` (or `session.start({ tools })` headless). **Handler signature:** ```ts type ToolHandler = ( args: Record, context: { toolName: string; settings: Record; collections: Collection[]; signal: AbortSignal; }, ) => unknown | Promise; ``` ```tsx useScenaroSession({ scenario: PUBLICATION_UUID, tools: { get_store_hours: async (_args, context) => { return { open: '9:00', close: '18:00', timezone: context.settings.timezone }; }, }, }); ``` The SDK: - Registers RPC methods on connect - Wraps your return value as `{ requestId, code: 'OK', data }` - Aborts `context.signal` after 10 seconds (`RPC_TIMEOUTS.FRONTEND_TOOL_MS`) - Returns `{ code: 'TIMEOUT' }` to the agent on timeout ## Pattern 2 — FeatureProvider tools **Use when:** the tool drives a UI feature panel — search results, product detail, comparison, shopping list — with history, visibility, and component rendering. **Registration:** `FeatureProvider` with a `Map` keyed by **feature type** (e.g. `collection-items-search`). **Handler signature** (different from session tools): ```ts type FeatureToolHandler = ( requestId: string, args: Record, settings?: Record, collections?: Collection[], ) => ToolExecutionResult; ``` ```tsx const registry = new Map([ ['collection-items-search', { toolNames: ['collection:items:search'], primaryTool: 'collection:items:search', component: SearchPanel, handlers: { 'collection:items:search': async (requestId, args, settings, collections) => { const results = await searchAPI(args, collections); return { success: true, data: results, requestId }; }, }, }], ]); {children} ``` `FeatureProvider` internally mounts `useFeatureToolRpc`, which: 1. Listens for `config` on the event bus 2. Registers RPC methods for tools listed in the wire config 3. Emits `tool:execute` → runs `executeTool` → responds to the agent See [Feature registry](/build/feature-registry) and [custom feature recipe](/guides/recipes/custom-feature). ## Local FeatureManager vs SDK FeatureProvider Both patterns exist in the Scenaro monorepo: | | SDK `FeatureProvider` | Local `FeatureManager` | |--|----------------------|------------------------| | **Used by** | platform-spa live page | All 7 experience apps | | **Session hook** | `useScenaroSession` | `useScenaroSession` (via thin adapter) | | **Tool RPC** | SDK `useFeatureToolRpc` | Local `AgentListener` → engine handle `registerRpcMethod` | | **Feature state** | SDK `useFeatures()` | Local `useFeatures()` in vendored runtime | | **When to choose** | Greenfield app, no vendored copy | Migrating existing experience with local runtime | Experience apps typically keep a thin adapter at `src/lib/scenaro-sdk/hooks/useScenarioSession.js` that maps `connect` → `start()`, `isConnected` → `status === 'connected'`, etc. The local `FeatureManager` + `AgentListener` handle tool RPC independently of SDK `FeatureProvider`. You do **not** need to migrate to SDK `FeatureProvider` unless you want to drop the vendored feature runtime. ## Config: curated vs raw After the config handshake you get two views: | Field | Type | Use for | |-------|------|---------| | `config` | `SessionConfig` | Features list, collections, avatar/dictation flags | | `rawConfig` | `ConfigPayload` | `restore_state`, tool wiring, snake_case backend fields | ```tsx const { config, rawConfig } = useScenaroSession({ scenario: PUBLICATION_UUID }); // Curated — most UI config?.features.map(f => f.type); // Wire format — state restoration, debugging rawConfig?.restore_state; ``` On the event bus, `events.on('config', …)` receives the **raw** `ConfigPayload`. On the session, `session.on('config', …)` receives the **curated** `SessionConfig`. ## RPC flow (both patterns) ```mermaid sequenceDiagram participant Agent as Voice agent participant Transport as Transport RPC participant SDK as SDK Transport layer participant Handler as Your handler Agent->>Transport: RPC (tool name, args) Transport->>SDK: registered method SDK->>Handler: invoke handler Handler-->>SDK: result SDK-->>Agent: JSON { requestId, code, data } ``` ## Topics not exposed as SDK helpers Some protocol topics exist but have no high-level SDK API yet. Use the engine handle (`room`) + manual data listeners if needed: | Topic | Purpose | |-------|---------| | `assistant:interrupt` | Stop agent speech | | `state:update` | Session checkpoints | | `dictation:transcript` | STT partials | | `event:fire` | Custom analytics events | See [Protocol topics](/voice-agent/protocol/topics). ## Next steps - [Feature registry](/build/feature-registry) — registry shape and `useFeatures()` API - [Events and listeners](/build/events-and-listener) — event bus catalog - [Product search recipe](/guides/recipes/product-search) — full feature example - [SDK reference: react](/sdk-reference/react) — `ToolHandler` types --- # Feature registry Source: https://docs.scenaro.io/build/feature-registry/ Not sure whether to use `FeatureProvider` or session tools? Read [Tools and features](/build/tools-and-features) first. `FeatureProvider` manages feature state (history, visibility, tool execution) but **ships no business features**. Your app provides a registry that maps agent tool names to React components and handlers. ## Architecture ```mermaid flowchart LR Agent[Voice Agent] -->|tool RPC| Session[useScenaroSession] Session -->|tool:request| Bus[ScenaroEventBus] Bus --> Runtime[FeatureProvider] Runtime -->|executeTool| Handler[Your handler] Handler --> Component[Your React component] ``` ## Basic setup ```tsx import { FeatureProvider } from '@scenaro/sdk/react'; import { SearchFeature } from './features/search'; import { searchHandler } from './features/search-handler'; const FEATURE_REGISTRY = new Map([ ['collection:items:search', { component: SearchFeature, handlers: { search_collection_items: searchHandler }, primaryTool: 'search_collection_items', toolNames: ['search_collection_items'], }], ]); bucketStorage.resetAll()} clearHistoryOnSessionEnd={false} initialVisibility={true} initialPosition="right" > {children} ``` ## ToolRegistryEntry | Field | Type | Description | |-------|------|-------------| | `component` | `React.ComponentType` | UI rendered when the feature is active | | `handlers` | `Record` | Async functions called on tool RPC | | `primaryTool` | `string` | Main tool name for this feature | | `toolNames` | `string[]` | All tool names that map to this feature | | `overlay` | `boolean` | Render as overlay vs inline panel | | `propsSelector` | `function` | Map tool state → component props | ## Tool handler signature ```ts type ToolHandler = ( requestId: string, args: unknown, settings?: Record, collections?: unknown[], ) => Promise | ToolExecutionResult; ``` Return shape: ```ts { requestId: string; response: { code: 'SUCCESS' | 'ERROR' | 'TIMEOUT' | 'CANCELLED'; data: unknown; // sent back to the agent }; componentData?: Record; // updates UI state } ``` ## Config handshake When the agent joins, it sends a `config` message with `features` and `collections`. `FeatureProvider`: 1. Registers available features from the config 2. Maps tool names to registry entries 3. The session (`useScenaroSession`) registers Transport RPC methods for each tool For simple one-off tools that don't need feature UI, you can skip the registry and pass plain functions via the `tools` option of `useScenaroSession`. See [React integration](/build/react-integration). ## Common feature IDs | Feature ID | Description | |------------|-------------| | `collection:items:search` | Search and explore catalog items | | `collection:items:display` | Product detail sheet | | `collection:items:comparison` | Side-by-side comparison | | `bucket:list:crud` | Basket / list management | | `workflow:conversation:end` | End conversation, redirect | Feature IDs come from Cockpit configuration. Your registry keys must match. ## What stays outside the SDK Per the architecture decision (July 2026): - Feature UI components (too brand-specific) - Collection API clients - Product templates and styling - Custom `FeatureManager` wrappers (legacy — migrate to `FeatureProvider`) ## Recipes - [Custom feature](/guides/recipes/custom-feature) - [Product search](/guides/recipes/product-search) - [E-commerce embed](/guides/recipes/ecommerce-embed) --- # Events and listeners Source: https://docs.scenaro.io/build/events-and-listener/ `useScenaroSession` handles the [Transport](/introduction/transport) data channel, config handshake, and tool RPC internally. You consume events through hook callbacks, returned state, and the scoped event bus. ## Session events via the hook ```tsx import { useScenaroSession } from '@scenaro/sdk/react'; const { status, agentState, messages, config, rawConfig } = useScenaroSession({ scenario: 'publication-uuid', onMessage: (m) => console.log('message', m), onError: (e) => console.error(e.code), onSessionEnd: (info) => showSummary(info), }); ``` | Field | Description | |-------|-------------| | `messages` | Accumulated conversation — no separate hook needed | | `config` | Curated session config after handshake | | `rawConfig` | Full wire `ConfigPayload` (`restore_state`, tool wiring) | | `status` | `'idle' \| 'connecting' \| 'connected' \| 'reconnecting' \| 'ended'` | | `agentState` | `'idle' \| 'listening' \| 'thinking' \| 'speaking'` | Outside React, the headless `ScenaroSession` class emits the same events: `status-change`, `agent-state`, `message`, `config`, `session-end`, `error`. See the [ScenaroSession reference](/sdk-reference/session). ## Event bus `ScenaroProvider` creates one `ScenaroEventBus` per provider tree. Subscribe with `useScenaroEvents()`: ```tsx import { useEffect } from 'react'; import { useScenaroEvents } from '@scenaro/sdk/react'; function DebugPanel() { const events = useScenaroEvents(); useEffect(() => { const onConfig = (payload) => console.log('config', payload); events.on('config', onConfig); return () => events.off('config', onConfig); }, [events]); return null; } ``` ### Event catalog | Event | Emitter | Payload | |-------|---------|---------| | `config` | `SessionEventBridge` | Raw `ConfigPayload` | | `message:new` | `SessionEventBridge` | `ConversationMessage` | | `session:end` | `SessionEventBridge` | `{ reason: string }` | | `tool:execute` | `useFeatureToolRpc` | `{ toolName, requestId, args }` | | `tool:response` | `FeatureProvider` | `{ requestId, response }` | | `tool:error` | `FeatureProvider` | `{ requestId, toolName, error }` | | `feature:show` | `FeatureProvider` | `{ reason, toolName, requestId }` | | `suggestions:show` | `FeatureProvider` | `{ suggestions }` | | `conversation:reset` | Your app (manual) | — triggers `FeatureProvider.reset()` | `session.on('config')` delivers the **curated** `SessionConfig`. `events.on('config')` delivers the **raw** wire payload. See [Tools and features](/build/tools-and-features#config-curated-vs-raw). ## Tool RPC flow For **session tools**, declare plain functions on `useScenaroSession`: ```mermaid sequenceDiagram participant Agent as Voice Agent participant Transport as Transport RPC participant SDK as useScenaroSession participant Handler as Your tool function Agent->>Transport: RPC call (tool name) Transport->>SDK: registered RPC method SDK->>Handler: tools[toolName](args, context) Handler-->>SDK: result SDK-->>Agent: JSON response ``` For **feature tools** via `FeatureProvider`, see [Tools and features](/build/tools-and-features). RPC timeout for frontend tools: **10 seconds** (`RPC_TIMEOUTS.FRONTEND_TOOL_MS`). The `context.signal` (`AbortSignal`) is aborted on timeout or session end. ## Text input Send user text with `sendText(text)` from the hook. It publishes on the `lk.chat` topic via the Transport data channel. ## Session end detection The SDK ends the session on: - Transport disconnected event - Agent participant leaving (2s grace period) - `session:end` data topic `status` becomes `'ended'` and `onSessionEnd` fires with the end payload. ## Pre-0.3 migration note `useScenarioSession`, `AgentListener`, and `useConversationMessages` were removed in **0.3.0**. Use `useScenaroSession` for session lifecycle and `messages` for conversation UI. See the [migration guide](/guides/migration-from-vendored-sdk). --- # Voice agent architecture Source: https://docs.scenaro.io/voice-agent/architecture/ The Scenaro voice agent is a Python service built on [LiveKit Agents](https://docs.livekit.io/agents/) — the current [Transport](/introduction/transport) implementation. It joins the Transport session, runs the voice pipeline (STT → LLM → TTS), and communicates with the frontend over the data channel. ## Repository Source: [github.com/scenaro/voice-agent](https://github.com/scenaro/voice-agent) Local development is bootstrapped from the [projects workspace](https://github.com/scenaro/projects) via `./bootstrap-scenaro-local.sh`. ## Session lifecycle ```mermaid sequenceDiagram participant FE as Frontend SDK participant API as Platform API participant Transport as Transport (LiveKit today) participant Agent as Voice Agent FE->>API: startSession API->>Transport: dispatch agent job FE->>Transport: connect (WebRTC) Agent->>Transport: join session Agent->>FE: config (data channel) FE->>Agent: config_ack loop Conversation Agent->>FE: message:new Agent->>FE: RPC tool call FE->>Agent: RPC tool response end Agent->>FE: session:end ``` ## Responsibilities | Layer | Responsibility | |-------|----------------| | **Platform API** | Session creation, agent dispatch, scenario config loading | | **Voice Agent** | Voice pipeline, LLM reasoning, tool selection, protocol messages | | **@scenaro/sdk** | Data channel listen, tool RPC handlers, feature UI | | **Transport (LiveKit today)** | WebRTC realtime engine, session/room management | ## Config handshake On join, the agent sends a `config` payload: ```json { "type": "config", "features": [{ "name": "search", "type": "collection:items:search", "tools": [...] }], "collections": [...], "company_id": 2, "restore_state": { "checkpoint": "...", "data": {} } } ``` The frontend responds with `config_ack`. Until ack is received, the agent may wait or retry with `config_request`. ## Tool execution model Tools fall into two categories: | Type | Executed by | Path | |------|-------------|------| | Backend tools | Voice agent | Internal Python functions | | Frontend tools | Your React app | Transport RPC (LiveKit today) → SDK session → your tool handler | Frontend tools declare a `frontend_method` in Cockpit. The agent calls it via Transport RPC; the SDK session routes it to your handler — a `tools` entry on `useScenaroSession` or a `FeatureProvider` registry handler. ## State sync The frontend can send experience checkpoints on `state:update`. The agent uses this for context restoration across page navigations or session resumes. ## Voice stack Configured per scenario in Cockpit. The agent supports multiple STT/LLM/TTS providers via LiveKit Agents plugins (the current Transport implementation's agent framework). ## Related docs - [Protocol topics](/voice-agent/protocol/topics) - [Protocol messages](/voice-agent/protocol/messages) - [RPC contracts](/voice-agent/protocol/rpc) - [Prompting guide](/voice-agent/prompting) --- # Prompting guide Source: https://docs.scenaro.io/voice-agent/prompting/ Effective prompts make the difference between a voice experience that feels natural and one that confuses users. This guide covers patterns that work well with Scenaro's feature + tool architecture. ## Core principles 1. **One job per turn** — ask one question or request one action at a time 2. **Tool-aware instructions** — tell the agent when to call each tool and what to say while waiting 3. **Speakable output** — write responses meant to be heard, not read 4. **Feature context** — reference what the user sees on screen when features are active ## Structure your system prompt ``` ## Role You are a helpful shopping assistant for [Brand]. ## Capabilities - Search the product catalog - Show product details - Compare items - Manage the shopping list ## Tool usage - When the user asks to find products, call search_collection_items - When showing a product, call display_collection_item - Keep speaking while tools execute: "Let me look that up for you" ## Conversation style - Short sentences, conversational tone - Confirm understanding before complex actions - Offer alternatives when search returns no results ``` ## Tool call narration Users hear silence during tool execution unless the agent speaks. Instruct the agent to: - Announce what it's doing: *"I'm searching for red wines under 30 euros"* - Summarize results concisely: *"I found 5 options, the first is…"* - Handle empty results gracefully: *"I didn't find an exact match — want me to broaden the search?"* ## Feature-aware prompts When features render UI panels, tell the agent to reference them: ``` When search results appear on screen, refer to them as "the results I'm showing you" rather than reading out every item. Highlight the top 2-3 verbally. ``` ## Language and locale Set the scenario language in Cockpit and pass the same BCP-47 tag from the frontend: ```tsx const { start } = useScenaroSession({ scenario: PUBLICATION_UUID, language: 'fr-FR' }); // or pass language in start({ language: 'fr-FR' }) ``` Keep prompt language consistent with the session language. ## State restoration If your experience uses `state:update` checkpoints, include restoration instructions: ``` If restore_state is provided, acknowledge what the user was doing: "Welcome back — you were looking at the Bordeaux selection." ``` ## Testing prompts 1. Test in Cockpit live page with real feature registry 2. Verify tool calls trigger the correct frontend RPC 3. Check behavior on timeout (slow network, empty results) 4. Test interruption — user speaks while agent is talking ## Anti-patterns | Avoid | Why | |-------|-----| | Long lists read aloud | Users can't scan audio — use UI features | | Calling tools without narration | Creates awkward silence | | Assuming tool success | Always handle ERROR and empty results | | Mixing languages | Mismatched STT/TTS/prompt languages degrade quality | --- # Data channel topics Source: https://docs.scenaro.io/voice-agent/protocol/topics/ Topics are defined in `@scenaro/sdk/protocol` and shared between the Python voice agent and all frontends. They travel over the [Transport](/introduction/transport) data channel — LiveKit's data channel today. ```ts import { DATA_TOPICS, toolDataTopic } from '@scenaro/sdk/protocol'; ``` ## Topic table | Topic | Direction | Role | |-------|-----------|------| | `config` | agent → front | Handshake: features, collections, restore state | | `config_ack` | front → agent | Acknowledge config received | | `config_request` | agent → front | Request config resend | | `message:new` | agent → front | Conversation message (user/assistant/tool/event) | | `session:end` | agent → front | Agent-initiated session end | | `dictation:transcript` | agent → front | Real-time STT transcript | | `state:update` | front → agent | Experience state checkpoint | | `ui:activity` | front → agent | UI activity heartbeat | | `assistant:interrupt` | front → agent | Interrupt assistant speech | | `session:control` | bidirectional | Session control signals | | `event:fire` | bidirectional | Custom event firing | | `lk.chat` | front → agent | User text message | | `tool:{toolName}` | dynamic | Tool response channel via `toolDataTopic()` | ## Source definition ```ts export const DATA_TOPICS = { CONFIG: 'config', CONFIG_ACK: 'config_ack', CONFIG_REQUEST: 'config_request', MESSAGE_NEW: 'message:new', SESSION_END: 'session:end', DICTATION_TRANSCRIPT: 'dictation:transcript', EVENT_FIRE: 'event:fire', STATE_UPDATE: 'state:update', SESSION_CONTROL: 'session:control', ASSISTANT_INTERRUPT: 'assistant:interrupt', UI_ACTIVITY: 'ui:activity', CHAT: 'lk.chat', } as const; ``` ## Dynamic tool topics ```ts toolDataTopic('search_collection_items'); // → 'tool:search_collection_items' ``` ## Handshake sequence Agent publishes on `config` with features, collections, and optional `restore_state`. The SDK session dispatches the payload on the event bus; `FeatureProvider` registers features and RPC methods. Frontend sends `config_ack` with `{ acknowledged: true, timestamp }`. ## Versioning The protocol package has **zero dependencies** and serves as the cross-language spec. Future Python/Go SDKs will align on this same topic and message definitions. Potential future extraction: standalone `scenaro-protocol` npm/PyPI package. ## See also - [Message types](/voice-agent/protocol/messages) - [RPC methods](/voice-agent/protocol/rpc) --- # Message types Source: https://docs.scenaro.io/voice-agent/protocol/messages/ All message types are exported from `@scenaro/sdk/protocol`. ```ts import type { ConfigPayload, ConversationMessage, PublicSessionResponse, } from '@scenaro/sdk/protocol'; ``` ## ConfigPayload Sent on the `config` topic at session start. | Field | Type | Description | |-------|------|-------------| | `features` | `FeatureDefinition[]` | Features and their tools | | `collections` | `unknown[]` | Collection metadata for features | | `company_id` | `string \| number` | Tenant identifier | | `restore_state` | `RestoreState` | Checkpoint for state restoration | | `voice_stack` | `string` | Active voice provider stack | | `audio_dictation` | `boolean` | Dictation mode enabled | | `conversation_id` | `string` | Active conversation ID | | `avatar_enabled` | `boolean` | Avatar output enabled | ### FeatureDefinition ```ts interface FeatureDefinition { name: string | Record; type: string; // e.g. 'collection:items:search' settings: Record; tools: ToolConfig[]; } ``` ### ToolConfig ```ts interface ToolConfig { name: string; type: string; settings?: Record; frontend_method?: string; // Transport RPC method name override (LiveKit today) rpc_timeout_s?: number; } ``` ## ConfigAckPayload ```ts interface ConfigAckPayload { acknowledged: boolean; timestamp: number; suppress_recap?: boolean; } ``` ## ConversationMessage Sent on `message:new`. | Field | Type | Description | |-------|------|-------------| | `id` | `string` | Message ID | | `role` | `'user' \| 'assistant' \| 'tool' \| 'event'` | Speaker role | | `content` | `string` | Message text | | `timestamp` | `string` | ISO timestamp | | `tool_calls` | `unknown[]` | Tool invocations (assistant) | | `tool_call_id` | `string` | Tool result reference | | `interrupted` | `boolean` | Speech was interrupted | | `experience_state` | `Record` | UI state snapshot | ## SessionEndPayload ```ts interface SessionEndPayload { reason?: string; ended_at?: string; } ``` ## API types (client) Used by `ScenaroClient` for HTTP calls: ### PublicTokenRequest / Response ```ts // Request { external_id: string; scenario_uuid: string } // Response { user_token: string; user_id: number; application_id: number; expires_in: number } ``` ### PublicSessionRequest / Response ```ts // Request { scenario_uuid: string; input_mode?: string; output_mode?: string; language?: string; resume_conversation_id?: string; metadata?: Record; } // Response — Transport credentials (LiveKit today, see /introduction/transport) { livekit_token: string; livekit_url: string; expires_in: number } ``` ## RestoreState ```ts interface RestoreState { checkpoint?: string; data?: Record; version?: number; updated_at?: string; } ``` --- # RPC contracts Source: https://docs.scenaro.io/voice-agent/protocol/rpc/ Frontend tool execution uses [Transport](/introduction/transport) RPC (LiveKit RPC today). Method names and timeouts are defined in `@scenaro/sdk/protocol`. ```ts import { RPC_METHODS, RPC_TIMEOUTS, resolveRpcMethodName, } from '@scenaro/sdk/protocol'; ``` ## Built-in RPC methods | Constant | Method name | Timeout | |----------|-------------|---------| | `UI_ACTIVITY_CHECK` | `scenaro__ui_activity_check` | 3000ms | | `COMPOSER_START_DICTATION` | `composer_start_dictation` | 10000ms | | `COMPOSER_END_DICTATION` | `composer_end_dictation` | 15000ms | | `COMPOSER_CANCEL_DICTATION` | `composer_cancel_dictation` | 8000ms | ## Timeout constants ```ts export const RPC_TIMEOUTS = { AGENT_DEFAULT_MS: 8000, FRONTEND_TOOL_MS: 10000, UI_ACTIVITY_MS: 3000, COMPOSER_START_MS: 10000, COMPOSER_END_MS: 15000, COMPOSER_CANCEL_MS: 8000, } as const; ``` Frontend tool handlers must respond within `FRONTEND_TOOL_MS` (10s) or the agent receives a timeout error. ## Tool method resolution Cockpit can override the RPC method name per tool: ```ts resolveRpcMethodName({ name: 'search_collection_items', frontend_method: 'custom_search_rpc', // optional override }); // → 'custom_search_rpc' or 'search_collection_items' ``` The SDK registers RPC methods dynamically from the `config` payload — via session tools (`useScenaroSession({ tools })`) or `FeatureProvider` / local feature runtime. ## Request / response shapes ### RpcToolRequest (inbound) ```ts interface RpcToolRequest { requestId: string; tool?: string; args?: Record; sentAt?: number; } ``` ### RpcToolResponse (outbound) ```ts interface RpcToolResponse { requestId?: string; code: 'SUCCESS' | 'ERROR' | 'TIMEOUT' | 'CANCELLED' | string; data?: unknown; } ``` ## Example handler ```ts async function searchHandler(requestId, args, settings, collections) { const items = await searchAPI(args.query, settings); return { requestId, response: { code: 'SUCCESS', data: { items: itemsForLLM } }, componentData: { items: itemsForUI, searchQuery: args.query }, }; } ``` The `response.data` is sent back to the agent. `componentData` updates the feature UI via `FeatureProvider`. ## Agent-side RPC The voice agent may also call built-in methods like `scenaro__ui_activity_check` to verify the user is interacting with feature panels. The SDK's `ui-activity-tracker` responds automatically during an active session. --- # Embed voice on your website Source: https://docs.scenaro.io/guides/recipes/embed-voice-website/ This recipe walks through a complete production embed — from install to a working voice button on your site. ## Goal A visitor clicks "Talk to us", gets a live voice session with your Scenaro agent, and sees feature panels (search, product details) alongside the conversation. ## Project structure ``` src/ ├── lib/ │ └── scenaro/ │ ├── client.ts # createScenaroClient │ ├── providers.tsx # ScenaroProvider + FeatureProvider │ └── features/ │ ├── registry.ts # FEATURE_REGISTRY map │ └── search/ │ ├── component.tsx │ └── handler.ts ├── components/ │ └── VoiceWidget.tsx └── App.tsx ``` ## Step 1 — Client ```ts // src/lib/scenaro/client.ts import { createScenaroClient } from '@scenaro/sdk/client'; export const scenaroClient = createScenaroClient({ apiUrl: import.meta.env.VITE_SCENARO_API_URL, apiPathPrefix: '/v1', }); ``` ## Step 2 — Providers ```tsx // src/lib/scenaro/providers.tsx import { ScenaroProvider, FeatureProvider } from '@scenaro/sdk/react'; import { scenaroClient } from './client'; import { FEATURE_REGISTRY } from './features/registry'; export function ScenaroProviders({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` ## Step 3 — Voice widget ```tsx // src/components/VoiceWidget.tsx 'use client'; import { useScenaroSession } from '@scenaro/sdk/react'; const PUBLICATION_UUID = import.meta.env.VITE_SCENARO_PUBLICATION_UUID; export function VoiceWidget() { const { status, error, start, end } = useScenaroSession({ scenario: PUBLICATION_UUID, }); return (
{error &&

{error.message}

} {status !== 'connected' ? ( ) : ( )}
); } ``` No `RoomContext` or listener component to wire up — the hook handles the data channel, config handshake, and tool RPC internally. Omit `language` to use the scenario's configured language, or pass one (e.g. `language: 'en-US'`) to override it. ## Step 4 — Mount in App ```tsx import { ScenaroProviders } from './lib/scenaro/providers'; import { VoiceWidget } from './components/VoiceWidget'; export default function App() { return (
{/* your site */}
); } ``` ## Environment variables ```env VITE_SCENARO_API_URL=https://api.scenaro.io VITE_SCENARO_PUBLICATION_UUID=your-publication-uuid ``` ## Production checklist - [ ] HTTPS required for microphone access - [ ] Publication UUID points to production scenario - [ ] Feature registry covers all Cockpit-configured tools - [ ] Error states shown to user (mic denied, network failure) - [ ] Session end cleans up UI state ## Variations - **Floating button** — position widget fixed bottom-right - **Mute button** — `setMicrophoneEnabled(false)` from the hook + custom mic toggle - **Text input** — `sendText('...')` alongside (or instead of) voice ## See also - [E-commerce embed](/guides/recipes/ecommerce-embed) - [Production checklist](/guides/production-checklist) --- # Custom feature Source: https://docs.scenaro.io/guides/recipes/custom-feature/ Add a custom feature that the voice agent can trigger via tool calls, with a React UI panel. ## Scenario You want a "store locator" feature: the agent calls `find_nearby_stores`, your frontend queries an API, shows a map, and returns results to the agent. ## Step 1 — Define the handler ```ts // src/lib/scenaro/features/store-locator/handler.ts import type { ToolHandler } from '@scenaro/sdk/react'; export const findNearbyStoresHandler: ToolHandler = async ( requestId, args: { city?: string; radius_km?: number }, ) => { const stores = await fetchStores(args.city, args.radius_km ?? 10); return { requestId, response: { code: 'SUCCESS', data: { stores: stores.map((s) => ({ name: s.name, address: s.address, distance_km: s.distance, })), }, }, componentData: { stores, city: args.city, }, }; }; ``` ## Step 2 — Build the component ```tsx // src/lib/scenaro/features/store-locator/component.tsx interface Props { settings: Record; toolState: Record; } export function StoreLocatorFeature({ toolState }: Props) { const stores = (toolState.stores as Store[]) ?? []; if (!stores.length) return null; return (

Nearby stores

    {stores.map((store) => (
  • {store.name} — {store.distance_km} km
  • ))}
); } ``` ## Step 3 — Register in the map ```ts // src/lib/scenaro/features/registry.ts import { StoreLocatorFeature } from './store-locator/component'; import { findNearbyStoresHandler } from './store-locator/handler'; export const FEATURE_REGISTRY = new Map([ ['store:locator', { component: StoreLocatorFeature, handlers: { find_nearby_stores: findNearbyStoresHandler }, primaryTool: 'find_nearby_stores', toolNames: ['find_nearby_stores'], }], ]); ``` ## Step 4 — Configure in Cockpit In Cockpit, add a feature with: - **Type**: `store:locator` (must match registry key) - **Tool name**: `find_nearby_stores` - **frontend_method**: optional RPC override ## Step 5 — Test 1. Start a voice session 2. Ask the agent to find nearby stores 3. Verify RPC fires, UI renders, agent receives `SUCCESS` data ## Error handling ```ts return { requestId, response: { code: 'ERROR', data: { stores: [], error: 'City not found' } }, componentData: { stores: [], city: args.city }, }; ``` Return `ERROR` (not throw) so the agent can explain the failure to the user. ## Tips - Keep `response.data` concise — it's sent to the LLM - Put rich UI data in `componentData` — it's for your React component only - Match feature `type` in Cockpit to registry key exactly - No UI panel needed? Skip the registry and pass the handler directly via the `tools` option of `useScenaroSession` — it receives `(args, context)` and returns the result, no request IDs or response wrapping --- # Product search feature Source: https://docs.scenaro.io/guides/recipes/product-search/ This recipe implements `collection:items:search`, the most common feature across Scenaro e-commerce experiences (Lupi, Le Petit Ballon, Urbansider, etc.). ## Feature overview | Property | Value | |----------|-------| | Feature type | `collection:items:search` | | Primary tool | `search_collection_items` | | UI | Search results panel with product rows | | Data source | Platform API collections | ## Handler pattern ```ts export const searchCollectionItemsHandler = async ( requestId: string, args: { query?: string; filter?: string }, settings: Record, collections: unknown[], ) => { const collectionId = getEffectiveCollectionId(settings, collections); const limit = Number(settings.limit ?? 10); const rawItems = await collectionItemsAPI.search({ collectionId, query: args.query ?? '', filter: args.filter ?? '', limit, searchField: settings.search_field as string, searchMode: settings.search_mode as string, }); const llmItems = rawItems.map((item) => filterItemByReturnFields(item, settings.return_fields as string[]), ); return { requestId, response: { code: 'SUCCESS', data: { items: llmItems } }, componentData: { items: rawItems, searchQuery: args.query, searchField: settings.search_field, limit, }, }; }; ``` ## Registry entry ```ts ['collection:items:search', { component: SearchFeature, handlers: { search_collection_items: searchCollectionItemsHandler }, primaryTool: 'search_collection_items', toolNames: ['search_collection_items'], propsSelector: (toolState, settings, collections) => ({ items: toolState.items, searchQuery: toolState.searchQuery, settings, collections, }), }], ``` Rendering results in your own UI instead of a feature panel? Skip the registry and pass the handler via the `tools` option of `useScenaroSession` — it receives `(args, context)` where `context.settings` and `context.collections` come from the scenario config. ## Search component essentials Your `SearchFeature` component should: 1. Read `toolState.items` from `useFeatures()` 2. Render product rows using a template (e.g. `ProductTemplate`) 3. Handle empty results with a friendly message 4. Support scroll boundary prevention on mobile ```tsx import { useFeatures } from '@scenaro/sdk/react'; export function SearchFeature({ toolState, settings, collections }) { const items = toolState.items ?? []; const template = getRowTemplate(settings.template); return (
{items.map((item) => template.render(item))}
); } ``` ## Authenticated API calls Search API calls need the user JWT. Use the shared client: ```ts import { scenaroClient } from '../client'; const token = await scenaroClient.getValidUserToken(); // Pass token to your collection API client ``` ## Agent prompt hints ``` When the user asks to find products: 1. Call search_collection_items with a clear query 2. Say "I'm showing you the results" — don't read every item 3. Offer to show details on a specific item ``` ## Related features | Feature | Tool | Purpose | |---------|------|---------| | `collection:items:display` | `display_collection_item` | Product detail sheet | | `collection:items:comparison` | `compare_collection_items` | Side-by-side compare | | `bucket:list:crud` | `add_to_bucket`, etc. | Shopping list | ## Reference implementation See `experiences/lupi/src/lib/scenaro-sdk/features/collection-items-search/` in the monorepo for a full production implementation. --- # E-commerce embed Source: https://docs.scenaro.io/guides/recipes/ecommerce-embed/ Build a voice shopping assistant with catalog search, product details, comparison, and basket management. ## Feature set Register all four standard e-commerce features: ```ts export const FEATURE_REGISTRY = new Map([ ['collection:items:search', { component: SearchFeature, handlers: { search_collection_items: searchHandler }, primaryTool: 'search_collection_items', toolNames: ['search_collection_items'], }], ['collection:items:display', { component: DisplayFeature, handlers: { display_collection_item: displayHandler }, primaryTool: 'display_collection_item', toolNames: ['display_collection_item'], }], ['collection:items:comparison', { component: ComparisonFeature, handlers: { compare_collection_items: compareHandler }, primaryTool: 'compare_collection_items', toolNames: ['compare_collection_items'], }], ['bucket:list:crud', { component: BucketFeature, handlers: { add_to_bucket: addHandler, remove_from_bucket: removeHandler, list_bucket: listHandler, clear_bucket: clearHandler, }, primaryTool: 'add_to_bucket', toolNames: ['add_to_bucket', 'remove_from_bucket', 'list_bucket', 'clear_bucket'], }], ]); ``` ## Layout pattern ``` ┌─────────────────────────────────────────┐ │ Your e-commerce site (header, nav) │ ├──────────────────────┬──────────────────┤ │ Main content │ Feature panel │ │ │ (search/cart) │ │ │ │ ├──────────────────────┴──────────────────┤ │ Voice controls (mic, end session) │ └─────────────────────────────────────────┘ ``` Use `FeatureProvider` with `initialPosition="right"` and control visibility via `setVisibility`. ## Collection API client Create a thin API layer that uses the Scenaro JWT: ```ts async function apiFetch(path: string, options = {}) { const token = await scenaroClient.getValidUserToken(); return fetch(`${API_URL}/v1${path}`, { ...options, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, ...options.headers, }, }); } ``` ## Product templates Use a shared `ProductTemplate` for consistent row rendering across search and comparison: ```ts // templates/ProductTemplate.ts export function getRowTemplate(templateName: string) { switch (templateName) { case 'wine': return WineRowTemplate; case 'product': return DefaultProductTemplate; default: return DefaultProductTemplate; } } ``` ## Session item map Track which products the user has viewed in the current session (used by display and comparison): ```ts // shared/session-item-map.ts export const sessionItemMap = new Map(); ``` ## End-of-conversation workflow Register `workflow:conversation:end` to handle checkout redirects: ```ts ['workflow:conversation:end', { component: EndConversationFeature, handlers: { end_conversation: endHandler }, primaryTool: 'end_conversation', }], ``` ## Cockpit configuration 1. Connect your product collection in Cockpit 2. Enable all four features with appropriate tool settings 3. Set `return_fields` on search to limit LLM context size 4. Configure the agent prompt for shopping flow ## Production experiences Reference implementations in the monorepo: | Experience | Features | |------------|----------| | `experiences/lupi` | Full e-commerce set | | `experiences/lepetitballon` | Search + comparison + bucket | | `experiences/urbansider` | Search + display + custom tool handlers | ## Next steps - [Product search](/guides/recipes/product-search) — deep dive on search handler - [Production checklist](/guides/production-checklist) --- # Multi-language sessions Source: https://docs.scenaro.io/guides/recipes/multi-language/ Run Scenaro voice sessions in multiple languages by aligning Cockpit configuration, frontend language tags, and UI translations. ## Frontend language tag Pass a BCP-47 language tag to `useScenaroSession`: ```tsx // French session useScenaroSession({ scenario: SCENARIO_UUID, language: 'fr-FR' }); // English session useScenaroSession({ scenario: SCENARIO_UUID, language: 'en-US' }); ``` This sets `language` on `POST /v1/public/session` and configures STT/TTS for the session. `language` is optional. If you omit it, the session uses the language configured on the scenario in Cockpit — the SDK no longer defaults to `fr-FR`. Only pass a tag when the user explicitly picks a language. ## Cockpit setup For each language: 1. Create or duplicate the scenario with a localized system prompt 2. Publish a separate publication UUID per language (or use dynamic language if supported) 3. Configure voice stack voices appropriate for the target language ## Frontend i18n Feature components should use your app's i18n system: ```tsx import { useTranslation } from '../hooks/useTranslation'; export function SearchFeature({ toolState }) { const { t } = useTranslation(); const items = toolState.items ?? []; return (

{t('search.results_title')}

{items.length === 0 &&

{t('search.no_results')}

} {items.map((item) => )}
); } ``` ## Language selector pattern ```tsx const LANGUAGES = [ { tag: 'fr-FR', label: 'Français' }, { tag: 'en-US', label: 'English' }, { tag: 'de-DE', label: 'Deutsch' }, ]; function LanguageSelector({ onSelect }) { return ( ); } // In your widget — undefined means "use the scenario's language": const [language, setLanguage] = useState(); const { start } = useScenaroSession({ scenario: SCENARIO_UUID, language }); ``` ## Prompt guidelines per language | Rule | Reason | |------|--------| | Write the system prompt in the target language | LLM responds in the prompt language | | Keep tool names in English | Tool names are code identifiers, not spoken | | Localize feature `name` fields in Cockpit | Agent uses feature names in conversation | | Test STT with native accents | Recognition quality varies by provider and locale | ## Text input for non-voice contexts For accessibility or noisy environments, disable the microphone and send text instead: ```tsx const { start, sendText, setMicrophoneEnabled } = useScenaroSession({ scenario: SCENARIO_UUID, language: 'fr-FR', }); await start(); setMicrophoneEnabled(false); sendText('Bonjour !'); ``` ## Experience reference `experiences/urbansider` supports multiple languages with `useTranslation` hooks and per-locale Cockpit scenarios. --- # Advanced: Transport extensions Source: https://docs.scenaro.io/guides/advanced-transport-integration/ `@scenaro/sdk` separates a **portable session contract** from the **Transport** that carries it — see [Transport](/introduction/transport) for the full picture. For the majority of integrations — a voice button embedded on a page — the [quickstart](/introduction/quickstart) is all you need, and you'll never see an engine-specific type. This guide is for the minority of integrations that need more: custom audio visualizers, avatars, multi-participant layouts, or anything else the current engine's ecosystem already does well. This is an assumed extension point, not an escape hatch you're forced into. Scenaro doesn't wrap or re-export the engine's components — you import them directly from their own packages and use them next to `useScenaroSession`. ## 1. When to use this guide Reach for Transport extensions when you need: - Custom audio visualizers or waveforms - Agent avatars driven by speaking state - Multi-participant layouts - Anything else already solved by the current engine's component library If you just need `status`, `messages`, `config`, or tool calls, stay on the portable contract — you don't need anything in this guide. ## 2. Portable contract vs engine extension `useScenaroSession` always returns the same portable contract (`status`, `agentState`, `messages`, `config`, `tools`, events) regardless of which Transport implementation is running underneath. On top of that, it also exposes `room` — an **engine extension**: the live engine handle for the current Transport implementation. | Contract | Stability | Where it's documented | |---|---|---| | `status`, `agentState`, `messages`, `config`, `tools`, events | Portable — stable across future Transport implementations | [Quickstart](/introduction/quickstart), [SDK reference](/sdk-reference/react) | | `room`, `RoomContext`, `@livekit/components-react` hooks/components | Engine-specific (LiveKit today) — not guaranteed to carry over to a future Transport implementation | This guide | Code that reads `status` or `agentState` keeps working if Scenaro ever adds another Transport implementation (e.g. Pipecat) behind `useScenaroSession`. Code that calls `room.localParticipant...` or uses `useVoiceAssistant()` is written against the current engine directly — the same trade-off you'd make using that engine standalone. See [Transport](/introduction/transport) for the internal boundary that makes this possible. ## 3. LiveKit implementation (today) The current Transport implementation is [LiveKit](https://livekit.io). This section is the concrete, engine-specific how-to. ### Install the LiveKit peer dependencies `livekit-client` and `@livekit/components-react` are peer dependencies of `@scenaro/sdk` — install them once you start using this guide (the [quickstart](/introduction/quickstart) does not require them): ```bash npm npm install livekit-client @livekit/components-react ``` ```bash pnpm pnpm add livekit-client @livekit/components-react ``` ### Using `room` with LiveKit components `ScenaroProvider` injects `RoomContext` for you, so LiveKit's components and hooks work as soon as a session is active — no manual `` wiring required. ```tsx import { useScenaroSession } from '@scenaro/sdk/react'; import { useVoiceAssistant, BarVisualizer, RoomAudioRenderer } from '@livekit/components-react'; function AssistantAvatar() { const { status, room } = useScenaroSession({ scenario: SCENARIO_UUID }); // portable contract const { state, audioTrack } = useVoiceAssistant(); // LiveKit, direct if (status !== 'connected' || !room) return null; return ( <> ); } ``` Scenaro never re-exports these components. When LiveKit ships something new — Agents UI, a new Session API, a new visualizer — you use it immediately, without waiting for a Scenaro equivalent. ### What stays yours to maintain Because this code is written against LiveKit's API surface directly, keep in mind: - **A future engine swap isn't automatic here.** If Scenaro adds a Pipecat Transport implementation behind `useScenaroSession` in the future, the portable contract will keep working unchanged. Code that calls `room.*` or LiveKit component hooks will need to be ported by hand — the same way any code written against a specific vendor's low-level API would. - **Support boundary.** Scenaro's contract covers session, auth, config, tools, and features. Anything under `room` — audio devices, track subscriptions, participant behavior — is LiveKit's surface; consult the [LiveKit docs](https://docs.livekit.io) for it. - **Don't mix contracts in shared components.** Keep components that only need the portable contract free of LiveKit imports, so they stay reusable if the Transport implementation ever changes. ## Next steps - [Transport](/introduction/transport) — the engine-agnostic boundary behind this guide - [SDK reference — React](/sdk-reference/react) — full `useScenaroSession` and `ScenaroSession` API - [LiveKit Components React docs](https://docs.livekit.io/reference/components/react/) — hooks and components available on `room` --- # Production checklist Source: https://docs.scenaro.io/guides/production-checklist/ Use this checklist before shipping a Scenaro integration to production. ## Environment | Variable | Required | Example | |----------|----------|---------| | API URL | Yes | `https://api.scenaro.io` | | Publication UUID | Yes | Cockpit production publication | | `apiPathPrefix` | Check | `'/v1'` or `''` if URL includes `/v1` | Double-check `apiPathPrefix`. A common bug is `/v1/v1/public/token` when the base URL already includes `/v1`. ## Security - [ ] HTTPS on your site (required for microphone) - [ ] Publication UUID is the production publication, not staging - [ ] No admin API keys in frontend code - [ ] `user_token` JWT is stored in localStorage only (never logged to analytics) ## Dependencies - [ ] `@scenaro/sdk` version pinned in `package.json` - [ ] `react`, `react-dom` installed - [ ] If using [Transport extensions](/guides/advanced-transport-integration): `livekit-client`, `@livekit/components-react` `>=2.0.0` installed ## Feature registry - [ ] Every tool in Cockpit config has a matching registry entry - [ ] Handlers return within 10s (`RPC_TIMEOUTS.FRONTEND_TOOL_MS`) - [ ] Error responses use `code: 'ERROR'` with agent-friendly data ## Session lifecycle - [ ] `end()` called on page unload or navigation (optional `beforeunload` handler) - [ ] `onSessionEnd` resets feature UI state - [ ] `status === 'reconnecting'` surfaced to the user (brief indicator) - [ ] `ScenaroError` codes handled: `MIC_PERMISSION_DENIED`, `CONNECTION_LOST`, `AGENT_UNAVAILABLE` ## Performance - [ ] `ScenaroProvider` mounted once at the app root — the identity JWT is cached and reused across sessions - [ ] Feature components lazy-loaded if heavy - [ ] Search results limited via Cockpit `limit` setting ## Browser support - [ ] Tested Chrome, Safari, Firefox (desktop + mobile) - [ ] Microphone permission flow handled gracefully - [ ] Fallback message when WebRTC blocked ## Monitoring - [ ] Client-side error logging for connection failures - [ ] Track session start/end events in your analytics - [ ] Monitor API error rates on `/public/token` and `/public/session` ## Build ```bash # Verify SDK builds in your CI pnpm --filter @scenaro/sdk build pnpm --filter @scenaro/sdk test ``` For Next.js: ```js transpilePackages: ['@scenaro/sdk'], ``` ## Deployment - [ ] Mintlify docs URL in your README: `https://docs.scenaro.io` - [ ] Changelog reviewed for breaking changes: [Changelog](/changelog) --- # Migration from vendored SDK Source: https://docs.scenaro.io/guides/migration-from-vendored-sdk/ There are two migration paths, depending on where your project is today: 1. **Vendored `src/lib/scenaro-sdk/` folder** → published `@scenaro/sdk` package 2. **Pre-0.3 API** (`useScenarioSession` + `AgentListener`) → `useScenaroSession` Both end at the same place: one provider, one hook, no transport wiring. ## Before and after | Vendored | @scenaro/sdk 0.1–0.2 | @scenaro/sdk 0.3 | |----------|----------------------|------------------| | `ScenarioPlayer` monolith | `useScenarioSession` + `AgentListener` + `RoomContext` | `useScenaroSession` (all-in-one) | | `generateToken()` one-shot | `identifyUser()` + `startSession()` | Handled by `start()` | | `window.__` event bus | `useScenaroEvents()` scoped bus | `useScenaroEvents()` (unchanged) | | Copied protocol files | `@scenaro/sdk/protocol` | `@scenaro/sdk/protocol` (unchanged) | | 8 divergent copies | One npm package | One npm package | ## Migration steps ```bash pnpm add @scenaro/sdk livekit-client @livekit/components-react ``` ```ts // Before (vendored) import { scenariosAPI } from './lib/scenaro-sdk/api/scenarios'; // After import { createScenaroClient } from '@scenaro/sdk/client'; export const scenaroClient = createScenaroClient({ apiUrl: import.meta.env.VITE_SCENARO_API_URL, apiPathPrefix: '/v1', }); ``` ```tsx // Before (vendored) import { SdkSessionProvider } from './lib/scenaro-sdk/SdkSessionProvider'; // After import { ScenaroProvider, FeatureProvider } from '@scenaro/sdk/react'; ``` ```tsx // Before (vendored) import useScenarioSession from './lib/scenaro-sdk/hooks/useScenarioSession'; // Before (0.1–0.2 — removed in 0.3.0) import { useScenarioSession, AgentListener } from '@scenaro/sdk/react'; // After (0.3) import { useScenaroSession } from '@scenaro/sdk/react'; const { status, start, end } = useScenaroSession({ scenario: PUBLICATION_UUID }); ``` `useScenaroSession` handles the data channel, config handshake, tool RPC, and `RoomContext` injection internally. Delete these from your components: ```tsx // Before (0.1) — all of this goes away {isConnected && } {/* ... */} ``` Remove any `generateToken` usage. The hook runs identify + session start automatically on `start()`. ```tsx // Before (vendored) window.__.on('config', handler); // After const events = useScenaroEvents(); events.on('config', handler); ``` Delete duplicated `AgentListener`, `protocol/`, and `auth/` files. Keep only feature components and your registry. ## Pre-0.3 API mapping (historical) These exports were **removed in 0.3.0**. If you still import them, upgrade to 0.3. | Removed (`useScenarioSession`) | Replacement (`useScenaroSession`) | |--------------------------------|-----------------------------------| | `connect({ language })` | `start()` — pass `language` as a hook option | | `disconnect()` | `end()` | | `isConnected` / `isConnecting` | `status` (`'connected'`, `'connecting'`, …) | | `error` (string) | `error` (`ScenaroError` with `code`) | | `` | Built in — remove it | | `RoomContext.Provider` | Built in — remove it | | `useConversationMessages()` | `messages` from the hook | | `room` | `room` (advanced only — see [Transport extensions](/guides/advanced-transport-integration)) | | `runtimeConfig` | `rawConfig` from the hook | ## What to keep locally | Keep in your app | Provided by @scenaro/sdk | |------------------|--------------------------| | Feature components | `ScenaroProvider` | | Feature registry / local FeatureManager | `useScenaroSession` | | Brand CSS | Data channel + session tool RPC | | Collection API clients | Protocol types | | Product templates | Session storage | ## Storage migration Since `@scenaro/sdk@0.1.1`, legacy storage keys are migrated automatically on read. No manual migration needed for users with existing sessions. ## Verify ```bash pnpm build # Test: start() → status 'connected' → config received → tool call → end() ``` ## Need help? - [Quickstart](/introduction/quickstart) - [Official docs](https://docs.scenaro.io) - [GitHub issues](https://github.com/scenaro/projects/issues) --- # @scenaro/sdk/react Source: https://docs.scenaro.io/sdk-reference/react/ React bindings for Scenaro voice sessions. Peer dependencies: `react`, `react-dom`, `livekit-client`, `@livekit/components-react` — the latter two back the current [Transport](/introduction/transport) implementation and are only required at runtime if you use [Transport extensions](/guides/advanced-transport-integration). ```tsx import { ScenaroProvider, useScenaroSession, FeatureProvider, useFeatures, } from '@scenaro/sdk/react'; ``` ## ScenaroProvider Owns the shared [`ScenaroSession`](/sdk-reference/session) and the API client. Also injects LiveKit's `RoomContext` (the current [Transport](/introduction/transport) implementation) so `@livekit/components-react` hooks work without extra wiring — see [Advanced: Transport extensions](/guides/advanced-transport-integration). ```tsx {children} ``` | Prop | Type | Default | Description | |------|------|---------|-------------| | `apiUrl` | `string` | — | API base URL (or pass `client`) | | `client` | `ScenaroClient` | — | Pre-built client instance | | `apiPathPrefix` | `string` | `'/v1'` | Prepended to all endpoints | | `storagePrefix` | `string` | `''` | Prefix for localStorage keys | | `storage` | `StorageAdapter \| null` | `localStorage` | Custom storage adapter | Throws if neither `client` nor `apiUrl` is provided. The session's transport listeners are released automatically when the provider unmounts. ## useScenaroSession Single entry point for the session contract. Must be called under a `ScenaroProvider`. ```tsx const { status, agentState, messages, config, conversationId, error, start, end, sendText, setMicrophoneEnabled, room, } = useScenaroSession({ scenario: 'publication-uuid', tools: { search_products: async ({ query }) => searchCatalog(query), }, onMessage: (message) => {}, onError: (error) => {}, onSessionEnd: (info) => {}, }); ``` ### Options `UseScenaroSessionOptions` — everything from [`StartSessionOptions`](/sdk-reference/session#startsessionoptions) plus React callbacks: | Option | Type | Description | |--------|------|-------------| | `scenario` | `string` | Publication UUID (required) | | `language` | `string` | BCP-47 — omit to use the scenario's configured language | | `inputMode` / `outputMode` | `'audio' \| 'text'` | Default `'audio'` | | `microphone` | `boolean` | Enable mic on connect (default `true`) | | `externalId` | `string \| null` | Stable visitor identifier | | `resumeConversationId` | `string` | Resume an earlier conversation | | `metadata` | `Record` | Attached to the session | | `tools` | `Record` | Frontend tool handlers | | `onMessage` | `(message: ConversationMessage) => void` | New conversation message | | `onError` | `(error: ScenaroError) => void` | Typed error callback | | `onSessionEnd` | `(info: SessionEndInfo) => void` | Session ended (any reason) | ### Return value | Field | Type | Contract | |-------|------|----------| | `status` | `'idle' \| 'connecting' \| 'connected' \| 'reconnecting' \| 'ended'` | Portable | | `agentState` | `'idle' \| 'listening' \| 'thinking' \| 'speaking'` | Portable | | `messages` | `ConversationMessage[]` | Portable | | `config` | `SessionConfig \| null` | Portable | | `conversationId` | `string \| null` | Portable | | `error` | `ScenaroError \| null` | Portable | | `start` | `(overrides?) => Promise` | Portable — overrides can't change `scenario` | | `end` | `() => Promise` | Portable | | `sendText` | `(text: string) => Promise` | Portable | | `setMicrophoneEnabled` | `(enabled: boolean) => Promise` | Portable | | `room` | `Room \| null` | **Engine extension** (LiveKit today) — see [Transport extensions](/guides/advanced-transport-integration) | | `rawConfig` | `ConfigPayload \| null` | Wire-format config (`restore_state`, tool wiring) | ### Tool handlers Plain functions. Return value is serialized back to the agent; throwing produces an `ERROR` response; exceeding the RPC timeout produces `TIMEOUT` and aborts `context.signal`. ```ts type ToolHandler = ( args: TArgs, context: { toolName: string; settings: Record; // from the scenario config collections: Collection[]; // typed — see @scenaro/sdk/protocol signal: AbortSignal; // aborted on timeout }, ) => TResult | Promise; ``` ## FeatureProvider Wires tool calls to UI components via an injectable registry. See [Feature registry](/build/feature-registry). | Prop | Type | Description | |------|------|-------------| | `registry` | `Map` | Feature registry | | `onResetBuckets` | `() => void` | Called on session reset | | `clearHistoryOnSessionEnd` | `boolean` | Clear feature history on end | | `initialVisibility` | `boolean` | Panel visible by default | | `initialPosition` | `'left' \| 'right' \| 'mobile' \| 'hidden'` | Panel position | ### useFeatures Returns `FeatureRuntimeContextValue`: | Method | Description | |--------|-------------| | `executeTool(name, requestId, args)` | Run a tool handler | | `selectFeature(name)` | Activate a feature panel | | `setVisibility(visible, position)` | Show/hide panel | | `getFeatureComponent(name)` | Get React component | | `getToolMapping(toolName)` | Resolve tool → feature | | `navigateHistory(direction)` | History back/forward | | `reset()` | Reset all state | ## Context hooks | Hook | Returns | |------|---------| | `useScenaro()` | `{ client, events, session }` | | `useScenaroClient()` | `ScenaroClient` | | `useScenaroEvents()` | `ScenaroEventBus` | --- # ScenaroSession Source: https://docs.scenaro.io/sdk-reference/session/ `ScenaroSession` is the framework-agnostic core of the SDK. `useScenaroSession` is a thin React binding over it — if you're not using React (vanilla JS, Vue, Svelte…), use this class directly. ```ts import { ScenaroSession } from '@scenaro/sdk'; const session = new ScenaroSession({ apiUrl: 'https://api.scenaro.io' }); session.on('status-change', (status) => console.log(status)); session.on('message', (message) => render(message)); await session.start({ scenario: 'publication-uuid', tools: { search_products: async ({ query }) => searchCatalog(query), }, }); ``` ## Constructor ```ts new ScenaroSession(options: ScenaroSessionOptions) ``` | Option | Type | Default | Description | |--------|------|---------|-------------| | `apiUrl` | `string` | — | API base URL (or pass `client`) | | `client` | `ScenaroClient` | — | Pre-built client instance | | `apiPathPrefix` | `string` | `'/v1'` | Prepended to all endpoints | | `storagePrefix` | `string` | `''` | Prefix for localStorage keys | | `storage` | `StorageAdapter \| null` | `localStorage` | Custom storage adapter | | `transport` | `Transport` | `LiveKitTransport` | Internal, non-public interface — injectable for tests | Throws if neither `client` nor `apiUrl` is provided. ## State All state is readable synchronously; changes are announced through [events](#events). | Getter | Type | |--------|------| | `status` | `'idle' \| 'connecting' \| 'connected' \| 'reconnecting' \| 'ended'` | | `agentState` | `'idle' \| 'listening' \| 'thinking' \| 'speaking'` | | `messages` | `ConversationMessage[]` | | `config` | `SessionConfig \| null` | | `rawConfig` | `ConfigPayload \| null` — wire format (`restore_state`, tool wiring) | | `conversationId` | `string \| null` | | `room` | `unknown` — **engine extension** (LiveKit today), see [Transport extensions](/guides/advanced-transport-integration) | ## Methods ### start ```ts start(options: StartSessionOptions): Promise ``` Identifies the visitor, starts a session, registers tools, and connects Transport. Throws `ScenaroError` with code `IDENTIFY_FAILED` or `SESSION_START_FAILED`. #### StartSessionOptions | Field | Type | Default | |-------|------|---------| | `scenario` | `string` | required | | `language` | `string` | scenario's configured language | | `inputMode` / `outputMode` | `'audio' \| 'text'` | `'audio'` | | `microphone` | `boolean` | `true` | | `externalId` | `string \| null` | auto-generated | | `resumeConversationId` | `string` | — | | `metadata` | `Record` | — | | `tools` | `Record` | `{}` | ### end ```ts end(): Promise ``` Unregisters tools, disconnects Transport, and sets `status` to `'ended'`. The session can be started again with `start()`. ### sendText / setMicrophoneEnabled ```ts sendText(text: string): Promise setMicrophoneEnabled(enabled: boolean): Promise ``` In-session actions never throw — failures are emitted on the `error` event instead, so fire-and-forget call sites don't produce unhandled rejections. ### destroy ```ts destroy(): void ``` Releases Transport listeners and timers. Call when the owning scope is torn down (`ScenaroProvider` does this automatically on unmount). ## Events Kebab-case event names, subscribed with `on` / `off`: | Event | Payload | |-------|---------| | `status-change` | `SessionStatus` | | `agent-state` | `AgentState` | | `message` | `ConversationMessage` | | `config` | `SessionConfig` | | `session-end` | `{ reason: string }` — `'shutdown'`, `'agent_disconnected'`, `'transport_disconnected'` | | `error` | `ScenaroError` | ## Errors ```ts class ScenaroError extends Error { code: 'IDENTIFY_FAILED' | 'SESSION_START_FAILED' | 'CONNECTION_LOST' | 'MIC_PERMISSION_DENIED' | 'TOOL_TIMEOUT' | 'AGENT_UNAVAILABLE'; cause?: unknown; } ``` `start()` throws; everything after connection emits on `error` instead. ## SessionConfig A curated view of the scenario config — backend plumbing (`company_id`, `voice_stack`, timestamps, tool wiring) is deliberately not exposed. ```ts interface SessionConfig { features: SessionFeature[]; // { name, type, settings } collections: Collection[]; // typed — see @scenaro/sdk/protocol avatarEnabled: boolean; audioDictationEnabled: boolean; } ``` --- # @scenaro/sdk/client Source: https://docs.scenaro.io/sdk-reference/client/ Framework-agnostic HTTP client for the Scenaro public API. No React or Transport dependencies. ```ts import { createScenaroClient, ScenaroClient, isTokenExpired, getJWTExpiry, getCompanyIdFromToken, } from '@scenaro/sdk/client'; ``` ## createScenaroClient ```ts function createScenaroClient(config: ScenaroClientConfig): ScenaroClient ``` ### ScenaroClientConfig | Field | Type | Default | Description | |-------|------|---------|-------------| | `apiUrl` | `string` | required | API base URL (no trailing slash) | | `apiPathPrefix` | `string` | `'/v1'` | Prepended to all endpoints | | `storagePrefix` | `string` | `''` | Prefix for localStorage keys | | `storage` | `StorageAdapter \| null` | `localStorage` | Custom storage adapter | ## ScenaroClient methods ### identifyUser ```ts identifyUser( scenarioUUID: string, externalId?: string | null, ): Promise ``` `POST /public/token` — returns JWT identity. Idempotent if valid token exists in storage. ### startSession ```ts startSession( scenarioUUID: string, options?: { inputMode?: string; // default 'audio' outputMode?: string; // default 'audio' language?: string; // omit to use the scenario's configured language sessionOptions?: SessionOptions; }, ): Promise ``` `POST /public/session` — returns Transport credentials (LiveKit today: `livekit_token`, `livekit_url` — see [Transport](/introduction/transport)). Requires stored JWT. When `language` is omitted, the backend applies the language configured on the scenario. ### getValidUserToken ```ts getValidUserToken(): Promise ``` Returns stored token if valid, otherwise silently re-identifies. ### getStoredUserToken ```ts getStoredUserToken( scenarioUUID?: string | null, applicationId?: number | null, ): string | null ``` Synchronous read from storage. ### clearStoredSession ```ts clearStoredSession(scenarioUUID?: string | null): void ``` Clears identity blob and scenario pointer. ### getSessionUserId / getSessionApplicationId ```ts getSessionUserId(): number | null getSessionApplicationId(): number | null ``` Read from stored identity blob. ## JWT utilities ```ts isTokenExpired(token: string): boolean // 90s buffer getJWTExpiry(token: string): number | null // Unix timestamp getCompanyIdFromToken(token: string): number | null ``` ## Storage exports ```ts import { buildStorageKeys, persistIdentity, type StorageAdapter, type SessionStoreKeys, } from '@scenaro/sdk/client'; ``` See [Session storage](/build/session-storage) for key format details. ## Types Re-exported from `@scenaro/sdk/protocol`: - `IdentityResponse` - `PublicTokenRequest` / `PublicTokenResponse` - `PublicSessionRequest` / `PublicSessionResponse` - `SessionOptions` --- # @scenaro/sdk/protocol Source: https://docs.scenaro.io/sdk-reference/protocol/ Zero-dependency protocol package. Shared contract between the Python voice agent and all frontends. ```ts import { DATA_TOPICS, toolDataTopic, RPC_METHODS, RPC_TIMEOUTS, resolveRpcMethodName, type ConfigPayload, type ConversationMessage, } from '@scenaro/sdk/protocol'; ``` ## Topics ```ts const DATA_TOPICS = { CONFIG: 'config', CONFIG_ACK: 'config_ack', CONFIG_REQUEST: 'config_request', MESSAGE_NEW: 'message:new', SESSION_END: 'session:end', DICTATION_TRANSCRIPT: 'dictation:transcript', EVENT_FIRE: 'event:fire', STATE_UPDATE: 'state:update', SESSION_CONTROL: 'session:control', ASSISTANT_INTERRUPT: 'assistant:interrupt', UI_ACTIVITY: 'ui:activity', CHAT: 'lk.chat', } as const; type DataTopic = (typeof DATA_TOPICS)[keyof typeof DATA_TOPICS]; ``` ```ts function toolDataTopic(toolName: string): string // Returns `tool:${toolName}` ``` See [Topics reference](/voice-agent/protocol/topics) for the full table. ## Message types | Type | Used on topic | |------|---------------| | `ConfigPayload` | `config` | | `ConfigAckPayload` | `config_ack` | | `ConfigRequestPayload` | `config_request` | | `ConversationMessage` | `message:new` | | `SessionEndPayload` | `session:end` | | `PublicTokenRequest` / `PublicTokenResponse` | HTTP `/public/token` | | `PublicSessionRequest` / `PublicSessionResponse` | HTTP `/public/session` | | `IdentityResponse` | Client identity | | `SessionOptions` | Session options | | `RestoreState` | Config restore | | `FeatureDefinition` | Config features | | `ToolConfig` | Config tools | | `Collection` | Config collections | See [Message types](/voice-agent/protocol/messages) for field details. ## Collection Content collections attached to the scenario, compiled server-side into the published version and delivered on the `config` topic: ```ts interface Collection { id: number; name: string; description: string; company_id: number; default_view_id?: number; view?: { template: string; // e.g. "product" mapping: Record; // e.g. { title: "{{source.name}}" } }; search_fields?: Array<{ name: string; index_type: string }>; filter_fields?: Array<{ name: string; filter_key: string; field_type: string; enum_options?: Array<{ value: string; label: string }>; }>; } ``` ## RPC ```ts const RPC_METHODS = { UI_ACTIVITY_CHECK: 'scenaro__ui_activity_check', COMPOSER_START_DICTATION: 'composer_start_dictation', COMPOSER_END_DICTATION: 'composer_end_dictation', COMPOSER_CANCEL_DICTATION: 'composer_cancel_dictation', } as const; const RPC_TIMEOUTS = { AGENT_DEFAULT_MS: 8000, FRONTEND_TOOL_MS: 10000, UI_ACTIVITY_MS: 3000, COMPOSER_START_MS: 10000, COMPOSER_END_MS: 15000, COMPOSER_CANCEL_MS: 8000, } as const; ``` ```ts function resolveRpcMethodName(tool: { name: string; frontend_method?: string; }): string ``` ### RPC payload types ```ts interface RpcToolRequest { requestId: string; tool?: string; args?: Record; sentAt?: number; } interface RpcToolResponse { requestId?: string; code: 'SUCCESS' | 'ERROR' | 'TIMEOUT' | 'CANCELLED' | string; data?: unknown; } ``` See [RPC contracts](/voice-agent/protocol/rpc). ## Multi-language spec The protocol package is the source of truth for cross-language SDKs. Future `scenaro-protocol` extraction (Python, Go) will align on these same definitions. Source: `packages/sdk/src/protocol/` in the [scenaro/projects](https://github.com/scenaro/projects) monorepo.