@nuancelabs/js-sdk 0.15.0 → 0.33.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -4,7 +4,7 @@ function isRecord(value) {
4
4
  }
5
5
 
6
6
  // src/core/sdk-version.ts
7
- var SDK_HEADER = `js/${"0.15.0"}`;
7
+ var SDK_HEADER = `js/${"0.33.2"}`;
8
8
 
9
9
  // src/utils.ts
10
10
  function normalizeHost(host) {
@@ -12,10 +12,13 @@ function normalizeHost(host) {
12
12
  }
13
13
 
14
14
  // src/api/client.ts
15
+ var NETWORK_RETRY_DELAYS_MS = [300, 1200];
16
+
15
17
  class ApiError extends Error {
16
18
  status;
17
19
  code;
18
20
  retryAfterSeconds;
21
+ renewsAt;
19
22
  body;
20
23
  constructor(status, detail, body = null) {
21
24
  super(detail);
@@ -24,6 +27,7 @@ class ApiError extends Error {
24
27
  this.body = body;
25
28
  this.code = isRecord(body) && typeof body.code === "string" ? body.code : null;
26
29
  this.retryAfterSeconds = isRecord(body) && typeof body.retry_after_seconds === "number" ? body.retry_after_seconds : null;
30
+ this.renewsAt = isRecord(body) && typeof body.renews_at === "string" ? body.renews_at : null;
27
31
  }
28
32
  }
29
33
 
@@ -31,18 +35,26 @@ class NuanceApiClient {
31
35
  host;
32
36
  headersProvider;
33
37
  tokenProvider;
38
+ networkRetryDelaysMs;
34
39
  tokens = new Map;
35
40
  constructor(config) {
36
41
  this.host = normalizeHost(config.host);
37
42
  this.headersProvider = config.headers;
38
43
  this.tokenProvider = config.token;
44
+ this.networkRetryDelaysMs = config.networkRetryDelaysMs ?? NETWORK_RETRY_DELAYS_MS;
39
45
  }
40
46
  async createConversation(request) {
41
47
  const token = this.tokenProvider ? await this.tokenProvider() : undefined;
42
48
  if (token !== undefined && (typeof token !== "string" || token === "")) {
43
49
  throw new TypeError(`The token option must resolve to the session token string, but it resolved to ${token === "" ? "an empty string" : typeof token}. Return the "token" field of your endpoint's response.`);
44
50
  }
45
- const conversation = await this.request("/conversation", { method: "POST", body: request, token });
51
+ const conversation = await this.request("/conversation", {
52
+ method: "POST",
53
+ body: request,
54
+ token,
55
+ page: pageHeaders(),
56
+ replaySafe: (headers) => !isApiKey(headers)
57
+ });
46
58
  if (token !== undefined)
47
59
  this.tokens.set(conversation.id, token);
48
60
  return conversation;
@@ -69,18 +81,21 @@ class NuanceApiClient {
69
81
  async request(path, options) {
70
82
  const headers = new Headers(await this.resolveHeaders());
71
83
  headers.set("X-Nuance-SDK", SDK_HEADER);
84
+ for (const [name, value] of Object.entries(options?.page ?? {})) {
85
+ headers.set(name, value);
86
+ }
72
87
  if (options?.token !== undefined) {
73
88
  headers.set("Authorization", `Bearer ${options.token}`);
74
89
  }
75
90
  if (options?.body !== undefined) {
76
91
  headers.set("Content-Type", "application/json");
77
92
  }
78
- const response = await fetch(`${this.host}${path}`, {
93
+ const response = await this.send(`${this.host}${path}`, {
79
94
  method: options?.method ?? "GET",
80
95
  headers,
81
96
  body: options?.body !== undefined ? JSON.stringify(options.body) : undefined,
82
97
  signal: options?.signal
83
- });
98
+ }, options?.replaySafe?.(headers) ?? true);
84
99
  if (!response.ok) {
85
100
  let detail = response.statusText;
86
101
  let body = null;
@@ -99,10 +114,51 @@ class NuanceApiClient {
99
114
  }
100
115
  return response.json();
101
116
  }
117
+ async send(url, init, replaySafe) {
118
+ for (let retry = 0;; retry++) {
119
+ try {
120
+ return await fetch(url, init);
121
+ } catch (error) {
122
+ const delayMs = this.networkRetryDelaysMs[retry];
123
+ if (!replaySafe || delayMs === undefined || !isNetworkFailure(error) || init.signal?.aborted) {
124
+ throw error;
125
+ }
126
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
127
+ if (init.signal?.aborted)
128
+ throw error;
129
+ }
130
+ }
131
+ }
102
132
  async resolveHeaders() {
103
133
  return this.headersProvider ? await this.headersProvider() : {};
104
134
  }
105
135
  }
136
+ function isNetworkFailure(error) {
137
+ return error instanceof TypeError;
138
+ }
139
+ function isApiKey(headers) {
140
+ return headers.get("Authorization")?.startsWith("Bearer nk_") ?? false;
141
+ }
142
+ function pageHeaders(location = globalThis.location, referrer = globalThis.document?.referrer) {
143
+ const headers = {};
144
+ const page = location && withoutQuery(location.href);
145
+ if (page)
146
+ headers["X-Nuance-Page"] = page;
147
+ const from = referrer && withoutQuery(referrer);
148
+ if (from)
149
+ headers["X-Nuance-Referrer"] = from;
150
+ return headers;
151
+ }
152
+ function withoutQuery(href) {
153
+ try {
154
+ const url = new URL(href);
155
+ if (url.protocol !== "http:" && url.protocol !== "https:")
156
+ return;
157
+ return `${url.origin}${url.pathname}`.slice(0, 512);
158
+ } catch {
159
+ return;
160
+ }
161
+ }
106
162
  // src/core/active-session-error.ts
107
163
  class ActiveSessionError extends Error {
108
164
  constructor() {
@@ -110,7 +166,6 @@ class ActiveSessionError extends Error {
110
166
  this.name = "ActiveSessionError";
111
167
  }
112
168
  }
113
-
114
169
  // src/core/guest-limit-error.ts
115
170
  class GuestLimitError extends Error {
116
171
  reason;
@@ -120,7 +175,6 @@ class GuestLimitError extends Error {
120
175
  this.reason = reason;
121
176
  }
122
177
  }
123
-
124
178
  // src/core/legal-required-error.ts
125
179
  class LegalRequiredError extends Error {
126
180
  constructor() {
@@ -128,7 +182,6 @@ class LegalRequiredError extends Error {
128
182
  this.name = "LegalRequiredError";
129
183
  }
130
184
  }
131
-
132
185
  // src/core/maintenance-error.ts
133
186
  class MaintenanceError extends Error {
134
187
  constructor(message) {
@@ -136,7 +189,6 @@ class MaintenanceError extends Error {
136
189
  this.name = "MaintenanceError";
137
190
  }
138
191
  }
139
-
140
192
  // src/core/media-error.ts
141
193
  function reasonFor(cause) {
142
194
  const name = cause instanceof Error ? cause.name : "";
@@ -177,24 +229,146 @@ async function ensureMicrophoneAccess(deviceId) {
177
229
  throw new MediaAccessError(error, "microphone");
178
230
  }
179
231
  }
180
-
181
232
  // src/core/policy-curfew-error.ts
182
233
  class PolicyCurfewError extends Error {
183
234
  retryAfterSeconds;
184
235
  constructor(retryAfterSeconds) {
185
- super("new sessions are paused after repeated policy violations");
236
+ super(curfewMessage(retryAfterSeconds));
186
237
  this.name = "PolicyCurfewError";
187
238
  this.retryAfterSeconds = retryAfterSeconds;
188
239
  }
189
240
  }
241
+ function curfewMessage(retryAfterSeconds) {
242
+ const paused = "New conversations are paused after repeated violations of our Acceptable Use Policy.";
243
+ if (retryAfterSeconds === null || !(retryAfterSeconds > 0)) {
244
+ return `${paused} Try again later.`;
245
+ }
246
+ const hours = Math.max(1, Math.ceil(retryAfterSeconds / 3600));
247
+ return `${paused} Try again in about ${hours} ${hours === 1 ? "hour" : "hours"}.`;
248
+ }
249
+ // src/core/types.ts
250
+ var ConversationState;
251
+ ((ConversationState2) => {
252
+ ConversationState2["Idle"] = "idle";
253
+ ConversationState2["Queued"] = "queued";
254
+ ConversationState2["Connecting"] = "connecting";
255
+ ConversationState2["Connected"] = "connected";
256
+ ConversationState2["Reconnecting"] = "reconnecting";
257
+ ConversationState2["Disconnected"] = "disconnected";
258
+ ConversationState2["Failed"] = "failed";
259
+ })(ConversationState ||= {});
260
+ // src/core/camera.ts
261
+ function cameraFacing(track) {
262
+ const facing = track.getSettings().facingMode;
263
+ return facing === "user" || facing === "environment" ? facing : null;
264
+ }
265
+ function requireCameraFacing(track, facing) {
266
+ const requested = typeof facing === "string" ? facing : !Array.isArray(facing) ? facing?.exact : undefined;
267
+ if (typeof requested !== "string")
268
+ return;
269
+ if (!track || track.getSettings().facingMode !== requested)
270
+ throw new DOMException("Requested camera direction is unavailable", "OverconstrainedError");
271
+ }
272
+ var BUILT_IN = /facetime|built-?in|integrated|internal|macbook|user[- ]facing|front[- ]facing/i;
273
+ var PHONE = /iphone|android|continuity|desk view/i;
274
+ function classifyCamera(label, phone, facing) {
275
+ if (phone)
276
+ return facing === "environment" ? "phone_back" : "phone_front";
277
+ if (!label)
278
+ return;
279
+ if (PHONE.test(label))
280
+ return "phone_back";
281
+ return BUILT_IN.test(label) ? "built_in" : "external";
282
+ }
283
+ function onPhone() {
284
+ return /Android|iPhone|iPad|iPod/i.test(navigator.userAgent) || navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1;
285
+ }
286
+ async function cameraSource(preferences) {
287
+ if (preferences.cameraEnabled === false)
288
+ return "off";
289
+ if (typeof navigator === "undefined")
290
+ return;
291
+ if (onPhone())
292
+ return classifyCamera("", true, preferences.cameraFacing);
293
+ try {
294
+ const cameras = (await navigator.mediaDevices.enumerateDevices()).filter((device) => device.kind === "videoinput");
295
+ const camera = cameras.find((device) => device.deviceId === preferences.cameraId) ?? cameras[0];
296
+ return camera ? classifyCamera(camera.label, false, null) : undefined;
297
+ } catch {
298
+ return;
299
+ }
300
+ }
190
301
 
191
- // src/core/session-events.ts
192
- function sessionEventsOf(type, handler) {
193
- return (change) => {
194
- if (change.type === "session-event" && change.event.type === type) {
195
- handler(change.event);
196
- }
197
- };
302
+ // src/core/dispatch.ts
303
+ function deliver(handler, event) {
304
+ try {
305
+ handler(event);
306
+ } catch (error) {
307
+ report(error);
308
+ }
309
+ }
310
+ function report(error) {
311
+ if (typeof globalThis.reportError === "function") {
312
+ globalThis.reportError(error);
313
+ return;
314
+ }
315
+ console.error("Nuance SDK: an event handler threw", error);
316
+ }
317
+
318
+ // src/core/end-reason.ts
319
+ var FROM_V1 = {
320
+ client_ended: "close_requested",
321
+ input_end: "close_requested",
322
+ time_limit: "expired",
323
+ max_pause: "expired",
324
+ policy_violation: "content",
325
+ age_check: "age_check",
326
+ agent_ended: "end_call",
327
+ runtime_error: "error",
328
+ capacity_lost: "error",
329
+ job_ended: "error",
330
+ pacer_failed: "error",
331
+ output_not_ready: "error",
332
+ client_gone: "connection_lost",
333
+ client_stalled: "connection_lost",
334
+ closed: "connection_lost"
335
+ };
336
+ var RETIRED = {
337
+ remote_hangup: "end_call"
338
+ };
339
+ var KNOWN = new Set([
340
+ "close_requested",
341
+ "content",
342
+ "age_check",
343
+ "expired",
344
+ "end_call",
345
+ "error",
346
+ "connection_lost"
347
+ ]);
348
+ function endReasonOf(reason) {
349
+ if (typeof reason !== "string" || reason === "")
350
+ return;
351
+ if (KNOWN.has(reason))
352
+ return reason;
353
+ return RETIRED[reason] ?? reason;
354
+ }
355
+ function v1EndReason(reason) {
356
+ if (typeof reason !== "string" || reason === "")
357
+ return;
358
+ if (KNOWN.has(reason))
359
+ return reason;
360
+ return FROM_V1[reason] ?? "error";
361
+ }
362
+
363
+ // src/core/resolvers.ts
364
+ function resolvers() {
365
+ let resolve;
366
+ let reject;
367
+ const promise = new Promise((settle, fail) => {
368
+ resolve = settle;
369
+ reject = fail;
370
+ });
371
+ return { promise, resolve, reject };
198
372
  }
199
373
 
200
374
  // src/core/ticker.ts
@@ -248,11 +422,172 @@ var pageTicker = backgroundTicker(globalThis);
248
422
  // src/providers/livekit/conversation.ts
249
423
  import {
250
424
  ConnectionQuality,
251
- ConnectionState as ConnectionState2,
425
+ ConnectionState as ConnectionState3,
426
+ ParticipantKind,
252
427
  Room,
253
- RoomEvent as RoomEvent3
428
+ RoomEvent as RoomEvent4
254
429
  } from "livekit-client";
255
430
 
431
+ // src/providers/livekit/attach.ts
432
+ function isAttach(participant) {
433
+ if (participant === undefined)
434
+ return false;
435
+ return participant.identity.startsWith("attach-") || participant.attributes?.["nuance.attach"] === "1";
436
+ }
437
+
438
+ // src/providers/livekit/audio-backlog.ts
439
+ import {
440
+ ConnectionState,
441
+ RoomEvent,
442
+ Track
443
+ } from "livekit-client";
444
+ var BACKLOG_MS = 300;
445
+ var AUDIO_BACKLOG_TIMING = {
446
+ sampleMs: 500,
447
+ stuckSamples: 2,
448
+ cooldownMs: 1e4,
449
+ maxCooldownMs: 120000
450
+ };
451
+
452
+ class AudioBacklogWatch {
453
+ read;
454
+ drop;
455
+ timing;
456
+ now;
457
+ timer = null;
458
+ previous = null;
459
+ stuck = 0;
460
+ droppedAt = null;
461
+ cooldown;
462
+ sampling = false;
463
+ constructor(read, drop, timing = AUDIO_BACKLOG_TIMING, now = () => performance.now()) {
464
+ this.read = read;
465
+ this.drop = drop;
466
+ this.timing = timing;
467
+ this.now = now;
468
+ this.cooldown = timing.cooldownMs;
469
+ }
470
+ start() {
471
+ if (this.timer !== null)
472
+ return;
473
+ this.timer = setInterval(() => {
474
+ this.sample();
475
+ }, this.timing.sampleMs);
476
+ }
477
+ stop() {
478
+ if (this.timer !== null) {
479
+ clearInterval(this.timer);
480
+ this.timer = null;
481
+ }
482
+ this.previous = null;
483
+ this.stuck = 0;
484
+ }
485
+ async sample() {
486
+ if (this.sampling || this.timer === null)
487
+ return;
488
+ this.sampling = true;
489
+ try {
490
+ const buffer = await this.read().catch(() => null);
491
+ if (this.timer === null)
492
+ return;
493
+ this.observe(buffer, this.now());
494
+ } finally {
495
+ this.sampling = false;
496
+ }
497
+ }
498
+ observe(buffer, now) {
499
+ const previous = this.previous;
500
+ this.previous = buffer;
501
+ if (buffer === null || previous === null || buffer.id !== previous.id) {
502
+ this.stuck = 0;
503
+ return;
504
+ }
505
+ const emitted = buffer.emitted - previous.emitted;
506
+ if (emitted <= 0)
507
+ return;
508
+ const backlog = {
509
+ jitterBufferMs: (buffer.delay - previous.delay) / emitted * 1000,
510
+ targetMs: (buffer.target - previous.target) / emitted * 1000
511
+ };
512
+ const behind = backlog.jitterBufferMs - backlog.targetMs > BACKLOG_MS;
513
+ this.stuck = behind ? this.stuck + 1 : 0;
514
+ if (this.stuck < this.timing.stuckSamples)
515
+ return;
516
+ if (this.droppedAt !== null) {
517
+ if (now - this.droppedAt < this.cooldown)
518
+ return;
519
+ this.cooldown = Math.min(this.cooldown * 2, this.timing.maxCooldownMs);
520
+ }
521
+ this.stuck = 0;
522
+ this.droppedAt = now;
523
+ this.previous = null;
524
+ this.drop(backlog);
525
+ }
526
+ }
527
+ function avatarAudio(room) {
528
+ for (const participant of room.remoteParticipants.values()) {
529
+ if (isAttach(participant))
530
+ continue;
531
+ for (const publication of participant.trackPublications.values()) {
532
+ if (publication.kind === Track.Kind.Audio && publication.isSubscribed) {
533
+ return publication;
534
+ }
535
+ }
536
+ }
537
+ return null;
538
+ }
539
+ function readJitterBuffer(report2) {
540
+ let buffer = null;
541
+ report2?.forEach((stat) => {
542
+ if (stat.type !== "inbound-rtp" || stat.kind !== "audio")
543
+ return;
544
+ const {
545
+ id,
546
+ jitterBufferDelay,
547
+ jitterBufferTargetDelay,
548
+ jitterBufferEmittedCount
549
+ } = stat;
550
+ if (typeof id === "string" && typeof jitterBufferDelay === "number" && typeof jitterBufferTargetDelay === "number" && typeof jitterBufferEmittedCount === "number") {
551
+ buffer = {
552
+ id,
553
+ delay: jitterBufferDelay,
554
+ target: jitterBufferTargetDelay,
555
+ emitted: jitterBufferEmittedCount
556
+ };
557
+ }
558
+ });
559
+ return buffer;
560
+ }
561
+ async function avatarJitterBuffer(room) {
562
+ const track = avatarAudio(room)?.track;
563
+ if (!track)
564
+ return null;
565
+ return readJitterBuffer(await track.getRTCStatsReport());
566
+ }
567
+ var UNSUBSCRIBE_WAIT_MS = 2000;
568
+ async function resubscribeAvatarAudio(room) {
569
+ const publication = avatarAudio(room);
570
+ if (!publication)
571
+ return;
572
+ await new Promise((resolve) => {
573
+ const done = () => {
574
+ clearTimeout(timer);
575
+ room.off(RoomEvent.TrackUnsubscribed, unsubscribed);
576
+ resolve();
577
+ };
578
+ const unsubscribed = (_track, gone) => {
579
+ if (gone === publication)
580
+ done();
581
+ };
582
+ const timer = setTimeout(done, UNSUBSCRIBE_WAIT_MS);
583
+ room.on(RoomEvent.TrackUnsubscribed, unsubscribed);
584
+ publication.setSubscribed(false);
585
+ });
586
+ if (room.state === ConnectionState.Disconnected)
587
+ return;
588
+ publication.setSubscribed(true);
589
+ }
590
+
256
591
  // src/providers/livekit/tracks.ts
257
592
  class LiveKitVideoTrack {
258
593
  track;
@@ -323,6 +658,9 @@ class LiveKitAvatarParticipant {
323
658
  constructor(participant) {
324
659
  this.participant = participant;
325
660
  }
661
+ get identity() {
662
+ return this.participant?.identity ?? null;
663
+ }
326
664
  updateParticipant(participant, keepReady = false) {
327
665
  if (!keepReady && (participant === null || this.participant !== null && participant !== this.participant)) {
328
666
  this._ready = false;
@@ -394,7 +732,7 @@ class LiveKitAvatarParticipant {
394
732
  }
395
733
 
396
734
  // src/providers/livekit/layer-fallback.ts
397
- import { Track, VideoQuality } from "livekit-client";
735
+ import { Track as Track2, VideoQuality } from "livekit-client";
398
736
  var LAYER_FALLBACK_TIMING = {
399
737
  sampleMs: 1000,
400
738
  slowFps: 12,
@@ -478,14 +816,16 @@ async function avatarVideoRate(room) {
478
816
  if (typeof document !== "undefined" && document.hidden)
479
817
  return null;
480
818
  for (const participant of room.remoteParticipants.values()) {
481
- const publication = participant.getTrackPublication(Track.Source.Camera);
819
+ if (isAttach(participant))
820
+ continue;
821
+ const publication = participant.getTrackPublication(Track2.Source.Camera);
482
822
  const track = publication?.track;
483
823
  if (!publication?.isSubscribed || !track)
484
824
  continue;
485
- const report = await track.getRTCStatsReport();
825
+ const report2 = await track.getRTCStatsReport();
486
826
  let width = null;
487
827
  let framesDecoded = null;
488
- report?.forEach((stat) => {
828
+ report2?.forEach((stat) => {
489
829
  if (stat.type === "inbound-rtp" && stat.kind === "video") {
490
830
  width = typeof stat.frameWidth === "number" ? stat.frameWidth : null;
491
831
  framesDecoded = typeof stat.framesDecoded === "number" ? stat.framesDecoded : null;
@@ -502,7 +842,9 @@ async function avatarVideoRate(room) {
502
842
  }
503
843
  function pinAvatarLayer(room, layer) {
504
844
  for (const participant of room.remoteParticipants.values()) {
505
- const publication = participant.getTrackPublication(Track.Source.Camera);
845
+ if (isAttach(participant))
846
+ continue;
847
+ const publication = participant.getTrackPublication(Track2.Source.Camera);
506
848
  if (!publication?.isSubscribed)
507
849
  continue;
508
850
  publication.setVideoQuality(layer === "low" ? VideoQuality.LOW : VideoQuality.HIGH);
@@ -512,10 +854,10 @@ function pinAvatarLayer(room, layer) {
512
854
 
513
855
  // src/providers/livekit/network-stats.ts
514
856
  import {
515
- ConnectionState,
857
+ ConnectionState as ConnectionState2,
516
858
  ParticipantEvent,
517
- RoomEvent,
518
- Track as Track2
859
+ RoomEvent as RoomEvent2,
860
+ Track as Track3
519
861
  } from "livekit-client";
520
862
 
521
863
  // src/providers/livekit/mic-silence.ts
@@ -582,9 +924,9 @@ function num(value) {
582
924
  function str(value) {
583
925
  return typeof value === "string" && value ? value : null;
584
926
  }
585
- function entries(report) {
927
+ function entries(report2) {
586
928
  const out = [];
587
- report.forEach((entry) => {
929
+ report2.forEach((entry) => {
588
930
  out.push(entry);
589
931
  });
590
932
  return out;
@@ -709,7 +1051,7 @@ function buildSample(reports, hidden) {
709
1051
  reports.avatarVideo,
710
1052
  reports.mic,
711
1053
  reports.camera
712
- ].map((report) => report ? entries(report) : null);
1054
+ ].map((report2) => report2 ? entries(report2) : null);
713
1055
  const byId = new Map;
714
1056
  for (const list of perReport) {
715
1057
  if (list)
@@ -728,7 +1070,7 @@ function buildSample(reports, hidden) {
728
1070
  }
729
1071
  const inboundOf = (list) => list ? digestInbound(list.find((entry) => entry.type === "inbound-rtp"), byId) : null;
730
1072
  const outboundOf = (list) => list ? digestOutbound(list.find((entry) => entry.type === "outbound-rtp" && entry.active !== false), byId) : null;
731
- const avatarAudio = inboundOf(perReport[0]);
1073
+ const avatarAudio2 = inboundOf(perReport[0]);
732
1074
  const avatarVideo = inboundOf(perReport[1]);
733
1075
  const mic = outboundOf(perReport[2]);
734
1076
  if (mic && reports.micTrack) {
@@ -736,17 +1078,18 @@ function buildSample(reports, hidden) {
736
1078
  mic.track_ready_state = reports.micTrack.readyState;
737
1079
  mic.track_enabled = reports.micTrack.enabled;
738
1080
  }
739
- const audioPlayout = avatarAudio?.estimated_playout_timestamp_ms ?? null;
1081
+ const audioPlayout = avatarAudio2?.estimated_playout_timestamp_ms ?? null;
740
1082
  const videoPlayout = avatarVideo?.estimated_playout_timestamp_ms ?? null;
741
1083
  return {
742
1084
  ts: Date.now() / 1000,
743
1085
  hidden,
744
1086
  ...link,
745
1087
  av_offset_ms: audioPlayout != null && videoPlayout != null ? audioPlayout - videoPlayout : null,
746
- avatar_audio: avatarAudio,
1088
+ avatar_audio: avatarAudio2,
747
1089
  avatar_video: avatarVideo,
748
1090
  mic,
749
- camera: outboundOf(perReport[3])
1091
+ camera: outboundOf(perReport[3]),
1092
+ avatar_audio_drop: null
750
1093
  };
751
1094
  }
752
1095
  function environment() {
@@ -775,6 +1118,7 @@ class NetworkStatsReporter {
775
1118
  timer = null;
776
1119
  seq = 0;
777
1120
  pending = [];
1121
+ audioDrop = null;
778
1122
  stopped = false;
779
1123
  onVisibility = () => {
780
1124
  if (document.visibilityState === "hidden")
@@ -787,7 +1131,7 @@ class NetworkStatsReporter {
787
1131
  this.resetMic();
788
1132
  };
789
1133
  onMicMute = (publication) => {
790
- if (publication.source === Track2.Source.Microphone)
1134
+ if (publication.source === Track3.Source.Microphone)
791
1135
  this.resetMic();
792
1136
  };
793
1137
  constructor(room, onMicSilent = () => {}) {
@@ -803,9 +1147,17 @@ class NetworkStatsReporter {
803
1147
  }, SAMPLE_MS);
804
1148
  document.addEventListener("visibilitychange", this.onVisibility);
805
1149
  window.addEventListener("pagehide", this.onPageHide);
806
- this.room.on(RoomEvent.Reconnecting, this.onLinkChange).on(RoomEvent.Reconnected, this.onLinkChange).on(RoomEvent.Disconnected, this.onLinkChange);
1150
+ this.room.on(RoomEvent2.Reconnecting, this.onLinkChange).on(RoomEvent2.Reconnected, this.onLinkChange).on(RoomEvent2.Disconnected, this.onLinkChange);
807
1151
  this.room.localParticipant.on(ParticipantEvent.TrackMuted, this.onMicMute).on(ParticipantEvent.TrackUnmuted, this.onMicMute);
808
1152
  }
1153
+ noteAudioDrop(backlog) {
1154
+ const round = (ms) => Math.round(ms);
1155
+ this.audioDrop = {
1156
+ jitter_buffer_ms: round(backlog.jitterBufferMs),
1157
+ target_ms: round(backlog.targetMs),
1158
+ behind_ms: round(backlog.jitterBufferMs - backlog.targetMs)
1159
+ };
1160
+ }
809
1161
  stop() {
810
1162
  if (!this.teardown())
811
1163
  return Promise.resolve();
@@ -825,7 +1177,7 @@ class NetworkStatsReporter {
825
1177
  }
826
1178
  document.removeEventListener("visibilitychange", this.onVisibility);
827
1179
  window.removeEventListener("pagehide", this.onPageHide);
828
- this.room.off(RoomEvent.Reconnecting, this.onLinkChange).off(RoomEvent.Reconnected, this.onLinkChange).off(RoomEvent.Disconnected, this.onLinkChange);
1180
+ this.room.off(RoomEvent2.Reconnecting, this.onLinkChange).off(RoomEvent2.Reconnected, this.onLinkChange).off(RoomEvent2.Disconnected, this.onLinkChange);
829
1181
  this.room.localParticipant.off(ParticipantEvent.TrackMuted, this.onMicMute).off(ParticipantEvent.TrackUnmuted, this.onMicMute);
830
1182
  this.resetMic();
831
1183
  return true;
@@ -838,6 +1190,8 @@ class NetworkStatsReporter {
838
1190
  }
839
1191
  remoteTrack(source) {
840
1192
  for (const participant of this.room.remoteParticipants.values()) {
1193
+ if (isAttach(participant))
1194
+ continue;
841
1195
  const publication = participant.getTrackPublication(source);
842
1196
  if (publication?.isSubscribed && hasStats(publication.track)) {
843
1197
  return publication.track;
@@ -862,20 +1216,22 @@ class NetworkStatsReporter {
862
1216
  if (this.stopped)
863
1217
  return;
864
1218
  try {
865
- const [avatarAudio, avatarVideo, mic, camera] = await Promise.all([
866
- this.report(this.remoteTrack(Track2.Source.Microphone)),
867
- this.report(this.remoteTrack(Track2.Source.Camera)),
868
- this.report(this.localTrack(Track2.Source.Microphone)),
869
- this.report(this.localTrack(Track2.Source.Camera))
1219
+ const [avatarAudio2, avatarVideo, mic, camera] = await Promise.all([
1220
+ this.report(this.remoteTrack(Track3.Source.Microphone)),
1221
+ this.report(this.remoteTrack(Track3.Source.Camera)),
1222
+ this.report(this.localTrack(Track3.Source.Microphone)),
1223
+ this.report(this.localTrack(Track3.Source.Camera))
870
1224
  ]);
871
1225
  if (this.stopped)
872
1226
  return;
873
- const micPublication = this.room.localParticipant.getTrackPublication(Track2.Source.Microphone);
1227
+ const micPublication = this.room.localParticipant.getTrackPublication(Track3.Source.Microphone);
874
1228
  const micTrack = micPublication?.track?.mediaStreamTrack ?? null;
875
- const sample = avatarAudio || avatarVideo || mic || camera ? buildSample({ avatarAudio, avatarVideo, mic, camera, micTrack }, document.hidden) : null;
1229
+ const sample = avatarAudio2 || avatarVideo || mic || camera ? buildSample({ avatarAudio: avatarAudio2, avatarVideo, mic, camera, micTrack }, document.hidden) : null;
876
1230
  this.watchMic(!!micPublication && !micPublication.isMuted && micTrack !== null, micTrack, mic ? sample?.mic?.packets_sent ?? 0 : null);
877
1231
  if (!sample)
878
1232
  return;
1233
+ sample.avatar_audio_drop = this.audioDrop;
1234
+ this.audioDrop = null;
879
1235
  this.pending.push(sample);
880
1236
  if (this.pending.length >= BATCH)
881
1237
  this.flush();
@@ -885,7 +1241,7 @@ class NetworkStatsReporter {
885
1241
  const was = this.micSilence.silent;
886
1242
  const silent = this.micSilence.observe({
887
1243
  at: Date.now(),
888
- connected: this.room.state === ConnectionState.Connected,
1244
+ connected: this.room.state === ConnectionState2.Connected,
889
1245
  live,
890
1246
  packetsSent,
891
1247
  readyState: track?.readyState ?? null,
@@ -911,8 +1267,90 @@ class NetworkStatsReporter {
911
1267
  }
912
1268
  }
913
1269
 
1270
+ // src/providers/livekit/parts.ts
1271
+ var PART_TYPE = "message.part";
1272
+ var PARTS_MAX = 64;
1273
+ var TEXT_MAX = 1024 * 1024;
1274
+ var DROPPED_KEPT = 16;
1275
+
1276
+ class Parts {
1277
+ onDrop;
1278
+ open = null;
1279
+ dropped = new Set;
1280
+ constructor(onDrop) {
1281
+ this.onDrop = onDrop;
1282
+ }
1283
+ take(packet) {
1284
+ if (packet === null) {
1285
+ this.cut("a packet that is no event arrived between its parts");
1286
+ return null;
1287
+ }
1288
+ if (packet.type !== PART_TYPE) {
1289
+ this.cut("another event arrived between its parts");
1290
+ return packet;
1291
+ }
1292
+ const { id, index, count, text } = packet;
1293
+ if (typeof id !== "string" || typeof text !== "string" || typeof index !== "number" || typeof count !== "number" || !Number.isInteger(index) || !Number.isInteger(count) || count < 1 || count > PARTS_MAX || index < 0 || index >= count) {
1294
+ this.cut("a part broke the framing");
1295
+ if (typeof id === "string")
1296
+ this.report(id, "a part broke the framing");
1297
+ return null;
1298
+ }
1299
+ if (index === 0) {
1300
+ this.cut("a new event began before its last part");
1301
+ this.open = { id, count, next: 0, text: "" };
1302
+ }
1303
+ const open = this.open;
1304
+ if (open === null) {
1305
+ this.report(id, "its first part never arrived");
1306
+ return null;
1307
+ }
1308
+ if (open.id !== id || open.next !== index || open.count !== count) {
1309
+ this.cut("a part arrived out of turn");
1310
+ this.report(id, "a part arrived out of turn");
1311
+ return null;
1312
+ }
1313
+ open.text += text;
1314
+ open.next += 1;
1315
+ if (open.text.length > TEXT_MAX) {
1316
+ this.cut("its parts carried more than 1 MiB");
1317
+ return null;
1318
+ }
1319
+ if (open.next < open.count)
1320
+ return null;
1321
+ this.open = null;
1322
+ try {
1323
+ const event = JSON.parse(open.text);
1324
+ if (isRecord(event))
1325
+ return event;
1326
+ } catch {}
1327
+ this.report(open.id, "its parts joined to no JSON object");
1328
+ return null;
1329
+ }
1330
+ clear() {
1331
+ this.open = null;
1332
+ this.dropped.clear();
1333
+ }
1334
+ cut(why) {
1335
+ if (this.open === null)
1336
+ return;
1337
+ const { id } = this.open;
1338
+ this.open = null;
1339
+ this.report(id, why);
1340
+ }
1341
+ report(id, why) {
1342
+ if (this.dropped.has(id))
1343
+ return;
1344
+ this.dropped.add(id);
1345
+ if (this.dropped.size > DROPPED_KEPT) {
1346
+ this.dropped.delete(this.dropped.values().next().value);
1347
+ }
1348
+ this.onDrop(id, why);
1349
+ }
1350
+ }
1351
+
914
1352
  // src/providers/livekit/presentation.ts
915
- import { Track as Track3 } from "livekit-client";
1353
+ import { Track as Track4 } from "livekit-client";
916
1354
  var PRESENTATION_TIMING = {
917
1355
  sampleMs: 250,
918
1356
  settleMs: 3000,
@@ -974,14 +1412,16 @@ class PresentationWatch {
974
1412
  }
975
1413
  async function avatarVideoSize(room) {
976
1414
  for (const participant of room.remoteParticipants.values()) {
977
- const publication = participant.getTrackPublication(Track3.Source.Camera);
1415
+ if (isAttach(participant))
1416
+ continue;
1417
+ const publication = participant.getTrackPublication(Track4.Source.Camera);
978
1418
  const track = publication?.track;
979
1419
  if (!publication?.isSubscribed || !track)
980
1420
  continue;
981
1421
  const publishedWidth = publication.dimensions?.width ?? null;
982
- const report = await track.getRTCStatsReport();
1422
+ const report2 = await track.getRTCStatsReport();
983
1423
  let width = null;
984
- report?.forEach((stat) => {
1424
+ report2?.forEach((stat) => {
985
1425
  if (stat.type === "inbound-rtp" && stat.kind === "video") {
986
1426
  width = typeof stat.frameWidth === "number" ? stat.frameWidth : null;
987
1427
  }
@@ -992,7 +1432,7 @@ async function avatarVideoSize(room) {
992
1432
  }
993
1433
 
994
1434
  // src/providers/livekit/ready-fallback.ts
995
- import { Track as Track4 } from "livekit-client";
1435
+ import { Track as Track5 } from "livekit-client";
996
1436
  var READY_FALLBACK_TIMING = {
997
1437
  sampleMs: 500,
998
1438
  delayMs: 5000,
@@ -1055,13 +1495,15 @@ class ReadyFallback {
1055
1495
  }
1056
1496
  async function avatarFramesDecoded(room) {
1057
1497
  for (const participant of room.remoteParticipants.values()) {
1058
- const publication = participant.getTrackPublication(Track4.Source.Camera);
1498
+ if (isAttach(participant))
1499
+ continue;
1500
+ const publication = participant.getTrackPublication(Track5.Source.Camera);
1059
1501
  const track = publication?.track;
1060
1502
  if (!publication?.isSubscribed || !track)
1061
1503
  continue;
1062
- const report = await track.getRTCStatsReport();
1504
+ const report2 = await track.getRTCStatsReport();
1063
1505
  let frames = null;
1064
- report?.forEach((stat) => {
1506
+ report2?.forEach((stat) => {
1065
1507
  if (stat.type === "inbound-rtp" && stat.kind === "video") {
1066
1508
  frames = typeof stat.framesDecoded === "number" ? stat.framesDecoded : null;
1067
1509
  }
@@ -1071,69 +1513,6 @@ async function avatarFramesDecoded(room) {
1071
1513
  return null;
1072
1514
  }
1073
1515
 
1074
- // src/providers/livekit/sequence.ts
1075
- class EventSequence {
1076
- deliver;
1077
- replay;
1078
- last = 0;
1079
- held = new Map;
1080
- replaying = false;
1081
- stopped = false;
1082
- constructor(deliver, replay) {
1083
- this.deliver = deliver;
1084
- this.replay = replay;
1085
- }
1086
- repeats(seq) {
1087
- return seq !== 0 && (seq <= this.last || this.held.has(seq));
1088
- }
1089
- accept(seq, item) {
1090
- if (this.stopped || this.repeats(seq))
1091
- return;
1092
- if (seq === 0) {
1093
- this.deliver(item);
1094
- return;
1095
- }
1096
- if (seq !== this.last + 1) {
1097
- this.held.set(seq, item);
1098
- this.requestReplay();
1099
- return;
1100
- }
1101
- this.last = seq;
1102
- this.deliver(item);
1103
- this.release(false);
1104
- }
1105
- catchUp() {
1106
- if (!this.stopped)
1107
- this.requestReplay();
1108
- }
1109
- stop() {
1110
- this.stopped = true;
1111
- this.held.clear();
1112
- }
1113
- requestReplay() {
1114
- if (this.replaying)
1115
- return;
1116
- this.replaying = true;
1117
- this.replay(this.last).catch(() => {
1118
- return;
1119
- }).finally(() => {
1120
- this.replaying = false;
1121
- this.release(true);
1122
- });
1123
- }
1124
- release(all) {
1125
- const pending = [...this.held.keys()].sort((a, b) => a - b);
1126
- for (const seq of pending) {
1127
- if (this.stopped || !all && seq !== this.last + 1)
1128
- return;
1129
- const item = this.held.get(seq);
1130
- this.held.delete(seq);
1131
- this.last = seq;
1132
- this.deliver(item);
1133
- }
1134
- }
1135
- }
1136
-
1137
1516
  // src/providers/livekit/signals.ts
1138
1517
  function decode(payload) {
1139
1518
  try {
@@ -1143,9 +1522,37 @@ function decode(payload) {
1143
1522
  return null;
1144
1523
  }
1145
1524
  }
1525
+ function legacyReason(cause) {
1526
+ if (cause === "content")
1527
+ return "policy-violation";
1528
+ if (cause === "age_check")
1529
+ return "age-check";
1530
+ if (cause === "expired")
1531
+ return "time-limit";
1532
+ if (cause === "connection_lost")
1533
+ return "connection-lost";
1534
+ return "ended";
1535
+ }
1536
+ function closedEnd(decoded) {
1537
+ if (decoded.type !== "session.closed")
1538
+ return null;
1539
+ const cause = endReasonOf(decoded.reason);
1540
+ const ended = {
1541
+ reason: legacyReason(cause),
1542
+ byClient: cause === "close_requested"
1543
+ };
1544
+ if (cause !== undefined)
1545
+ ended.endReason = cause;
1546
+ if (typeof decoded.message === "string" && decoded.message !== "") {
1547
+ ended.message = decoded.message;
1548
+ }
1549
+ return ended;
1550
+ }
1146
1551
  function endReason(reason) {
1147
1552
  if (reason === "policy_violation")
1148
1553
  return "policy-violation";
1554
+ if (reason === "age_check")
1555
+ return "age-check";
1149
1556
  if (reason === "time_limit")
1150
1557
  return "time-limit";
1151
1558
  if (reason === "invalid_spec" || reason === "runtime_failed") {
@@ -1153,105 +1560,21 @@ function endReason(reason) {
1153
1560
  }
1154
1561
  return "ended";
1155
1562
  }
1156
- function parseSessionEvent(payload) {
1157
- const decoded = decode(payload);
1158
- if (!decoded)
1159
- return null;
1160
- const seq = Number.isSafeInteger(decoded.seq) && decoded.seq > 0 ? decoded.seq : 0;
1161
- return { seq, event: protocolEvent(decoded, seq) };
1162
- }
1163
- function protocolEvent(decoded, seq) {
1164
- if (decoded.type === "session.output_ready") {
1165
- return { type: "output-ready" };
1166
- }
1167
- if (decoded.type === "command.result") {
1168
- return {
1169
- type: "command-result",
1170
- id: typeof decoded.id === "string" ? decoded.id : null,
1171
- result: {
1172
- status: decoded.status === "applied" ? "applied" : "rejected",
1173
- error: typeof decoded.error === "string" ? decoded.error : null
1174
- }
1175
- };
1176
- }
1177
- if (decoded.type === "tool.call" && typeof decoded.call_id === "string" && typeof decoded.name === "string" && isRecord(decoded.arguments)) {
1178
- return {
1179
- type: "tool-call",
1180
- call: {
1181
- callId: decoded.call_id,
1182
- name: decoded.name,
1183
- label: typeof decoded.label === "string" ? decoded.label : "",
1184
- arguments: decoded.arguments
1185
- }
1186
- };
1187
- }
1188
- if (decoded.type === "session.status" && decoded.status === "ended") {
1189
- return {
1190
- type: "ended",
1191
- reason: endReason(decoded.reason),
1192
- byClient: decoded.reason === "client_ended"
1193
- };
1194
- }
1195
- if (decoded.type === "error" && decoded.fatal === true) {
1196
- const ended = {
1197
- type: "ended",
1198
- reason: endReason(decoded.code),
1199
- byClient: false
1200
- };
1201
- if (typeof decoded.message === "string" && decoded.message !== "") {
1202
- ended.message = decoded.message;
1203
- }
1204
- return ended;
1205
- }
1206
- if (decoded.type === "error" && decoded.code === "bad_command") {
1207
- return { type: "command-unreadable" };
1208
- }
1209
- const event = sessionEvent(decoded, seq);
1210
- return event ? { type: "session-event", event } : { type: "other" };
1211
- }
1212
- var isString = (value) => typeof value === "string";
1213
- function defaulted(value, absent) {
1214
- if (value === undefined)
1215
- return absent;
1216
- return isString(value) ? value : null;
1217
- }
1218
- var READERS = {
1219
- "user.transcript": (wire) => {
1220
- const source = defaulted(wire.source, "asr");
1221
- return isString(wire.text) && source !== null ? { text: wire.text, source } : null;
1222
- },
1223
- "user.speech.started": () => ({}),
1224
- "user.speech.ended": () => ({}),
1225
- "user.turn.ended": (wire) => isString(wire.text) ? { text: wire.text } : null,
1226
- "agent.turn.started": (wire) => {
1227
- const source = defaulted(wire.source, "reply");
1228
- return isString(wire.turn_id) && isString(wire.opened_by) && source !== null ? { turnId: wire.turn_id, openedBy: wire.opened_by, source } : null;
1229
- },
1230
- "agent.speech.started": (wire) => isString(wire.turn_id) ? { turnId: wire.turn_id } : null,
1231
- "agent.text": (wire) => {
1232
- const turnId = wire.turn_id ?? null;
1233
- return (turnId === null || isString(turnId)) && isString(wire.text) ? { turnId, text: wire.text } : null;
1234
- },
1235
- "agent.turn.ended": (wire) => isString(wire.turn_id) && isString(wire.reason) && isString(wire.spoken) ? { turnId: wire.turn_id, reason: wire.reason, spoken: wire.spoken } : null,
1236
- floor: (wire) => isString(wire.state) ? { state: wire.state } : null
1237
- };
1238
- function sessionEvent(decoded, seq) {
1239
- const type = decoded.type;
1240
- if (!isString(type) || !Object.hasOwn(READERS, type))
1241
- return null;
1242
- const fields = READERS[type](decoded);
1243
- if (fields === null)
1244
- return null;
1245
- const atMs = Number.isFinite(decoded.at_ms) ? decoded.at_ms : null;
1246
- return { type, seq, atMs, ...fields };
1247
- }
1248
1563
  function parseRuntimeSignal(payload) {
1249
1564
  const decoded = decode(payload);
1250
1565
  if (decoded?.type === "output_ready") {
1251
1566
  return { type: "output-ready" };
1252
1567
  }
1253
1568
  if (decoded?.type === "session_ended") {
1254
- return { type: "session-ended", reason: endReason(decoded.reason) };
1569
+ const reason = endReason(decoded.reason);
1570
+ const signal = { type: "session-ended", reason };
1571
+ const cause = reason === "refused" ? undefined : v1EndReason(decoded.reason);
1572
+ if (cause !== undefined)
1573
+ signal.endReason = cause;
1574
+ if (typeof decoded.message === "string" && decoded.message !== "") {
1575
+ signal.message = decoded.message;
1576
+ }
1577
+ return signal;
1255
1578
  }
1256
1579
  if (decoded?.type === "link_poor") {
1257
1580
  return { type: "link-poor" };
@@ -1271,6 +1594,10 @@ function clockSyncPong(payload, receivedAt, now) {
1271
1594
  };
1272
1595
  return new TextEncoder().encode(JSON.stringify(reply));
1273
1596
  }
1597
+ function parseInterfaceEvent(payload) {
1598
+ const decoded = decode(payload);
1599
+ return Array.isArray(decoded) ? null : decoded;
1600
+ }
1274
1601
  function parseDataEvent(payload) {
1275
1602
  const decoded = decode(payload);
1276
1603
  if (!decoded)
@@ -1293,38 +1620,14 @@ function parseDataEvent(payload) {
1293
1620
  import {
1294
1621
  AudioPresets,
1295
1622
  ParticipantEvent as ParticipantEvent2,
1296
- RoomEvent as RoomEvent2,
1297
- Track as Track5
1623
+ RoomEvent as RoomEvent3,
1624
+ Track as Track6
1298
1625
  } from "livekit-client";
1299
-
1300
- // src/core/camera.ts
1301
- function cameraFacing(track) {
1302
- const facing = track.getSettings().facingMode;
1303
- return facing === "user" || facing === "environment" ? facing : null;
1304
- }
1305
- function requireCameraFacing(track, facing) {
1306
- const requested = typeof facing === "string" ? facing : !Array.isArray(facing) ? facing?.exact : undefined;
1307
- if (typeof requested !== "string")
1308
- return;
1309
- if (!track || track.getSettings().facingMode !== requested)
1310
- throw new DOMException("Requested camera direction is unavailable", "OverconstrainedError");
1311
- }
1312
-
1313
- // src/providers/livekit/user-participant.ts
1314
1626
  function microphonePublishOptions(resilient) {
1315
1627
  if (!resilient)
1316
1628
  return;
1317
1629
  return { dtx: false, audioPreset: AudioPresets.speech };
1318
1630
  }
1319
- var SCREEN_CAPTURE = {
1320
- audio: false,
1321
- resolution: { width: 1920, height: 1080, frameRate: 2 },
1322
- contentHint: "detail"
1323
- };
1324
- var SCREEN_PUBLISH = {
1325
- screenShareEncoding: { maxBitrate: 1500000, maxFramerate: 2 },
1326
- simulcast: false
1327
- };
1328
1631
 
1329
1632
  class LiveKitUserParticipant {
1330
1633
  room;
@@ -1332,8 +1635,6 @@ class LiveKitUserParticipant {
1332
1635
  onSpeakingChanged;
1333
1636
  onMicrophoneChanged;
1334
1637
  onCameraChanged;
1335
- onScreenShareChanged;
1336
- screenTrack;
1337
1638
  inputs = {
1338
1639
  audioinput: { changing: false, unavailable: false },
1339
1640
  videoinput: { changing: false, unavailable: false }
@@ -1348,7 +1649,7 @@ class LiveKitUserParticipant {
1348
1649
  lastAudioSource = null;
1349
1650
  _isSpeaking = false;
1350
1651
  speakingHandler = null;
1351
- constructor(room, onTracksChanged, onSpeakingChanged, onMicrophoneChanged, onCameraChanged = () => {}, initialCameraFacing, resilient = false, onScreenShareChanged = () => {}) {
1652
+ constructor(room, onTracksChanged, onSpeakingChanged, onMicrophoneChanged, onCameraChanged = () => {}, initialCameraFacing, resilient = false) {
1352
1653
  this.room = room;
1353
1654
  this.onTracksChanged = onTracksChanged;
1354
1655
  this.onSpeakingChanged = onSpeakingChanged;
@@ -1356,13 +1657,11 @@ class LiveKitUserParticipant {
1356
1657
  this.onCameraChanged = onCameraChanged;
1357
1658
  this.initialCameraFacing = initialCameraFacing;
1358
1659
  this.microphoneOptions = microphonePublishOptions(resilient);
1359
- this.onScreenShareChanged = onScreenShareChanged;
1360
- this.room.on(RoomEvent2.ActiveDeviceChanged, this.handleDeviceChanged);
1361
- this.room.localParticipant.on(ParticipantEvent2.LocalTrackUnpublished, this.handleTrackUnpublished);
1660
+ this.room.on(RoomEvent3.ActiveDeviceChanged, this.handleDeviceChanged);
1362
1661
  }
1363
1662
  updateTracks() {
1364
1663
  let changed = false;
1365
- const videoPub = this.room.localParticipant.getTrackPublication(Track5.Source.Camera);
1664
+ const videoPub = this.room.localParticipant.getTrackPublication(Track6.Source.Camera);
1366
1665
  const videoSource = videoPub?.track ?? null;
1367
1666
  if (videoSource !== this.lastVideoSource) {
1368
1667
  this._videoTrack?.detach();
@@ -1370,7 +1669,7 @@ class LiveKitUserParticipant {
1370
1669
  this.lastVideoSource = videoSource;
1371
1670
  changed = true;
1372
1671
  }
1373
- const audioPub = this.room.localParticipant.getTrackPublication(Track5.Source.Microphone);
1672
+ const audioPub = this.room.localParticipant.getTrackPublication(Track6.Source.Microphone);
1374
1673
  const audioSource = audioPub?.track ?? null;
1375
1674
  if (audioSource !== this.lastAudioSource) {
1376
1675
  this._audioTrack?.detach();
@@ -1452,7 +1751,7 @@ class LiveKitUserParticipant {
1452
1751
  }
1453
1752
  }
1454
1753
  inputTrack(kind) {
1455
- const publication = this.room.localParticipant.getTrackPublication(kind === "audioinput" ? Track5.Source.Microphone : Track5.Source.Camera);
1754
+ const publication = this.room.localParticipant.getTrackPublication(kind === "audioinput" ? Track6.Source.Microphone : Track6.Source.Camera);
1456
1755
  return kind === "audioinput" ? publication?.audioTrack : publication?.videoTrack;
1457
1756
  }
1458
1757
  async switchInput(kind, selection) {
@@ -1519,14 +1818,9 @@ class LiveKitUserParticipant {
1519
1818
  this.notifyInput(kind);
1520
1819
  }
1521
1820
  };
1522
- handleTrackUnpublished = (publication) => {
1523
- if (!this.disposed && publication.source === Track5.Source.ScreenShare)
1524
- this.onScreenShareChanged();
1525
- };
1526
1821
  cleanup() {
1527
1822
  this.disposed = true;
1528
- this.room.off(RoomEvent2.ActiveDeviceChanged, this.handleDeviceChanged);
1529
- this.room.localParticipant.off(ParticipantEvent2.LocalTrackUnpublished, this.handleTrackUnpublished);
1823
+ this.room.off(RoomEvent3.ActiveDeviceChanged, this.handleDeviceChanged);
1530
1824
  if (this.inputs.audioinput.changing)
1531
1825
  this.inputTrack("audioinput")?.stop();
1532
1826
  if (this.inputs.videoinput.changing)
@@ -1604,44 +1898,10 @@ class LiveKitUserParticipant {
1604
1898
  get isVideoEnabled() {
1605
1899
  return this.room.localParticipant.isCameraEnabled;
1606
1900
  }
1607
- async startScreenShare() {
1608
- await this.setScreenShare(true);
1609
- }
1610
- async stopScreenShare() {
1611
- const track = this.screenTrack;
1612
- this.screenTrack = undefined;
1613
- try {
1614
- if (!this.disposed)
1615
- await this.setScreenShare(false);
1616
- } finally {
1617
- track?.stop();
1618
- }
1619
- }
1620
- get isScreenSharing() {
1621
- return this.room.localParticipant.isScreenShareEnabled;
1622
- }
1623
- async setScreenShare(enabled) {
1624
- if (this.disposed)
1625
- throw new Error("Participant is disconnected");
1626
- try {
1627
- const publication = await this.room.localParticipant.setScreenShareEnabled(enabled, SCREEN_CAPTURE, SCREEN_PUBLISH);
1628
- if (this.disposed)
1629
- publication?.track?.stop();
1630
- else if (enabled)
1631
- this.screenTrack = publication?.track;
1632
- } catch (error) {
1633
- throw new MediaAccessError(error);
1634
- } finally {
1635
- if (!this.disposed)
1636
- this.onScreenShareChanged();
1637
- }
1638
- }
1639
1901
  releaseDevices() {
1640
1902
  for (const pub of this.room.localParticipant.trackPublications.values()) {
1641
1903
  pub.track?.stop();
1642
1904
  }
1643
- this.screenTrack?.stop();
1644
- this.screenTrack = undefined;
1645
1905
  }
1646
1906
  startSpeakingDetection() {
1647
1907
  this.stopSpeakingDetection();
@@ -1667,37 +1927,31 @@ class LiveKitUserParticipant {
1667
1927
 
1668
1928
  // src/providers/livekit/conversation.ts
1669
1929
  var DATA_EVENT_TOPIC = "nuance.data";
1670
- var SESSION_TOPIC = "nuance.v1";
1671
- var PACKET_MAX_BYTES = 14000;
1930
+ var INTERFACE_EVENT_MAX_BYTES = 64000;
1672
1931
  var RUNTIME_TOPIC = "runtime";
1673
1932
  var CLOCK_SYNC_TOPIC = "clock-sync";
1674
1933
  var STATS_FLUSH_TIMEOUT_MS = 300;
1675
- var SESSION_END_TIMEOUT_MS = 300;
1676
- var SESSION_END_ID = "client-session-end";
1934
+ var SESSION_CLOSE_TIMEOUT_MS = 300;
1935
+ var SESSION_TOPIC = "nuance.v1";
1936
+ var READY_ID = "client-ready";
1937
+ var INTERFACE_CLOSE_ID = "client-disconnect";
1677
1938
  var REFUSAL_REASON_WAIT_MS = 500;
1678
- var COMMAND_TIMEOUT_MS = 5000;
1679
- var STREAM_TIMEOUT_MS = 30000;
1680
- var BACKGROUND_ID = /^[a-z0-9][a-z0-9_-]{0,63}$/;
1681
1939
  var AVATAR_RETURN_TIMEOUT_MS = 8000;
1682
1940
  var RUNTIME_RETURN_TIMEOUT_MS = 20000;
1683
1941
  var RUNTIME_RECHECK_MS = 1000;
1684
- function contextItem(item) {
1685
- const name = item.name === undefined ? {} : { name: item.name };
1686
- return item.type === "text" ? { type: "text", text: item.text, ...name } : { type: "image", media_type: item.mediaType, data: item.data, ...name };
1687
- }
1688
1942
  function isReconnecting(lkState) {
1689
- return lkState === ConnectionState2.Reconnecting || lkState === ConnectionState2.SignalReconnecting;
1943
+ return lkState === ConnectionState3.Reconnecting || lkState === ConnectionState3.SignalReconnecting;
1690
1944
  }
1691
1945
  function mapConnectionState(lkState, resilient = false) {
1692
1946
  switch (lkState) {
1693
- case ConnectionState2.Connected:
1947
+ case ConnectionState3.Connected:
1694
1948
  return "connected";
1695
- case ConnectionState2.Connecting:
1949
+ case ConnectionState3.Connecting:
1696
1950
  return "connecting";
1697
- case ConnectionState2.Reconnecting:
1698
- case ConnectionState2.SignalReconnecting:
1951
+ case ConnectionState3.Reconnecting:
1952
+ case ConnectionState3.SignalReconnecting:
1699
1953
  return resilient ? "reconnecting" : "connecting";
1700
- case ConnectionState2.Disconnected:
1954
+ case ConnectionState3.Disconnected:
1701
1955
  return "disconnected";
1702
1956
  }
1703
1957
  }
@@ -1726,27 +1980,24 @@ class LiveKitConversation {
1726
1980
  presentation = null;
1727
1981
  readyFallback = null;
1728
1982
  layerFallback = null;
1983
+ audioBacklog = null;
1729
1984
  changeHandlers = new Set;
1730
1985
  userQuality = "good";
1731
1986
  avatarQuality = "good";
1732
1987
  network = "good";
1733
1988
  eventsRegistered = false;
1989
+ parts = new Parts((id, why) => {
1990
+ console.warn(`Nuance SDK: interface event ${id} was dropped: ${why}.`);
1991
+ });
1734
1992
  sessionEnded = false;
1735
1993
  refusal = null;
1736
- speaksProtocol = false;
1737
1994
  avatarSpeaking = false;
1738
- sequence = this.newSequence();
1739
1995
  answered = null;
1740
- commands = new Map;
1741
- commandCount = 0;
1742
- readyOwed = false;
1743
1996
  everConnected = false;
1744
1997
  away = false;
1745
1998
  awayTimer = null;
1746
1999
  runtimeTimer = null;
1747
2000
  presentationPaused = false;
1748
- readySending = false;
1749
- contextSent = null;
1750
2001
  leaving = null;
1751
2002
  connectGeneration = 0;
1752
2003
  _id;
@@ -1754,10 +2005,12 @@ class LiveKitConversation {
1754
2005
  cameraEnabled;
1755
2006
  cameraFacing;
1756
2007
  tokenProvider;
1757
- opensOnReady;
1758
2008
  presentsAtFullSize;
1759
- initialContext;
2009
+ opensOnReady;
2010
+ readyOwed = false;
1760
2011
  resilient;
2012
+ dropsAudioBacklog;
2013
+ interfaceTopic;
1761
2014
  constructor(config) {
1762
2015
  this.room = new Room({
1763
2016
  audioCaptureDefaults: config.mediaDevices?.microphoneId ? { deviceId: { ideal: config.mediaDevices.microphoneId } } : undefined,
@@ -1777,10 +2030,11 @@ class LiveKitConversation {
1777
2030
  this.cameraEnabled = config.cameraEnabled ?? true;
1778
2031
  this.cameraFacing = config.mediaDevices?.cameraFacing;
1779
2032
  this.tokenProvider = config.tokenProvider;
2033
+ this.presentsAtFullSize = config.presentsAtFullSize ?? false;
1780
2034
  this.opensOnReady = config.opensOnReady ?? false;
1781
- this.presentsAtFullSize = config.presentsAtFullSize ?? this.opensOnReady;
1782
- this.initialContext = config.initialContext ?? [];
1783
2035
  this.resilient = config.resilientReconnect ?? false;
2036
+ this.dropsAudioBacklog = config.dropAudioBacklog ?? false;
2037
+ this.interfaceTopic = config.interface;
1784
2038
  }
1785
2039
  get id() {
1786
2040
  return this._id;
@@ -1791,11 +2045,7 @@ class LiveKitConversation {
1791
2045
  }
1792
2046
  this.setState("connecting");
1793
2047
  this.sessionEnded = false;
1794
- this.speaksProtocol = false;
1795
- this.sequence.stop();
1796
- this.sequence = this.newSequence();
1797
- this.readyOwed = false;
1798
- this.contextSent = null;
2048
+ this.parts.clear();
1799
2049
  this.everConnected = false;
1800
2050
  if (!this.eventsRegistered) {
1801
2051
  this.registerRoomEvents();
@@ -1822,7 +2072,7 @@ class LiveKitConversation {
1822
2072
  type: "speaking-changed",
1823
2073
  participant: "user",
1824
2074
  isSpeaking
1825
- }), () => this.emit({ type: "microphone-changed" }), () => this.emit({ type: "camera-changed" }), this.cameraFacing, this.resilient, () => this.emit({ type: "screen-share-changed" }));
2075
+ }), () => this.emit({ type: "microphone-changed" }), () => this.emit({ type: "camera-changed" }), this.cameraFacing, this.resilient);
1826
2076
  this._user.startSpeakingDetection();
1827
2077
  try {
1828
2078
  await this._user.unmuteAudio();
@@ -1845,6 +2095,7 @@ class LiveKitConversation {
1845
2095
  this.checkForExistingAvatar();
1846
2096
  this.stats = new NetworkStatsReporter(this.room, (silent) => this.emit({ type: "mic-silent-changed", silent }));
1847
2097
  this.stats.start();
2098
+ this.startAudioBacklog();
1848
2099
  } catch (error) {
1849
2100
  if (!await aborted())
1850
2101
  this.setState("failed");
@@ -1862,18 +2113,21 @@ class LiveKitConversation {
1862
2113
  this.stopPresentation();
1863
2114
  this.stopReadyFallback();
1864
2115
  this.stopLayerFallback();
2116
+ this.stopAudioBacklog();
1865
2117
  this.clearAway();
1866
2118
  this.stopRefusal();
1867
2119
  this._user?.cleanup();
1868
2120
  this._user?.releaseDevices();
1869
2121
  await this.flushStats();
1870
- await this.sendSessionEnd();
1871
- this.sequence.stop();
2122
+ await this.sendInterfaceClose();
1872
2123
  await this.room.disconnect();
1873
2124
  this._avatar.updateParticipant(null);
1874
2125
  this._user = null;
1875
2126
  this.setState("disconnected");
1876
2127
  }
2128
+ get interface() {
2129
+ return this.interfaceTopic;
2130
+ }
1877
2131
  get state() {
1878
2132
  return this._state;
1879
2133
  }
@@ -1886,51 +2140,55 @@ class LiveKitConversation {
1886
2140
  get avatar() {
1887
2141
  return this._avatar;
1888
2142
  }
1889
- async setBackground(background) {
1890
- if (!BACKGROUND_ID.test(background)) {
1891
- throw new Error(`Not a background id: ${JSON.stringify(background)}`);
2143
+ async sendInterfaceEvent(event) {
2144
+ if (this.room.state !== ConnectionState3.Connected) {
2145
+ throw new Error("Not connected — call connect() first");
1892
2146
  }
1893
- return this.command({ type: "scene.background", background });
2147
+ const text = JSON.stringify(event);
2148
+ const payload = new TextEncoder().encode(text);
2149
+ if (payload.byteLength > INTERFACE_EVENT_MAX_BYTES) {
2150
+ await this.room.localParticipant.sendText(text, {
2151
+ topic: this.interfaceTopic
2152
+ });
2153
+ return;
2154
+ }
2155
+ await this.room.localParticipant.publishData(payload, {
2156
+ reliable: true,
2157
+ topic: this.interfaceTopic
2158
+ });
1894
2159
  }
1895
- async sendText(text) {
1896
- const line = text.trim();
1897
- if (!line)
1898
- throw new Error("Nothing to send: the text is blank");
1899
- return this.command({ type: "input.text", text: line, respond: true });
2160
+ onInterfacePacket(payload) {
2161
+ const event = this.parts.take(parseInterfaceEvent(payload));
2162
+ if (event !== null)
2163
+ this.deliverInterfaceEvent(event);
1900
2164
  }
1901
- async holdAgent() {
1902
- await this.command({ type: "agent.stop" }).catch(() => {
2165
+ deliverInterfaceEvent(event) {
2166
+ this.emit({ type: "interface-event", event });
2167
+ this.followAvatarSpeech(event);
2168
+ const ended = closedEnd(event);
2169
+ if (ended === null)
1903
2170
  return;
1904
- });
1905
- return this.command({ type: "floor.hold" });
1906
- }
1907
- async releaseAgent() {
1908
- return this.command({ type: "floor.release" });
1909
- }
1910
- async sendToolResult(callId, output, options = {}) {
1911
- return this.command({
1912
- type: "tool.result",
1913
- call_id: callId,
1914
- output,
1915
- ...options.images?.length && {
1916
- images: options.images.map(({ mediaType, data }) => ({
1917
- media_type: mediaType,
1918
- data
1919
- }))
1920
- },
1921
- is_error: options.isError ?? false,
1922
- final: options.final ?? true
1923
- });
2171
+ if (ended.byClient)
2172
+ this.answered?.();
2173
+ if (this.refusal !== null) {
2174
+ this.reportEnd("refused", ended.message);
2175
+ return;
2176
+ }
2177
+ this.reportEnd(ended.reason, ended.message, ended.endReason);
2178
+ }
2179
+ followAvatarSpeech(event) {
2180
+ if (event.type === "session.output_transcript.delta") {
2181
+ this.setAvatarSpeaking(true);
2182
+ } else if (event.type === "session.output_audio.done") {
2183
+ this.setAvatarSpeaking(false);
2184
+ }
1924
2185
  }
1925
2186
  onChange(handler) {
1926
2187
  return this.onEvent((event) => {
1927
- if (event.type !== "data-event" && event.type !== "screen-share-changed" && event.type !== "tool-call" && event.type !== "session-event")
2188
+ if (event.type !== "data-event" && event.type !== "interface-event")
1928
2189
  handler(event);
1929
2190
  });
1930
2191
  }
1931
- on(type, handler) {
1932
- return this.onEvent(sessionEventsOf(type, handler));
1933
- }
1934
2192
  onEvent(handler) {
1935
2193
  this.changeHandlers.add(handler);
1936
2194
  return () => {
@@ -1944,41 +2202,11 @@ class LiveKitConversation {
1944
2202
  }
1945
2203
  });
1946
2204
  }
1947
- onToolCall(handler) {
2205
+ onInterfaceEvent(handler) {
1948
2206
  return this.onEvent((event) => {
1949
- if (event.type === "tool-call")
1950
- handler(event.call);
1951
- });
1952
- }
1953
- async command(command) {
1954
- if (this.room.state !== ConnectionState2.Connected) {
1955
- throw new Error("Not connected — call connect() first");
1956
- }
1957
- if (!this.speaksProtocol) {
1958
- throw new Error("The runtime takes no session commands");
1959
- }
1960
- const id = `client-command-${++this.commandCount}`;
1961
- const answer = Promise.withResolvers();
1962
- this.commands.set(id, { type: command.type, settle: answer.resolve });
1963
- let timer;
1964
- const text = JSON.stringify({ ...command, id });
1965
- const payload = new TextEncoder().encode(text);
1966
- const streamed = payload.byteLength > PACKET_MAX_BYTES;
1967
- const sent = streamed ? this.room.localParticipant.sendText(text, { topic: SESSION_TOPIC }) : this.room.localParticipant.publishData(payload, {
1968
- reliable: true,
1969
- topic: SESSION_TOPIC
2207
+ if (event.type === "interface-event")
2208
+ handler(event.event);
1970
2209
  });
1971
- try {
1972
- return await Promise.race([
1973
- sent.then(() => answer.promise),
1974
- new Promise((_, reject) => {
1975
- timer = setTimeout(() => reject(new Error(`The runtime did not answer ${command.type}`)), streamed ? STREAM_TIMEOUT_MS : COMMAND_TIMEOUT_MS);
1976
- })
1977
- ]);
1978
- } finally {
1979
- clearTimeout(timer);
1980
- this.commands.delete(id);
1981
- }
1982
2210
  }
1983
2211
  emit(event) {
1984
2212
  for (const handler of this.changeHandlers) {
@@ -1996,8 +2224,13 @@ class LiveKitConversation {
1996
2224
  this.network = worst;
1997
2225
  this.emit({ type: "network-changed", quality: worst });
1998
2226
  }
1999
- isAvatarParticipant(_participant) {
2000
- return true;
2227
+ fromRuntime(participant) {
2228
+ if (participant === undefined)
2229
+ return false;
2230
+ return participant.kind === ParticipantKind.AGENT || participant.identity === this._avatar.identity;
2231
+ }
2232
+ isAvatarParticipant(participant) {
2233
+ return !isAttach(participant);
2001
2234
  }
2002
2235
  checkForExistingAvatar() {
2003
2236
  for (const [, participant] of this.room.remoteParticipants) {
@@ -2013,27 +2246,26 @@ class LiveKitConversation {
2013
2246
  }
2014
2247
  }
2015
2248
  registerRoomEvents() {
2016
- this.room.on(RoomEvent3.ConnectionStateChanged, (lkState) => {
2249
+ this.room.on(RoomEvent4.ConnectionStateChanged, (lkState) => {
2017
2250
  if (this._state === "idle")
2018
2251
  return;
2019
- if (lkState === ConnectionState2.Disconnected)
2252
+ if (lkState === ConnectionState3.Disconnected)
2020
2253
  return;
2021
2254
  const mapped = mapConnectionState(lkState, this.resilient && this.everConnected);
2022
2255
  if (mapped === "connected")
2023
2256
  this.everConnected = true;
2024
- if (lkState === ConnectionState2.Reconnecting)
2257
+ if (lkState === ConnectionState3.Reconnecting)
2025
2258
  this.setAvatarSpeaking(false);
2026
2259
  if (mapped !== this._state) {
2027
2260
  this.setState(mapped);
2028
2261
  }
2029
- if (lkState === ConnectionState2.Connected) {
2030
- this.sendOwedReady();
2031
- if (this.away)
2032
- this.awaitAvatarReturn();
2262
+ if (lkState === ConnectionState3.Connected && this.away) {
2263
+ this.awaitAvatarReturn();
2033
2264
  }
2265
+ if (lkState === ConnectionState3.Connected)
2266
+ this.sendOwedReady();
2034
2267
  });
2035
- this.room.on(RoomEvent3.Reconnected, () => this.catchUp());
2036
- this.room.on(RoomEvent3.ParticipantConnected, (participant) => {
2268
+ this.room.on(RoomEvent4.ParticipantConnected, (participant) => {
2037
2269
  if (this.isAvatarParticipant(participant)) {
2038
2270
  const tracksChanged = this._avatar.updateParticipant(participant);
2039
2271
  this.emit({ type: "avatar-connected" });
@@ -2042,10 +2274,9 @@ class LiveKitConversation {
2042
2274
  }
2043
2275
  if (this.away)
2044
2276
  this.avatarBack();
2045
- this.catchUp();
2046
2277
  }
2047
2278
  });
2048
- this.room.on(RoomEvent3.ParticipantDisconnected, (participant) => {
2279
+ this.room.on(RoomEvent4.ParticipantDisconnected, (participant) => {
2049
2280
  if (!this.isAvatarParticipant(participant))
2050
2281
  return;
2051
2282
  if (!this.resilient) {
@@ -2053,7 +2284,7 @@ class LiveKitConversation {
2053
2284
  return;
2054
2285
  }
2055
2286
  queueMicrotask(() => {
2056
- if (this.room.state === ConnectionState2.Disconnected)
2287
+ if (this.room.state === ConnectionState3.Disconnected)
2057
2288
  return;
2058
2289
  if (isReconnecting(this.room.state)) {
2059
2290
  this.avatarAway();
@@ -2065,16 +2296,15 @@ class LiveKitConversation {
2065
2296
  }
2066
2297
  });
2067
2298
  });
2068
- this.room.on(RoomEvent3.Disconnected, () => {
2299
+ this.room.on(RoomEvent4.Disconnected, () => {
2069
2300
  this.stats?.abandon();
2070
2301
  this.stats = null;
2071
- this.sequence.stop();
2072
2302
  this.setAvatarSpeaking(false);
2073
2303
  this.stopPresentation();
2074
2304
  this.stopReadyFallback();
2075
2305
  this.stopLayerFallback();
2306
+ this.stopAudioBacklog();
2076
2307
  this.clearAway();
2077
- this.readyOwed = false;
2078
2308
  this._user?.cleanup();
2079
2309
  this._avatar.updateParticipant(null);
2080
2310
  this._user = null;
@@ -2089,7 +2319,7 @@ class LiveKitConversation {
2089
2319
  }
2090
2320
  this.setState("disconnected");
2091
2321
  });
2092
- this.room.on(RoomEvent3.ConnectionQualityChanged, (quality, participant) => {
2322
+ this.room.on(RoomEvent4.ConnectionQualityChanged, (quality, participant) => {
2093
2323
  const mapped = mapConnectionQuality(quality);
2094
2324
  if (participant === this.room.localParticipant) {
2095
2325
  this.userQuality = mapped;
@@ -2100,7 +2330,15 @@ class LiveKitConversation {
2100
2330
  }
2101
2331
  this.updateNetwork();
2102
2332
  });
2103
- this.room.on(RoomEvent3.TrackSubscribed, (_track, _publication, participant) => {
2333
+ this.room.on(RoomEvent4.TrackPublished, (publication, participant) => {
2334
+ if (isAttach(participant))
2335
+ publication.setSubscribed(false);
2336
+ });
2337
+ this.room.on(RoomEvent4.TrackSubscribed, (_track, publication, participant) => {
2338
+ if (isAttach(participant)) {
2339
+ publication.setSubscribed(false);
2340
+ return;
2341
+ }
2104
2342
  if (this.isAvatarParticipant(participant)) {
2105
2343
  if (this._avatar.updateTracks()) {
2106
2344
  this.emit({ type: "tracks-changed" });
@@ -2108,18 +2346,16 @@ class LiveKitConversation {
2108
2346
  this.armReadyFallback();
2109
2347
  }
2110
2348
  });
2111
- this.room.on(RoomEvent3.TrackUnsubscribed, (_track, _publication, participant) => {
2349
+ this.room.on(RoomEvent4.TrackUnsubscribed, (_track, _publication, participant) => {
2112
2350
  if (this.isAvatarParticipant(participant)) {
2113
2351
  if (this._avatar.updateTracks()) {
2114
2352
  this.emit({ type: "tracks-changed" });
2115
2353
  }
2116
2354
  }
2117
2355
  });
2118
- this.room.on(RoomEvent3.DataReceived, (payload, _participant, _kind, topic) => {
2119
- if (topic === SESSION_TOPIC) {
2120
- this.handleSessionEvent(payload);
2356
+ this.room.on(RoomEvent4.DataReceived, (payload, participant, _kind, topic) => {
2357
+ if (isAttach(participant))
2121
2358
  return;
2122
- }
2123
2359
  if (topic === RUNTIME_TOPIC) {
2124
2360
  this.handleRuntimeSignal(payload);
2125
2361
  return;
@@ -2128,6 +2364,11 @@ class LiveKitConversation {
2128
2364
  this.handleClockSyncPing(payload);
2129
2365
  return;
2130
2366
  }
2367
+ if (topic === this.interfaceTopic) {
2368
+ if (this.fromRuntime(participant))
2369
+ this.onInterfacePacket(payload);
2370
+ return;
2371
+ }
2131
2372
  if (topic !== DATA_EVENT_TOPIC)
2132
2373
  return;
2133
2374
  const event = parseDataEvent(payload);
@@ -2148,71 +2389,23 @@ class LiveKitConversation {
2148
2389
  return;
2149
2390
  });
2150
2391
  }
2151
- async sendSessionEnd() {
2152
- if (this.sessionEnded || !this.speaksProtocol || this.room.state !== ConnectionState2.Connected) {
2392
+ async sendInterfaceClose() {
2393
+ if (this.sessionEnded || this.room.state !== ConnectionState3.Connected) {
2153
2394
  return;
2154
2395
  }
2155
2396
  this.sessionEnded = true;
2156
- const answered = new Promise((resolve) => {
2397
+ const closed = new Promise((resolve) => {
2157
2398
  this.answered = resolve;
2158
2399
  });
2159
- const command = new TextEncoder().encode(JSON.stringify({ type: "session.end", id: SESSION_END_ID }));
2400
+ const close = new TextEncoder().encode(JSON.stringify({ type: "session.close", event_id: INTERFACE_CLOSE_ID }));
2160
2401
  await Promise.race([
2161
- this.room.localParticipant.publishData(command, { reliable: true, topic: SESSION_TOPIC }).then(() => answered),
2162
- new Promise((resolve) => setTimeout(resolve, SESSION_END_TIMEOUT_MS))
2402
+ this.room.localParticipant.publishData(close, { reliable: true, topic: this.interfaceTopic }).then(() => closed),
2403
+ new Promise((resolve) => setTimeout(resolve, SESSION_CLOSE_TIMEOUT_MS))
2163
2404
  ]).catch(() => {
2164
2405
  return;
2165
2406
  });
2166
2407
  this.answered = null;
2167
2408
  }
2168
- handleSessionEvent(payload) {
2169
- const packet = parseSessionEvent(payload);
2170
- if (packet === null)
2171
- return;
2172
- this.speaksProtocol = true;
2173
- this.sendContext();
2174
- const { seq, event } = packet;
2175
- if (this.sequence.repeats(seq))
2176
- return;
2177
- if (event.type === "command-result") {
2178
- if (event.id === SESSION_END_ID)
2179
- this.answered?.();
2180
- else if (event.id !== null) {
2181
- this.commands.get(event.id)?.settle(event.result);
2182
- }
2183
- } else if (event.type === "command-unreadable") {
2184
- const [oldest] = this.commands;
2185
- if (oldest) {
2186
- const [id, { type, settle }] = oldest;
2187
- this.commands.delete(id);
2188
- settle({ status: "rejected", error: `unknown command ${type}` });
2189
- }
2190
- }
2191
- this.sequence.accept(seq, event);
2192
- }
2193
- deliver(event) {
2194
- if (event.type === "output-ready") {
2195
- this.markAvatarReady();
2196
- } else if (event.type === "tool-call") {
2197
- this.emit({ type: "tool-call", call: event.call });
2198
- } else if (event.type === "ended") {
2199
- this.answered?.();
2200
- if (!event.byClient)
2201
- this.reportEnd(event.reason, event.message);
2202
- } else if (event.type === "session-event") {
2203
- if (event.event.type === "floor") {
2204
- this.setAvatarSpeaking(event.event.state === "speaking");
2205
- }
2206
- this.emit({ type: "session-event", event: event.event });
2207
- }
2208
- }
2209
- newSequence() {
2210
- return new EventSequence((event) => this.deliver(event), (afterSeq) => this.command({ type: "session.replay", after_seq: afterSeq }));
2211
- }
2212
- catchUp() {
2213
- if (this.speaksProtocol)
2214
- this.sequence.catchUp();
2215
- }
2216
2409
  handleRuntimeSignal(payload) {
2217
2410
  const signal = parseRuntimeSignal(payload);
2218
2411
  if (signal?.type === "session-ended") {
@@ -2220,7 +2413,7 @@ class LiveKitConversation {
2220
2413
  this.refusal ??= setTimeout(() => this.reportEnd("refused"), REFUSAL_REASON_WAIT_MS);
2221
2414
  return;
2222
2415
  }
2223
- this.reportEnd(signal.reason);
2416
+ this.reportEnd(signal.reason, signal.message, signal.endReason);
2224
2417
  } else if (signal?.type === "output-ready") {
2225
2418
  this.markAvatarReady();
2226
2419
  } else if (signal?.type === "link-poor") {
@@ -2233,8 +2426,6 @@ class LiveKitConversation {
2233
2426
  }
2234
2427
  this.readyFallback = new ReadyFallback(() => avatarFramesDecoded(this.room), () => this._avatar.isVideoPlaying, () => {
2235
2428
  this.readyFallback = null;
2236
- if (this.opensOnReady)
2237
- this.speaksProtocol = true;
2238
2429
  this.markAvatarReady();
2239
2430
  });
2240
2431
  this.readyFallback.start();
@@ -2260,10 +2451,6 @@ class LiveKitConversation {
2260
2451
  this._avatar.markReady();
2261
2452
  this.startLayerFallback();
2262
2453
  this.emit({ type: "avatar-ready" });
2263
- if (!this.opensOnReady) {
2264
- this.emit({ type: "avatar-presented" });
2265
- return;
2266
- }
2267
2454
  if (!this.presentsAtFullSize) {
2268
2455
  this.markAvatarPresented();
2269
2456
  return;
@@ -2277,42 +2464,19 @@ class LiveKitConversation {
2277
2464
  }
2278
2465
  markAvatarPresented() {
2279
2466
  this.presentation = null;
2280
- if (this.speaksProtocol && this.room.state === ConnectionState2.Connected) {
2281
- this.ready().catch(() => {
2282
- return;
2283
- });
2284
- } else if (this.resilient && this.speaksProtocol && isReconnecting(this.room.state)) {
2467
+ if (this.opensOnReady) {
2285
2468
  this.readyOwed = true;
2469
+ this.sendOwedReady();
2286
2470
  }
2287
2471
  this.emit({ type: "avatar-presented" });
2288
2472
  }
2289
- sendContext() {
2290
- if (this.contextSent !== null || this.initialContext.length === 0)
2291
- return;
2292
- this.contextSent = this.command({
2293
- type: "session.context",
2294
- items: this.initialContext.map(contextItem)
2295
- }).then(() => {
2296
- return;
2297
- }, () => {
2298
- return;
2299
- });
2300
- }
2301
- ready() {
2302
- this.sendContext();
2303
- const ready = () => this.command({ type: "session.ready" });
2304
- return this.contextSent === null ? ready() : this.contextSent.then(ready);
2305
- }
2306
2473
  sendOwedReady() {
2307
- if (!this.readyOwed || !this.speaksProtocol || this.readySending)
2308
- return;
2309
- this.readySending = true;
2310
- this.ready().then(() => {
2311
- this.readyOwed = false;
2312
- }).catch(() => {
2474
+ if (!this.readyOwed || this.room.state !== ConnectionState3.Connected)
2313
2475
  return;
2314
- }).finally(() => {
2315
- this.readySending = false;
2476
+ this.readyOwed = false;
2477
+ const ready = new TextEncoder().encode(JSON.stringify({ type: "session.ready", id: READY_ID }));
2478
+ this.room.localParticipant.publishData(ready, { reliable: true, topic: SESSION_TOPIC }).catch(() => {
2479
+ this.readyOwed = true;
2316
2480
  });
2317
2481
  }
2318
2482
  avatarLeft() {
@@ -2360,7 +2524,7 @@ class LiveKitConversation {
2360
2524
  this.awaitRuntimeReturn(RUNTIME_RECHECK_MS);
2361
2525
  return;
2362
2526
  }
2363
- if (this.room.state === ConnectionState2.Disconnected)
2527
+ if (this.room.state === ConnectionState3.Disconnected)
2364
2528
  return;
2365
2529
  if (this.avatarInRoom())
2366
2530
  return;
@@ -2406,16 +2570,38 @@ class LiveKitConversation {
2406
2570
  this.layerFallback?.stop();
2407
2571
  this.layerFallback = null;
2408
2572
  }
2573
+ startAudioBacklog() {
2574
+ if (!this.dropsAudioBacklog || this.audioBacklog !== null)
2575
+ return;
2576
+ this.audioBacklog = new AudioBacklogWatch(() => avatarJitterBuffer(this.room), (backlog) => {
2577
+ const behind = Math.round(backlog.jitterBufferMs - backlog.targetMs);
2578
+ console.info(`Nuance SDK: the avatar's audio was ${behind} ms behind (jitter buffer ${Math.round(backlog.jitterBufferMs)} ms, target ${Math.round(backlog.targetMs)} ms); dropped it to live.`);
2579
+ this.stats?.noteAudioDrop(backlog);
2580
+ resubscribeAvatarAudio(this.room).catch(() => {
2581
+ return;
2582
+ });
2583
+ });
2584
+ this.audioBacklog.start();
2585
+ }
2586
+ stopAudioBacklog() {
2587
+ this.audioBacklog?.stop();
2588
+ this.audioBacklog = null;
2589
+ }
2409
2590
  stopPresentation() {
2410
2591
  this.presentation?.stop();
2411
2592
  this.presentation = null;
2412
2593
  }
2413
- reportEnd(reason, message) {
2594
+ reportEnd(reason, message, cause) {
2414
2595
  this.stopRefusal();
2415
2596
  if (this.sessionEnded)
2416
2597
  return;
2417
2598
  this.sessionEnded = true;
2418
- this.emit(message === undefined ? { type: "session-ended", reason } : { type: "session-ended", reason, message });
2599
+ this.emit({
2600
+ type: "session-ended",
2601
+ reason,
2602
+ ...cause !== undefined && { endReason: cause },
2603
+ ...message !== undefined && { message }
2604
+ });
2419
2605
  }
2420
2606
  stopRefusal() {
2421
2607
  if (this.refusal === null)
@@ -2436,16 +2622,16 @@ class LiveKitConversation {
2436
2622
 
2437
2623
  // src/conversation.ts
2438
2624
  var DEFAULT_HOST = "https://api.nuancelabs.ai";
2625
+ var LIVE_INTERFACE = "nuance_live";
2626
+ var LIVE_INTERFACES = new Set([LIVE_INTERFACE, "gpt_live"]);
2439
2627
  var QUEUE_HEARTBEAT_INTERVAL_MS = 5000;
2440
2628
  var HEARTBEAT_INTERVAL_MS = 5000;
2441
2629
  var EVENTS_RETRY_MS = 5000;
2442
2630
  var ADMISSION_FAILED_REQUESTS = 6;
2443
2631
  var BUSY_RETRY_LIMIT_MS = 60000;
2444
2632
  var BUSY_RETRY_DEFAULT_MS = 3000;
2633
+ var CAPACITY_RETRY_LIMIT_MS = 15000;
2445
2634
  var LOST_WINDOW_MS = 30000;
2446
- function createNuanceConversation(options = {}) {
2447
- return new NuanceConversation(options);
2448
- }
2449
2635
  var TERMINAL_STATUSES = new Set([
2450
2636
  "failed",
2451
2637
  "expired",
@@ -2474,9 +2660,15 @@ function isMaintenanceBody(body) {
2474
2660
  function isBusyBody(body) {
2475
2661
  return isRecord(body) && body.code === "busy";
2476
2662
  }
2663
+ function isCapacityBody(body) {
2664
+ return isRecord(body) && body.code === "capacity_unavailable";
2665
+ }
2477
2666
  function isGuestLimitBody(body) {
2478
2667
  return isRecord(body) && body.code === "guest_limit";
2479
2668
  }
2669
+ function refusedByServer(error) {
2670
+ return error instanceof ApiError || error instanceof ActiveSessionError || error instanceof LegalRequiredError || error instanceof MaintenanceError || error instanceof GuestLimitError || error instanceof PolicyCurfewError;
2671
+ }
2480
2672
  function isCurfewBody(body) {
2481
2673
  return isRecord(body) && body.code === "policy_curfew";
2482
2674
  }
@@ -2501,14 +2693,18 @@ class NuanceConversation {
2501
2693
  ticker;
2502
2694
  sessionOverrides;
2503
2695
  personality;
2504
- tools;
2505
- initialContext;
2506
2696
  variant;
2507
2697
  maxSessionSeconds;
2508
2698
  videoCapture;
2509
2699
  startOn;
2700
+ greenScreen;
2701
+ alphaFrames;
2510
2702
  mediaDevices;
2511
2703
  resilientReconnect;
2704
+ dropAudioBacklog;
2705
+ interfaceName = LIVE_INTERFACE;
2706
+ stayConnectedOnEnd;
2707
+ joined = false;
2512
2708
  delegate = null;
2513
2709
  delegateUnsubscribe = null;
2514
2710
  stopBeats = null;
@@ -2519,6 +2715,7 @@ class NuanceConversation {
2519
2715
  reconnectingAt = null;
2520
2716
  lostContact = false;
2521
2717
  attempt = 0;
2718
+ toldPreparing = -1;
2522
2719
  lastPosition = null;
2523
2720
  lastEstimate = null;
2524
2721
  capped = false;
@@ -2541,24 +2738,28 @@ class NuanceConversation {
2541
2738
  this.ticker = deps.ticker ?? pageTicker;
2542
2739
  this.sessionOverrides = options.sessionOverrides ?? {};
2543
2740
  this.personality = options.personality;
2544
- this.tools = options.tools;
2545
- this.initialContext = options.initialContext ?? [];
2546
2741
  this.variant = options.variant;
2547
2742
  this.maxSessionSeconds = options.maxSessionSeconds;
2548
2743
  this.videoCapture = options.videoCapture;
2549
2744
  this.startOn = options.startOn ?? "first-frame";
2745
+ this.greenScreen = options.greenScreen ?? false;
2746
+ this.alphaFrames = options.alphaFrames ?? false;
2550
2747
  this.mediaDevices = options.mediaDevices;
2551
2748
  this.resilientReconnect = options.resilientReconnect ?? false;
2749
+ this.dropAudioBacklog = options.dropAudioBacklog ?? false;
2750
+ this.stayConnectedOnEnd = options.stayConnectedOnEnd ?? false;
2552
2751
  }
2553
2752
  async connect() {
2554
2753
  if (this._state === "connecting" || this._state === "queued" || this._state === "connected" || this._state === "reconnecting") {
2555
2754
  throw new Error(`Cannot connect while in "${this._state}" state`);
2556
2755
  }
2557
2756
  const attempt = ++this.attempt;
2757
+ let created = false;
2558
2758
  this.setState("connecting");
2559
2759
  this.clearLimit();
2560
2760
  this.capped = false;
2561
2761
  this.ended = false;
2762
+ this.joined = false;
2562
2763
  this.reconnectingAt = null;
2563
2764
  this.lostContact = false;
2564
2765
  this.lastPosition = null;
@@ -2572,21 +2773,27 @@ class NuanceConversation {
2572
2773
  try {
2573
2774
  const request = {
2574
2775
  session_overrides: this.sessionOverrides,
2575
- client_ready: this.startOn === "full-quality" || this.initialContext.length > 0
2776
+ ...this.startOn === "full-quality" ? { client_ready: true } : {}
2576
2777
  };
2577
2778
  const timezone = this.timezone();
2578
2779
  if (timezone)
2579
2780
  request.timezone = timezone;
2781
+ const camera = await cameraSource(mediaDevices);
2782
+ if (camera)
2783
+ request.camera = camera;
2580
2784
  if (this.personality)
2581
2785
  request.personality = this.personality;
2582
- if (this.tools)
2583
- request.tools = this.tools;
2584
2786
  if (this.variant)
2585
2787
  request.variant = this.variant;
2586
2788
  if (this.maxSessionSeconds !== undefined) {
2587
2789
  request.max_session_seconds = this.maxSessionSeconds;
2588
2790
  }
2791
+ if (this.greenScreen)
2792
+ request.green_screen = true;
2793
+ if (this.alphaFrames)
2794
+ request.alpha_frames = true;
2589
2795
  conversation = await this.createWhenNotBusy(request, attempt);
2796
+ created = true;
2590
2797
  } catch (error) {
2591
2798
  if (error instanceof ApiError && error.status === 409) {
2592
2799
  throw new ActiveSessionError;
@@ -2622,7 +2829,7 @@ class NuanceConversation {
2622
2829
  if (event.type === "session-ended" && (event.reason === "connection-lost" || event.reason === "refused")) {
2623
2830
  const held = this.held;
2624
2831
  this.held = null;
2625
- this.endSession(event.reason, event.message);
2832
+ this.endSession(event.reason, event.message, event.endReason, false);
2626
2833
  this.disconnect().catch(() => {});
2627
2834
  if (held !== null) {
2628
2835
  this.api.abandonConversation(held).catch(() => {});
@@ -2630,12 +2837,26 @@ class NuanceConversation {
2630
2837
  return;
2631
2838
  }
2632
2839
  if (event.type === "session-ended") {
2633
- this.endSession(event.reason, event.message);
2840
+ this.endSession(event.reason, event.message, event.endReason);
2634
2841
  return;
2635
2842
  }
2636
2843
  this.emit(event);
2637
2844
  });
2638
- await this.delegate.connect();
2845
+ if (this.ended && !this.stayConnectedOnEnd) {
2846
+ await this.disconnect();
2847
+ return;
2848
+ }
2849
+ try {
2850
+ await this.delegate.connect();
2851
+ } catch (error) {
2852
+ if (!this.ended || this.stayConnectedOnEnd)
2853
+ throw error;
2854
+ }
2855
+ this.joined = true;
2856
+ if (this.ended && !this.stayConnectedOnEnd) {
2857
+ await this.disconnect();
2858
+ return;
2859
+ }
2639
2860
  if (grant === null) {
2640
2861
  this.startHeartbeat(conversation.id, HEARTBEAT_INTERVAL_MS);
2641
2862
  }
@@ -2643,6 +2864,19 @@ class NuanceConversation {
2643
2864
  if (attempt !== this.attempt) {
2644
2865
  throw error;
2645
2866
  }
2867
+ if (!created && refusedByServer(error)) {
2868
+ const refused = {
2869
+ type: "session-ended",
2870
+ reason: "refused",
2871
+ message: error.message,
2872
+ error
2873
+ };
2874
+ for (const handler of this.changeHandlers) {
2875
+ try {
2876
+ handler(refused);
2877
+ } catch {}
2878
+ }
2879
+ }
2646
2880
  await this.disconnect().catch(() => {});
2647
2881
  this.setState("failed");
2648
2882
  throw error;
@@ -2650,6 +2884,7 @@ class NuanceConversation {
2650
2884
  }
2651
2885
  async disconnect() {
2652
2886
  this.attempt += 1;
2887
+ this.joined = false;
2653
2888
  this.clearLimit();
2654
2889
  this.stopHeartbeat();
2655
2890
  this.stopEvents?.();
@@ -2658,7 +2893,12 @@ class NuanceConversation {
2658
2893
  this.admitted = null;
2659
2894
  this.delegateUnsubscribe?.();
2660
2895
  this.delegateUnsubscribe = null;
2661
- await this.delegate?.disconnect();
2896
+ const trailing = this.ended ? this.delegate?.onInterfaceEvent((event) => this.emit({ type: "interface-event", event })) : undefined;
2897
+ try {
2898
+ await this.delegate?.disconnect();
2899
+ } finally {
2900
+ trailing?.();
2901
+ }
2662
2902
  this.delegate = null;
2663
2903
  await this.release();
2664
2904
  this.setState("disconnected");
@@ -2669,6 +2909,9 @@ class NuanceConversation {
2669
2909
  get id() {
2670
2910
  return this._id;
2671
2911
  }
2912
+ get interface() {
2913
+ return this.interfaceName;
2914
+ }
2672
2915
  get user() {
2673
2916
  if (!this.delegate) {
2674
2917
  throw new Error("Not connected — call connect() first");
@@ -2681,45 +2924,18 @@ class NuanceConversation {
2681
2924
  }
2682
2925
  return this.delegate.avatar;
2683
2926
  }
2684
- async setBackground(background) {
2685
- if (!this.delegate) {
2686
- throw new Error("Not connected — call connect() first");
2687
- }
2688
- return this.delegate.setBackground(background);
2689
- }
2690
- async sendText(text) {
2691
- if (!this.delegate) {
2692
- throw new Error("Not connected — call connect() first");
2693
- }
2694
- return this.delegate.sendText(text);
2695
- }
2696
- async holdAgent() {
2697
- if (!this.delegate) {
2698
- throw new Error("Not connected — call connect() first");
2699
- }
2700
- return this.delegate.holdAgent();
2701
- }
2702
- async releaseAgent() {
2927
+ async sendInterfaceEvent(event) {
2703
2928
  if (!this.delegate) {
2704
2929
  throw new Error("Not connected — call connect() first");
2705
2930
  }
2706
- return this.delegate.releaseAgent();
2707
- }
2708
- async sendToolResult(callId, output, options) {
2709
- if (!this.delegate) {
2710
- throw new Error("Not connected — call connect() first");
2711
- }
2712
- return this.delegate.sendToolResult(callId, output, options);
2931
+ return this.delegate.sendInterfaceEvent(event);
2713
2932
  }
2714
2933
  onChange(handler) {
2715
2934
  return this.onEvent((event) => {
2716
- if (event.type !== "data-event" && event.type !== "screen-share-changed" && event.type !== "tool-call" && event.type !== "session-event")
2935
+ if (event.type !== "data-event" && event.type !== "interface-event")
2717
2936
  handler(event);
2718
2937
  });
2719
2938
  }
2720
- on(type, handler) {
2721
- return this.onEvent(sessionEventsOf(type, handler));
2722
- }
2723
2939
  onEvent(handler) {
2724
2940
  this.changeHandlers.add(handler);
2725
2941
  return () => {
@@ -2733,10 +2949,10 @@ class NuanceConversation {
2733
2949
  }
2734
2950
  });
2735
2951
  }
2736
- onToolCall(handler) {
2952
+ onInterfaceEvent(handler) {
2737
2953
  return this.onEvent((event) => {
2738
- if (event.type === "tool-call")
2739
- handler(event.call);
2954
+ if (event.type === "interface-event")
2955
+ handler(event.event);
2740
2956
  });
2741
2957
  }
2742
2958
  async waitForSlot(conversationId, attempt) {
@@ -2745,7 +2961,7 @@ class NuanceConversation {
2745
2961
  this.followLive(conversationId, admission.after, attempt);
2746
2962
  return admission.grant;
2747
2963
  }
2748
- this.admitted = Promise.withResolvers();
2964
+ this.admitted = resolvers();
2749
2965
  this.startHeartbeat(conversationId, QUEUE_HEARTBEAT_INTERVAL_MS);
2750
2966
  await this.admitted.promise;
2751
2967
  return null;
@@ -2798,6 +3014,8 @@ class NuanceConversation {
2798
3014
  if (event.type === "queued" && event.position != null) {
2799
3015
  this.reportPlace(event.position, event.estimated_wait_seconds ?? null, null, event.preparing ?? false);
2800
3016
  }
3017
+ if (event.type === "preparing")
3018
+ this.reportPreparing();
2801
3019
  }
2802
3020
  }
2803
3021
  } finally {
@@ -2838,7 +3056,7 @@ class NuanceConversation {
2838
3056
  this.armLimit(event.seconds_remaining, receivedAt);
2839
3057
  }
2840
3058
  if (event.type === "ended") {
2841
- this.endSession(this.endReason(event.reason));
3059
+ this.endSession(this.endReason(event.reason), undefined, v1EndReason(event.reason));
2842
3060
  return;
2843
3061
  }
2844
3062
  }
@@ -2853,6 +3071,8 @@ class NuanceConversation {
2853
3071
  return "time-limit";
2854
3072
  if (reason === "policy_violation")
2855
3073
  return "policy-violation";
3074
+ if (reason === "age_check")
3075
+ return "age-check";
2856
3076
  if (reason === "runtime_silent" || this.cutOff())
2857
3077
  return "connection-lost";
2858
3078
  return "ended";
@@ -2873,6 +3093,11 @@ class NuanceConversation {
2873
3093
  if (conversation.provider !== "livekit") {
2874
3094
  throw new Error(`Unsupported provider ${conversation.provider}`);
2875
3095
  }
3096
+ const named = conversation.interface ?? null;
3097
+ if (named === null || !LIVE_INTERFACES.has(named)) {
3098
+ throw new Error(`The session runs on ${named ?? "nuance.v1"}, which this SDK does not speak: it needs Nuance Live (interface "nuance_live")`);
3099
+ }
3100
+ this.interfaceName = named;
2876
3101
  const connection = grant ?? await this.connectRoom(conversation.id);
2877
3102
  if (attempt !== this.attempt) {
2878
3103
  throw new DisconnectedError;
@@ -2883,10 +3108,11 @@ class NuanceConversation {
2883
3108
  mediaDevices,
2884
3109
  cameraEnabled: !this.sessionOverrides.disable_camera && mediaDevices.cameraEnabled !== false,
2885
3110
  videoCapture: this.videoCapture,
2886
- opensOnReady: conversation.provider_metadata?.client_ready === true,
2887
3111
  presentsAtFullSize: this.startOn === "full-quality",
2888
- initialContext: this.initialContext,
2889
- resilientReconnect: this.resilient
3112
+ opensOnReady: conversation.provider_metadata?.client_ready === true,
3113
+ resilientReconnect: this.resilient,
3114
+ dropAudioBacklog: typeof this.dropAudioBacklog === "function" ? this.dropAudioBacklog() : this.dropAudioBacklog,
3115
+ interface: this.interfaceName
2890
3116
  });
2891
3117
  }
2892
3118
  async connectRoom(conversationId) {
@@ -2897,18 +3123,24 @@ class NuanceConversation {
2897
3123
  return response.connection;
2898
3124
  }
2899
3125
  async createWhenNotBusy(request, attempt) {
2900
- const giveUpAt = Date.now() + BUSY_RETRY_LIMIT_MS;
3126
+ const started = Date.now();
2901
3127
  for (;; ) {
2902
3128
  try {
2903
3129
  return await this.api.createConversation(request);
2904
3130
  } catch (error) {
2905
- if (!(error instanceof ApiError) || error.status !== 503 || !isBusyBody(error.body) || attempt !== this.attempt) {
3131
+ if (!(error instanceof ApiError) || attempt !== this.attempt) {
2906
3132
  throw error;
2907
3133
  }
2908
- const waitMs = typeof error.body.retry_after_seconds === "number" ? error.body.retry_after_seconds * 1000 : BUSY_RETRY_DEFAULT_MS;
2909
- if (Date.now() + waitMs > giveUpAt)
3134
+ const busy = error.status === 503 && isBusyBody(error.body);
3135
+ const full = error.status === 429 && isCapacityBody(error.body);
3136
+ if (!busy && !full)
2910
3137
  throw error;
2911
- if (this._state !== "queued")
3138
+ const asked = error.body.retry_after_seconds;
3139
+ const waitMs = typeof asked === "number" ? asked * 1000 : BUSY_RETRY_DEFAULT_MS;
3140
+ const limitMs = busy ? BUSY_RETRY_LIMIT_MS : CAPACITY_RETRY_LIMIT_MS;
3141
+ if (Date.now() + waitMs > started + limitMs)
3142
+ throw error;
3143
+ if (busy && this._state !== "queued")
2912
3144
  this.setState("queued");
2913
3145
  await new Promise((resolve) => setTimeout(resolve, waitMs));
2914
3146
  if (attempt !== this.attempt)
@@ -2979,11 +3211,21 @@ class NuanceConversation {
2979
3211
  this.admitted = null;
2980
3212
  return;
2981
3213
  }
3214
+ if (state.status === "queued" && state.position == null && state.preparing) {
3215
+ this.reportPreparing();
3216
+ return;
3217
+ }
2982
3218
  if (state.status !== "queued" || state.position == null) {
2983
3219
  return;
2984
3220
  }
2985
3221
  this.reportPlace(state.position, state.estimated_wait_seconds ?? null, state.pool_slots ?? null, state.preparing ?? false);
2986
3222
  }
3223
+ reportPreparing() {
3224
+ if (this.toldPreparing === this.attempt)
3225
+ return;
3226
+ this.toldPreparing = this.attempt;
3227
+ this.emit({ type: "avatar-preparing" });
3228
+ }
2987
3229
  reportPlace(position, estimate, poolSlots, preparing) {
2988
3230
  if (this._state !== "queued") {
2989
3231
  this.setState("queued");
@@ -3029,14 +3271,28 @@ class NuanceConversation {
3029
3271
  }
3030
3272
  await this.api.abandonConversation(held).catch(() => {});
3031
3273
  }
3032
- endSession(reason, message) {
3274
+ endSession(reason, message, cause, leave = true) {
3033
3275
  if (this.ended) {
3034
3276
  return;
3035
3277
  }
3036
3278
  this.ended = true;
3037
3279
  this.clearLimit();
3038
3280
  this.stopHeartbeat();
3039
- this.emit(message === undefined ? { type: "session-ended", reason } : { type: "session-ended", reason, message });
3281
+ this.emit({
3282
+ type: "session-ended",
3283
+ reason,
3284
+ ...cause !== undefined && { endReason: cause },
3285
+ ...message !== undefined && { message }
3286
+ });
3287
+ if (!leave || this.stayConnectedOnEnd)
3288
+ return;
3289
+ if (!this.joined) {
3290
+ this.delegateUnsubscribe?.();
3291
+ this.delegateUnsubscribe = null;
3292
+ this.delegate?.disconnect().catch(() => {});
3293
+ return;
3294
+ }
3295
+ this.disconnect().catch(() => {});
3040
3296
  }
3041
3297
  stopHeartbeat() {
3042
3298
  this.stopBeats?.();
@@ -3044,7 +3300,7 @@ class NuanceConversation {
3044
3300
  }
3045
3301
  emit(event) {
3046
3302
  for (const handler of this.changeHandlers) {
3047
- handler(event);
3303
+ deliver(handler, event);
3048
3304
  }
3049
3305
  }
3050
3306
  atLimit() {
@@ -3070,17 +3326,43 @@ class NuanceConversation {
3070
3326
  this.emit({ type: "state-changed", state });
3071
3327
  }
3072
3328
  }
3073
- // src/core/types.ts
3074
- var ConversationState;
3075
- ((ConversationState2) => {
3076
- ConversationState2["Idle"] = "idle";
3077
- ConversationState2["Queued"] = "queued";
3078
- ConversationState2["Connecting"] = "connecting";
3079
- ConversationState2["Connected"] = "connected";
3080
- ConversationState2["Reconnecting"] = "reconnecting";
3081
- ConversationState2["Disconnected"] = "disconnected";
3082
- ConversationState2["Failed"] = "failed";
3083
- })(ConversationState ||= {});
3329
+
3330
+ // src/create.ts
3331
+ var KNOWN2 = {
3332
+ host: true,
3333
+ headers: true,
3334
+ token: true,
3335
+ sessionOverrides: true,
3336
+ personality: true,
3337
+ variant: true,
3338
+ maxSessionSeconds: true,
3339
+ videoCapture: true,
3340
+ mediaDevices: true,
3341
+ startOn: true,
3342
+ greenScreen: true,
3343
+ alphaFrames: true,
3344
+ resilientReconnect: true,
3345
+ dropAudioBacklog: true,
3346
+ stayConnectedOnEnd: true
3347
+ };
3348
+ var MEDIA_DEVICES = new Set([
3349
+ "microphoneId",
3350
+ "cameraId",
3351
+ "cameraEnabled",
3352
+ "cameraFacing"
3353
+ ]);
3354
+ function warnUnknown(options) {
3355
+ for (const name in options) {
3356
+ if (Object.hasOwn(KNOWN2, name))
3357
+ continue;
3358
+ const hint = MEDIA_DEVICES.has(name) ? ` It belongs under mediaDevices: { ${name} }.` : "";
3359
+ console.warn(`Nuance SDK: unknown option "${name}" is ignored.${hint}`);
3360
+ }
3361
+ }
3362
+ function createNuanceConversation(options = {}) {
3363
+ warnUnknown(options);
3364
+ return new NuanceConversation(options);
3365
+ }
3084
3366
  export {
3085
3367
  createNuanceConversation,
3086
3368
  PolicyCurfewError,