@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.
- package/README.md +303 -1
- package/dist/core/ConvaiClient.d.ts +59 -2
- package/dist/core/ConvaiClient.d.ts.map +1 -1
- package/dist/core/ConvaiClient.js +457 -6
- package/dist/core/ConvaiClient.js.map +1 -1
- package/dist/core/MessageHandler.d.ts +11 -0
- package/dist/core/MessageHandler.d.ts.map +1 -1
- package/dist/core/MessageHandler.js +325 -30
- package/dist/core/MessageHandler.js.map +1 -1
- package/dist/core/SSESession.js +2 -2
- package/dist/core/SSESession.js.map +1 -1
- package/dist/core/connectRequest.d.ts +38 -3
- package/dist/core/connectRequest.d.ts.map +1 -1
- package/dist/core/connectRequest.js +57 -11
- package/dist/core/connectRequest.js.map +1 -1
- package/dist/core/index.d.ts +5 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +8 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/logicalTurn.d.ts +4 -1
- package/dist/core/logicalTurn.d.ts.map +1 -1
- package/dist/core/logicalTurn.js +35 -4
- package/dist/core/logicalTurn.js.map +1 -1
- package/dist/core/modelOutput.d.ts +40 -0
- package/dist/core/modelOutput.d.ts.map +1 -0
- package/dist/core/modelOutput.js +187 -0
- package/dist/core/modelOutput.js.map +1 -0
- package/dist/core/publishedChat.d.ts +9 -0
- package/dist/core/publishedChat.d.ts.map +1 -0
- package/dist/core/publishedChat.js +13 -0
- package/dist/core/publishedChat.js.map +1 -0
- package/dist/core/types.d.ts +225 -25
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/embed/ConvaiChatElement.d.ts +103 -0
- package/dist/embed/ConvaiChatElement.d.ts.map +1 -0
- package/dist/embed/ConvaiChatElement.js +679 -0
- package/dist/embed/ConvaiChatElement.js.map +1 -0
- package/dist/embed/browser.d.ts +3 -0
- package/dist/embed/browser.d.ts.map +1 -0
- package/dist/embed/browser.js +5 -0
- package/dist/embed/browser.js.map +1 -0
- package/dist/embed/chat-embed-v1.js +200 -0
- package/dist/embed/chat-embed-v1.js.map +7 -0
- package/dist/embed/index.d.ts +9 -0
- package/dist/embed/index.d.ts.map +1 -0
- package/dist/embed/index.js +9 -0
- package/dist/embed/index.js.map +1 -0
- package/dist/embed/publicationApi.d.ts +23 -0
- package/dist/embed/publicationApi.d.ts.map +1 -0
- package/dist/embed/publicationApi.js +141 -0
- package/dist/embed/publicationApi.js.map +1 -0
- package/dist/react/components/ConvaiWidget.d.ts +11 -1
- package/dist/react/components/ConvaiWidget.d.ts.map +1 -1
- package/dist/react/components/ConvaiWidget.js +67 -11
- package/dist/react/components/ConvaiWidget.js.map +1 -1
- package/dist/react/components/rtc-widget/components/MarkdownRenderer.d.ts +7 -0
- package/dist/react/components/rtc-widget/components/MarkdownRenderer.d.ts.map +1 -1
- package/dist/react/components/rtc-widget/components/MarkdownRenderer.js +111 -17
- package/dist/react/components/rtc-widget/components/MarkdownRenderer.js.map +1 -1
- package/dist/react/components/rtc-widget/components/UserMessage.js +1 -1
- package/dist/react/components/rtc-widget/components/UserMessage.js.map +1 -1
- package/dist/react/components/rtc-widget/components/conviComponents/SettingsTray.d.ts +1 -1
- package/dist/react/components/rtc-widget/components/conviComponents/SettingsTray.d.ts.map +1 -1
- package/dist/react/components/rtc-widget/components/conviComponents/SettingsTray.js +2 -2
- package/dist/react/components/rtc-widget/components/conviComponents/SettingsTray.js.map +1 -1
- package/dist/react/hooks/useConvaiClient.d.ts +2 -0
- package/dist/react/hooks/useConvaiClient.d.ts.map +1 -1
- package/dist/react/hooks/useConvaiClient.js +8 -0
- package/dist/react/hooks/useConvaiClient.js.map +1 -1
- package/dist/react/index.d.ts +4 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +8 -1
- package/dist/react/index.js.map +1 -1
- package/dist/utils/inlineMarkdown.d.ts +22 -0
- package/dist/utils/inlineMarkdown.d.ts.map +1 -1
- package/dist/utils/inlineMarkdown.js +84 -7
- package/dist/utils/inlineMarkdown.js.map +1 -1
- package/dist/vanilla/ConvaiWidget.d.ts.map +1 -1
- package/dist/vanilla/ConvaiWidget.js +290 -43
- package/dist/vanilla/ConvaiWidget.js.map +1 -1
- package/dist/vanilla/WidgetEventSubscriptions.d.ts +15 -0
- package/dist/vanilla/WidgetEventSubscriptions.d.ts.map +1 -0
- package/dist/vanilla/WidgetEventSubscriptions.js +37 -0
- package/dist/vanilla/WidgetEventSubscriptions.js.map +1 -0
- package/dist/vanilla/index.d.ts +3 -1
- package/dist/vanilla/index.d.ts.map +1 -1
- package/dist/vanilla/index.js +3 -1
- package/dist/vanilla/index.js.map +1 -1
- package/dist/vanilla/styles.d.ts.map +1 -1
- package/dist/vanilla/styles.js +12 -0
- package/dist/vanilla/styles.js.map +1 -1
- package/dist/vanilla/types.d.ts +8 -1
- package/dist/vanilla/types.d.ts.map +1 -1
- package/dist/vanilla/types.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- 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,
|
|
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
|
-
|
|
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
|
-
...(
|
|
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
|
*
|