@craftedxp/voice-js 0.5.4 → 0.6.0

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,
@@ -31,63 +29,57 @@ __export(browser_exports, {
31
29
  handleServerMessage: () => handleServerMessage,
32
30
  joinRoom: () => joinRoom,
33
31
  parseIncomingCall: () => parseIncomingCall,
34
- })
35
- module.exports = __toCommonJS(browser_exports)
32
+ startTextSession: () => startTextSession
33
+ });
34
+ module.exports = __toCommonJS(browser_exports);
36
35
 
37
36
  // src/config.ts
38
37
  function normalizeConfig(config) {
39
- if (!config) throw new Error('configureVoiceClient: config is required')
40
- if ('apiKey' in config) {
38
+ if (!config) throw new Error("configureVoiceClient: config is required");
39
+ if ("apiKey" in config) {
41
40
  throw new Error(
42
- '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.',
43
- )
41
+ "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."
42
+ );
44
43
  }
45
44
  if (!config.apiBase) {
46
- throw new Error('configureVoiceClient: apiBase is required')
45
+ throw new Error("configureVoiceClient: apiBase is required");
47
46
  }
48
- if (typeof config.fetchToken !== 'function') {
49
- throw new Error('configureVoiceClient: fetchToken must be a function')
47
+ if (typeof config.fetchToken !== "function") {
48
+ throw new Error("configureVoiceClient: fetchToken must be a function");
50
49
  }
51
50
  return {
52
51
  ...config,
53
- apiBase: config.apiBase.replace(/\/+$/, ''),
54
- }
52
+ apiBase: config.apiBase.replace(/\/+$/, "")
53
+ };
55
54
  }
56
55
  function mergeStartCallContext(factory, call) {
57
- const context =
58
- factory.defaultContext || call.context
59
- ? { ...(factory.defaultContext ?? {}), ...(call.context ?? {}) }
60
- : void 0
61
- const metadata =
62
- factory.defaultMetadata || call.metadata
63
- ? { ...(factory.defaultMetadata ?? {}), ...(call.metadata ?? {}) }
64
- : void 0
65
- return { context, metadata }
56
+ const context = factory.defaultContext || call.context ? { ...factory.defaultContext ?? {}, ...call.context ?? {} } : void 0;
57
+ const metadata = factory.defaultMetadata || call.metadata ? { ...factory.defaultMetadata ?? {}, ...call.metadata ?? {} } : void 0;
58
+ return { context, metadata };
66
59
  }
67
60
 
68
61
  // src/worklets/mic-downsampler.worklet.js
69
- var mic_downsampler_worklet_default =
70
- "// 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"
62
+ 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";
71
63
 
72
64
  // src/AudioCapture.ts
73
- var VOLUME_INTERVAL_MS = 100
65
+ var VOLUME_INTERVAL_MS = 100;
74
66
  var createAudioCapture = (options) => {
75
- let audioContext = null
76
- let mediaStream = null
77
- let sourceNode = null
78
- let workletNode = null
79
- let analyser = null
80
- let volumeTimer = null
81
- let muted = false
82
- let capturing = false
67
+ let audioContext = null;
68
+ let mediaStream = null;
69
+ let sourceNode = null;
70
+ let workletNode = null;
71
+ let analyser = null;
72
+ let volumeTimer = null;
73
+ let muted = false;
74
+ let capturing = false;
83
75
  const computeRms = (buf) => {
84
- let sum = 0
85
- for (let i = 0; i < buf.length; i++) sum += buf[i] * buf[i]
86
- const rms = Math.sqrt(sum / buf.length)
87
- return Math.min(1, rms * 1.8)
88
- }
76
+ let sum = 0;
77
+ for (let i = 0; i < buf.length; i++) sum += buf[i] * buf[i];
78
+ const rms = Math.sqrt(sum / buf.length);
79
+ return Math.min(1, rms * 1.8);
80
+ };
89
81
  const start = async () => {
90
- if (capturing) return
82
+ if (capturing) return;
91
83
  try {
92
84
  mediaStream = await navigator.mediaDevices.getUserMedia({
93
85
  audio: {
@@ -98,748 +90,751 @@ var createAudioCapture = (options) => {
98
90
  echoCancellation: true,
99
91
  noiseSuppression: true,
100
92
  autoGainControl: true,
101
- channelCount: 1,
102
- },
103
- })
104
- audioContext = new AudioContext()
105
- if (audioContext.state === 'suspended') await audioContext.resume()
106
- const blob = new Blob([mic_downsampler_worklet_default], { type: 'application/javascript' })
107
- const url = URL.createObjectURL(blob)
93
+ channelCount: 1
94
+ }
95
+ });
96
+ audioContext = new AudioContext();
97
+ if (audioContext.state === "suspended") await audioContext.resume();
98
+ const blob = new Blob([mic_downsampler_worklet_default], { type: "application/javascript" });
99
+ const url = URL.createObjectURL(blob);
108
100
  try {
109
- await audioContext.audioWorklet.addModule(url)
101
+ await audioContext.audioWorklet.addModule(url);
110
102
  } finally {
111
- URL.revokeObjectURL(url)
103
+ URL.revokeObjectURL(url);
112
104
  }
113
- sourceNode = audioContext.createMediaStreamSource(mediaStream)
114
- workletNode = new AudioWorkletNode(audioContext, 'mic-downsampler')
105
+ sourceNode = audioContext.createMediaStreamSource(mediaStream);
106
+ workletNode = new AudioWorkletNode(audioContext, "mic-downsampler");
115
107
  workletNode.port.onmessage = (event) => {
116
- if (muted) return
117
- options.onChunk(event.data)
118
- }
108
+ if (muted) return;
109
+ options.onChunk(event.data);
110
+ };
119
111
  if (options.onVolume) {
120
- analyser = audioContext.createAnalyser()
121
- analyser.fftSize = 256
122
- sourceNode.connect(analyser)
123
- const buf = new Float32Array(analyser.fftSize)
112
+ analyser = audioContext.createAnalyser();
113
+ analyser.fftSize = 256;
114
+ sourceNode.connect(analyser);
115
+ const buf = new Float32Array(analyser.fftSize);
124
116
  volumeTimer = setInterval(() => {
125
- if (!analyser) return
126
- analyser.getFloatTimeDomainData(buf)
127
- options.onVolume?.(computeRms(buf))
128
- }, VOLUME_INTERVAL_MS)
117
+ if (!analyser) return;
118
+ analyser.getFloatTimeDomainData(buf);
119
+ options.onVolume?.(computeRms(buf));
120
+ }, VOLUME_INTERVAL_MS);
129
121
  }
130
- sourceNode.connect(workletNode)
131
- const sink = audioContext.createGain()
132
- sink.gain.value = 0
133
- workletNode.connect(sink).connect(audioContext.destination)
134
- capturing = true
122
+ sourceNode.connect(workletNode);
123
+ const sink = audioContext.createGain();
124
+ sink.gain.value = 0;
125
+ workletNode.connect(sink).connect(audioContext.destination);
126
+ capturing = true;
135
127
  } catch (err) {
136
- const wrapped =
137
- err instanceof Error ? err : new Error(typeof err === 'string' ? err : 'capture failed')
138
- options.onError?.(wrapped)
139
- throw wrapped
128
+ const wrapped = err instanceof Error ? err : new Error(typeof err === "string" ? err : "capture failed");
129
+ options.onError?.(wrapped);
130
+ throw wrapped;
140
131
  }
141
- }
132
+ };
142
133
  const stop = () => {
143
- if (!capturing) return
144
- capturing = false
134
+ if (!capturing) return;
135
+ capturing = false;
145
136
  if (volumeTimer) {
146
- clearInterval(volumeTimer)
147
- volumeTimer = null
137
+ clearInterval(volumeTimer);
138
+ volumeTimer = null;
148
139
  }
149
140
  try {
150
- workletNode?.disconnect()
151
- analyser?.disconnect()
152
- sourceNode?.disconnect()
153
- } catch {}
154
- workletNode = null
155
- analyser = null
156
- sourceNode = null
141
+ workletNode?.disconnect();
142
+ analyser?.disconnect();
143
+ sourceNode?.disconnect();
144
+ } catch {
145
+ }
146
+ workletNode = null;
147
+ analyser = null;
148
+ sourceNode = null;
157
149
  if (mediaStream) {
158
- for (const track of mediaStream.getTracks()) track.stop()
159
- mediaStream = null
150
+ for (const track of mediaStream.getTracks()) track.stop();
151
+ mediaStream = null;
160
152
  }
161
- if (audioContext && audioContext.state !== 'closed') {
162
- void audioContext.close().catch(() => void 0)
153
+ if (audioContext && audioContext.state !== "closed") {
154
+ void audioContext.close().catch(() => void 0);
163
155
  }
164
- audioContext = null
165
- }
156
+ audioContext = null;
157
+ };
166
158
  return {
167
159
  start,
168
160
  stop,
169
161
  mute: (v) => {
170
- muted = v
162
+ muted = v;
171
163
  },
172
- isCapturing: () => capturing,
173
- }
174
- }
164
+ isCapturing: () => capturing
165
+ };
166
+ };
175
167
 
176
168
  // src/AudioPlayback.ts
177
- var DEFAULT_SAMPLE_RATE = 16e3
178
- var VOLUME_INTERVAL_MS2 = 100
169
+ var DEFAULT_SAMPLE_RATE = 16e3;
170
+ var VOLUME_INTERVAL_MS2 = 100;
179
171
  var createAudioPlayback = (options = {}) => {
180
- const sampleRate = options.sampleRate ?? DEFAULT_SAMPLE_RATE
181
- let audioContext = null
182
- let gainNode = null
183
- let analyser = null
184
- let volumeTimer = null
185
- let nextStartTime = 0
186
- let scheduledNodes = []
187
- let speaking = false
172
+ const sampleRate = options.sampleRate ?? DEFAULT_SAMPLE_RATE;
173
+ let audioContext = null;
174
+ let gainNode = null;
175
+ let analyser = null;
176
+ let volumeTimer = null;
177
+ let nextStartTime = 0;
178
+ let scheduledNodes = [];
179
+ let speaking = false;
188
180
  const ensureContext = async () => {
189
181
  if (audioContext) {
190
- if (audioContext.state === 'suspended') await audioContext.resume()
191
- return
182
+ if (audioContext.state === "suspended") await audioContext.resume();
183
+ return;
192
184
  }
193
- audioContext = new AudioContext({ sampleRate })
194
- gainNode = audioContext.createGain()
185
+ audioContext = new AudioContext({ sampleRate });
186
+ gainNode = audioContext.createGain();
195
187
  if (options.onVolume) {
196
- analyser = audioContext.createAnalyser()
197
- analyser.fftSize = 256
198
- gainNode.connect(analyser)
199
- const buf = new Float32Array(analyser.fftSize)
188
+ analyser = audioContext.createAnalyser();
189
+ analyser.fftSize = 256;
190
+ gainNode.connect(analyser);
191
+ const buf = new Float32Array(analyser.fftSize);
200
192
  volumeTimer = setInterval(() => {
201
- if (!analyser) return
202
- analyser.getFloatTimeDomainData(buf)
203
- let sum = 0
204
- for (let i = 0; i < buf.length; i++) sum += buf[i] * buf[i]
205
- const rms = Math.sqrt(sum / buf.length)
206
- options.onVolume?.(Math.min(1, rms * 1.8))
207
- }, VOLUME_INTERVAL_MS2)
193
+ if (!analyser) return;
194
+ analyser.getFloatTimeDomainData(buf);
195
+ let sum = 0;
196
+ for (let i = 0; i < buf.length; i++) sum += buf[i] * buf[i];
197
+ const rms = Math.sqrt(sum / buf.length);
198
+ options.onVolume?.(Math.min(1, rms * 1.8));
199
+ }, VOLUME_INTERVAL_MS2);
208
200
  }
209
- gainNode.connect(audioContext.destination)
210
- nextStartTime = audioContext.currentTime
211
- }
201
+ gainNode.connect(audioContext.destination);
202
+ nextStartTime = audioContext.currentTime;
203
+ };
212
204
  const setSpeaking = (v) => {
213
- if (v === speaking) return
214
- speaking = v
215
- options.onSpeakingChange?.(v)
216
- }
205
+ if (v === speaking) return;
206
+ speaking = v;
207
+ options.onSpeakingChange?.(v);
208
+ };
217
209
  const pruneFinished = () => {
218
- const now = audioContext?.currentTime ?? 0
210
+ const now = audioContext?.currentTime ?? 0;
219
211
  scheduledNodes = scheduledNodes.filter((n) => {
220
- const node = n
221
- return (node._endsAt ?? 0) > now
222
- })
223
- if (scheduledNodes.length === 0) setSpeaking(false)
224
- }
212
+ const node = n;
213
+ return (node._endsAt ?? 0) > now;
214
+ });
215
+ if (scheduledNodes.length === 0) setSpeaking(false);
216
+ };
225
217
  const enqueue = (pcm) => {
226
218
  if (!audioContext) {
227
- void ensureContext().then(() => enqueue(pcm))
228
- return
219
+ void ensureContext().then(() => enqueue(pcm));
220
+ return;
229
221
  }
230
- if (!audioContext || !gainNode) return
231
- const int16 = new Int16Array(pcm)
232
- if (int16.length === 0) return
233
- const audioBuffer = audioContext.createBuffer(1, int16.length, sampleRate)
234
- const float32 = audioBuffer.getChannelData(0)
222
+ if (!audioContext || !gainNode) return;
223
+ const int16 = new Int16Array(pcm);
224
+ if (int16.length === 0) return;
225
+ const audioBuffer = audioContext.createBuffer(1, int16.length, sampleRate);
226
+ const float32 = audioBuffer.getChannelData(0);
235
227
  for (let i = 0; i < int16.length; i++) {
236
- float32[i] = int16[i] / 32768
228
+ float32[i] = int16[i] / 32768;
237
229
  }
238
- const node = audioContext.createBufferSource()
239
- node.buffer = audioBuffer
240
- node.connect(gainNode)
241
- const now = audioContext.currentTime
242
- const startAt = Math.max(now, nextStartTime)
243
- node.start(startAt)
244
- const duration = int16.length / sampleRate
245
- node._endsAt = startAt + duration
246
- nextStartTime = startAt + duration
247
- scheduledNodes.push(node)
248
- setSpeaking(true)
249
- node.onended = () => pruneFinished()
250
- }
230
+ const node = audioContext.createBufferSource();
231
+ node.buffer = audioBuffer;
232
+ node.connect(gainNode);
233
+ const now = audioContext.currentTime;
234
+ const startAt = Math.max(now, nextStartTime);
235
+ node.start(startAt);
236
+ const duration = int16.length / sampleRate;
237
+ node._endsAt = startAt + duration;
238
+ nextStartTime = startAt + duration;
239
+ scheduledNodes.push(node);
240
+ setSpeaking(true);
241
+ node.onended = () => pruneFinished();
242
+ };
251
243
  const flush = () => {
252
- if (!audioContext || !gainNode) return
244
+ if (!audioContext || !gainNode) return;
253
245
  for (const node of scheduledNodes) {
254
246
  try {
255
- node.stop()
256
- } catch {}
247
+ node.stop();
248
+ } catch {
249
+ }
257
250
  }
258
- scheduledNodes = []
259
- gainNode.disconnect()
260
- gainNode = audioContext.createGain()
251
+ scheduledNodes = [];
252
+ gainNode.disconnect();
253
+ gainNode = audioContext.createGain();
261
254
  if (analyser) {
262
- analyser.disconnect()
263
- gainNode.connect(analyser)
255
+ analyser.disconnect();
256
+ gainNode.connect(analyser);
264
257
  }
265
- gainNode.connect(audioContext.destination)
266
- nextStartTime = audioContext.currentTime
267
- setSpeaking(false)
268
- }
258
+ gainNode.connect(audioContext.destination);
259
+ nextStartTime = audioContext.currentTime;
260
+ setSpeaking(false);
261
+ };
269
262
  const close = () => {
270
- flush()
263
+ flush();
271
264
  if (volumeTimer) {
272
- clearInterval(volumeTimer)
273
- volumeTimer = null
265
+ clearInterval(volumeTimer);
266
+ volumeTimer = null;
274
267
  }
275
- if (audioContext && audioContext.state !== 'closed') {
276
- void audioContext.close().catch(() => void 0)
268
+ if (audioContext && audioContext.state !== "closed") {
269
+ void audioContext.close().catch(() => void 0);
277
270
  }
278
- audioContext = null
279
- gainNode = null
280
- analyser = null
281
- }
271
+ audioContext = null;
272
+ gainNode = null;
273
+ analyser = null;
274
+ };
282
275
  const resume = async () => {
283
- await ensureContext()
284
- }
285
- return { enqueue, flush, close, resume }
286
- }
276
+ await ensureContext();
277
+ };
278
+ return { enqueue, flush, close, resume };
279
+ };
287
280
 
288
281
  // src/ReconnectingWebSocket.ts
289
- var READYSTATE_OPEN = 1
290
- var READYSTATE_CLOSED = 3
282
+ var READYSTATE_OPEN = 1;
283
+ var READYSTATE_CLOSED = 3;
291
284
  var createReconnectingWebSocket = (options, onEvent) => {
292
- const maxRetries = options.maxRetries ?? 3
293
- const initialBackoff = options.initialBackoffMs ?? 500
294
- const maxBackoff = options.maxBackoffMs ?? 8e3
295
- let ws = null
296
- let intentionalClose = false
297
- let retries = 0
298
- let backoff = initialBackoff
299
- let reconnectTimer = null
285
+ const maxRetries = options.maxRetries ?? 3;
286
+ const initialBackoff = options.initialBackoffMs ?? 500;
287
+ const maxBackoff = options.maxBackoffMs ?? 8e3;
288
+ let ws = null;
289
+ let intentionalClose = false;
290
+ let retries = 0;
291
+ let backoff = initialBackoff;
292
+ let reconnectTimer = null;
300
293
  const openOnce = () => {
301
- ws = options.wsFactory(options.url)
302
- ws.binaryType = 'arraybuffer'
294
+ ws = options.wsFactory(options.url);
295
+ ws.binaryType = "arraybuffer";
303
296
  ws.onopen = () => {
304
- if (retries === 0) onEvent({ type: 'open' })
305
- else onEvent({ type: 'reconnected' })
306
- retries = 0
307
- backoff = initialBackoff
308
- }
297
+ if (retries === 0) onEvent({ type: "open" });
298
+ else onEvent({ type: "reconnected" });
299
+ retries = 0;
300
+ backoff = initialBackoff;
301
+ };
309
302
  ws.onmessage = (ev) => {
310
- onEvent({ type: 'message', data: ev.data })
311
- }
303
+ onEvent({ type: "message", data: ev.data });
304
+ };
312
305
  ws.onerror = () => {
313
- onEvent({ type: 'error', error: new Error('WebSocket error') })
314
- }
306
+ onEvent({ type: "error", error: new Error("WebSocket error") });
307
+ };
315
308
  ws.onclose = (ev) => {
316
- ws = null
317
- const shouldRetry = !intentionalClose && retries < maxRetries
309
+ ws = null;
310
+ const shouldRetry = !intentionalClose && retries < maxRetries;
318
311
  if (!shouldRetry) {
319
312
  onEvent({
320
- type: 'close',
313
+ type: "close",
321
314
  code: ev.code,
322
315
  reason: ev.reason,
323
- permanent: true,
324
- })
325
- return
316
+ permanent: true
317
+ });
318
+ return;
326
319
  }
327
320
  onEvent({
328
- type: 'close',
321
+ type: "close",
329
322
  code: ev.code,
330
323
  reason: ev.reason,
331
- permanent: false,
332
- })
333
- retries++
334
- const delay = Math.min(backoff, maxBackoff)
335
- backoff = Math.min(backoff * 2, maxBackoff)
336
- reconnectTimer = setTimeout(openOnce, delay)
337
- }
338
- }
339
- openOnce()
324
+ permanent: false
325
+ });
326
+ retries++;
327
+ const delay = Math.min(backoff, maxBackoff);
328
+ backoff = Math.min(backoff * 2, maxBackoff);
329
+ reconnectTimer = setTimeout(openOnce, delay);
330
+ };
331
+ };
332
+ openOnce();
340
333
  return {
341
334
  send: (data) => {
342
- if (ws && ws.readyState === READYSTATE_OPEN) ws.send(data)
335
+ if (ws && ws.readyState === READYSTATE_OPEN) ws.send(data);
343
336
  },
344
- close: (code = 1e3, reason = 'client-requested') => {
345
- intentionalClose = true
337
+ close: (code = 1e3, reason = "client-requested") => {
338
+ intentionalClose = true;
346
339
  if (reconnectTimer) {
347
- clearTimeout(reconnectTimer)
348
- reconnectTimer = null
340
+ clearTimeout(reconnectTimer);
341
+ reconnectTimer = null;
349
342
  }
350
343
  try {
351
- ws?.close(code, reason)
352
- } catch {}
344
+ ws?.close(code, reason);
345
+ } catch {
346
+ }
353
347
  },
354
- readyState: () => ws?.readyState ?? READYSTATE_CLOSED,
355
- }
356
- }
348
+ readyState: () => ws?.readyState ?? READYSTATE_CLOSED
349
+ };
350
+ };
357
351
 
358
352
  // src/protocol.ts
359
353
  var createProtocolState = () => ({
360
- state: 'idle',
354
+ state: "idle",
361
355
  transcript: [],
362
356
  agentBubbleId: null,
363
357
  idCounter: 0,
364
- endReason: null,
365
- })
358
+ endReason: null
359
+ });
366
360
  var mapEndReason = (raw) => {
367
- if (raw === 'agent_ended') return 'agent_ended'
368
- if (raw === 'caller_hung_up') return 'user_hangup'
369
- if (raw === 'silence_timeout' || raw === 'max_duration') return 'timeout'
370
- return 'error'
371
- }
361
+ if (raw === "agent_ended") return "agent_ended";
362
+ if (raw === "caller_hung_up") return "user_hangup";
363
+ if (raw === "silence_timeout" || raw === "max_duration") return "timeout";
364
+ return "error";
365
+ };
372
366
  function handleServerMessage(raw, state, cb) {
373
- let msg
367
+ let msg;
374
368
  try {
375
- msg = JSON.parse(raw)
369
+ msg = JSON.parse(raw);
376
370
  } catch {
377
- return
371
+ return;
378
372
  }
379
373
  switch (msg.type) {
380
- case 'connected':
381
- cb.onConnected()
382
- setState(state, 'listening', cb)
383
- return
384
- case 'transcript': {
385
- const text = msg.text ?? ''
386
- if (!text) return
387
- const isFinal = !!msg.isFinal
388
- if (!isFinal) setState(state, 'user_speaking', cb)
389
- upsertUserPartial(state, text, isFinal)
390
- cb.onTranscript(state.transcript)
391
- return
374
+ case "connected":
375
+ cb.onConnected();
376
+ setState(state, "listening", cb);
377
+ return;
378
+ case "transcript": {
379
+ const text = msg.text ?? "";
380
+ if (!text) return;
381
+ const isFinal = !!msg.isFinal;
382
+ if (!isFinal) setState(state, "user_speaking", cb);
383
+ upsertUserPartial(state, text, isFinal);
384
+ cb.onTranscript(state.transcript);
385
+ return;
392
386
  }
393
- case 'agent_turn_start': {
394
- const id = `m${state.idCounter++}`
395
- state.agentBubbleId = id
396
- state.transcript = [...state.transcript, { id, role: 'agent', text: '' }]
397
- cb.onTranscript(state.transcript)
398
- const seq = typeof msg.seq === 'number' ? msg.seq : void 0
399
- cb.onAgentTurnStart(seq)
400
- setState(state, 'agent_speaking', cb)
401
- return
387
+ case "agent_turn_start": {
388
+ const id = `m${state.idCounter++}`;
389
+ state.agentBubbleId = id;
390
+ state.transcript = [...state.transcript, { id, role: "agent", text: "" }];
391
+ cb.onTranscript(state.transcript);
392
+ const seq = typeof msg.seq === "number" ? msg.seq : void 0;
393
+ cb.onAgentTurnStart(seq);
394
+ setState(state, "agent_speaking", cb);
395
+ return;
402
396
  }
403
- case 'agent_text': {
404
- const delta = msg.text ?? ''
405
- if (!delta || !state.agentBubbleId) return
406
- const id = state.agentBubbleId
407
- state.transcript = state.transcript.map((e) =>
408
- e.id === id && e.role === 'agent' ? { ...e, text: e.text + delta } : e,
409
- )
410
- cb.onTranscript(state.transcript)
411
- return
397
+ case "agent_text": {
398
+ const delta = msg.text ?? "";
399
+ if (!delta || !state.agentBubbleId) return;
400
+ const id = state.agentBubbleId;
401
+ state.transcript = state.transcript.map(
402
+ (e) => e.id === id && e.role === "agent" ? { ...e, text: e.text + delta } : e
403
+ );
404
+ cb.onTranscript(state.transcript);
405
+ return;
412
406
  }
413
- case 'agent_turn_end': {
414
- state.agentBubbleId = null
415
- const seq = typeof msg.seq === 'number' ? msg.seq : void 0
416
- cb.onAgentTurnEnd(seq)
417
- setState(state, 'listening', cb)
418
- return
407
+ case "agent_turn_end": {
408
+ state.agentBubbleId = null;
409
+ const seq = typeof msg.seq === "number" ? msg.seq : void 0;
410
+ cb.onAgentTurnEnd(seq);
411
+ setState(state, "listening", cb);
412
+ return;
419
413
  }
420
- case 'interrupt':
421
- cb.onInterrupt()
422
- return
423
- case 'agent_turn_abort': {
424
- const committed = (msg.committedText ?? '').trim()
414
+ case "interrupt":
415
+ cb.onInterrupt();
416
+ return;
417
+ case "agent_turn_abort": {
418
+ const committed = (msg.committedText ?? "").trim();
425
419
  if (state.agentBubbleId) {
426
- const id = state.agentBubbleId
420
+ const id = state.agentBubbleId;
427
421
  if (committed) {
428
- state.transcript = state.transcript.map((e) =>
429
- e.id === id && e.role === 'agent' ? { ...e, text: committed, interrupted: true } : e,
430
- )
422
+ state.transcript = state.transcript.map(
423
+ (e) => e.id === id && e.role === "agent" ? { ...e, text: committed, interrupted: true } : e
424
+ );
431
425
  } else {
432
- state.transcript = state.transcript.filter((e) => e.id !== id)
426
+ state.transcript = state.transcript.filter((e) => e.id !== id);
433
427
  }
434
- cb.onTranscript(state.transcript)
428
+ cb.onTranscript(state.transcript);
435
429
  }
436
- state.agentBubbleId = null
437
- return
430
+ state.agentBubbleId = null;
431
+ return;
438
432
  }
439
- case 'tool_call':
433
+ case "tool_call":
440
434
  state.transcript = [
441
435
  ...state.transcript,
442
436
  {
443
437
  id: `m${state.idCounter++}`,
444
- role: 'tool',
445
- text: `\u2192 ${String(msg.tool ?? '?')}(${msg.args ? JSON.stringify(msg.args) : ''})`,
446
- },
447
- ]
448
- cb.onTranscript(state.transcript)
449
- return
450
- case 'tool_result':
438
+ role: "tool",
439
+ text: `\u2192 ${String(msg.tool ?? "?")}(${msg.args ? JSON.stringify(msg.args) : ""})`
440
+ }
441
+ ];
442
+ cb.onTranscript(state.transcript);
443
+ return;
444
+ case "tool_result":
451
445
  state.transcript = [
452
446
  ...state.transcript,
453
447
  {
454
448
  id: `m${state.idCounter++}`,
455
- role: 'tool',
456
- text: `${msg.ok ? '\u2713' : '\u2717'} ${String(msg.tool ?? '?')}`,
457
- },
458
- ]
459
- cb.onTranscript(state.transcript)
460
- return
461
- case 'client_tool_call': {
462
- const toolCallId = String(msg.toolCallId ?? '')
463
- const name = String(msg.name ?? '')
464
- const args = msg.args ?? {}
465
- if (!toolCallId || !name) return
466
- cb.onClientToolCall({ toolCallId, name, args })
467
- return
449
+ role: "tool",
450
+ text: `${msg.ok ? "\u2713" : "\u2717"} ${String(msg.tool ?? "?")}`
451
+ }
452
+ ];
453
+ cb.onTranscript(state.transcript);
454
+ return;
455
+ case "client_tool_call": {
456
+ const toolCallId = String(msg.toolCallId ?? "");
457
+ const name = String(msg.name ?? "");
458
+ const args = msg.args ?? {};
459
+ if (!toolCallId || !name) return;
460
+ cb.onClientToolCall({ toolCallId, name, args });
461
+ return;
468
462
  }
469
- case 'call_end': {
470
- const reasonRaw = String(msg.reason ?? '')
471
- const reason = mapEndReason(reasonRaw)
472
- state.endReason = reason
463
+ case "call_end": {
464
+ const reasonRaw = String(msg.reason ?? "");
465
+ const reason = mapEndReason(reasonRaw);
466
+ state.endReason = reason;
473
467
  state.transcript = [
474
468
  ...state.transcript,
475
469
  {
476
470
  id: `m${state.idCounter++}`,
477
- role: 'system',
478
- text: `call ended${reasonRaw ? ` (${reasonRaw})` : ''}`,
479
- },
480
- ]
481
- cb.onTranscript(state.transcript)
482
- cb.onCallEnd(reason)
483
- return
471
+ role: "system",
472
+ text: `call ended${reasonRaw ? ` (${reasonRaw})` : ""}`
473
+ }
474
+ ];
475
+ cb.onTranscript(state.transcript);
476
+ cb.onCallEnd(reason);
477
+ return;
484
478
  }
485
- case 'error': {
486
- const code = msg.code ?? 'server_error'
487
- const message = msg.message ?? 'server error'
488
- cb.onError({ code, message })
489
- return
479
+ case "error": {
480
+ const code = msg.code ?? "server_error";
481
+ const message = msg.message ?? "server error";
482
+ cb.onError({ code, message });
483
+ return;
490
484
  }
491
485
  }
492
486
  }
493
487
  var setState = (state, next, cb) => {
494
- if (state.state === next) return
495
- cb.onState(next)
496
- }
488
+ if (state.state === next) return;
489
+ cb.onState(next);
490
+ };
497
491
  var upsertUserPartial = (state, text, isFinal) => {
498
- let idx = -1
492
+ let idx = -1;
499
493
  for (let i = state.transcript.length - 1; i >= 0; i--) {
500
- const e = state.transcript[i]
501
- if (e.role === 'user' && e.committed === false) {
502
- idx = i
503
- break
494
+ const e = state.transcript[i];
495
+ if (e.role === "user" && e.committed === false) {
496
+ idx = i;
497
+ break;
504
498
  }
505
499
  }
506
500
  if (idx === -1) {
507
501
  state.transcript = [
508
502
  ...state.transcript,
509
- { id: `m${state.idCounter++}`, role: 'user', text, committed: isFinal },
510
- ]
511
- return
512
- }
513
- const target = state.transcript[idx]
514
- const next = [...state.transcript]
515
- next[idx] = { ...target, text, committed: isFinal }
516
- state.transcript = next
517
- }
503
+ { id: `m${state.idCounter++}`, role: "user", text, committed: isFinal }
504
+ ];
505
+ return;
506
+ }
507
+ const target = state.transcript[idx];
508
+ const next = [...state.transcript];
509
+ next[idx] = { ...target, text, committed: isFinal };
510
+ state.transcript = next;
511
+ };
518
512
  function buildWsUrl(args) {
519
- const base = new URL(args.apiBase)
520
- const proto = base.protocol === 'https:' ? 'wss:' : 'ws:'
521
- const bargeQS = args.bargeIn === false ? '&barge=off' : ''
522
- return `${proto}//${base.host}/v1/agents/${encodeURIComponent(args.agentId)}/call?token=${encodeURIComponent(args.token)}${bargeQS}`
513
+ const base = new URL(args.apiBase);
514
+ const proto = base.protocol === "https:" ? "wss:" : "ws:";
515
+ const bargeQS = args.bargeIn === false ? "&barge=off" : "";
516
+ return `${proto}//${base.host}/v1/agents/${encodeURIComponent(args.agentId)}/call?token=${encodeURIComponent(args.token)}${bargeQS}`;
523
517
  }
524
518
 
525
519
  // src/clientTools.ts
526
- var NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/
527
- var MAX_TOOLS = 64
528
- var MAX_USAGE = 500
529
- var MAX_TIMEOUT_MS = 3e4
520
+ var NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
521
+ var MAX_TOOLS = 64;
522
+ var MAX_USAGE = 500;
523
+ var MAX_TIMEOUT_MS = 3e4;
530
524
  var validateClientToolMap = (tools) => {
531
- if (tools === void 0) return
532
- if (typeof tools !== 'object' || tools === null || Array.isArray(tools)) {
533
- throw new Error('clientTools must be an object keyed by tool name')
525
+ if (tools === void 0) return;
526
+ if (typeof tools !== "object" || tools === null || Array.isArray(tools)) {
527
+ throw new Error("clientTools must be an object keyed by tool name");
534
528
  }
535
- const entries = Object.entries(tools)
529
+ const entries = Object.entries(tools);
536
530
  if (entries.length > MAX_TOOLS) {
537
- throw new Error(`clientTools may declare at most 64 tools (got ${entries.length})`)
531
+ throw new Error(`clientTools may declare at most 64 tools (got ${entries.length})`);
538
532
  }
539
533
  for (const [name, def] of entries) {
540
534
  if (!NAME_RE.test(name)) {
541
535
  throw new Error(
542
- `clientTools["${name}"]: name must be a valid identifier (^[a-zA-Z_][a-zA-Z0-9_]*$)`,
543
- )
536
+ `clientTools["${name}"]: name must be a valid identifier (^[a-zA-Z_][a-zA-Z0-9_]*$)`
537
+ );
544
538
  }
545
- if (!def || typeof def !== 'object') {
546
- throw new Error(`clientTools["${name}"]: must be an object`)
539
+ if (!def || typeof def !== "object") {
540
+ throw new Error(`clientTools["${name}"]: must be an object`);
547
541
  }
548
- if (typeof def.description !== 'string' || def.description.length === 0) {
549
- throw new Error(`clientTools["${name}"]: must have a description`)
542
+ if (typeof def.description !== "string" || def.description.length === 0) {
543
+ throw new Error(`clientTools["${name}"]: must have a description`);
550
544
  }
551
- if (typeof def.handler !== 'function') {
552
- throw new Error(`clientTools["${name}"]: must have a handler function`)
545
+ if (typeof def.handler !== "function") {
546
+ throw new Error(`clientTools["${name}"]: must have a handler function`);
553
547
  }
554
548
  if (def.usage !== void 0 && def.usage.length > MAX_USAGE) {
555
- throw new Error(`clientTools["${name}"]: usage must be \u2264500 chars`)
549
+ throw new Error(`clientTools["${name}"]: usage must be \u2264500 chars`);
556
550
  }
557
- if (
558
- def.timeoutMs !== void 0 &&
559
- (!Number.isFinite(def.timeoutMs) || def.timeoutMs <= 0 || def.timeoutMs > MAX_TIMEOUT_MS)
560
- ) {
561
- throw new Error(`clientTools["${name}"]: timeoutMs must be in (0, 30000]`)
551
+ if (def.timeoutMs !== void 0 && (!Number.isFinite(def.timeoutMs) || def.timeoutMs <= 0 || def.timeoutMs > MAX_TIMEOUT_MS)) {
552
+ throw new Error(`clientTools["${name}"]: timeoutMs must be in (0, 30000]`);
562
553
  }
563
554
  }
564
- }
555
+ };
565
556
  var buildRegisterFrame = (tools) => ({
566
- type: 'client_tools_register',
557
+ type: "client_tools_register",
567
558
  tools: Object.entries(tools).map(([name, def]) => ({
568
559
  name,
569
560
  description: def.description,
570
561
  parameters: def.parameters,
571
- ...(def.usage !== void 0 ? { usage: def.usage } : {}),
572
- ...(def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}),
573
- })),
574
- })
562
+ ...def.usage !== void 0 ? { usage: def.usage } : {},
563
+ ...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
564
+ }))
565
+ });
575
566
  var dispatchClientToolCall = (send, tools, frame) => {
576
567
  const safeSend = (payload) => {
577
568
  try {
578
- send(payload)
579
- } catch {}
580
- }
581
- const tool = tools[frame.name]
569
+ send(payload);
570
+ } catch {
571
+ }
572
+ };
573
+ const tool = tools[frame.name];
582
574
  if (!tool) {
583
575
  safeSend({
584
- type: 'client_tool_result',
576
+ type: "client_tool_result",
585
577
  toolCallId: frame.toolCallId,
586
- error: `No handler for ${frame.name}`,
587
- })
588
- return
578
+ error: `No handler for ${frame.name}`
579
+ });
580
+ return;
589
581
  }
590
582
  void (async () => {
591
583
  try {
592
- const out = await tool.handler(frame.args)
584
+ const out = await tool.handler(frame.args);
593
585
  safeSend({
594
- type: 'client_tool_result',
586
+ type: "client_tool_result",
595
587
  toolCallId: frame.toolCallId,
596
- result: typeof out === 'string' ? out : JSON.stringify(out),
597
- })
588
+ result: typeof out === "string" ? out : JSON.stringify(out)
589
+ });
598
590
  } catch (err) {
599
591
  safeSend({
600
- type: 'client_tool_result',
592
+ type: "client_tool_result",
601
593
  toolCallId: frame.toolCallId,
602
- error: err instanceof Error ? err.message : String(err),
603
- })
594
+ error: err instanceof Error ? err.message : String(err)
595
+ });
604
596
  }
605
- })()
606
- }
597
+ })();
598
+ };
607
599
 
608
600
  // src/ClientMarksBuffer.ts
609
601
  var createClientMarksBuffer = (args) => {
610
- const now = args.now ?? (() => performance.now())
611
- let pendingFirstOutboundAt = null
612
- const inFlight = /* @__PURE__ */ new Map()
602
+ const now = args.now ?? (() => performance.now());
603
+ let pendingFirstOutboundAt = null;
604
+ const inFlight = /* @__PURE__ */ new Map();
613
605
  const tryEmit = (seq) => {
614
- const slot = inFlight.get(seq)
615
- if (!slot) return
616
- if (!slot.ended) return
617
- const marks = {}
606
+ const slot = inFlight.get(seq);
607
+ if (!slot) return;
608
+ if (!slot.ended) return;
609
+ const marks = {};
618
610
  if (slot.firstOutboundAt !== null && slot.firstAudibleAt !== null) {
619
- marks.client_mic_to_first_audible_ms = slot.firstAudibleAt - slot.firstOutboundAt
611
+ marks.client_mic_to_first_audible_ms = slot.firstAudibleAt - slot.firstOutboundAt;
620
612
  }
621
613
  args.send({
622
- type: 'client_marks',
614
+ type: "client_marks",
623
615
  seq,
624
616
  marks,
625
- clientNow: Date.now(),
626
- })
627
- inFlight.delete(seq)
628
- }
617
+ clientNow: Date.now()
618
+ });
619
+ inFlight.delete(seq);
620
+ };
629
621
  const markFirstOutboundAudio = () => {
630
- if (pendingFirstOutboundAt !== null) return
631
- pendingFirstOutboundAt = now()
632
- }
622
+ if (pendingFirstOutboundAt !== null) return;
623
+ pendingFirstOutboundAt = now();
624
+ };
633
625
  const markFirstAudibleOutput = () => {
634
- let target
626
+ let target;
635
627
  for (const slot of inFlight.values()) {
636
628
  if (!slot.ended) {
637
- target = slot
629
+ target = slot;
638
630
  }
639
631
  }
640
- if (!target) return
641
- if (target.firstAudibleAt !== null) return
642
- target.firstAudibleAt = now()
643
- }
632
+ if (!target) return;
633
+ if (target.firstAudibleAt !== null) return;
634
+ target.firstAudibleAt = now();
635
+ };
644
636
  const onAgentTurnStart = (seq) => {
645
637
  inFlight.set(seq, {
646
638
  firstOutboundAt: pendingFirstOutboundAt,
647
639
  firstAudibleAt: null,
648
- ended: false,
649
- })
650
- pendingFirstOutboundAt = null
651
- }
640
+ ended: false
641
+ });
642
+ pendingFirstOutboundAt = null;
643
+ };
652
644
  const onAgentTurnEnd = (seq) => {
653
- const slot = inFlight.get(seq)
645
+ const slot = inFlight.get(seq);
654
646
  if (!slot) {
655
- args.send({ type: 'client_marks', seq, marks: {}, clientNow: Date.now() })
656
- return
647
+ args.send({ type: "client_marks", seq, marks: {}, clientNow: Date.now() });
648
+ return;
657
649
  }
658
- slot.ended = true
659
- tryEmit(seq)
660
- }
650
+ slot.ended = true;
651
+ tryEmit(seq);
652
+ };
661
653
  const flush = () => {
662
654
  for (const seq of [...inFlight.keys()]) {
663
- const slot = inFlight.get(seq)
664
- slot.ended = true
665
- tryEmit(seq)
655
+ const slot = inFlight.get(seq);
656
+ slot.ended = true;
657
+ tryEmit(seq);
666
658
  }
667
- pendingFirstOutboundAt = null
668
- }
659
+ pendingFirstOutboundAt = null;
660
+ };
669
661
  return {
670
662
  markFirstOutboundAudio,
671
663
  markFirstAudibleOutput,
672
664
  onAgentTurnStart,
673
665
  onAgentTurnEnd,
674
- flush,
675
- }
676
- }
666
+ flush
667
+ };
668
+ };
677
669
 
678
670
  // src/VoiceClient.ts
679
671
  var BrowserVoiceClient = class {
680
672
  constructor(args) {
681
- this.rws = null
682
- this.capture = null
683
- this.playback = null
684
- this.muted = false
685
- this.inputVolume = 0
686
- this.outputVolume = 0
687
- this.startedAt = null
688
- this.endedFired = false
689
- this.lastError = null
673
+ this.rws = null;
674
+ this.capture = null;
675
+ this.playback = null;
676
+ this.muted = false;
677
+ this.inputVolume = 0;
678
+ this.outputVolume = 0;
679
+ this.startedAt = null;
680
+ this.endedFired = false;
681
+ this.lastError = null;
690
682
  this.end = () => {
691
- this.teardown('user_hangup')
692
- }
683
+ this.teardown("user_hangup");
684
+ };
693
685
  this.mute = () => {
694
- if (this.muted) return
695
- this.muted = true
696
- this.capture?.mute(true)
697
- }
686
+ if (this.muted) return;
687
+ this.muted = true;
688
+ this.capture?.mute(true);
689
+ };
698
690
  this.unmute = () => {
699
- if (!this.muted) return
700
- this.muted = false
701
- this.capture?.mute(false)
702
- }
691
+ if (!this.muted) return;
692
+ this.muted = false;
693
+ this.capture?.mute(false);
694
+ };
703
695
  // ---------------------------------------------------------------
704
696
  // Internal
705
697
  // ---------------------------------------------------------------
706
698
  this.sendClientToolsRegister = () => {
707
- const frame = buildRegisterFrame(this.args.options.clientTools ?? {})
708
- this.rws?.send(JSON.stringify(frame))
709
- }
699
+ const frame = buildRegisterFrame(this.args.options.clientTools ?? {});
700
+ this.rws?.send(JSON.stringify(frame));
701
+ };
710
702
  this.setState = (next) => {
711
- if (this.proto.state === next) return
712
- this.proto.state = next
713
- this.args.options.onStateChange?.(next)
714
- }
703
+ if (this.proto.state === next) return;
704
+ this.proto.state = next;
705
+ this.args.options.onStateChange?.(next);
706
+ };
715
707
  this.emitError = (err) => {
716
- this.lastError = err
717
- this.args.options.onError?.(err)
718
- }
708
+ this.lastError = err;
709
+ this.args.options.onError?.(err);
710
+ };
719
711
  this.handleSocketEvent = (ev) => {
720
712
  switch (ev.type) {
721
- case 'open':
722
- void this.startCapture()
723
- break
724
- case 'reconnected':
725
- this.proto.transcript = []
726
- this.proto.agentBubbleId = null
727
- this.args.options.onTranscript?.(this.proto.transcript)
728
- void this.startCapture()
729
- this.setState('listening')
730
- break
731
- case 'message':
732
- if (typeof ev.data === 'string') {
713
+ case "open":
714
+ void this.startCapture();
715
+ break;
716
+ case "reconnected":
717
+ this.proto.transcript = [];
718
+ this.proto.agentBubbleId = null;
719
+ this.args.options.onTranscript?.(this.proto.transcript);
720
+ void this.startCapture();
721
+ this.setState("listening");
722
+ break;
723
+ case "message":
724
+ if (typeof ev.data === "string") {
733
725
  handleServerMessage(ev.data, this.proto, {
734
726
  onState: this.setState,
735
727
  onTranscript: (entries) => this.args.options.onTranscript?.(entries),
736
728
  onError: this.emitError,
737
729
  onInterrupt: () => {
738
- this.playback?.flush()
739
- this.args.options.onInterrupt?.()
730
+ this.playback?.flush();
731
+ this.args.options.onInterrupt?.();
740
732
  },
741
733
  onAgentTurnStart: (seq) => {
742
- if (typeof seq === 'number') this.marks.onAgentTurnStart(seq)
743
- this.args.options.onAgentTurnStart?.()
734
+ if (typeof seq === "number") this.marks.onAgentTurnStart(seq);
735
+ this.args.options.onAgentTurnStart?.();
744
736
  },
745
737
  onAgentTurnEnd: (seq) => {
746
- if (typeof seq === 'number') this.marks.onAgentTurnEnd(seq)
738
+ if (typeof seq === "number") this.marks.onAgentTurnEnd(seq);
747
739
  },
748
740
  onCallEnd: (reason) => this.teardown(reason),
749
741
  onConnected: () => this.sendClientToolsRegister(),
750
- onClientToolCall: (frame) =>
751
- dispatchClientToolCall(
752
- (f) => this.rws?.send(JSON.stringify(f)),
753
- this.args.options.clientTools ?? {},
754
- frame,
755
- ),
756
- })
742
+ onClientToolCall: (frame) => dispatchClientToolCall(
743
+ (f) => this.rws?.send(JSON.stringify(f)),
744
+ this.args.options.clientTools ?? {},
745
+ frame
746
+ )
747
+ });
757
748
  } else {
758
- this.marks.markFirstAudibleOutput()
759
- this.playback?.enqueue(ev.data)
749
+ this.marks.markFirstAudibleOutput();
750
+ this.playback?.enqueue(ev.data);
760
751
  }
761
- break
762
- case 'close':
752
+ break;
753
+ case "close":
763
754
  if (ev.permanent) {
764
- const reason = this.proto.endReason ?? (this.lastError ? 'error' : 'user_hangup')
765
- this.teardown(reason)
755
+ const reason = this.proto.endReason ?? (this.lastError ? "error" : "user_hangup");
756
+ this.teardown(reason);
766
757
  }
767
- break
768
- case 'error':
769
- this.emitError({ code: 'socket_error', message: ev.error.message })
770
- break
758
+ break;
759
+ case "error":
760
+ this.emitError({ code: "socket_error", message: ev.error.message });
761
+ break;
771
762
  }
772
- }
763
+ };
773
764
  this.startCapture = async () => {
774
- if (this.capture?.isCapturing()) return
765
+ if (this.capture?.isCapturing()) return;
775
766
  this.capture = createAudioCapture({
776
767
  onChunk: (pcm) => {
777
- this.marks.markFirstOutboundAudio()
778
- this.rws?.send(pcm)
768
+ this.marks.markFirstOutboundAudio();
769
+ this.rws?.send(pcm);
779
770
  },
780
771
  onVolume: (v) => {
781
- this.inputVolume = v
782
- this.args.options.onVolume?.({ input: v, output: this.outputVolume })
772
+ this.inputVolume = v;
773
+ this.args.options.onVolume?.({ input: v, output: this.outputVolume });
783
774
  },
784
775
  onError: (err) => {
785
776
  this.emitError({
786
- code: err.name === 'NotAllowedError' ? 'mic_denied' : 'mic_start_failed',
787
- message: err.message,
788
- })
789
- },
790
- })
791
- if (this.muted) this.capture.mute(true)
777
+ code: err.name === "NotAllowedError" ? "mic_denied" : "mic_start_failed",
778
+ message: err.message
779
+ });
780
+ }
781
+ });
782
+ if (this.muted) this.capture.mute(true);
792
783
  try {
793
- await this.capture.start()
794
- } catch {}
795
- }
784
+ await this.capture.start();
785
+ } catch {
786
+ }
787
+ };
796
788
  this.teardown = (reason) => {
797
789
  try {
798
- this.marks.flush()
799
- } catch {}
800
- this.capture?.stop()
801
- this.capture = null
802
- this.playback?.close()
803
- this.playback = null
790
+ this.marks.flush();
791
+ } catch {
792
+ }
793
+ this.capture?.stop();
794
+ this.capture = null;
795
+ this.playback?.close();
796
+ this.playback = null;
804
797
  try {
805
- this.rws?.close(1e3, reason)
806
- } catch {}
807
- this.rws = null
808
- this.setState('ended')
809
- this.fireEndOnce(reason)
810
- }
798
+ this.rws?.close(1e3, reason);
799
+ } catch {
800
+ }
801
+ this.rws = null;
802
+ this.setState("ended");
803
+ this.fireEndOnce(reason);
804
+ };
811
805
  this.fireEndOnce = (reason) => {
812
- if (this.endedFired) return
813
- this.endedFired = true
814
- const startedAt = this.startedAt ?? Date.now()
806
+ if (this.endedFired) return;
807
+ this.endedFired = true;
808
+ const startedAt = this.startedAt ?? Date.now();
815
809
  this.args.options.onEnd?.({
816
810
  reason,
817
- errorCode: reason === 'error' ? this.lastError?.code : void 0,
818
- durationMs: Date.now() - startedAt,
819
- })
820
- }
821
- this.args = args
822
- this.proto = createProtocolState()
823
- validateClientToolMap(args.options.clientTools)
811
+ errorCode: reason === "error" ? this.lastError?.code : void 0,
812
+ durationMs: Date.now() - startedAt
813
+ });
814
+ };
815
+ this.args = args;
816
+ this.proto = createProtocolState();
817
+ validateClientToolMap(args.options.clientTools);
824
818
  this.marks = createClientMarksBuffer({
825
819
  send: (frame) => {
826
820
  try {
827
- this.rws?.send(JSON.stringify(frame))
828
- } catch {}
829
- },
830
- })
821
+ this.rws?.send(JSON.stringify(frame));
822
+ } catch {
823
+ }
824
+ }
825
+ });
831
826
  }
832
827
  // ---------------------------------------------------------------
833
828
  // Call interface
834
829
  // ---------------------------------------------------------------
835
830
  get state() {
836
- return this.proto.state
831
+ return this.proto.state;
837
832
  }
838
833
  get transcript() {
839
- return this.proto.transcript.slice()
834
+ return this.proto.transcript.slice();
840
835
  }
841
836
  get isMuted() {
842
- return this.muted
837
+ return this.muted;
843
838
  }
844
839
  // ---------------------------------------------------------------
845
840
  // Lifecycle — called by the factory immediately after construction.
@@ -847,52 +842,54 @@ var BrowserVoiceClient = class {
847
842
  // failures arrive via `onError`.
848
843
  // ---------------------------------------------------------------
849
844
  async start() {
850
- this.setState('connecting')
851
- this.startedAt = Date.now()
845
+ this.setState("connecting");
846
+ this.startedAt = Date.now();
852
847
  const url = buildWsUrl({
853
848
  apiBase: this.args.config.apiBase,
854
849
  agentId: this.args.options.agentId,
855
850
  token: this.args.token,
856
- bargeIn: this.args.options.bargeIn,
857
- })
851
+ bargeIn: this.args.options.bargeIn
852
+ });
858
853
  this.playback = createAudioPlayback({
859
854
  onVolume: (v) => {
860
- this.outputVolume = v
861
- this.args.options.onVolume?.({ input: this.inputVolume, output: v })
862
- },
863
- })
855
+ this.outputVolume = v;
856
+ this.args.options.onVolume?.({ input: this.inputVolume, output: v });
857
+ }
858
+ });
864
859
  try {
865
- await this.playback.resume()
866
- } catch {}
860
+ await this.playback.resume();
861
+ } catch {
862
+ }
867
863
  this.rws = createReconnectingWebSocket(
868
864
  {
869
865
  url,
870
866
  wsFactory: this.args.wsFactory,
871
- maxRetries: 3,
867
+ maxRetries: 3
872
868
  },
873
- (ev) => this.handleSocketEvent(ev),
874
- )
869
+ (ev) => this.handleSocketEvent(ev)
870
+ );
875
871
  }
876
- }
872
+ };
877
873
 
878
874
  // src/webrtc/createWebRtcCall.ts
879
875
  async function createWebRtcCall(opts) {
880
- validateClientToolMap(opts.clientTools)
881
- const proto = createProtocolState()
882
- let muted = false
883
- let ended = false
884
- const tools = opts.clientTools ?? {}
876
+ validateClientToolMap(opts.clientTools);
877
+ const proto = createProtocolState();
878
+ let muted = false;
879
+ let ended = false;
880
+ const tools = opts.clientTools ?? {};
885
881
  const sendControl = (frame) => {
886
- if (dc?.readyState !== 'open') return
882
+ if (dc?.readyState !== "open") return;
887
883
  try {
888
- dc.send(JSON.stringify(frame))
889
- } catch {}
890
- }
884
+ dc.send(JSON.stringify(frame));
885
+ } catch {
886
+ }
887
+ };
891
888
  const fireState = (next) => {
892
- if (proto.state === next) return
893
- proto.state = next
894
- opts.onStateChange?.(next)
895
- }
889
+ if (proto.state === next) return;
890
+ proto.state = next;
891
+ opts.onStateChange?.(next);
892
+ };
896
893
  const dispatch = (raw) => {
897
894
  handleServerMessage(raw, proto, {
898
895
  onState: fireState,
@@ -900,383 +897,466 @@ async function createWebRtcCall(opts) {
900
897
  onError: (err) => opts.onError?.(err),
901
898
  onInterrupt: () => opts.onInterrupt?.(),
902
899
  onAgentTurnStart: () => opts.onAgentTurnStart?.(),
903
- onAgentTurnEnd: () => {},
900
+ onAgentTurnEnd: () => {
901
+ },
904
902
  onCallEnd: () => teardown(),
905
903
  onConnected: () => {
906
904
  if (Object.keys(tools).length > 0) {
907
- sendControl(buildRegisterFrame(tools))
905
+ sendControl(buildRegisterFrame(tools));
908
906
  }
909
907
  },
910
908
  onClientToolCall: (frame) => {
911
- dispatchClientToolCall(sendControl, tools, frame)
912
- },
913
- })
914
- }
915
- fireState('connecting')
909
+ dispatchClientToolCall(sendControl, tools, frame);
910
+ }
911
+ });
912
+ };
913
+ fireState("connecting");
916
914
  const pc = new RTCPeerConnection({
917
- iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
918
- })
919
- const audioEl = document.createElement('audio')
920
- audioEl.autoplay = true
921
- audioEl.style.display = 'none'
922
- document.body.appendChild(audioEl)
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);
923
921
  pc.ontrack = (event) => {
924
- audioEl.srcObject = event.streams[0] ?? new MediaStream([event.track])
925
- }
926
- let mic
922
+ audioEl.srcObject = event.streams[0] ?? new MediaStream([event.track]);
923
+ };
924
+ let mic;
927
925
  try {
928
- mic = await navigator.mediaDevices.getUserMedia({ audio: true })
926
+ mic = await navigator.mediaDevices.getUserMedia({ audio: true });
929
927
  } catch (err) {
930
- const code =
931
- err instanceof DOMException && err.name === 'NotAllowedError'
932
- ? 'mic_denied'
933
- : 'mic_start_failed'
928
+ const code = err instanceof DOMException && err.name === "NotAllowedError" ? "mic_denied" : "mic_start_failed";
934
929
  opts.onError?.({
935
930
  code,
936
- message: err instanceof Error ? err.message : 'getUserMedia failed',
937
- })
938
- fireState('error')
939
- pc.close()
940
- audioEl.remove()
941
- throw err
942
- }
943
- for (const track of mic.getAudioTracks()) pc.addTrack(track, mic)
944
- const dc = pc.createDataChannel('control', { ordered: true })
931
+ message: err instanceof Error ? err.message : "getUserMedia failed"
932
+ });
933
+ fireState("error");
934
+ pc.close();
935
+ audioEl.remove();
936
+ throw err;
937
+ }
938
+ for (const track of mic.getAudioTracks()) pc.addTrack(track, mic);
939
+ const dc = pc.createDataChannel("control", { ordered: true });
945
940
  dc.onmessage = (e) => {
946
- if (typeof e.data === 'string') dispatch(e.data)
947
- }
941
+ if (typeof e.data === "string") dispatch(e.data);
942
+ };
948
943
  dc.onerror = () => {
949
- opts.onError?.({ code: 'socket_error', message: 'control channel error' })
950
- }
944
+ opts.onError?.({ code: "socket_error", message: "control channel error" });
945
+ };
951
946
  dc.onopen = () => {
952
947
  if (Object.keys(tools).length > 0) {
953
- sendControl(buildRegisterFrame(tools))
948
+ sendControl(buildRegisterFrame(tools));
954
949
  }
955
- }
956
- const gateway = opts.webrtcGatewayBase || ''
957
- const offerUrl = gateway
958
- ? `${gateway}/webrtc/offer?token=${encodeURIComponent(opts.token)}`
959
- : `${opts.apiBase}/v1/agents/${encodeURIComponent(opts.agentId)}/webrtc/offer?token=${encodeURIComponent(opts.token)}`
960
- const iceUrl = gateway
961
- ? `${gateway}/webrtc/ice?token=${encodeURIComponent(opts.token)}`
962
- : `${opts.apiBase}/v1/agents/${encodeURIComponent(opts.agentId)}/webrtc/ice?token=${encodeURIComponent(opts.token)}`
950
+ };
951
+ const gateway = opts.webrtcGatewayBase || "";
952
+ const offerUrl = gateway ? `${gateway}/webrtc/offer?token=${encodeURIComponent(opts.token)}` : `${opts.apiBase}/v1/agents/${encodeURIComponent(opts.agentId)}/webrtc/offer?token=${encodeURIComponent(opts.token)}`;
953
+ const iceUrl = gateway ? `${gateway}/webrtc/ice?token=${encodeURIComponent(opts.token)}` : `${opts.apiBase}/v1/agents/${encodeURIComponent(opts.agentId)}/webrtc/ice?token=${encodeURIComponent(opts.token)}`;
963
954
  const teardown = () => {
964
- if (ended) return
965
- ended = true
955
+ if (ended) return;
956
+ ended = true;
966
957
  try {
967
- mic.getTracks().forEach((t) => t.stop())
968
- } catch {}
958
+ mic.getTracks().forEach((t) => t.stop());
959
+ } catch {
960
+ }
969
961
  try {
970
- pc.close()
971
- } catch {}
962
+ pc.close();
963
+ } catch {
964
+ }
972
965
  try {
973
- audioEl.remove()
974
- } catch {}
975
- fireState('ended')
976
- opts.onEnd?.()
977
- }
978
- let callId = null
979
- const pendingCandidates = []
966
+ audioEl.remove();
967
+ } catch {
968
+ }
969
+ fireState("ended");
970
+ opts.onEnd?.();
971
+ };
972
+ let callId = null;
973
+ const pendingCandidates = [];
980
974
  const postCandidate = (candidate) => {
981
975
  void fetch(iceUrl, {
982
- method: 'POST',
983
- headers: { 'content-type': 'application/json' },
984
- body: JSON.stringify({ callId, candidate }),
985
- }).catch(() => {})
986
- }
976
+ method: "POST",
977
+ headers: { "content-type": "application/json" },
978
+ body: JSON.stringify({ callId, candidate })
979
+ }).catch(() => {
980
+ });
981
+ };
987
982
  pc.onicecandidate = (e) => {
988
- if (!e.candidate) return
989
- if (callId) postCandidate(e.candidate)
990
- else pendingCandidates.push(e.candidate)
991
- }
983
+ if (!e.candidate) return;
984
+ if (callId) postCandidate(e.candidate);
985
+ else pendingCandidates.push(e.candidate);
986
+ };
992
987
  pc.onconnectionstatechange = () => {
993
- const s = pc.connectionState
994
- if (s === 'connected') fireState('listening')
995
- if (s === 'failed' || s === 'disconnected') {
996
- opts.onError?.({ code: 'socket_error', message: `webrtc connection ${s}` })
997
- teardown()
988
+ const s = pc.connectionState;
989
+ if (s === "connected") fireState("listening");
990
+ if (s === "failed" || s === "disconnected") {
991
+ opts.onError?.({ code: "socket_error", message: `webrtc connection ${s}` });
992
+ teardown();
998
993
  }
999
- if (s === 'closed' && !ended) teardown()
1000
- }
1001
- await pc.setLocalDescription(await pc.createOffer())
994
+ if (s === "closed" && !ended) teardown();
995
+ };
996
+ await pc.setLocalDescription(await pc.createOffer());
1002
997
  try {
1003
998
  const offerRes = await fetch(offerUrl, {
1004
- method: 'POST',
1005
- headers: { 'content-type': 'application/json' },
1006
- body: JSON.stringify({ sdp: pc.localDescription.sdp, type: 'offer', agentId: opts.agentId }),
1007
- })
999
+ method: "POST",
1000
+ headers: { "content-type": "application/json" },
1001
+ body: JSON.stringify({ sdp: pc.localDescription.sdp, type: "offer", agentId: opts.agentId })
1002
+ });
1008
1003
  if (!offerRes.ok) {
1009
- const code = offerRes.status === 401 ? 'unauthorized' : 'server_error'
1010
- opts.onError?.({ code, message: `signaling failed: HTTP ${offerRes.status}` })
1011
- fireState('error')
1012
- mic.getTracks().forEach((t) => t.stop())
1013
- pc.close()
1014
- audioEl.remove()
1015
- throw new Error(`webrtc offer failed: ${offerRes.status}`)
1004
+ const code = offerRes.status === 401 ? "unauthorized" : "server_error";
1005
+ opts.onError?.({ code, message: `signaling failed: HTTP ${offerRes.status}` });
1006
+ fireState("error");
1007
+ mic.getTracks().forEach((t) => t.stop());
1008
+ pc.close();
1009
+ audioEl.remove();
1010
+ throw new Error(`webrtc offer failed: ${offerRes.status}`);
1016
1011
  }
1017
- const body = await offerRes.json()
1018
- callId = body.callId
1019
- await pc.setRemoteDescription({ type: 'answer', sdp: body.sdp })
1020
- while (pendingCandidates.length > 0) postCandidate(pendingCandidates.shift())
1012
+ const body = await offerRes.json();
1013
+ callId = body.callId;
1014
+ await pc.setRemoteDescription({ type: "answer", sdp: body.sdp });
1015
+ while (pendingCandidates.length > 0) postCandidate(pendingCandidates.shift());
1021
1016
  } catch (err) {
1022
1017
  if (!ended) {
1023
1018
  opts.onError?.({
1024
- code: 'network_unreachable',
1025
- message: err instanceof Error ? err.message : 'signaling failed',
1026
- })
1027
- fireState('error')
1028
- mic.getTracks().forEach((t) => t.stop())
1029
- pc.close()
1030
- audioEl.remove()
1019
+ code: "network_unreachable",
1020
+ message: err instanceof Error ? err.message : "signaling failed"
1021
+ });
1022
+ fireState("error");
1023
+ mic.getTracks().forEach((t) => t.stop());
1024
+ pc.close();
1025
+ audioEl.remove();
1031
1026
  }
1032
- throw err
1027
+ throw err;
1033
1028
  }
1034
1029
  return {
1035
1030
  get state() {
1036
- return proto.state
1031
+ return proto.state;
1037
1032
  },
1038
1033
  get transcript() {
1039
- return proto.transcript.slice()
1034
+ return proto.transcript.slice();
1040
1035
  },
1041
1036
  get isMuted() {
1042
- return muted
1037
+ return muted;
1043
1038
  },
1044
1039
  end: () => teardown(),
1045
1040
  mute: () => {
1046
- if (muted) return
1047
- muted = true
1048
- mic.getAudioTracks().forEach((t) => (t.enabled = false))
1041
+ if (muted) return;
1042
+ muted = true;
1043
+ mic.getAudioTracks().forEach((t) => t.enabled = false);
1049
1044
  },
1050
1045
  unmute: () => {
1051
- if (!muted) return
1052
- muted = false
1053
- mic.getAudioTracks().forEach((t) => (t.enabled = true))
1054
- },
1055
- }
1046
+ if (!muted) return;
1047
+ muted = false;
1048
+ mic.getAudioTracks().forEach((t) => t.enabled = true);
1049
+ }
1050
+ };
1056
1051
  }
1057
1052
 
1058
1053
  // src/room.ts
1059
- var import_livekit_client = require('livekit-client')
1054
+ var import_livekit_client = require("livekit-client");
1060
1055
 
1061
1056
  // src/roomProtocol.ts
1062
- var SYSTEM_TOPIC = 'system'
1063
- var TRANSCRIPT_TOPIC = 'transcript'
1057
+ var SYSTEM_TOPIC = "system";
1058
+ var TRANSCRIPT_TOPIC = "transcript";
1064
1059
  var decodeSystem = (bytes) => {
1065
1060
  try {
1066
- const v = JSON.parse(new TextDecoder().decode(bytes))
1067
- if (v && typeof v.kind === 'string') return v
1068
- return null
1061
+ const v = JSON.parse(new TextDecoder().decode(bytes));
1062
+ if (v && typeof v.kind === "string") return v;
1063
+ return null;
1069
1064
  } catch {
1070
- return null
1065
+ return null;
1071
1066
  }
1072
- }
1067
+ };
1073
1068
  var decodeTranscript = (bytes) => {
1074
1069
  try {
1075
- const v = JSON.parse(new TextDecoder().decode(bytes))
1076
- if (v && v.kind === 'partial') return v
1077
- return null
1070
+ const v = JSON.parse(new TextDecoder().decode(bytes));
1071
+ if (v && v.kind === "partial") return v;
1072
+ return null;
1078
1073
  } catch {
1079
- return null
1074
+ return null;
1080
1075
  }
1081
- }
1076
+ };
1082
1077
 
1083
1078
  // src/room.ts
1084
- var identityToPid = (identity) =>
1085
- identity.startsWith('guest:') ? identity.slice('guest:'.length) : identity
1079
+ var identityToPid = (identity) => identity.startsWith("guest:") ? identity.slice("guest:".length) : identity;
1086
1080
  var joinRoom = async (opts) => {
1087
- const exchangeUrl = `${opts.apiBase.replace(/\/+$/, '')}/v1/rooms/${encodeURIComponent(
1088
- opts.roomId,
1089
- )}/join`
1081
+ const exchangeUrl = `${opts.apiBase.replace(/\/+$/, "")}/v1/rooms/${encodeURIComponent(
1082
+ opts.roomId
1083
+ )}/join`;
1090
1084
  const exchangeRes = await fetch(exchangeUrl, {
1091
- method: 'POST',
1092
- headers: { 'Content-Type': 'application/json' },
1093
- body: JSON.stringify({ code: opts.joinCode, name: opts.name }),
1094
- })
1085
+ method: "POST",
1086
+ headers: { "Content-Type": "application/json" },
1087
+ body: JSON.stringify({ code: opts.joinCode, name: opts.name })
1088
+ });
1095
1089
  if (!exchangeRes.ok) {
1096
- const err = await exchangeRes.json().catch(() => ({}))
1097
- throw new Error(err.error?.code ?? `join_failed_${exchangeRes.status}`)
1090
+ const err = await exchangeRes.json().catch(() => ({}));
1091
+ throw new Error(err.error?.code ?? `join_failed_${exchangeRes.status}`);
1098
1092
  }
1099
- const exchange = await exchangeRes.json()
1100
- const handlers = /* @__PURE__ */ new Map()
1093
+ const exchange = await exchangeRes.json();
1094
+ const handlers = /* @__PURE__ */ new Map();
1101
1095
  const emit = (e, payload) => {
1102
1096
  handlers.get(e)?.forEach((h) => {
1103
1097
  try {
1104
- h(payload)
1105
- } catch {}
1106
- })
1107
- }
1108
- const room = new import_livekit_client.Room({ adaptiveStream: true, dynacast: true })
1109
- room.on(import_livekit_client.RoomEvent.ParticipantConnected, (p) =>
1110
- emit('participant.joined', {
1098
+ h(payload);
1099
+ } catch {
1100
+ }
1101
+ });
1102
+ };
1103
+ const room = new import_livekit_client.Room({ adaptiveStream: true, dynacast: true });
1104
+ room.on(
1105
+ import_livekit_client.RoomEvent.ParticipantConnected,
1106
+ (p) => emit("participant.joined", {
1111
1107
  participantId: identityToPid(p.identity),
1112
- name: p.name ?? '',
1113
- }),
1114
- )
1115
- room.on(import_livekit_client.RoomEvent.ParticipantDisconnected, (p) =>
1116
- emit('participant.left', {
1108
+ name: p.name ?? ""
1109
+ })
1110
+ );
1111
+ room.on(
1112
+ import_livekit_client.RoomEvent.ParticipantDisconnected,
1113
+ (p) => emit("participant.left", {
1117
1114
  participantId: identityToPid(p.identity),
1118
- name: p.name ?? '',
1119
- }),
1120
- )
1121
- room.on(import_livekit_client.RoomEvent.Disconnected, () => emit('room.ended', void 0))
1115
+ name: p.name ?? ""
1116
+ })
1117
+ );
1118
+ room.on(import_livekit_client.RoomEvent.Disconnected, () => emit("room.ended", void 0));
1122
1119
  room.on(import_livekit_client.RoomEvent.DataReceived, (data, _participant, _kind, topic) => {
1123
1120
  if (topic === SYSTEM_TOPIC) {
1124
- const m = decodeSystem(data)
1125
- if (m) emit('system.message', m)
1121
+ const m = decodeSystem(data);
1122
+ if (m) emit("system.message", m);
1126
1123
  } else if (topic === TRANSCRIPT_TOPIC) {
1127
- const m = decodeTranscript(data)
1128
- if (m) emit('transcript.partial', m)
1124
+ const m = decodeTranscript(data);
1125
+ if (m) emit("transcript.partial", m);
1129
1126
  }
1130
- })
1131
- const trackKind = (t) => (t.kind === import_livekit_client.Track.Kind.Video ? 'video' : 'audio')
1127
+ });
1128
+ const trackKind = (t) => t.kind === import_livekit_client.Track.Kind.Video ? "video" : "audio";
1132
1129
  const trackSource = (s) => {
1133
1130
  switch (s) {
1134
1131
  case import_livekit_client.Track.Source.Camera:
1135
- return 'camera'
1132
+ return "camera";
1136
1133
  case import_livekit_client.Track.Source.Microphone:
1137
- return 'microphone'
1134
+ return "microphone";
1138
1135
  case import_livekit_client.Track.Source.ScreenShare:
1139
- return 'screen_share'
1136
+ return "screen_share";
1140
1137
  case import_livekit_client.Track.Source.ScreenShareAudio:
1141
- return 'screen_share_audio'
1138
+ return "screen_share_audio";
1142
1139
  default:
1143
- return 'unknown'
1140
+ return "unknown";
1144
1141
  }
1145
- }
1146
- room.on(import_livekit_client.RoomEvent.TrackSubscribed, (track, pub, participant) =>
1147
- emit('track.subscribed', {
1142
+ };
1143
+ room.on(
1144
+ import_livekit_client.RoomEvent.TrackSubscribed,
1145
+ (track, pub, participant) => emit("track.subscribed", {
1148
1146
  participantId: identityToPid(participant.identity),
1149
1147
  kind: trackKind(track),
1150
1148
  source: trackSource(pub.source),
1151
- track,
1152
- }),
1153
- )
1154
- room.on(import_livekit_client.RoomEvent.TrackUnsubscribed, (track, pub, participant) =>
1155
- emit('track.unsubscribed', {
1149
+ track
1150
+ })
1151
+ );
1152
+ room.on(
1153
+ import_livekit_client.RoomEvent.TrackUnsubscribed,
1154
+ (track, pub, participant) => emit("track.unsubscribed", {
1156
1155
  participantId: identityToPid(participant.identity),
1157
1156
  kind: trackKind(track),
1158
1157
  source: trackSource(pub.source),
1159
- track,
1160
- }),
1161
- )
1162
- room.on(import_livekit_client.RoomEvent.ActiveSpeakersChanged, (speakers) =>
1163
- emit(
1164
- 'active.speakers',
1165
- speakers.map((p) => identityToPid(p.identity)),
1166
- ),
1167
- )
1168
- await room.connect(exchange.livekit.url, exchange.livekit.token)
1158
+ track
1159
+ })
1160
+ );
1161
+ room.on(
1162
+ import_livekit_client.RoomEvent.ActiveSpeakersChanged,
1163
+ (speakers) => emit(
1164
+ "active.speakers",
1165
+ speakers.map((p) => identityToPid(p.identity))
1166
+ )
1167
+ );
1168
+ await room.connect(exchange.livekit.url, exchange.livekit.token);
1169
1169
  return {
1170
1170
  participantId: exchange.participantId,
1171
1171
  get participants() {
1172
1172
  return [...room.remoteParticipants.values()].map((p) => ({
1173
1173
  participantId: identityToPid(p.identity),
1174
- name: p.name ?? '',
1175
- }))
1174
+ name: p.name ?? ""
1175
+ }));
1176
1176
  },
1177
1177
  on(event, handler) {
1178
- const set = handlers.get(event) ?? /* @__PURE__ */ new Set()
1179
- set.add(handler)
1180
- handlers.set(event, set)
1178
+ const set = handlers.get(event) ?? /* @__PURE__ */ new Set();
1179
+ set.add(handler);
1180
+ handlers.set(event, set);
1181
1181
  },
1182
1182
  publishMic: async () => {
1183
- await room.localParticipant.setMicrophoneEnabled(true)
1183
+ await room.localParticipant.setMicrophoneEnabled(true);
1184
1184
  },
1185
1185
  publishCamera: async () => {
1186
- await room.localParticipant.setCameraEnabled(true)
1186
+ await room.localParticipant.setCameraEnabled(true);
1187
1187
  },
1188
1188
  setMicEnabled: async (on) => {
1189
- await room.localParticipant.setMicrophoneEnabled(on)
1189
+ await room.localParticipant.setMicrophoneEnabled(on);
1190
1190
  },
1191
1191
  setCameraEnabled: async (on) => {
1192
- await room.localParticipant.setCameraEnabled(on)
1192
+ await room.localParticipant.setCameraEnabled(on);
1193
1193
  },
1194
1194
  isMicEnabled: () => room.localParticipant.isMicrophoneEnabled,
1195
1195
  isCameraEnabled: () => room.localParticipant.isCameraEnabled,
1196
- getLocalCameraTrack: () =>
1197
- room.localParticipant.getTrackPublication(import_livekit_client.Track.Source.Camera)
1198
- ?.videoTrack ?? null,
1196
+ getLocalCameraTrack: () => room.localParticipant.getTrackPublication(import_livekit_client.Track.Source.Camera)?.videoTrack ?? null,
1199
1197
  getRemoteTracks: () => {
1200
- const out = []
1198
+ const out = [];
1201
1199
  for (const p of room.remoteParticipants.values()) {
1202
1200
  for (const pub of p.trackPublications.values()) {
1203
- const track = pub.track
1204
- if (!track) continue
1201
+ const track = pub.track;
1202
+ if (!track) continue;
1205
1203
  out.push({
1206
1204
  participantId: identityToPid(p.identity),
1207
1205
  kind: trackKind(track),
1208
1206
  source: trackSource(pub.source),
1209
- track,
1210
- })
1207
+ track
1208
+ });
1211
1209
  }
1212
1210
  }
1213
- return out
1211
+ return out;
1214
1212
  },
1215
1213
  setScreenShareEnabled: async (on, opts2) => {
1216
- await room.localParticipant.setScreenShareEnabled(on, { audio: opts2?.audio ?? false })
1214
+ await room.localParticipant.setScreenShareEnabled(on, { audio: opts2?.audio ?? false });
1217
1215
  },
1218
1216
  isScreenShareEnabled: () => room.localParticipant.isScreenShareEnabled,
1219
- getLocalScreenTrack: () =>
1220
- room.localParticipant.getTrackPublication(import_livekit_client.Track.Source.ScreenShare)
1221
- ?.videoTrack ?? null,
1217
+ getLocalScreenTrack: () => room.localParticipant.getTrackPublication(import_livekit_client.Track.Source.ScreenShare)?.videoTrack ?? null,
1222
1218
  leave: async () => {
1223
- await room.disconnect()
1219
+ await room.disconnect();
1220
+ }
1221
+ };
1222
+ };
1223
+
1224
+ // src/textSession.ts
1225
+ async function startTextSession(opts) {
1226
+ const f = opts.fetch ?? fetch;
1227
+ const tokenQs = `?token=${encodeURIComponent(opts.token)}`;
1228
+ const startUrl = `${opts.baseUrl}/v1/agents/${opts.agentId}/chat${tokenQs}`;
1229
+ const startBody = opts.text ? JSON.stringify({ text: opts.text }) : "{}";
1230
+ const res = await f(startUrl, {
1231
+ method: "POST",
1232
+ headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
1233
+ body: startBody
1234
+ });
1235
+ if (!res.ok || !res.body) {
1236
+ const text = await res.text().catch(() => "");
1237
+ throw new Error(`startTextSession failed: ${res.status} ${text}`);
1238
+ }
1239
+ const iter = parseSse(res.body);
1240
+ let chatId = "";
1241
+ let callId = "";
1242
+ const buffered = [];
1243
+ const it = iter[Symbol.asyncIterator]();
1244
+ while (true) {
1245
+ const { value, done } = await it.next();
1246
+ if (done) break;
1247
+ if (value.type === "chat.started") {
1248
+ chatId = value.chatId;
1249
+ callId = value.callId;
1250
+ break;
1251
+ }
1252
+ buffered.push(value);
1253
+ }
1254
+ return {
1255
+ id: chatId,
1256
+ callId,
1257
+ greeting: replayThen(buffered, { [Symbol.asyncIterator]: () => it }),
1258
+ async send(text) {
1259
+ const r = await f(`${opts.baseUrl}/v1/chats/${chatId}/messages${tokenQs}`, {
1260
+ method: "POST",
1261
+ headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
1262
+ body: JSON.stringify({ text })
1263
+ });
1264
+ if (!r.ok || !r.body) {
1265
+ const errText = await r.text().catch(() => "");
1266
+ throw new Error(`send failed: ${r.status} ${errText}`);
1267
+ }
1268
+ return parseSse(r.body);
1224
1269
  },
1270
+ async end() {
1271
+ await f(`${opts.baseUrl}/v1/calls/${callId}`, { method: "DELETE" });
1272
+ }
1273
+ };
1274
+ }
1275
+ async function* parseSse(body) {
1276
+ const reader = body.getReader();
1277
+ const decoder = new TextDecoder();
1278
+ let buf = "";
1279
+ while (true) {
1280
+ const { value, done } = await reader.read();
1281
+ if (done) return;
1282
+ buf += decoder.decode(value, { stream: true });
1283
+ let idx;
1284
+ while ((idx = buf.indexOf("\n\n")) >= 0) {
1285
+ const chunk = buf.slice(0, idx);
1286
+ buf = buf.slice(idx + 2);
1287
+ let event = "message";
1288
+ let data = "";
1289
+ for (const line of chunk.split("\n")) {
1290
+ if (line.startsWith(":")) continue;
1291
+ if (line.startsWith("event:")) event = line.slice(6).trim();
1292
+ else if (line.startsWith("data:")) data += line.slice(5).trim();
1293
+ }
1294
+ if (!data) continue;
1295
+ try {
1296
+ const parsed = JSON.parse(data);
1297
+ yield { type: event, ...parsed };
1298
+ } catch {
1299
+ }
1300
+ }
1225
1301
  }
1226
1302
  }
1303
+ async function* replayThen(buffered, rest) {
1304
+ for (const x of buffered) yield x;
1305
+ for await (const x of rest) yield x;
1306
+ }
1227
1307
 
1228
1308
  // src/incomingCall.ts
1229
1309
  var parseIncomingCall = (raw) => {
1230
- if (typeof raw !== 'object' || raw === null) {
1231
- throw new Error('parseIncomingCall: payload must be an object')
1232
- }
1233
- const p = raw
1234
- if (typeof p.token !== 'string' || !p.token.startsWith('ct_')) {
1235
- throw new Error('parseIncomingCall: missing or invalid `token` (expected a ct_ string)')
1236
- }
1237
- if (typeof p.agentId !== 'string' || p.agentId.length === 0) {
1238
- throw new Error('parseIncomingCall: missing `agentId`')
1239
- }
1240
- const transport = p.transport === 'webrtc' ? 'webrtc' : 'ws'
1241
- const out = { token: p.token, agentId: p.agentId, transport }
1242
- if (transport === 'webrtc' && typeof p.webrtcGatewayBase === 'string') {
1243
- out.webrtcGatewayBase = p.webrtcGatewayBase
1244
- }
1245
- if (typeof p.expiresAt === 'number') out.expiresAt = p.expiresAt
1246
- if (typeof p.agentName === 'string') out.agentName = p.agentName
1247
- if (typeof p.agentAvatarUrl === 'string') out.agentAvatarUrl = p.agentAvatarUrl
1248
- return out
1249
- }
1310
+ if (typeof raw !== "object" || raw === null) {
1311
+ throw new Error("parseIncomingCall: payload must be an object");
1312
+ }
1313
+ const p = raw;
1314
+ if (typeof p.token !== "string" || !p.token.startsWith("ct_")) {
1315
+ throw new Error("parseIncomingCall: missing or invalid `token` (expected a ct_ string)");
1316
+ }
1317
+ if (typeof p.agentId !== "string" || p.agentId.length === 0) {
1318
+ throw new Error("parseIncomingCall: missing `agentId`");
1319
+ }
1320
+ const transport = p.transport === "webrtc" ? "webrtc" : "ws";
1321
+ const out = { token: p.token, agentId: p.agentId, transport };
1322
+ if (transport === "webrtc" && typeof p.webrtcGatewayBase === "string") {
1323
+ out.webrtcGatewayBase = p.webrtcGatewayBase;
1324
+ }
1325
+ if (typeof p.expiresAt === "number") out.expiresAt = p.expiresAt;
1326
+ if (typeof p.agentName === "string") out.agentName = p.agentName;
1327
+ if (typeof p.agentAvatarUrl === "string") out.agentAvatarUrl = p.agentAvatarUrl;
1328
+ return out;
1329
+ };
1250
1330
 
1251
1331
  // src/browser.ts
1252
- var browserWsFactory = (url) => new globalThis.WebSocket(url)
1332
+ var browserWsFactory = (url) => new globalThis.WebSocket(url);
1253
1333
  var BrowserVoiceFactory = class {
1254
1334
  constructor(config) {
1255
1335
  this.startCall = async (options) => {
1256
1336
  if (!options.agentId) {
1257
- throw new Error('startCall: agentId is required')
1337
+ throw new Error("startCall: agentId is required");
1258
1338
  }
1259
- const { context, metadata } = mergeStartCallContext(this.config, options)
1339
+ const { context, metadata } = mergeStartCallContext(this.config, options);
1260
1340
  const fetchArgs = {
1261
1341
  agentId: options.agentId,
1262
1342
  userId: options.userId,
1263
1343
  context,
1264
- metadata,
1265
- }
1266
- let resolved
1344
+ metadata
1345
+ };
1346
+ let resolved;
1267
1347
  if (options.token) {
1268
- resolved = { token: options.token, transport: 'ws' }
1348
+ resolved = { token: options.token, transport: "ws" };
1269
1349
  } else {
1270
- const r = await this.config.fetchToken(fetchArgs)
1350
+ const r = await this.config.fetchToken(fetchArgs);
1271
1351
  if (!r) {
1272
- throw new Error('configureVoiceClient.fetchToken returned empty token')
1352
+ throw new Error("configureVoiceClient.fetchToken returned empty token");
1273
1353
  }
1274
- resolved = typeof r === 'string' ? { token: r, transport: 'ws' } : r
1354
+ resolved = typeof r === "string" ? { token: r, transport: "ws" } : r;
1275
1355
  if (!resolved.token) {
1276
- throw new Error('configureVoiceClient.fetchToken returned an object without `token`')
1356
+ throw new Error("configureVoiceClient.fetchToken returned an object without `token`");
1277
1357
  }
1278
1358
  }
1279
- if (resolved.transport === 'webrtc') {
1359
+ if (resolved.transport === "webrtc") {
1280
1360
  return createWebRtcCall({
1281
1361
  agentId: options.agentId,
1282
1362
  apiBase: this.config.apiBase,
@@ -1288,13 +1368,11 @@ var BrowserVoiceFactory = class {
1288
1368
  // Synthesise a minimal CallEndEvent. WebRTC doesn't carry an end reason
1289
1369
  // from the server yet — use 'agent_ended' as placeholder. durationMs is
1290
1370
  // tracked at 0 until the followup lands (see spec Followups section).
1291
- onEnd: options.onEnd
1292
- ? () => options.onEnd({ reason: 'agent_ended', durationMs: 0 })
1293
- : void 0,
1371
+ onEnd: options.onEnd ? () => options.onEnd({ reason: "agent_ended", durationMs: 0 }) : void 0,
1294
1372
  onInterrupt: options.onInterrupt,
1295
1373
  onAgentTurnStart: options.onAgentTurnStart,
1296
- clientTools: options.clientTools,
1297
- })
1374
+ clientTools: options.clientTools
1375
+ });
1298
1376
  }
1299
1377
  const client = new BrowserVoiceClient({
1300
1378
  config: this.config,
@@ -1302,11 +1380,11 @@ var BrowserVoiceFactory = class {
1302
1380
  // see what the SDK saw.
1303
1381
  options: { ...options, context, metadata },
1304
1382
  token: resolved.token,
1305
- wsFactory: browserWsFactory,
1306
- })
1307
- await client.start()
1308
- return client
1309
- }
1383
+ wsFactory: browserWsFactory
1384
+ });
1385
+ await client.start();
1386
+ return client;
1387
+ };
1310
1388
  // Multi-party rooms (Phase 7 video).
1311
1389
  //
1312
1390
  // The guest's browser calls this with the roomId + joinCode it parsed
@@ -1314,24 +1392,31 @@ var BrowserVoiceFactory = class {
1314
1392
  // JWT against `${apiBase}/v1/rooms/:roomId/join` (an AUTH-EXEMPT
1315
1393
  // endpoint — the opaque code is the only credential), then connects
1316
1394
  // to LiveKit and returns a typed event surface.
1317
- this.joinRoom = (opts) => joinRoom({ apiBase: this.config.apiBase, ...opts })
1318
- this.config = config
1319
- }
1320
- }
1395
+ this.joinRoom = (opts) => joinRoom({ apiBase: this.config.apiBase, ...opts });
1396
+ // Text-channel chat session (no microphone / audio).
1397
+ // Mint a `ct_` token with `channel: 'text'` on your backend, then call
1398
+ // this to open an SSE stream against the chat API.
1399
+ this.startTextSession = (opts) => startTextSession({
1400
+ ...opts,
1401
+ baseUrl: this.config.apiBase
1402
+ });
1403
+ this.config = config;
1404
+ }
1405
+ };
1321
1406
  function configureVoiceClient(config) {
1322
- return new BrowserVoiceFactory(normalizeConfig(config))
1407
+ return new BrowserVoiceFactory(normalizeConfig(config));
1323
1408
  }
1324
1409
  // Annotate the CommonJS export names for ESM import in node:
1325
- 0 &&
1326
- (module.exports = {
1327
- buildWsUrl,
1328
- configureVoiceClient,
1329
- createAudioCapture,
1330
- createAudioPlayback,
1331
- createProtocolState,
1332
- createReconnectingWebSocket,
1333
- handleServerMessage,
1334
- joinRoom,
1335
- parseIncomingCall,
1336
- })
1337
- //# sourceMappingURL=browser.js.map
1410
+ 0 && (module.exports = {
1411
+ buildWsUrl,
1412
+ configureVoiceClient,
1413
+ createAudioCapture,
1414
+ createAudioPlayback,
1415
+ createProtocolState,
1416
+ createReconnectingWebSocket,
1417
+ handleServerMessage,
1418
+ joinRoom,
1419
+ parseIncomingCall,
1420
+ startTextSession
1421
+ });
1422
+ //# sourceMappingURL=browser.js.map