@craftedxp/voice-js 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.js +22 -2
- package/dist/browser.js.map +1 -1
- package/dist/browser.mjs +21 -2
- package/dist/browser.mjs.map +1 -1
- package/dist/embed.iife.js +235 -219
- package/package.json +1 -1
package/dist/browser.js
CHANGED
|
@@ -875,9 +875,17 @@ var BrowserVoiceClient = class {
|
|
|
875
875
|
|
|
876
876
|
// src/webrtc/createWebRtcCall.ts
|
|
877
877
|
async function createWebRtcCall(opts) {
|
|
878
|
+
validateClientToolMap(opts.clientTools)
|
|
878
879
|
const proto = createProtocolState()
|
|
879
880
|
let muted = false
|
|
880
881
|
let ended = false
|
|
882
|
+
const tools = opts.clientTools ?? {}
|
|
883
|
+
const sendControl = (frame) => {
|
|
884
|
+
if (dc?.readyState !== 'open') return
|
|
885
|
+
try {
|
|
886
|
+
dc.send(JSON.stringify(frame))
|
|
887
|
+
} catch {}
|
|
888
|
+
}
|
|
881
889
|
const fireState = (next) => {
|
|
882
890
|
if (proto.state === next) return
|
|
883
891
|
proto.state = next
|
|
@@ -892,8 +900,14 @@ async function createWebRtcCall(opts) {
|
|
|
892
900
|
onAgentTurnStart: () => opts.onAgentTurnStart?.(),
|
|
893
901
|
onAgentTurnEnd: () => {},
|
|
894
902
|
onCallEnd: () => teardown(),
|
|
895
|
-
onConnected: () => {
|
|
896
|
-
|
|
903
|
+
onConnected: () => {
|
|
904
|
+
if (Object.keys(tools).length > 0) {
|
|
905
|
+
sendControl(buildRegisterFrame(tools))
|
|
906
|
+
}
|
|
907
|
+
},
|
|
908
|
+
onClientToolCall: (frame) => {
|
|
909
|
+
dispatchClientToolCall(sendControl, tools, frame)
|
|
910
|
+
},
|
|
897
911
|
})
|
|
898
912
|
}
|
|
899
913
|
fireState('connecting')
|
|
@@ -932,6 +946,11 @@ async function createWebRtcCall(opts) {
|
|
|
932
946
|
dc.onerror = () => {
|
|
933
947
|
opts.onError?.({ code: 'socket_error', message: 'control channel error' })
|
|
934
948
|
}
|
|
949
|
+
dc.onopen = () => {
|
|
950
|
+
if (Object.keys(tools).length > 0) {
|
|
951
|
+
sendControl(buildRegisterFrame(tools))
|
|
952
|
+
}
|
|
953
|
+
}
|
|
935
954
|
const gateway = opts.webrtcGatewayBase || ''
|
|
936
955
|
const offerUrl = gateway
|
|
937
956
|
? `${gateway}/webrtc/offer?token=${encodeURIComponent(opts.token)}`
|
|
@@ -1073,6 +1092,7 @@ var BrowserVoiceFactory = class {
|
|
|
1073
1092
|
: void 0,
|
|
1074
1093
|
onInterrupt: options.onInterrupt,
|
|
1075
1094
|
onAgentTurnStart: options.onAgentTurnStart,
|
|
1095
|
+
clientTools: options.clientTools,
|
|
1076
1096
|
})
|
|
1077
1097
|
}
|
|
1078
1098
|
const client = new BrowserVoiceClient({
|
package/dist/browser.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/browser.ts","../src/config.ts","../src/worklets/mic-downsampler.worklet.js","../src/AudioCapture.ts","../src/AudioPlayback.ts","../src/ReconnectingWebSocket.ts","../src/protocol.ts","../src/clientTools.ts","../src/ClientMarksBuffer.ts","../src/VoiceClient.ts","../src/webrtc/createWebRtcCall.ts"],"sourcesContent":["// Browser entry — `import { configureVoiceClient } from '@craftedxp/voice-js'`\n//\n// Pre-injects:\n// - native `WebSocket` (globalThis.WebSocket)\n// - browser audio adapter (AudioContext + AudioWorklet + getUserMedia)\n//\n// Electron renderer + browser tabs both pick this entry via the\n// `browser` condition in package.json `exports`.\n\nimport {\n mergeStartCallContext,\n normalizeConfig,\n type Call,\n type FetchTokenResult,\n type StartCallOptions,\n type VoiceClientConfig,\n type VoiceClientFactory,\n} from './config'\nimport type { WebSocketFactory, WebSocketLike } from './ReconnectingWebSocket'\nimport { BrowserVoiceClient } from './VoiceClient'\nimport { createWebRtcCall } from './webrtc/createWebRtcCall'\n\n// Adapter from native browser WebSocket → our WebSocketLike contract.\n// One-line wrapper; the shapes already match.\nconst browserWsFactory: WebSocketFactory = (url) =>\n new (globalThis as unknown as { WebSocket: new (u: string) => WebSocketLike }).WebSocket(url)\n\nclass BrowserVoiceFactory implements VoiceClientFactory {\n readonly config: VoiceClientConfig\n\n constructor(config: VoiceClientConfig) {\n this.config = config\n }\n\n startCall = async (options: StartCallOptions): Promise<Call> => {\n if (!options.agentId) {\n throw new Error('startCall: agentId is required')\n }\n\n const { context, metadata } = mergeStartCallContext(this.config, options)\n const fetchArgs = {\n agentId: options.agentId,\n userId: options.userId,\n context,\n metadata,\n }\n\n // Resolve the token + transport. Three input shapes:\n // 1. options.token (test escape hatch) — always WS, current behaviour\n // 2. fetchToken returns a bare string — always WS (back-compat)\n // 3. fetchToken returns FetchTokenResult — server-chosen transport\n let resolved: FetchTokenResult\n if (options.token) {\n resolved = { token: options.token, transport: 'ws' }\n } else {\n const r = await this.config.fetchToken(fetchArgs)\n if (!r) {\n throw new Error('configureVoiceClient.fetchToken returned empty token')\n }\n resolved = typeof r === 'string' ? { token: r, transport: 'ws' } : r\n if (!resolved.token) {\n throw new Error('configureVoiceClient.fetchToken returned an object without `token`')\n }\n }\n\n if (resolved.transport === 'webrtc') {\n return createWebRtcCall({\n agentId: options.agentId,\n apiBase: this.config.apiBase,\n token: resolved.token,\n webrtcGatewayBase: resolved.webrtcGatewayBase,\n onStateChange: options.onStateChange,\n onTranscript: options.onTranscript,\n onError: options.onError,\n // Synthesise a minimal CallEndEvent. WebRTC doesn't carry an end reason\n // from the server yet — use 'agent_ended' as placeholder. durationMs is\n // tracked at 0 until the followup lands (see spec Followups section).\n onEnd: options.onEnd\n ? () => options.onEnd!({ reason: 'agent_ended', durationMs: 0 })\n : undefined,\n onInterrupt: options.onInterrupt,\n onAgentTurnStart: options.onAgentTurnStart,\n })\n }\n\n const client = new BrowserVoiceClient({\n config: this.config,\n // Carry merged context/metadata through to startCall so server can\n // see what the SDK saw.\n options: { ...options, context, metadata },\n token: resolved.token,\n wsFactory: browserWsFactory,\n })\n await client.start()\n return client\n }\n}\n\n/**\n * One-time SDK setup. Returns a factory you call `startCall` on for\n * every voice call.\n *\n * Example:\n * const voice = configureVoiceClient({\n * apiBase: 'https://api.your-server.com',\n * fetchToken: async ({ agentId }) => {\n * const r = await fetch('/api/voice-token', {\n * method: 'POST',\n * body: JSON.stringify({ agentId }),\n * })\n * return (await r.json()).token\n * },\n * })\n *\n * // Per call (typically inside a click handler):\n * const call = await voice.startCall({\n * agentId: 'agt_xxx',\n * onTranscript: (entries) => render(entries),\n * onEnd: ({ reason }) => log(reason),\n * })\n * call.mute()\n * call.end()\n */\nexport function configureVoiceClient(config: VoiceClientConfig): VoiceClientFactory {\n return new BrowserVoiceFactory(normalizeConfig(config))\n}\n\n// ---------------------------------------------------------------------------\n// Public re-exports — types + advanced primitives.\n// ---------------------------------------------------------------------------\n\nexport type {\n Call,\n ClientTool,\n ClientToolMap,\n FetchToken,\n FetchTokenArgs,\n FetchTokenResult,\n StartCallOptions,\n VoiceClientConfig,\n VoiceClientFactory,\n} from './config'\n\nexport type {\n CallEndEvent,\n CallEndReason,\n CallError,\n CallErrorCode,\n CallState,\n TranscriptEntry,\n VolumeEvent,\n} from './protocol'\n\n// Low-level primitives — exposed for advanced consumers who want to\n// drive just the mic, just the playback, or build a custom transport on\n// top of the protocol decoder. Most users should stick to\n// `configureVoiceClient().startCall()`.\nexport { createAudioCapture } from './AudioCapture'\nexport type { CaptureController, CaptureOptions, OnChunk, OnError, OnVolume } from './AudioCapture'\n\nexport { createAudioPlayback } from './AudioPlayback'\nexport type { PlaybackController, PlaybackOptions, OnAgentSpeakingChange } from './AudioPlayback'\n\nexport { createReconnectingWebSocket } from './ReconnectingWebSocket'\nexport type {\n ReconnectingWebSocket,\n RWSEvent,\n RWSOptions,\n WebSocketFactory,\n WebSocketLike,\n} from './ReconnectingWebSocket'\n\nexport { handleServerMessage, createProtocolState, buildWsUrl } from './protocol'\nexport type { ProtocolState, ProtocolCallbacks, ServerMessage } from './protocol'\n","// Public configuration surface.\n//\n// `configureVoiceClient` returns a small factory that knows how to mint\n// `ct_` tokens via your callback. Per-call you call `factory.startCall`.\n//\n// Parity with @craftedxp/voice-rn:\n// voice-rn calls `configureVoiceClient` once at app startup as a\n// side-effect (singleton), because in RN there is exactly one host\n// app and one running `<AgentCall>` at a time. In JS environments\n// the same process can drive multiple clients (multi-tenant\n// dashboards, terminal multiplexers, electron apps with several\n// panels), so the JS SDK returns the factory rather than mutating a\n// module-level singleton. Same option names, same callback shape.\n//\n// Auth model:\n// - `apiKey` is INTENTIONALLY ABSENT from the public surface.\n// Pre-0.2 had it; baking an `sk_` into a JS bundle ships your\n// server-grade credentials to every client. The right pattern is\n// `fetchToken` — your code asks YOUR backend for a short-lived\n// `ct_` and your backend uses its `sk_` (via @craftedxp/sdk-node)\n// to mint it.\n// - For tests + local prototypes a bare `token` may be passed to\n// `startCall` directly, bypassing `fetchToken`. Don't ship that to\n// production — `ct_` lifetimes are short and you want the SDK to\n// re-mint on expiry.\n\nimport type { CallEndEvent, CallError, CallState, TranscriptEntry, VolumeEvent } from './protocol'\nimport type { ClientToolMap } from './clientTools'\n\n// ---------------------------------------------------------------------------\n// fetchToken contract — matches voice-rn FetchTokenArgs verbatim.\n// ---------------------------------------------------------------------------\n\nexport interface FetchTokenArgs {\n /** The agent the SDK is about to call. */\n agentId: string\n /**\n * Optional consumer-side user identifier. Round-tripped to the server\n * as `contactId` for Phase 11 contact memory. The SDK does not\n * inspect this; your backend uses it to scope the token mint.\n */\n userId?: string\n /**\n * Per-call structured context lowered into the agent's effective\n * system prompt server-side at session open. Opaque to the SDK.\n */\n context?: Record<string, unknown>\n /**\n * String key/value pairs round-tripped on the `call.ended` webhook.\n * Capped at 1 KB total server-side. NOT lowered into the system prompt.\n */\n metadata?: Record<string, string>\n}\n\n/**\n * What `fetchToken` may return. The rich object form lets the server\n * choose the transport per call. Returning a bare string is backwards-\n * compatible — the SDK treats it as `{ token, transport: 'ws' }`.\n */\nexport interface FetchTokenResult {\n /** Raw `ct_` to feed into the WS open / WebRTC offer. */\n token: string\n /** Server-selected transport. Default `'ws'` if absent. */\n transport?: 'ws' | 'webrtc'\n /** Required when `transport === 'webrtc'` AND the server uses a\n * separate signaling gateway. When omitted on a webrtc result, the\n * SDK falls back to the API base's Phase-1 routes (local dev). */\n webrtcGatewayBase?: string\n}\n\nexport type FetchToken = (args: FetchTokenArgs) => Promise<string | FetchTokenResult>\n\n// ---------------------------------------------------------------------------\n// Factory configuration\n// ---------------------------------------------------------------------------\n\nexport interface VoiceClientConfig {\n /**\n * Full HTTPS URL of the Voissia server. The WebSocket scheme is\n * derived: `https` → `wss`, `http` → `ws`. No trailing slash needed.\n */\n apiBase: string\n /**\n * Called by the SDK whenever it needs a fresh `ct_` token (initial\n * connect; mid-call refresh on `token_expired`). Your implementation\n * should hit YOUR backend, which holds the `sk_` API key and mints\n * via `POST /v1/call-tokens` (or `client.callTokens.mint` from\n * @craftedxp/sdk-node). Never embed `sk_` in JS code that ships to a\n * client.\n */\n fetchToken: FetchToken\n /**\n * Optional metadata applied to EVERY startCall. Per-call `metadata`\n * in `startCall` is merged on top (per-call wins on key conflicts).\n * Useful for dashboard-wide tags like `{ surface: 'web', appVersion }`.\n */\n defaultMetadata?: Record<string, string>\n /**\n * Optional context applied to EVERY startCall. Per-call `context` in\n * `startCall` is merged on top. Useful for cross-call invariants like\n * the signed-in user's locale.\n */\n defaultContext?: Record<string, unknown>\n}\n\n// ---------------------------------------------------------------------------\n// Per-call options — startCall({ ... })\n// ---------------------------------------------------------------------------\n\nexport interface StartCallOptions {\n /** The agent to call. */\n agentId: string\n /** Per-call user identifier. Round-tripped to fetchToken as `userId`. */\n userId?: string\n /**\n * Per-call structured context. Merged on top of `defaultContext`\n * configured at factory time.\n */\n context?: Record<string, unknown>\n /**\n * Per-call metadata. Merged on top of `defaultMetadata` configured\n * at factory time.\n */\n metadata?: Record<string, string>\n /**\n * When false, the SDK + server stay full-duplex but barge-in is\n * suppressed. Useful for alarm-style flows where the user shouldn't\n * accidentally interrupt the script. Default true.\n */\n bargeIn?: boolean\n /**\n * Client-side tools the agent's LLM can call mid-conversation. Each\n * tool's handler runs on the consumer's side; result is fed back to\n * the LLM through the existing call WebSocket. Schema and handler\n * colocate. Validated synchronously at startCall — bad input throws.\n *\n * See docs/integration-echocheck.md for the wire protocol and the\n * server-side guarantees.\n */\n clientTools?: ClientToolMap\n /**\n * Test-only escape hatch — pass a pre-minted `ct_` directly and skip\n * the `fetchToken` call. Don't use this in production code: tokens\n * expire and the SDK can't re-mint without the callback.\n */\n token?: string\n\n // Event callbacks — same shape as voice-rn UseVoiceCallOptions.\n onStateChange?: (state: CallState) => void\n onTranscript?: (entries: TranscriptEntry[]) => void\n onError?: (err: CallError) => void\n onEnd?: (end: CallEndEvent) => void\n /** Volume-meter event for VU UIs. ~10 Hz cadence (browser bundle only). */\n onVolume?: (vol: VolumeEvent) => void\n /**\n * Fires when the server signals barge-in (the user started talking\n * mid-agent-turn). The browser bundle automatically flushes its\n * built-in audio playback before this callback runs; the callback is\n * fired regardless. Node / Electron consumers with custom playback\n * should drain their audio queue here so the agent goes silent\n * immediately.\n */\n onInterrupt?: () => void\n /**\n * Fires on `agent_turn_start` — the server has begun a new agent\n * turn. The state-machine transition to `agent_speaking` happens at\n * the same moment via `onStateChange`; use this when you want a\n * precise turn anchor (e.g. \"agent has been speaking for N ms\" UIs)\n * without diffing state.\n */\n onAgentTurnStart?: () => void\n}\n\n// ---------------------------------------------------------------------------\n// Call handle returned from startCall — matches voice-rn\n// VoiceCallController where it makes sense.\n// ---------------------------------------------------------------------------\n\nexport interface Call {\n /** Current state. Snapshot — subscribe via onStateChange for live updates. */\n readonly state: CallState\n /** Full transcript so far. Snapshot — subscribe via onTranscript for live updates. */\n readonly transcript: TranscriptEntry[]\n /** True after `mute()` and before `unmute()`. */\n readonly isMuted: boolean\n /** End the call locally. Closes the WS, stops the mic, fires onEnd. Idempotent. */\n end: () => void\n /** Mute mic frames. Wire stays active so server endpointing doesn't false-positive. Idempotent. */\n mute: () => void\n /** Unmute mic frames. Idempotent. */\n unmute: () => void\n}\n\n// ---------------------------------------------------------------------------\n// Factory contract — what configureVoiceClient returns. The actual\n// implementation differs between browser (audio-equipped) and node (raw\n// PCM) bundles; both satisfy this interface.\n// ---------------------------------------------------------------------------\n\nexport interface VoiceClientFactory {\n /** Read back the resolved config (post trailing-slash normalisation). */\n readonly config: VoiceClientConfig\n /**\n * Open a fresh call. Returns when the WS is open; rejects on\n * pre-flight failure (missing config, fetchToken throw, etc). Mid-\n * call failures arrive via the per-call `onError` callback — they\n * don't reject this promise.\n */\n startCall: (options: StartCallOptions) => Promise<Call>\n}\n\n// ---------------------------------------------------------------------------\n// Pre-flight validation. Pulled out so both bundles share the exact\n// same \"missing field\" error messages.\n// ---------------------------------------------------------------------------\n\nexport function normalizeConfig(config: VoiceClientConfig): VoiceClientConfig {\n if (!config) throw new Error('configureVoiceClient: config is required')\n if ('apiKey' in (config as object)) {\n throw new Error(\n 'configureVoiceClient: `apiKey` is no longer supported. Embedding sk_ in JS code ships server-grade credentials to every client. Pass `fetchToken: async ({ agentId }) => { /* call YOUR backend mint */ }` instead — see the @craftedxp/voice-js README for the migration recipe.',\n )\n }\n if (!config.apiBase) {\n throw new Error('configureVoiceClient: apiBase is required')\n }\n if (typeof config.fetchToken !== 'function') {\n throw new Error('configureVoiceClient: fetchToken must be a function')\n }\n return {\n ...config,\n apiBase: config.apiBase.replace(/\\/+$/, ''),\n }\n}\n\n// Merge factory-level defaults with per-call overrides. Per-call wins.\nexport function mergeStartCallContext(\n factory: VoiceClientConfig,\n call: StartCallOptions,\n): { context?: Record<string, unknown>; metadata?: Record<string, string> } {\n const context =\n factory.defaultContext || call.context\n ? { ...(factory.defaultContext ?? {}), ...(call.context ?? {}) }\n : undefined\n const metadata =\n factory.defaultMetadata || call.metadata\n ? { ...(factory.defaultMetadata ?? {}), ...(call.metadata ?? {}) }\n : undefined\n return { context, metadata }\n}\n\nexport type { ClientTool, ClientToolMap } from './clientTools'\n","// AudioWorklet — runs off the main thread in the audio rendering graph.\n//\n// Input: Float32 samples at the AudioContext's native sampleRate (typically\n// 48000 Hz on desktop, 44100 Hz on some iOS devices).\n// Output: 16 kHz mono Int16 PCM, shipped to the main thread via\n// `port.postMessage(ArrayBuffer, [ArrayBuffer])` (transferred, not copied).\n//\n// Why AudioWorklet instead of ScriptProcessorNode: ScriptProcessorNode is\n// deprecated + main-thread-bound, so any JS jank produces audible audio\n// glitches. AudioWorklet's `process()` runs on the audio rendering thread\n// at the graph's block cadence (128 frames by default) and backpressures\n// via returning `true` / `false`.\n//\n// This file is loaded as text (see tsup.config.ts loader) and registered\n// at runtime via `audioWorklet.addModule(blobUrl)`.\n\nclass MicDownsampler extends AudioWorkletProcessor {\n constructor() {\n super()\n // Target sample rate for STT. Matches Deepgram Nova-3 + the platform's\n // server-side SAMPLE_RATE constant in AgentCallHandler.\n this.targetRate = 16000\n // Accumulator for the downsample. We collect incoming samples and emit\n // an Int16 chunk when we've accumulated ~1024 target-rate samples\n // (~64 ms at 16 kHz) — matches the mobile SDK's chunk size so both\n // platforms have the same server-side framing.\n this.outputFrames = 1024\n this.acc = []\n // Running index used for fractional resampling.\n this.readCursor = 0\n }\n\n // `inputs[0][0]` = first channel of first input. 128 Float32 samples per\n // call at the context's sampleRate. Return true = keep processing.\n process(inputs) {\n const input = inputs[0]\n if (!input || input.length === 0) return true\n const channel = input[0]\n if (!channel || channel.length === 0) return true\n\n const ctxRate = sampleRate // global inside AudioWorkletProcessor\n const ratio = ctxRate / this.targetRate\n\n // Simple linear-interp downsample. For 48000 → 16000 that's 3:1, which\n // linear handles fine for voice. Anti-alias filtering would be\n // theoretically better but inaudible for speech.\n for (let i = 0; i < channel.length; i++) {\n this.acc.push(channel[i])\n }\n\n while (this.acc.length - this.readCursor >= ratio * this.outputFrames) {\n const out = new Int16Array(this.outputFrames)\n let readIdx = this.readCursor\n for (let i = 0; i < this.outputFrames; i++) {\n // Linear interp between floor(readIdx) and ceil(readIdx)\n const low = Math.floor(readIdx)\n const high = Math.min(low + 1, this.acc.length - 1)\n const frac = readIdx - low\n const sample = this.acc[low] * (1 - frac) + this.acc[high] * frac\n // Clip + convert to int16\n const clipped = Math.max(-1, Math.min(1, sample))\n out[i] = clipped < 0 ? clipped * 0x8000 : clipped * 0x7fff\n readIdx += ratio\n }\n // Transfer the ArrayBuffer (zero-copy) to the main thread.\n this.port.postMessage(out.buffer, [out.buffer])\n this.readCursor = readIdx\n }\n\n // Garbage-collect the consumed portion of `acc` every so often so it\n // doesn't grow without bound. Leave ~one chunk of headroom.\n if (this.readCursor > ratio * this.outputFrames) {\n this.acc = this.acc.slice(Math.floor(this.readCursor))\n this.readCursor -= Math.floor(this.readCursor)\n }\n\n return true\n }\n}\n\nregisterProcessor('mic-downsampler', MicDownsampler)\n","// Mic capture pipeline:\n// getUserMedia → AudioContext → AudioWorkletNode → Int16 PCM chunks\n//\n// We register the worklet from an inline Blob URL so consumers don't need\n// to host or bundle a separate .js file. The worklet source is loaded as\n// text at build time via tsup's custom loader (see tsup.config.ts).\n\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore — text-loaded by tsup\nimport workletSource from './worklets/mic-downsampler.worklet.js'\n\nexport type OnChunk = (pcm: ArrayBuffer) => void\nexport type OnVolume = (rms01: number) => void\nexport type OnError = (err: Error) => void\n\nexport interface CaptureOptions {\n onChunk: OnChunk\n // Optional — when set we compute input RMS and call this at ~10Hz. Used\n // for volume visualisers in consumer UIs.\n onVolume?: OnVolume\n onError?: OnError\n}\n\nexport interface CaptureController {\n start: () => Promise<void>\n stop: () => void\n mute: (muted: boolean) => void\n isCapturing: () => boolean\n}\n\n// Volume-meter cadence. 100ms = 10Hz updates; smoother than per-chunk\n// (16Hz) without flicker.\nconst VOLUME_INTERVAL_MS = 100\n\nexport const createAudioCapture = (options: CaptureOptions): CaptureController => {\n let audioContext: AudioContext | null = null\n let mediaStream: MediaStream | null = null\n let sourceNode: MediaStreamAudioSourceNode | null = null\n let workletNode: AudioWorkletNode | null = null\n let analyser: AnalyserNode | null = null\n let volumeTimer: ReturnType<typeof setInterval> | null = null\n let muted = false\n let capturing = false\n\n const computeRms = (buf: Float32Array): number => {\n let sum = 0\n for (let i = 0; i < buf.length; i++) sum += buf[i] * buf[i]\n const rms = Math.sqrt(sum / buf.length)\n // Clamp to 0..1 — RMS of a full-scale signal is 0.707, so boost a bit\n // so \"normal speech\" hits ~0.5 in the visualiser.\n return Math.min(1, rms * 1.8)\n }\n\n const start = async () => {\n if (capturing) return\n try {\n // Browsers only grant getUserMedia on secure contexts (https or\n // localhost). The thrown DOMException is clear enough — we let it\n // propagate with a wrapped error message for clarity.\n mediaStream = await navigator.mediaDevices.getUserMedia({\n audio: {\n // Hand tuning for voice agent use: we want the raw signal so the\n // server-side STT can do its own noise handling. Disable browser\n // AEC/AGC/NR — experimentally they fight with whatever processing\n // the TTS playback path feeds back in over speakers.\n echoCancellation: true,\n noiseSuppression: true,\n autoGainControl: true,\n channelCount: 1,\n },\n })\n\n audioContext = new AudioContext()\n // Most browsers suspend AudioContext until a user gesture. connect()\n // is idempotent; if we were constructed inside a click/touch handler\n // it's already running.\n if (audioContext.state === 'suspended') await audioContext.resume()\n\n // Load + register the worklet from an inline Blob URL. One-time per\n // context; subsequent `new AudioWorkletNode` calls are cheap.\n const blob = new Blob([workletSource], { type: 'application/javascript' })\n const url = URL.createObjectURL(blob)\n try {\n await audioContext.audioWorklet.addModule(url)\n } finally {\n URL.revokeObjectURL(url)\n }\n\n sourceNode = audioContext.createMediaStreamSource(mediaStream)\n workletNode = new AudioWorkletNode(audioContext, 'mic-downsampler')\n\n // Worklet → main thread: Int16 PCM chunks\n workletNode.port.onmessage = (event: MessageEvent<ArrayBuffer>) => {\n if (muted) return\n options.onChunk(event.data)\n }\n\n // Analyser sits in parallel to the worklet for the volume meter. Not\n // strictly necessary — we could compute RMS inside the worklet and\n // post it too — but AnalyserNode avoids extra bridge chatter.\n if (options.onVolume) {\n analyser = audioContext.createAnalyser()\n analyser.fftSize = 256\n sourceNode.connect(analyser)\n const buf = new Float32Array(analyser.fftSize)\n volumeTimer = setInterval(() => {\n if (!analyser) return\n analyser.getFloatTimeDomainData(buf)\n options.onVolume?.(computeRms(buf))\n }, VOLUME_INTERVAL_MS)\n }\n\n sourceNode.connect(workletNode)\n // The worklet doesn't need to feed into the destination — its only\n // job is to emit messages. But the AudioContext graph keeps the\n // worklet running only if it's connected somewhere; connect to a\n // zero-gain node to park it.\n const sink = audioContext.createGain()\n sink.gain.value = 0\n workletNode.connect(sink).connect(audioContext.destination)\n\n capturing = true\n } catch (err) {\n const wrapped =\n err instanceof Error ? err : new Error(typeof err === 'string' ? err : 'capture failed')\n options.onError?.(wrapped)\n throw wrapped\n }\n }\n\n const stop = () => {\n if (!capturing) return\n capturing = false\n if (volumeTimer) {\n clearInterval(volumeTimer)\n volumeTimer = null\n }\n try {\n workletNode?.disconnect()\n analyser?.disconnect()\n sourceNode?.disconnect()\n } catch {\n // already disconnected\n }\n workletNode = null\n analyser = null\n sourceNode = null\n if (mediaStream) {\n for (const track of mediaStream.getTracks()) track.stop()\n mediaStream = null\n }\n if (audioContext && audioContext.state !== 'closed') {\n void audioContext.close().catch(() => undefined)\n }\n audioContext = null\n }\n\n return {\n start,\n stop,\n mute: (v) => {\n muted = v\n },\n isCapturing: () => capturing,\n }\n}\n","// Playback pipeline:\n// WebSocket binary frame (Int16 PCM @ 16 kHz) → AudioBufferSourceNode\n// chained for gapless playback → AudioContext destination.\n//\n// We deliberately don't use MediaSource or a single streaming source —\n// each agent TTS chunk is scheduled as its own short AudioBuffer so we\n// can interrupt (flush the queue) the moment a barge-in lands. One chunk\n// ≈ 20–100ms of audio.\n\nexport type OnVolume = (rms01: number) => void\nexport type OnAgentSpeakingChange = (speaking: boolean) => void\n\nexport interface PlaybackOptions {\n // Sample rate of the incoming PCM — matches AgentCallHandler.SAMPLE_RATE.\n sampleRate?: number\n onVolume?: OnVolume\n onSpeakingChange?: OnAgentSpeakingChange\n}\n\nexport interface PlaybackController {\n enqueue: (pcm: ArrayBuffer) => void\n flush: () => void\n close: () => void\n resume: () => Promise<void>\n}\n\nconst DEFAULT_SAMPLE_RATE = 16_000\nconst VOLUME_INTERVAL_MS = 100\n\nexport const createAudioPlayback = (options: PlaybackOptions = {}): PlaybackController => {\n const sampleRate = options.sampleRate ?? DEFAULT_SAMPLE_RATE\n let audioContext: AudioContext | null = null\n let gainNode: GainNode | null = null\n let analyser: AnalyserNode | null = null\n let volumeTimer: ReturnType<typeof setInterval> | null = null\n let nextStartTime = 0\n let scheduledNodes: AudioBufferSourceNode[] = []\n let speaking = false\n\n const ensureContext = async () => {\n if (audioContext) {\n if (audioContext.state === 'suspended') await audioContext.resume()\n return\n }\n // Pass the target sample rate as a hint. Browsers may choose a\n // different rate (48000 is most common); `createBuffer(numChannels,\n // length, sampleRate)` with a different rate than the context will\n // trigger resampling on `start()`, which the browser handles natively.\n audioContext = new AudioContext({ sampleRate })\n gainNode = audioContext.createGain()\n if (options.onVolume) {\n analyser = audioContext.createAnalyser()\n analyser.fftSize = 256\n gainNode.connect(analyser)\n const buf = new Float32Array(analyser.fftSize)\n volumeTimer = setInterval(() => {\n if (!analyser) return\n analyser.getFloatTimeDomainData(buf)\n let sum = 0\n for (let i = 0; i < buf.length; i++) sum += buf[i] * buf[i]\n const rms = Math.sqrt(sum / buf.length)\n options.onVolume?.(Math.min(1, rms * 1.8))\n }, VOLUME_INTERVAL_MS)\n }\n gainNode.connect(audioContext.destination)\n nextStartTime = audioContext.currentTime\n }\n\n const setSpeaking = (v: boolean) => {\n if (v === speaking) return\n speaking = v\n options.onSpeakingChange?.(v)\n }\n\n const pruneFinished = () => {\n const now = audioContext?.currentTime ?? 0\n scheduledNodes = scheduledNodes.filter((n) => {\n const node = n as AudioBufferSourceNode & { _endsAt?: number }\n return (node._endsAt ?? 0) > now\n })\n if (scheduledNodes.length === 0) setSpeaking(false)\n }\n\n const enqueue = (pcm: ArrayBuffer) => {\n // We don't want to await here — WS onmessage is hot-path, and AudioContext\n // resume is cheap once warmed. `void` the promise and proceed on next\n // tick; if the context isn't ready, the first chunk may land a hair\n // late but subsequent chunks catch up.\n if (!audioContext) {\n void ensureContext().then(() => enqueue(pcm))\n return\n }\n if (!audioContext || !gainNode) return\n\n const int16 = new Int16Array(pcm)\n if (int16.length === 0) return\n\n const audioBuffer = audioContext.createBuffer(1, int16.length, sampleRate)\n const float32 = audioBuffer.getChannelData(0)\n for (let i = 0; i < int16.length; i++) {\n float32[i] = int16[i] / 0x8000\n }\n\n const node = audioContext.createBufferSource()\n node.buffer = audioBuffer\n node.connect(gainNode)\n\n const now = audioContext.currentTime\n // Schedule back-to-back. If we've fallen behind (mic lag, WS gap),\n // re-anchor to now so we don't skew the whole queue further.\n const startAt = Math.max(now, nextStartTime)\n node.start(startAt)\n\n const duration = int16.length / sampleRate\n ;(node as AudioBufferSourceNode & { _endsAt?: number })._endsAt = startAt + duration\n nextStartTime = startAt + duration\n\n scheduledNodes.push(node)\n setSpeaking(true)\n\n // Prune after every enqueue so `speaking` collapses quickly when the\n // queue drains. Cheap O(n) over a short list.\n node.onended = () => pruneFinished()\n }\n\n // Barge-in: stop anything pending, reset the schedule pointer.\n //\n // We can't simply `.stop()` playing sources mid-buffer and expect the\n // AudioContext's destination to be silent immediately — Safari in\n // particular sometimes plays out a few ms of tail. Reliable approach:\n // swap the gainNode out so nothing after this point is audible, then\n // clean up the old one on the next tick.\n const flush = () => {\n if (!audioContext || !gainNode) return\n for (const node of scheduledNodes) {\n try {\n node.stop()\n } catch {\n // already stopped\n }\n }\n scheduledNodes = []\n // Swap gainNode so even stubborn trailing samples on the old node\n // don't reach the speakers.\n gainNode.disconnect()\n gainNode = audioContext.createGain()\n if (analyser) {\n analyser.disconnect()\n gainNode.connect(analyser)\n }\n gainNode.connect(audioContext.destination)\n nextStartTime = audioContext.currentTime\n setSpeaking(false)\n }\n\n const close = () => {\n flush()\n if (volumeTimer) {\n clearInterval(volumeTimer)\n volumeTimer = null\n }\n if (audioContext && audioContext.state !== 'closed') {\n void audioContext.close().catch(() => undefined)\n }\n audioContext = null\n gainNode = null\n analyser = null\n }\n\n const resume = async () => {\n await ensureContext()\n }\n\n return { enqueue, flush, close, resume }\n}\n","// Minimal auto-reconnecting WebSocket wrapper.\n//\n// Scope: transparent reconnection on unexpected drops (network blip,\n// server restart). We deliberately do NOT try to preserve call state\n// across reconnects — the server's AgentCallHandler is session-scoped\n// and won't resume where we left off. If the consumer needs mid-call\n// resilience, that's a server-side resumable-session feature, not a\n// client-side retry.\n//\n// In v1 we reconnect only when `connect()` has been called and\n// `.close(1000, ...)` has NOT been called by the consumer (i.e., we drop\n// because of network, not because the user hung up). On unclean close\n// the consumer gets a fresh connection + a `reconnected` event so they\n// can re-issue any greeting / context.\n//\n// Transport agnostic — accepts a WebSocket-constructor factory so the\n// browser bundle uses native `WebSocket` and the node bundle injects the\n// `ws` package.\n\nexport type RWSEvent =\n | { type: 'open' }\n | { type: 'reconnected' }\n | { type: 'message'; data: string | ArrayBuffer }\n | { type: 'close'; code: number; reason: string; permanent: boolean }\n | { type: 'error'; error: Error }\n\n// Minimal WebSocket-like contract. Both browser `WebSocket` and the\n// `ws` package's WebSocket satisfy it.\nexport interface WebSocketLike {\n binaryType: string\n readyState: number\n onopen: ((ev: unknown) => void) | null\n onmessage: ((ev: { data: string | ArrayBuffer }) => void) | null\n onerror: ((ev: unknown) => void) | null\n onclose: ((ev: { code: number; reason: string }) => void) | null\n send: (data: string | ArrayBuffer | ArrayBufferView) => void\n close: (code?: number, reason?: string) => void\n}\n\nexport type WebSocketFactory = (url: string) => WebSocketLike\n\nexport interface RWSOptions {\n url: string\n // Factory so we can swap browser-native WebSocket for the `ws` package\n // in node. Browser entry pre-fills with a wrapper around globalThis.WebSocket.\n wsFactory: WebSocketFactory\n // Cap the number of auto-reconnect attempts. `0` disables retry entirely\n // (closer to native WebSocket semantics). Default: 3.\n maxRetries?: number\n // Initial backoff; doubles up to maxBackoffMs.\n initialBackoffMs?: number\n maxBackoffMs?: number\n}\n\nconst READYSTATE_OPEN = 1\nconst READYSTATE_CLOSED = 3\n\nexport const createReconnectingWebSocket = (\n options: RWSOptions,\n onEvent: (ev: RWSEvent) => void,\n) => {\n const maxRetries = options.maxRetries ?? 3\n const initialBackoff = options.initialBackoffMs ?? 500\n const maxBackoff = options.maxBackoffMs ?? 8000\n\n let ws: WebSocketLike | null = null\n let intentionalClose = false\n let retries = 0\n let backoff = initialBackoff\n let reconnectTimer: ReturnType<typeof setTimeout> | null = null\n\n const openOnce = () => {\n ws = options.wsFactory(options.url)\n // Binary frames arrive as Int16 PCM from the server. ArrayBuffer is\n // more convenient than Blob (one fewer async read) — we `.arrayBuffer()`\n // a Blob manually only if we receive one unexpectedly.\n ws.binaryType = 'arraybuffer'\n ws.onopen = () => {\n if (retries === 0) onEvent({ type: 'open' })\n else onEvent({ type: 'reconnected' })\n retries = 0\n backoff = initialBackoff\n }\n ws.onmessage = (ev) => {\n onEvent({ type: 'message', data: ev.data as string | ArrayBuffer })\n }\n ws.onerror = () => {\n onEvent({ type: 'error', error: new Error('WebSocket error') })\n }\n ws.onclose = (ev) => {\n ws = null\n const shouldRetry = !intentionalClose && retries < maxRetries\n if (!shouldRetry) {\n onEvent({\n type: 'close',\n code: ev.code,\n reason: ev.reason,\n permanent: true,\n })\n return\n }\n onEvent({\n type: 'close',\n code: ev.code,\n reason: ev.reason,\n permanent: false,\n })\n retries++\n const delay = Math.min(backoff, maxBackoff)\n backoff = Math.min(backoff * 2, maxBackoff)\n reconnectTimer = setTimeout(openOnce, delay)\n }\n }\n\n openOnce()\n\n return {\n send: (data: string | ArrayBuffer | ArrayBufferView) => {\n if (ws && ws.readyState === READYSTATE_OPEN) ws.send(data)\n },\n close: (code = 1000, reason = 'client-requested') => {\n intentionalClose = true\n if (reconnectTimer) {\n clearTimeout(reconnectTimer)\n reconnectTimer = null\n }\n try {\n ws?.close(code, reason)\n } catch {\n // already closed\n }\n },\n readyState: () => ws?.readyState ?? READYSTATE_CLOSED,\n }\n}\n\nexport type ReconnectingWebSocket = ReturnType<typeof createReconnectingWebSocket>\n","// Wire-protocol types + stateless transcript reducer.\n//\n// Shared between the browser and node bundles. The protocol surface is\n// matched (where it makes sense) to @craftedxp/voice-rn 0.3.x so a\n// consumer can write the same `onTranscript` / `onStateChange` /\n// `onError` / `onEnd` shape for both web and RN clients.\n\nimport type { ClientToolCallFrame } from './clientTools'\n\n// Re-export so consumers / dashboards can type the wire shape. The\n// `client_marks` frame itself is purely an internal observability\n// channel between the SDK and server — it's emitted by the SDK and\n// logged on the server, never surfaced to consumer code.\nexport type { ClientMarksFrame } from './ClientMarksBuffer'\n\n// ---------------------------------------------------------------------------\n// State machine\n// ---------------------------------------------------------------------------\n\nexport type CallState =\n | 'idle'\n | 'connecting'\n | 'listening'\n | 'user_speaking'\n | 'agent_speaking'\n | 'ended'\n | 'error'\n\n// ---------------------------------------------------------------------------\n// Transcript model — matches voice-rn TranscriptEntry\n// ---------------------------------------------------------------------------\n\nexport type TranscriptEntry =\n | { id: string; role: 'user'; text: string; committed: boolean }\n | { id: string; role: 'agent'; text: string; interrupted?: boolean }\n | { id: string; role: 'tool'; text: string }\n | { id: string; role: 'system'; text: string }\n\n// ---------------------------------------------------------------------------\n// Stable error code contract — matches voice-rn CallErrorCode where the\n// failure modes overlap. Web-specific codes (mic_denied via getUserMedia\n// rejection, etc.) keep their voice-rn names so cross-platform consumers\n// can write one switch statement.\n// ---------------------------------------------------------------------------\n\nexport type CallErrorCode =\n // Programming errors — surface loudly to the host's developer.\n | 'missing_credentials'\n | 'forbidden'\n // Browser audio failures.\n | 'mic_denied'\n | 'mic_start_failed'\n | 'audio_session_failed'\n // Auth lifecycle.\n | 'token_expired'\n | 'token_invalid'\n | 'unauthorized'\n // Network / connectivity.\n | 'network_unreachable'\n | 'socket_error'\n // Business state.\n | 'payment_required'\n | 'not_found'\n // End-of-call states surfaced via onError (also via onEnd reason='timeout').\n | 'silence_timeout'\n // Catch-all for unexpected server / 5xx / 1011.\n | 'server_error'\n\nexport interface CallError {\n code: CallErrorCode\n message: string\n}\n\n// ---------------------------------------------------------------------------\n// End-of-call signal — matches voice-rn CallEndReason / CallEndEvent\n// ---------------------------------------------------------------------------\n\nexport type CallEndReason = 'agent_ended' | 'user_hangup' | 'timeout' | 'error'\n\nexport interface CallEndEvent {\n reason: CallEndReason\n // Present iff reason === 'error'. Mirrors the code from the most\n // recent onError.\n errorCode?: CallErrorCode\n // Wallclock from start() resolving to the WS close. Useful for billing\n // / \"you spoke for 1m23s\" UIs without forcing the host to track\n // timestamps themselves.\n durationMs: number\n}\n\n// ---------------------------------------------------------------------------\n// Volume meter event (browser bundle only — node bundle leaves volume to\n// the consumer if they're processing PCM themselves).\n// ---------------------------------------------------------------------------\n\nexport interface VolumeEvent {\n // 0-1 RMS over the last ~100ms. Bind to a waveform / level meter.\n input: number\n output: number\n}\n\n// ---------------------------------------------------------------------------\n// Server → client message envelope. Loose typing — the server can add new\n// `type` values without breaking the SDK; unknown types are ignored by\n// the dispatch in handleServerMessage.\n// ---------------------------------------------------------------------------\n\nexport type ServerMessage = Record<string, unknown> & { type?: string }\n\n// ---------------------------------------------------------------------------\n// Stateless transcript reducer + state-machine helpers. Both the browser\n// and node clients call into these so the shape of the transcript stays\n// identical across environments.\n// ---------------------------------------------------------------------------\n\nexport interface ProtocolState {\n state: CallState\n transcript: TranscriptEntry[]\n agentBubbleId: string | null\n idCounter: number\n // Reason latched from the server's call_end frame. Read by the\n // surrounding client when the WS finally closes so onEnd fires with the\n // right reason instead of falling back to user_hangup.\n endReason: CallEndReason | null\n}\n\nexport const createProtocolState = (): ProtocolState => ({\n state: 'idle',\n transcript: [],\n agentBubbleId: null,\n idCounter: 0,\n endReason: null,\n})\n\n// Side-effect callbacks the protocol layer fires as it processes server\n// frames. The surrounding client wires these up to event emitters\n// (browser) or to user-supplied callbacks (node).\nexport interface ProtocolCallbacks {\n onState: (next: CallState) => void\n onTranscript: (entries: TranscriptEntry[]) => void\n onError: (err: CallError) => void\n // Fires on `interrupt` — caller should flush its audio playback queue.\n onInterrupt: () => void\n // Fires on `agent_turn_start` — caller may want to reset its turn\n // anchor for \"agent has been speaking N ms\" UIs. `seq` is the\n // server-assigned turn ID; absent on legacy server builds that don't\n // emit one (we treat that case as \"no correlation possible\").\n onAgentTurnStart: (seq?: number) => void\n // Fires on `agent_turn_end` — caller may use this to flush per-turn\n // observability marks. `seq` matches the corresponding agent_turn_start.\n onAgentTurnEnd: (seq?: number) => void\n // Fires on `call_end` — caller closes its WS and resolves onEnd.\n onCallEnd: (reason: CallEndReason) => void\n // Fires on `connected` — caller should send the client_tools_register\n // frame here so the server's first-turn grace window unblocks.\n onConnected: () => void\n // Fires on `client_tool_call` — caller dispatches to the matching\n // tool handler and posts back a client_tool_result frame.\n onClientToolCall: (frame: ClientToolCallFrame) => void\n}\n\n// Map server-supplied endReason strings onto our SDK-side CallEndReason.\nconst mapEndReason = (raw: string): CallEndReason => {\n if (raw === 'agent_ended') return 'agent_ended'\n if (raw === 'caller_hung_up') return 'user_hangup'\n if (raw === 'silence_timeout' || raw === 'max_duration') return 'timeout'\n return 'error'\n}\n\n// Pure-ish transcript reducer + dispatcher. Mutates `state` in place to\n// match the imperative pattern used by both clients; returns nothing.\n//\n// The \"agent_text\" / \"transcript\" interim handling is identical to\n// voice-rn's useVoiceCall — one growing user bubble while interim, one\n// growing agent bubble per turn.\nexport function handleServerMessage(\n raw: string,\n state: ProtocolState,\n cb: ProtocolCallbacks,\n): void {\n let msg: ServerMessage\n try {\n msg = JSON.parse(raw)\n } catch {\n return\n }\n\n switch (msg.type) {\n case 'connected':\n cb.onConnected()\n setState(state, 'listening', cb)\n return\n\n case 'transcript': {\n const text = (msg.text as string) ?? ''\n if (!text) return\n const isFinal = !!msg.isFinal\n if (!isFinal) setState(state, 'user_speaking', cb)\n upsertUserPartial(state, text, isFinal)\n cb.onTranscript(state.transcript)\n return\n }\n\n case 'agent_turn_start': {\n const id = `m${state.idCounter++}`\n state.agentBubbleId = id\n state.transcript = [...state.transcript, { id, role: 'agent', text: '' }]\n cb.onTranscript(state.transcript)\n const seq = typeof msg.seq === 'number' ? (msg.seq as number) : undefined\n cb.onAgentTurnStart(seq)\n setState(state, 'agent_speaking', cb)\n return\n }\n\n case 'agent_text': {\n const delta = (msg.text as string) ?? ''\n if (!delta || !state.agentBubbleId) return\n const id = state.agentBubbleId\n state.transcript = state.transcript.map((e) =>\n e.id === id && e.role === 'agent' ? { ...e, text: e.text + delta } : e,\n )\n cb.onTranscript(state.transcript)\n return\n }\n\n case 'agent_turn_end': {\n state.agentBubbleId = null\n const seq = typeof msg.seq === 'number' ? (msg.seq as number) : undefined\n cb.onAgentTurnEnd(seq)\n setState(state, 'listening', cb)\n return\n }\n\n case 'interrupt':\n cb.onInterrupt()\n return\n\n case 'agent_turn_abort': {\n const committed = ((msg.committedText as string) ?? '').trim()\n if (state.agentBubbleId) {\n const id = state.agentBubbleId\n if (committed) {\n state.transcript = state.transcript.map((e) =>\n e.id === id && e.role === 'agent' ? { ...e, text: committed, interrupted: true } : e,\n )\n } else {\n state.transcript = state.transcript.filter((e) => e.id !== id)\n }\n cb.onTranscript(state.transcript)\n }\n state.agentBubbleId = null\n return\n }\n\n case 'tool_call':\n state.transcript = [\n ...state.transcript,\n {\n id: `m${state.idCounter++}`,\n role: 'tool',\n text: `→ ${String(msg.tool ?? '?')}(${msg.args ? JSON.stringify(msg.args) : ''})`,\n },\n ]\n cb.onTranscript(state.transcript)\n return\n\n case 'tool_result':\n state.transcript = [\n ...state.transcript,\n {\n id: `m${state.idCounter++}`,\n role: 'tool',\n text: `${msg.ok ? '✓' : '✗'} ${String(msg.tool ?? '?')}`,\n },\n ]\n cb.onTranscript(state.transcript)\n return\n\n case 'client_tool_call': {\n const toolCallId = String(msg.toolCallId ?? '')\n const name = String(msg.name ?? '')\n const args = (msg.args as Record<string, unknown>) ?? {}\n if (!toolCallId || !name) return\n cb.onClientToolCall({ toolCallId, name, args })\n return\n }\n\n case 'call_end': {\n const reasonRaw = String(msg.reason ?? '')\n const reason = mapEndReason(reasonRaw)\n state.endReason = reason\n state.transcript = [\n ...state.transcript,\n {\n id: `m${state.idCounter++}`,\n role: 'system',\n text: `call ended${reasonRaw ? ` (${reasonRaw})` : ''}`,\n },\n ]\n cb.onTranscript(state.transcript)\n cb.onCallEnd(reason)\n return\n }\n\n case 'error': {\n const code = (msg.code as CallErrorCode) ?? 'server_error'\n const message = (msg.message as string) ?? 'server error'\n cb.onError({ code, message })\n return\n }\n }\n}\n\nconst setState = (state: ProtocolState, next: CallState, cb: ProtocolCallbacks) => {\n if (state.state === next) return\n // The consumer's setState (cb.onState) is responsible for both\n // updating state.state and firing the user's onStateChange callback.\n // We deliberately don't mutate here — if we did, the consumer's\n // setState would early-return on the dedup check and never fire\n // onStateChange for server-driven transitions.\n cb.onState(next)\n}\n\n// Find the last uncommitted user bubble and grow it; or append a new\n// uncommitted bubble. Mirrors voice-rn upsertUserPartial.\nconst upsertUserPartial = (state: ProtocolState, text: string, isFinal: boolean) => {\n let idx = -1\n for (let i = state.transcript.length - 1; i >= 0; i--) {\n const e = state.transcript[i]\n if (e.role === 'user' && e.committed === false) {\n idx = i\n break\n }\n }\n if (idx === -1) {\n state.transcript = [\n ...state.transcript,\n { id: `m${state.idCounter++}`, role: 'user', text, committed: isFinal },\n ]\n return\n }\n const target = state.transcript[idx] as Extract<TranscriptEntry, { role: 'user' }>\n const next = [...state.transcript]\n next[idx] = { ...target, text, committed: isFinal }\n state.transcript = next\n}\n\n// ---------------------------------------------------------------------------\n// WebSocket URL builder. Identical between browser + node bundles —\n// kept here so the rare server-side change to the path / query shape is\n// a one-line edit.\n// ---------------------------------------------------------------------------\n\nexport interface BuildWsUrlArgs {\n apiBase: string\n agentId: string\n token: string\n bargeIn?: boolean\n}\n\nexport function buildWsUrl(args: BuildWsUrlArgs): string {\n const base = new URL(args.apiBase)\n const proto = base.protocol === 'https:' ? 'wss:' : 'ws:'\n const bargeQS = args.bargeIn === false ? '&barge=off' : ''\n return `${proto}//${base.host}/v1/agents/${encodeURIComponent(args.agentId)}/call?token=${encodeURIComponent(args.token)}${bargeQS}`\n}\n","// Bundle-agnostic client-tools core for @craftedxp/voice-js.\n//\n// Imports neither `ws` nor DOM `WebSocket` — the dispatcher takes an\n// injected `send` callback so the same code runs in browsers and Node.\n//\n// Wire protocol (matches the server's Zod schemas in\n// server/src/agents/clientToolFrames.ts):\n//\n// client → server client_tools_register { type, tools[] }\n// server → client client_tool_call { type, toolCallId, name, args }\n// client → server client_tool_result { type, toolCallId, result?, error? }\n\nexport interface ClientTool {\n description: string\n parameters: Record<string, unknown> // JSON Schema\n usage?: string // ≤500 chars; appended to agent system prompt\n timeoutMs?: number // server-enforced; (0, 30_000]\n example?: string // surface-only UI hint; not sent on wire\n handler: (args: Record<string, unknown>) => Promise<string | object> | string | object\n}\n\nexport type ClientToolMap = Record<string, ClientTool>\n\n// Server → client frame shape. Used by both protocol.ts (when surfacing\n// the frame to a callback) and dispatchClientToolCall (when consuming it).\nexport interface ClientToolCallFrame {\n toolCallId: string\n name: string\n args: Record<string, unknown>\n}\n\nconst NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/\nconst MAX_TOOLS = 64\nconst MAX_USAGE = 500\nconst MAX_TIMEOUT_MS = 30_000\n\n// Synchronous, throw-on-error validation. Called once at startCall time\n// before the WS opens, so bad input fails fast at the configuration\n// boundary instead of mid-call.\nexport const validateClientToolMap = (tools: ClientToolMap | undefined): void => {\n if (tools === undefined) return\n if (typeof tools !== 'object' || tools === null || Array.isArray(tools)) {\n throw new Error('clientTools must be an object keyed by tool name')\n }\n const entries = Object.entries(tools)\n if (entries.length > MAX_TOOLS) {\n throw new Error(`clientTools may declare at most 64 tools (got ${entries.length})`)\n }\n for (const [name, def] of entries) {\n if (!NAME_RE.test(name)) {\n throw new Error(\n `clientTools[\"${name}\"]: name must be a valid identifier (^[a-zA-Z_][a-zA-Z0-9_]*$)`,\n )\n }\n if (!def || typeof def !== 'object') {\n throw new Error(`clientTools[\"${name}\"]: must be an object`)\n }\n if (typeof def.description !== 'string' || def.description.length === 0) {\n throw new Error(`clientTools[\"${name}\"]: must have a description`)\n }\n if (typeof def.handler !== 'function') {\n throw new Error(`clientTools[\"${name}\"]: must have a handler function`)\n }\n if (def.usage !== undefined && def.usage.length > MAX_USAGE) {\n throw new Error(`clientTools[\"${name}\"]: usage must be ≤500 chars`)\n }\n if (\n def.timeoutMs !== undefined &&\n (!Number.isFinite(def.timeoutMs) || def.timeoutMs <= 0 || def.timeoutMs > MAX_TIMEOUT_MS)\n ) {\n throw new Error(`clientTools[\"${name}\"]: timeoutMs must be in (0, 30000]`)\n }\n }\n}\n\n// Build the registration frame the SDK sends right after `connected`.\n// Strips runtime-only fields (handler, example) and only includes the\n// optional fields when set.\nexport const buildRegisterFrame = (tools: ClientToolMap): object => ({\n type: 'client_tools_register',\n tools: Object.entries(tools).map(([name, def]) => ({\n name,\n description: def.description,\n parameters: def.parameters,\n ...(def.usage !== undefined ? { usage: def.usage } : {}),\n ...(def.timeoutMs !== undefined ? { timeoutMs: def.timeoutMs } : {}),\n })),\n})\n\ntype FrameSender = (frame: object) => void\n\n// Run the matching handler asynchronously and post a result/error frame\n// back through `send`. Send-side throws are swallowed (the WS may have\n// closed mid-call; the server has already cancelled the in-flight\n// invocation on its side).\nexport const dispatchClientToolCall = (\n send: FrameSender,\n tools: ClientToolMap,\n frame: ClientToolCallFrame,\n): void => {\n const safeSend = (payload: object) => {\n try {\n send(payload)\n } catch {\n // WS likely closed mid-handler. Server-side already cancelled.\n }\n }\n\n const tool = tools[frame.name]\n if (!tool) {\n safeSend({\n type: 'client_tool_result',\n toolCallId: frame.toolCallId,\n error: `No handler for ${frame.name}`,\n })\n return\n }\n\n void (async () => {\n try {\n const out = await tool.handler(frame.args)\n safeSend({\n type: 'client_tool_result',\n toolCallId: frame.toolCallId,\n result: typeof out === 'string' ? out : JSON.stringify(out),\n })\n } catch (err) {\n safeSend({\n type: 'client_tool_result',\n toolCallId: frame.toolCallId,\n error: err instanceof Error ? err.message : String(err),\n })\n }\n })()\n}\n","// ClientMarksBuffer — per-turn latency-mark correlator shared between the\n// browser and node SDK clients.\n//\n// Why this exists:\n// The server mints a `seq` (turnId) only AFTER the user finishes\n// speaking, at VoiceOrchestrator.handleTranscript. The client, however,\n// captures its first user-perceived mark — `t_client_mic_to_first_frame`\n// — before any server frame for the turn exists. We therefore buffer\n// client-side marks under a \"pending\" slot and back-fill the seq when\n// the server's `agent_turn_start { seq }` arrives. When `agent_turn_end\n// { seq }` lands AND we've captured the inbound `client_first_audible`\n// mark, the buffer flushes a `client_marks` frame to the server.\n//\n// Lifetime + bounds:\n// - One pending bucket at a time. A new `agent_turn_start` flushes\n// anything still pending (shouldn't happen in normal flow, but is\n// safer than silently dropping marks).\n// - If `agent_turn_end` arrives but we never captured first_audible\n// (e.g. agent had no audio output), we still emit a `client_marks`\n// frame with whatever marks are present. Missing marks are simply\n// omitted from the wire payload.\n// - The buffer is GC'd by replacement on every turn boundary; there is\n// no long-term retention. A turn whose `agent_turn_end` never\n// arrives (server crash mid-turn) is implicitly dropped on the next\n// `agent_turn_start` or when the call ends. We do NOT add an\n// explicit watchdog — observability marks aren't worth the\n// complexity of a timer here.\n//\n// Time source:\n// `performance.now()` in both browser and Node. Wall-clock-monotonic,\n// sub-ms precision. Marks are stored as raw `performance.now()` values;\n// only the deltas between them ever go on the wire.\n//\n// Wire shape (see protocol.ts):\n// { type: 'client_marks', seq, marks: { ... }, clientNow }\n//\n// The CLAUDE.md / functional-style preference: this module is a small\n// factory returning a closure-based controller. No classes, no shared\n// mutable module state.\n\nexport interface ClientMarksFrame {\n type: 'client_marks'\n seq: number\n marks: {\n // The headline number: client-measured wall-clock between the first\n // outbound audio frame for this turn and the first agent-audio\n // chunk committed for playback. Includes mic-encode time, network\n // RTT in both directions, server-side processing (STT + LLM + TTS),\n // and any client-side scheduling delay. Absent if either endpoint\n // wasn't captured.\n client_mic_to_first_audible_ms?: number\n }\n // Wall-clock at send time, ms-since-epoch. Lets the analysis script\n // estimate clock skew between client and server logs even though the\n // marks themselves are deltas.\n clientNow?: number\n}\n\n// What the consumer-facing controller exposes. The browser and node\n// clients both wire to this same surface.\nexport interface ClientMarksController {\n // Outbound mark. Called on every outgoing audio chunk; the buffer\n // gates so only the first chunk PER TURN is timestamped. `markTurnBoundary`\n // resets the gate so the next chunk is treated as the start of the\n // next turn's outbound stream.\n markFirstOutboundAudio: () => void\n\n // Inbound mark. Called every time the SDK commits an agent-audio\n // chunk for playback (browser: AudioContext schedule; node: handed to\n // consumer via onAudioChunk). Gated to first-per-turn.\n markFirstAudibleOutput: () => void\n\n // Called when the SDK receives `agent_turn_start { seq }`. Tags any\n // accumulated outbound marks with this seq. Also resets the\n // first-outbound gate so the NEXT turn's outbound mark can be\n // captured again. (Outbound is captured for a turn before its seq is\n // known; inbound is captured after, with the seq tagged.)\n onAgentTurnStart: (seq: number) => void\n\n // Called when the SDK receives `agent_turn_end { seq }`. Triggers\n // emission of the buffered `client_marks` frame for that seq, with\n // whatever marks are present (missing ones omitted).\n onAgentTurnEnd: (seq: number) => void\n\n // Force-flush any pending state. Called on call end / teardown so\n // the last turn's marks aren't dropped on hangup.\n flush: () => void\n}\n\n// `send` is the raw WS-send sink injected by the surrounding client.\n// Stringified JSON in browser/node. Returns whether the send was\n// accepted (we don't surface failure beyond the boolean — observability\n// frames are best-effort).\nexport type ClientMarksSend = (frame: ClientMarksFrame) => void\n\n// `now` defaults to performance.now() but is injected so unit tests can\n// drive a fake clock deterministically.\nexport interface CreateClientMarksBufferArgs {\n send: ClientMarksSend\n now?: () => number\n}\n\nexport const createClientMarksBuffer = (\n args: CreateClientMarksBufferArgs,\n): ClientMarksController => {\n const now = args.now ?? (() => performance.now())\n\n // Pending bucket — one in-flight turn at a time. We hold the OUTBOUND\n // mark before we know `seq`, then graduate to a per-seq bucket on\n // agent_turn_start so a second turn's inbound marks can't trample the\n // first turn's outbound mark before flush.\n let pendingFirstOutboundAt: number | null = null\n\n // Once agent_turn_start { seq } arrives we move the pending outbound\n // mark into a per-seq slot and start collecting the inbound mark.\n // Map keyed by seq; in practice never holds more than 1-2 entries\n // (one currently-flushing, one just-started).\n const inFlight = new Map<\n number,\n {\n firstOutboundAt: number | null\n firstAudibleAt: number | null\n ended: boolean\n }\n >()\n\n const tryEmit = (seq: number) => {\n const slot = inFlight.get(seq)\n if (!slot) return\n if (!slot.ended) return\n // Build wire payload. Only include marks we actually captured.\n const marks: ClientMarksFrame['marks'] = {}\n if (slot.firstOutboundAt !== null && slot.firstAudibleAt !== null) {\n marks.client_mic_to_first_audible_ms = slot.firstAudibleAt - slot.firstOutboundAt\n }\n args.send({\n type: 'client_marks',\n seq,\n marks,\n clientNow: Date.now(),\n })\n inFlight.delete(seq)\n }\n\n const markFirstOutboundAudio = () => {\n if (pendingFirstOutboundAt !== null) return // already stamped this turn\n pendingFirstOutboundAt = now()\n }\n\n const markFirstAudibleOutput = () => {\n // Inbound is captured AFTER agent_turn_start, so we know which seq\n // it belongs to: the most recent un-ended in-flight slot. If there's\n // somehow none (race: audio before agent_turn_start), drop silently.\n let target: { firstAudibleAt: number | null; ended: boolean } | undefined\n for (const slot of inFlight.values()) {\n if (!slot.ended) {\n target = slot\n // No break — last un-ended wins, matching the \"most recent\n // turn\" intent if multiple are in flight.\n }\n }\n if (!target) return\n if (target.firstAudibleAt !== null) return // first-per-turn gate\n target.firstAudibleAt = now()\n }\n\n const onAgentTurnStart = (seq: number) => {\n inFlight.set(seq, {\n firstOutboundAt: pendingFirstOutboundAt,\n firstAudibleAt: null,\n ended: false,\n })\n // Reset outbound gate so the NEXT turn captures cleanly. Note:\n // pendingFirstOutboundAt is already moved into the slot above; we\n // don't need to preserve it.\n pendingFirstOutboundAt = null\n }\n\n const onAgentTurnEnd = (seq: number) => {\n const slot = inFlight.get(seq)\n if (!slot) {\n // Got an end with no matching start (server bug, or restart). Best\n // effort: emit an empty marks frame so the analysis script knows\n // the client saw the turn at all.\n args.send({ type: 'client_marks', seq, marks: {}, clientNow: Date.now() })\n return\n }\n slot.ended = true\n tryEmit(seq)\n }\n\n const flush = () => {\n for (const seq of [...inFlight.keys()]) {\n const slot = inFlight.get(seq)!\n slot.ended = true\n tryEmit(seq)\n }\n pendingFirstOutboundAt = null\n }\n\n return {\n markFirstOutboundAudio,\n markFirstAudibleOutput,\n onAgentTurnStart,\n onAgentTurnEnd,\n flush,\n }\n}\n","// Browser VoiceClient — drives one in-progress call.\n//\n// Created by `BrowserVoiceFactory.startCall` (see browser.ts). Most\n// consumers never touch this class directly — they receive a `Call`\n// handle that exposes `end`/`mute`/`unmute` and subscribe to events\n// via the `onState` / `onTranscript` / `onError` / `onEnd` callbacks\n// they passed to `startCall`.\n//\n// This is the rough equivalent of @craftedxp/voice-rn's `useVoiceCall`\n// hook, but environment-agnostic (no React).\n\nimport { createAudioCapture, type CaptureController } from './AudioCapture'\nimport { createAudioPlayback, type PlaybackController } from './AudioPlayback'\nimport {\n createReconnectingWebSocket,\n type ReconnectingWebSocket,\n type WebSocketFactory,\n} from './ReconnectingWebSocket'\nimport {\n buildWsUrl,\n createProtocolState,\n handleServerMessage,\n type CallEndReason,\n type CallError,\n type ProtocolState,\n} from './protocol'\nimport type { Call, StartCallOptions, VoiceClientConfig } from './config'\nimport { buildRegisterFrame, dispatchClientToolCall, validateClientToolMap } from './clientTools'\nimport { createClientMarksBuffer, type ClientMarksController } from './ClientMarksBuffer'\n\ninterface VoiceClientArgs {\n // Pre-resolved by the factory.\n config: VoiceClientConfig\n options: StartCallOptions\n token: string\n // Browser uses native WebSocket; node injects `ws`.\n wsFactory: WebSocketFactory\n}\n\nexport class BrowserVoiceClient implements Call {\n private readonly args: VoiceClientArgs\n private readonly proto: ProtocolState\n\n private rws: ReconnectingWebSocket | null = null\n private capture: CaptureController | null = null\n private playback: PlaybackController | null = null\n\n private muted = false\n private inputVolume = 0\n private outputVolume = 0\n private startedAt: number | null = null\n private endedFired = false\n private lastError: CallError | null = null\n private marks: ClientMarksController\n\n constructor(args: VoiceClientArgs) {\n this.args = args\n this.proto = createProtocolState()\n validateClientToolMap(args.options.clientTools)\n // Best-effort observability frame. Wraps WS send so a failed send\n // (closed socket) is silently swallowed — these marks are never\n // load-bearing for the call itself.\n this.marks = createClientMarksBuffer({\n send: (frame) => {\n try {\n this.rws?.send(JSON.stringify(frame))\n } catch {\n // WS already gone; drop the frame.\n }\n },\n })\n }\n\n // ---------------------------------------------------------------\n // Call interface\n // ---------------------------------------------------------------\n\n get state() {\n return this.proto.state\n }\n\n get transcript() {\n return this.proto.transcript.slice()\n }\n\n get isMuted() {\n return this.muted\n }\n\n end = () => {\n this.teardown('user_hangup')\n }\n\n mute = () => {\n if (this.muted) return\n this.muted = true\n this.capture?.mute(true)\n }\n\n unmute = () => {\n if (!this.muted) return\n this.muted = false\n this.capture?.mute(false)\n }\n\n // ---------------------------------------------------------------\n // Lifecycle — called by the factory immediately after construction.\n // Resolves once the WS is open and capture is starting; mid-call\n // failures arrive via `onError`.\n // ---------------------------------------------------------------\n\n async start(): Promise<void> {\n this.setState('connecting')\n this.startedAt = Date.now()\n\n const url = buildWsUrl({\n apiBase: this.args.config.apiBase,\n agentId: this.args.options.agentId,\n token: this.args.token,\n bargeIn: this.args.options.bargeIn,\n })\n\n // Playback context needs a user gesture before its AudioContext can\n // produce sound. In practice startCall is invoked inside a click\n // handler; resume() is cheap and idempotent.\n this.playback = createAudioPlayback({\n onVolume: (v) => {\n this.outputVolume = v\n this.args.options.onVolume?.({ input: this.inputVolume, output: v })\n },\n })\n try {\n await this.playback.resume()\n } catch {\n // AudioContext may refuse if we weren't called from a gesture.\n // First enqueue() will retry the resume.\n }\n\n this.rws = createReconnectingWebSocket(\n {\n url,\n wsFactory: this.args.wsFactory,\n maxRetries: 3,\n },\n (ev) => this.handleSocketEvent(ev),\n )\n }\n\n // ---------------------------------------------------------------\n // Internal\n // ---------------------------------------------------------------\n\n private sendClientToolsRegister = () => {\n const frame = buildRegisterFrame(this.args.options.clientTools ?? {})\n this.rws?.send(JSON.stringify(frame))\n }\n\n private setState = (next: ProtocolState['state']) => {\n if (this.proto.state === next) return\n this.proto.state = next\n this.args.options.onStateChange?.(next)\n }\n\n private emitError = (err: CallError) => {\n this.lastError = err\n this.args.options.onError?.(err)\n }\n\n private handleSocketEvent = (\n ev:\n | { type: 'open' }\n | { type: 'reconnected' }\n | { type: 'message'; data: string | ArrayBuffer }\n | { type: 'close'; code: number; reason: string; permanent: boolean }\n | { type: 'error'; error: Error },\n ) => {\n switch (ev.type) {\n case 'open':\n void this.startCapture()\n break\n case 'reconnected':\n // Treat as a fresh call — server session is gone, no resume.\n this.proto.transcript = []\n this.proto.agentBubbleId = null\n this.args.options.onTranscript?.(this.proto.transcript)\n void this.startCapture()\n this.setState('listening')\n break\n case 'message':\n if (typeof ev.data === 'string') {\n handleServerMessage(ev.data, this.proto, {\n onState: this.setState,\n onTranscript: (entries) => this.args.options.onTranscript?.(entries),\n onError: this.emitError,\n onInterrupt: () => {\n this.playback?.flush()\n this.args.options.onInterrupt?.()\n },\n onAgentTurnStart: (seq) => {\n if (typeof seq === 'number') this.marks.onAgentTurnStart(seq)\n this.args.options.onAgentTurnStart?.()\n },\n onAgentTurnEnd: (seq) => {\n if (typeof seq === 'number') this.marks.onAgentTurnEnd(seq)\n },\n onCallEnd: (reason) => this.teardown(reason),\n onConnected: () => this.sendClientToolsRegister(),\n onClientToolCall: (frame) =>\n dispatchClientToolCall(\n (f) => this.rws?.send(JSON.stringify(f)),\n this.args.options.clientTools ?? {},\n frame,\n ),\n })\n } else {\n // Inbound mark: stamped at the moment we COMMIT this chunk for\n // playback (enqueue schedules into AudioContext). We\n // intentionally don't await — enqueue handles its own\n // AudioContext warm-up; the mark captures the SDK's commit\n // moment, which is the closest we can get to \"user heard it\"\n // without instrumenting the AudioContext output graph.\n this.marks.markFirstAudibleOutput()\n this.playback?.enqueue(ev.data)\n }\n break\n case 'close':\n if (ev.permanent) {\n // Pick the most informative reason: server-supplied wins, then\n // last error, then default to user_hangup.\n const reason: CallEndReason =\n this.proto.endReason ?? (this.lastError ? 'error' : 'user_hangup')\n this.teardown(reason)\n }\n break\n case 'error':\n this.emitError({ code: 'socket_error', message: ev.error.message })\n break\n }\n }\n\n private startCapture = async () => {\n if (this.capture?.isCapturing()) return\n this.capture = createAudioCapture({\n onChunk: (pcm) => {\n // Outbound mark — stamps the first frame leaving the SDK for\n // each turn. The buffer gates so only the first chunk per\n // pending-turn window is timestamped; subsequent chunks are\n // no-ops until agent_turn_start moves the slot to a per-seq\n // bucket and resets the gate. Semantic: this is \"moment audio\n // is dispatched to the WS\", which in continuous open-mic mode\n // is essentially the encode-and-handoff time per first frame\n // after a turn boundary.\n this.marks.markFirstOutboundAudio()\n this.rws?.send(pcm)\n },\n onVolume: (v) => {\n this.inputVolume = v\n this.args.options.onVolume?.({ input: v, output: this.outputVolume })\n },\n onError: (err) => {\n this.emitError({\n code: err.name === 'NotAllowedError' ? 'mic_denied' : 'mic_start_failed',\n message: err.message,\n })\n },\n })\n if (this.muted) this.capture.mute(true)\n try {\n await this.capture.start()\n } catch {\n // onError already emitted inside createAudioCapture.\n }\n }\n\n private teardown = (reason: CallEndReason) => {\n // Flush any in-flight observability marks BEFORE the WS closes so\n // the final turn's client_marks frame doesn't get dropped on\n // hangup.\n try {\n this.marks.flush()\n } catch {\n // never fatal\n }\n this.capture?.stop()\n this.capture = null\n this.playback?.close()\n this.playback = null\n try {\n this.rws?.close(1000, reason)\n } catch {\n // already closed\n }\n this.rws = null\n this.setState('ended')\n this.fireEndOnce(reason)\n }\n\n private fireEndOnce = (reason: CallEndReason) => {\n if (this.endedFired) return\n this.endedFired = true\n const startedAt = this.startedAt ?? Date.now()\n this.args.options.onEnd?.({\n reason,\n errorCode: reason === 'error' ? this.lastError?.code : undefined,\n durationMs: Date.now() - startedAt,\n })\n }\n}\n","// WebRTC transport for voice-js. Returns a `Call` with the same surface\n// as the WS path so consumers don't branch on transport. Signaling is\n// one-shot HTTP: POST /webrtc/offer (returns the SDP answer + callId);\n// trickled candidates go to POST /webrtc/ice. Audio flows over the\n// RTCPeerConnection; protocol frames (transcript, agent_turn_start, ...)\n// arrive on an in-band DataChannel named `control`.\n\nimport { createProtocolState, handleServerMessage, type ProtocolState } from '../protocol'\nimport type { CallError, CallState, TranscriptEntry } from '../protocol'\nimport type { Call } from '../config'\n\nexport interface CreateWebRtcCallOptions {\n agentId: string\n /** API base (`https://api.voissia.com` etc). Used as the signaling\n * fallback when `webrtcGatewayBase` is unset. */\n apiBase: string\n /** Pre-minted ct_. */\n token: string\n /** Gateway base from the mint response. When falsy, signaling goes to\n * `${apiBase}/v1/agents/:agentId/webrtc/{offer,ice}` (Phase-1 routes,\n * local dev). */\n webrtcGatewayBase?: string\n onStateChange?: (s: CallState) => void\n onTranscript?: (entries: TranscriptEntry[]) => void\n onError?: (e: CallError) => void\n onEnd?: () => void\n onInterrupt?: () => void\n onAgentTurnStart?: () => void\n}\n\nexport async function createWebRtcCall(opts: CreateWebRtcCallOptions): Promise<Call> {\n const proto: ProtocolState = createProtocolState()\n let muted = false\n let ended = false\n\n const fireState = (next: CallState) => {\n if (proto.state === next) return\n proto.state = next\n opts.onStateChange?.(next)\n }\n\n const dispatch = (raw: string) => {\n handleServerMessage(raw, proto, {\n onState: fireState,\n onTranscript: (entries) => opts.onTranscript?.(entries),\n onError: (err) => opts.onError?.(err),\n onInterrupt: () => opts.onInterrupt?.(),\n onAgentTurnStart: () => opts.onAgentTurnStart?.(),\n onAgentTurnEnd: () => {},\n onCallEnd: () => teardown(),\n onConnected: () => {},\n onClientToolCall: () => {},\n })\n }\n\n fireState('connecting')\n\n const pc = new RTCPeerConnection({\n iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],\n })\n\n const audioEl = document.createElement('audio')\n audioEl.autoplay = true\n audioEl.style.display = 'none'\n document.body.appendChild(audioEl)\n pc.ontrack = (event) => {\n audioEl.srcObject = event.streams[0] ?? new MediaStream([event.track])\n }\n\n let mic: MediaStream\n try {\n mic = await navigator.mediaDevices.getUserMedia({ audio: true })\n } catch (err) {\n const code =\n err instanceof DOMException && err.name === 'NotAllowedError'\n ? 'mic_denied'\n : 'mic_start_failed'\n opts.onError?.({\n code,\n message: err instanceof Error ? err.message : 'getUserMedia failed',\n })\n fireState('error')\n pc.close()\n audioEl.remove()\n throw err\n }\n for (const track of mic.getAudioTracks()) pc.addTrack(track, mic)\n\n const dc = pc.createDataChannel('control', { ordered: true })\n dc.onmessage = (e) => {\n if (typeof e.data === 'string') dispatch(e.data)\n }\n dc.onerror = () => {\n opts.onError?.({ code: 'socket_error', message: 'control channel error' })\n }\n\n const gateway = opts.webrtcGatewayBase || ''\n const offerUrl = gateway\n ? `${gateway}/webrtc/offer?token=${encodeURIComponent(opts.token)}`\n : `${opts.apiBase}/v1/agents/${encodeURIComponent(opts.agentId)}/webrtc/offer?token=${encodeURIComponent(opts.token)}`\n const iceUrl = gateway\n ? `${gateway}/webrtc/ice?token=${encodeURIComponent(opts.token)}`\n : `${opts.apiBase}/v1/agents/${encodeURIComponent(opts.agentId)}/webrtc/ice?token=${encodeURIComponent(opts.token)}`\n\n await pc.setLocalDescription(await pc.createOffer())\n\n let callId: string\n try {\n const offerRes = await fetch(offerUrl, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ sdp: pc.localDescription!.sdp, type: 'offer', agentId: opts.agentId }),\n })\n if (!offerRes.ok) {\n const code = offerRes.status === 401 ? 'unauthorized' : 'server_error'\n opts.onError?.({ code, message: `signaling failed: HTTP ${offerRes.status}` })\n fireState('error')\n mic.getTracks().forEach((t) => t.stop())\n pc.close()\n audioEl.remove()\n throw new Error(`webrtc offer failed: ${offerRes.status}`)\n }\n const body = (await offerRes.json()) as { sdp: string; type: 'answer'; callId: string }\n callId = body.callId\n await pc.setRemoteDescription({ type: 'answer', sdp: body.sdp })\n } catch (err) {\n if (!ended) {\n opts.onError?.({\n code: 'network_unreachable',\n message: err instanceof Error ? err.message : 'signaling failed',\n })\n fireState('error')\n mic.getTracks().forEach((t) => t.stop())\n pc.close()\n audioEl.remove()\n }\n throw err\n }\n\n pc.onicecandidate = (e) => {\n if (!e.candidate) return\n void fetch(iceUrl, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ callId, candidate: e.candidate }),\n }).catch(() => {\n /* best-effort */\n })\n }\n\n pc.onconnectionstatechange = () => {\n const s = pc.connectionState\n if (s === 'connected') fireState('listening')\n if (s === 'failed' || s === 'disconnected') {\n opts.onError?.({ code: 'socket_error', message: `webrtc connection ${s}` })\n teardown()\n }\n if (s === 'closed' && !ended) teardown()\n }\n\n const teardown = () => {\n if (ended) return\n ended = true\n try {\n mic.getTracks().forEach((t) => t.stop())\n } catch {\n /* already stopped */\n }\n try {\n pc.close()\n } catch {\n /* already closed */\n }\n try {\n audioEl.remove()\n } catch {\n /* already removed */\n }\n fireState('ended')\n opts.onEnd?.()\n }\n\n return {\n get state() {\n return proto.state\n },\n get transcript() {\n return proto.transcript.slice()\n },\n get isMuted() {\n return muted\n },\n end: () => teardown(),\n mute: () => {\n if (muted) return\n muted = true\n mic.getAudioTracks().forEach((t) => (t.enabled = false))\n },\n unmute: () => {\n if (!muted) return\n muted = false\n mic.getAudioTracks().forEach((t) => (t.enabled = true))\n },\n }\n}\n"],"mappings":"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","names":["VOLUME_INTERVAL_MS"]}
|
|
1
|
+
{"version":3,"sources":["../src/browser.ts","../src/config.ts","../src/worklets/mic-downsampler.worklet.js","../src/AudioCapture.ts","../src/AudioPlayback.ts","../src/ReconnectingWebSocket.ts","../src/protocol.ts","../src/clientTools.ts","../src/ClientMarksBuffer.ts","../src/VoiceClient.ts","../src/webrtc/createWebRtcCall.ts"],"sourcesContent":["// Browser entry — `import { configureVoiceClient } from '@craftedxp/voice-js'`\n//\n// Pre-injects:\n// - native `WebSocket` (globalThis.WebSocket)\n// - browser audio adapter (AudioContext + AudioWorklet + getUserMedia)\n//\n// Electron renderer + browser tabs both pick this entry via the\n// `browser` condition in package.json `exports`.\n\nimport {\n mergeStartCallContext,\n normalizeConfig,\n type Call,\n type FetchTokenResult,\n type StartCallOptions,\n type VoiceClientConfig,\n type VoiceClientFactory,\n} from './config'\nimport type { WebSocketFactory, WebSocketLike } from './ReconnectingWebSocket'\nimport { BrowserVoiceClient } from './VoiceClient'\nimport { createWebRtcCall } from './webrtc/createWebRtcCall'\n\n// Adapter from native browser WebSocket → our WebSocketLike contract.\n// One-line wrapper; the shapes already match.\nconst browserWsFactory: WebSocketFactory = (url) =>\n new (globalThis as unknown as { WebSocket: new (u: string) => WebSocketLike }).WebSocket(url)\n\nclass BrowserVoiceFactory implements VoiceClientFactory {\n readonly config: VoiceClientConfig\n\n constructor(config: VoiceClientConfig) {\n this.config = config\n }\n\n startCall = async (options: StartCallOptions): Promise<Call> => {\n if (!options.agentId) {\n throw new Error('startCall: agentId is required')\n }\n\n const { context, metadata } = mergeStartCallContext(this.config, options)\n const fetchArgs = {\n agentId: options.agentId,\n userId: options.userId,\n context,\n metadata,\n }\n\n // Resolve the token + transport. Three input shapes:\n // 1. options.token (test escape hatch) — always WS, current behaviour\n // 2. fetchToken returns a bare string — always WS (back-compat)\n // 3. fetchToken returns FetchTokenResult — server-chosen transport\n let resolved: FetchTokenResult\n if (options.token) {\n resolved = { token: options.token, transport: 'ws' }\n } else {\n const r = await this.config.fetchToken(fetchArgs)\n if (!r) {\n throw new Error('configureVoiceClient.fetchToken returned empty token')\n }\n resolved = typeof r === 'string' ? { token: r, transport: 'ws' } : r\n if (!resolved.token) {\n throw new Error('configureVoiceClient.fetchToken returned an object without `token`')\n }\n }\n\n if (resolved.transport === 'webrtc') {\n return createWebRtcCall({\n agentId: options.agentId,\n apiBase: this.config.apiBase,\n token: resolved.token,\n webrtcGatewayBase: resolved.webrtcGatewayBase,\n onStateChange: options.onStateChange,\n onTranscript: options.onTranscript,\n onError: options.onError,\n // Synthesise a minimal CallEndEvent. WebRTC doesn't carry an end reason\n // from the server yet — use 'agent_ended' as placeholder. durationMs is\n // tracked at 0 until the followup lands (see spec Followups section).\n onEnd: options.onEnd\n ? () => options.onEnd!({ reason: 'agent_ended', durationMs: 0 })\n : undefined,\n onInterrupt: options.onInterrupt,\n onAgentTurnStart: options.onAgentTurnStart,\n clientTools: options.clientTools,\n })\n }\n\n const client = new BrowserVoiceClient({\n config: this.config,\n // Carry merged context/metadata through to startCall so server can\n // see what the SDK saw.\n options: { ...options, context, metadata },\n token: resolved.token,\n wsFactory: browserWsFactory,\n })\n await client.start()\n return client\n }\n}\n\n/**\n * One-time SDK setup. Returns a factory you call `startCall` on for\n * every voice call.\n *\n * Example:\n * const voice = configureVoiceClient({\n * apiBase: 'https://api.your-server.com',\n * fetchToken: async ({ agentId }) => {\n * const r = await fetch('/api/voice-token', {\n * method: 'POST',\n * body: JSON.stringify({ agentId }),\n * })\n * return (await r.json()).token\n * },\n * })\n *\n * // Per call (typically inside a click handler):\n * const call = await voice.startCall({\n * agentId: 'agt_xxx',\n * onTranscript: (entries) => render(entries),\n * onEnd: ({ reason }) => log(reason),\n * })\n * call.mute()\n * call.end()\n */\nexport function configureVoiceClient(config: VoiceClientConfig): VoiceClientFactory {\n return new BrowserVoiceFactory(normalizeConfig(config))\n}\n\n// ---------------------------------------------------------------------------\n// Public re-exports — types + advanced primitives.\n// ---------------------------------------------------------------------------\n\nexport type {\n Call,\n ClientTool,\n ClientToolMap,\n FetchToken,\n FetchTokenArgs,\n FetchTokenResult,\n StartCallOptions,\n VoiceClientConfig,\n VoiceClientFactory,\n} from './config'\n\nexport type {\n CallEndEvent,\n CallEndReason,\n CallError,\n CallErrorCode,\n CallState,\n TranscriptEntry,\n VolumeEvent,\n} from './protocol'\n\n// Low-level primitives — exposed for advanced consumers who want to\n// drive just the mic, just the playback, or build a custom transport on\n// top of the protocol decoder. Most users should stick to\n// `configureVoiceClient().startCall()`.\nexport { createAudioCapture } from './AudioCapture'\nexport type { CaptureController, CaptureOptions, OnChunk, OnError, OnVolume } from './AudioCapture'\n\nexport { createAudioPlayback } from './AudioPlayback'\nexport type { PlaybackController, PlaybackOptions, OnAgentSpeakingChange } from './AudioPlayback'\n\nexport { createReconnectingWebSocket } from './ReconnectingWebSocket'\nexport type {\n ReconnectingWebSocket,\n RWSEvent,\n RWSOptions,\n WebSocketFactory,\n WebSocketLike,\n} from './ReconnectingWebSocket'\n\nexport { handleServerMessage, createProtocolState, buildWsUrl } from './protocol'\nexport type { ProtocolState, ProtocolCallbacks, ServerMessage } from './protocol'\n","// Public configuration surface.\n//\n// `configureVoiceClient` returns a small factory that knows how to mint\n// `ct_` tokens via your callback. Per-call you call `factory.startCall`.\n//\n// Parity with @craftedxp/voice-rn:\n// voice-rn calls `configureVoiceClient` once at app startup as a\n// side-effect (singleton), because in RN there is exactly one host\n// app and one running `<AgentCall>` at a time. In JS environments\n// the same process can drive multiple clients (multi-tenant\n// dashboards, terminal multiplexers, electron apps with several\n// panels), so the JS SDK returns the factory rather than mutating a\n// module-level singleton. Same option names, same callback shape.\n//\n// Auth model:\n// - `apiKey` is INTENTIONALLY ABSENT from the public surface.\n// Pre-0.2 had it; baking an `sk_` into a JS bundle ships your\n// server-grade credentials to every client. The right pattern is\n// `fetchToken` — your code asks YOUR backend for a short-lived\n// `ct_` and your backend uses its `sk_` (via @craftedxp/sdk-node)\n// to mint it.\n// - For tests + local prototypes a bare `token` may be passed to\n// `startCall` directly, bypassing `fetchToken`. Don't ship that to\n// production — `ct_` lifetimes are short and you want the SDK to\n// re-mint on expiry.\n\nimport type { CallEndEvent, CallError, CallState, TranscriptEntry, VolumeEvent } from './protocol'\nimport type { ClientToolMap } from './clientTools'\n\n// ---------------------------------------------------------------------------\n// fetchToken contract — matches voice-rn FetchTokenArgs verbatim.\n// ---------------------------------------------------------------------------\n\nexport interface FetchTokenArgs {\n /** The agent the SDK is about to call. */\n agentId: string\n /**\n * Optional consumer-side user identifier. Round-tripped to the server\n * as `contactId` for Phase 11 contact memory. The SDK does not\n * inspect this; your backend uses it to scope the token mint.\n */\n userId?: string\n /**\n * Per-call structured context lowered into the agent's effective\n * system prompt server-side at session open. Opaque to the SDK.\n */\n context?: Record<string, unknown>\n /**\n * String key/value pairs round-tripped on the `call.ended` webhook.\n * Capped at 1 KB total server-side. NOT lowered into the system prompt.\n */\n metadata?: Record<string, string>\n}\n\n/**\n * What `fetchToken` may return. The rich object form lets the server\n * choose the transport per call. Returning a bare string is backwards-\n * compatible — the SDK treats it as `{ token, transport: 'ws' }`.\n */\nexport interface FetchTokenResult {\n /** Raw `ct_` to feed into the WS open / WebRTC offer. */\n token: string\n /** Server-selected transport. Default `'ws'` if absent. */\n transport?: 'ws' | 'webrtc'\n /** Required when `transport === 'webrtc'` AND the server uses a\n * separate signaling gateway. When omitted on a webrtc result, the\n * SDK falls back to the API base's Phase-1 routes (local dev). */\n webrtcGatewayBase?: string\n}\n\nexport type FetchToken = (args: FetchTokenArgs) => Promise<string | FetchTokenResult>\n\n// ---------------------------------------------------------------------------\n// Factory configuration\n// ---------------------------------------------------------------------------\n\nexport interface VoiceClientConfig {\n /**\n * Full HTTPS URL of the Voissia server. The WebSocket scheme is\n * derived: `https` → `wss`, `http` → `ws`. No trailing slash needed.\n */\n apiBase: string\n /**\n * Called by the SDK whenever it needs a fresh `ct_` token (initial\n * connect; mid-call refresh on `token_expired`). Your implementation\n * should hit YOUR backend, which holds the `sk_` API key and mints\n * via `POST /v1/call-tokens` (or `client.callTokens.mint` from\n * @craftedxp/sdk-node). Never embed `sk_` in JS code that ships to a\n * client.\n */\n fetchToken: FetchToken\n /**\n * Optional metadata applied to EVERY startCall. Per-call `metadata`\n * in `startCall` is merged on top (per-call wins on key conflicts).\n * Useful for dashboard-wide tags like `{ surface: 'web', appVersion }`.\n */\n defaultMetadata?: Record<string, string>\n /**\n * Optional context applied to EVERY startCall. Per-call `context` in\n * `startCall` is merged on top. Useful for cross-call invariants like\n * the signed-in user's locale.\n */\n defaultContext?: Record<string, unknown>\n}\n\n// ---------------------------------------------------------------------------\n// Per-call options — startCall({ ... })\n// ---------------------------------------------------------------------------\n\nexport interface StartCallOptions {\n /** The agent to call. */\n agentId: string\n /** Per-call user identifier. Round-tripped to fetchToken as `userId`. */\n userId?: string\n /**\n * Per-call structured context. Merged on top of `defaultContext`\n * configured at factory time.\n */\n context?: Record<string, unknown>\n /**\n * Per-call metadata. Merged on top of `defaultMetadata` configured\n * at factory time.\n */\n metadata?: Record<string, string>\n /**\n * When false, the SDK + server stay full-duplex but barge-in is\n * suppressed. Useful for alarm-style flows where the user shouldn't\n * accidentally interrupt the script. Default true.\n */\n bargeIn?: boolean\n /**\n * Client-side tools the agent's LLM can call mid-conversation. Each\n * tool's handler runs on the consumer's side; result is fed back to\n * the LLM through the existing call WebSocket. Schema and handler\n * colocate. Validated synchronously at startCall — bad input throws.\n *\n * See docs/integration-echocheck.md for the wire protocol and the\n * server-side guarantees.\n */\n clientTools?: ClientToolMap\n /**\n * Test-only escape hatch — pass a pre-minted `ct_` directly and skip\n * the `fetchToken` call. Don't use this in production code: tokens\n * expire and the SDK can't re-mint without the callback.\n */\n token?: string\n\n // Event callbacks — same shape as voice-rn UseVoiceCallOptions.\n onStateChange?: (state: CallState) => void\n onTranscript?: (entries: TranscriptEntry[]) => void\n onError?: (err: CallError) => void\n onEnd?: (end: CallEndEvent) => void\n /** Volume-meter event for VU UIs. ~10 Hz cadence (browser bundle only). */\n onVolume?: (vol: VolumeEvent) => void\n /**\n * Fires when the server signals barge-in (the user started talking\n * mid-agent-turn). The browser bundle automatically flushes its\n * built-in audio playback before this callback runs; the callback is\n * fired regardless. Node / Electron consumers with custom playback\n * should drain their audio queue here so the agent goes silent\n * immediately.\n */\n onInterrupt?: () => void\n /**\n * Fires on `agent_turn_start` — the server has begun a new agent\n * turn. The state-machine transition to `agent_speaking` happens at\n * the same moment via `onStateChange`; use this when you want a\n * precise turn anchor (e.g. \"agent has been speaking for N ms\" UIs)\n * without diffing state.\n */\n onAgentTurnStart?: () => void\n}\n\n// ---------------------------------------------------------------------------\n// Call handle returned from startCall — matches voice-rn\n// VoiceCallController where it makes sense.\n// ---------------------------------------------------------------------------\n\nexport interface Call {\n /** Current state. Snapshot — subscribe via onStateChange for live updates. */\n readonly state: CallState\n /** Full transcript so far. Snapshot — subscribe via onTranscript for live updates. */\n readonly transcript: TranscriptEntry[]\n /** True after `mute()` and before `unmute()`. */\n readonly isMuted: boolean\n /** End the call locally. Closes the WS, stops the mic, fires onEnd. Idempotent. */\n end: () => void\n /** Mute mic frames. Wire stays active so server endpointing doesn't false-positive. Idempotent. */\n mute: () => void\n /** Unmute mic frames. Idempotent. */\n unmute: () => void\n}\n\n// ---------------------------------------------------------------------------\n// Factory contract — what configureVoiceClient returns. The actual\n// implementation differs between browser (audio-equipped) and node (raw\n// PCM) bundles; both satisfy this interface.\n// ---------------------------------------------------------------------------\n\nexport interface VoiceClientFactory {\n /** Read back the resolved config (post trailing-slash normalisation). */\n readonly config: VoiceClientConfig\n /**\n * Open a fresh call. Returns when the WS is open; rejects on\n * pre-flight failure (missing config, fetchToken throw, etc). Mid-\n * call failures arrive via the per-call `onError` callback — they\n * don't reject this promise.\n */\n startCall: (options: StartCallOptions) => Promise<Call>\n}\n\n// ---------------------------------------------------------------------------\n// Pre-flight validation. Pulled out so both bundles share the exact\n// same \"missing field\" error messages.\n// ---------------------------------------------------------------------------\n\nexport function normalizeConfig(config: VoiceClientConfig): VoiceClientConfig {\n if (!config) throw new Error('configureVoiceClient: config is required')\n if ('apiKey' in (config as object)) {\n throw new Error(\n 'configureVoiceClient: `apiKey` is no longer supported. Embedding sk_ in JS code ships server-grade credentials to every client. Pass `fetchToken: async ({ agentId }) => { /* call YOUR backend mint */ }` instead — see the @craftedxp/voice-js README for the migration recipe.',\n )\n }\n if (!config.apiBase) {\n throw new Error('configureVoiceClient: apiBase is required')\n }\n if (typeof config.fetchToken !== 'function') {\n throw new Error('configureVoiceClient: fetchToken must be a function')\n }\n return {\n ...config,\n apiBase: config.apiBase.replace(/\\/+$/, ''),\n }\n}\n\n// Merge factory-level defaults with per-call overrides. Per-call wins.\nexport function mergeStartCallContext(\n factory: VoiceClientConfig,\n call: StartCallOptions,\n): { context?: Record<string, unknown>; metadata?: Record<string, string> } {\n const context =\n factory.defaultContext || call.context\n ? { ...(factory.defaultContext ?? {}), ...(call.context ?? {}) }\n : undefined\n const metadata =\n factory.defaultMetadata || call.metadata\n ? { ...(factory.defaultMetadata ?? {}), ...(call.metadata ?? {}) }\n : undefined\n return { context, metadata }\n}\n\nexport type { ClientTool, ClientToolMap } from './clientTools'\n","// AudioWorklet — runs off the main thread in the audio rendering graph.\n//\n// Input: Float32 samples at the AudioContext's native sampleRate (typically\n// 48000 Hz on desktop, 44100 Hz on some iOS devices).\n// Output: 16 kHz mono Int16 PCM, shipped to the main thread via\n// `port.postMessage(ArrayBuffer, [ArrayBuffer])` (transferred, not copied).\n//\n// Why AudioWorklet instead of ScriptProcessorNode: ScriptProcessorNode is\n// deprecated + main-thread-bound, so any JS jank produces audible audio\n// glitches. AudioWorklet's `process()` runs on the audio rendering thread\n// at the graph's block cadence (128 frames by default) and backpressures\n// via returning `true` / `false`.\n//\n// This file is loaded as text (see tsup.config.ts loader) and registered\n// at runtime via `audioWorklet.addModule(blobUrl)`.\n\nclass MicDownsampler extends AudioWorkletProcessor {\n constructor() {\n super()\n // Target sample rate for STT. Matches Deepgram Nova-3 + the platform's\n // server-side SAMPLE_RATE constant in AgentCallHandler.\n this.targetRate = 16000\n // Accumulator for the downsample. We collect incoming samples and emit\n // an Int16 chunk when we've accumulated ~1024 target-rate samples\n // (~64 ms at 16 kHz) — matches the mobile SDK's chunk size so both\n // platforms have the same server-side framing.\n this.outputFrames = 1024\n this.acc = []\n // Running index used for fractional resampling.\n this.readCursor = 0\n }\n\n // `inputs[0][0]` = first channel of first input. 128 Float32 samples per\n // call at the context's sampleRate. Return true = keep processing.\n process(inputs) {\n const input = inputs[0]\n if (!input || input.length === 0) return true\n const channel = input[0]\n if (!channel || channel.length === 0) return true\n\n const ctxRate = sampleRate // global inside AudioWorkletProcessor\n const ratio = ctxRate / this.targetRate\n\n // Simple linear-interp downsample. For 48000 → 16000 that's 3:1, which\n // linear handles fine for voice. Anti-alias filtering would be\n // theoretically better but inaudible for speech.\n for (let i = 0; i < channel.length; i++) {\n this.acc.push(channel[i])\n }\n\n while (this.acc.length - this.readCursor >= ratio * this.outputFrames) {\n const out = new Int16Array(this.outputFrames)\n let readIdx = this.readCursor\n for (let i = 0; i < this.outputFrames; i++) {\n // Linear interp between floor(readIdx) and ceil(readIdx)\n const low = Math.floor(readIdx)\n const high = Math.min(low + 1, this.acc.length - 1)\n const frac = readIdx - low\n const sample = this.acc[low] * (1 - frac) + this.acc[high] * frac\n // Clip + convert to int16\n const clipped = Math.max(-1, Math.min(1, sample))\n out[i] = clipped < 0 ? clipped * 0x8000 : clipped * 0x7fff\n readIdx += ratio\n }\n // Transfer the ArrayBuffer (zero-copy) to the main thread.\n this.port.postMessage(out.buffer, [out.buffer])\n this.readCursor = readIdx\n }\n\n // Garbage-collect the consumed portion of `acc` every so often so it\n // doesn't grow without bound. Leave ~one chunk of headroom.\n if (this.readCursor > ratio * this.outputFrames) {\n this.acc = this.acc.slice(Math.floor(this.readCursor))\n this.readCursor -= Math.floor(this.readCursor)\n }\n\n return true\n }\n}\n\nregisterProcessor('mic-downsampler', MicDownsampler)\n","// Mic capture pipeline:\n// getUserMedia → AudioContext → AudioWorkletNode → Int16 PCM chunks\n//\n// We register the worklet from an inline Blob URL so consumers don't need\n// to host or bundle a separate .js file. The worklet source is loaded as\n// text at build time via tsup's custom loader (see tsup.config.ts).\n\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore — text-loaded by tsup\nimport workletSource from './worklets/mic-downsampler.worklet.js'\n\nexport type OnChunk = (pcm: ArrayBuffer) => void\nexport type OnVolume = (rms01: number) => void\nexport type OnError = (err: Error) => void\n\nexport interface CaptureOptions {\n onChunk: OnChunk\n // Optional — when set we compute input RMS and call this at ~10Hz. Used\n // for volume visualisers in consumer UIs.\n onVolume?: OnVolume\n onError?: OnError\n}\n\nexport interface CaptureController {\n start: () => Promise<void>\n stop: () => void\n mute: (muted: boolean) => void\n isCapturing: () => boolean\n}\n\n// Volume-meter cadence. 100ms = 10Hz updates; smoother than per-chunk\n// (16Hz) without flicker.\nconst VOLUME_INTERVAL_MS = 100\n\nexport const createAudioCapture = (options: CaptureOptions): CaptureController => {\n let audioContext: AudioContext | null = null\n let mediaStream: MediaStream | null = null\n let sourceNode: MediaStreamAudioSourceNode | null = null\n let workletNode: AudioWorkletNode | null = null\n let analyser: AnalyserNode | null = null\n let volumeTimer: ReturnType<typeof setInterval> | null = null\n let muted = false\n let capturing = false\n\n const computeRms = (buf: Float32Array): number => {\n let sum = 0\n for (let i = 0; i < buf.length; i++) sum += buf[i] * buf[i]\n const rms = Math.sqrt(sum / buf.length)\n // Clamp to 0..1 — RMS of a full-scale signal is 0.707, so boost a bit\n // so \"normal speech\" hits ~0.5 in the visualiser.\n return Math.min(1, rms * 1.8)\n }\n\n const start = async () => {\n if (capturing) return\n try {\n // Browsers only grant getUserMedia on secure contexts (https or\n // localhost). The thrown DOMException is clear enough — we let it\n // propagate with a wrapped error message for clarity.\n mediaStream = await navigator.mediaDevices.getUserMedia({\n audio: {\n // Hand tuning for voice agent use: we want the raw signal so the\n // server-side STT can do its own noise handling. Disable browser\n // AEC/AGC/NR — experimentally they fight with whatever processing\n // the TTS playback path feeds back in over speakers.\n echoCancellation: true,\n noiseSuppression: true,\n autoGainControl: true,\n channelCount: 1,\n },\n })\n\n audioContext = new AudioContext()\n // Most browsers suspend AudioContext until a user gesture. connect()\n // is idempotent; if we were constructed inside a click/touch handler\n // it's already running.\n if (audioContext.state === 'suspended') await audioContext.resume()\n\n // Load + register the worklet from an inline Blob URL. One-time per\n // context; subsequent `new AudioWorkletNode` calls are cheap.\n const blob = new Blob([workletSource], { type: 'application/javascript' })\n const url = URL.createObjectURL(blob)\n try {\n await audioContext.audioWorklet.addModule(url)\n } finally {\n URL.revokeObjectURL(url)\n }\n\n sourceNode = audioContext.createMediaStreamSource(mediaStream)\n workletNode = new AudioWorkletNode(audioContext, 'mic-downsampler')\n\n // Worklet → main thread: Int16 PCM chunks\n workletNode.port.onmessage = (event: MessageEvent<ArrayBuffer>) => {\n if (muted) return\n options.onChunk(event.data)\n }\n\n // Analyser sits in parallel to the worklet for the volume meter. Not\n // strictly necessary — we could compute RMS inside the worklet and\n // post it too — but AnalyserNode avoids extra bridge chatter.\n if (options.onVolume) {\n analyser = audioContext.createAnalyser()\n analyser.fftSize = 256\n sourceNode.connect(analyser)\n const buf = new Float32Array(analyser.fftSize)\n volumeTimer = setInterval(() => {\n if (!analyser) return\n analyser.getFloatTimeDomainData(buf)\n options.onVolume?.(computeRms(buf))\n }, VOLUME_INTERVAL_MS)\n }\n\n sourceNode.connect(workletNode)\n // The worklet doesn't need to feed into the destination — its only\n // job is to emit messages. But the AudioContext graph keeps the\n // worklet running only if it's connected somewhere; connect to a\n // zero-gain node to park it.\n const sink = audioContext.createGain()\n sink.gain.value = 0\n workletNode.connect(sink).connect(audioContext.destination)\n\n capturing = true\n } catch (err) {\n const wrapped =\n err instanceof Error ? err : new Error(typeof err === 'string' ? err : 'capture failed')\n options.onError?.(wrapped)\n throw wrapped\n }\n }\n\n const stop = () => {\n if (!capturing) return\n capturing = false\n if (volumeTimer) {\n clearInterval(volumeTimer)\n volumeTimer = null\n }\n try {\n workletNode?.disconnect()\n analyser?.disconnect()\n sourceNode?.disconnect()\n } catch {\n // already disconnected\n }\n workletNode = null\n analyser = null\n sourceNode = null\n if (mediaStream) {\n for (const track of mediaStream.getTracks()) track.stop()\n mediaStream = null\n }\n if (audioContext && audioContext.state !== 'closed') {\n void audioContext.close().catch(() => undefined)\n }\n audioContext = null\n }\n\n return {\n start,\n stop,\n mute: (v) => {\n muted = v\n },\n isCapturing: () => capturing,\n }\n}\n","// Playback pipeline:\n// WebSocket binary frame (Int16 PCM @ 16 kHz) → AudioBufferSourceNode\n// chained for gapless playback → AudioContext destination.\n//\n// We deliberately don't use MediaSource or a single streaming source —\n// each agent TTS chunk is scheduled as its own short AudioBuffer so we\n// can interrupt (flush the queue) the moment a barge-in lands. One chunk\n// ≈ 20–100ms of audio.\n\nexport type OnVolume = (rms01: number) => void\nexport type OnAgentSpeakingChange = (speaking: boolean) => void\n\nexport interface PlaybackOptions {\n // Sample rate of the incoming PCM — matches AgentCallHandler.SAMPLE_RATE.\n sampleRate?: number\n onVolume?: OnVolume\n onSpeakingChange?: OnAgentSpeakingChange\n}\n\nexport interface PlaybackController {\n enqueue: (pcm: ArrayBuffer) => void\n flush: () => void\n close: () => void\n resume: () => Promise<void>\n}\n\nconst DEFAULT_SAMPLE_RATE = 16_000\nconst VOLUME_INTERVAL_MS = 100\n\nexport const createAudioPlayback = (options: PlaybackOptions = {}): PlaybackController => {\n const sampleRate = options.sampleRate ?? DEFAULT_SAMPLE_RATE\n let audioContext: AudioContext | null = null\n let gainNode: GainNode | null = null\n let analyser: AnalyserNode | null = null\n let volumeTimer: ReturnType<typeof setInterval> | null = null\n let nextStartTime = 0\n let scheduledNodes: AudioBufferSourceNode[] = []\n let speaking = false\n\n const ensureContext = async () => {\n if (audioContext) {\n if (audioContext.state === 'suspended') await audioContext.resume()\n return\n }\n // Pass the target sample rate as a hint. Browsers may choose a\n // different rate (48000 is most common); `createBuffer(numChannels,\n // length, sampleRate)` with a different rate than the context will\n // trigger resampling on `start()`, which the browser handles natively.\n audioContext = new AudioContext({ sampleRate })\n gainNode = audioContext.createGain()\n if (options.onVolume) {\n analyser = audioContext.createAnalyser()\n analyser.fftSize = 256\n gainNode.connect(analyser)\n const buf = new Float32Array(analyser.fftSize)\n volumeTimer = setInterval(() => {\n if (!analyser) return\n analyser.getFloatTimeDomainData(buf)\n let sum = 0\n for (let i = 0; i < buf.length; i++) sum += buf[i] * buf[i]\n const rms = Math.sqrt(sum / buf.length)\n options.onVolume?.(Math.min(1, rms * 1.8))\n }, VOLUME_INTERVAL_MS)\n }\n gainNode.connect(audioContext.destination)\n nextStartTime = audioContext.currentTime\n }\n\n const setSpeaking = (v: boolean) => {\n if (v === speaking) return\n speaking = v\n options.onSpeakingChange?.(v)\n }\n\n const pruneFinished = () => {\n const now = audioContext?.currentTime ?? 0\n scheduledNodes = scheduledNodes.filter((n) => {\n const node = n as AudioBufferSourceNode & { _endsAt?: number }\n return (node._endsAt ?? 0) > now\n })\n if (scheduledNodes.length === 0) setSpeaking(false)\n }\n\n const enqueue = (pcm: ArrayBuffer) => {\n // We don't want to await here — WS onmessage is hot-path, and AudioContext\n // resume is cheap once warmed. `void` the promise and proceed on next\n // tick; if the context isn't ready, the first chunk may land a hair\n // late but subsequent chunks catch up.\n if (!audioContext) {\n void ensureContext().then(() => enqueue(pcm))\n return\n }\n if (!audioContext || !gainNode) return\n\n const int16 = new Int16Array(pcm)\n if (int16.length === 0) return\n\n const audioBuffer = audioContext.createBuffer(1, int16.length, sampleRate)\n const float32 = audioBuffer.getChannelData(0)\n for (let i = 0; i < int16.length; i++) {\n float32[i] = int16[i] / 0x8000\n }\n\n const node = audioContext.createBufferSource()\n node.buffer = audioBuffer\n node.connect(gainNode)\n\n const now = audioContext.currentTime\n // Schedule back-to-back. If we've fallen behind (mic lag, WS gap),\n // re-anchor to now so we don't skew the whole queue further.\n const startAt = Math.max(now, nextStartTime)\n node.start(startAt)\n\n const duration = int16.length / sampleRate\n ;(node as AudioBufferSourceNode & { _endsAt?: number })._endsAt = startAt + duration\n nextStartTime = startAt + duration\n\n scheduledNodes.push(node)\n setSpeaking(true)\n\n // Prune after every enqueue so `speaking` collapses quickly when the\n // queue drains. Cheap O(n) over a short list.\n node.onended = () => pruneFinished()\n }\n\n // Barge-in: stop anything pending, reset the schedule pointer.\n //\n // We can't simply `.stop()` playing sources mid-buffer and expect the\n // AudioContext's destination to be silent immediately — Safari in\n // particular sometimes plays out a few ms of tail. Reliable approach:\n // swap the gainNode out so nothing after this point is audible, then\n // clean up the old one on the next tick.\n const flush = () => {\n if (!audioContext || !gainNode) return\n for (const node of scheduledNodes) {\n try {\n node.stop()\n } catch {\n // already stopped\n }\n }\n scheduledNodes = []\n // Swap gainNode so even stubborn trailing samples on the old node\n // don't reach the speakers.\n gainNode.disconnect()\n gainNode = audioContext.createGain()\n if (analyser) {\n analyser.disconnect()\n gainNode.connect(analyser)\n }\n gainNode.connect(audioContext.destination)\n nextStartTime = audioContext.currentTime\n setSpeaking(false)\n }\n\n const close = () => {\n flush()\n if (volumeTimer) {\n clearInterval(volumeTimer)\n volumeTimer = null\n }\n if (audioContext && audioContext.state !== 'closed') {\n void audioContext.close().catch(() => undefined)\n }\n audioContext = null\n gainNode = null\n analyser = null\n }\n\n const resume = async () => {\n await ensureContext()\n }\n\n return { enqueue, flush, close, resume }\n}\n","// Minimal auto-reconnecting WebSocket wrapper.\n//\n// Scope: transparent reconnection on unexpected drops (network blip,\n// server restart). We deliberately do NOT try to preserve call state\n// across reconnects — the server's AgentCallHandler is session-scoped\n// and won't resume where we left off. If the consumer needs mid-call\n// resilience, that's a server-side resumable-session feature, not a\n// client-side retry.\n//\n// In v1 we reconnect only when `connect()` has been called and\n// `.close(1000, ...)` has NOT been called by the consumer (i.e., we drop\n// because of network, not because the user hung up). On unclean close\n// the consumer gets a fresh connection + a `reconnected` event so they\n// can re-issue any greeting / context.\n//\n// Transport agnostic — accepts a WebSocket-constructor factory so the\n// browser bundle uses native `WebSocket` and the node bundle injects the\n// `ws` package.\n\nexport type RWSEvent =\n | { type: 'open' }\n | { type: 'reconnected' }\n | { type: 'message'; data: string | ArrayBuffer }\n | { type: 'close'; code: number; reason: string; permanent: boolean }\n | { type: 'error'; error: Error }\n\n// Minimal WebSocket-like contract. Both browser `WebSocket` and the\n// `ws` package's WebSocket satisfy it.\nexport interface WebSocketLike {\n binaryType: string\n readyState: number\n onopen: ((ev: unknown) => void) | null\n onmessage: ((ev: { data: string | ArrayBuffer }) => void) | null\n onerror: ((ev: unknown) => void) | null\n onclose: ((ev: { code: number; reason: string }) => void) | null\n send: (data: string | ArrayBuffer | ArrayBufferView) => void\n close: (code?: number, reason?: string) => void\n}\n\nexport type WebSocketFactory = (url: string) => WebSocketLike\n\nexport interface RWSOptions {\n url: string\n // Factory so we can swap browser-native WebSocket for the `ws` package\n // in node. Browser entry pre-fills with a wrapper around globalThis.WebSocket.\n wsFactory: WebSocketFactory\n // Cap the number of auto-reconnect attempts. `0` disables retry entirely\n // (closer to native WebSocket semantics). Default: 3.\n maxRetries?: number\n // Initial backoff; doubles up to maxBackoffMs.\n initialBackoffMs?: number\n maxBackoffMs?: number\n}\n\nconst READYSTATE_OPEN = 1\nconst READYSTATE_CLOSED = 3\n\nexport const createReconnectingWebSocket = (\n options: RWSOptions,\n onEvent: (ev: RWSEvent) => void,\n) => {\n const maxRetries = options.maxRetries ?? 3\n const initialBackoff = options.initialBackoffMs ?? 500\n const maxBackoff = options.maxBackoffMs ?? 8000\n\n let ws: WebSocketLike | null = null\n let intentionalClose = false\n let retries = 0\n let backoff = initialBackoff\n let reconnectTimer: ReturnType<typeof setTimeout> | null = null\n\n const openOnce = () => {\n ws = options.wsFactory(options.url)\n // Binary frames arrive as Int16 PCM from the server. ArrayBuffer is\n // more convenient than Blob (one fewer async read) — we `.arrayBuffer()`\n // a Blob manually only if we receive one unexpectedly.\n ws.binaryType = 'arraybuffer'\n ws.onopen = () => {\n if (retries === 0) onEvent({ type: 'open' })\n else onEvent({ type: 'reconnected' })\n retries = 0\n backoff = initialBackoff\n }\n ws.onmessage = (ev) => {\n onEvent({ type: 'message', data: ev.data as string | ArrayBuffer })\n }\n ws.onerror = () => {\n onEvent({ type: 'error', error: new Error('WebSocket error') })\n }\n ws.onclose = (ev) => {\n ws = null\n const shouldRetry = !intentionalClose && retries < maxRetries\n if (!shouldRetry) {\n onEvent({\n type: 'close',\n code: ev.code,\n reason: ev.reason,\n permanent: true,\n })\n return\n }\n onEvent({\n type: 'close',\n code: ev.code,\n reason: ev.reason,\n permanent: false,\n })\n retries++\n const delay = Math.min(backoff, maxBackoff)\n backoff = Math.min(backoff * 2, maxBackoff)\n reconnectTimer = setTimeout(openOnce, delay)\n }\n }\n\n openOnce()\n\n return {\n send: (data: string | ArrayBuffer | ArrayBufferView) => {\n if (ws && ws.readyState === READYSTATE_OPEN) ws.send(data)\n },\n close: (code = 1000, reason = 'client-requested') => {\n intentionalClose = true\n if (reconnectTimer) {\n clearTimeout(reconnectTimer)\n reconnectTimer = null\n }\n try {\n ws?.close(code, reason)\n } catch {\n // already closed\n }\n },\n readyState: () => ws?.readyState ?? READYSTATE_CLOSED,\n }\n}\n\nexport type ReconnectingWebSocket = ReturnType<typeof createReconnectingWebSocket>\n","// Wire-protocol types + stateless transcript reducer.\n//\n// Shared between the browser and node bundles. The protocol surface is\n// matched (where it makes sense) to @craftedxp/voice-rn 0.3.x so a\n// consumer can write the same `onTranscript` / `onStateChange` /\n// `onError` / `onEnd` shape for both web and RN clients.\n\nimport type { ClientToolCallFrame } from './clientTools'\n\n// Re-export so consumers / dashboards can type the wire shape. The\n// `client_marks` frame itself is purely an internal observability\n// channel between the SDK and server — it's emitted by the SDK and\n// logged on the server, never surfaced to consumer code.\nexport type { ClientMarksFrame } from './ClientMarksBuffer'\n\n// ---------------------------------------------------------------------------\n// State machine\n// ---------------------------------------------------------------------------\n\nexport type CallState =\n | 'idle'\n | 'connecting'\n | 'listening'\n | 'user_speaking'\n | 'agent_speaking'\n | 'ended'\n | 'error'\n\n// ---------------------------------------------------------------------------\n// Transcript model — matches voice-rn TranscriptEntry\n// ---------------------------------------------------------------------------\n\nexport type TranscriptEntry =\n | { id: string; role: 'user'; text: string; committed: boolean }\n | { id: string; role: 'agent'; text: string; interrupted?: boolean }\n | { id: string; role: 'tool'; text: string }\n | { id: string; role: 'system'; text: string }\n\n// ---------------------------------------------------------------------------\n// Stable error code contract — matches voice-rn CallErrorCode where the\n// failure modes overlap. Web-specific codes (mic_denied via getUserMedia\n// rejection, etc.) keep their voice-rn names so cross-platform consumers\n// can write one switch statement.\n// ---------------------------------------------------------------------------\n\nexport type CallErrorCode =\n // Programming errors — surface loudly to the host's developer.\n | 'missing_credentials'\n | 'forbidden'\n // Browser audio failures.\n | 'mic_denied'\n | 'mic_start_failed'\n | 'audio_session_failed'\n // Auth lifecycle.\n | 'token_expired'\n | 'token_invalid'\n | 'unauthorized'\n // Network / connectivity.\n | 'network_unreachable'\n | 'socket_error'\n // Business state.\n | 'payment_required'\n | 'not_found'\n // End-of-call states surfaced via onError (also via onEnd reason='timeout').\n | 'silence_timeout'\n // Catch-all for unexpected server / 5xx / 1011.\n | 'server_error'\n\nexport interface CallError {\n code: CallErrorCode\n message: string\n}\n\n// ---------------------------------------------------------------------------\n// End-of-call signal — matches voice-rn CallEndReason / CallEndEvent\n// ---------------------------------------------------------------------------\n\nexport type CallEndReason = 'agent_ended' | 'user_hangup' | 'timeout' | 'error'\n\nexport interface CallEndEvent {\n reason: CallEndReason\n // Present iff reason === 'error'. Mirrors the code from the most\n // recent onError.\n errorCode?: CallErrorCode\n // Wallclock from start() resolving to the WS close. Useful for billing\n // / \"you spoke for 1m23s\" UIs without forcing the host to track\n // timestamps themselves.\n durationMs: number\n}\n\n// ---------------------------------------------------------------------------\n// Volume meter event (browser bundle only — node bundle leaves volume to\n// the consumer if they're processing PCM themselves).\n// ---------------------------------------------------------------------------\n\nexport interface VolumeEvent {\n // 0-1 RMS over the last ~100ms. Bind to a waveform / level meter.\n input: number\n output: number\n}\n\n// ---------------------------------------------------------------------------\n// Server → client message envelope. Loose typing — the server can add new\n// `type` values without breaking the SDK; unknown types are ignored by\n// the dispatch in handleServerMessage.\n// ---------------------------------------------------------------------------\n\nexport type ServerMessage = Record<string, unknown> & { type?: string }\n\n// ---------------------------------------------------------------------------\n// Stateless transcript reducer + state-machine helpers. Both the browser\n// and node clients call into these so the shape of the transcript stays\n// identical across environments.\n// ---------------------------------------------------------------------------\n\nexport interface ProtocolState {\n state: CallState\n transcript: TranscriptEntry[]\n agentBubbleId: string | null\n idCounter: number\n // Reason latched from the server's call_end frame. Read by the\n // surrounding client when the WS finally closes so onEnd fires with the\n // right reason instead of falling back to user_hangup.\n endReason: CallEndReason | null\n}\n\nexport const createProtocolState = (): ProtocolState => ({\n state: 'idle',\n transcript: [],\n agentBubbleId: null,\n idCounter: 0,\n endReason: null,\n})\n\n// Side-effect callbacks the protocol layer fires as it processes server\n// frames. The surrounding client wires these up to event emitters\n// (browser) or to user-supplied callbacks (node).\nexport interface ProtocolCallbacks {\n onState: (next: CallState) => void\n onTranscript: (entries: TranscriptEntry[]) => void\n onError: (err: CallError) => void\n // Fires on `interrupt` — caller should flush its audio playback queue.\n onInterrupt: () => void\n // Fires on `agent_turn_start` — caller may want to reset its turn\n // anchor for \"agent has been speaking N ms\" UIs. `seq` is the\n // server-assigned turn ID; absent on legacy server builds that don't\n // emit one (we treat that case as \"no correlation possible\").\n onAgentTurnStart: (seq?: number) => void\n // Fires on `agent_turn_end` — caller may use this to flush per-turn\n // observability marks. `seq` matches the corresponding agent_turn_start.\n onAgentTurnEnd: (seq?: number) => void\n // Fires on `call_end` — caller closes its WS and resolves onEnd.\n onCallEnd: (reason: CallEndReason) => void\n // Fires on `connected` — caller should send the client_tools_register\n // frame here so the server's first-turn grace window unblocks.\n onConnected: () => void\n // Fires on `client_tool_call` — caller dispatches to the matching\n // tool handler and posts back a client_tool_result frame.\n onClientToolCall: (frame: ClientToolCallFrame) => void\n}\n\n// Map server-supplied endReason strings onto our SDK-side CallEndReason.\nconst mapEndReason = (raw: string): CallEndReason => {\n if (raw === 'agent_ended') return 'agent_ended'\n if (raw === 'caller_hung_up') return 'user_hangup'\n if (raw === 'silence_timeout' || raw === 'max_duration') return 'timeout'\n return 'error'\n}\n\n// Pure-ish transcript reducer + dispatcher. Mutates `state` in place to\n// match the imperative pattern used by both clients; returns nothing.\n//\n// The \"agent_text\" / \"transcript\" interim handling is identical to\n// voice-rn's useVoiceCall — one growing user bubble while interim, one\n// growing agent bubble per turn.\nexport function handleServerMessage(\n raw: string,\n state: ProtocolState,\n cb: ProtocolCallbacks,\n): void {\n let msg: ServerMessage\n try {\n msg = JSON.parse(raw)\n } catch {\n return\n }\n\n switch (msg.type) {\n case 'connected':\n cb.onConnected()\n setState(state, 'listening', cb)\n return\n\n case 'transcript': {\n const text = (msg.text as string) ?? ''\n if (!text) return\n const isFinal = !!msg.isFinal\n if (!isFinal) setState(state, 'user_speaking', cb)\n upsertUserPartial(state, text, isFinal)\n cb.onTranscript(state.transcript)\n return\n }\n\n case 'agent_turn_start': {\n const id = `m${state.idCounter++}`\n state.agentBubbleId = id\n state.transcript = [...state.transcript, { id, role: 'agent', text: '' }]\n cb.onTranscript(state.transcript)\n const seq = typeof msg.seq === 'number' ? (msg.seq as number) : undefined\n cb.onAgentTurnStart(seq)\n setState(state, 'agent_speaking', cb)\n return\n }\n\n case 'agent_text': {\n const delta = (msg.text as string) ?? ''\n if (!delta || !state.agentBubbleId) return\n const id = state.agentBubbleId\n state.transcript = state.transcript.map((e) =>\n e.id === id && e.role === 'agent' ? { ...e, text: e.text + delta } : e,\n )\n cb.onTranscript(state.transcript)\n return\n }\n\n case 'agent_turn_end': {\n state.agentBubbleId = null\n const seq = typeof msg.seq === 'number' ? (msg.seq as number) : undefined\n cb.onAgentTurnEnd(seq)\n setState(state, 'listening', cb)\n return\n }\n\n case 'interrupt':\n cb.onInterrupt()\n return\n\n case 'agent_turn_abort': {\n const committed = ((msg.committedText as string) ?? '').trim()\n if (state.agentBubbleId) {\n const id = state.agentBubbleId\n if (committed) {\n state.transcript = state.transcript.map((e) =>\n e.id === id && e.role === 'agent' ? { ...e, text: committed, interrupted: true } : e,\n )\n } else {\n state.transcript = state.transcript.filter((e) => e.id !== id)\n }\n cb.onTranscript(state.transcript)\n }\n state.agentBubbleId = null\n return\n }\n\n case 'tool_call':\n state.transcript = [\n ...state.transcript,\n {\n id: `m${state.idCounter++}`,\n role: 'tool',\n text: `→ ${String(msg.tool ?? '?')}(${msg.args ? JSON.stringify(msg.args) : ''})`,\n },\n ]\n cb.onTranscript(state.transcript)\n return\n\n case 'tool_result':\n state.transcript = [\n ...state.transcript,\n {\n id: `m${state.idCounter++}`,\n role: 'tool',\n text: `${msg.ok ? '✓' : '✗'} ${String(msg.tool ?? '?')}`,\n },\n ]\n cb.onTranscript(state.transcript)\n return\n\n case 'client_tool_call': {\n const toolCallId = String(msg.toolCallId ?? '')\n const name = String(msg.name ?? '')\n const args = (msg.args as Record<string, unknown>) ?? {}\n if (!toolCallId || !name) return\n cb.onClientToolCall({ toolCallId, name, args })\n return\n }\n\n case 'call_end': {\n const reasonRaw = String(msg.reason ?? '')\n const reason = mapEndReason(reasonRaw)\n state.endReason = reason\n state.transcript = [\n ...state.transcript,\n {\n id: `m${state.idCounter++}`,\n role: 'system',\n text: `call ended${reasonRaw ? ` (${reasonRaw})` : ''}`,\n },\n ]\n cb.onTranscript(state.transcript)\n cb.onCallEnd(reason)\n return\n }\n\n case 'error': {\n const code = (msg.code as CallErrorCode) ?? 'server_error'\n const message = (msg.message as string) ?? 'server error'\n cb.onError({ code, message })\n return\n }\n }\n}\n\nconst setState = (state: ProtocolState, next: CallState, cb: ProtocolCallbacks) => {\n if (state.state === next) return\n // The consumer's setState (cb.onState) is responsible for both\n // updating state.state and firing the user's onStateChange callback.\n // We deliberately don't mutate here — if we did, the consumer's\n // setState would early-return on the dedup check and never fire\n // onStateChange for server-driven transitions.\n cb.onState(next)\n}\n\n// Find the last uncommitted user bubble and grow it; or append a new\n// uncommitted bubble. Mirrors voice-rn upsertUserPartial.\nconst upsertUserPartial = (state: ProtocolState, text: string, isFinal: boolean) => {\n let idx = -1\n for (let i = state.transcript.length - 1; i >= 0; i--) {\n const e = state.transcript[i]\n if (e.role === 'user' && e.committed === false) {\n idx = i\n break\n }\n }\n if (idx === -1) {\n state.transcript = [\n ...state.transcript,\n { id: `m${state.idCounter++}`, role: 'user', text, committed: isFinal },\n ]\n return\n }\n const target = state.transcript[idx] as Extract<TranscriptEntry, { role: 'user' }>\n const next = [...state.transcript]\n next[idx] = { ...target, text, committed: isFinal }\n state.transcript = next\n}\n\n// ---------------------------------------------------------------------------\n// WebSocket URL builder. Identical between browser + node bundles —\n// kept here so the rare server-side change to the path / query shape is\n// a one-line edit.\n// ---------------------------------------------------------------------------\n\nexport interface BuildWsUrlArgs {\n apiBase: string\n agentId: string\n token: string\n bargeIn?: boolean\n}\n\nexport function buildWsUrl(args: BuildWsUrlArgs): string {\n const base = new URL(args.apiBase)\n const proto = base.protocol === 'https:' ? 'wss:' : 'ws:'\n const bargeQS = args.bargeIn === false ? '&barge=off' : ''\n return `${proto}//${base.host}/v1/agents/${encodeURIComponent(args.agentId)}/call?token=${encodeURIComponent(args.token)}${bargeQS}`\n}\n","// Bundle-agnostic client-tools core for @craftedxp/voice-js.\n//\n// Imports neither `ws` nor DOM `WebSocket` — the dispatcher takes an\n// injected `send` callback so the same code runs in browsers and Node.\n//\n// Wire protocol (matches the server's Zod schemas in\n// server/src/agents/clientToolFrames.ts):\n//\n// client → server client_tools_register { type, tools[] }\n// server → client client_tool_call { type, toolCallId, name, args }\n// client → server client_tool_result { type, toolCallId, result?, error? }\n\nexport interface ClientTool {\n description: string\n parameters: Record<string, unknown> // JSON Schema\n usage?: string // ≤500 chars; appended to agent system prompt\n timeoutMs?: number // server-enforced; (0, 30_000]\n example?: string // surface-only UI hint; not sent on wire\n handler: (args: Record<string, unknown>) => Promise<string | object> | string | object\n}\n\nexport type ClientToolMap = Record<string, ClientTool>\n\n// Server → client frame shape. Used by both protocol.ts (when surfacing\n// the frame to a callback) and dispatchClientToolCall (when consuming it).\nexport interface ClientToolCallFrame {\n toolCallId: string\n name: string\n args: Record<string, unknown>\n}\n\nconst NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/\nconst MAX_TOOLS = 64\nconst MAX_USAGE = 500\nconst MAX_TIMEOUT_MS = 30_000\n\n// Synchronous, throw-on-error validation. Called once at startCall time\n// before the WS opens, so bad input fails fast at the configuration\n// boundary instead of mid-call.\nexport const validateClientToolMap = (tools: ClientToolMap | undefined): void => {\n if (tools === undefined) return\n if (typeof tools !== 'object' || tools === null || Array.isArray(tools)) {\n throw new Error('clientTools must be an object keyed by tool name')\n }\n const entries = Object.entries(tools)\n if (entries.length > MAX_TOOLS) {\n throw new Error(`clientTools may declare at most 64 tools (got ${entries.length})`)\n }\n for (const [name, def] of entries) {\n if (!NAME_RE.test(name)) {\n throw new Error(\n `clientTools[\"${name}\"]: name must be a valid identifier (^[a-zA-Z_][a-zA-Z0-9_]*$)`,\n )\n }\n if (!def || typeof def !== 'object') {\n throw new Error(`clientTools[\"${name}\"]: must be an object`)\n }\n if (typeof def.description !== 'string' || def.description.length === 0) {\n throw new Error(`clientTools[\"${name}\"]: must have a description`)\n }\n if (typeof def.handler !== 'function') {\n throw new Error(`clientTools[\"${name}\"]: must have a handler function`)\n }\n if (def.usage !== undefined && def.usage.length > MAX_USAGE) {\n throw new Error(`clientTools[\"${name}\"]: usage must be ≤500 chars`)\n }\n if (\n def.timeoutMs !== undefined &&\n (!Number.isFinite(def.timeoutMs) || def.timeoutMs <= 0 || def.timeoutMs > MAX_TIMEOUT_MS)\n ) {\n throw new Error(`clientTools[\"${name}\"]: timeoutMs must be in (0, 30000]`)\n }\n }\n}\n\n// Build the registration frame the SDK sends right after `connected`.\n// Strips runtime-only fields (handler, example) and only includes the\n// optional fields when set.\nexport const buildRegisterFrame = (tools: ClientToolMap): object => ({\n type: 'client_tools_register',\n tools: Object.entries(tools).map(([name, def]) => ({\n name,\n description: def.description,\n parameters: def.parameters,\n ...(def.usage !== undefined ? { usage: def.usage } : {}),\n ...(def.timeoutMs !== undefined ? { timeoutMs: def.timeoutMs } : {}),\n })),\n})\n\ntype FrameSender = (frame: object) => void\n\n// Run the matching handler asynchronously and post a result/error frame\n// back through `send`. Send-side throws are swallowed (the WS may have\n// closed mid-call; the server has already cancelled the in-flight\n// invocation on its side).\nexport const dispatchClientToolCall = (\n send: FrameSender,\n tools: ClientToolMap,\n frame: ClientToolCallFrame,\n): void => {\n const safeSend = (payload: object) => {\n try {\n send(payload)\n } catch {\n // WS likely closed mid-handler. Server-side already cancelled.\n }\n }\n\n const tool = tools[frame.name]\n if (!tool) {\n safeSend({\n type: 'client_tool_result',\n toolCallId: frame.toolCallId,\n error: `No handler for ${frame.name}`,\n })\n return\n }\n\n void (async () => {\n try {\n const out = await tool.handler(frame.args)\n safeSend({\n type: 'client_tool_result',\n toolCallId: frame.toolCallId,\n result: typeof out === 'string' ? out : JSON.stringify(out),\n })\n } catch (err) {\n safeSend({\n type: 'client_tool_result',\n toolCallId: frame.toolCallId,\n error: err instanceof Error ? err.message : String(err),\n })\n }\n })()\n}\n","// ClientMarksBuffer — per-turn latency-mark correlator shared between the\n// browser and node SDK clients.\n//\n// Why this exists:\n// The server mints a `seq` (turnId) only AFTER the user finishes\n// speaking, at VoiceOrchestrator.handleTranscript. The client, however,\n// captures its first user-perceived mark — `t_client_mic_to_first_frame`\n// — before any server frame for the turn exists. We therefore buffer\n// client-side marks under a \"pending\" slot and back-fill the seq when\n// the server's `agent_turn_start { seq }` arrives. When `agent_turn_end\n// { seq }` lands AND we've captured the inbound `client_first_audible`\n// mark, the buffer flushes a `client_marks` frame to the server.\n//\n// Lifetime + bounds:\n// - One pending bucket at a time. A new `agent_turn_start` flushes\n// anything still pending (shouldn't happen in normal flow, but is\n// safer than silently dropping marks).\n// - If `agent_turn_end` arrives but we never captured first_audible\n// (e.g. agent had no audio output), we still emit a `client_marks`\n// frame with whatever marks are present. Missing marks are simply\n// omitted from the wire payload.\n// - The buffer is GC'd by replacement on every turn boundary; there is\n// no long-term retention. A turn whose `agent_turn_end` never\n// arrives (server crash mid-turn) is implicitly dropped on the next\n// `agent_turn_start` or when the call ends. We do NOT add an\n// explicit watchdog — observability marks aren't worth the\n// complexity of a timer here.\n//\n// Time source:\n// `performance.now()` in both browser and Node. Wall-clock-monotonic,\n// sub-ms precision. Marks are stored as raw `performance.now()` values;\n// only the deltas between them ever go on the wire.\n//\n// Wire shape (see protocol.ts):\n// { type: 'client_marks', seq, marks: { ... }, clientNow }\n//\n// The CLAUDE.md / functional-style preference: this module is a small\n// factory returning a closure-based controller. No classes, no shared\n// mutable module state.\n\nexport interface ClientMarksFrame {\n type: 'client_marks'\n seq: number\n marks: {\n // The headline number: client-measured wall-clock between the first\n // outbound audio frame for this turn and the first agent-audio\n // chunk committed for playback. Includes mic-encode time, network\n // RTT in both directions, server-side processing (STT + LLM + TTS),\n // and any client-side scheduling delay. Absent if either endpoint\n // wasn't captured.\n client_mic_to_first_audible_ms?: number\n }\n // Wall-clock at send time, ms-since-epoch. Lets the analysis script\n // estimate clock skew between client and server logs even though the\n // marks themselves are deltas.\n clientNow?: number\n}\n\n// What the consumer-facing controller exposes. The browser and node\n// clients both wire to this same surface.\nexport interface ClientMarksController {\n // Outbound mark. Called on every outgoing audio chunk; the buffer\n // gates so only the first chunk PER TURN is timestamped. `markTurnBoundary`\n // resets the gate so the next chunk is treated as the start of the\n // next turn's outbound stream.\n markFirstOutboundAudio: () => void\n\n // Inbound mark. Called every time the SDK commits an agent-audio\n // chunk for playback (browser: AudioContext schedule; node: handed to\n // consumer via onAudioChunk). Gated to first-per-turn.\n markFirstAudibleOutput: () => void\n\n // Called when the SDK receives `agent_turn_start { seq }`. Tags any\n // accumulated outbound marks with this seq. Also resets the\n // first-outbound gate so the NEXT turn's outbound mark can be\n // captured again. (Outbound is captured for a turn before its seq is\n // known; inbound is captured after, with the seq tagged.)\n onAgentTurnStart: (seq: number) => void\n\n // Called when the SDK receives `agent_turn_end { seq }`. Triggers\n // emission of the buffered `client_marks` frame for that seq, with\n // whatever marks are present (missing ones omitted).\n onAgentTurnEnd: (seq: number) => void\n\n // Force-flush any pending state. Called on call end / teardown so\n // the last turn's marks aren't dropped on hangup.\n flush: () => void\n}\n\n// `send` is the raw WS-send sink injected by the surrounding client.\n// Stringified JSON in browser/node. Returns whether the send was\n// accepted (we don't surface failure beyond the boolean — observability\n// frames are best-effort).\nexport type ClientMarksSend = (frame: ClientMarksFrame) => void\n\n// `now` defaults to performance.now() but is injected so unit tests can\n// drive a fake clock deterministically.\nexport interface CreateClientMarksBufferArgs {\n send: ClientMarksSend\n now?: () => number\n}\n\nexport const createClientMarksBuffer = (\n args: CreateClientMarksBufferArgs,\n): ClientMarksController => {\n const now = args.now ?? (() => performance.now())\n\n // Pending bucket — one in-flight turn at a time. We hold the OUTBOUND\n // mark before we know `seq`, then graduate to a per-seq bucket on\n // agent_turn_start so a second turn's inbound marks can't trample the\n // first turn's outbound mark before flush.\n let pendingFirstOutboundAt: number | null = null\n\n // Once agent_turn_start { seq } arrives we move the pending outbound\n // mark into a per-seq slot and start collecting the inbound mark.\n // Map keyed by seq; in practice never holds more than 1-2 entries\n // (one currently-flushing, one just-started).\n const inFlight = new Map<\n number,\n {\n firstOutboundAt: number | null\n firstAudibleAt: number | null\n ended: boolean\n }\n >()\n\n const tryEmit = (seq: number) => {\n const slot = inFlight.get(seq)\n if (!slot) return\n if (!slot.ended) return\n // Build wire payload. Only include marks we actually captured.\n const marks: ClientMarksFrame['marks'] = {}\n if (slot.firstOutboundAt !== null && slot.firstAudibleAt !== null) {\n marks.client_mic_to_first_audible_ms = slot.firstAudibleAt - slot.firstOutboundAt\n }\n args.send({\n type: 'client_marks',\n seq,\n marks,\n clientNow: Date.now(),\n })\n inFlight.delete(seq)\n }\n\n const markFirstOutboundAudio = () => {\n if (pendingFirstOutboundAt !== null) return // already stamped this turn\n pendingFirstOutboundAt = now()\n }\n\n const markFirstAudibleOutput = () => {\n // Inbound is captured AFTER agent_turn_start, so we know which seq\n // it belongs to: the most recent un-ended in-flight slot. If there's\n // somehow none (race: audio before agent_turn_start), drop silently.\n let target: { firstAudibleAt: number | null; ended: boolean } | undefined\n for (const slot of inFlight.values()) {\n if (!slot.ended) {\n target = slot\n // No break — last un-ended wins, matching the \"most recent\n // turn\" intent if multiple are in flight.\n }\n }\n if (!target) return\n if (target.firstAudibleAt !== null) return // first-per-turn gate\n target.firstAudibleAt = now()\n }\n\n const onAgentTurnStart = (seq: number) => {\n inFlight.set(seq, {\n firstOutboundAt: pendingFirstOutboundAt,\n firstAudibleAt: null,\n ended: false,\n })\n // Reset outbound gate so the NEXT turn captures cleanly. Note:\n // pendingFirstOutboundAt is already moved into the slot above; we\n // don't need to preserve it.\n pendingFirstOutboundAt = null\n }\n\n const onAgentTurnEnd = (seq: number) => {\n const slot = inFlight.get(seq)\n if (!slot) {\n // Got an end with no matching start (server bug, or restart). Best\n // effort: emit an empty marks frame so the analysis script knows\n // the client saw the turn at all.\n args.send({ type: 'client_marks', seq, marks: {}, clientNow: Date.now() })\n return\n }\n slot.ended = true\n tryEmit(seq)\n }\n\n const flush = () => {\n for (const seq of [...inFlight.keys()]) {\n const slot = inFlight.get(seq)!\n slot.ended = true\n tryEmit(seq)\n }\n pendingFirstOutboundAt = null\n }\n\n return {\n markFirstOutboundAudio,\n markFirstAudibleOutput,\n onAgentTurnStart,\n onAgentTurnEnd,\n flush,\n }\n}\n","// Browser VoiceClient — drives one in-progress call.\n//\n// Created by `BrowserVoiceFactory.startCall` (see browser.ts). Most\n// consumers never touch this class directly — they receive a `Call`\n// handle that exposes `end`/`mute`/`unmute` and subscribe to events\n// via the `onState` / `onTranscript` / `onError` / `onEnd` callbacks\n// they passed to `startCall`.\n//\n// This is the rough equivalent of @craftedxp/voice-rn's `useVoiceCall`\n// hook, but environment-agnostic (no React).\n\nimport { createAudioCapture, type CaptureController } from './AudioCapture'\nimport { createAudioPlayback, type PlaybackController } from './AudioPlayback'\nimport {\n createReconnectingWebSocket,\n type ReconnectingWebSocket,\n type WebSocketFactory,\n} from './ReconnectingWebSocket'\nimport {\n buildWsUrl,\n createProtocolState,\n handleServerMessage,\n type CallEndReason,\n type CallError,\n type ProtocolState,\n} from './protocol'\nimport type { Call, StartCallOptions, VoiceClientConfig } from './config'\nimport { buildRegisterFrame, dispatchClientToolCall, validateClientToolMap } from './clientTools'\nimport { createClientMarksBuffer, type ClientMarksController } from './ClientMarksBuffer'\n\ninterface VoiceClientArgs {\n // Pre-resolved by the factory.\n config: VoiceClientConfig\n options: StartCallOptions\n token: string\n // Browser uses native WebSocket; node injects `ws`.\n wsFactory: WebSocketFactory\n}\n\nexport class BrowserVoiceClient implements Call {\n private readonly args: VoiceClientArgs\n private readonly proto: ProtocolState\n\n private rws: ReconnectingWebSocket | null = null\n private capture: CaptureController | null = null\n private playback: PlaybackController | null = null\n\n private muted = false\n private inputVolume = 0\n private outputVolume = 0\n private startedAt: number | null = null\n private endedFired = false\n private lastError: CallError | null = null\n private marks: ClientMarksController\n\n constructor(args: VoiceClientArgs) {\n this.args = args\n this.proto = createProtocolState()\n validateClientToolMap(args.options.clientTools)\n // Best-effort observability frame. Wraps WS send so a failed send\n // (closed socket) is silently swallowed — these marks are never\n // load-bearing for the call itself.\n this.marks = createClientMarksBuffer({\n send: (frame) => {\n try {\n this.rws?.send(JSON.stringify(frame))\n } catch {\n // WS already gone; drop the frame.\n }\n },\n })\n }\n\n // ---------------------------------------------------------------\n // Call interface\n // ---------------------------------------------------------------\n\n get state() {\n return this.proto.state\n }\n\n get transcript() {\n return this.proto.transcript.slice()\n }\n\n get isMuted() {\n return this.muted\n }\n\n end = () => {\n this.teardown('user_hangup')\n }\n\n mute = () => {\n if (this.muted) return\n this.muted = true\n this.capture?.mute(true)\n }\n\n unmute = () => {\n if (!this.muted) return\n this.muted = false\n this.capture?.mute(false)\n }\n\n // ---------------------------------------------------------------\n // Lifecycle — called by the factory immediately after construction.\n // Resolves once the WS is open and capture is starting; mid-call\n // failures arrive via `onError`.\n // ---------------------------------------------------------------\n\n async start(): Promise<void> {\n this.setState('connecting')\n this.startedAt = Date.now()\n\n const url = buildWsUrl({\n apiBase: this.args.config.apiBase,\n agentId: this.args.options.agentId,\n token: this.args.token,\n bargeIn: this.args.options.bargeIn,\n })\n\n // Playback context needs a user gesture before its AudioContext can\n // produce sound. In practice startCall is invoked inside a click\n // handler; resume() is cheap and idempotent.\n this.playback = createAudioPlayback({\n onVolume: (v) => {\n this.outputVolume = v\n this.args.options.onVolume?.({ input: this.inputVolume, output: v })\n },\n })\n try {\n await this.playback.resume()\n } catch {\n // AudioContext may refuse if we weren't called from a gesture.\n // First enqueue() will retry the resume.\n }\n\n this.rws = createReconnectingWebSocket(\n {\n url,\n wsFactory: this.args.wsFactory,\n maxRetries: 3,\n },\n (ev) => this.handleSocketEvent(ev),\n )\n }\n\n // ---------------------------------------------------------------\n // Internal\n // ---------------------------------------------------------------\n\n private sendClientToolsRegister = () => {\n const frame = buildRegisterFrame(this.args.options.clientTools ?? {})\n this.rws?.send(JSON.stringify(frame))\n }\n\n private setState = (next: ProtocolState['state']) => {\n if (this.proto.state === next) return\n this.proto.state = next\n this.args.options.onStateChange?.(next)\n }\n\n private emitError = (err: CallError) => {\n this.lastError = err\n this.args.options.onError?.(err)\n }\n\n private handleSocketEvent = (\n ev:\n | { type: 'open' }\n | { type: 'reconnected' }\n | { type: 'message'; data: string | ArrayBuffer }\n | { type: 'close'; code: number; reason: string; permanent: boolean }\n | { type: 'error'; error: Error },\n ) => {\n switch (ev.type) {\n case 'open':\n void this.startCapture()\n break\n case 'reconnected':\n // Treat as a fresh call — server session is gone, no resume.\n this.proto.transcript = []\n this.proto.agentBubbleId = null\n this.args.options.onTranscript?.(this.proto.transcript)\n void this.startCapture()\n this.setState('listening')\n break\n case 'message':\n if (typeof ev.data === 'string') {\n handleServerMessage(ev.data, this.proto, {\n onState: this.setState,\n onTranscript: (entries) => this.args.options.onTranscript?.(entries),\n onError: this.emitError,\n onInterrupt: () => {\n this.playback?.flush()\n this.args.options.onInterrupt?.()\n },\n onAgentTurnStart: (seq) => {\n if (typeof seq === 'number') this.marks.onAgentTurnStart(seq)\n this.args.options.onAgentTurnStart?.()\n },\n onAgentTurnEnd: (seq) => {\n if (typeof seq === 'number') this.marks.onAgentTurnEnd(seq)\n },\n onCallEnd: (reason) => this.teardown(reason),\n onConnected: () => this.sendClientToolsRegister(),\n onClientToolCall: (frame) =>\n dispatchClientToolCall(\n (f) => this.rws?.send(JSON.stringify(f)),\n this.args.options.clientTools ?? {},\n frame,\n ),\n })\n } else {\n // Inbound mark: stamped at the moment we COMMIT this chunk for\n // playback (enqueue schedules into AudioContext). We\n // intentionally don't await — enqueue handles its own\n // AudioContext warm-up; the mark captures the SDK's commit\n // moment, which is the closest we can get to \"user heard it\"\n // without instrumenting the AudioContext output graph.\n this.marks.markFirstAudibleOutput()\n this.playback?.enqueue(ev.data)\n }\n break\n case 'close':\n if (ev.permanent) {\n // Pick the most informative reason: server-supplied wins, then\n // last error, then default to user_hangup.\n const reason: CallEndReason =\n this.proto.endReason ?? (this.lastError ? 'error' : 'user_hangup')\n this.teardown(reason)\n }\n break\n case 'error':\n this.emitError({ code: 'socket_error', message: ev.error.message })\n break\n }\n }\n\n private startCapture = async () => {\n if (this.capture?.isCapturing()) return\n this.capture = createAudioCapture({\n onChunk: (pcm) => {\n // Outbound mark — stamps the first frame leaving the SDK for\n // each turn. The buffer gates so only the first chunk per\n // pending-turn window is timestamped; subsequent chunks are\n // no-ops until agent_turn_start moves the slot to a per-seq\n // bucket and resets the gate. Semantic: this is \"moment audio\n // is dispatched to the WS\", which in continuous open-mic mode\n // is essentially the encode-and-handoff time per first frame\n // after a turn boundary.\n this.marks.markFirstOutboundAudio()\n this.rws?.send(pcm)\n },\n onVolume: (v) => {\n this.inputVolume = v\n this.args.options.onVolume?.({ input: v, output: this.outputVolume })\n },\n onError: (err) => {\n this.emitError({\n code: err.name === 'NotAllowedError' ? 'mic_denied' : 'mic_start_failed',\n message: err.message,\n })\n },\n })\n if (this.muted) this.capture.mute(true)\n try {\n await this.capture.start()\n } catch {\n // onError already emitted inside createAudioCapture.\n }\n }\n\n private teardown = (reason: CallEndReason) => {\n // Flush any in-flight observability marks BEFORE the WS closes so\n // the final turn's client_marks frame doesn't get dropped on\n // hangup.\n try {\n this.marks.flush()\n } catch {\n // never fatal\n }\n this.capture?.stop()\n this.capture = null\n this.playback?.close()\n this.playback = null\n try {\n this.rws?.close(1000, reason)\n } catch {\n // already closed\n }\n this.rws = null\n this.setState('ended')\n this.fireEndOnce(reason)\n }\n\n private fireEndOnce = (reason: CallEndReason) => {\n if (this.endedFired) return\n this.endedFired = true\n const startedAt = this.startedAt ?? Date.now()\n this.args.options.onEnd?.({\n reason,\n errorCode: reason === 'error' ? this.lastError?.code : undefined,\n durationMs: Date.now() - startedAt,\n })\n }\n}\n","// WebRTC transport for voice-js. Returns a `Call` with the same surface\n// as the WS path so consumers don't branch on transport. Signaling is\n// one-shot HTTP: POST /webrtc/offer (returns the SDP answer + callId);\n// trickled candidates go to POST /webrtc/ice. Audio flows over the\n// RTCPeerConnection; protocol frames (transcript, agent_turn_start, ...)\n// arrive on an in-band DataChannel named `control`.\n\nimport { createProtocolState, handleServerMessage, type ProtocolState } from '../protocol'\nimport type { CallError, CallState, TranscriptEntry } from '../protocol'\nimport type { Call } from '../config'\nimport {\n buildRegisterFrame,\n dispatchClientToolCall,\n validateClientToolMap,\n type ClientToolMap,\n} from '../clientTools'\n\nexport interface CreateWebRtcCallOptions {\n agentId: string\n /** API base (`https://api.voissia.com` etc). Used as the signaling\n * fallback when `webrtcGatewayBase` is unset. */\n apiBase: string\n /** Pre-minted ct_. */\n token: string\n /** Gateway base from the mint response. When falsy, signaling goes to\n * `${apiBase}/v1/agents/:agentId/webrtc/{offer,ice}` (Phase-1 routes,\n * local dev). */\n webrtcGatewayBase?: string\n onStateChange?: (s: CallState) => void\n onTranscript?: (entries: TranscriptEntry[]) => void\n onError?: (e: CallError) => void\n onEnd?: () => void\n onInterrupt?: () => void\n onAgentTurnStart?: () => void\n /** Tool palette the agent's LLM may invoke during the call. Registered\n * with the server on DataChannel open and dispatched on incoming\n * client_tool_call frames. Same contract as the WS path. */\n clientTools?: ClientToolMap\n}\n\nexport async function createWebRtcCall(opts: CreateWebRtcCallOptions): Promise<Call> {\n // Validate up front so a malformed tool map fails before mic prompt.\n validateClientToolMap(opts.clientTools)\n\n const proto: ProtocolState = createProtocolState()\n let muted = false\n let ended = false\n const tools: ClientToolMap = opts.clientTools ?? {}\n\n // Send a control-channel frame as JSON. Used by the tool register on\n // open + by dispatchClientToolCall to ship results back. Drops the\n // send if the channel isn't open yet (the server will retry the tool\n // call if it doesn't get a result).\n const sendControl = (frame: object): void => {\n if (dc?.readyState !== 'open') return\n try {\n dc.send(JSON.stringify(frame))\n } catch {\n // channel closed mid-send\n }\n }\n\n const fireState = (next: CallState) => {\n if (proto.state === next) return\n proto.state = next\n opts.onStateChange?.(next)\n }\n\n const dispatch = (raw: string) => {\n handleServerMessage(raw, proto, {\n onState: fireState,\n onTranscript: (entries) => opts.onTranscript?.(entries),\n onError: (err) => opts.onError?.(err),\n onInterrupt: () => opts.onInterrupt?.(),\n onAgentTurnStart: () => opts.onAgentTurnStart?.(),\n onAgentTurnEnd: () => {},\n onCallEnd: () => teardown(),\n onConnected: () => {\n // Server's `connected` frame is our cue to register the tool\n // palette. Mirrors the WS path's behaviour in VoiceClient.ts.\n if (Object.keys(tools).length > 0) {\n sendControl(buildRegisterFrame(tools))\n }\n },\n onClientToolCall: (frame) => {\n dispatchClientToolCall(sendControl, tools, frame)\n },\n })\n }\n\n fireState('connecting')\n\n const pc = new RTCPeerConnection({\n iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],\n })\n\n const audioEl = document.createElement('audio')\n audioEl.autoplay = true\n audioEl.style.display = 'none'\n document.body.appendChild(audioEl)\n pc.ontrack = (event) => {\n audioEl.srcObject = event.streams[0] ?? new MediaStream([event.track])\n }\n\n let mic: MediaStream\n try {\n mic = await navigator.mediaDevices.getUserMedia({ audio: true })\n } catch (err) {\n const code =\n err instanceof DOMException && err.name === 'NotAllowedError'\n ? 'mic_denied'\n : 'mic_start_failed'\n opts.onError?.({\n code,\n message: err instanceof Error ? err.message : 'getUserMedia failed',\n })\n fireState('error')\n pc.close()\n audioEl.remove()\n throw err\n }\n for (const track of mic.getAudioTracks()) pc.addTrack(track, mic)\n\n const dc: RTCDataChannel = pc.createDataChannel('control', { ordered: true })\n dc.onmessage = (e) => {\n if (typeof e.data === 'string') dispatch(e.data)\n }\n dc.onerror = () => {\n opts.onError?.({ code: 'socket_error', message: 'control channel error' })\n }\n // Defensive: if the server never emits a `connected` protocol frame\n // but the channel opens (older server builds or future protocol\n // skew), still register the tools so they're available the moment\n // the LLM tries to call one.\n dc.onopen = () => {\n if (Object.keys(tools).length > 0) {\n sendControl(buildRegisterFrame(tools))\n }\n }\n\n const gateway = opts.webrtcGatewayBase || ''\n const offerUrl = gateway\n ? `${gateway}/webrtc/offer?token=${encodeURIComponent(opts.token)}`\n : `${opts.apiBase}/v1/agents/${encodeURIComponent(opts.agentId)}/webrtc/offer?token=${encodeURIComponent(opts.token)}`\n const iceUrl = gateway\n ? `${gateway}/webrtc/ice?token=${encodeURIComponent(opts.token)}`\n : `${opts.apiBase}/v1/agents/${encodeURIComponent(opts.agentId)}/webrtc/ice?token=${encodeURIComponent(opts.token)}`\n\n await pc.setLocalDescription(await pc.createOffer())\n\n let callId: string\n try {\n const offerRes = await fetch(offerUrl, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ sdp: pc.localDescription!.sdp, type: 'offer', agentId: opts.agentId }),\n })\n if (!offerRes.ok) {\n const code = offerRes.status === 401 ? 'unauthorized' : 'server_error'\n opts.onError?.({ code, message: `signaling failed: HTTP ${offerRes.status}` })\n fireState('error')\n mic.getTracks().forEach((t) => t.stop())\n pc.close()\n audioEl.remove()\n throw new Error(`webrtc offer failed: ${offerRes.status}`)\n }\n const body = (await offerRes.json()) as { sdp: string; type: 'answer'; callId: string }\n callId = body.callId\n await pc.setRemoteDescription({ type: 'answer', sdp: body.sdp })\n } catch (err) {\n if (!ended) {\n opts.onError?.({\n code: 'network_unreachable',\n message: err instanceof Error ? err.message : 'signaling failed',\n })\n fireState('error')\n mic.getTracks().forEach((t) => t.stop())\n pc.close()\n audioEl.remove()\n }\n throw err\n }\n\n pc.onicecandidate = (e) => {\n if (!e.candidate) return\n void fetch(iceUrl, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ callId, candidate: e.candidate }),\n }).catch(() => {\n /* best-effort */\n })\n }\n\n pc.onconnectionstatechange = () => {\n const s = pc.connectionState\n if (s === 'connected') fireState('listening')\n if (s === 'failed' || s === 'disconnected') {\n opts.onError?.({ code: 'socket_error', message: `webrtc connection ${s}` })\n teardown()\n }\n if (s === 'closed' && !ended) teardown()\n }\n\n const teardown = () => {\n if (ended) return\n ended = true\n try {\n mic.getTracks().forEach((t) => t.stop())\n } catch {\n /* already stopped */\n }\n try {\n pc.close()\n } catch {\n /* already closed */\n }\n try {\n audioEl.remove()\n } catch {\n /* already removed */\n }\n fireState('ended')\n opts.onEnd?.()\n }\n\n return {\n get state() {\n return proto.state\n },\n get transcript() {\n return proto.transcript.slice()\n },\n get isMuted() {\n return muted\n },\n end: () => teardown(),\n mute: () => {\n if (muted) return\n muted = true\n mic.getAudioTracks().forEach((t) => (t.enabled = false))\n },\n unmute: () => {\n if (!muted) return\n muted = false\n mic.getAudioTracks().forEach((t) => (t.enabled = true))\n },\n }\n}\n"],"mappings":"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","names":["VOLUME_INTERVAL_MS"]}
|