@nuancelabs/js-sdk 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/index.d.ts +324 -0
  2. package/index.js +158 -14
  3. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -7,6 +7,85 @@ export declare class ActiveSessionError extends Error {
7
7
  constructor();
8
8
  }
9
9
 
10
+ /**
11
+ * The turn's first audio. `atMs` is when it plays, which can be after the
12
+ * event arrives.
13
+ */
14
+ export declare interface AgentSpeechStartedEvent extends SessionEventBase {
15
+ type: "agent.speech.started";
16
+ /** The turn, as {@link AgentTurnStartedEvent.turnId} names it. */
17
+ turnId: string;
18
+ }
19
+
20
+ /**
21
+ * The agent's words, in whole words, as they are committed to the
22
+ * conversation. They arrive ahead of their audio: `atMs` is when the first
23
+ * of them plays. When the agent's turn is interrupted, words whose `atMs` is
24
+ * later than the {@link AgentTurnEndedEvent}'s were never heard.
25
+ */
26
+ export declare interface AgentTextEvent extends SessionEventBase {
27
+ type: "agent.text";
28
+ /**
29
+ * The turn the words belong to, as {@link AgentTurnStartedEvent.turnId}
30
+ * names it; null for words outside any turn.
31
+ */
32
+ turnId: string | null;
33
+ /** The words, with the spacing to join them to the turn's other words. */
34
+ text: string;
35
+ }
36
+
37
+ /**
38
+ * The agent's turn is over. `atMs` is when the turn's audio ends: for an
39
+ * interrupted turn, where the agent was cut off.
40
+ */
41
+ export declare interface AgentTurnEndedEvent extends SessionEventBase {
42
+ type: "agent.turn.ended";
43
+ /** The turn, as {@link AgentTurnStartedEvent.turnId} names it. */
44
+ turnId: string;
45
+ /**
46
+ * `"completed"`: the agent said all of it. `"interrupted"`: the user cut
47
+ * in. `"stopped"`: a command stopped the agent. `"abandoned"`: the turn
48
+ * gave up, as when nothing of it was heard for a while or it had nothing
49
+ * to say; `spoken` is then the runtime's estimate of what played. A newer
50
+ * runtime may send other values.
51
+ */
52
+ reason: "completed" | "interrupted" | "stopped" | "abandoned" | (string & {});
53
+ /**
54
+ * Every word the turn committed, as its {@link AgentTextEvent}s carried
55
+ * them. After an interruption it includes words that were cut before they
56
+ * played; their `agent.text` events have an `atMs` later than this
57
+ * event's.
58
+ */
59
+ spoken: string;
60
+ }
61
+
62
+ /**
63
+ * The agent took the floor, before its first audio. Its words follow as
64
+ * {@link AgentTextEvent}s and the turn closes with an
65
+ * {@link AgentTurnEndedEvent}, both with the same `turnId`. `atMs` is where
66
+ * on the timeline the agent took it.
67
+ */
68
+ export declare interface AgentTurnStartedEvent extends SessionEventBase {
69
+ type: "agent.turn.started";
70
+ /** The turn's id, unique within the session. */
71
+ turnId: string;
72
+ /**
73
+ * What opened the turn. `"user_done"`: the agent judged the user had
74
+ * finished. `"silence"`: the user went quiet long enough. `"check_in"`:
75
+ * both were quiet and the agent spoke first, as in its opening greeting.
76
+ * `"command"`: a command asked for the turn. `"tool"`: a tool's result
77
+ * reached the agent while both were quiet. A newer runtime may send other
78
+ * values.
79
+ */
80
+ openedBy: "user_done" | "silence" | "check_in" | "command" | "tool" | (string & {});
81
+ /**
82
+ * `"reply"`: the agent wrote the turn's words itself. `"say"`: the agent
83
+ * says a line it was given, word for word. A newer runtime may send other
84
+ * values.
85
+ */
86
+ source: "reply" | "say" | (string & {});
87
+ }
88
+
10
89
  /**
11
90
  * An audio track that can be attached to or detached from an audio element.
12
91
  */
@@ -119,6 +198,56 @@ export declare interface Conversation {
119
198
  * @returns A function that removes the event handler.
120
199
  */
121
200
  onChange(handler: (event: ConversationEvent) => void): () => void;
201
+ /**
202
+ * Subscribes to one type of the session's events: what the user and the
203
+ * agent say, their turns, and who has the floor.
204
+ *
205
+ * Each event arrives once, in the session's order (its `seq`). Events the
206
+ * connection missed while it was down are asked of the runtime again and
207
+ * delivered in their place. Subscribe before `connect()` to see the
208
+ * session's first events.
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * const stop = conversation.on("agent.text", (event) => {
213
+ * transcript.textContent += event.text;
214
+ * });
215
+ * ```
216
+ *
217
+ * @returns A function that removes the handler.
218
+ */
219
+ on<K extends keyof SessionEventMap>(type: K, handler: (event: SessionEventMap[K]) => void): () => void;
220
+ /**
221
+ * Subscribes to the agent's calls of the conversation's tools, named when
222
+ * your server mints the session token. Run the tool in your page and answer each call with
223
+ * {@link Conversation.sendToolResult}. The agent keeps talking while a call
224
+ * is open, and uses the result when it arrives.
225
+ *
226
+ * @example
227
+ * ```ts
228
+ * conversation.onToolCall(async (call) => {
229
+ * if (call.name === "show_confetti") {
230
+ * launchConfetti();
231
+ * await conversation.sendToolResult(call.callId, "Confetti is falling.");
232
+ * }
233
+ * });
234
+ * ```
235
+ *
236
+ * @returns A function that removes the handler.
237
+ */
238
+ onToolCall(handler: (call: ToolCall) => void): () => void;
239
+ /**
240
+ * Answers a tool call: what it produced, or with `isError` what went wrong.
241
+ * The agent keeps the first 2,000 characters of an output, so return what
242
+ * it needs, not everything you have.
243
+ *
244
+ * @param callId - The call's {@link ToolCall.callId}.
245
+ * @returns Whether the runtime took the result: `status` is `"rejected"`,
246
+ * with the reason in `error`, for an unknown or closed call.
247
+ * @throws If the conversation is not connected, or the runtime does not
248
+ * answer in time.
249
+ */
250
+ sendToolResult(callId: string, output: ToolOutput, options?: ToolResultOptions): Promise<CommandResult>;
122
251
  }
123
252
 
124
253
 
@@ -376,6 +505,46 @@ export declare interface CreateNuanceConversationOptions {
376
505
  }
377
506
 
378
507
 
508
+ /**
509
+ * Who has the floor changed. `atMs` is when the change plays.
510
+ */
511
+ export declare interface FloorEvent extends SessionEventBase {
512
+ type: "floor";
513
+ /**
514
+ * `"speaking"`: the agent has the floor. `"listening"`: the user has it
515
+ * and the agent listens. `"waiting"`: nobody has it; the agent waits for
516
+ * the user. A newer runtime may send other values.
517
+ */
518
+ state: "waiting" | "listening" | "speaking" | (string & {});
519
+ }
520
+
521
+ /**
522
+ * A tool the agent may call, which your page runs: show something on the
523
+ * screen, read what the user has open, look something up in your app. The
524
+ * agent decides when to call it from its `description`, and fills its
525
+ * arguments to match its `parameters`. A conversation has up to 16 tools.
526
+ */
527
+ export declare interface FunctionTool {
528
+ /** Always `"function"`, the default. */
529
+ type?: "function";
530
+ /**
531
+ * The tool's name: letters, digits, `_` and `-`, up to 64 characters,
532
+ * unique in the conversation. `ask_user`, `get_current_time`, `web_search`
533
+ * and the agent's other own tools' names are taken.
534
+ */
535
+ name: string;
536
+ /**
537
+ * What the tool does and when to use it, written for the agent, up to
538
+ * 2,000 characters. Say when to call it, not only what it does.
539
+ */
540
+ description: string;
541
+ /**
542
+ * The arguments, as a JSON Schema whose `type` is `"object"`, up to 16,000
543
+ * characters as compact JSON. `{ type: "object", properties: {} }` takes none.
544
+ */
545
+ parameters: Record<string, unknown>;
546
+ }
547
+
379
548
  /**
380
549
  * Raised by {@link Conversation.connect} when the server refuses a guest's new
381
550
  * session because a guest limit is used up. `reason` says which: `used` (this
@@ -473,6 +642,47 @@ export declare class PolicyCurfewError extends Error {
473
642
  */
474
643
  export declare type SessionEndReason = "time-limit" | "ended" | "policy-violation" | "refused" | "connection-lost";
475
644
 
645
+ /** Any one of the session's events. */
646
+ export declare type SessionEvent = SessionEventMap[keyof SessionEventMap];
647
+
648
+ /**
649
+ * What every session event carries: its place in the session's order and on
650
+ * the session's timeline.
651
+ */
652
+ export declare interface SessionEventBase {
653
+ /**
654
+ * The event's number in the session's sequence: from 1, one more for each
655
+ * event the runtime sends, of every kind, so the events one handler sees
656
+ * skip numbers. The SDK delivers events in this order, each once. 0 for an
657
+ * event the runtime did not number.
658
+ */
659
+ seq: number;
660
+ /**
661
+ * Where the event falls on the session's timeline, in milliseconds. The
662
+ * timeline counts the user's audio from the start of the session. A user
663
+ * event is stamped on the user's audio, as where the user said it; an
664
+ * agent event where the agent's audio for it plays, which can be ahead of
665
+ * when the event arrives. Every event of a session is on this one timeline, so
666
+ * their `atMs` compare. Null when the runtime gives no time.
667
+ */
668
+ atMs: number | null;
669
+ }
670
+
671
+ /**
672
+ * The session's events by their `type`, for {@link Conversation.on}.
673
+ */
674
+ export declare interface SessionEventMap {
675
+ "user.transcript": UserTranscriptEvent;
676
+ "user.speech.started": UserSpeechStartedEvent;
677
+ "user.speech.ended": UserSpeechEndedEvent;
678
+ "user.turn.ended": UserTurnEndedEvent;
679
+ "agent.turn.started": AgentTurnStartedEvent;
680
+ "agent.speech.started": AgentSpeechStartedEvent;
681
+ "agent.text": AgentTextEvent;
682
+ "agent.turn.ended": AgentTurnEndedEvent;
683
+ floor: FloorEvent;
684
+ }
685
+
476
686
 
477
687
 
478
688
  /** Who the avatar is in a conversation. */
@@ -499,6 +709,23 @@ export declare interface SessionPersonality {
499
709
  export declare interface SessionTokenRequest {
500
710
  /** Who the avatar is in the conversation. Leave it out for the default personality. */
501
711
  personality?: SessionPersonality;
712
+ /**
713
+ * Tools the agent may call, which your page runs and answers with
714
+ * `onToolCall` and `sendToolResult`;
715
+ * up to 16. Fixed for the conversation: the page can't change them.
716
+ *
717
+ * @example
718
+ * ```ts
719
+ * tools: [
720
+ * {
721
+ * name: "show_confetti",
722
+ * description: "Plays confetti on the user's screen. Use it to celebrate with them.",
723
+ * parameters: { type: "object", properties: {} },
724
+ * },
725
+ * ]
726
+ * ```
727
+ */
728
+ tools?: FunctionTool[];
502
729
  /**
503
730
  * Your own id for the person this conversation is for, such as a hash of
504
731
  * your user id. After repeated policy violations, new conversations pause
@@ -521,9 +748,53 @@ export declare interface SessionTokenResponse {
521
748
  /** When the avatar starts speaking: see {@link CreateNuanceConversationOptions.startOn}. */
522
749
  export declare type StartOn = "first-frame" | "full-quality";
523
750
 
751
+ /**
752
+ * The agent's call of one of the conversation's tools: run it, then answer
753
+ * with {@link Conversation.sendToolResult} and its `callId`.
754
+ */
755
+ export declare interface ToolCall {
756
+ /** Identifies the call; answer with it. */
757
+ callId: string;
758
+ /** The tool, by its `name`. */
759
+ name: string;
760
+ /** The agent's own short name for this use of the tool, such as `"confetti"`. */
761
+ label: string;
762
+ /** The call's arguments, held to the tool's `parameters` schema. */
763
+ arguments: Record<string, unknown>;
764
+ }
524
765
 
766
+ /**
767
+ * An image a tool produced, for the agent to look at: a screenshot, say, or
768
+ * a page of a document. Up to 8,000,000 base64 characters (about 6 MB); the
769
+ * runtime fits it to 1920 px on its long side.
770
+ */
771
+ export declare interface ToolImage {
772
+ mediaType: "image/jpeg" | "image/png";
773
+ /** The encoded image, base64. */
774
+ data: string;
775
+ }
525
776
 
777
+ /**
778
+ * What a tool call produced: text, or JSON the agent reads as it is. Up to
779
+ * 24,000 characters (its JSON, for an object); the agent keeps the first 2,000.
780
+ */
781
+ export declare type ToolOutput = string | Record<string, unknown> | unknown[];
526
782
 
783
+ /**
784
+ * How {@link Conversation.sendToolResult} reports a result.
785
+ */
786
+ export declare interface ToolResultOptions {
787
+ /** Images the tool produced, up to 4; the agent looks at them, in order,
788
+ * where the result enters the conversation. */
789
+ images?: ToolImage[];
790
+ /** The output says what went wrong rather than what the tool produced. */
791
+ isError?: boolean;
792
+ /** Whether this is the call's last part; a part with `final: false` adds to
793
+ * what the agent knows and keeps the call open.
794
+ *
795
+ * @defaultValue `true` */
796
+ final?: boolean;
797
+ }
527
798
 
528
799
  /**
529
800
  * The local user participant in a conversation.
@@ -561,6 +832,59 @@ export declare interface UserParticipant {
561
832
  readonly isSpeaking: boolean;
562
833
  }
563
834
 
835
+ /**
836
+ * The user's voice went off, as the same detection reads it. `atMs` is where
837
+ * on the timeline it went off.
838
+ */
839
+ export declare interface UserSpeechEndedEvent extends SessionEventBase {
840
+ type: "user.speech.ended";
841
+ }
842
+
843
+ /**
844
+ * The user's voice came on, as the runtime's voice-activity detection reads
845
+ * the microphone. `atMs` is where on the timeline it came on.
846
+ */
847
+ export declare interface UserSpeechStartedEvent extends SessionEventBase {
848
+ type: "user.speech.started";
849
+ }
850
+
851
+ /**
852
+ * The user's words, live: one recognized segment at a time, as the runtime
853
+ * hears them. A later {@link UserTurnEndedEvent} may read the same speech
854
+ * differently; its `text` is the record of what the agent answered.
855
+ */
856
+ export declare interface UserTranscriptEvent extends SessionEventBase {
857
+ type: "user.transcript";
858
+ /** The segment's words. */
859
+ text: string;
860
+ /**
861
+ * `"asr"`: recognized from the user's microphone. `"text"`: a line given
862
+ * as the user's turn in text rather than spoken. `"forced"`: a transcript
863
+ * supplied for the user's audio in place of recognition. A newer runtime
864
+ * may send other values.
865
+ */
866
+ source: "asr" | "forced" | "text" | (string & {});
867
+ }
868
+
869
+ /**
870
+ * The agent is taking the turn to answer the user; sent each time it does.
871
+ * `atMs` is the point in the user's audio where the agent took the turn: the
872
+ * same moment as the turn's {@link AgentTurnStartedEvent}, stamped on the
873
+ * user's side of the timeline, so it is earlier than that event's `atMs` by
874
+ * the delay before the agent's audio plays.
875
+ */
876
+ export declare interface UserTurnEndedEvent extends SessionEventBase {
877
+ type: "user.turn.ended";
878
+ /**
879
+ * The user's words since the agent last spoke, as the agent read them: a
880
+ * second, more careful hearing's words in place of the live
881
+ * {@link UserTranscriptEvent}s' where the runtime has one. When an agent
882
+ * turn ends before it said a word, the next `user.turn.ended` covers the
883
+ * same words again.
884
+ */
885
+ text: string;
886
+ }
887
+
564
888
  /**
565
889
  * Capture constraints for the user's published camera track.
566
890
  *
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.11.0"}`;
7
+ var SDK_HEADER = `js/${"0.13.0"}`;
8
8
 
9
9
  // src/utils.ts
10
10
  function normalizeHost(host) {
@@ -185,6 +185,15 @@ class PolicyCurfewError extends Error {
185
185
  }
186
186
  }
187
187
 
188
+ // src/core/session-events.ts
189
+ function sessionEventsOf(type, handler) {
190
+ return (change) => {
191
+ if (change.type === "session-event" && change.event.type === type) {
192
+ handler(change.event);
193
+ }
194
+ };
195
+ }
196
+
188
197
  // src/core/ticker.ts
189
198
  var WORKER_SOURCE = "onmessage = (e) => { setInterval(() => postMessage(0), e.data); };";
190
199
  function backgroundTicker(host) {
@@ -1059,6 +1068,69 @@ async function avatarFramesDecoded(room) {
1059
1068
  return null;
1060
1069
  }
1061
1070
 
1071
+ // src/providers/livekit/sequence.ts
1072
+ class EventSequence {
1073
+ deliver;
1074
+ replay;
1075
+ last = 0;
1076
+ held = new Map;
1077
+ replaying = false;
1078
+ stopped = false;
1079
+ constructor(deliver, replay) {
1080
+ this.deliver = deliver;
1081
+ this.replay = replay;
1082
+ }
1083
+ repeats(seq) {
1084
+ return seq !== 0 && (seq <= this.last || this.held.has(seq));
1085
+ }
1086
+ accept(seq, item) {
1087
+ if (this.stopped || this.repeats(seq))
1088
+ return;
1089
+ if (seq === 0) {
1090
+ this.deliver(item);
1091
+ return;
1092
+ }
1093
+ if (seq !== this.last + 1) {
1094
+ this.held.set(seq, item);
1095
+ this.requestReplay();
1096
+ return;
1097
+ }
1098
+ this.last = seq;
1099
+ this.deliver(item);
1100
+ this.release(false);
1101
+ }
1102
+ catchUp() {
1103
+ if (!this.stopped)
1104
+ this.requestReplay();
1105
+ }
1106
+ stop() {
1107
+ this.stopped = true;
1108
+ this.held.clear();
1109
+ }
1110
+ requestReplay() {
1111
+ if (this.replaying)
1112
+ return;
1113
+ this.replaying = true;
1114
+ this.replay(this.last).catch(() => {
1115
+ return;
1116
+ }).finally(() => {
1117
+ this.replaying = false;
1118
+ this.release(true);
1119
+ });
1120
+ }
1121
+ release(all) {
1122
+ const pending = [...this.held.keys()].sort((a, b) => a - b);
1123
+ for (const seq of pending) {
1124
+ if (this.stopped || !all && seq !== this.last + 1)
1125
+ return;
1126
+ const item = this.held.get(seq);
1127
+ this.held.delete(seq);
1128
+ this.last = seq;
1129
+ this.deliver(item);
1130
+ }
1131
+ }
1132
+ }
1133
+
1062
1134
  // src/providers/livekit/signals.ts
1063
1135
  function decode(payload) {
1064
1136
  try {
@@ -1082,6 +1154,10 @@ function parseSessionEvent(payload) {
1082
1154
  const decoded = decode(payload);
1083
1155
  if (!decoded)
1084
1156
  return null;
1157
+ const seq = Number.isSafeInteger(decoded.seq) && decoded.seq > 0 ? decoded.seq : 0;
1158
+ return { seq, event: protocolEvent(decoded, seq) };
1159
+ }
1160
+ function protocolEvent(decoded, seq) {
1085
1161
  if (decoded.type === "session.output_ready") {
1086
1162
  return { type: "output-ready" };
1087
1163
  }
@@ -1095,9 +1171,6 @@ function parseSessionEvent(payload) {
1095
1171
  }
1096
1172
  };
1097
1173
  }
1098
- if (decoded.type === "floor") {
1099
- return { type: "floor", speaking: decoded.state === "speaking" };
1100
- }
1101
1174
  if (decoded.type === "tool.call" && typeof decoded.call_id === "string" && typeof decoded.name === "string" && isRecord(decoded.arguments)) {
1102
1175
  return {
1103
1176
  type: "tool-call",
@@ -1130,7 +1203,44 @@ function parseSessionEvent(payload) {
1130
1203
  if (decoded.type === "error" && decoded.code === "bad_command") {
1131
1204
  return { type: "command-unreadable" };
1132
1205
  }
1133
- return { type: "other" };
1206
+ const event = sessionEvent(decoded, seq);
1207
+ return event ? { type: "session-event", event } : { type: "other" };
1208
+ }
1209
+ var isString = (value) => typeof value === "string";
1210
+ function defaulted(value, absent) {
1211
+ if (value === undefined)
1212
+ return absent;
1213
+ return isString(value) ? value : null;
1214
+ }
1215
+ var READERS = {
1216
+ "user.transcript": (wire) => {
1217
+ const source = defaulted(wire.source, "asr");
1218
+ return isString(wire.text) && source !== null ? { text: wire.text, source } : null;
1219
+ },
1220
+ "user.speech.started": () => ({}),
1221
+ "user.speech.ended": () => ({}),
1222
+ "user.turn.ended": (wire) => isString(wire.text) ? { text: wire.text } : null,
1223
+ "agent.turn.started": (wire) => {
1224
+ const source = defaulted(wire.source, "reply");
1225
+ return isString(wire.turn_id) && isString(wire.opened_by) && source !== null ? { turnId: wire.turn_id, openedBy: wire.opened_by, source } : null;
1226
+ },
1227
+ "agent.speech.started": (wire) => isString(wire.turn_id) ? { turnId: wire.turn_id } : null,
1228
+ "agent.text": (wire) => {
1229
+ const turnId = wire.turn_id ?? null;
1230
+ return (turnId === null || isString(turnId)) && isString(wire.text) ? { turnId, text: wire.text } : null;
1231
+ },
1232
+ "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,
1233
+ floor: (wire) => isString(wire.state) ? { state: wire.state } : null
1234
+ };
1235
+ function sessionEvent(decoded, seq) {
1236
+ const type = decoded.type;
1237
+ if (!isString(type) || !Object.hasOwn(READERS, type))
1238
+ return null;
1239
+ const fields = READERS[type](decoded);
1240
+ if (fields === null)
1241
+ return null;
1242
+ const atMs = Number.isFinite(decoded.at_ms) ? decoded.at_ms : null;
1243
+ return { type, seq, atMs, ...fields };
1134
1244
  }
1135
1245
  function parseRuntimeSignal(payload) {
1136
1246
  const decoded = decode(payload);
@@ -1616,6 +1726,7 @@ class LiveKitConversation {
1616
1726
  refusal = null;
1617
1727
  speaksProtocol = false;
1618
1728
  avatarSpeaking = false;
1729
+ sequence = this.newSequence();
1619
1730
  answered = null;
1620
1731
  commands = new Map;
1621
1732
  commandCount = 0;
@@ -1666,6 +1777,8 @@ class LiveKitConversation {
1666
1777
  this.setState("connecting");
1667
1778
  this.sessionEnded = false;
1668
1779
  this.speaksProtocol = false;
1780
+ this.sequence.stop();
1781
+ this.sequence = this.newSequence();
1669
1782
  this.readyOwed = false;
1670
1783
  this.everConnected = false;
1671
1784
  if (!this.eventsRegistered) {
@@ -1739,6 +1852,7 @@ class LiveKitConversation {
1739
1852
  this._user?.releaseDevices();
1740
1853
  await this.flushStats();
1741
1854
  await this.sendSessionEnd();
1855
+ this.sequence.stop();
1742
1856
  await this.room.disconnect();
1743
1857
  this._avatar.updateParticipant(null);
1744
1858
  this._user = null;
@@ -1794,10 +1908,13 @@ class LiveKitConversation {
1794
1908
  }
1795
1909
  onChange(handler) {
1796
1910
  return this.onEvent((event) => {
1797
- if (event.type !== "data-event" && event.type !== "screen-share-changed" && event.type !== "tool-call")
1911
+ if (event.type !== "data-event" && event.type !== "screen-share-changed" && event.type !== "tool-call" && event.type !== "session-event")
1798
1912
  handler(event);
1799
1913
  });
1800
1914
  }
1915
+ on(type, handler) {
1916
+ return this.onEvent(sessionEventsOf(type, handler));
1917
+ }
1801
1918
  onEvent(handler) {
1802
1919
  this.changeHandlers.add(handler);
1803
1920
  return () => {
@@ -1899,6 +2016,7 @@ class LiveKitConversation {
1899
2016
  this.awaitAvatarReturn();
1900
2017
  }
1901
2018
  });
2019
+ this.room.on(RoomEvent3.Reconnected, () => this.catchUp());
1902
2020
  this.room.on(RoomEvent3.ParticipantConnected, (participant) => {
1903
2021
  if (this.isAvatarParticipant(participant)) {
1904
2022
  const tracksChanged = this._avatar.updateParticipant(participant);
@@ -1908,6 +2026,7 @@ class LiveKitConversation {
1908
2026
  }
1909
2027
  if (this.away)
1910
2028
  this.avatarBack();
2029
+ this.catchUp();
1911
2030
  }
1912
2031
  });
1913
2032
  this.room.on(RoomEvent3.ParticipantDisconnected, (participant) => {
@@ -1929,6 +2048,7 @@ class LiveKitConversation {
1929
2048
  this.room.on(RoomEvent3.Disconnected, () => {
1930
2049
  this.stats?.abandon();
1931
2050
  this.stats = null;
2051
+ this.sequence.stop();
1932
2052
  this.setAvatarSpeaking(false);
1933
2053
  this.stopPresentation();
1934
2054
  this.stopReadyFallback();
@@ -2026,13 +2146,14 @@ class LiveKitConversation {
2026
2146
  this.answered = null;
2027
2147
  }
2028
2148
  handleSessionEvent(payload) {
2029
- const event = parseSessionEvent(payload);
2030
- if (event === null)
2149
+ const packet = parseSessionEvent(payload);
2150
+ if (packet === null)
2031
2151
  return;
2032
2152
  this.speaksProtocol = true;
2033
- if (event.type === "output-ready") {
2034
- this.markAvatarReady();
2035
- } else if (event.type === "command-result") {
2153
+ const { seq, event } = packet;
2154
+ if (this.sequence.repeats(seq))
2155
+ return;
2156
+ if (event.type === "command-result") {
2036
2157
  if (event.id === SESSION_END_ID)
2037
2158
  this.answered?.();
2038
2159
  else if (event.id !== null) {
@@ -2045,16 +2166,32 @@ class LiveKitConversation {
2045
2166
  this.commands.delete(id);
2046
2167
  settle({ status: "rejected", error: `unknown command ${type}` });
2047
2168
  }
2048
- } else if (event.type === "floor") {
2049
- this.setAvatarSpeaking(event.speaking);
2169
+ }
2170
+ this.sequence.accept(seq, event);
2171
+ }
2172
+ deliver(event) {
2173
+ if (event.type === "output-ready") {
2174
+ this.markAvatarReady();
2050
2175
  } else if (event.type === "tool-call") {
2051
2176
  this.emit({ type: "tool-call", call: event.call });
2052
2177
  } else if (event.type === "ended") {
2053
2178
  this.answered?.();
2054
2179
  if (!event.byClient)
2055
2180
  this.reportEnd(event.reason, event.message);
2181
+ } else if (event.type === "session-event") {
2182
+ if (event.event.type === "floor") {
2183
+ this.setAvatarSpeaking(event.event.state === "speaking");
2184
+ }
2185
+ this.emit({ type: "session-event", event: event.event });
2056
2186
  }
2057
2187
  }
2188
+ newSequence() {
2189
+ return new EventSequence((event) => this.deliver(event), (afterSeq) => this.command({ type: "session.replay", after_seq: afterSeq }));
2190
+ }
2191
+ catchUp() {
2192
+ if (this.speaksProtocol)
2193
+ this.sequence.catchUp();
2194
+ }
2058
2195
  handleRuntimeSignal(payload) {
2059
2196
  const signal = parseRuntimeSignal(payload);
2060
2197
  if (signal?.type === "session-ended") {
@@ -2291,6 +2428,7 @@ class NuanceConversation {
2291
2428
  ticker;
2292
2429
  sessionOverrides;
2293
2430
  personality;
2431
+ tools;
2294
2432
  variant;
2295
2433
  maxSessionSeconds;
2296
2434
  videoCapture;
@@ -2329,6 +2467,7 @@ class NuanceConversation {
2329
2467
  this.ticker = deps.ticker ?? pageTicker;
2330
2468
  this.sessionOverrides = options.sessionOverrides ?? {};
2331
2469
  this.personality = options.personality;
2470
+ this.tools = options.tools;
2332
2471
  this.variant = options.variant;
2333
2472
  this.maxSessionSeconds = options.maxSessionSeconds;
2334
2473
  this.videoCapture = options.videoCapture;
@@ -2365,6 +2504,8 @@ class NuanceConversation {
2365
2504
  request.timezone = timezone;
2366
2505
  if (this.personality)
2367
2506
  request.personality = this.personality;
2507
+ if (this.tools)
2508
+ request.tools = this.tools;
2368
2509
  if (this.variant)
2369
2510
  request.variant = this.variant;
2370
2511
  if (this.maxSessionSeconds !== undefined) {
@@ -2497,10 +2638,13 @@ class NuanceConversation {
2497
2638
  }
2498
2639
  onChange(handler) {
2499
2640
  return this.onEvent((event) => {
2500
- if (event.type !== "data-event" && event.type !== "screen-share-changed" && event.type !== "tool-call")
2641
+ if (event.type !== "data-event" && event.type !== "screen-share-changed" && event.type !== "tool-call" && event.type !== "session-event")
2501
2642
  handler(event);
2502
2643
  });
2503
2644
  }
2645
+ on(type, handler) {
2646
+ return this.onEvent(sessionEventsOf(type, handler));
2647
+ }
2504
2648
  onEvent(handler) {
2505
2649
  this.changeHandlers.add(handler);
2506
2650
  return () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuancelabs/js-sdk",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "JavaScript SDK for Nuance Labs conversation video AI avatar",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {