@nuancelabs/js-sdk 0.9.0 → 0.10.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.
Files changed (3) hide show
  1. package/index.d.ts +16 -0
  2. package/index.js +77 -5
  3. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -376,6 +376,18 @@ export declare interface CreateNuanceConversationOptions {
376
376
  }
377
377
 
378
378
 
379
+ /**
380
+ * Raised by {@link Conversation.connect} when the server refuses a guest's new
381
+ * session because a guest limit is used up. `reason` says which: `used` (this
382
+ * guest has had its conversations), `device` (so has this browser) or `network`
383
+ * (so has this network today). The message is the server's, written for the
384
+ * person.
385
+ */
386
+ export declare class GuestLimitError extends Error {
387
+ readonly reason: string;
388
+ constructor(reason: string, message: string);
389
+ }
390
+
379
391
  /**
380
392
  * Raised by {@link Conversation.connect} when the server refuses a new session
381
393
  * because the account has not completed onboarding or accepted the current
@@ -500,6 +512,10 @@ export declare interface SessionTokenResponse {
500
512
  /** When the avatar starts speaking: see {@link CreateNuanceConversationOptions.startOn}. */
501
513
  export declare type StartOn = "first-frame" | "full-quality";
502
514
 
515
+
516
+
517
+
518
+
503
519
  /**
504
520
  * The local user participant in a conversation.
505
521
  */
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.9.0"}`;
7
+ var SDK_HEADER = `js/${"0.10.1"}`;
8
8
 
9
9
  // src/utils.ts
10
10
  function normalizeHost(host) {
@@ -108,6 +108,16 @@ class ActiveSessionError extends Error {
108
108
  }
109
109
  }
110
110
 
111
+ // src/core/guest-limit-error.ts
112
+ class GuestLimitError extends Error {
113
+ reason;
114
+ constructor(reason, message) {
115
+ super(message);
116
+ this.name = "GuestLimitError";
117
+ this.reason = reason;
118
+ }
119
+ }
120
+
111
121
  // src/core/legal-required-error.ts
112
122
  class LegalRequiredError extends Error {
113
123
  constructor() {
@@ -1088,6 +1098,17 @@ function parseSessionEvent(payload) {
1088
1098
  if (decoded.type === "floor") {
1089
1099
  return { type: "floor", speaking: decoded.state === "speaking" };
1090
1100
  }
1101
+ if (decoded.type === "tool.call" && typeof decoded.call_id === "string" && typeof decoded.name === "string" && isRecord(decoded.arguments)) {
1102
+ return {
1103
+ type: "tool-call",
1104
+ call: {
1105
+ callId: decoded.call_id,
1106
+ name: decoded.name,
1107
+ label: typeof decoded.label === "string" ? decoded.label : "",
1108
+ arguments: decoded.arguments
1109
+ }
1110
+ };
1111
+ }
1091
1112
  if (decoded.type === "session.status" && decoded.status === "ended") {
1092
1113
  return {
1093
1114
  type: "ended",
@@ -1534,6 +1555,7 @@ class LiveKitUserParticipant {
1534
1555
  // src/providers/livekit/conversation.ts
1535
1556
  var DATA_EVENT_TOPIC = "nuance.data";
1536
1557
  var SESSION_TOPIC = "nuance.v1";
1558
+ var PACKET_MAX_BYTES = 14000;
1537
1559
  var RUNTIME_TOPIC = "runtime";
1538
1560
  var CLOCK_SYNC_TOPIC = "clock-sync";
1539
1561
  var STATS_FLUSH_TIMEOUT_MS = 300;
@@ -1541,6 +1563,7 @@ var SESSION_END_TIMEOUT_MS = 300;
1541
1563
  var SESSION_END_ID = "client-session-end";
1542
1564
  var REFUSAL_REASON_WAIT_MS = 500;
1543
1565
  var COMMAND_TIMEOUT_MS = 5000;
1566
+ var STREAM_TIMEOUT_MS = 30000;
1544
1567
  var BACKGROUND_ID = /^[a-z0-9][a-z0-9_-]{0,63}$/;
1545
1568
  var AVATAR_RETURN_TIMEOUT_MS = 8000;
1546
1569
  function isReconnecting(lkState) {
@@ -1754,9 +1777,24 @@ class LiveKitConversation {
1754
1777
  async releaseAgent() {
1755
1778
  return this.command({ type: "floor.release" });
1756
1779
  }
1780
+ async sendToolResult(callId, output, options = {}) {
1781
+ return this.command({
1782
+ type: "tool.result",
1783
+ call_id: callId,
1784
+ output,
1785
+ ...options.images?.length && {
1786
+ images: options.images.map(({ mediaType, data }) => ({
1787
+ media_type: mediaType,
1788
+ data
1789
+ }))
1790
+ },
1791
+ is_error: options.isError ?? false,
1792
+ final: options.final ?? true
1793
+ });
1794
+ }
1757
1795
  onChange(handler) {
1758
1796
  return this.onEvent((event) => {
1759
- if (event.type !== "data-event" && event.type !== "screen-share-changed")
1797
+ if (event.type !== "data-event" && event.type !== "screen-share-changed" && event.type !== "tool-call")
1760
1798
  handler(event);
1761
1799
  });
1762
1800
  }
@@ -1773,6 +1811,12 @@ class LiveKitConversation {
1773
1811
  }
1774
1812
  });
1775
1813
  }
1814
+ onToolCall(handler) {
1815
+ return this.onEvent((event) => {
1816
+ if (event.type === "tool-call")
1817
+ handler(event.call);
1818
+ });
1819
+ }
1776
1820
  async command(command) {
1777
1821
  if (this.room.state !== ConnectionState2.Connected) {
1778
1822
  throw new Error("Not connected — call connect() first");
@@ -1784,11 +1828,18 @@ class LiveKitConversation {
1784
1828
  const answer = Promise.withResolvers();
1785
1829
  this.commands.set(id, { type: command.type, settle: answer.resolve });
1786
1830
  let timer;
1831
+ const text = JSON.stringify({ ...command, id });
1832
+ const payload = new TextEncoder().encode(text);
1833
+ const streamed = payload.byteLength > PACKET_MAX_BYTES;
1834
+ const sent = streamed ? this.room.localParticipant.sendText(text, { topic: SESSION_TOPIC }) : this.room.localParticipant.publishData(payload, {
1835
+ reliable: true,
1836
+ topic: SESSION_TOPIC
1837
+ });
1787
1838
  try {
1788
1839
  return await Promise.race([
1789
- this.room.localParticipant.publishData(new TextEncoder().encode(JSON.stringify({ ...command, id })), { reliable: true, topic: SESSION_TOPIC }).then(() => answer.promise),
1840
+ sent.then(() => answer.promise),
1790
1841
  new Promise((_, reject) => {
1791
- timer = setTimeout(() => reject(new Error(`The runtime did not answer ${command.type}`)), COMMAND_TIMEOUT_MS);
1842
+ timer = setTimeout(() => reject(new Error(`The runtime did not answer ${command.type}`)), streamed ? STREAM_TIMEOUT_MS : COMMAND_TIMEOUT_MS);
1792
1843
  })
1793
1844
  ]);
1794
1845
  } finally {
@@ -1996,6 +2047,8 @@ class LiveKitConversation {
1996
2047
  }
1997
2048
  } else if (event.type === "floor") {
1998
2049
  this.setAvatarSpeaking(event.speaking);
2050
+ } else if (event.type === "tool-call") {
2051
+ this.emit({ type: "tool-call", call: event.call });
1999
2052
  } else if (event.type === "ended") {
2000
2053
  this.answered?.();
2001
2054
  if (!event.byClient)
@@ -2211,6 +2264,9 @@ function isMaintenanceBody(body) {
2211
2264
  function isBusyBody(body) {
2212
2265
  return isRecord(body) && body.code === "busy";
2213
2266
  }
2267
+ function isGuestLimitBody(body) {
2268
+ return isRecord(body) && body.code === "guest_limit";
2269
+ }
2214
2270
  function isCurfewBody(body) {
2215
2271
  return isRecord(body) && body.code === "policy_curfew";
2216
2272
  }
@@ -2325,6 +2381,9 @@ class NuanceConversation {
2325
2381
  if (error instanceof ApiError && error.status === 503 && isMaintenanceBody(error.body)) {
2326
2382
  throw new MaintenanceError(typeof error.body.detail === "string" ? error.body.detail : "The service is under maintenance");
2327
2383
  }
2384
+ if (error instanceof ApiError && error.status === 429 && isGuestLimitBody(error.body)) {
2385
+ throw new GuestLimitError(typeof error.body.reason === "string" ? error.body.reason : "used", typeof error.body.detail === "string" ? error.body.detail : "You've had your free conversation");
2386
+ }
2328
2387
  if (error instanceof ApiError && error.status === 403 && isCurfewBody(error.body)) {
2329
2388
  throw new PolicyCurfewError(typeof error.body.retry_after_seconds === "number" ? error.body.retry_after_seconds : null);
2330
2389
  }
@@ -2430,9 +2489,15 @@ class NuanceConversation {
2430
2489
  }
2431
2490
  return this.delegate.releaseAgent();
2432
2491
  }
2492
+ async sendToolResult(callId, output, options) {
2493
+ if (!this.delegate) {
2494
+ throw new Error("Not connected — call connect() first");
2495
+ }
2496
+ return this.delegate.sendToolResult(callId, output, options);
2497
+ }
2433
2498
  onChange(handler) {
2434
2499
  return this.onEvent((event) => {
2435
- if (event.type !== "data-event" && event.type !== "screen-share-changed")
2500
+ if (event.type !== "data-event" && event.type !== "screen-share-changed" && event.type !== "tool-call")
2436
2501
  handler(event);
2437
2502
  });
2438
2503
  }
@@ -2449,6 +2514,12 @@ class NuanceConversation {
2449
2514
  }
2450
2515
  });
2451
2516
  }
2517
+ onToolCall(handler) {
2518
+ return this.onEvent((event) => {
2519
+ if (event.type === "tool-call")
2520
+ handler(event.call);
2521
+ });
2522
+ }
2452
2523
  async waitForSlot(conversationId, attempt) {
2453
2524
  const admission = await this.followEvents(conversationId, attempt);
2454
2525
  if (admission !== null) {
@@ -2795,6 +2866,7 @@ export {
2795
2866
  MediaAccessError,
2796
2867
  MaintenanceError,
2797
2868
  LegalRequiredError,
2869
+ GuestLimitError,
2798
2870
  ConversationState,
2799
2871
  ActiveSessionError
2800
2872
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuancelabs/js-sdk",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "JavaScript SDK for Nuance Labs conversation video AI avatar",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {