@convai/web-sdk 1.8.0-beta.5 → 1.8.0-beta.6
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 +49 -13
- package/dist/core/CharacterRoster.d.ts +84 -0
- package/dist/core/CharacterRoster.d.ts.map +1 -0
- package/dist/core/CharacterRoster.js +348 -0
- package/dist/core/CharacterRoster.js.map +1 -0
- package/dist/core/ConvaiClient.d.ts +94 -1
- package/dist/core/ConvaiClient.d.ts.map +1 -1
- package/dist/core/ConvaiClient.js +583 -47
- package/dist/core/ConvaiClient.js.map +1 -1
- package/dist/core/ConvaiRoomError.d.ts +48 -0
- package/dist/core/ConvaiRoomError.d.ts.map +1 -0
- package/dist/core/ConvaiRoomError.js +111 -0
- package/dist/core/ConvaiRoomError.js.map +1 -0
- package/dist/core/MessageHandler.d.ts +14 -0
- package/dist/core/MessageHandler.d.ts.map +1 -1
- package/dist/core/MessageHandler.js +111 -5
- package/dist/core/MessageHandler.js.map +1 -1
- package/dist/core/SSESession.d.ts +15 -0
- package/dist/core/SSESession.d.ts.map +1 -1
- package/dist/core/SSESession.js +92 -3
- package/dist/core/SSESession.js.map +1 -1
- package/dist/core/connectRequest.d.ts +1 -0
- package/dist/core/connectRequest.d.ts.map +1 -1
- package/dist/core/connectRequest.js +11 -0
- package/dist/core/connectRequest.js.map +1 -1
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +3 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/rosterRequest.d.ts +57 -0
- package/dist/core/rosterRequest.d.ts.map +1 -0
- package/dist/core/rosterRequest.js +210 -0
- package/dist/core/rosterRequest.js.map +1 -0
- package/dist/core/types.d.ts +462 -5
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/react/components/rtc-widget/components/MarkdownRenderer.d.ts.map +1 -1
- package/dist/react/components/rtc-widget/components/MarkdownRenderer.js +39 -44
- package/dist/react/components/rtc-widget/components/MarkdownRenderer.js.map +1 -1
- 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/utils/inlineMarkdown.d.ts +38 -0
- package/dist/utils/inlineMarkdown.d.ts.map +1 -0
- package/dist/utils/inlineMarkdown.js +106 -0
- package/dist/utils/inlineMarkdown.js.map +1 -0
- package/dist/vanilla/AudioRenderer.d.ts.map +1 -1
- package/dist/vanilla/AudioRenderer.js +6 -0
- package/dist/vanilla/AudioRenderer.js.map +1 -1
- package/dist/vanilla/ConvaiWidget.d.ts.map +1 -1
- package/dist/vanilla/ConvaiWidget.js +71 -57
- package/dist/vanilla/ConvaiWidget.js.map +1 -1
- package/dist/vanilla/index.d.ts +1 -0
- package/dist/vanilla/index.d.ts.map +1 -1
- package/dist/vanilla/index.js +1 -0
- package/dist/vanilla/index.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 +1 -1
|
@@ -5,13 +5,16 @@ import { AudioManager } from "./AudioManager.js";
|
|
|
5
5
|
import { VideoManager } from "./VideoManager.js";
|
|
6
6
|
import { ScreenShareManager } from "./ScreenShareManager.js";
|
|
7
7
|
import { MessageHandler } from "./MessageHandler.js";
|
|
8
|
+
import { CharacterRoster } from "./CharacterRoster.js";
|
|
9
|
+
import { buildJoinRequestBody, buildRosterConnectFields, isRosterConfig, validateJoinOptions, validateProtocolTopology, validateRosterConfig, } from "./rosterRequest.js";
|
|
10
|
+
import { ConvaiRoomError, roomErrorFromResponse } from "./ConvaiRoomError.js";
|
|
8
11
|
import { SDK_VERSION } from "../version.js";
|
|
9
12
|
import { EventEmitter } from "./EventEmitter.js";
|
|
10
13
|
import { MemoryManager } from "./MemoryManager.js";
|
|
11
14
|
import { CharacterVersionManager } from "./CharacterVersionManager.js";
|
|
12
15
|
import { resolveCharacterReference, } from "./characterReference.js";
|
|
13
16
|
import { ConnectionStateHandler } from "./ConnectionStateHandler.js";
|
|
14
|
-
import { buildActionConnectConfig, buildBlendshapeConnectConfig, buildEmotionConnectConfig, } from "./connectRequest.js";
|
|
17
|
+
import { buildActionConnectConfig, buildBlendshapeConnectConfig, buildEmotionConnectConfig, serializeActionProtocolCapabilities, } from "./connectRequest.js";
|
|
15
18
|
import { shouldPreemptForContextUpdateResponse, shouldPreemptForExplicitRespondMode, } from "./contextUpdateRequest.js";
|
|
16
19
|
import { resolveVisionInputConfig, serializeVisionInputConfig, } from "./visionRequest.js";
|
|
17
20
|
import { SSESession } from "./SSESession.js";
|
|
@@ -88,6 +91,11 @@ export class ConvaiClient extends EventEmitter {
|
|
|
88
91
|
this._characterVersionManager = null;
|
|
89
92
|
this._characterVersionManagerKey = null;
|
|
90
93
|
this._characterSessionId = "-1";
|
|
94
|
+
/** Roster state for multi-character rooms. Empty for single-character sessions. */
|
|
95
|
+
this._roster = new CharacterRoster();
|
|
96
|
+
/** Roster commands awaiting their ack, keyed by command id. */
|
|
97
|
+
this._pendingRosterCommands = new Map();
|
|
98
|
+
this._rosterCommandSeq = 0;
|
|
91
99
|
this._isBotReady = false;
|
|
92
100
|
this._participantSid = "";
|
|
93
101
|
this._storedConfig = null;
|
|
@@ -116,6 +124,23 @@ export class ConvaiClient extends EventEmitter {
|
|
|
116
124
|
if (config) {
|
|
117
125
|
this._storedConfig = config;
|
|
118
126
|
this._logRtviMessages = config.logRtviMessages !== false;
|
|
127
|
+
// Derive the version readouts from the config eagerly, as
|
|
128
|
+
// characterVersions already is, so a React consumer destructuring
|
|
129
|
+
// characterReference off the hook sees the selector on first render
|
|
130
|
+
// rather than null until connect(). A malformed selector is left for
|
|
131
|
+
// connect() to reject -- the constructor has never thrown on config and
|
|
132
|
+
// starting to would be a behaviour change.
|
|
133
|
+
if (config.characterId && !isRosterConfig(config)) {
|
|
134
|
+
try {
|
|
135
|
+
const ref = resolveCharacterReference(config.characterId, config.characterVersion);
|
|
136
|
+
this._characterId = ref.characterId;
|
|
137
|
+
this._characterVersion = ref.version;
|
|
138
|
+
this._characterReference = ref.reference;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
/* reported by connect() */
|
|
142
|
+
}
|
|
143
|
+
}
|
|
119
144
|
}
|
|
120
145
|
// Initialize room with no reconnect policy
|
|
121
146
|
this._room = new Room({
|
|
@@ -133,6 +158,8 @@ export class ConvaiClient extends EventEmitter {
|
|
|
133
158
|
endUserId: null,
|
|
134
159
|
endUserMetadata: null,
|
|
135
160
|
metrics: [],
|
|
161
|
+
characters: [],
|
|
162
|
+
activeMembershipId: null,
|
|
136
163
|
disconnectReason: null,
|
|
137
164
|
};
|
|
138
165
|
// Default audio settings for optimal interruption handling
|
|
@@ -223,6 +250,41 @@ export class ConvaiClient extends EventEmitter {
|
|
|
223
250
|
get characterSessionId() {
|
|
224
251
|
return this._characterSessionId;
|
|
225
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* Server-owned snapshot of a multi-character room, or null for a
|
|
255
|
+
* single-character session. Carries the room id, epochs and roster.
|
|
256
|
+
*/
|
|
257
|
+
get roomSession() {
|
|
258
|
+
return this._roster.roomSession;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Character instances in a multi-character room, in roster order. Empty for a
|
|
262
|
+
* single-character session, so `length` distinguishes the two.
|
|
263
|
+
*/
|
|
264
|
+
get characters() {
|
|
265
|
+
return this._roster.characters;
|
|
266
|
+
}
|
|
267
|
+
/** Membership currently receiving user turns, or null. */
|
|
268
|
+
get activeMembershipId() {
|
|
269
|
+
return this._roster.activeMembershipId;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Start or stop the server streaming per-turn usage/cost breakdowns as
|
|
273
|
+
* `usageUpdate` events. Informational only; server-side usage tracking and
|
|
274
|
+
* billing are unaffected either way.
|
|
275
|
+
*/
|
|
276
|
+
toggleUsageUpdates(enabled) {
|
|
277
|
+
if (!this.isTransportReady())
|
|
278
|
+
return;
|
|
279
|
+
this.publishMessage("usage-toggle", { enabled });
|
|
280
|
+
}
|
|
281
|
+
/** Insert a transcript line locally. Nothing is sent; nothing is billed. */
|
|
282
|
+
appendMessage(message) {
|
|
283
|
+
if (!message?.content?.trim()) {
|
|
284
|
+
throw new Error("appendMessage requires non-empty content");
|
|
285
|
+
}
|
|
286
|
+
return this._messageHandler.appendLocalMessage(message);
|
|
287
|
+
}
|
|
226
288
|
get isBotReady() {
|
|
227
289
|
return this._isBotReady;
|
|
228
290
|
}
|
|
@@ -249,12 +311,60 @@ export class ConvaiClient extends EventEmitter {
|
|
|
249
311
|
*/
|
|
250
312
|
setupEventListeners() {
|
|
251
313
|
// Room event listeners
|
|
314
|
+
// Per-membership audio binding. In a roster room every character is its
|
|
315
|
+
// own participant with identity `character:{membership_id}`; the guide's
|
|
316
|
+
// rule is to bind a track to the exact member or drop it, never to fall
|
|
317
|
+
// back to the active character. Single-character rooms keep the legacy
|
|
318
|
+
// behaviour: the track is surfaced with no attribution.
|
|
319
|
+
this._room.on(RoomEvent.TrackSubscribed, (track, _publication, participant) => {
|
|
320
|
+
if (track.kind !== "audio")
|
|
321
|
+
return;
|
|
322
|
+
const identity = participant.identity;
|
|
323
|
+
if (!this._roster.isMultiCharacter) {
|
|
324
|
+
this.emit("characterAudioTrack", {
|
|
325
|
+
membershipId: null,
|
|
326
|
+
characterId: this._characterId,
|
|
327
|
+
participantIdentity: identity,
|
|
328
|
+
track: track.mediaStreamTrack,
|
|
329
|
+
});
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const membershipId = identity.startsWith("character:")
|
|
333
|
+
? identity.slice("character:".length)
|
|
334
|
+
: null;
|
|
335
|
+
const member = membershipId ? this._roster.find(membershipId) : undefined;
|
|
336
|
+
if (!member) {
|
|
337
|
+
// Unattributable in a roster room: quarantine rather than guess.
|
|
338
|
+
if (this._logRtviMessages) {
|
|
339
|
+
console.warn(`[ConvaiClient] dropped audio track from unattributed participant "${identity}"`);
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
this.emit("characterAudioTrack", {
|
|
344
|
+
membershipId: member.membershipId,
|
|
345
|
+
characterId: member.characterId,
|
|
346
|
+
participantIdentity: identity,
|
|
347
|
+
track: track.mediaStreamTrack,
|
|
348
|
+
});
|
|
349
|
+
});
|
|
252
350
|
this._room.on(RoomEvent.Disconnected, (reason) => {
|
|
253
351
|
this._connectionStateHandler.handleDisconnected(reason);
|
|
254
352
|
});
|
|
255
353
|
this._room.on(RoomEvent.ConnectionStateChanged, this._connectionStateHandler.handleConnectionStateChanged.bind(this._connectionStateHandler));
|
|
256
354
|
// Message handler events
|
|
257
|
-
this._messageHandler.on("botReady", () => {
|
|
355
|
+
this._messageHandler.on("botReady", (about) => {
|
|
356
|
+
// Each roster member sends its own bot-ready. Feed the roster before the
|
|
357
|
+
// de-dupe below, which only guards the single consumer-facing event --
|
|
358
|
+
// otherwise every member after the first would be dropped.
|
|
359
|
+
if (this._roster.applyBotReady(about)) {
|
|
360
|
+
this.updateState({
|
|
361
|
+
characters: this._roster.characters,
|
|
362
|
+
activeMembershipId: this._roster.activeMembershipId,
|
|
363
|
+
});
|
|
364
|
+
const membershipId = about?.membership_id;
|
|
365
|
+
if (membershipId)
|
|
366
|
+
this.emit("characterReady", this._roster.find(membershipId));
|
|
367
|
+
}
|
|
258
368
|
// Server may send multiple bot-ready frames (one per client-ready
|
|
259
369
|
// we retried). Only surface the first to consumers per connect.
|
|
260
370
|
if (this._isBotReady) {
|
|
@@ -348,6 +458,9 @@ export class ConvaiClient extends EventEmitter {
|
|
|
348
458
|
this._messageHandler.on("llmNoResponse", () => {
|
|
349
459
|
this.emit("llmNoResponse");
|
|
350
460
|
});
|
|
461
|
+
this._messageHandler.on("botTurnCompleted", (data) => {
|
|
462
|
+
this.emit("botTurnCompleted", data);
|
|
463
|
+
});
|
|
351
464
|
// Forward bot-output event (aggregated output with spoken status)
|
|
352
465
|
this._messageHandler.on("botOutput", (data) => {
|
|
353
466
|
this.emit("botOutput", data);
|
|
@@ -373,7 +486,20 @@ export class ConvaiClient extends EventEmitter {
|
|
|
373
486
|
this._messageHandler.on("actionResponse", (data) => {
|
|
374
487
|
this.emit("actionResponse", data);
|
|
375
488
|
});
|
|
489
|
+
this._messageHandler.on("usageUpdate", (data) => {
|
|
490
|
+
this.emit("usageUpdate", data);
|
|
491
|
+
});
|
|
492
|
+
this._messageHandler.on("characterStatus", (status) => {
|
|
493
|
+
if (this._roster.applyCharacterStatus(status)) {
|
|
494
|
+
this.updateState({
|
|
495
|
+
characters: this._roster.characters,
|
|
496
|
+
activeMembershipId: this._roster.activeMembershipId,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
this.emit("characterStatus", status);
|
|
500
|
+
});
|
|
376
501
|
this._messageHandler.on("serverResponse", (response) => {
|
|
502
|
+
this._handleRosterServerResponse(response);
|
|
377
503
|
this.emit("serverResponse", response);
|
|
378
504
|
});
|
|
379
505
|
this._messageHandler.on("interactionCreated", (data) => {
|
|
@@ -520,6 +646,14 @@ export class ConvaiClient extends EventEmitter {
|
|
|
520
646
|
enableEmotion: configWithDefaults.enableEmotion,
|
|
521
647
|
emotionConfig: configWithDefaults.emotionConfig,
|
|
522
648
|
actionConfig: configWithDefaults.actionConfig,
|
|
649
|
+
capabilities: configWithDefaults.capabilities,
|
|
650
|
+
dynamicInfo: configWithDefaults.dynamicInfo,
|
|
651
|
+
keepInContext: configWithDefaults.keepInContext,
|
|
652
|
+
narrativeTemplateKeys: configWithDefaults.narrativeTemplateKeys,
|
|
653
|
+
sceneDescription: configWithDefaults.sceneDescription,
|
|
654
|
+
respondModes: configWithDefaults.respondModes,
|
|
655
|
+
useResponseLifecycleContract: configWithDefaults.useResponseLifecycleContract,
|
|
656
|
+
invocationMetadata: configWithDefaults.invocationMetadata,
|
|
523
657
|
debug: configWithDefaults.debug,
|
|
524
658
|
onMessage: (payload) => this._messageHandler.handleDataReceivedPublic(payload),
|
|
525
659
|
onError: (error) => this.emit("error", error),
|
|
@@ -539,8 +673,11 @@ export class ConvaiClient extends EventEmitter {
|
|
|
539
673
|
}
|
|
540
674
|
// Prepare request body with required parameters
|
|
541
675
|
const characterSessionIdToSend = configWithDefaults.characterSessionId ?? this._characterSessionId;
|
|
676
|
+
const isRoster = isRosterConfig(configWithDefaults);
|
|
542
677
|
const requestBody = {
|
|
543
|
-
|
|
678
|
+
...(isRoster
|
|
679
|
+
? buildRosterConnectFields(configWithDefaults)
|
|
680
|
+
: { character_id: configWithDefaults.characterReference }),
|
|
544
681
|
...(stateOfMind ? { state_of_mind: stateOfMind } : {}),
|
|
545
682
|
...(configWithDefaults.endUserId && {
|
|
546
683
|
end_user_id: configWithDefaults.endUserId,
|
|
@@ -551,7 +688,6 @@ export class ConvaiClient extends EventEmitter {
|
|
|
551
688
|
transport: transportType,
|
|
552
689
|
connection_type: connType,
|
|
553
690
|
...buildBlendshapeConnectConfig(configWithDefaults),
|
|
554
|
-
llm_provider: "dynamic",
|
|
555
691
|
...(buildEmotionConnectConfig(configWithDefaults) && {
|
|
556
692
|
emotion_config: buildEmotionConnectConfig(configWithDefaults),
|
|
557
693
|
}),
|
|
@@ -565,7 +701,16 @@ export class ConvaiClient extends EventEmitter {
|
|
|
565
701
|
...(configWithDefaults.respondModes && {
|
|
566
702
|
respond_modes: this.serializeRespondModes(configWithDefaults.respondModes),
|
|
567
703
|
}),
|
|
568
|
-
...(
|
|
704
|
+
...(serializeActionProtocolCapabilities(configWithDefaults.capabilities) && {
|
|
705
|
+
capabilities: serializeActionProtocolCapabilities(configWithDefaults.capabilities),
|
|
706
|
+
}),
|
|
707
|
+
...(configWithDefaults.sceneDescription && {
|
|
708
|
+
scene_description: configWithDefaults.sceneDescription,
|
|
709
|
+
}),
|
|
710
|
+
// A roster room owns its members' sessions; the singular resume id is
|
|
711
|
+
// a compatibility mirror and must not go back on the wire.
|
|
712
|
+
...(!isRoster &&
|
|
713
|
+
characterSessionIdToSend &&
|
|
569
714
|
characterSessionIdToSend !== "-1" && {
|
|
570
715
|
character_session_id: characterSessionIdToSend,
|
|
571
716
|
}),
|
|
@@ -587,6 +732,12 @@ export class ConvaiClient extends EventEmitter {
|
|
|
587
732
|
...(configWithDefaults.debug !== undefined && {
|
|
588
733
|
debug: configWithDefaults.debug,
|
|
589
734
|
}),
|
|
735
|
+
...(configWithDefaults.preserveLinksInOutput !== undefined && {
|
|
736
|
+
preserve_links_in_output: configWithDefaults.preserveLinksInOutput,
|
|
737
|
+
}),
|
|
738
|
+
...(configWithDefaults.useResponseLifecycleContract !== undefined && {
|
|
739
|
+
use_response_lifecycle_contract: configWithDefaults.useResponseLifecycleContract,
|
|
740
|
+
}),
|
|
590
741
|
invocation_metadata: {
|
|
591
742
|
source: configWithDefaults.invocationMetadata?.source ?? "web_sdk",
|
|
592
743
|
client_version: configWithDefaults.invocationMetadata?.clientVersion ??
|
|
@@ -605,41 +756,62 @@ export class ConvaiClient extends EventEmitter {
|
|
|
605
756
|
else if (configWithDefaults.apiKey) {
|
|
606
757
|
connectHeaders["X-API-Key"] = configWithDefaults.apiKey;
|
|
607
758
|
}
|
|
608
|
-
// HTTP POST /connect — same request for both transports
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
759
|
+
// HTTP POST /connect — same request for both transports. A roster room
|
|
760
|
+
// may still be provisioning from an identical concurrent create, which
|
|
761
|
+
// the runtime answers with a retryable 409; the wrapper replays the
|
|
762
|
+
// identical payload and is a straight pass-through otherwise.
|
|
763
|
+
const connectOnce = async () => {
|
|
764
|
+
const response = await fetch(`${configWithDefaults.url}/connect`, {
|
|
765
|
+
method: "POST",
|
|
766
|
+
headers: connectHeaders,
|
|
767
|
+
body: JSON.stringify(requestBody),
|
|
768
|
+
});
|
|
769
|
+
if (!response.ok) {
|
|
770
|
+
const errorText = await response.text();
|
|
771
|
+
let errorMessage = `HTTP ${response.status}`;
|
|
772
|
+
try {
|
|
773
|
+
const errorData = JSON.parse(errorText);
|
|
774
|
+
const raw = errorData.message ?? errorData.error ?? errorData.detail;
|
|
775
|
+
if (typeof raw === "string") {
|
|
776
|
+
errorMessage = raw;
|
|
777
|
+
}
|
|
778
|
+
else if (Array.isArray(raw)) {
|
|
779
|
+
// FastAPI/Pydantic 422 — detail is an array of validation error objects
|
|
780
|
+
errorMessage = raw
|
|
781
|
+
.map((e) => [e.loc?.join("."), e.msg].filter(Boolean).join(": "))
|
|
782
|
+
.join("; ") || errorMessage;
|
|
783
|
+
}
|
|
784
|
+
else if (raw != null) {
|
|
785
|
+
errorMessage = JSON.stringify(raw);
|
|
786
|
+
}
|
|
787
|
+
else if (errorText) {
|
|
788
|
+
errorMessage = `${errorMessage}: ${errorText}`;
|
|
789
|
+
}
|
|
628
790
|
}
|
|
629
|
-
|
|
630
|
-
|
|
791
|
+
catch {
|
|
792
|
+
if (errorText)
|
|
793
|
+
errorMessage = `${errorMessage}: ${errorText}`;
|
|
631
794
|
}
|
|
632
|
-
|
|
633
|
-
|
|
795
|
+
// A roster room's failures are named and machine-actionable, and the
|
|
796
|
+
// trace id is what backend support correlates on, so they get a typed
|
|
797
|
+
// error. Single-character behaviour is unchanged.
|
|
798
|
+
if (isRoster) {
|
|
799
|
+
let parsed = errorText;
|
|
800
|
+
try {
|
|
801
|
+
parsed = JSON.parse(errorText);
|
|
802
|
+
}
|
|
803
|
+
catch {
|
|
804
|
+
/* keep the raw text as the detail */
|
|
805
|
+
}
|
|
806
|
+
throw roomErrorFromResponse(response.status, parsed);
|
|
634
807
|
}
|
|
808
|
+
throw new Error(errorMessage);
|
|
635
809
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
}
|
|
642
|
-
const connectionData = await response.json();
|
|
810
|
+
return await response.json();
|
|
811
|
+
};
|
|
812
|
+
const connectionData = isRoster
|
|
813
|
+
? await this._withProvisioningRetry(configWithDefaults, connectOnce)
|
|
814
|
+
: await connectOnce();
|
|
643
815
|
return await this.consumeConnectionData(connectionData, configWithDefaults);
|
|
644
816
|
}
|
|
645
817
|
catch (error) {
|
|
@@ -703,6 +875,270 @@ export class ConvaiClient extends EventEmitter {
|
|
|
703
875
|
throw error;
|
|
704
876
|
}
|
|
705
877
|
}
|
|
878
|
+
/** Correlate roster acks back to the promise that issued the command. */
|
|
879
|
+
_handleRosterServerResponse(response) {
|
|
880
|
+
const eventType = response.event_type;
|
|
881
|
+
if (eventType !== "interaction-target" &&
|
|
882
|
+
eventType !== "character-roster-update") {
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
const extras = response.extras ?? null;
|
|
886
|
+
const commandId = extras?.command_id;
|
|
887
|
+
const pending = commandId
|
|
888
|
+
? this._pendingRosterCommands.get(commandId)
|
|
889
|
+
: undefined;
|
|
890
|
+
if (response.status === "error") {
|
|
891
|
+
// Adopt the server's epochs even from a rejection -- a stale-epoch error
|
|
892
|
+
// carries the authoritative value, and that is what makes a retry
|
|
893
|
+
// meaningful.
|
|
894
|
+
this._roster.adoptEpochs(extras);
|
|
895
|
+
this._pushRosterState();
|
|
896
|
+
if (pending && commandId) {
|
|
897
|
+
this._pendingRosterCommands.delete(commandId);
|
|
898
|
+
clearTimeout(pending.timer);
|
|
899
|
+
pending.reject(Object.assign(new Error(response.message ?? `${eventType} was rejected`), { code: extras?.code ?? null, extras }));
|
|
900
|
+
}
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
// Apply the change whether or not this client asked for it: another
|
|
904
|
+
// participant's switch reaches us through the same event.
|
|
905
|
+
let change;
|
|
906
|
+
if (eventType === "interaction-target") {
|
|
907
|
+
change = this._roster.applyInteractionTarget(extras);
|
|
908
|
+
this._pushRosterState();
|
|
909
|
+
this.emit("activeCharacterChange", change);
|
|
910
|
+
}
|
|
911
|
+
else {
|
|
912
|
+
change = this._roster.applyRosterUpdate(extras);
|
|
913
|
+
this._pushRosterState();
|
|
914
|
+
this.emit("rosterChange", change);
|
|
915
|
+
}
|
|
916
|
+
if (pending && commandId) {
|
|
917
|
+
this._pendingRosterCommands.delete(commandId);
|
|
918
|
+
clearTimeout(pending.timer);
|
|
919
|
+
pending.resolve(change);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Settle every in-flight roster command.
|
|
924
|
+
*
|
|
925
|
+
* Without this a caller who disconnects mid-command keeps a live 10s timer
|
|
926
|
+
* and gets a rejection long after the session ended -- an unhandled rejection
|
|
927
|
+
* if they stopped awaiting when they tore the client down. The roster is
|
|
928
|
+
* reset alongside, so the command could not be applied meaningfully anyway.
|
|
929
|
+
*/
|
|
930
|
+
_failPendingRosterCommands(reason) {
|
|
931
|
+
for (const [, pending] of this._pendingRosterCommands) {
|
|
932
|
+
clearTimeout(pending.timer);
|
|
933
|
+
pending.reject(new Error(reason));
|
|
934
|
+
}
|
|
935
|
+
this._pendingRosterCommands.clear();
|
|
936
|
+
}
|
|
937
|
+
_pushRosterState() {
|
|
938
|
+
this.updateState({
|
|
939
|
+
characters: this._roster.characters,
|
|
940
|
+
activeMembershipId: this._roster.activeMembershipId,
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
/** Publish a roster command and settle when its ack arrives. */
|
|
944
|
+
_sendRosterCommand(type, build) {
|
|
945
|
+
if (!this._roster.isMultiCharacter) {
|
|
946
|
+
return Promise.reject(new Error(`Cannot send ${type}: this session was not created with a characters array`));
|
|
947
|
+
}
|
|
948
|
+
if (!this.isTransportReady()) {
|
|
949
|
+
return Promise.reject(new Error(`Cannot send ${type}: not connected`));
|
|
950
|
+
}
|
|
951
|
+
const commandId = `${type}-${++this._rosterCommandSeq}-${Date.now()}`;
|
|
952
|
+
return new Promise((resolve, reject) => {
|
|
953
|
+
const timer = setTimeout(() => {
|
|
954
|
+
this._pendingRosterCommands.delete(commandId);
|
|
955
|
+
reject(new Error(`${type} was not acknowledged within ${ConvaiClient.ROSTER_COMMAND_TIMEOUT_MS}ms. ` +
|
|
956
|
+
`The outcome is unknown -- acks can be suppressed as duplicates. ` +
|
|
957
|
+
`Reconcile from the next authoritative room state before sending a conflicting command.`));
|
|
958
|
+
}, ConvaiClient.ROSTER_COMMAND_TIMEOUT_MS);
|
|
959
|
+
this._pendingRosterCommands.set(commandId, { resolve, reject, timer });
|
|
960
|
+
this.publishMessage(type, build(commandId), commandId);
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Route subsequent user turns to a different character instance.
|
|
965
|
+
*
|
|
966
|
+
* Pass null to close every gate, so no member consumes the user until a
|
|
967
|
+
* target is set again.
|
|
968
|
+
*/
|
|
969
|
+
async setInteractionTarget(membershipId) {
|
|
970
|
+
if (!this._roster.isMultiCharacter) {
|
|
971
|
+
throw new Error("Cannot send interaction-target: this session was not created with a characters array");
|
|
972
|
+
}
|
|
973
|
+
if (membershipId !== null) {
|
|
974
|
+
if (!membershipId) {
|
|
975
|
+
throw new Error("setInteractionTarget requires a membershipId, or null to close all gates");
|
|
976
|
+
}
|
|
977
|
+
const instance = this._roster.find(membershipId);
|
|
978
|
+
if (this._roster.isMultiCharacter && !instance) {
|
|
979
|
+
throw new Error(`Unknown membershipId "${membershipId}". Address instances by membershipId, not characterId.`);
|
|
980
|
+
}
|
|
981
|
+
if (instance && !instance.isReady) {
|
|
982
|
+
throw new Error(`Membership "${membershipId}" has not sent bot-ready yet; the runtime refuses an unready target`);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
const send = () => this._sendRosterCommand("interaction-target", (commandId) => this._roster.buildInteractionTarget(membershipId, commandId));
|
|
986
|
+
try {
|
|
987
|
+
return (await send());
|
|
988
|
+
}
|
|
989
|
+
catch (error) {
|
|
990
|
+
// Another participant switched first. The roster adopted the server's
|
|
991
|
+
// epoch when the rejection arrived, so exactly one retry is now fenced
|
|
992
|
+
// correctly; a second would mean we are losing a race we should report.
|
|
993
|
+
if (error?.code !== "stale_route_epoch")
|
|
994
|
+
throw error;
|
|
995
|
+
return (await send());
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
/** Add or remove character instances on a live room, without reconnecting. */
|
|
999
|
+
async updateCharacterRoster(options) {
|
|
1000
|
+
// Report the wrong-session-shape first. Otherwise a single-character
|
|
1001
|
+
// client, whose roster is empty, trips the "cannot be emptied" rule below
|
|
1002
|
+
// and gets an error about a room it never had.
|
|
1003
|
+
if (!this._roster.isMultiCharacter) {
|
|
1004
|
+
throw new Error("Cannot send character-roster-update: this session was not created with a characters array");
|
|
1005
|
+
}
|
|
1006
|
+
const add = options?.add ?? [];
|
|
1007
|
+
const remove = options?.remove ?? [];
|
|
1008
|
+
if (add.length === 0 && remove.length === 0) {
|
|
1009
|
+
throw new Error("updateCharacterRoster requires at least one add or remove");
|
|
1010
|
+
}
|
|
1011
|
+
if (add.some((c) => !c?.characterId?.trim())) {
|
|
1012
|
+
throw new Error("Every entry in add requires a characterId");
|
|
1013
|
+
}
|
|
1014
|
+
if (remove.length && remove.length >= this._roster.characters.length && add.length === 0) {
|
|
1015
|
+
throw new Error("A roster room cannot be emptied; add a replacement or keep one member");
|
|
1016
|
+
}
|
|
1017
|
+
// Removing the active member needs an explicit, ready replacement --
|
|
1018
|
+
// the runtime answers `replacement_target_required` otherwise, and
|
|
1019
|
+
// `replacement_target_unavailable` for one that has not sent bot-ready.
|
|
1020
|
+
const active = this._roster.activeMembershipId;
|
|
1021
|
+
const effective = !options.replacementTarget && active && remove.includes(active)
|
|
1022
|
+
? { ...options, replacementTarget: this._roster.pickReplacement(remove) }
|
|
1023
|
+
: options;
|
|
1024
|
+
return (await this._sendRosterCommand("character-roster-update", (commandId) => this._roster.buildRosterUpdate(effective, commandId)));
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Run a room request, retrying only the conflicts that mean "still working".
|
|
1028
|
+
*
|
|
1029
|
+
* The runtime answers a create that collides with an in-flight identical
|
|
1030
|
+
* create with 409 `ROSTER_PROVISIONING_IN_PROGRESS`, and the documented
|
|
1031
|
+
* recovery is to retry the same key with bounded backoff. Left to callers,
|
|
1032
|
+
* every integration reimplements that, usually without a cap. The payload is
|
|
1033
|
+
* replayed verbatim -- changing it would turn the retry into a fingerprint
|
|
1034
|
+
* mismatch.
|
|
1035
|
+
*/
|
|
1036
|
+
async _withProvisioningRetry(config, run) {
|
|
1037
|
+
const setting = config.rosterProvisioningRetry;
|
|
1038
|
+
if (setting === false)
|
|
1039
|
+
return run();
|
|
1040
|
+
const attempts = Math.max(1, setting?.attempts ?? ConvaiClient.ROSTER_RETRY_ATTEMPTS);
|
|
1041
|
+
const initialDelayMs = Math.max(0, setting?.initialDelayMs ?? ConvaiClient.ROSTER_RETRY_INITIAL_MS);
|
|
1042
|
+
let lastError;
|
|
1043
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
1044
|
+
try {
|
|
1045
|
+
return await run();
|
|
1046
|
+
}
|
|
1047
|
+
catch (error) {
|
|
1048
|
+
lastError = error;
|
|
1049
|
+
const retryable = error instanceof ConvaiRoomError && error.isRetryable;
|
|
1050
|
+
if (!retryable || attempt === attempts - 1)
|
|
1051
|
+
throw error;
|
|
1052
|
+
// Jitter so a lobby of clients waking on the same room does not
|
|
1053
|
+
// retry in lockstep.
|
|
1054
|
+
const base = initialDelayMs * 2 ** attempt;
|
|
1055
|
+
const jittered = base * (0.8 + Math.random() * 0.4);
|
|
1056
|
+
await new Promise((resolve) => setTimeout(resolve, jittered));
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
throw lastError;
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Join a multi-character room that already exists.
|
|
1063
|
+
*
|
|
1064
|
+
* A join supplies a locator and the joining human, and nothing else: the
|
|
1065
|
+
* roster is server-owned, so resending topology is rejected. It never spawns,
|
|
1066
|
+
* removes or repairs a character -- only room creation does that.
|
|
1067
|
+
*
|
|
1068
|
+
* Capacity counts distinct speakers derived from `endUserId`, and defaults to
|
|
1069
|
+
* one, so raise `maxNumParticipants` at creation before expecting a second
|
|
1070
|
+
* human to get in.
|
|
1071
|
+
*
|
|
1072
|
+
* @example
|
|
1073
|
+
* ```ts
|
|
1074
|
+
* const client = new ConvaiClient({ apiKey: 'YOUR_API_KEY' })
|
|
1075
|
+
* await client.joinRoom({
|
|
1076
|
+
* roomSessionId: 'the-room-session-id',
|
|
1077
|
+
* endUserId: 'player-99',
|
|
1078
|
+
* })
|
|
1079
|
+
* ```
|
|
1080
|
+
*/
|
|
1081
|
+
async joinRoom(options) {
|
|
1082
|
+
validateJoinOptions(options);
|
|
1083
|
+
const stored = this._storedConfig ?? {};
|
|
1084
|
+
const url = stored.url ?? "https://realtime-api.convai.com";
|
|
1085
|
+
if (!stored.apiKey && !stored.authToken) {
|
|
1086
|
+
throw new Error("Either apiKey or authToken is required to join a room");
|
|
1087
|
+
}
|
|
1088
|
+
this._logRtviMessages = stored.logRtviMessages !== false;
|
|
1089
|
+
this._messageHandler.setRtviMessageLogging(this._logRtviMessages);
|
|
1090
|
+
this.updateState({ isConnecting: true });
|
|
1091
|
+
try {
|
|
1092
|
+
const connectionData = await this._withProvisioningRetry(stored, async () => {
|
|
1093
|
+
const response = await fetch(`${url}/connect`, {
|
|
1094
|
+
method: "POST",
|
|
1095
|
+
headers: {
|
|
1096
|
+
"Content-Type": "application/json",
|
|
1097
|
+
...(stored.apiKey
|
|
1098
|
+
? { "X-API-Key": stored.apiKey }
|
|
1099
|
+
: { "API-AUTH-TOKEN": stored.authToken }),
|
|
1100
|
+
},
|
|
1101
|
+
body: JSON.stringify(buildJoinRequestBody(options)),
|
|
1102
|
+
});
|
|
1103
|
+
if (!response.ok) {
|
|
1104
|
+
const text = await response.text();
|
|
1105
|
+
let parsed = text;
|
|
1106
|
+
try {
|
|
1107
|
+
parsed = JSON.parse(text);
|
|
1108
|
+
}
|
|
1109
|
+
catch {
|
|
1110
|
+
/* keep the raw text as the detail */
|
|
1111
|
+
}
|
|
1112
|
+
throw roomErrorFromResponse(response.status, parsed);
|
|
1113
|
+
}
|
|
1114
|
+
return (await response.json());
|
|
1115
|
+
});
|
|
1116
|
+
// The join response carries the persisted roster, so the joining client
|
|
1117
|
+
// learns the topology it never sent.
|
|
1118
|
+
const rosterCharacters = connectionData.characters;
|
|
1119
|
+
const initialCharacterId = rosterCharacters?.[0]?.character_id;
|
|
1120
|
+
const joinConfig = {
|
|
1121
|
+
...stored,
|
|
1122
|
+
characters: undefined,
|
|
1123
|
+
characterId: initialCharacterId ?? stored.characterId,
|
|
1124
|
+
endUserId: options.endUserId,
|
|
1125
|
+
...(options.endUserMetadata
|
|
1126
|
+
? { endUserMetadata: options.endUserMetadata }
|
|
1127
|
+
: {}),
|
|
1128
|
+
};
|
|
1129
|
+
this._storedConfig = joinConfig;
|
|
1130
|
+
const configWithDefaults = this.applyConfigDefaults(joinConfig, {
|
|
1131
|
+
requireCredential: false,
|
|
1132
|
+
});
|
|
1133
|
+
return await this.consumeConnectionData(connectionData, configWithDefaults);
|
|
1134
|
+
}
|
|
1135
|
+
catch (error) {
|
|
1136
|
+
await this.cleanupWebSocketSessionAfterConnectFailure();
|
|
1137
|
+
this.updateState({ isConnected: false, isConnecting: false });
|
|
1138
|
+
this.emit("error", error);
|
|
1139
|
+
throw error;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
706
1142
|
/**
|
|
707
1143
|
* Add default URL, apply RTVI-logging config, and validate that either an
|
|
708
1144
|
* API key or auth token plus a character ID are present.
|
|
@@ -727,9 +1163,27 @@ export class ConvaiClient extends EventEmitter {
|
|
|
727
1163
|
const hasApiKey = Boolean(configWithDefaults.apiKey);
|
|
728
1164
|
const hasAuthToken = Boolean(configWithDefaults.authToken);
|
|
729
1165
|
const missingCredential = requireCredential && !hasApiKey && !hasAuthToken;
|
|
730
|
-
|
|
1166
|
+
// Runs for every topology: the v2 protocol constraints rule out
|
|
1167
|
+
// sharedSessionKey and extra human capacity in single-character rooms too.
|
|
1168
|
+
validateProtocolTopology(configWithDefaults);
|
|
1169
|
+
const roster = isRosterConfig(configWithDefaults);
|
|
1170
|
+
if (missingCredential || (!roster && !configWithDefaults.characterId)) {
|
|
731
1171
|
throw new Error("Either apiKey or authToken is required, and characterId is required");
|
|
732
1172
|
}
|
|
1173
|
+
// A roster names its members individually; every roster rule is checked
|
|
1174
|
+
// before a request is built.
|
|
1175
|
+
if (roster) {
|
|
1176
|
+
validateRosterConfig(configWithDefaults);
|
|
1177
|
+
const initial = configWithDefaults.characters[0].characterId.trim();
|
|
1178
|
+
return {
|
|
1179
|
+
...configWithDefaults,
|
|
1180
|
+
// Singular readouts mirror characters[0], exactly as the wire's own
|
|
1181
|
+
// top-level session_id/character_session_id aliases do.
|
|
1182
|
+
characterId: initial,
|
|
1183
|
+
characterVersion: null,
|
|
1184
|
+
characterReference: initial,
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
733
1187
|
// Split the character id from its version selector. The bare UUID names
|
|
734
1188
|
// the character everywhere inside the SDK (memory, character info); the
|
|
735
1189
|
// joined reference is what the runtime receives.
|
|
@@ -790,6 +1244,14 @@ export class ConvaiClient extends EventEmitter {
|
|
|
790
1244
|
characterSessionId: connectionData.character_session_id,
|
|
791
1245
|
};
|
|
792
1246
|
}
|
|
1247
|
+
// Seed the roster before the transport comes up, so the first bot-ready and
|
|
1248
|
+
// any early roster change already have instances to match against.
|
|
1249
|
+
if (this._roster.applyConnectResponse(connectionData)) {
|
|
1250
|
+
this.updateState({
|
|
1251
|
+
characters: this._roster.characters,
|
|
1252
|
+
activeMembershipId: this._roster.activeMembershipId,
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
793
1255
|
if (connectionData.end_user_id) {
|
|
794
1256
|
this._endUserId = connectionData.end_user_id;
|
|
795
1257
|
}
|
|
@@ -1027,6 +1489,10 @@ export class ConvaiClient extends EventEmitter {
|
|
|
1027
1489
|
*/
|
|
1028
1490
|
async disconnect() {
|
|
1029
1491
|
this._stopClientReadyHandshake();
|
|
1492
|
+
// Unconditional: the branches below each depend on a transport being
|
|
1493
|
+
// matched, and none of them runs when there is nothing to tear down. A
|
|
1494
|
+
// command in flight must still be settled either way.
|
|
1495
|
+
this._failPendingRosterCommands("Disconnected before the roster command was acknowledged; its outcome is unknown");
|
|
1030
1496
|
if (this._activeTransport === "sse" && this._sseSession) {
|
|
1031
1497
|
await this._sseSession.disconnect();
|
|
1032
1498
|
this._connectionStateHandler.resetConnectionState();
|
|
@@ -1059,6 +1525,15 @@ export class ConvaiClient extends EventEmitter {
|
|
|
1059
1525
|
this.clearAllManagers();
|
|
1060
1526
|
}
|
|
1061
1527
|
}
|
|
1528
|
+
else if (this._state.isConnected || this._state.isConnecting) {
|
|
1529
|
+
// No transport matched, but the client believes it is (or is becoming)
|
|
1530
|
+
// connected -- a connect() that failed after flipping isConnecting, or
|
|
1531
|
+
// was torn down underneath us. Without this branch disconnect() was a
|
|
1532
|
+
// silent no-op here and the flags stayed stuck, so a UI bound to
|
|
1533
|
+
// stateChange kept showing a session that did not exist.
|
|
1534
|
+
this._connectionStateHandler.resetConnectionState();
|
|
1535
|
+
this.clearAllManagers();
|
|
1536
|
+
}
|
|
1062
1537
|
}
|
|
1063
1538
|
/**
|
|
1064
1539
|
* Clear all manager states
|
|
@@ -1067,6 +1542,8 @@ export class ConvaiClient extends EventEmitter {
|
|
|
1067
1542
|
this._connectionType = null;
|
|
1068
1543
|
this._activeTransport = null;
|
|
1069
1544
|
this._wsSession = null;
|
|
1545
|
+
this._failPendingRosterCommands("Disconnected before the roster command was acknowledged; its outcome is unknown");
|
|
1546
|
+
this._roster.reset();
|
|
1070
1547
|
this._sseSession = null;
|
|
1071
1548
|
this._apiKey = null;
|
|
1072
1549
|
this._authToken = null;
|
|
@@ -1095,6 +1572,13 @@ export class ConvaiClient extends EventEmitter {
|
|
|
1095
1572
|
* Reset the session ID to start a new conversation
|
|
1096
1573
|
*/
|
|
1097
1574
|
resetSession() {
|
|
1575
|
+
// In a roster room the singular session id is a compatibility mirror of
|
|
1576
|
+
// characters[0], and character sessions are server-owned -- there is no
|
|
1577
|
+
// client-side reset for them. Throwing names the situation; a silent no-op
|
|
1578
|
+
// would look like it worked.
|
|
1579
|
+
if (this._roster.isMultiCharacter) {
|
|
1580
|
+
throw new Error("resetSession() is not supported in a multi-character room; character sessions are server-owned");
|
|
1581
|
+
}
|
|
1098
1582
|
this._characterSessionId = "-1";
|
|
1099
1583
|
this.emit("characterSessionId", "-1");
|
|
1100
1584
|
this._messageHandler.reset();
|
|
@@ -1136,15 +1620,28 @@ export class ConvaiClient extends EventEmitter {
|
|
|
1136
1620
|
const stateOfMind = normalizeStateOfMind(options.stateOfMind) ?? null;
|
|
1137
1621
|
if (stateOfMind)
|
|
1138
1622
|
this._stateOfMind = stateOfMind;
|
|
1139
|
-
if (
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1623
|
+
if (this._activeTransport === "sse") {
|
|
1624
|
+
const requestedDynamicInfo = options.dynamicInfo?.trim();
|
|
1625
|
+
if (requestedDynamicInfo) {
|
|
1626
|
+
this._sseSession?.updateDynamicInfo(requestedDynamicInfo);
|
|
1627
|
+
}
|
|
1628
|
+
const messages = [];
|
|
1629
|
+
const dynamicInfo = this._sseSession?.getDynamicInfo()?.trim();
|
|
1630
|
+
if (dynamicInfo) {
|
|
1631
|
+
messages.push({
|
|
1632
|
+
type: "update-dynamic-info",
|
|
1633
|
+
data: { dynamic_info: { text: dynamicInfo } },
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
if (stateOfMind) {
|
|
1637
|
+
messages.push({ type: "update-emotion", data: { state_of_mind: stateOfMind } });
|
|
1638
|
+
}
|
|
1639
|
+
messages.push({
|
|
1640
|
+
type: "user_text_message",
|
|
1641
|
+
data: userTextMessage,
|
|
1642
|
+
...(logicalTurnId !== undefined ? { id: logicalTurnId } : {}),
|
|
1643
|
+
});
|
|
1644
|
+
this._sseSession?.sendMessages(messages);
|
|
1148
1645
|
}
|
|
1149
1646
|
else {
|
|
1150
1647
|
if (stateOfMind) {
|
|
@@ -1260,8 +1757,16 @@ export class ConvaiClient extends EventEmitter {
|
|
|
1260
1757
|
updateDynamicInfo(dynamicInfo) {
|
|
1261
1758
|
if (!this.isTransportReady() || !dynamicInfo?.trim())
|
|
1262
1759
|
return;
|
|
1760
|
+
const nextDynamicInfo = dynamicInfo.trim();
|
|
1761
|
+
if (this._storedConfig) {
|
|
1762
|
+
this._storedConfig = { ...this._storedConfig, dynamicInfo: nextDynamicInfo };
|
|
1763
|
+
}
|
|
1764
|
+
if (this._activeTransport === "sse" && this._sseSession) {
|
|
1765
|
+
this._sseSession.updateDynamicInfo(nextDynamicInfo);
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1263
1768
|
this.publishMessage("update-dynamic-info", {
|
|
1264
|
-
dynamic_info: { text:
|
|
1769
|
+
dynamic_info: { text: nextDynamicInfo },
|
|
1265
1770
|
participant_sid: this._participantSid || this._room.localParticipant?.sid,
|
|
1266
1771
|
});
|
|
1267
1772
|
}
|
|
@@ -1309,6 +1814,22 @@ export class ConvaiClient extends EventEmitter {
|
|
|
1309
1814
|
if (shouldPreemptLipsync) {
|
|
1310
1815
|
this.startTrackedConversation(`[context-update:${options.mode ?? "append"}]`);
|
|
1311
1816
|
}
|
|
1817
|
+
if (this._activeTransport === "sse" && this._sseSession) {
|
|
1818
|
+
const data = {
|
|
1819
|
+
...(hasText && { text: options.text.trim() }),
|
|
1820
|
+
...(options.mode && { mode: options.mode }),
|
|
1821
|
+
...(options.run_llm && { run_llm: options.run_llm }),
|
|
1822
|
+
...(options.respond_mode && { respond_mode: options.respond_mode }),
|
|
1823
|
+
...(hasAttention && { current_attention_object: options.current_attention_object }),
|
|
1824
|
+
};
|
|
1825
|
+
if (options.run_llm === "false") {
|
|
1826
|
+
this._sseSession.queueMessage("context-update", data);
|
|
1827
|
+
}
|
|
1828
|
+
else {
|
|
1829
|
+
this._sseSession.sendMessage("context-update", data);
|
|
1830
|
+
}
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1312
1833
|
this.publishMessage("context-update", {
|
|
1313
1834
|
...(hasText && { text: options.text.trim() }),
|
|
1314
1835
|
...(options.mode && { mode: options.mode }),
|
|
@@ -1370,6 +1891,8 @@ export class ConvaiClient extends EventEmitter {
|
|
|
1370
1891
|
}
|
|
1371
1892
|
if (!this.isTransportReady())
|
|
1372
1893
|
return;
|
|
1894
|
+
if (this._activeTransport === "sse")
|
|
1895
|
+
return;
|
|
1373
1896
|
this.publishMessage("update-emotion", {
|
|
1374
1897
|
state_of_mind: normalized,
|
|
1375
1898
|
participant_sid: this._participantSid || this._room.localParticipant?.sid,
|
|
@@ -1464,4 +1987,17 @@ export class ConvaiClient extends EventEmitter {
|
|
|
1464
1987
|
}
|
|
1465
1988
|
ConvaiClient.CLIENT_READY_RETRY_MS = 500;
|
|
1466
1989
|
ConvaiClient.CLIENT_READY_TIMEOUT_MS = 45000;
|
|
1990
|
+
/**
|
|
1991
|
+
* How long to wait for a roster command's ack.
|
|
1992
|
+
*
|
|
1993
|
+
* A timeout is *not* a failure. Every character participant observes the
|
|
1994
|
+
* broadcast and duplicate forwards are response-suppressed after one actor
|
|
1995
|
+
* answers, so a lost ack is an expected outcome. The contract's rule is to
|
|
1996
|
+
* treat it as unknown and reconcile from the next authoritative snapshot,
|
|
1997
|
+
* which is what the rejection below tells the caller to do.
|
|
1998
|
+
*/
|
|
1999
|
+
ConvaiClient.ROSTER_COMMAND_TIMEOUT_MS = 10000;
|
|
2000
|
+
/** Default bounded retry while a roster room is still provisioning. */
|
|
2001
|
+
ConvaiClient.ROSTER_RETRY_ATTEMPTS = 4;
|
|
2002
|
+
ConvaiClient.ROSTER_RETRY_INITIAL_MS = 500;
|
|
1467
2003
|
//# sourceMappingURL=ConvaiClient.js.map
|