@nuancelabs/js-sdk 0.13.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,6 +18,7 @@ const conversation = createNuanceConversation({
18
18
  // Your endpoint, which mints a session token with your API key.
19
19
  token: async () => {
20
20
  const response = await fetch("/api/nuance-token", { method: "POST" });
21
+ if (!response.ok) throw new Error(await response.text());
21
22
  return (await response.json()).token;
22
23
  },
23
24
  });
package/index.d.ts CHANGED
@@ -86,6 +86,38 @@ export declare interface AgentTurnStartedEvent extends SessionEventBase {
86
86
  source: "reply" | "say" | (string & {});
87
87
  }
88
88
 
89
+ /**
90
+ * An error answer from the Nuance API that the SDK has no more specific error
91
+ * for, such as a `401` for an expired or used session token, a `429` when this
92
+ * month's conversation time is used, or a `503` once the SDK has stopped
93
+ * retrying a busy service. See the Errors guide for each status and `code`.
94
+ *
95
+ * Act on `status` and `code`, never on `message`: the message is the server's
96
+ * `detail`, written for you, and it can change.
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * try {
101
+ * await conversation.connect();
102
+ * } catch (error) {
103
+ * if (error instanceof ApiError && error.status === 401) {
104
+ * // The session token expired or was used: the next connect() fetches a new one.
105
+ * }
106
+ * }
107
+ * ```
108
+ */
109
+ export declare class ApiError extends Error {
110
+ /** The HTTP status, such as `401` or `503`. */
111
+ readonly status: number;
112
+ /** The error's `code` when the server sent one, such as `"busy"` or `"unavailable"`. */
113
+ readonly code: string | null;
114
+ /** How long to wait before trying again, when the server said. */
115
+ readonly retryAfterSeconds: number | null;
116
+ /** The parsed JSON body of the answer, or `null` when it had none. */
117
+ readonly body: unknown;
118
+ constructor(status: number, detail: string, body?: unknown);
119
+ }
120
+
89
121
  /**
90
122
  * An audio track that can be attached to or detached from an audio element.
91
123
  */
@@ -362,7 +394,8 @@ export declare type ConversationEvent =
362
394
  * `"time-limit"` when the session was capped and ran out its clock,
363
395
  * `"policy-violation"` when the runtime ended it for content, `"refused"`
364
396
  * when the runtime would not start it, with its reason in `message` when it
365
- * gave one.
397
+ * gave one. Call {@link Conversation.disconnect} then: after the runtime's
398
+ * own end the call stays connected, with the camera and microphone on.
366
399
  */
367
400
  | {
368
401
  type: "session-ended";
@@ -416,6 +449,7 @@ export declare type ConversationStateValue = `${ConversationState}`;
416
449
  * // Your server mints a session token with its API key.
417
450
  * token: async () => {
418
451
  * const response = await fetch("/api/nuance-token", { method: "POST" });
452
+ * if (!response.ok) throw new Error(await response.text());
419
453
  * return (await response.json()).token;
420
454
  * },
421
455
  * });
@@ -448,6 +482,7 @@ export declare interface CreateNuanceConversationOptions {
448
482
  * ```ts
449
483
  * token: async () => {
450
484
  * const response = await fetch("/api/nuance-token", { method: "POST" });
485
+ * if (!response.ok) throw new Error(await response.text());
451
486
  * return (await response.json()).token;
452
487
  * }
453
488
  * ```
@@ -557,6 +592,7 @@ export declare class GuestLimitError extends Error {
557
592
  constructor(reason: string, message: string);
558
593
  }
559
594
 
595
+
560
596
  /**
561
597
  * Raised by {@link Conversation.connect} when the server refuses a new session
562
598
  * because the account has not completed onboarding or accepted the current
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.13.0"}`;
7
+ var SDK_HEADER = `js/${"0.14.1"}`;
8
8
 
9
9
  // src/utils.ts
10
10
  function normalizeHost(host) {
@@ -14,12 +14,16 @@ function normalizeHost(host) {
14
14
  // src/api/client.ts
15
15
  class ApiError extends Error {
16
16
  status;
17
+ code;
18
+ retryAfterSeconds;
17
19
  body;
18
20
  constructor(status, detail, body = null) {
19
21
  super(detail);
20
22
  this.name = "ApiError";
21
23
  this.status = status;
22
24
  this.body = body;
25
+ this.code = isRecord(body) && typeof body.code === "string" ? body.code : null;
26
+ this.retryAfterSeconds = isRecord(body) && typeof body.retry_after_seconds === "number" ? body.retry_after_seconds : null;
23
27
  }
24
28
  }
25
29
 
@@ -99,7 +103,6 @@ class NuanceApiClient {
99
103
  return this.headersProvider ? await this.headersProvider() : {};
100
104
  }
101
105
  }
102
-
103
106
  // src/core/active-session-error.ts
104
107
  class ActiveSessionError extends Error {
105
108
  constructor() {
@@ -1676,6 +1679,12 @@ var COMMAND_TIMEOUT_MS = 5000;
1676
1679
  var STREAM_TIMEOUT_MS = 30000;
1677
1680
  var BACKGROUND_ID = /^[a-z0-9][a-z0-9_-]{0,63}$/;
1678
1681
  var AVATAR_RETURN_TIMEOUT_MS = 8000;
1682
+ var RUNTIME_RETURN_TIMEOUT_MS = 20000;
1683
+ 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
+ }
1679
1688
  function isReconnecting(lkState) {
1680
1689
  return lkState === ConnectionState2.Reconnecting || lkState === ConnectionState2.SignalReconnecting;
1681
1690
  }
@@ -1734,8 +1743,10 @@ class LiveKitConversation {
1734
1743
  everConnected = false;
1735
1744
  away = false;
1736
1745
  awayTimer = null;
1746
+ runtimeTimer = null;
1737
1747
  presentationPaused = false;
1738
1748
  readySending = false;
1749
+ contextSent = null;
1739
1750
  leaving = null;
1740
1751
  connectGeneration = 0;
1741
1752
  _id;
@@ -1744,6 +1755,8 @@ class LiveKitConversation {
1744
1755
  cameraFacing;
1745
1756
  tokenProvider;
1746
1757
  opensOnReady;
1758
+ presentsAtFullSize;
1759
+ initialContext;
1747
1760
  resilient;
1748
1761
  constructor(config) {
1749
1762
  this.room = new Room({
@@ -1765,6 +1778,8 @@ class LiveKitConversation {
1765
1778
  this.cameraFacing = config.mediaDevices?.cameraFacing;
1766
1779
  this.tokenProvider = config.tokenProvider;
1767
1780
  this.opensOnReady = config.opensOnReady ?? false;
1781
+ this.presentsAtFullSize = config.presentsAtFullSize ?? this.opensOnReady;
1782
+ this.initialContext = config.initialContext ?? [];
1768
1783
  this.resilient = config.resilientReconnect ?? false;
1769
1784
  }
1770
1785
  get id() {
@@ -1780,6 +1795,7 @@ class LiveKitConversation {
1780
1795
  this.sequence.stop();
1781
1796
  this.sequence = this.newSequence();
1782
1797
  this.readyOwed = false;
1798
+ this.contextSent = null;
1783
1799
  this.everConnected = false;
1784
1800
  if (!this.eventsRegistered) {
1785
1801
  this.registerRoomEvents();
@@ -2039,10 +2055,14 @@ class LiveKitConversation {
2039
2055
  queueMicrotask(() => {
2040
2056
  if (this.room.state === ConnectionState2.Disconnected)
2041
2057
  return;
2042
- if (isReconnecting(this.room.state))
2058
+ if (isReconnecting(this.room.state)) {
2043
2059
  this.avatarAway();
2044
- else
2060
+ } else if (this.sessionEnded) {
2045
2061
  this.avatarLeft();
2062
+ } else if (this.avatarInRoom()) {} else {
2063
+ this.avatarAway();
2064
+ this.awaitRuntimeReturn();
2065
+ }
2046
2066
  });
2047
2067
  });
2048
2068
  this.room.on(RoomEvent3.Disconnected, () => {
@@ -2150,6 +2170,7 @@ class LiveKitConversation {
2150
2170
  if (packet === null)
2151
2171
  return;
2152
2172
  this.speaksProtocol = true;
2173
+ this.sendContext();
2153
2174
  const { seq, event } = packet;
2154
2175
  if (this.sequence.repeats(seq))
2155
2176
  return;
@@ -2243,6 +2264,10 @@ class LiveKitConversation {
2243
2264
  this.emit({ type: "avatar-presented" });
2244
2265
  return;
2245
2266
  }
2267
+ if (!this.presentsAtFullSize) {
2268
+ this.markAvatarPresented();
2269
+ return;
2270
+ }
2246
2271
  this.startPresentation();
2247
2272
  }
2248
2273
  startPresentation() {
@@ -2253,7 +2278,7 @@ class LiveKitConversation {
2253
2278
  markAvatarPresented() {
2254
2279
  this.presentation = null;
2255
2280
  if (this.speaksProtocol && this.room.state === ConnectionState2.Connected) {
2256
- this.command({ type: "session.ready" }).catch(() => {
2281
+ this.ready().catch(() => {
2257
2282
  return;
2258
2283
  });
2259
2284
  } else if (this.resilient && this.speaksProtocol && isReconnecting(this.room.state)) {
@@ -2261,11 +2286,28 @@ class LiveKitConversation {
2261
2286
  }
2262
2287
  this.emit({ type: "avatar-presented" });
2263
2288
  }
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
+ }
2264
2306
  sendOwedReady() {
2265
2307
  if (!this.readyOwed || !this.speaksProtocol || this.readySending)
2266
2308
  return;
2267
2309
  this.readySending = true;
2268
- this.command({ type: "session.ready" }).then(() => {
2310
+ this.ready().then(() => {
2269
2311
  this.readyOwed = false;
2270
2312
  }).catch(() => {
2271
2313
  return;
@@ -2307,6 +2349,33 @@ class LiveKitConversation {
2307
2349
  this.avatarLeft();
2308
2350
  }, AVATAR_RETURN_TIMEOUT_MS);
2309
2351
  }
2352
+ awaitRuntimeReturn(delay = RUNTIME_RETURN_TIMEOUT_MS) {
2353
+ if (this.runtimeTimer !== null)
2354
+ return;
2355
+ this.runtimeTimer = setTimeout(() => {
2356
+ this.runtimeTimer = null;
2357
+ if (!this.away)
2358
+ return;
2359
+ if (isReconnecting(this.room.state)) {
2360
+ this.awaitRuntimeReturn(RUNTIME_RECHECK_MS);
2361
+ return;
2362
+ }
2363
+ if (this.room.state === ConnectionState2.Disconnected)
2364
+ return;
2365
+ if (this.avatarInRoom())
2366
+ return;
2367
+ this.clearAway();
2368
+ this.avatarLeft();
2369
+ this.reportEnd("connection-lost");
2370
+ }, delay);
2371
+ }
2372
+ avatarInRoom() {
2373
+ for (const [, participant] of this.room.remoteParticipants) {
2374
+ if (this.isAvatarParticipant(participant))
2375
+ return true;
2376
+ }
2377
+ return false;
2378
+ }
2310
2379
  avatarBack() {
2311
2380
  const resume = this.presentationPaused && this._avatar.isReady;
2312
2381
  this.clearAway();
@@ -2322,6 +2391,10 @@ class LiveKitConversation {
2322
2391
  clearTimeout(this.awayTimer);
2323
2392
  this.awayTimer = null;
2324
2393
  }
2394
+ if (this.runtimeTimer !== null) {
2395
+ clearTimeout(this.runtimeTimer);
2396
+ this.runtimeTimer = null;
2397
+ }
2325
2398
  }
2326
2399
  startLayerFallback() {
2327
2400
  if (this.layerFallback !== null)
@@ -2429,6 +2502,7 @@ class NuanceConversation {
2429
2502
  sessionOverrides;
2430
2503
  personality;
2431
2504
  tools;
2505
+ initialContext;
2432
2506
  variant;
2433
2507
  maxSessionSeconds;
2434
2508
  videoCapture;
@@ -2468,6 +2542,7 @@ class NuanceConversation {
2468
2542
  this.sessionOverrides = options.sessionOverrides ?? {};
2469
2543
  this.personality = options.personality;
2470
2544
  this.tools = options.tools;
2545
+ this.initialContext = options.initialContext ?? [];
2471
2546
  this.variant = options.variant;
2472
2547
  this.maxSessionSeconds = options.maxSessionSeconds;
2473
2548
  this.videoCapture = options.videoCapture;
@@ -2497,7 +2572,7 @@ class NuanceConversation {
2497
2572
  try {
2498
2573
  const request = {
2499
2574
  session_overrides: this.sessionOverrides,
2500
- client_ready: this.startOn === "full-quality"
2575
+ client_ready: this.startOn === "full-quality" || this.initialContext.length > 0
2501
2576
  };
2502
2577
  const timezone = this.timezone();
2503
2578
  if (timezone)
@@ -2778,7 +2853,7 @@ class NuanceConversation {
2778
2853
  return "time-limit";
2779
2854
  if (reason === "policy_violation")
2780
2855
  return "policy-violation";
2781
- if (this.cutOff())
2856
+ if (reason === "runtime_silent" || this.cutOff())
2782
2857
  return "connection-lost";
2783
2858
  return "ended";
2784
2859
  }
@@ -2809,6 +2884,8 @@ class NuanceConversation {
2809
2884
  cameraEnabled: !this.sessionOverrides.disable_camera && mediaDevices.cameraEnabled !== false,
2810
2885
  videoCapture: this.videoCapture,
2811
2886
  opensOnReady: conversation.provider_metadata?.client_ready === true,
2887
+ presentsAtFullSize: this.startOn === "full-quality",
2888
+ initialContext: this.initialContext,
2812
2889
  resilientReconnect: this.resilient
2813
2890
  });
2814
2891
  }
@@ -3012,5 +3089,6 @@ export {
3012
3089
  LegalRequiredError,
3013
3090
  GuestLimitError,
3014
3091
  ConversationState,
3092
+ ApiError,
3015
3093
  ActiveSessionError
3016
3094
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuancelabs/js-sdk",
3
- "version": "0.13.0",
3
+ "version": "0.14.1",
4
4
  "description": "JavaScript SDK for Nuance Labs conversation video AI avatar",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {