@convai/web-sdk 1.8.0-beta.4 → 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 +64 -3
- 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/CharacterVersionManager.d.ts +100 -0
- package/dist/core/CharacterVersionManager.d.ts.map +1 -0
- package/dist/core/CharacterVersionManager.js +227 -0
- package/dist/core/CharacterVersionManager.js.map +1 -0
- package/dist/core/ConvaiClient.d.ts +109 -1
- package/dist/core/ConvaiClient.d.ts.map +1 -1
- package/dist/core/ConvaiClient.js +642 -49
- 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/characterReference.d.ts +40 -0
- package/dist/core/characterReference.d.ts.map +1 -0
- package/dist/core/characterReference.js +72 -0
- package/dist/core/characterReference.js.map +1 -0
- 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 +5 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +6 -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 +717 -7
- 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 +15 -2
- 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 +2 -0
- package/dist/vanilla/index.d.ts.map +1 -1
- package/dist/vanilla/index.js +2 -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,11 +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";
|
|
14
|
+
import { CharacterVersionManager } from "./CharacterVersionManager.js";
|
|
15
|
+
import { resolveCharacterReference, } from "./characterReference.js";
|
|
11
16
|
import { ConnectionStateHandler } from "./ConnectionStateHandler.js";
|
|
12
|
-
import { buildActionConnectConfig, buildBlendshapeConnectConfig, buildEmotionConnectConfig, } from "./connectRequest.js";
|
|
17
|
+
import { buildActionConnectConfig, buildBlendshapeConnectConfig, buildEmotionConnectConfig, serializeActionProtocolCapabilities, } from "./connectRequest.js";
|
|
13
18
|
import { shouldPreemptForContextUpdateResponse, shouldPreemptForExplicitRespondMode, } from "./contextUpdateRequest.js";
|
|
14
19
|
import { resolveVisionInputConfig, serializeVisionInputConfig, } from "./visionRequest.js";
|
|
15
20
|
import { SSESession } from "./SSESession.js";
|
|
@@ -81,7 +86,16 @@ export class ConvaiClient extends EventEmitter {
|
|
|
81
86
|
this._apiKey = null;
|
|
82
87
|
this._authToken = null;
|
|
83
88
|
this._characterId = null;
|
|
89
|
+
this._characterVersion = null;
|
|
90
|
+
this._characterReference = null;
|
|
91
|
+
this._characterVersionManager = null;
|
|
92
|
+
this._characterVersionManagerKey = null;
|
|
84
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;
|
|
85
99
|
this._isBotReady = false;
|
|
86
100
|
this._participantSid = "";
|
|
87
101
|
this._storedConfig = null;
|
|
@@ -110,6 +124,23 @@ export class ConvaiClient extends EventEmitter {
|
|
|
110
124
|
if (config) {
|
|
111
125
|
this._storedConfig = config;
|
|
112
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
|
+
}
|
|
113
144
|
}
|
|
114
145
|
// Initialize room with no reconnect policy
|
|
115
146
|
this._room = new Room({
|
|
@@ -127,6 +158,8 @@ export class ConvaiClient extends EventEmitter {
|
|
|
127
158
|
endUserId: null,
|
|
128
159
|
endUserMetadata: null,
|
|
129
160
|
metrics: [],
|
|
161
|
+
characters: [],
|
|
162
|
+
activeMembershipId: null,
|
|
130
163
|
disconnectReason: null,
|
|
131
164
|
};
|
|
132
165
|
// Default audio settings for optimal interruption handling
|
|
@@ -169,6 +202,42 @@ export class ConvaiClient extends EventEmitter {
|
|
|
169
202
|
get characterId() {
|
|
170
203
|
return this._characterId;
|
|
171
204
|
}
|
|
205
|
+
get characterVersion() {
|
|
206
|
+
return this._characterVersion;
|
|
207
|
+
}
|
|
208
|
+
get characterReference() {
|
|
209
|
+
return this._characterReference;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Character version manager for the configured character. Built from the
|
|
213
|
+
* live connection when there is one, otherwise from the stored config, so
|
|
214
|
+
* versions can be listed and released before connecting. Null without an
|
|
215
|
+
* API key (the Character REST platform does not accept realtime auth tokens).
|
|
216
|
+
*/
|
|
217
|
+
get characterVersions() {
|
|
218
|
+
const apiKey = this._apiKey ?? this._storedConfig?.apiKey ?? null;
|
|
219
|
+
const rawCharacterId = this._characterId ?? this._storedConfig?.characterId ?? null;
|
|
220
|
+
if (!apiKey || !rawCharacterId)
|
|
221
|
+
return null;
|
|
222
|
+
let characterId = rawCharacterId;
|
|
223
|
+
try {
|
|
224
|
+
characterId = resolveCharacterReference(rawCharacterId).characterId;
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// A malformed id is reported by connect(); use it verbatim here.
|
|
228
|
+
}
|
|
229
|
+
const baseUrl = this._storedConfig?.characterApiUrl ?? undefined;
|
|
230
|
+
const key = `${apiKey}\u0000${characterId}\u0000${baseUrl ?? ""}`;
|
|
231
|
+
if (this._characterVersionManagerKey !== key) {
|
|
232
|
+
this._characterVersionManager = new CharacterVersionManager({
|
|
233
|
+
apiKey,
|
|
234
|
+
characterId,
|
|
235
|
+
baseUrl,
|
|
236
|
+
});
|
|
237
|
+
this._characterVersionManagerKey = key;
|
|
238
|
+
}
|
|
239
|
+
return this._characterVersionManager;
|
|
240
|
+
}
|
|
172
241
|
get room() {
|
|
173
242
|
return this._room;
|
|
174
243
|
}
|
|
@@ -181,6 +250,41 @@ export class ConvaiClient extends EventEmitter {
|
|
|
181
250
|
get characterSessionId() {
|
|
182
251
|
return this._characterSessionId;
|
|
183
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
|
+
}
|
|
184
288
|
get isBotReady() {
|
|
185
289
|
return this._isBotReady;
|
|
186
290
|
}
|
|
@@ -207,12 +311,60 @@ export class ConvaiClient extends EventEmitter {
|
|
|
207
311
|
*/
|
|
208
312
|
setupEventListeners() {
|
|
209
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
|
+
});
|
|
210
350
|
this._room.on(RoomEvent.Disconnected, (reason) => {
|
|
211
351
|
this._connectionStateHandler.handleDisconnected(reason);
|
|
212
352
|
});
|
|
213
353
|
this._room.on(RoomEvent.ConnectionStateChanged, this._connectionStateHandler.handleConnectionStateChanged.bind(this._connectionStateHandler));
|
|
214
354
|
// Message handler events
|
|
215
|
-
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
|
+
}
|
|
216
368
|
// Server may send multiple bot-ready frames (one per client-ready
|
|
217
369
|
// we retried). Only surface the first to consumers per connect.
|
|
218
370
|
if (this._isBotReady) {
|
|
@@ -306,6 +458,9 @@ export class ConvaiClient extends EventEmitter {
|
|
|
306
458
|
this._messageHandler.on("llmNoResponse", () => {
|
|
307
459
|
this.emit("llmNoResponse");
|
|
308
460
|
});
|
|
461
|
+
this._messageHandler.on("botTurnCompleted", (data) => {
|
|
462
|
+
this.emit("botTurnCompleted", data);
|
|
463
|
+
});
|
|
309
464
|
// Forward bot-output event (aggregated output with spoken status)
|
|
310
465
|
this._messageHandler.on("botOutput", (data) => {
|
|
311
466
|
this.emit("botOutput", data);
|
|
@@ -331,7 +486,20 @@ export class ConvaiClient extends EventEmitter {
|
|
|
331
486
|
this._messageHandler.on("actionResponse", (data) => {
|
|
332
487
|
this.emit("actionResponse", data);
|
|
333
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
|
+
});
|
|
334
501
|
this._messageHandler.on("serverResponse", (response) => {
|
|
502
|
+
this._handleRosterServerResponse(response);
|
|
335
503
|
this.emit("serverResponse", response);
|
|
336
504
|
});
|
|
337
505
|
this._messageHandler.on("interactionCreated", (data) => {
|
|
@@ -430,6 +598,8 @@ export class ConvaiClient extends EventEmitter {
|
|
|
430
598
|
this._apiKey = configWithDefaults.apiKey ?? null;
|
|
431
599
|
this._authToken = configWithDefaults.authToken ?? null;
|
|
432
600
|
this._characterId = configWithDefaults.characterId;
|
|
601
|
+
this._characterVersion = configWithDefaults.characterVersion ?? null;
|
|
602
|
+
this._characterReference = configWithDefaults.characterReference;
|
|
433
603
|
// Determine connection type based on enableVideo
|
|
434
604
|
const connType = configWithDefaults.enableVideo ? "video" : "audio";
|
|
435
605
|
this._connectionType = connType;
|
|
@@ -453,7 +623,7 @@ export class ConvaiClient extends EventEmitter {
|
|
|
453
623
|
this._sseSession = new SSESession({
|
|
454
624
|
interactionApiUrl: configWithDefaults.interactionApiUrl,
|
|
455
625
|
authorization: configWithDefaults.authToken ?? configWithDefaults.apiKey,
|
|
456
|
-
characterId: configWithDefaults.
|
|
626
|
+
characterId: configWithDefaults.characterReference,
|
|
457
627
|
getCharacterSessionId: () => this._characterSessionId,
|
|
458
628
|
takeStateOfMind: () => {
|
|
459
629
|
const stateOfMind = this._stateOfMind;
|
|
@@ -476,6 +646,14 @@ export class ConvaiClient extends EventEmitter {
|
|
|
476
646
|
enableEmotion: configWithDefaults.enableEmotion,
|
|
477
647
|
emotionConfig: configWithDefaults.emotionConfig,
|
|
478
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,
|
|
479
657
|
debug: configWithDefaults.debug,
|
|
480
658
|
onMessage: (payload) => this._messageHandler.handleDataReceivedPublic(payload),
|
|
481
659
|
onError: (error) => this.emit("error", error),
|
|
@@ -495,8 +673,11 @@ export class ConvaiClient extends EventEmitter {
|
|
|
495
673
|
}
|
|
496
674
|
// Prepare request body with required parameters
|
|
497
675
|
const characterSessionIdToSend = configWithDefaults.characterSessionId ?? this._characterSessionId;
|
|
676
|
+
const isRoster = isRosterConfig(configWithDefaults);
|
|
498
677
|
const requestBody = {
|
|
499
|
-
|
|
678
|
+
...(isRoster
|
|
679
|
+
? buildRosterConnectFields(configWithDefaults)
|
|
680
|
+
: { character_id: configWithDefaults.characterReference }),
|
|
500
681
|
...(stateOfMind ? { state_of_mind: stateOfMind } : {}),
|
|
501
682
|
...(configWithDefaults.endUserId && {
|
|
502
683
|
end_user_id: configWithDefaults.endUserId,
|
|
@@ -507,7 +688,6 @@ export class ConvaiClient extends EventEmitter {
|
|
|
507
688
|
transport: transportType,
|
|
508
689
|
connection_type: connType,
|
|
509
690
|
...buildBlendshapeConnectConfig(configWithDefaults),
|
|
510
|
-
llm_provider: "dynamic",
|
|
511
691
|
...(buildEmotionConnectConfig(configWithDefaults) && {
|
|
512
692
|
emotion_config: buildEmotionConnectConfig(configWithDefaults),
|
|
513
693
|
}),
|
|
@@ -521,7 +701,16 @@ export class ConvaiClient extends EventEmitter {
|
|
|
521
701
|
...(configWithDefaults.respondModes && {
|
|
522
702
|
respond_modes: this.serializeRespondModes(configWithDefaults.respondModes),
|
|
523
703
|
}),
|
|
524
|
-
...(
|
|
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 &&
|
|
525
714
|
characterSessionIdToSend !== "-1" && {
|
|
526
715
|
character_session_id: characterSessionIdToSend,
|
|
527
716
|
}),
|
|
@@ -543,6 +732,12 @@ export class ConvaiClient extends EventEmitter {
|
|
|
543
732
|
...(configWithDefaults.debug !== undefined && {
|
|
544
733
|
debug: configWithDefaults.debug,
|
|
545
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
|
+
}),
|
|
546
741
|
invocation_metadata: {
|
|
547
742
|
source: configWithDefaults.invocationMetadata?.source ?? "web_sdk",
|
|
548
743
|
client_version: configWithDefaults.invocationMetadata?.clientVersion ??
|
|
@@ -561,41 +756,62 @@ export class ConvaiClient extends EventEmitter {
|
|
|
561
756
|
else if (configWithDefaults.apiKey) {
|
|
562
757
|
connectHeaders["X-API-Key"] = configWithDefaults.apiKey;
|
|
563
758
|
}
|
|
564
|
-
// HTTP POST /connect — same request for both transports
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
+
}
|
|
584
790
|
}
|
|
585
|
-
|
|
586
|
-
|
|
791
|
+
catch {
|
|
792
|
+
if (errorText)
|
|
793
|
+
errorMessage = `${errorMessage}: ${errorText}`;
|
|
587
794
|
}
|
|
588
|
-
|
|
589
|
-
|
|
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);
|
|
590
807
|
}
|
|
808
|
+
throw new Error(errorMessage);
|
|
591
809
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
}
|
|
598
|
-
const connectionData = await response.json();
|
|
810
|
+
return await response.json();
|
|
811
|
+
};
|
|
812
|
+
const connectionData = isRoster
|
|
813
|
+
? await this._withProvisioningRetry(configWithDefaults, connectOnce)
|
|
814
|
+
: await connectOnce();
|
|
599
815
|
return await this.consumeConnectionData(connectionData, configWithDefaults);
|
|
600
816
|
}
|
|
601
817
|
catch (error) {
|
|
@@ -659,6 +875,270 @@ export class ConvaiClient extends EventEmitter {
|
|
|
659
875
|
throw error;
|
|
660
876
|
}
|
|
661
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
|
+
}
|
|
662
1142
|
/**
|
|
663
1143
|
* Add default URL, apply RTVI-logging config, and validate that either an
|
|
664
1144
|
* API key or auth token plus a character ID are present.
|
|
@@ -683,10 +1163,37 @@ export class ConvaiClient extends EventEmitter {
|
|
|
683
1163
|
const hasApiKey = Boolean(configWithDefaults.apiKey);
|
|
684
1164
|
const hasAuthToken = Boolean(configWithDefaults.authToken);
|
|
685
1165
|
const missingCredential = requireCredential && !hasApiKey && !hasAuthToken;
|
|
686
|
-
|
|
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)) {
|
|
687
1171
|
throw new Error("Either apiKey or authToken is required, and characterId is required");
|
|
688
1172
|
}
|
|
689
|
-
|
|
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
|
+
}
|
|
1187
|
+
// Split the character id from its version selector. The bare UUID names
|
|
1188
|
+
// the character everywhere inside the SDK (memory, character info); the
|
|
1189
|
+
// joined reference is what the runtime receives.
|
|
1190
|
+
const reference = resolveCharacterReference(configWithDefaults.characterId, configWithDefaults.characterVersion);
|
|
1191
|
+
return {
|
|
1192
|
+
...configWithDefaults,
|
|
1193
|
+
characterId: reference.characterId,
|
|
1194
|
+
characterVersion: reference.version,
|
|
1195
|
+
characterReference: reference.reference,
|
|
1196
|
+
};
|
|
690
1197
|
}
|
|
691
1198
|
/**
|
|
692
1199
|
* Consume a /connect response and bring up the transport.
|
|
@@ -718,6 +1225,8 @@ export class ConvaiClient extends EventEmitter {
|
|
|
718
1225
|
this._apiKey = configWithDefaults.apiKey ?? null;
|
|
719
1226
|
this._authToken = configWithDefaults.authToken ?? null;
|
|
720
1227
|
this._characterId = configWithDefaults.characterId;
|
|
1228
|
+
this._characterVersion = configWithDefaults.characterVersion ?? null;
|
|
1229
|
+
this._characterReference = configWithDefaults.characterReference;
|
|
721
1230
|
const connType = configWithDefaults.enableVideo ? "video" : "audio";
|
|
722
1231
|
this._connectionType = connType;
|
|
723
1232
|
const transportType = configWithDefaults.transport ?? "livekit";
|
|
@@ -735,6 +1244,14 @@ export class ConvaiClient extends EventEmitter {
|
|
|
735
1244
|
characterSessionId: connectionData.character_session_id,
|
|
736
1245
|
};
|
|
737
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
|
+
}
|
|
738
1255
|
if (connectionData.end_user_id) {
|
|
739
1256
|
this._endUserId = connectionData.end_user_id;
|
|
740
1257
|
}
|
|
@@ -972,6 +1489,10 @@ export class ConvaiClient extends EventEmitter {
|
|
|
972
1489
|
*/
|
|
973
1490
|
async disconnect() {
|
|
974
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");
|
|
975
1496
|
if (this._activeTransport === "sse" && this._sseSession) {
|
|
976
1497
|
await this._sseSession.disconnect();
|
|
977
1498
|
this._connectionStateHandler.resetConnectionState();
|
|
@@ -1004,6 +1525,15 @@ export class ConvaiClient extends EventEmitter {
|
|
|
1004
1525
|
this.clearAllManagers();
|
|
1005
1526
|
}
|
|
1006
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
|
+
}
|
|
1007
1537
|
}
|
|
1008
1538
|
/**
|
|
1009
1539
|
* Clear all manager states
|
|
@@ -1012,10 +1542,14 @@ export class ConvaiClient extends EventEmitter {
|
|
|
1012
1542
|
this._connectionType = null;
|
|
1013
1543
|
this._activeTransport = null;
|
|
1014
1544
|
this._wsSession = null;
|
|
1545
|
+
this._failPendingRosterCommands("Disconnected before the roster command was acknowledged; its outcome is unknown");
|
|
1546
|
+
this._roster.reset();
|
|
1015
1547
|
this._sseSession = null;
|
|
1016
1548
|
this._apiKey = null;
|
|
1017
1549
|
this._authToken = null;
|
|
1018
1550
|
this._characterId = null;
|
|
1551
|
+
this._characterVersion = null;
|
|
1552
|
+
this._characterReference = null;
|
|
1019
1553
|
this._endUserId = null;
|
|
1020
1554
|
this._endUserMetadata = null;
|
|
1021
1555
|
this._memoryManager = null; // Clear memory manager on disconnect
|
|
@@ -1038,6 +1572,13 @@ export class ConvaiClient extends EventEmitter {
|
|
|
1038
1572
|
* Reset the session ID to start a new conversation
|
|
1039
1573
|
*/
|
|
1040
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
|
+
}
|
|
1041
1582
|
this._characterSessionId = "-1";
|
|
1042
1583
|
this.emit("characterSessionId", "-1");
|
|
1043
1584
|
this._messageHandler.reset();
|
|
@@ -1079,15 +1620,28 @@ export class ConvaiClient extends EventEmitter {
|
|
|
1079
1620
|
const stateOfMind = normalizeStateOfMind(options.stateOfMind) ?? null;
|
|
1080
1621
|
if (stateOfMind)
|
|
1081
1622
|
this._stateOfMind = stateOfMind;
|
|
1082
|
-
if (
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
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);
|
|
1091
1645
|
}
|
|
1092
1646
|
else {
|
|
1093
1647
|
if (stateOfMind) {
|
|
@@ -1203,8 +1757,16 @@ export class ConvaiClient extends EventEmitter {
|
|
|
1203
1757
|
updateDynamicInfo(dynamicInfo) {
|
|
1204
1758
|
if (!this.isTransportReady() || !dynamicInfo?.trim())
|
|
1205
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
|
+
}
|
|
1206
1768
|
this.publishMessage("update-dynamic-info", {
|
|
1207
|
-
dynamic_info: { text:
|
|
1769
|
+
dynamic_info: { text: nextDynamicInfo },
|
|
1208
1770
|
participant_sid: this._participantSid || this._room.localParticipant?.sid,
|
|
1209
1771
|
});
|
|
1210
1772
|
}
|
|
@@ -1252,6 +1814,22 @@ export class ConvaiClient extends EventEmitter {
|
|
|
1252
1814
|
if (shouldPreemptLipsync) {
|
|
1253
1815
|
this.startTrackedConversation(`[context-update:${options.mode ?? "append"}]`);
|
|
1254
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
|
+
}
|
|
1255
1833
|
this.publishMessage("context-update", {
|
|
1256
1834
|
...(hasText && { text: options.text.trim() }),
|
|
1257
1835
|
...(options.mode && { mode: options.mode }),
|
|
@@ -1313,6 +1891,8 @@ export class ConvaiClient extends EventEmitter {
|
|
|
1313
1891
|
}
|
|
1314
1892
|
if (!this.isTransportReady())
|
|
1315
1893
|
return;
|
|
1894
|
+
if (this._activeTransport === "sse")
|
|
1895
|
+
return;
|
|
1316
1896
|
this.publishMessage("update-emotion", {
|
|
1317
1897
|
state_of_mind: normalized,
|
|
1318
1898
|
participant_sid: this._participantSid || this._room.localParticipant?.sid,
|
|
@@ -1407,4 +1987,17 @@ export class ConvaiClient extends EventEmitter {
|
|
|
1407
1987
|
}
|
|
1408
1988
|
ConvaiClient.CLIENT_READY_RETRY_MS = 500;
|
|
1409
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;
|
|
1410
2003
|
//# sourceMappingURL=ConvaiClient.js.map
|