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