@convai/web-sdk 1.8.0-beta.6 → 1.8.0-beta.8

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 (99) hide show
  1. package/README.md +303 -1
  2. package/dist/core/ConvaiClient.d.ts +59 -2
  3. package/dist/core/ConvaiClient.d.ts.map +1 -1
  4. package/dist/core/ConvaiClient.js +457 -6
  5. package/dist/core/ConvaiClient.js.map +1 -1
  6. package/dist/core/MessageHandler.d.ts +11 -0
  7. package/dist/core/MessageHandler.d.ts.map +1 -1
  8. package/dist/core/MessageHandler.js +325 -30
  9. package/dist/core/MessageHandler.js.map +1 -1
  10. package/dist/core/SSESession.js +2 -2
  11. package/dist/core/SSESession.js.map +1 -1
  12. package/dist/core/connectRequest.d.ts +38 -3
  13. package/dist/core/connectRequest.d.ts.map +1 -1
  14. package/dist/core/connectRequest.js +57 -11
  15. package/dist/core/connectRequest.js.map +1 -1
  16. package/dist/core/index.d.ts +5 -1
  17. package/dist/core/index.d.ts.map +1 -1
  18. package/dist/core/index.js +8 -1
  19. package/dist/core/index.js.map +1 -1
  20. package/dist/core/logicalTurn.d.ts +4 -1
  21. package/dist/core/logicalTurn.d.ts.map +1 -1
  22. package/dist/core/logicalTurn.js +35 -4
  23. package/dist/core/logicalTurn.js.map +1 -1
  24. package/dist/core/modelOutput.d.ts +40 -0
  25. package/dist/core/modelOutput.d.ts.map +1 -0
  26. package/dist/core/modelOutput.js +187 -0
  27. package/dist/core/modelOutput.js.map +1 -0
  28. package/dist/core/publishedChat.d.ts +9 -0
  29. package/dist/core/publishedChat.d.ts.map +1 -0
  30. package/dist/core/publishedChat.js +13 -0
  31. package/dist/core/publishedChat.js.map +1 -0
  32. package/dist/core/types.d.ts +225 -25
  33. package/dist/core/types.d.ts.map +1 -1
  34. package/dist/core/types.js.map +1 -1
  35. package/dist/embed/ConvaiChatElement.d.ts +103 -0
  36. package/dist/embed/ConvaiChatElement.d.ts.map +1 -0
  37. package/dist/embed/ConvaiChatElement.js +679 -0
  38. package/dist/embed/ConvaiChatElement.js.map +1 -0
  39. package/dist/embed/browser.d.ts +3 -0
  40. package/dist/embed/browser.d.ts.map +1 -0
  41. package/dist/embed/browser.js +5 -0
  42. package/dist/embed/browser.js.map +1 -0
  43. package/dist/embed/chat-embed-v1.js +200 -0
  44. package/dist/embed/chat-embed-v1.js.map +7 -0
  45. package/dist/embed/index.d.ts +9 -0
  46. package/dist/embed/index.d.ts.map +1 -0
  47. package/dist/embed/index.js +9 -0
  48. package/dist/embed/index.js.map +1 -0
  49. package/dist/embed/publicationApi.d.ts +23 -0
  50. package/dist/embed/publicationApi.d.ts.map +1 -0
  51. package/dist/embed/publicationApi.js +141 -0
  52. package/dist/embed/publicationApi.js.map +1 -0
  53. package/dist/react/components/ConvaiWidget.d.ts +11 -1
  54. package/dist/react/components/ConvaiWidget.d.ts.map +1 -1
  55. package/dist/react/components/ConvaiWidget.js +67 -11
  56. package/dist/react/components/ConvaiWidget.js.map +1 -1
  57. package/dist/react/components/rtc-widget/components/MarkdownRenderer.d.ts +7 -0
  58. package/dist/react/components/rtc-widget/components/MarkdownRenderer.d.ts.map +1 -1
  59. package/dist/react/components/rtc-widget/components/MarkdownRenderer.js +111 -17
  60. package/dist/react/components/rtc-widget/components/MarkdownRenderer.js.map +1 -1
  61. package/dist/react/components/rtc-widget/components/UserMessage.js +1 -1
  62. package/dist/react/components/rtc-widget/components/UserMessage.js.map +1 -1
  63. package/dist/react/components/rtc-widget/components/conviComponents/SettingsTray.d.ts +1 -1
  64. package/dist/react/components/rtc-widget/components/conviComponents/SettingsTray.d.ts.map +1 -1
  65. package/dist/react/components/rtc-widget/components/conviComponents/SettingsTray.js +2 -2
  66. package/dist/react/components/rtc-widget/components/conviComponents/SettingsTray.js.map +1 -1
  67. package/dist/react/hooks/useConvaiClient.d.ts +2 -0
  68. package/dist/react/hooks/useConvaiClient.d.ts.map +1 -1
  69. package/dist/react/hooks/useConvaiClient.js +8 -0
  70. package/dist/react/hooks/useConvaiClient.js.map +1 -1
  71. package/dist/react/index.d.ts +4 -1
  72. package/dist/react/index.d.ts.map +1 -1
  73. package/dist/react/index.js +8 -1
  74. package/dist/react/index.js.map +1 -1
  75. package/dist/utils/inlineMarkdown.d.ts +22 -0
  76. package/dist/utils/inlineMarkdown.d.ts.map +1 -1
  77. package/dist/utils/inlineMarkdown.js +84 -7
  78. package/dist/utils/inlineMarkdown.js.map +1 -1
  79. package/dist/vanilla/ConvaiWidget.d.ts.map +1 -1
  80. package/dist/vanilla/ConvaiWidget.js +290 -43
  81. package/dist/vanilla/ConvaiWidget.js.map +1 -1
  82. package/dist/vanilla/WidgetEventSubscriptions.d.ts +15 -0
  83. package/dist/vanilla/WidgetEventSubscriptions.d.ts.map +1 -0
  84. package/dist/vanilla/WidgetEventSubscriptions.js +37 -0
  85. package/dist/vanilla/WidgetEventSubscriptions.js.map +1 -0
  86. package/dist/vanilla/index.d.ts +3 -1
  87. package/dist/vanilla/index.d.ts.map +1 -1
  88. package/dist/vanilla/index.js +3 -1
  89. package/dist/vanilla/index.js.map +1 -1
  90. package/dist/vanilla/styles.d.ts.map +1 -1
  91. package/dist/vanilla/styles.js +12 -0
  92. package/dist/vanilla/styles.js.map +1 -1
  93. package/dist/vanilla/types.d.ts +8 -1
  94. package/dist/vanilla/types.d.ts.map +1 -1
  95. package/dist/vanilla/types.js.map +1 -1
  96. package/dist/version.d.ts +1 -1
  97. package/dist/version.js +1 -1
  98. package/dist/version.js.map +1 -1
  99. package/package.json +48 -13
@@ -14,10 +14,11 @@ import { MemoryManager } from "./MemoryManager.js";
14
14
  import { CharacterVersionManager } from "./CharacterVersionManager.js";
15
15
  import { resolveCharacterReference, } from "./characterReference.js";
16
16
  import { ConnectionStateHandler } from "./ConnectionStateHandler.js";
17
- import { buildActionConnectConfig, buildBlendshapeConnectConfig, buildEmotionConnectConfig, serializeActionProtocolCapabilities, } from "./connectRequest.js";
17
+ import { buildActionConnectConfig, buildBlendshapeConnectConfig, buildEmotionConnectConfig, serializeProtocolCapabilities, validateActionProtocolSelection, validateModelOutputVersionSelection, } from "./connectRequest.js";
18
18
  import { shouldPreemptForContextUpdateResponse, shouldPreemptForExplicitRespondMode, } from "./contextUpdateRequest.js";
19
19
  import { resolveVisionInputConfig, serializeVisionInputConfig, } from "./visionRequest.js";
20
20
  import { SSESession } from "./SSESession.js";
21
+ import { isJsonValue } from "./modelOutput.js";
21
22
  import { isValidLogicalTurnId } from "./logicalTurn.js";
22
23
  // Module-level registry — populated by importing @convai/web-sdk/vanilla/websocket
23
24
  let _wsFactory = null;
@@ -27,6 +28,33 @@ function normalizeStateOfMind(value) {
27
28
  const normalized = value.trim().toLowerCase();
28
29
  return normalized === "neutral" || normalized === "" ? null : normalized;
29
30
  }
31
+ /**
32
+ * Rejection from {@link ConvaiClient.sendActionResultAndWait} after local
33
+ * validation succeeds. Inspect `reason` rather than parsing the message.
34
+ */
35
+ export class ActionResultAckError extends Error {
36
+ constructor(message, options) {
37
+ super(message);
38
+ this.name =
39
+ options.reason === "aborted"
40
+ ? "AbortError"
41
+ : options.reason === "timeout"
42
+ ? "TimeoutError"
43
+ : "ActionResultAckError";
44
+ this.reason = options.reason;
45
+ this.toolCallId = options.toolCallId;
46
+ this.errorCode = options.errorCode;
47
+ this.response = options.response;
48
+ this.cause = options.cause;
49
+ }
50
+ }
51
+ /**
52
+ * Marks a publication exchange failure that is safe to retry with the same
53
+ * logical connect attempt. The launch grant itself is single-use, so callers
54
+ * must never mint or submit a second grant for this recovery path.
55
+ */
56
+ class RetryablePublishedChatExchangeError extends Error {
57
+ }
30
58
  /**
31
59
  * Custom reconnect policy that disables automatic reconnection
32
60
  */
@@ -96,6 +124,7 @@ export class ConvaiClient extends EventEmitter {
96
124
  /** Roster commands awaiting their ack, keyed by command id. */
97
125
  this._pendingRosterCommands = new Map();
98
126
  this._rosterCommandSeq = 0;
127
+ this._requestTraceId = null;
99
128
  this._isBotReady = false;
100
129
  this._participantSid = "";
101
130
  this._storedConfig = null;
@@ -111,10 +140,12 @@ export class ConvaiClient extends EventEmitter {
111
140
  this._sseSession = null;
112
141
  this._logRtviMessages = true;
113
142
  this._isDisconnecting = false;
143
+ this._isPublishedChatSession = false;
114
144
  // RTVI client-ready handshake (race-proof bot-ready delivery)
115
145
  this._clientReadyRetryTimer = null;
116
146
  this._clientReadyTimeoutTimer = null;
117
147
  this._clientReadyMsgId = null;
148
+ this._pendingActionResultAcks = new Map();
118
149
  // Conversation session tracking
119
150
  this._conversationSessionId = 0;
120
151
  this._conversationStartTime = 0;
@@ -193,6 +224,9 @@ export class ConvaiClient extends EventEmitter {
193
224
  get connectionType() {
194
225
  return this._connectionType;
195
226
  }
227
+ get isPublishedChatSession() {
228
+ return this._isPublishedChatSession;
229
+ }
196
230
  get apiKey() {
197
231
  return this._apiKey;
198
232
  }
@@ -250,6 +284,9 @@ export class ConvaiClient extends EventEmitter {
250
284
  get characterSessionId() {
251
285
  return this._characterSessionId;
252
286
  }
287
+ on(event, callback) {
288
+ return super.on(event, callback);
289
+ }
253
290
  /**
254
291
  * Server-owned snapshot of a multi-character room, or null for a
255
292
  * single-character session. Carries the room id, epochs and roster.
@@ -285,6 +322,10 @@ export class ConvaiClient extends EventEmitter {
285
322
  }
286
323
  return this._messageHandler.appendLocalMessage(message);
287
324
  }
325
+ /** Trace id of the /connect that produced this session; quote it to backend support. */
326
+ get requestTraceId() {
327
+ return this._requestTraceId;
328
+ }
288
329
  get isBotReady() {
289
330
  return this._isBotReady;
290
331
  }
@@ -375,8 +416,17 @@ export class ConvaiClient extends EventEmitter {
375
416
  this.updateState({ isConnected: true });
376
417
  this.emit("botReady");
377
418
  });
378
- this._messageHandler.on("messagesChange", (messages) => {
379
- this.emit("message", messages[messages.length - 1]);
419
+ this._messageHandler.on("messagesChange", (messages, updatedMessage, updatedMessages) => {
420
+ // Most updates target the tail. Correlated v2 chunks can update an
421
+ // earlier streaming row. A v2 stop can finalize several rows at once.
422
+ if (updatedMessages?.length) {
423
+ for (const message of updatedMessages) {
424
+ this.emit("message", message);
425
+ }
426
+ }
427
+ else {
428
+ this.emit("message", updatedMessage ?? messages[messages.length - 1]);
429
+ }
380
430
  this.emit("messagesChange", messages);
381
431
  });
382
432
  this._messageHandler.on("userTranscriptionChange", (transcription) => {
@@ -498,6 +548,15 @@ export class ConvaiClient extends EventEmitter {
498
548
  }
499
549
  this.emit("characterStatus", status);
500
550
  });
551
+ this._messageHandler.on("modelOutput", (data) => {
552
+ this.emit("modelOutput", data);
553
+ });
554
+ this._messageHandler.on("botLlmTextRaw", (data) => {
555
+ this.emit("botLlmTextRaw", data);
556
+ });
557
+ this._messageHandler.on("modelOutputProtocolError", (data) => {
558
+ this.emit("modelOutputProtocolError", data);
559
+ });
501
560
  this._messageHandler.on("serverResponse", (response) => {
502
561
  this._handleRosterServerResponse(response);
503
562
  this.emit("serverResponse", response);
@@ -563,6 +622,30 @@ export class ConvaiClient extends EventEmitter {
563
622
  this._room.localParticipant.publishData(encodedData, { reliable: true });
564
623
  }
565
624
  }
625
+ /**
626
+ * Publish exactly one message while retaining the transport's local completion
627
+ * signal. Server acceptance is still proved separately by `serverResponse`.
628
+ */
629
+ async publishMessageAndWaitForLocalPublication(type, data, id) {
630
+ const message = id === undefined ? { type, data } : { type, id, data };
631
+ if (this._activeTransport === "websocket") {
632
+ if (!this._wsSession?.isConnected) {
633
+ throw new Error("The action-result WebSocket transport is no longer available");
634
+ }
635
+ this._wsSession.sendMessage(type, data);
636
+ return;
637
+ }
638
+ if (this._activeTransport === "livekit" &&
639
+ this._room?.localParticipant &&
640
+ this._room.state !== "disconnected") {
641
+ const encodedData = new TextEncoder().encode(JSON.stringify(message));
642
+ await this._room.localParticipant.publishData(encodedData, {
643
+ reliable: true,
644
+ });
645
+ return;
646
+ }
647
+ throw new Error("The action-result transport is no longer available");
648
+ }
566
649
  /**
567
650
  * Returns true when a message can be sent on the active transport.
568
651
  */
@@ -586,13 +669,18 @@ export class ConvaiClient extends EventEmitter {
586
669
  }
587
670
  // Store config for reconnection
588
671
  this._storedConfig = { ...finalConfig };
672
+ this._isPublishedChatSession = false;
589
673
  // Use characterSessionId from config when reconnecting (so we resume the same session)
590
674
  if (finalConfig.characterSessionId &&
591
675
  finalConfig.characterSessionId !== "-1") {
592
676
  this._characterSessionId = finalConfig.characterSessionId;
593
677
  }
594
678
  const configWithDefaults = this.applyConfigDefaults(finalConfig);
679
+ // A fresh connect starts on the v1 model-output contract; the /connect
680
+ // response upgrades it once the server confirms the negotiated version.
681
+ this._messageHandler.setModelOutputVersion(1);
595
682
  this.updateState({ isConnecting: true });
683
+ this._requestTraceId = null;
596
684
  try {
597
685
  // Store connection config
598
686
  this._apiKey = configWithDefaults.apiKey ?? null;
@@ -674,6 +762,7 @@ export class ConvaiClient extends EventEmitter {
674
762
  // Prepare request body with required parameters
675
763
  const characterSessionIdToSend = configWithDefaults.characterSessionId ?? this._characterSessionId;
676
764
  const isRoster = isRosterConfig(configWithDefaults);
765
+ const serializedCapabilities = serializeProtocolCapabilities(configWithDefaults.capabilities);
677
766
  const requestBody = {
678
767
  ...(isRoster
679
768
  ? buildRosterConnectFields(configWithDefaults)
@@ -686,6 +775,9 @@ export class ConvaiClient extends EventEmitter {
686
775
  end_user_metadata: configWithDefaults.endUserMetadata,
687
776
  }),
688
777
  transport: transportType,
778
+ ...(serializedCapabilities && {
779
+ capabilities: serializedCapabilities,
780
+ }),
689
781
  connection_type: connType,
690
782
  ...buildBlendshapeConnectConfig(configWithDefaults),
691
783
  ...(buildEmotionConnectConfig(configWithDefaults) && {
@@ -698,12 +790,13 @@ export class ConvaiClient extends EventEmitter {
698
790
  ...(effectiveVisionInputConfig && {
699
791
  vision_input_config: serializeVisionInputConfig(effectiveVisionInputConfig),
700
792
  }),
793
+ ...(configWithDefaults.visionPromptMode !== undefined && {
794
+ vision_prompt_mode: configWithDefaults.visionPromptMode,
795
+ }),
701
796
  ...(configWithDefaults.respondModes && {
702
797
  respond_modes: this.serializeRespondModes(configWithDefaults.respondModes),
703
798
  }),
704
- ...(serializeActionProtocolCapabilities(configWithDefaults.capabilities) && {
705
- capabilities: serializeActionProtocolCapabilities(configWithDefaults.capabilities),
706
- }),
799
+ ...(serializedCapabilities && { capabilities: serializedCapabilities }),
707
800
  ...(configWithDefaults.sceneDescription && {
708
801
  scene_description: configWithDefaults.sceneDescription,
709
802
  }),
@@ -844,6 +937,10 @@ export class ConvaiClient extends EventEmitter {
844
937
  * await client.connectWithConnectionData(data)
845
938
  */
846
939
  async connectWithConnectionData(data, config) {
940
+ // A client can be reused after a published session. This is a normal
941
+ // credentialed/proxy connection path, so never leave published-only UI
942
+ // semantics (fresh grant requirement, text-only controls) enabled.
943
+ this._isPublishedChatSession = false;
847
944
  const finalConfig = config || this._storedConfig;
848
945
  if (!finalConfig) {
849
946
  throw new Error("No configuration provided. Pass config to connectWithConnectionData() or store it in the client");
@@ -1080,6 +1177,10 @@ export class ConvaiClient extends EventEmitter {
1080
1177
  */
1081
1178
  async joinRoom(options) {
1082
1179
  validateJoinOptions(options);
1180
+ // joinRoom is a credentialed Workspace connection, not a publication.
1181
+ // Reset this before further validation so a rejected normal attempt also
1182
+ // cannot leave a reused client in published-session mode.
1183
+ this._isPublishedChatSession = false;
1083
1184
  const stored = this._storedConfig ?? {};
1084
1185
  const url = stored.url ?? "https://realtime-api.convai.com";
1085
1186
  if (!stored.apiKey && !stored.authToken) {
@@ -1244,6 +1345,16 @@ export class ConvaiClient extends EventEmitter {
1244
1345
  characterSessionId: connectionData.character_session_id,
1245
1346
  };
1246
1347
  }
1348
+ // The server answers a capability request with what it actually selected.
1349
+ // A silent downgrade would leave the client parsing v2 frames as v1 (or the
1350
+ // reverse), so each selection is validated and the model-output version the
1351
+ // message handler decodes is set from the server's answer, not the request.
1352
+ const selectedCapabilities = connectionData.capabilities ?? {};
1353
+ const requestedCapabilities = configWithDefaults.capabilities;
1354
+ validateActionProtocolSelection(requestedCapabilities?.actionProtocolVersion, selectedCapabilities.action_protocol_version);
1355
+ this._messageHandler.setModelOutputVersion(validateModelOutputVersionSelection(requestedCapabilities?.modelOutputVersion, selectedCapabilities.model_output_version));
1356
+ this._requestTraceId =
1357
+ connectionData.request_trace_id ?? null;
1247
1358
  // Seed the roster before the transport comes up, so the first bot-ready and
1248
1359
  // any early roster change already have instances to match against.
1249
1360
  if (this._roster.applyConnectResponse(connectionData)) {
@@ -1425,6 +1536,189 @@ export class ConvaiClient extends EventEmitter {
1425
1536
  }
1426
1537
  this.clearAllManagers();
1427
1538
  }
1539
+ /**
1540
+ * Connect to a published text-chat revision without publisher credentials.
1541
+ *
1542
+ * The launch grant is single-use and is never retained for reconnect. This
1543
+ * path is LiveKit data-only: it does not enable microphone, camera, TTS,
1544
+ * video, or lipsync.
1545
+ */
1546
+ async connectWithPublicationGrant(launchToken, options = {}) {
1547
+ if (typeof launchToken !== "string" ||
1548
+ launchToken.length < 32 ||
1549
+ launchToken.length > 512) {
1550
+ throw new Error("A valid publication launch token is required");
1551
+ }
1552
+ if (this._state.isConnected || this._state.isConnecting) {
1553
+ throw new Error("Disconnect the current session before connecting again");
1554
+ }
1555
+ const throwIfAborted = () => {
1556
+ if (options.signal?.aborted) {
1557
+ throw new DOMException("The operation was aborted", "AbortError");
1558
+ }
1559
+ };
1560
+ throwIfAborted();
1561
+ let abortDisconnect = null;
1562
+ const disconnectOnAbort = () => {
1563
+ abortDisconnect ?? (abortDisconnect = this.disconnect().catch(() => undefined));
1564
+ };
1565
+ const apiUrl = (options.url ?? "https://realtime-api.convai.com").replace(/\/+$/, "");
1566
+ const connectAttemptId = options.connectAttemptId ??
1567
+ (typeof crypto !== "undefined" && "randomUUID" in crypto
1568
+ ? `published_${crypto.randomUUID().replace(/-/g, "")}`
1569
+ : `published_${Date.now()}_${Math.random().toString(36).slice(2)}`);
1570
+ if (!/^[a-zA-Z0-9_-]{1,128}$/.test(connectAttemptId)) {
1571
+ throw new Error("A valid published chat connect attempt id is required");
1572
+ }
1573
+ this._storedConfig = null;
1574
+ this._isPublishedChatSession = true;
1575
+ this._requestTraceId = null;
1576
+ this._apiKey = null;
1577
+ this._authToken = null;
1578
+ this._characterId = null;
1579
+ this._characterSessionId = "-1";
1580
+ this._endUserId = null;
1581
+ this._endUserMetadata = null;
1582
+ this._requestTraceId = null;
1583
+ this._memoryManager = null;
1584
+ this._connectionType = "text";
1585
+ this._activeTransport = "livekit";
1586
+ // Published chat does not negotiate protocol capabilities. A client may
1587
+ // have previously connected to a v2 character, so restore the default v1
1588
+ // handling before accepting this text-only session's legacy envelopes.
1589
+ this._messageHandler.setModelOutputVersion(1);
1590
+ this.updateState({ isConnecting: true });
1591
+ try {
1592
+ options.signal?.addEventListener("abort", disconnectOnAbort, {
1593
+ once: true,
1594
+ });
1595
+ const exchangeGrant = () => fetch(`${apiUrl}/published-chat/connect`, {
1596
+ method: "POST",
1597
+ // A published launch token is bearer-like. Never include any ambient
1598
+ // cookies when the host happens to be same-origin with Core.
1599
+ credentials: "omit",
1600
+ headers: { "Content-Type": "application/json" },
1601
+ body: JSON.stringify({
1602
+ launch_token: launchToken,
1603
+ connect_attempt_id: connectAttemptId,
1604
+ }),
1605
+ signal: options.signal,
1606
+ });
1607
+ const readConnectionResponse = async (response, retryTransientFailure) => {
1608
+ if (!response.ok) {
1609
+ if (retryTransientFailure && response.status >= 500) {
1610
+ throw new RetryablePublishedChatExchangeError();
1611
+ }
1612
+ const errorText = await response.text();
1613
+ let errorMessage = `HTTP ${response.status}`;
1614
+ try {
1615
+ const errorData = JSON.parse(errorText);
1616
+ this._requestTraceId =
1617
+ errorData.request_trace_id ?? errorData.requestTraceId ?? null;
1618
+ const raw = errorData.message ?? errorData.error ?? errorData.detail;
1619
+ errorMessage =
1620
+ typeof raw === "string"
1621
+ ? raw
1622
+ : raw == null
1623
+ ? errorMessage
1624
+ : JSON.stringify(raw);
1625
+ }
1626
+ catch {
1627
+ if (errorText)
1628
+ errorMessage = `${errorMessage}: ${errorText}`;
1629
+ }
1630
+ throw new Error(errorMessage);
1631
+ }
1632
+ let connectionData;
1633
+ try {
1634
+ connectionData = await response.json();
1635
+ }
1636
+ catch {
1637
+ if (retryTransientFailure) {
1638
+ throw new RetryablePublishedChatExchangeError();
1639
+ }
1640
+ throw new Error("Published chat returned an invalid connection response");
1641
+ }
1642
+ throwIfAborted();
1643
+ if (typeof connectionData !== "object" ||
1644
+ connectionData === null ||
1645
+ typeof connectionData.room_url !== "string" ||
1646
+ typeof connectionData.token !== "string" ||
1647
+ typeof connectionData.character_session_id !==
1648
+ "string") {
1649
+ if (retryTransientFailure) {
1650
+ throw new RetryablePublishedChatExchangeError();
1651
+ }
1652
+ throw new Error("Published chat returned an invalid connection response");
1653
+ }
1654
+ return connectionData;
1655
+ };
1656
+ const requestConnection = async (retryTransientFailure) => {
1657
+ let response;
1658
+ try {
1659
+ response = await exchangeGrant();
1660
+ }
1661
+ catch (error) {
1662
+ throwIfAborted();
1663
+ if (retryTransientFailure) {
1664
+ throw new RetryablePublishedChatExchangeError();
1665
+ }
1666
+ throw error;
1667
+ }
1668
+ return readConnectionResponse(response, retryTransientFailure);
1669
+ };
1670
+ let connectionData;
1671
+ try {
1672
+ connectionData = await requestConnection(true);
1673
+ }
1674
+ catch (error) {
1675
+ throwIfAborted();
1676
+ if (!(error instanceof RetryablePublishedChatExchangeError)) {
1677
+ throw error;
1678
+ }
1679
+ // A response can be lost or corrupted after Core consumed the
1680
+ // single-use grant. Retry exactly once with the same attempt key;
1681
+ // Core/Character API replay that logical attempt instead of creating
1682
+ // a second session.
1683
+ connectionData = await requestConnection(false);
1684
+ }
1685
+ throwIfAborted();
1686
+ this._requestTraceId = connectionData.request_trace_id ?? null;
1687
+ this._characterSessionId = connectionData.character_session_id;
1688
+ this._endUserId = connectionData.end_user_id ?? null;
1689
+ this._endUserMetadata = connectionData.end_user_metadata ?? null;
1690
+ await this._room.connect(connectionData.room_url, connectionData.token, {
1691
+ rtcConfig: {
1692
+ // Hosted Core has TURN, so relay is the safe default. On-prem
1693
+ // deployments without TURN may explicitly allow host/srflx paths.
1694
+ iceTransportPolicy: options.iceTransportPolicy ?? "relay",
1695
+ },
1696
+ });
1697
+ throwIfAborted();
1698
+ this._audioManager.syncStateFromRoom({ emit: true });
1699
+ this._participantSid = this._room.localParticipant.sid;
1700
+ this.updateState({
1701
+ isConnected: true,
1702
+ isConnecting: false,
1703
+ endUserId: this._endUserId,
1704
+ endUserMetadata: this._endUserMetadata,
1705
+ });
1706
+ this.emit("connect");
1707
+ this._startClientReadyHandshake();
1708
+ }
1709
+ catch (error) {
1710
+ if (options.signal?.aborted) {
1711
+ abortDisconnect ?? (abortDisconnect = this.disconnect().catch(() => undefined));
1712
+ await abortDisconnect;
1713
+ }
1714
+ this.updateState({ isConnected: false, isConnecting: false });
1715
+ this.emit("error", error);
1716
+ throw error;
1717
+ }
1718
+ finally {
1719
+ options.signal?.removeEventListener("abort", disconnectOnAbort);
1720
+ }
1721
+ }
1428
1722
  /**
1429
1723
  * RTVI client-ready handshake. Server-side gates bot-ready emission on
1430
1724
  * receipt of client-ready (and re-emits bot-ready on every client-ready),
@@ -1652,11 +1946,167 @@ export class ConvaiClient extends EventEmitter {
1652
1946
  }
1653
1947
  this.publishMessage("user_text_message", userTextMessage, logicalTurnId);
1654
1948
  }
1949
+ // The submitted line is appended locally on every transport, so it shows
1950
+ // without waiting for a server echo. Sessions that do echo it back as a
1951
+ // final user-transcription are deduplicated in the message handler.
1952
+ this._messageHandler.addUserTextMessage(text);
1655
1953
  }
1656
1954
  catch (error) {
1657
1955
  throw error;
1658
1956
  }
1659
1957
  }
1958
+ /**
1959
+ * Return a correlated terminal result for a client-executed tool call.
1960
+ */
1961
+ sendActionResult(result) {
1962
+ const payload = this.buildActionResultPayload(result);
1963
+ this.publishMessage("action-result", payload);
1964
+ }
1965
+ /**
1966
+ * Publish one correlated terminal result and wait for Core's exact
1967
+ * `server-response` acknowledgement. This method performs one publication;
1968
+ * callers own any idempotent retry policy.
1969
+ */
1970
+ async sendActionResultAndWait(result, options = {}) {
1971
+ const timeoutMs = options.timeoutMs ?? ConvaiClient.ACTION_RESULT_ACK_TIMEOUT_MS;
1972
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
1973
+ throw new Error("Action result acknowledgement timeout must be greater than zero");
1974
+ }
1975
+ if (options.signal?.aborted) {
1976
+ throw new ActionResultAckError("Action result acknowledgement was aborted", {
1977
+ reason: "aborted",
1978
+ toolCallId: result.id,
1979
+ });
1980
+ }
1981
+ const payload = this.buildActionResultPayload(result);
1982
+ const toolCallId = payload.id;
1983
+ if (this._pendingActionResultAcks.has(toolCallId)) {
1984
+ throw new ActionResultAckError(`An action result acknowledgement is already pending for ${toolCallId}`, {
1985
+ reason: "duplicate_in_flight",
1986
+ toolCallId,
1987
+ });
1988
+ }
1989
+ const reservation = Symbol(toolCallId);
1990
+ this._pendingActionResultAcks.set(toolCallId, reservation);
1991
+ return new Promise((resolve, reject) => {
1992
+ let settled = false;
1993
+ let timeout = null;
1994
+ let unsubscribe = null;
1995
+ const cleanup = () => {
1996
+ if (timeout !== null) {
1997
+ clearTimeout(timeout);
1998
+ timeout = null;
1999
+ }
2000
+ if (unsubscribe) {
2001
+ unsubscribe();
2002
+ unsubscribe = null;
2003
+ }
2004
+ options.signal?.removeEventListener("abort", handleAbort);
2005
+ if (this._pendingActionResultAcks.get(toolCallId) === reservation) {
2006
+ this._pendingActionResultAcks.delete(toolCallId);
2007
+ }
2008
+ };
2009
+ const resolveOnce = (ack) => {
2010
+ if (settled)
2011
+ return;
2012
+ settled = true;
2013
+ cleanup();
2014
+ resolve(ack);
2015
+ };
2016
+ const rejectOnce = (error) => {
2017
+ if (settled)
2018
+ return;
2019
+ settled = true;
2020
+ cleanup();
2021
+ reject(error);
2022
+ };
2023
+ const handleAbort = () => {
2024
+ rejectOnce(new ActionResultAckError("Action result acknowledgement was aborted", {
2025
+ reason: "aborted",
2026
+ toolCallId,
2027
+ }));
2028
+ };
2029
+ const handleServerResponse = (response) => {
2030
+ if (!response || typeof response !== "object")
2031
+ return;
2032
+ const candidate = response;
2033
+ if (candidate.event_type !== "action-result")
2034
+ return;
2035
+ const extras = candidate.extras;
2036
+ if (!extras || extras.tool_call_id !== toolCallId)
2037
+ return;
2038
+ const message = typeof candidate.message === "string" ? candidate.message : null;
2039
+ if (candidate.status === "success") {
2040
+ resolveOnce({
2041
+ toolCallId,
2042
+ status: "success",
2043
+ message,
2044
+ idempotent: extras.idempotent === true,
2045
+ });
2046
+ return;
2047
+ }
2048
+ if (candidate.status !== "error")
2049
+ return;
2050
+ const errorCode = typeof extras.error_code === "string" ? extras.error_code : undefined;
2051
+ rejectOnce(new ActionResultAckError(message ?? errorCode ?? "Core rejected the action result", {
2052
+ reason: "server_error",
2053
+ toolCallId,
2054
+ ...(errorCode ? { errorCode } : {}),
2055
+ response: candidate,
2056
+ }));
2057
+ };
2058
+ // Register every terminal observer before publication so a synchronous
2059
+ // WebSocket acknowledgement cannot outrun its waiter.
2060
+ unsubscribe = this.on("serverResponse", handleServerResponse);
2061
+ options.signal?.addEventListener("abort", handleAbort, { once: true });
2062
+ timeout = setTimeout(() => {
2063
+ rejectOnce(new ActionResultAckError(`Timed out waiting for action result acknowledgement for ${toolCallId}`, {
2064
+ reason: "timeout",
2065
+ toolCallId,
2066
+ }));
2067
+ }, timeoutMs);
2068
+ void this.publishMessageAndWaitForLocalPublication("action-result", payload).catch((cause) => {
2069
+ const detail = cause instanceof Error ? `: ${cause.message}` : "";
2070
+ rejectOnce(new ActionResultAckError(`Failed to publish action result ${toolCallId}${detail}`, {
2071
+ reason: "publication_error",
2072
+ toolCallId,
2073
+ cause,
2074
+ }));
2075
+ });
2076
+ });
2077
+ }
2078
+ buildActionResultPayload(result) {
2079
+ if (!this.isTransportReady()) {
2080
+ throw new Error("Cannot send an action result before the session is ready");
2081
+ }
2082
+ if (!result.id.trim())
2083
+ throw new Error("Action result id is required");
2084
+ if (result.status !== "completed" &&
2085
+ result.status !== "error" &&
2086
+ result.status !== "cancelled") {
2087
+ throw new Error("Unsupported action result status");
2088
+ }
2089
+ if (result.status === "completed" && result.error !== undefined) {
2090
+ throw new Error("Completed action results cannot include error");
2091
+ }
2092
+ if (result.output !== undefined && !isJsonValue(result.output)) {
2093
+ throw new Error("Action result output must contain only finite JSON values");
2094
+ }
2095
+ if (result.error !== undefined && !isJsonValue(result.error)) {
2096
+ throw new Error("Action result error must contain only finite JSON values");
2097
+ }
2098
+ const characterSessionId = result.characterSessionId ?? this._characterSessionId;
2099
+ return {
2100
+ id: result.id,
2101
+ status: result.status,
2102
+ ...(result.output !== undefined && { output: result.output }),
2103
+ ...(result.error !== undefined && { error: result.error }),
2104
+ ...(characterSessionId &&
2105
+ characterSessionId !== "-1" && {
2106
+ character_session_id: characterSessionId,
2107
+ }),
2108
+ };
2109
+ }
1660
2110
  /**
1661
2111
  * Send a trigger message to invoke specific character actions
1662
2112
  */
@@ -1987,6 +2437,7 @@ export class ConvaiClient extends EventEmitter {
1987
2437
  }
1988
2438
  ConvaiClient.CLIENT_READY_RETRY_MS = 500;
1989
2439
  ConvaiClient.CLIENT_READY_TIMEOUT_MS = 45000;
2440
+ ConvaiClient.ACTION_RESULT_ACK_TIMEOUT_MS = 10000;
1990
2441
  /**
1991
2442
  * How long to wait for a roster command's ack.
1992
2443
  *