From fab67b5765da4419d5ce23964ec67924235ab00c Mon Sep 17 00:00:00 2001 From: Sunil Pai Date: Tue, 14 Apr 2026 10:42:20 +0100 Subject: [PATCH 1/2] Docs: add Voice agents and update browser tools Add a new Voice agents API reference and a Build-a-voice-agent guide; introduce real-time voice agent docs (withVoice, withVoiceInput, client hooks, providers, Telephony, examples, and Durable Object config). Rewrite and expand the Browse the web doc into a browser tools reference (CDP-based browser_search/browser_execute tools, usage, examples, CDP helper API, security considerations, limitations, Puppeteer/Browserbase examples, and config). Also update agents index to reference browser tools and voice agents and make small example/content tweaks. --- .../agents/api-reference/browse-the-web.mdx | 347 ++++++++- .../docs/agents/api-reference/voice.mdx | 730 ++++++++++++++++++ .../agents/guides/build-a-voice-agent.mdx | 299 +++++++ src/content/docs/agents/index.mdx | 3 +- 4 files changed, 1345 insertions(+), 34 deletions(-) create mode 100644 src/content/docs/agents/api-reference/voice.mdx create mode 100644 src/content/docs/agents/guides/build-a-voice-agent.mdx diff --git a/src/content/docs/agents/api-reference/browse-the-web.mdx b/src/content/docs/agents/api-reference/browse-the-web.mdx index 16dbdd88e74..65181298be8 100644 --- a/src/content/docs/agents/api-reference/browse-the-web.mdx +++ b/src/content/docs/agents/api-reference/browse-the-web.mdx @@ -1,30 +1,315 @@ --- title: Browse the web -pcx_content_type: concept +pcx_content_type: reference sidebar: order: 15 --- import { - MetaInfo, - Render, - Type, + InlineBadge, TypeScriptExample, WranglerConfig, PackageManagers, } from "~/components"; -Agents can browse the web using the [Browser Rendering](/browser-rendering/) API or your preferred headless browser service. +Give your agents full access to the Chrome DevTools Protocol (CDP) with browser tools. -### Browser Rendering API +Instead of a fixed set of browser actions (click, screenshot, navigate), the LLM writes JavaScript code that runs CDP commands against a live browser session — accessing all domains, commands, events, and types in the protocol. -The [Browser Rendering](/browser-rendering/) allows you to spin up headless browser instances, render web pages, and interact with websites through your Agent. +Two tools are provided: -You can define a method that uses Puppeteer to pull the content of a web page, parse the DOM, and extract relevant information by calling a model via [Workers AI](/workers-ai/): +| Tool | Description | +| --- | --- | +| `browser_search` | Query the CDP spec to discover commands, events, and types. The spec is fetched dynamically from the browser's CDP endpoint and cached. | +| `browser_execute` | Run CDP commands against a live browser via a `cdp` helper. Each call opens a fresh browser session, executes the code, and closes it. | + +## When to use browser tools + +Browser tools are useful when your agent needs to: + +- **Inspect web pages** — DOM structure, computed styles, accessibility tree +- **Debug frontend issues** — network waterfalls, console errors, performance traces +- **Scrape structured data** — extract content from rendered pages +- **Capture screenshots or PDFs** — visual snapshots of web content +- **Profile performance** — Core Web Vitals, JavaScript profiling, memory analysis + +For basic page fetches that do not need a rendered DOM, use `fetch()` instead. + +## Install + +Browser tools require the Agents SDK and `@cloudflare/codemode`: + +```sh +npm install agents @cloudflare/codemode ai zod +``` + +## Quick start + +### 1. Configure bindings + +Add the Browser Rendering and Worker Loader bindings to your wrangler configuration: + + + +```toml +[browser] +binding = "BROWSER" + +[[worker_loaders]] +binding = "LOADER" + +compatibility_flags = ["nodejs_compat"] +``` + + + +### 2. Create browser tools + + + +```ts +import { createBrowserTools } from "agents/browser/ai"; + +const browserTools = createBrowserTools({ + browser: env.BROWSER, + loader: env.LOADER, +}); +``` + + + +To connect to a custom CDP endpoint instead of the Browser Rendering binding, pass `cdpUrl`. + +### 3. Use with streamText + +Pass browser tools alongside your other tools. The `model` can be any AI SDK provider — here using Workers AI: + + + +```ts +import { streamText } from "ai"; +import { createWorkersAI } from "workers-ai-provider"; + +const workersai = createWorkersAI({ binding: env.AI }); + +const result = streamText({ + model: workersai("@cf/zai-org/glm-4.7-flash"), + system: "You are a helpful assistant that can inspect web pages.", + messages, + tools: { + ...browserTools, + ...otherTools, + }, +}); +``` + + + +Both tools accept a `code` parameter containing a JavaScript async arrow function. The sandbox injects globals depending on the tool — `spec` for `browser_search` and `cdp` for `browser_execute`. + +When the LLM uses `browser_search`, the code queries the CDP spec via the injected `spec` object: + +```js +async () => { + const s = await spec.get(); + return s.domains + .find((d) => d.name === "Network") + .commands.map((c) => ({ method: c.method, description: c.description })); +}; +``` + +When the LLM uses `browser_execute`, the code runs CDP commands via the injected `cdp` helper: + +```js +async () => { + const { targetId } = await cdp.send("Target.createTarget", { + url: "https://example.com", + }); + const sessionId = await cdp.attachToTarget(targetId); + const { root } = await cdp.send("DOM.getDocument", {}, { sessionId }); + const { outerHTML } = await cdp.send( + "DOM.getOuterHTML", + { nodeId: root.nodeId }, + { sessionId }, + ); + await cdp.send("Target.closeTarget", { targetId }); + return outerHTML; +}; +``` + +## Use with an Agent + +The typical pattern is to create browser tools inside an [`AIChatAgent`](/agents/api-reference/chat-agents/) message handler, which gives you message persistence and streaming: + + + +```ts +import { AIChatAgent } from "@cloudflare/ai-chat"; +import { createBrowserTools } from "agents/browser/ai"; +import { createWorkersAI } from "workers-ai-provider"; +import { streamText, convertToModelMessages, stepCountIs } from "ai"; + +export class MyAgent extends AIChatAgent { + async onChatMessage() { + const workersai = createWorkersAI({ binding: this.env.AI }); + const browserTools = createBrowserTools({ + browser: this.env.BROWSER, + loader: this.env.LOADER, + }); + + const result = streamText({ + model: workersai("@cf/zai-org/glm-4.7-flash"), + system: "You can browse the web and inspect pages.", + messages: await convertToModelMessages(this.messages), + tools: { + ...browserTools, + }, + stopWhen: stepCountIs(10), + }); + + return result.toUIMessageStreamResponse(); + } +} +``` + + + +## TanStack AI + +For TanStack AI, use the `/tanstack-ai` export: ```ts +import { createBrowserTools } from "agents/browser/tanstack-ai"; +import { chat, workersAIText } from "@tanstack/ai"; + +const browserTools = createBrowserTools({ + browser: env.BROWSER, + loader: env.LOADER, +}); + +const stream = chat({ + adapter: workersAIText(env.AI, "@cf/zai-org/glm-4.7-flash"), + tools: [...browserTools, ...otherTools], + messages, +}); +``` + + + +## Execution model + +- `browser_search` fetches the live CDP protocol from the browser's `/json/protocol` endpoint and caches it briefly. +- `browser_execute` opens a fresh browser session for each call, exposes a small `cdp` helper API to sandboxed code, and closes the session when execution finishes. +- LLM-generated code runs in a Worker sandbox. CDP traffic stays in the host Worker. + +## CDP helper API + +Inside `browser_execute`, the following functions are available to the sandboxed code. + +### `cdp.send(method, params?, options?)` + +Send a CDP command and wait for the response. + +| Parameter | Type | Description | +| --- | --- | --- | +| `method` | `string` | CDP method, for example `"DOM.getDocument"` or `"Network.enable"` | +| `params` | `unknown` | Method parameters | +| `options.timeoutMs` | `number` | Per-command timeout (default: 10 seconds) | +| `options.sessionId` | `string` | Target session ID (required for page-scoped commands) | + +### `cdp.attachToTarget(targetId, options?)` + +Attach to a target and get a session ID. Uses `Target.attachToTarget` with `flatten: true`. + +| Parameter | Type | Description | +| --- | --- | --- | +| `targetId` | `string` | The target to attach to | +| `options.timeoutMs` | `number` | Timeout for the attach command | + +Returns the `sessionId` string. + +### `cdp.getDebugLog(limit?)` + +Get recent CDP debug log entries (sends, receives, errors). Defaults to the last 50 entries, max 400. + +### `cdp.clearDebugLog()` + +Clear the debug log buffer. + +## Configuration + +### `createBrowserTools(options)` + +Returns AI SDK tools (`browser_search` and `browser_execute`). + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `browser` | `Fetcher` | — | Browser Rendering binding | +| `cdpUrl` | `string` | — | Optional override for a custom CDP endpoint | +| `cdpHeaders` | `Record` | — | Headers for CDP URL discovery (for example, Cloudflare Access) | +| `loader` | `WorkerLoader` | required | Worker Loader binding for sandboxed execution | +| `timeout` | `number` | `30000` | Execution timeout in milliseconds | + +Either `browser` or `cdpUrl` must be provided. When both are set, `cdpUrl` takes priority. + +### Raw access + +For custom integrations, import the building blocks directly: + + + +```ts +import { + CdpSession, + connectBrowser, + connectUrl, + createBrowserToolHandlers, +} from "agents/browser"; + +// Connect to a custom CDP endpoint +const session = await connectUrl("http://localhost:9222"); +const version = await session.send("Browser.getVersion"); +session.close(); +``` + + + +## Local development + +Recent Wrangler releases support Browser Rendering in local development. `npx wrangler dev` provisions the browser automatically, so the same `browser: env.BROWSER` setup works locally and when deployed. + +Use `cdpUrl` only when you intentionally want to connect to some other CDP-compatible browser endpoint, such as a tunnel or a manually managed Chrome instance. + +## Security considerations + +- LLM-generated code runs in **isolated Worker sandboxes** — each execution gets its own Worker instance +- External network access (`fetch`, `connect`) is **blocked** in the sandbox at the runtime level +- CDP commands are dispatched via Workers RPC — the WebSocket lives in the host, not the sandbox +- The CDP spec stays on the server — only query results flow to the LLM +- Responses are truncated to approximately 6,000 tokens to prevent context window overflow + +## Current limitations + +- **One session per execute call** — each `browser_execute` invocation opens a fresh browser session. Multi-step workflows must be completed within a single code block. +- **No authenticated sessions** — the browser starts without any cookies or login state. +- Requires `@cloudflare/codemode` as a peer dependency. +- Limited to JavaScript execution in the sandbox (no TypeScript syntax). + +--- + +## Using Puppeteer directly + +If you prefer to control the browser programmatically without LLM-generated code, you can use Puppeteer with the [Browser Rendering](/browser-rendering/) API directly. + + + + + +```ts +import puppeteer from "@cloudflare/puppeteer"; + interface Env { BROWSER: Fetcher; AI: Ai; @@ -48,7 +333,7 @@ export class MyAgent extends Agent { messages: [ { role: "user", - content: `Return a JSON object with the product names, prices and URLs with the following format: { "name": "Product Name", "price": "Price", "url": "URL" } from the website content below. ${bodyContent}`, + content: `Return a JSON object with the product names, prices and URLs from the website content below. ${bodyContent}`, }, ], }); @@ -64,28 +349,21 @@ export class MyAgent extends Agent { -You'll also need to add install the `@cloudflare/puppeteer` package and add the following to the wrangler configuration of your Agent: - - +Add the browser binding to your wrangler configuration: -```jsonc -{ - // ... - "ai": { - "binding": "AI", - }, - "browser": { - "binding": "MYBROWSER", - }, - // ... -} +```toml +[ai] +binding = "AI" + +[browser] +binding = "MYBROWSER" ``` -### Browserbase +## Using Browserbase You can also use [Browserbase](https://docs.browserbase.com/integrations/cloudflare/typescript) by using the Browserbase API directly from within your Agent. @@ -96,12 +374,6 @@ cd your-agent-project-folder npx wrangler@latest secret put BROWSERBASE_API_KEY ``` -```sh output -Enter a secret value: ****** -Creating the secret for the Worker "agents-example" -Success! Uploaded secret BROWSERBASE_API_KEY -``` - Install the `@cloudflare/puppeteer` package and use it from within your Agent to call the Browserbase API: @@ -109,13 +381,22 @@ Install the `@cloudflare/puppeteer` package and use it from within your Agent to ```ts +import puppeteer from "@cloudflare/puppeteer"; + interface Env { BROWSERBASE_API_KEY: string; } -export class MyAgent extends Agent { - constructor(env: Env) { - super(env); +export class MyAgent extends Agent { + async browse(url: string) { + const browser = await puppeteer.connect({ + browserWSEndpoint: `wss://connect.browserbase.com?apiKey=${this.env.BROWSERBASE_API_KEY}`, + }); + const page = await browser.newPage(); + await page.goto(url); + const content = await page.content(); + await browser.close(); + return content; } } ``` diff --git a/src/content/docs/agents/api-reference/voice.mdx b/src/content/docs/agents/api-reference/voice.mdx new file mode 100644 index 00000000000..798f8cb1db4 --- /dev/null +++ b/src/content/docs/agents/api-reference/voice.mdx @@ -0,0 +1,730 @@ +--- +title: Voice agents +pcx_content_type: reference +sidebar: + order: 16 +--- + +import { + InlineBadge, + TypeScriptExample, + WranglerConfig, + PackageManagers, +} from "~/components"; + +Build real-time voice agents with speech-to-text, text-to-speech, and conversation persistence. Audio streams over WebSocket — no SFU or meeting infrastructure required. + +## Overview + +`@cloudflare/voice` provides two server-side mixins and matching client libraries: + +| Export | Import | Purpose | +| --- | --- | --- | +| `withVoice` | `@cloudflare/voice` | Full voice agent: STT, LLM, TTS, persistence | +| `withVoiceInput` | `@cloudflare/voice` | STT-only: transcription without response | +| `useVoiceAgent` | `@cloudflare/voice/react` | React hook for `withVoice` agents | +| `useVoiceInput` | `@cloudflare/voice/react` | React hook for `withVoiceInput` agents | +| `VoiceClient` | `@cloudflare/voice/client` | Framework-agnostic client | + +Built on Cloudflare Durable Objects, you get: + +- **Real-time audio** — mic audio streams as binary WebSocket frames, TTS audio streams back +- **Automatic conversation persistence** — messages stored in SQLite, survive restarts +- **Streaming TTS** — LLM tokens are sentence-chunked and synthesized concurrently +- **Interruption handling** — user speech during playback cancels the current response +- **Continuous STT** — per-call transcriber session, model handles turn detection +- **Pipeline hooks** — intercept and transform text at every stage + +## Quick start + +### Install + +```sh +npm install @cloudflare/voice agents +``` + +### Server + + + +```ts +import { Agent } from "agents"; +import { + withVoice, + WorkersAIFluxSTT, + WorkersAITTS, + type VoiceTurnContext, +} from "@cloudflare/voice"; + +const VoiceAgent = withVoice(Agent); + +export class MyAgent extends VoiceAgent { + transcriber = new WorkersAIFluxSTT(this.env.AI); + tts = new WorkersAITTS(this.env.AI); + + async onTurn(transcript: string, context: VoiceTurnContext) { + return "Hello! I heard you say: " + transcript; + } +} +``` + + + +### Client (React) + +```tsx +import { useVoiceAgent } from "@cloudflare/voice/react"; + +function VoiceUI() { + const { + status, + transcript, + interimTranscript, + audioLevel, + isMuted, + startCall, + endCall, + toggleMute, + } = useVoiceAgent({ agent: "MyAgent" }); + + return ( +
+

Status: {status}

+ + + + + + {interimTranscript && ( +

+ {interimTranscript} +

+ )} + + {transcript.map((msg, i) => ( +

+ {msg.role}: {msg.text} +

+ ))} +
+ ); +} +``` + +### Wrangler configuration + + + +```toml +[ai] +binding = "AI" + +[[durable_objects.bindings]] +name = "MyAgent" +class_name = "MyAgent" + +[[migrations]] +tag = "v1" +new_sqlite_classes = ["MyAgent"] +``` + + + +## How it works + +```txt +Browser Durable Object (withVoice) +┌──────────┐ ┌──────────────────────────┐ +│ Mic │ binary PCM (16kHz) │ Transcriber session │ +│ │ ──────────────────────► │ (per-call, continuous) │ +│ │ │ ↓ model detects turn │ +│ │ JSON: transcript │ onTurn() → your LLM code │ +│ │ ◄────────────────────── │ ↓ (sentence chunking) │ +│ │ binary: audio │ TTS │ +│ Speaker │ ◄────────────────────── │ │ +└──────────┘ └──────────────────────────┘ +``` + +1. The client captures mic audio and sends it as binary WebSocket frames (16kHz mono 16-bit PCM). +2. Audio streams continuously to the transcriber session (created at `start_call`, lives for the entire call). +3. The STT model detects when the user finishes an utterance and fires `onUtterance`. All providers use **model-driven turn detection** — the client does not need to signal end-of-speech for STT. +4. Your `onTurn()` method runs — typically an LLM call. +5. The response is sentence-chunked and synthesized via TTS. +6. Audio streams back to the client for playback. + +The client receives `transcript_interim` messages with partial results as the user speaks, so you can show real-time feedback in the UI. + +## Server API: `withVoice` + +`withVoice(Agent)` adds the full voice pipeline to an Agent class. + +### Providers + +Set providers as class properties. Class field initializers run after `super()`, so `this.env` is available. + +| Property | Type | Required | Description | +| --- | --- | --- | --- | +| `transcriber` | `Transcriber` | Yes | Continuous per-call STT provider | +| `tts` | `TTSProvider` | Yes | Text-to-speech | + + + +```ts +import { withVoice, WorkersAIFluxSTT, WorkersAITTS } from "@cloudflare/voice"; + +const VoiceAgent = withVoice(Agent); + +export class MyAgent extends VoiceAgent { + transcriber = new WorkersAIFluxSTT(this.env.AI); + tts = new WorkersAITTS(this.env.AI); +} +``` + + + +For runtime model switching (for example, a Flux vs Nova 3 dropdown), override `createTranscriber`: + + + +```ts +export class MyAgent extends VoiceAgent { + tts = new WorkersAITTS(this.env.AI); + + createTranscriber(connection: Connection): Transcriber { + return new WorkersAIFluxSTT(this.env.AI); + } +} +``` + + + +### `onTurn(transcript, context)` + +**Required.** Called when the user finishes speaking and the transcript is ready. + +Return a `string`, `AsyncIterable`, or `ReadableStream` for streaming responses. + +**Simple response:** + + + +```ts +export class MyAgent extends VoiceAgent { + transcriber = new WorkersAIFluxSTT(this.env.AI); + tts = new WorkersAITTS(this.env.AI); + + async onTurn(transcript: string, context: VoiceTurnContext) { + return "You said: " + transcript; + } +} +``` + + + +**Streaming response (recommended for LLM):** + + + +```ts +import { streamText } from "ai"; +import { createWorkersAI } from "workers-ai-provider"; + +export class MyAgent extends VoiceAgent { + transcriber = new WorkersAIFluxSTT(this.env.AI); + tts = new WorkersAITTS(this.env.AI); + + async onTurn(transcript: string, context: VoiceTurnContext) { + const workersai = createWorkersAI({ binding: this.env.AI }); + + const result = streamText({ + model: workersai("@cf/moonshotai/kimi-k2.5"), + system: "You are a helpful voice assistant. Keep responses concise.", + messages: [ + ...context.messages.map(m => ({ + role: m.role as "user" | "assistant", + content: m.content, + })), + { role: "user", content: transcript }, + ], + abortSignal: context.signal, + }); + + return result.textStream; + } +} +``` + + + +The `context` object provides: + +| Field | Type | Description | +| --- | --- | --- | +| `connection` | `Connection` | The WebSocket connection | +| `messages` | `Array<{ role: string; content: string }>` | Conversation history from SQLite | +| `signal` | `AbortSignal` | Aborted on interrupt or disconnect | + +### Lifecycle hooks + +| Method | Description | +| --- | --- | +| `beforeCallStart(connection)` | Return `false` to reject the call | +| `onCallStart(connection)` | Called after a call is accepted | +| `onCallEnd(connection)` | Called when a call ends | +| `onInterrupt(connection)` | Called when user interrupts during playback | + +### Pipeline hooks + +Intercept and transform data at each pipeline stage. Return `null` to skip the current utterance. + +| Method | Receives | Can skip? | +| --- | --- | --- | +| `afterTranscribe(transcript, connection)` | STT text | Yes | +| `beforeSynthesize(text, connection)` | Text before TTS | Yes | +| `afterSynthesize(audio, text, connection)` | Audio after TTS | Yes | + + + +```ts +import { type Connection } from "agents"; + +export class MyAgent extends VoiceAgent { + transcriber = new WorkersAIFluxSTT(this.env.AI); + tts = new WorkersAITTS(this.env.AI); + + afterTranscribe(transcript: string, connection: Connection) { + if (transcript.length < 3) return null; + return transcript; + } + + beforeSynthesize(text: string, connection: Connection) { + return text.replace(/\bAI\b/g, "A.I."); + } + + async onTurn(transcript: string, context: VoiceTurnContext) { + return transcript; + } +} +``` + + + +### Convenience methods + +| Method | Description | +| --- | --- | +| `speak(connection, text)` | Synthesize and send audio to one connection | +| `speakAll(text)` | Synthesize and send audio to all connections | +| `forceEndCall(connection)` | Programmatically end a call | +| `saveMessage(role, text)` | Persist a message to conversation history | +| `getConversationHistory()` | Retrieve conversation history from SQLite | + +### Configuration options + +Pass options to `withVoice()` as the second argument: + + + +```ts +const VoiceAgent = withVoice(Agent, { + historyLimit: 20, + audioFormat: "mp3", + maxMessageCount: 1000, +}); +``` + + + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `historyLimit` | `number` | `20` | Max messages loaded for context | +| `audioFormat` | `string` | `"mp3"` | Audio format sent to client | +| `maxMessageCount` | `number` | `1000` | Max messages stored in SQLite | + +## Server API: `withVoiceInput` + +`withVoiceInput(Agent)` adds STT-only voice input — no TTS, no LLM, no response generation. Use this for dictation, search-by-voice, or any UI where you need speech-to-text without a conversational agent. + + + +```ts +import { Agent } from "agents"; +import { withVoiceInput, WorkersAINova3STT } from "@cloudflare/voice"; + +const InputAgent = withVoiceInput(Agent); + +export class DictationAgent extends InputAgent { + transcriber = new WorkersAINova3STT(this.env.AI); + + onTranscript(text: string, connection: Connection) { + console.log("User said:", text); + } +} +``` + + + +### `onTranscript(text, connection)` + +Called after each utterance is transcribed. Override this to process the transcript. + +### Hooks + +`withVoiceInput` supports the same lifecycle hooks as `withVoice`: + +- `beforeCallStart(connection)` — return `false` to reject +- `onCallStart(connection)`, `onCallEnd(connection)`, `onInterrupt(connection)` +- `createTranscriber(connection)` — override for runtime model switching +- `afterTranscribe(transcript, connection)` — filter or transform transcripts + +It does **not** have TTS hooks (`beforeSynthesize`, `afterSynthesize`) or `onTurn`. + +## Client API: React hooks + +### `useVoiceAgent` + +Wraps `VoiceClient` for `withVoice` agents. Manages connection, mic capture, playback, silence detection, and interrupt detection. + +```tsx +import { useVoiceAgent } from "@cloudflare/voice/react"; + +const { + status, // "idle" | "listening" | "thinking" | "speaking" + transcript, // TranscriptMessage[] — conversation history + interimTranscript, // string | null — real-time partial transcript + metrics, // VoicePipelineMetrics | null + audioLevel, // number (0–1) — current mic RMS level + isMuted, // boolean + connected, // boolean — WebSocket connected + error, // string | null + startCall, // () => Promise + endCall, // () => void + toggleMute, // () => void + sendText, // (text: string) => void — bypass STT + sendJSON, // (data: Record) => void + lastCustomMessage, // unknown — last non-voice message from server +} = useVoiceAgent({ + agent: "MyAgent", + name: "default", + host: window.location.host, +}); +``` + +#### Tuning options + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `silenceThreshold` | `number` | `0.04` | RMS below this is silence | +| `silenceDurationMs` | `number` | `500` | Silence duration before `end_of_speech` (ms) | +| `interruptThreshold` | `number` | `0.05` | RMS to detect speech during playback | +| `interruptChunks` | `number` | `2` | Consecutive high-RMS chunks to trigger interrupt | + +Changing tuning options triggers a client reconnect (the connection key includes them). + +### `useVoiceInput` + +Lightweight hook for dictation and voice-to-text. Accumulates user transcripts into a single string. + +```tsx +import { useVoiceInput } from "@cloudflare/voice/react"; + +function Dictation() { + const { + transcript, // string — accumulated text from all utterances + interimTranscript, // string | null — current partial transcript + isListening, // boolean + audioLevel, // number (0–1) + isMuted, // boolean + error, // string | null + start, // () => Promise + stop, // () => void + toggleMute, // () => void + clear, // () => void — clear accumulated transcript + } = useVoiceInput({ agent: "DictationAgent" }); + + return ( +
+