@craftedxp/voice-js 0.4.1 → 0.4.2

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 CHANGED
@@ -1,26 +1,24 @@
1
- 'use strict'
2
- var __defProp = Object.defineProperty
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor
4
- var __getOwnPropNames = Object.getOwnPropertyNames
5
- var __hasOwnProp = Object.prototype.hasOwnProperty
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
6
  var __export = (target, all) => {
7
- for (var name in all) __defProp(target, name, { get: all[name], enumerable: true })
8
- }
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
9
10
  var __copyProps = (to, from, except, desc) => {
10
- if ((from && typeof from === 'object') || typeof from === 'function') {
11
+ if (from && typeof from === "object" || typeof from === "function") {
11
12
  for (let key of __getOwnPropNames(from))
12
13
  if (!__hasOwnProp.call(to, key) && key !== except)
13
- __defProp(to, key, {
14
- get: () => from[key],
15
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable,
16
- })
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
15
  }
18
- return to
19
- }
20
- var __toCommonJS = (mod) => __copyProps(__defProp({}, '__esModule', { value: true }), mod)
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
19
 
22
20
  // src/browser.ts
23
- var browser_exports = {}
21
+ var browser_exports = {};
24
22
  __export(browser_exports, {
25
23
  buildWsUrl: () => buildWsUrl,
26
24
  configureVoiceClient: () => configureVoiceClient,
@@ -28,64 +26,57 @@ __export(browser_exports, {
28
26
  createAudioPlayback: () => createAudioPlayback,
29
27
  createProtocolState: () => createProtocolState,
30
28
  createReconnectingWebSocket: () => createReconnectingWebSocket,
31
- handleServerMessage: () => handleServerMessage,
32
- })
33
- module.exports = __toCommonJS(browser_exports)
29
+ handleServerMessage: () => handleServerMessage
30
+ });
31
+ module.exports = __toCommonJS(browser_exports);
34
32
 
35
33
  // src/config.ts
36
34
  function normalizeConfig(config) {
37
- if (!config) throw new Error('configureVoiceClient: config is required')
38
- if ('apiKey' in config) {
35
+ if (!config) throw new Error("configureVoiceClient: config is required");
36
+ if ("apiKey" in config) {
39
37
  throw new Error(
40
- '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 \u2014 see the @craftedxp/voice-js README for the migration recipe.',
41
- )
38
+ "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 \u2014 see the @craftedxp/voice-js README for the migration recipe."
39
+ );
42
40
  }
43
41
  if (!config.apiBase) {
44
- throw new Error('configureVoiceClient: apiBase is required')
42
+ throw new Error("configureVoiceClient: apiBase is required");
45
43
  }
46
- if (typeof config.fetchToken !== 'function') {
47
- throw new Error('configureVoiceClient: fetchToken must be a function')
44
+ if (typeof config.fetchToken !== "function") {
45
+ throw new Error("configureVoiceClient: fetchToken must be a function");
48
46
  }
49
47
  return {
50
48
  ...config,
51
- apiBase: config.apiBase.replace(/\/+$/, ''),
52
- }
49
+ apiBase: config.apiBase.replace(/\/+$/, "")
50
+ };
53
51
  }
54
52
  function mergeStartCallContext(factory, call) {
55
- const context =
56
- factory.defaultContext || call.context
57
- ? { ...(factory.defaultContext ?? {}), ...(call.context ?? {}) }
58
- : void 0
59
- const metadata =
60
- factory.defaultMetadata || call.metadata
61
- ? { ...(factory.defaultMetadata ?? {}), ...(call.metadata ?? {}) }
62
- : void 0
63
- return { context, metadata }
53
+ const context = factory.defaultContext || call.context ? { ...factory.defaultContext ?? {}, ...call.context ?? {} } : void 0;
54
+ const metadata = factory.defaultMetadata || call.metadata ? { ...factory.defaultMetadata ?? {}, ...call.metadata ?? {} } : void 0;
55
+ return { context, metadata };
64
56
  }
65
57
 
66
58
  // src/worklets/mic-downsampler.worklet.js
67
- var mic_downsampler_worklet_default =
68
- "// AudioWorklet \u2014 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) \u2014 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 \u2192 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"
59
+ var mic_downsampler_worklet_default = "// AudioWorklet \u2014 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) \u2014 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 \u2192 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";
69
60
 
70
61
  // src/AudioCapture.ts
71
- var VOLUME_INTERVAL_MS = 100
62
+ var VOLUME_INTERVAL_MS = 100;
72
63
  var createAudioCapture = (options) => {
73
- let audioContext = null
74
- let mediaStream = null
75
- let sourceNode = null
76
- let workletNode = null
77
- let analyser = null
78
- let volumeTimer = null
79
- let muted = false
80
- let capturing = false
64
+ let audioContext = null;
65
+ let mediaStream = null;
66
+ let sourceNode = null;
67
+ let workletNode = null;
68
+ let analyser = null;
69
+ let volumeTimer = null;
70
+ let muted = false;
71
+ let capturing = false;
81
72
  const computeRms = (buf) => {
82
- let sum = 0
83
- for (let i = 0; i < buf.length; i++) sum += buf[i] * buf[i]
84
- const rms = Math.sqrt(sum / buf.length)
85
- return Math.min(1, rms * 1.8)
86
- }
73
+ let sum = 0;
74
+ for (let i = 0; i < buf.length; i++) sum += buf[i] * buf[i];
75
+ const rms = Math.sqrt(sum / buf.length);
76
+ return Math.min(1, rms * 1.8);
77
+ };
87
78
  const start = async () => {
88
- if (capturing) return
79
+ if (capturing) return;
89
80
  try {
90
81
  mediaStream = await navigator.mediaDevices.getUserMedia({
91
82
  audio: {
@@ -96,748 +87,751 @@ var createAudioCapture = (options) => {
96
87
  echoCancellation: true,
97
88
  noiseSuppression: true,
98
89
  autoGainControl: true,
99
- channelCount: 1,
100
- },
101
- })
102
- audioContext = new AudioContext()
103
- if (audioContext.state === 'suspended') await audioContext.resume()
104
- const blob = new Blob([mic_downsampler_worklet_default], { type: 'application/javascript' })
105
- const url = URL.createObjectURL(blob)
90
+ channelCount: 1
91
+ }
92
+ });
93
+ audioContext = new AudioContext();
94
+ if (audioContext.state === "suspended") await audioContext.resume();
95
+ const blob = new Blob([mic_downsampler_worklet_default], { type: "application/javascript" });
96
+ const url = URL.createObjectURL(blob);
106
97
  try {
107
- await audioContext.audioWorklet.addModule(url)
98
+ await audioContext.audioWorklet.addModule(url);
108
99
  } finally {
109
- URL.revokeObjectURL(url)
100
+ URL.revokeObjectURL(url);
110
101
  }
111
- sourceNode = audioContext.createMediaStreamSource(mediaStream)
112
- workletNode = new AudioWorkletNode(audioContext, 'mic-downsampler')
102
+ sourceNode = audioContext.createMediaStreamSource(mediaStream);
103
+ workletNode = new AudioWorkletNode(audioContext, "mic-downsampler");
113
104
  workletNode.port.onmessage = (event) => {
114
- if (muted) return
115
- options.onChunk(event.data)
116
- }
105
+ if (muted) return;
106
+ options.onChunk(event.data);
107
+ };
117
108
  if (options.onVolume) {
118
- analyser = audioContext.createAnalyser()
119
- analyser.fftSize = 256
120
- sourceNode.connect(analyser)
121
- const buf = new Float32Array(analyser.fftSize)
109
+ analyser = audioContext.createAnalyser();
110
+ analyser.fftSize = 256;
111
+ sourceNode.connect(analyser);
112
+ const buf = new Float32Array(analyser.fftSize);
122
113
  volumeTimer = setInterval(() => {
123
- if (!analyser) return
124
- analyser.getFloatTimeDomainData(buf)
125
- options.onVolume?.(computeRms(buf))
126
- }, VOLUME_INTERVAL_MS)
114
+ if (!analyser) return;
115
+ analyser.getFloatTimeDomainData(buf);
116
+ options.onVolume?.(computeRms(buf));
117
+ }, VOLUME_INTERVAL_MS);
127
118
  }
128
- sourceNode.connect(workletNode)
129
- const sink = audioContext.createGain()
130
- sink.gain.value = 0
131
- workletNode.connect(sink).connect(audioContext.destination)
132
- capturing = true
119
+ sourceNode.connect(workletNode);
120
+ const sink = audioContext.createGain();
121
+ sink.gain.value = 0;
122
+ workletNode.connect(sink).connect(audioContext.destination);
123
+ capturing = true;
133
124
  } catch (err) {
134
- const wrapped =
135
- err instanceof Error ? err : new Error(typeof err === 'string' ? err : 'capture failed')
136
- options.onError?.(wrapped)
137
- throw wrapped
125
+ const wrapped = err instanceof Error ? err : new Error(typeof err === "string" ? err : "capture failed");
126
+ options.onError?.(wrapped);
127
+ throw wrapped;
138
128
  }
139
- }
129
+ };
140
130
  const stop = () => {
141
- if (!capturing) return
142
- capturing = false
131
+ if (!capturing) return;
132
+ capturing = false;
143
133
  if (volumeTimer) {
144
- clearInterval(volumeTimer)
145
- volumeTimer = null
134
+ clearInterval(volumeTimer);
135
+ volumeTimer = null;
146
136
  }
147
137
  try {
148
- workletNode?.disconnect()
149
- analyser?.disconnect()
150
- sourceNode?.disconnect()
151
- } catch {}
152
- workletNode = null
153
- analyser = null
154
- sourceNode = null
138
+ workletNode?.disconnect();
139
+ analyser?.disconnect();
140
+ sourceNode?.disconnect();
141
+ } catch {
142
+ }
143
+ workletNode = null;
144
+ analyser = null;
145
+ sourceNode = null;
155
146
  if (mediaStream) {
156
- for (const track of mediaStream.getTracks()) track.stop()
157
- mediaStream = null
147
+ for (const track of mediaStream.getTracks()) track.stop();
148
+ mediaStream = null;
158
149
  }
159
- if (audioContext && audioContext.state !== 'closed') {
160
- void audioContext.close().catch(() => void 0)
150
+ if (audioContext && audioContext.state !== "closed") {
151
+ void audioContext.close().catch(() => void 0);
161
152
  }
162
- audioContext = null
163
- }
153
+ audioContext = null;
154
+ };
164
155
  return {
165
156
  start,
166
157
  stop,
167
158
  mute: (v) => {
168
- muted = v
159
+ muted = v;
169
160
  },
170
- isCapturing: () => capturing,
171
- }
172
- }
161
+ isCapturing: () => capturing
162
+ };
163
+ };
173
164
 
174
165
  // src/AudioPlayback.ts
175
- var DEFAULT_SAMPLE_RATE = 16e3
176
- var VOLUME_INTERVAL_MS2 = 100
166
+ var DEFAULT_SAMPLE_RATE = 16e3;
167
+ var VOLUME_INTERVAL_MS2 = 100;
177
168
  var createAudioPlayback = (options = {}) => {
178
- const sampleRate = options.sampleRate ?? DEFAULT_SAMPLE_RATE
179
- let audioContext = null
180
- let gainNode = null
181
- let analyser = null
182
- let volumeTimer = null
183
- let nextStartTime = 0
184
- let scheduledNodes = []
185
- let speaking = false
169
+ const sampleRate = options.sampleRate ?? DEFAULT_SAMPLE_RATE;
170
+ let audioContext = null;
171
+ let gainNode = null;
172
+ let analyser = null;
173
+ let volumeTimer = null;
174
+ let nextStartTime = 0;
175
+ let scheduledNodes = [];
176
+ let speaking = false;
186
177
  const ensureContext = async () => {
187
178
  if (audioContext) {
188
- if (audioContext.state === 'suspended') await audioContext.resume()
189
- return
179
+ if (audioContext.state === "suspended") await audioContext.resume();
180
+ return;
190
181
  }
191
- audioContext = new AudioContext({ sampleRate })
192
- gainNode = audioContext.createGain()
182
+ audioContext = new AudioContext({ sampleRate });
183
+ gainNode = audioContext.createGain();
193
184
  if (options.onVolume) {
194
- analyser = audioContext.createAnalyser()
195
- analyser.fftSize = 256
196
- gainNode.connect(analyser)
197
- const buf = new Float32Array(analyser.fftSize)
185
+ analyser = audioContext.createAnalyser();
186
+ analyser.fftSize = 256;
187
+ gainNode.connect(analyser);
188
+ const buf = new Float32Array(analyser.fftSize);
198
189
  volumeTimer = setInterval(() => {
199
- if (!analyser) return
200
- analyser.getFloatTimeDomainData(buf)
201
- let sum = 0
202
- for (let i = 0; i < buf.length; i++) sum += buf[i] * buf[i]
203
- const rms = Math.sqrt(sum / buf.length)
204
- options.onVolume?.(Math.min(1, rms * 1.8))
205
- }, VOLUME_INTERVAL_MS2)
190
+ if (!analyser) return;
191
+ analyser.getFloatTimeDomainData(buf);
192
+ let sum = 0;
193
+ for (let i = 0; i < buf.length; i++) sum += buf[i] * buf[i];
194
+ const rms = Math.sqrt(sum / buf.length);
195
+ options.onVolume?.(Math.min(1, rms * 1.8));
196
+ }, VOLUME_INTERVAL_MS2);
206
197
  }
207
- gainNode.connect(audioContext.destination)
208
- nextStartTime = audioContext.currentTime
209
- }
198
+ gainNode.connect(audioContext.destination);
199
+ nextStartTime = audioContext.currentTime;
200
+ };
210
201
  const setSpeaking = (v) => {
211
- if (v === speaking) return
212
- speaking = v
213
- options.onSpeakingChange?.(v)
214
- }
202
+ if (v === speaking) return;
203
+ speaking = v;
204
+ options.onSpeakingChange?.(v);
205
+ };
215
206
  const pruneFinished = () => {
216
- const now = audioContext?.currentTime ?? 0
207
+ const now = audioContext?.currentTime ?? 0;
217
208
  scheduledNodes = scheduledNodes.filter((n) => {
218
- const node = n
219
- return (node._endsAt ?? 0) > now
220
- })
221
- if (scheduledNodes.length === 0) setSpeaking(false)
222
- }
209
+ const node = n;
210
+ return (node._endsAt ?? 0) > now;
211
+ });
212
+ if (scheduledNodes.length === 0) setSpeaking(false);
213
+ };
223
214
  const enqueue = (pcm) => {
224
215
  if (!audioContext) {
225
- void ensureContext().then(() => enqueue(pcm))
226
- return
216
+ void ensureContext().then(() => enqueue(pcm));
217
+ return;
227
218
  }
228
- if (!audioContext || !gainNode) return
229
- const int16 = new Int16Array(pcm)
230
- if (int16.length === 0) return
231
- const audioBuffer = audioContext.createBuffer(1, int16.length, sampleRate)
232
- const float32 = audioBuffer.getChannelData(0)
219
+ if (!audioContext || !gainNode) return;
220
+ const int16 = new Int16Array(pcm);
221
+ if (int16.length === 0) return;
222
+ const audioBuffer = audioContext.createBuffer(1, int16.length, sampleRate);
223
+ const float32 = audioBuffer.getChannelData(0);
233
224
  for (let i = 0; i < int16.length; i++) {
234
- float32[i] = int16[i] / 32768
225
+ float32[i] = int16[i] / 32768;
235
226
  }
236
- const node = audioContext.createBufferSource()
237
- node.buffer = audioBuffer
238
- node.connect(gainNode)
239
- const now = audioContext.currentTime
240
- const startAt = Math.max(now, nextStartTime)
241
- node.start(startAt)
242
- const duration = int16.length / sampleRate
243
- node._endsAt = startAt + duration
244
- nextStartTime = startAt + duration
245
- scheduledNodes.push(node)
246
- setSpeaking(true)
247
- node.onended = () => pruneFinished()
248
- }
227
+ const node = audioContext.createBufferSource();
228
+ node.buffer = audioBuffer;
229
+ node.connect(gainNode);
230
+ const now = audioContext.currentTime;
231
+ const startAt = Math.max(now, nextStartTime);
232
+ node.start(startAt);
233
+ const duration = int16.length / sampleRate;
234
+ node._endsAt = startAt + duration;
235
+ nextStartTime = startAt + duration;
236
+ scheduledNodes.push(node);
237
+ setSpeaking(true);
238
+ node.onended = () => pruneFinished();
239
+ };
249
240
  const flush = () => {
250
- if (!audioContext || !gainNode) return
241
+ if (!audioContext || !gainNode) return;
251
242
  for (const node of scheduledNodes) {
252
243
  try {
253
- node.stop()
254
- } catch {}
244
+ node.stop();
245
+ } catch {
246
+ }
255
247
  }
256
- scheduledNodes = []
257
- gainNode.disconnect()
258
- gainNode = audioContext.createGain()
248
+ scheduledNodes = [];
249
+ gainNode.disconnect();
250
+ gainNode = audioContext.createGain();
259
251
  if (analyser) {
260
- analyser.disconnect()
261
- gainNode.connect(analyser)
252
+ analyser.disconnect();
253
+ gainNode.connect(analyser);
262
254
  }
263
- gainNode.connect(audioContext.destination)
264
- nextStartTime = audioContext.currentTime
265
- setSpeaking(false)
266
- }
255
+ gainNode.connect(audioContext.destination);
256
+ nextStartTime = audioContext.currentTime;
257
+ setSpeaking(false);
258
+ };
267
259
  const close = () => {
268
- flush()
260
+ flush();
269
261
  if (volumeTimer) {
270
- clearInterval(volumeTimer)
271
- volumeTimer = null
262
+ clearInterval(volumeTimer);
263
+ volumeTimer = null;
272
264
  }
273
- if (audioContext && audioContext.state !== 'closed') {
274
- void audioContext.close().catch(() => void 0)
265
+ if (audioContext && audioContext.state !== "closed") {
266
+ void audioContext.close().catch(() => void 0);
275
267
  }
276
- audioContext = null
277
- gainNode = null
278
- analyser = null
279
- }
268
+ audioContext = null;
269
+ gainNode = null;
270
+ analyser = null;
271
+ };
280
272
  const resume = async () => {
281
- await ensureContext()
282
- }
283
- return { enqueue, flush, close, resume }
284
- }
273
+ await ensureContext();
274
+ };
275
+ return { enqueue, flush, close, resume };
276
+ };
285
277
 
286
278
  // src/ReconnectingWebSocket.ts
287
- var READYSTATE_OPEN = 1
288
- var READYSTATE_CLOSED = 3
279
+ var READYSTATE_OPEN = 1;
280
+ var READYSTATE_CLOSED = 3;
289
281
  var createReconnectingWebSocket = (options, onEvent) => {
290
- const maxRetries = options.maxRetries ?? 3
291
- const initialBackoff = options.initialBackoffMs ?? 500
292
- const maxBackoff = options.maxBackoffMs ?? 8e3
293
- let ws = null
294
- let intentionalClose = false
295
- let retries = 0
296
- let backoff = initialBackoff
297
- let reconnectTimer = null
282
+ const maxRetries = options.maxRetries ?? 3;
283
+ const initialBackoff = options.initialBackoffMs ?? 500;
284
+ const maxBackoff = options.maxBackoffMs ?? 8e3;
285
+ let ws = null;
286
+ let intentionalClose = false;
287
+ let retries = 0;
288
+ let backoff = initialBackoff;
289
+ let reconnectTimer = null;
298
290
  const openOnce = () => {
299
- ws = options.wsFactory(options.url)
300
- ws.binaryType = 'arraybuffer'
291
+ ws = options.wsFactory(options.url);
292
+ ws.binaryType = "arraybuffer";
301
293
  ws.onopen = () => {
302
- if (retries === 0) onEvent({ type: 'open' })
303
- else onEvent({ type: 'reconnected' })
304
- retries = 0
305
- backoff = initialBackoff
306
- }
294
+ if (retries === 0) onEvent({ type: "open" });
295
+ else onEvent({ type: "reconnected" });
296
+ retries = 0;
297
+ backoff = initialBackoff;
298
+ };
307
299
  ws.onmessage = (ev) => {
308
- onEvent({ type: 'message', data: ev.data })
309
- }
300
+ onEvent({ type: "message", data: ev.data });
301
+ };
310
302
  ws.onerror = () => {
311
- onEvent({ type: 'error', error: new Error('WebSocket error') })
312
- }
303
+ onEvent({ type: "error", error: new Error("WebSocket error") });
304
+ };
313
305
  ws.onclose = (ev) => {
314
- ws = null
315
- const shouldRetry = !intentionalClose && retries < maxRetries
306
+ ws = null;
307
+ const shouldRetry = !intentionalClose && retries < maxRetries;
316
308
  if (!shouldRetry) {
317
309
  onEvent({
318
- type: 'close',
310
+ type: "close",
319
311
  code: ev.code,
320
312
  reason: ev.reason,
321
- permanent: true,
322
- })
323
- return
313
+ permanent: true
314
+ });
315
+ return;
324
316
  }
325
317
  onEvent({
326
- type: 'close',
318
+ type: "close",
327
319
  code: ev.code,
328
320
  reason: ev.reason,
329
- permanent: false,
330
- })
331
- retries++
332
- const delay = Math.min(backoff, maxBackoff)
333
- backoff = Math.min(backoff * 2, maxBackoff)
334
- reconnectTimer = setTimeout(openOnce, delay)
335
- }
336
- }
337
- openOnce()
321
+ permanent: false
322
+ });
323
+ retries++;
324
+ const delay = Math.min(backoff, maxBackoff);
325
+ backoff = Math.min(backoff * 2, maxBackoff);
326
+ reconnectTimer = setTimeout(openOnce, delay);
327
+ };
328
+ };
329
+ openOnce();
338
330
  return {
339
331
  send: (data) => {
340
- if (ws && ws.readyState === READYSTATE_OPEN) ws.send(data)
332
+ if (ws && ws.readyState === READYSTATE_OPEN) ws.send(data);
341
333
  },
342
- close: (code = 1e3, reason = 'client-requested') => {
343
- intentionalClose = true
334
+ close: (code = 1e3, reason = "client-requested") => {
335
+ intentionalClose = true;
344
336
  if (reconnectTimer) {
345
- clearTimeout(reconnectTimer)
346
- reconnectTimer = null
337
+ clearTimeout(reconnectTimer);
338
+ reconnectTimer = null;
347
339
  }
348
340
  try {
349
- ws?.close(code, reason)
350
- } catch {}
341
+ ws?.close(code, reason);
342
+ } catch {
343
+ }
351
344
  },
352
- readyState: () => ws?.readyState ?? READYSTATE_CLOSED,
353
- }
354
- }
345
+ readyState: () => ws?.readyState ?? READYSTATE_CLOSED
346
+ };
347
+ };
355
348
 
356
349
  // src/protocol.ts
357
350
  var createProtocolState = () => ({
358
- state: 'idle',
351
+ state: "idle",
359
352
  transcript: [],
360
353
  agentBubbleId: null,
361
354
  idCounter: 0,
362
- endReason: null,
363
- })
355
+ endReason: null
356
+ });
364
357
  var mapEndReason = (raw) => {
365
- if (raw === 'agent_ended') return 'agent_ended'
366
- if (raw === 'caller_hung_up') return 'user_hangup'
367
- if (raw === 'silence_timeout' || raw === 'max_duration') return 'timeout'
368
- return 'error'
369
- }
358
+ if (raw === "agent_ended") return "agent_ended";
359
+ if (raw === "caller_hung_up") return "user_hangup";
360
+ if (raw === "silence_timeout" || raw === "max_duration") return "timeout";
361
+ return "error";
362
+ };
370
363
  function handleServerMessage(raw, state, cb) {
371
- let msg
364
+ let msg;
372
365
  try {
373
- msg = JSON.parse(raw)
366
+ msg = JSON.parse(raw);
374
367
  } catch {
375
- return
368
+ return;
376
369
  }
377
370
  switch (msg.type) {
378
- case 'connected':
379
- cb.onConnected()
380
- setState(state, 'listening', cb)
381
- return
382
- case 'transcript': {
383
- const text = msg.text ?? ''
384
- if (!text) return
385
- const isFinal = !!msg.isFinal
386
- if (!isFinal) setState(state, 'user_speaking', cb)
387
- upsertUserPartial(state, text, isFinal)
388
- cb.onTranscript(state.transcript)
389
- return
371
+ case "connected":
372
+ cb.onConnected();
373
+ setState(state, "listening", cb);
374
+ return;
375
+ case "transcript": {
376
+ const text = msg.text ?? "";
377
+ if (!text) return;
378
+ const isFinal = !!msg.isFinal;
379
+ if (!isFinal) setState(state, "user_speaking", cb);
380
+ upsertUserPartial(state, text, isFinal);
381
+ cb.onTranscript(state.transcript);
382
+ return;
390
383
  }
391
- case 'agent_turn_start': {
392
- const id = `m${state.idCounter++}`
393
- state.agentBubbleId = id
394
- state.transcript = [...state.transcript, { id, role: 'agent', text: '' }]
395
- cb.onTranscript(state.transcript)
396
- const seq = typeof msg.seq === 'number' ? msg.seq : void 0
397
- cb.onAgentTurnStart(seq)
398
- setState(state, 'agent_speaking', cb)
399
- return
384
+ case "agent_turn_start": {
385
+ const id = `m${state.idCounter++}`;
386
+ state.agentBubbleId = id;
387
+ state.transcript = [...state.transcript, { id, role: "agent", text: "" }];
388
+ cb.onTranscript(state.transcript);
389
+ const seq = typeof msg.seq === "number" ? msg.seq : void 0;
390
+ cb.onAgentTurnStart(seq);
391
+ setState(state, "agent_speaking", cb);
392
+ return;
400
393
  }
401
- case 'agent_text': {
402
- const delta = msg.text ?? ''
403
- if (!delta || !state.agentBubbleId) return
404
- const id = state.agentBubbleId
405
- state.transcript = state.transcript.map((e) =>
406
- e.id === id && e.role === 'agent' ? { ...e, text: e.text + delta } : e,
407
- )
408
- cb.onTranscript(state.transcript)
409
- return
394
+ case "agent_text": {
395
+ const delta = msg.text ?? "";
396
+ if (!delta || !state.agentBubbleId) return;
397
+ const id = state.agentBubbleId;
398
+ state.transcript = state.transcript.map(
399
+ (e) => e.id === id && e.role === "agent" ? { ...e, text: e.text + delta } : e
400
+ );
401
+ cb.onTranscript(state.transcript);
402
+ return;
410
403
  }
411
- case 'agent_turn_end': {
412
- state.agentBubbleId = null
413
- const seq = typeof msg.seq === 'number' ? msg.seq : void 0
414
- cb.onAgentTurnEnd(seq)
415
- setState(state, 'listening', cb)
416
- return
404
+ case "agent_turn_end": {
405
+ state.agentBubbleId = null;
406
+ const seq = typeof msg.seq === "number" ? msg.seq : void 0;
407
+ cb.onAgentTurnEnd(seq);
408
+ setState(state, "listening", cb);
409
+ return;
417
410
  }
418
- case 'interrupt':
419
- cb.onInterrupt()
420
- return
421
- case 'agent_turn_abort': {
422
- const committed = (msg.committedText ?? '').trim()
411
+ case "interrupt":
412
+ cb.onInterrupt();
413
+ return;
414
+ case "agent_turn_abort": {
415
+ const committed = (msg.committedText ?? "").trim();
423
416
  if (state.agentBubbleId) {
424
- const id = state.agentBubbleId
417
+ const id = state.agentBubbleId;
425
418
  if (committed) {
426
- state.transcript = state.transcript.map((e) =>
427
- e.id === id && e.role === 'agent' ? { ...e, text: committed, interrupted: true } : e,
428
- )
419
+ state.transcript = state.transcript.map(
420
+ (e) => e.id === id && e.role === "agent" ? { ...e, text: committed, interrupted: true } : e
421
+ );
429
422
  } else {
430
- state.transcript = state.transcript.filter((e) => e.id !== id)
423
+ state.transcript = state.transcript.filter((e) => e.id !== id);
431
424
  }
432
- cb.onTranscript(state.transcript)
425
+ cb.onTranscript(state.transcript);
433
426
  }
434
- state.agentBubbleId = null
435
- return
427
+ state.agentBubbleId = null;
428
+ return;
436
429
  }
437
- case 'tool_call':
430
+ case "tool_call":
438
431
  state.transcript = [
439
432
  ...state.transcript,
440
433
  {
441
434
  id: `m${state.idCounter++}`,
442
- role: 'tool',
443
- text: `\u2192 ${String(msg.tool ?? '?')}(${msg.args ? JSON.stringify(msg.args) : ''})`,
444
- },
445
- ]
446
- cb.onTranscript(state.transcript)
447
- return
448
- case 'tool_result':
435
+ role: "tool",
436
+ text: `\u2192 ${String(msg.tool ?? "?")}(${msg.args ? JSON.stringify(msg.args) : ""})`
437
+ }
438
+ ];
439
+ cb.onTranscript(state.transcript);
440
+ return;
441
+ case "tool_result":
449
442
  state.transcript = [
450
443
  ...state.transcript,
451
444
  {
452
445
  id: `m${state.idCounter++}`,
453
- role: 'tool',
454
- text: `${msg.ok ? '\u2713' : '\u2717'} ${String(msg.tool ?? '?')}`,
455
- },
456
- ]
457
- cb.onTranscript(state.transcript)
458
- return
459
- case 'client_tool_call': {
460
- const toolCallId = String(msg.toolCallId ?? '')
461
- const name = String(msg.name ?? '')
462
- const args = msg.args ?? {}
463
- if (!toolCallId || !name) return
464
- cb.onClientToolCall({ toolCallId, name, args })
465
- return
446
+ role: "tool",
447
+ text: `${msg.ok ? "\u2713" : "\u2717"} ${String(msg.tool ?? "?")}`
448
+ }
449
+ ];
450
+ cb.onTranscript(state.transcript);
451
+ return;
452
+ case "client_tool_call": {
453
+ const toolCallId = String(msg.toolCallId ?? "");
454
+ const name = String(msg.name ?? "");
455
+ const args = msg.args ?? {};
456
+ if (!toolCallId || !name) return;
457
+ cb.onClientToolCall({ toolCallId, name, args });
458
+ return;
466
459
  }
467
- case 'call_end': {
468
- const reasonRaw = String(msg.reason ?? '')
469
- const reason = mapEndReason(reasonRaw)
470
- state.endReason = reason
460
+ case "call_end": {
461
+ const reasonRaw = String(msg.reason ?? "");
462
+ const reason = mapEndReason(reasonRaw);
463
+ state.endReason = reason;
471
464
  state.transcript = [
472
465
  ...state.transcript,
473
466
  {
474
467
  id: `m${state.idCounter++}`,
475
- role: 'system',
476
- text: `call ended${reasonRaw ? ` (${reasonRaw})` : ''}`,
477
- },
478
- ]
479
- cb.onTranscript(state.transcript)
480
- cb.onCallEnd(reason)
481
- return
468
+ role: "system",
469
+ text: `call ended${reasonRaw ? ` (${reasonRaw})` : ""}`
470
+ }
471
+ ];
472
+ cb.onTranscript(state.transcript);
473
+ cb.onCallEnd(reason);
474
+ return;
482
475
  }
483
- case 'error': {
484
- const code = msg.code ?? 'server_error'
485
- const message = msg.message ?? 'server error'
486
- cb.onError({ code, message })
487
- return
476
+ case "error": {
477
+ const code = msg.code ?? "server_error";
478
+ const message = msg.message ?? "server error";
479
+ cb.onError({ code, message });
480
+ return;
488
481
  }
489
482
  }
490
483
  }
491
484
  var setState = (state, next, cb) => {
492
- if (state.state === next) return
493
- cb.onState(next)
494
- }
485
+ if (state.state === next) return;
486
+ cb.onState(next);
487
+ };
495
488
  var upsertUserPartial = (state, text, isFinal) => {
496
- let idx = -1
489
+ let idx = -1;
497
490
  for (let i = state.transcript.length - 1; i >= 0; i--) {
498
- const e = state.transcript[i]
499
- if (e.role === 'user' && e.committed === false) {
500
- idx = i
501
- break
491
+ const e = state.transcript[i];
492
+ if (e.role === "user" && e.committed === false) {
493
+ idx = i;
494
+ break;
502
495
  }
503
496
  }
504
497
  if (idx === -1) {
505
498
  state.transcript = [
506
499
  ...state.transcript,
507
- { id: `m${state.idCounter++}`, role: 'user', text, committed: isFinal },
508
- ]
509
- return
500
+ { id: `m${state.idCounter++}`, role: "user", text, committed: isFinal }
501
+ ];
502
+ return;
510
503
  }
511
- const target = state.transcript[idx]
512
- const next = [...state.transcript]
513
- next[idx] = { ...target, text, committed: isFinal }
514
- state.transcript = next
515
- }
504
+ const target = state.transcript[idx];
505
+ const next = [...state.transcript];
506
+ next[idx] = { ...target, text, committed: isFinal };
507
+ state.transcript = next;
508
+ };
516
509
  function buildWsUrl(args) {
517
- const base = new URL(args.apiBase)
518
- const proto = base.protocol === 'https:' ? 'wss:' : 'ws:'
519
- const bargeQS = args.bargeIn === false ? '&barge=off' : ''
520
- return `${proto}//${base.host}/v1/agents/${encodeURIComponent(args.agentId)}/call?token=${encodeURIComponent(args.token)}${bargeQS}`
510
+ const base = new URL(args.apiBase);
511
+ const proto = base.protocol === "https:" ? "wss:" : "ws:";
512
+ const bargeQS = args.bargeIn === false ? "&barge=off" : "";
513
+ return `${proto}//${base.host}/v1/agents/${encodeURIComponent(args.agentId)}/call?token=${encodeURIComponent(args.token)}${bargeQS}`;
521
514
  }
522
515
 
523
516
  // src/clientTools.ts
524
- var NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/
525
- var MAX_TOOLS = 64
526
- var MAX_USAGE = 500
527
- var MAX_TIMEOUT_MS = 3e4
517
+ var NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
518
+ var MAX_TOOLS = 64;
519
+ var MAX_USAGE = 500;
520
+ var MAX_TIMEOUT_MS = 3e4;
528
521
  var validateClientToolMap = (tools) => {
529
- if (tools === void 0) return
530
- if (typeof tools !== 'object' || tools === null || Array.isArray(tools)) {
531
- throw new Error('clientTools must be an object keyed by tool name')
522
+ if (tools === void 0) return;
523
+ if (typeof tools !== "object" || tools === null || Array.isArray(tools)) {
524
+ throw new Error("clientTools must be an object keyed by tool name");
532
525
  }
533
- const entries = Object.entries(tools)
526
+ const entries = Object.entries(tools);
534
527
  if (entries.length > MAX_TOOLS) {
535
- throw new Error(`clientTools may declare at most 64 tools (got ${entries.length})`)
528
+ throw new Error(`clientTools may declare at most 64 tools (got ${entries.length})`);
536
529
  }
537
530
  for (const [name, def] of entries) {
538
531
  if (!NAME_RE.test(name)) {
539
532
  throw new Error(
540
- `clientTools["${name}"]: name must be a valid identifier (^[a-zA-Z_][a-zA-Z0-9_]*$)`,
541
- )
533
+ `clientTools["${name}"]: name must be a valid identifier (^[a-zA-Z_][a-zA-Z0-9_]*$)`
534
+ );
542
535
  }
543
- if (!def || typeof def !== 'object') {
544
- throw new Error(`clientTools["${name}"]: must be an object`)
536
+ if (!def || typeof def !== "object") {
537
+ throw new Error(`clientTools["${name}"]: must be an object`);
545
538
  }
546
- if (typeof def.description !== 'string' || def.description.length === 0) {
547
- throw new Error(`clientTools["${name}"]: must have a description`)
539
+ if (typeof def.description !== "string" || def.description.length === 0) {
540
+ throw new Error(`clientTools["${name}"]: must have a description`);
548
541
  }
549
- if (typeof def.handler !== 'function') {
550
- throw new Error(`clientTools["${name}"]: must have a handler function`)
542
+ if (typeof def.handler !== "function") {
543
+ throw new Error(`clientTools["${name}"]: must have a handler function`);
551
544
  }
552
545
  if (def.usage !== void 0 && def.usage.length > MAX_USAGE) {
553
- throw new Error(`clientTools["${name}"]: usage must be \u2264500 chars`)
546
+ throw new Error(`clientTools["${name}"]: usage must be \u2264500 chars`);
554
547
  }
555
- if (
556
- def.timeoutMs !== void 0 &&
557
- (!Number.isFinite(def.timeoutMs) || def.timeoutMs <= 0 || def.timeoutMs > MAX_TIMEOUT_MS)
558
- ) {
559
- throw new Error(`clientTools["${name}"]: timeoutMs must be in (0, 30000]`)
548
+ if (def.timeoutMs !== void 0 && (!Number.isFinite(def.timeoutMs) || def.timeoutMs <= 0 || def.timeoutMs > MAX_TIMEOUT_MS)) {
549
+ throw new Error(`clientTools["${name}"]: timeoutMs must be in (0, 30000]`);
560
550
  }
561
551
  }
562
- }
552
+ };
563
553
  var buildRegisterFrame = (tools) => ({
564
- type: 'client_tools_register',
554
+ type: "client_tools_register",
565
555
  tools: Object.entries(tools).map(([name, def]) => ({
566
556
  name,
567
557
  description: def.description,
568
558
  parameters: def.parameters,
569
- ...(def.usage !== void 0 ? { usage: def.usage } : {}),
570
- ...(def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}),
571
- })),
572
- })
559
+ ...def.usage !== void 0 ? { usage: def.usage } : {},
560
+ ...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
561
+ }))
562
+ });
573
563
  var dispatchClientToolCall = (send, tools, frame) => {
574
564
  const safeSend = (payload) => {
575
565
  try {
576
- send(payload)
577
- } catch {}
578
- }
579
- const tool = tools[frame.name]
566
+ send(payload);
567
+ } catch {
568
+ }
569
+ };
570
+ const tool = tools[frame.name];
580
571
  if (!tool) {
581
572
  safeSend({
582
- type: 'client_tool_result',
573
+ type: "client_tool_result",
583
574
  toolCallId: frame.toolCallId,
584
- error: `No handler for ${frame.name}`,
585
- })
586
- return
575
+ error: `No handler for ${frame.name}`
576
+ });
577
+ return;
587
578
  }
588
579
  void (async () => {
589
580
  try {
590
- const out = await tool.handler(frame.args)
581
+ const out = await tool.handler(frame.args);
591
582
  safeSend({
592
- type: 'client_tool_result',
583
+ type: "client_tool_result",
593
584
  toolCallId: frame.toolCallId,
594
- result: typeof out === 'string' ? out : JSON.stringify(out),
595
- })
585
+ result: typeof out === "string" ? out : JSON.stringify(out)
586
+ });
596
587
  } catch (err) {
597
588
  safeSend({
598
- type: 'client_tool_result',
589
+ type: "client_tool_result",
599
590
  toolCallId: frame.toolCallId,
600
- error: err instanceof Error ? err.message : String(err),
601
- })
591
+ error: err instanceof Error ? err.message : String(err)
592
+ });
602
593
  }
603
- })()
604
- }
594
+ })();
595
+ };
605
596
 
606
597
  // src/ClientMarksBuffer.ts
607
598
  var createClientMarksBuffer = (args) => {
608
- const now = args.now ?? (() => performance.now())
609
- let pendingFirstOutboundAt = null
610
- const inFlight = /* @__PURE__ */ new Map()
599
+ const now = args.now ?? (() => performance.now());
600
+ let pendingFirstOutboundAt = null;
601
+ const inFlight = /* @__PURE__ */ new Map();
611
602
  const tryEmit = (seq) => {
612
- const slot = inFlight.get(seq)
613
- if (!slot) return
614
- if (!slot.ended) return
615
- const marks = {}
603
+ const slot = inFlight.get(seq);
604
+ if (!slot) return;
605
+ if (!slot.ended) return;
606
+ const marks = {};
616
607
  if (slot.firstOutboundAt !== null && slot.firstAudibleAt !== null) {
617
- marks.client_mic_to_first_audible_ms = slot.firstAudibleAt - slot.firstOutboundAt
608
+ marks.client_mic_to_first_audible_ms = slot.firstAudibleAt - slot.firstOutboundAt;
618
609
  }
619
610
  args.send({
620
- type: 'client_marks',
611
+ type: "client_marks",
621
612
  seq,
622
613
  marks,
623
- clientNow: Date.now(),
624
- })
625
- inFlight.delete(seq)
626
- }
614
+ clientNow: Date.now()
615
+ });
616
+ inFlight.delete(seq);
617
+ };
627
618
  const markFirstOutboundAudio = () => {
628
- if (pendingFirstOutboundAt !== null) return
629
- pendingFirstOutboundAt = now()
630
- }
619
+ if (pendingFirstOutboundAt !== null) return;
620
+ pendingFirstOutboundAt = now();
621
+ };
631
622
  const markFirstAudibleOutput = () => {
632
- let target
623
+ let target;
633
624
  for (const slot of inFlight.values()) {
634
625
  if (!slot.ended) {
635
- target = slot
626
+ target = slot;
636
627
  }
637
628
  }
638
- if (!target) return
639
- if (target.firstAudibleAt !== null) return
640
- target.firstAudibleAt = now()
641
- }
629
+ if (!target) return;
630
+ if (target.firstAudibleAt !== null) return;
631
+ target.firstAudibleAt = now();
632
+ };
642
633
  const onAgentTurnStart = (seq) => {
643
634
  inFlight.set(seq, {
644
635
  firstOutboundAt: pendingFirstOutboundAt,
645
636
  firstAudibleAt: null,
646
- ended: false,
647
- })
648
- pendingFirstOutboundAt = null
649
- }
637
+ ended: false
638
+ });
639
+ pendingFirstOutboundAt = null;
640
+ };
650
641
  const onAgentTurnEnd = (seq) => {
651
- const slot = inFlight.get(seq)
642
+ const slot = inFlight.get(seq);
652
643
  if (!slot) {
653
- args.send({ type: 'client_marks', seq, marks: {}, clientNow: Date.now() })
654
- return
644
+ args.send({ type: "client_marks", seq, marks: {}, clientNow: Date.now() });
645
+ return;
655
646
  }
656
- slot.ended = true
657
- tryEmit(seq)
658
- }
647
+ slot.ended = true;
648
+ tryEmit(seq);
649
+ };
659
650
  const flush = () => {
660
651
  for (const seq of [...inFlight.keys()]) {
661
- const slot = inFlight.get(seq)
662
- slot.ended = true
663
- tryEmit(seq)
652
+ const slot = inFlight.get(seq);
653
+ slot.ended = true;
654
+ tryEmit(seq);
664
655
  }
665
- pendingFirstOutboundAt = null
666
- }
656
+ pendingFirstOutboundAt = null;
657
+ };
667
658
  return {
668
659
  markFirstOutboundAudio,
669
660
  markFirstAudibleOutput,
670
661
  onAgentTurnStart,
671
662
  onAgentTurnEnd,
672
- flush,
673
- }
674
- }
663
+ flush
664
+ };
665
+ };
675
666
 
676
667
  // src/VoiceClient.ts
677
668
  var BrowserVoiceClient = class {
678
669
  constructor(args) {
679
- this.rws = null
680
- this.capture = null
681
- this.playback = null
682
- this.muted = false
683
- this.inputVolume = 0
684
- this.outputVolume = 0
685
- this.startedAt = null
686
- this.endedFired = false
687
- this.lastError = null
670
+ this.rws = null;
671
+ this.capture = null;
672
+ this.playback = null;
673
+ this.muted = false;
674
+ this.inputVolume = 0;
675
+ this.outputVolume = 0;
676
+ this.startedAt = null;
677
+ this.endedFired = false;
678
+ this.lastError = null;
688
679
  this.end = () => {
689
- this.teardown('user_hangup')
690
- }
680
+ this.teardown("user_hangup");
681
+ };
691
682
  this.mute = () => {
692
- if (this.muted) return
693
- this.muted = true
694
- this.capture?.mute(true)
695
- }
683
+ if (this.muted) return;
684
+ this.muted = true;
685
+ this.capture?.mute(true);
686
+ };
696
687
  this.unmute = () => {
697
- if (!this.muted) return
698
- this.muted = false
699
- this.capture?.mute(false)
700
- }
688
+ if (!this.muted) return;
689
+ this.muted = false;
690
+ this.capture?.mute(false);
691
+ };
701
692
  // ---------------------------------------------------------------
702
693
  // Internal
703
694
  // ---------------------------------------------------------------
704
695
  this.sendClientToolsRegister = () => {
705
- const frame = buildRegisterFrame(this.args.options.clientTools ?? {})
706
- this.rws?.send(JSON.stringify(frame))
707
- }
696
+ const frame = buildRegisterFrame(this.args.options.clientTools ?? {});
697
+ this.rws?.send(JSON.stringify(frame));
698
+ };
708
699
  this.setState = (next) => {
709
- if (this.proto.state === next) return
710
- this.proto.state = next
711
- this.args.options.onStateChange?.(next)
712
- }
700
+ if (this.proto.state === next) return;
701
+ this.proto.state = next;
702
+ this.args.options.onStateChange?.(next);
703
+ };
713
704
  this.emitError = (err) => {
714
- this.lastError = err
715
- this.args.options.onError?.(err)
716
- }
705
+ this.lastError = err;
706
+ this.args.options.onError?.(err);
707
+ };
717
708
  this.handleSocketEvent = (ev) => {
718
709
  switch (ev.type) {
719
- case 'open':
720
- void this.startCapture()
721
- break
722
- case 'reconnected':
723
- this.proto.transcript = []
724
- this.proto.agentBubbleId = null
725
- this.args.options.onTranscript?.(this.proto.transcript)
726
- void this.startCapture()
727
- this.setState('listening')
728
- break
729
- case 'message':
730
- if (typeof ev.data === 'string') {
710
+ case "open":
711
+ void this.startCapture();
712
+ break;
713
+ case "reconnected":
714
+ this.proto.transcript = [];
715
+ this.proto.agentBubbleId = null;
716
+ this.args.options.onTranscript?.(this.proto.transcript);
717
+ void this.startCapture();
718
+ this.setState("listening");
719
+ break;
720
+ case "message":
721
+ if (typeof ev.data === "string") {
731
722
  handleServerMessage(ev.data, this.proto, {
732
723
  onState: this.setState,
733
724
  onTranscript: (entries) => this.args.options.onTranscript?.(entries),
734
725
  onError: this.emitError,
735
726
  onInterrupt: () => {
736
- this.playback?.flush()
737
- this.args.options.onInterrupt?.()
727
+ this.playback?.flush();
728
+ this.args.options.onInterrupt?.();
738
729
  },
739
730
  onAgentTurnStart: (seq) => {
740
- if (typeof seq === 'number') this.marks.onAgentTurnStart(seq)
741
- this.args.options.onAgentTurnStart?.()
731
+ if (typeof seq === "number") this.marks.onAgentTurnStart(seq);
732
+ this.args.options.onAgentTurnStart?.();
742
733
  },
743
734
  onAgentTurnEnd: (seq) => {
744
- if (typeof seq === 'number') this.marks.onAgentTurnEnd(seq)
735
+ if (typeof seq === "number") this.marks.onAgentTurnEnd(seq);
745
736
  },
746
737
  onCallEnd: (reason) => this.teardown(reason),
747
738
  onConnected: () => this.sendClientToolsRegister(),
748
- onClientToolCall: (frame) =>
749
- dispatchClientToolCall(
750
- (f) => this.rws?.send(JSON.stringify(f)),
751
- this.args.options.clientTools ?? {},
752
- frame,
753
- ),
754
- })
739
+ onClientToolCall: (frame) => dispatchClientToolCall(
740
+ (f) => this.rws?.send(JSON.stringify(f)),
741
+ this.args.options.clientTools ?? {},
742
+ frame
743
+ )
744
+ });
755
745
  } else {
756
- this.marks.markFirstAudibleOutput()
757
- this.playback?.enqueue(ev.data)
746
+ this.marks.markFirstAudibleOutput();
747
+ this.playback?.enqueue(ev.data);
758
748
  }
759
- break
760
- case 'close':
749
+ break;
750
+ case "close":
761
751
  if (ev.permanent) {
762
- const reason = this.proto.endReason ?? (this.lastError ? 'error' : 'user_hangup')
763
- this.teardown(reason)
752
+ const reason = this.proto.endReason ?? (this.lastError ? "error" : "user_hangup");
753
+ this.teardown(reason);
764
754
  }
765
- break
766
- case 'error':
767
- this.emitError({ code: 'socket_error', message: ev.error.message })
768
- break
755
+ break;
756
+ case "error":
757
+ this.emitError({ code: "socket_error", message: ev.error.message });
758
+ break;
769
759
  }
770
- }
760
+ };
771
761
  this.startCapture = async () => {
772
- if (this.capture?.isCapturing()) return
762
+ if (this.capture?.isCapturing()) return;
773
763
  this.capture = createAudioCapture({
774
764
  onChunk: (pcm) => {
775
- this.marks.markFirstOutboundAudio()
776
- this.rws?.send(pcm)
765
+ this.marks.markFirstOutboundAudio();
766
+ this.rws?.send(pcm);
777
767
  },
778
768
  onVolume: (v) => {
779
- this.inputVolume = v
780
- this.args.options.onVolume?.({ input: v, output: this.outputVolume })
769
+ this.inputVolume = v;
770
+ this.args.options.onVolume?.({ input: v, output: this.outputVolume });
781
771
  },
782
772
  onError: (err) => {
783
773
  this.emitError({
784
- code: err.name === 'NotAllowedError' ? 'mic_denied' : 'mic_start_failed',
785
- message: err.message,
786
- })
787
- },
788
- })
789
- if (this.muted) this.capture.mute(true)
774
+ code: err.name === "NotAllowedError" ? "mic_denied" : "mic_start_failed",
775
+ message: err.message
776
+ });
777
+ }
778
+ });
779
+ if (this.muted) this.capture.mute(true);
790
780
  try {
791
- await this.capture.start()
792
- } catch {}
793
- }
781
+ await this.capture.start();
782
+ } catch {
783
+ }
784
+ };
794
785
  this.teardown = (reason) => {
795
786
  try {
796
- this.marks.flush()
797
- } catch {}
798
- this.capture?.stop()
799
- this.capture = null
800
- this.playback?.close()
801
- this.playback = null
787
+ this.marks.flush();
788
+ } catch {
789
+ }
790
+ this.capture?.stop();
791
+ this.capture = null;
792
+ this.playback?.close();
793
+ this.playback = null;
802
794
  try {
803
- this.rws?.close(1e3, reason)
804
- } catch {}
805
- this.rws = null
806
- this.setState('ended')
807
- this.fireEndOnce(reason)
808
- }
795
+ this.rws?.close(1e3, reason);
796
+ } catch {
797
+ }
798
+ this.rws = null;
799
+ this.setState("ended");
800
+ this.fireEndOnce(reason);
801
+ };
809
802
  this.fireEndOnce = (reason) => {
810
- if (this.endedFired) return
811
- this.endedFired = true
812
- const startedAt = this.startedAt ?? Date.now()
803
+ if (this.endedFired) return;
804
+ this.endedFired = true;
805
+ const startedAt = this.startedAt ?? Date.now();
813
806
  this.args.options.onEnd?.({
814
807
  reason,
815
- errorCode: reason === 'error' ? this.lastError?.code : void 0,
816
- durationMs: Date.now() - startedAt,
817
- })
818
- }
819
- this.args = args
820
- this.proto = createProtocolState()
821
- validateClientToolMap(args.options.clientTools)
808
+ errorCode: reason === "error" ? this.lastError?.code : void 0,
809
+ durationMs: Date.now() - startedAt
810
+ });
811
+ };
812
+ this.args = args;
813
+ this.proto = createProtocolState();
814
+ validateClientToolMap(args.options.clientTools);
822
815
  this.marks = createClientMarksBuffer({
823
816
  send: (frame) => {
824
817
  try {
825
- this.rws?.send(JSON.stringify(frame))
826
- } catch {}
827
- },
828
- })
818
+ this.rws?.send(JSON.stringify(frame));
819
+ } catch {
820
+ }
821
+ }
822
+ });
829
823
  }
830
824
  // ---------------------------------------------------------------
831
825
  // Call interface
832
826
  // ---------------------------------------------------------------
833
827
  get state() {
834
- return this.proto.state
828
+ return this.proto.state;
835
829
  }
836
830
  get transcript() {
837
- return this.proto.transcript.slice()
831
+ return this.proto.transcript.slice();
838
832
  }
839
833
  get isMuted() {
840
- return this.muted
834
+ return this.muted;
841
835
  }
842
836
  // ---------------------------------------------------------------
843
837
  // Lifecycle — called by the factory immediately after construction.
@@ -845,52 +839,54 @@ var BrowserVoiceClient = class {
845
839
  // failures arrive via `onError`.
846
840
  // ---------------------------------------------------------------
847
841
  async start() {
848
- this.setState('connecting')
849
- this.startedAt = Date.now()
842
+ this.setState("connecting");
843
+ this.startedAt = Date.now();
850
844
  const url = buildWsUrl({
851
845
  apiBase: this.args.config.apiBase,
852
846
  agentId: this.args.options.agentId,
853
847
  token: this.args.token,
854
- bargeIn: this.args.options.bargeIn,
855
- })
848
+ bargeIn: this.args.options.bargeIn
849
+ });
856
850
  this.playback = createAudioPlayback({
857
851
  onVolume: (v) => {
858
- this.outputVolume = v
859
- this.args.options.onVolume?.({ input: this.inputVolume, output: v })
860
- },
861
- })
852
+ this.outputVolume = v;
853
+ this.args.options.onVolume?.({ input: this.inputVolume, output: v });
854
+ }
855
+ });
862
856
  try {
863
- await this.playback.resume()
864
- } catch {}
857
+ await this.playback.resume();
858
+ } catch {
859
+ }
865
860
  this.rws = createReconnectingWebSocket(
866
861
  {
867
862
  url,
868
863
  wsFactory: this.args.wsFactory,
869
- maxRetries: 3,
864
+ maxRetries: 3
870
865
  },
871
- (ev) => this.handleSocketEvent(ev),
872
- )
866
+ (ev) => this.handleSocketEvent(ev)
867
+ );
873
868
  }
874
- }
869
+ };
875
870
 
876
871
  // src/webrtc/createWebRtcCall.ts
877
872
  async function createWebRtcCall(opts) {
878
- validateClientToolMap(opts.clientTools)
879
- const proto = createProtocolState()
880
- let muted = false
881
- let ended = false
882
- const tools = opts.clientTools ?? {}
873
+ validateClientToolMap(opts.clientTools);
874
+ const proto = createProtocolState();
875
+ let muted = false;
876
+ let ended = false;
877
+ const tools = opts.clientTools ?? {};
883
878
  const sendControl = (frame) => {
884
- if (dc?.readyState !== 'open') return
879
+ if (dc?.readyState !== "open") return;
885
880
  try {
886
- dc.send(JSON.stringify(frame))
887
- } catch {}
888
- }
881
+ dc.send(JSON.stringify(frame));
882
+ } catch {
883
+ }
884
+ };
889
885
  const fireState = (next) => {
890
- if (proto.state === next) return
891
- proto.state = next
892
- opts.onStateChange?.(next)
893
- }
886
+ if (proto.state === next) return;
887
+ proto.state = next;
888
+ opts.onStateChange?.(next);
889
+ };
894
890
  const dispatch = (raw) => {
895
891
  handleServerMessage(raw, proto, {
896
892
  onState: fireState,
@@ -898,184 +894,188 @@ async function createWebRtcCall(opts) {
898
894
  onError: (err) => opts.onError?.(err),
899
895
  onInterrupt: () => opts.onInterrupt?.(),
900
896
  onAgentTurnStart: () => opts.onAgentTurnStart?.(),
901
- onAgentTurnEnd: () => {},
897
+ onAgentTurnEnd: () => {
898
+ },
902
899
  onCallEnd: () => teardown(),
903
900
  onConnected: () => {
904
901
  if (Object.keys(tools).length > 0) {
905
- sendControl(buildRegisterFrame(tools))
902
+ sendControl(buildRegisterFrame(tools));
906
903
  }
907
904
  },
908
905
  onClientToolCall: (frame) => {
909
- dispatchClientToolCall(sendControl, tools, frame)
910
- },
911
- })
912
- }
913
- fireState('connecting')
906
+ dispatchClientToolCall(sendControl, tools, frame);
907
+ }
908
+ });
909
+ };
910
+ fireState("connecting");
914
911
  const pc = new RTCPeerConnection({
915
- iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
916
- })
917
- const audioEl = document.createElement('audio')
918
- audioEl.autoplay = true
919
- audioEl.style.display = 'none'
920
- document.body.appendChild(audioEl)
912
+ iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
913
+ });
914
+ const audioEl = document.createElement("audio");
915
+ audioEl.autoplay = true;
916
+ audioEl.style.display = "none";
917
+ document.body.appendChild(audioEl);
921
918
  pc.ontrack = (event) => {
922
- audioEl.srcObject = event.streams[0] ?? new MediaStream([event.track])
923
- }
924
- let mic
919
+ audioEl.srcObject = event.streams[0] ?? new MediaStream([event.track]);
920
+ };
921
+ let mic;
925
922
  try {
926
- mic = await navigator.mediaDevices.getUserMedia({ audio: true })
923
+ mic = await navigator.mediaDevices.getUserMedia({ audio: true });
927
924
  } catch (err) {
928
- const code =
929
- err instanceof DOMException && err.name === 'NotAllowedError'
930
- ? 'mic_denied'
931
- : 'mic_start_failed'
925
+ const code = err instanceof DOMException && err.name === "NotAllowedError" ? "mic_denied" : "mic_start_failed";
932
926
  opts.onError?.({
933
927
  code,
934
- message: err instanceof Error ? err.message : 'getUserMedia failed',
935
- })
936
- fireState('error')
937
- pc.close()
938
- audioEl.remove()
939
- throw err
928
+ message: err instanceof Error ? err.message : "getUserMedia failed"
929
+ });
930
+ fireState("error");
931
+ pc.close();
932
+ audioEl.remove();
933
+ throw err;
940
934
  }
941
- for (const track of mic.getAudioTracks()) pc.addTrack(track, mic)
942
- const dc = pc.createDataChannel('control', { ordered: true })
935
+ for (const track of mic.getAudioTracks()) pc.addTrack(track, mic);
936
+ const dc = pc.createDataChannel("control", { ordered: true });
943
937
  dc.onmessage = (e) => {
944
- if (typeof e.data === 'string') dispatch(e.data)
945
- }
938
+ if (typeof e.data === "string") dispatch(e.data);
939
+ };
946
940
  dc.onerror = () => {
947
- opts.onError?.({ code: 'socket_error', message: 'control channel error' })
948
- }
941
+ opts.onError?.({ code: "socket_error", message: "control channel error" });
942
+ };
949
943
  dc.onopen = () => {
950
944
  if (Object.keys(tools).length > 0) {
951
- sendControl(buildRegisterFrame(tools))
945
+ sendControl(buildRegisterFrame(tools));
952
946
  }
953
- }
954
- const gateway = opts.webrtcGatewayBase || ''
955
- const offerUrl = gateway
956
- ? `${gateway}/webrtc/offer?token=${encodeURIComponent(opts.token)}`
957
- : `${opts.apiBase}/v1/agents/${encodeURIComponent(opts.agentId)}/webrtc/offer?token=${encodeURIComponent(opts.token)}`
958
- const iceUrl = gateway
959
- ? `${gateway}/webrtc/ice?token=${encodeURIComponent(opts.token)}`
960
- : `${opts.apiBase}/v1/agents/${encodeURIComponent(opts.agentId)}/webrtc/ice?token=${encodeURIComponent(opts.token)}`
961
- await pc.setLocalDescription(await pc.createOffer())
962
- let callId
947
+ };
948
+ const gateway = opts.webrtcGatewayBase || "";
949
+ const offerUrl = gateway ? `${gateway}/webrtc/offer?token=${encodeURIComponent(opts.token)}` : `${opts.apiBase}/v1/agents/${encodeURIComponent(opts.agentId)}/webrtc/offer?token=${encodeURIComponent(opts.token)}`;
950
+ const iceUrl = gateway ? `${gateway}/webrtc/ice?token=${encodeURIComponent(opts.token)}` : `${opts.apiBase}/v1/agents/${encodeURIComponent(opts.agentId)}/webrtc/ice?token=${encodeURIComponent(opts.token)}`;
951
+ const teardown = () => {
952
+ if (ended) return;
953
+ ended = true;
954
+ try {
955
+ mic.getTracks().forEach((t) => t.stop());
956
+ } catch {
957
+ }
958
+ try {
959
+ pc.close();
960
+ } catch {
961
+ }
962
+ try {
963
+ audioEl.remove();
964
+ } catch {
965
+ }
966
+ fireState("ended");
967
+ opts.onEnd?.();
968
+ };
969
+ let callId = null;
970
+ const pendingCandidates = [];
971
+ const postCandidate = (candidate) => {
972
+ void fetch(iceUrl, {
973
+ method: "POST",
974
+ headers: { "content-type": "application/json" },
975
+ body: JSON.stringify({ callId, candidate })
976
+ }).catch(() => {
977
+ });
978
+ };
979
+ pc.onicecandidate = (e) => {
980
+ if (!e.candidate) return;
981
+ if (callId) postCandidate(e.candidate);
982
+ else pendingCandidates.push(e.candidate);
983
+ };
984
+ pc.onconnectionstatechange = () => {
985
+ const s = pc.connectionState;
986
+ if (s === "connected") fireState("listening");
987
+ if (s === "failed" || s === "disconnected") {
988
+ opts.onError?.({ code: "socket_error", message: `webrtc connection ${s}` });
989
+ teardown();
990
+ }
991
+ if (s === "closed" && !ended) teardown();
992
+ };
993
+ await pc.setLocalDescription(await pc.createOffer());
963
994
  try {
964
995
  const offerRes = await fetch(offerUrl, {
965
- method: 'POST',
966
- headers: { 'content-type': 'application/json' },
967
- body: JSON.stringify({ sdp: pc.localDescription.sdp, type: 'offer', agentId: opts.agentId }),
968
- })
996
+ method: "POST",
997
+ headers: { "content-type": "application/json" },
998
+ body: JSON.stringify({ sdp: pc.localDescription.sdp, type: "offer", agentId: opts.agentId })
999
+ });
969
1000
  if (!offerRes.ok) {
970
- const code = offerRes.status === 401 ? 'unauthorized' : 'server_error'
971
- opts.onError?.({ code, message: `signaling failed: HTTP ${offerRes.status}` })
972
- fireState('error')
973
- mic.getTracks().forEach((t) => t.stop())
974
- pc.close()
975
- audioEl.remove()
976
- throw new Error(`webrtc offer failed: ${offerRes.status}`)
1001
+ const code = offerRes.status === 401 ? "unauthorized" : "server_error";
1002
+ opts.onError?.({ code, message: `signaling failed: HTTP ${offerRes.status}` });
1003
+ fireState("error");
1004
+ mic.getTracks().forEach((t) => t.stop());
1005
+ pc.close();
1006
+ audioEl.remove();
1007
+ throw new Error(`webrtc offer failed: ${offerRes.status}`);
977
1008
  }
978
- const body = await offerRes.json()
979
- callId = body.callId
980
- await pc.setRemoteDescription({ type: 'answer', sdp: body.sdp })
1009
+ const body = await offerRes.json();
1010
+ callId = body.callId;
1011
+ await pc.setRemoteDescription({ type: "answer", sdp: body.sdp });
1012
+ while (pendingCandidates.length > 0) postCandidate(pendingCandidates.shift());
981
1013
  } catch (err) {
982
1014
  if (!ended) {
983
1015
  opts.onError?.({
984
- code: 'network_unreachable',
985
- message: err instanceof Error ? err.message : 'signaling failed',
986
- })
987
- fireState('error')
988
- mic.getTracks().forEach((t) => t.stop())
989
- pc.close()
990
- audioEl.remove()
1016
+ code: "network_unreachable",
1017
+ message: err instanceof Error ? err.message : "signaling failed"
1018
+ });
1019
+ fireState("error");
1020
+ mic.getTracks().forEach((t) => t.stop());
1021
+ pc.close();
1022
+ audioEl.remove();
991
1023
  }
992
- throw err
993
- }
994
- pc.onicecandidate = (e) => {
995
- if (!e.candidate) return
996
- void fetch(iceUrl, {
997
- method: 'POST',
998
- headers: { 'content-type': 'application/json' },
999
- body: JSON.stringify({ callId, candidate: e.candidate }),
1000
- }).catch(() => {})
1001
- }
1002
- pc.onconnectionstatechange = () => {
1003
- const s = pc.connectionState
1004
- if (s === 'connected') fireState('listening')
1005
- if (s === 'failed' || s === 'disconnected') {
1006
- opts.onError?.({ code: 'socket_error', message: `webrtc connection ${s}` })
1007
- teardown()
1008
- }
1009
- if (s === 'closed' && !ended) teardown()
1010
- }
1011
- const teardown = () => {
1012
- if (ended) return
1013
- ended = true
1014
- try {
1015
- mic.getTracks().forEach((t) => t.stop())
1016
- } catch {}
1017
- try {
1018
- pc.close()
1019
- } catch {}
1020
- try {
1021
- audioEl.remove()
1022
- } catch {}
1023
- fireState('ended')
1024
- opts.onEnd?.()
1024
+ throw err;
1025
1025
  }
1026
1026
  return {
1027
1027
  get state() {
1028
- return proto.state
1028
+ return proto.state;
1029
1029
  },
1030
1030
  get transcript() {
1031
- return proto.transcript.slice()
1031
+ return proto.transcript.slice();
1032
1032
  },
1033
1033
  get isMuted() {
1034
- return muted
1034
+ return muted;
1035
1035
  },
1036
1036
  end: () => teardown(),
1037
1037
  mute: () => {
1038
- if (muted) return
1039
- muted = true
1040
- mic.getAudioTracks().forEach((t) => (t.enabled = false))
1038
+ if (muted) return;
1039
+ muted = true;
1040
+ mic.getAudioTracks().forEach((t) => t.enabled = false);
1041
1041
  },
1042
1042
  unmute: () => {
1043
- if (!muted) return
1044
- muted = false
1045
- mic.getAudioTracks().forEach((t) => (t.enabled = true))
1046
- },
1047
- }
1043
+ if (!muted) return;
1044
+ muted = false;
1045
+ mic.getAudioTracks().forEach((t) => t.enabled = true);
1046
+ }
1047
+ };
1048
1048
  }
1049
1049
 
1050
1050
  // src/browser.ts
1051
- var browserWsFactory = (url) => new globalThis.WebSocket(url)
1051
+ var browserWsFactory = (url) => new globalThis.WebSocket(url);
1052
1052
  var BrowserVoiceFactory = class {
1053
1053
  constructor(config) {
1054
1054
  this.startCall = async (options) => {
1055
1055
  if (!options.agentId) {
1056
- throw new Error('startCall: agentId is required')
1056
+ throw new Error("startCall: agentId is required");
1057
1057
  }
1058
- const { context, metadata } = mergeStartCallContext(this.config, options)
1058
+ const { context, metadata } = mergeStartCallContext(this.config, options);
1059
1059
  const fetchArgs = {
1060
1060
  agentId: options.agentId,
1061
1061
  userId: options.userId,
1062
1062
  context,
1063
- metadata,
1064
- }
1065
- let resolved
1063
+ metadata
1064
+ };
1065
+ let resolved;
1066
1066
  if (options.token) {
1067
- resolved = { token: options.token, transport: 'ws' }
1067
+ resolved = { token: options.token, transport: "ws" };
1068
1068
  } else {
1069
- const r = await this.config.fetchToken(fetchArgs)
1069
+ const r = await this.config.fetchToken(fetchArgs);
1070
1070
  if (!r) {
1071
- throw new Error('configureVoiceClient.fetchToken returned empty token')
1071
+ throw new Error("configureVoiceClient.fetchToken returned empty token");
1072
1072
  }
1073
- resolved = typeof r === 'string' ? { token: r, transport: 'ws' } : r
1073
+ resolved = typeof r === "string" ? { token: r, transport: "ws" } : r;
1074
1074
  if (!resolved.token) {
1075
- throw new Error('configureVoiceClient.fetchToken returned an object without `token`')
1075
+ throw new Error("configureVoiceClient.fetchToken returned an object without `token`");
1076
1076
  }
1077
1077
  }
1078
- if (resolved.transport === 'webrtc') {
1078
+ if (resolved.transport === "webrtc") {
1079
1079
  return createWebRtcCall({
1080
1080
  agentId: options.agentId,
1081
1081
  apiBase: this.config.apiBase,
@@ -1087,13 +1087,11 @@ var BrowserVoiceFactory = class {
1087
1087
  // Synthesise a minimal CallEndEvent. WebRTC doesn't carry an end reason
1088
1088
  // from the server yet — use 'agent_ended' as placeholder. durationMs is
1089
1089
  // tracked at 0 until the followup lands (see spec Followups section).
1090
- onEnd: options.onEnd
1091
- ? () => options.onEnd({ reason: 'agent_ended', durationMs: 0 })
1092
- : void 0,
1090
+ onEnd: options.onEnd ? () => options.onEnd({ reason: "agent_ended", durationMs: 0 }) : void 0,
1093
1091
  onInterrupt: options.onInterrupt,
1094
1092
  onAgentTurnStart: options.onAgentTurnStart,
1095
- clientTools: options.clientTools,
1096
- })
1093
+ clientTools: options.clientTools
1094
+ });
1097
1095
  }
1098
1096
  const client = new BrowserVoiceClient({
1099
1097
  config: this.config,
@@ -1101,26 +1099,25 @@ var BrowserVoiceFactory = class {
1101
1099
  // see what the SDK saw.
1102
1100
  options: { ...options, context, metadata },
1103
1101
  token: resolved.token,
1104
- wsFactory: browserWsFactory,
1105
- })
1106
- await client.start()
1107
- return client
1108
- }
1109
- this.config = config
1102
+ wsFactory: browserWsFactory
1103
+ });
1104
+ await client.start();
1105
+ return client;
1106
+ };
1107
+ this.config = config;
1110
1108
  }
1111
- }
1109
+ };
1112
1110
  function configureVoiceClient(config) {
1113
- return new BrowserVoiceFactory(normalizeConfig(config))
1111
+ return new BrowserVoiceFactory(normalizeConfig(config));
1114
1112
  }
1115
1113
  // Annotate the CommonJS export names for ESM import in node:
1116
- 0 &&
1117
- (module.exports = {
1118
- buildWsUrl,
1119
- configureVoiceClient,
1120
- createAudioCapture,
1121
- createAudioPlayback,
1122
- createProtocolState,
1123
- createReconnectingWebSocket,
1124
- handleServerMessage,
1125
- })
1126
- //# sourceMappingURL=browser.js.map
1114
+ 0 && (module.exports = {
1115
+ buildWsUrl,
1116
+ configureVoiceClient,
1117
+ createAudioCapture,
1118
+ createAudioPlayback,
1119
+ createProtocolState,
1120
+ createReconnectingWebSocket,
1121
+ handleServerMessage
1122
+ });
1123
+ //# sourceMappingURL=browser.js.map