@convai/web-sdk 1.8.0-beta.6 → 1.8.0-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +303 -1
- package/dist/core/ConvaiClient.d.ts +59 -2
- package/dist/core/ConvaiClient.d.ts.map +1 -1
- package/dist/core/ConvaiClient.js +457 -6
- package/dist/core/ConvaiClient.js.map +1 -1
- package/dist/core/MessageHandler.d.ts +11 -0
- package/dist/core/MessageHandler.d.ts.map +1 -1
- package/dist/core/MessageHandler.js +325 -30
- package/dist/core/MessageHandler.js.map +1 -1
- package/dist/core/SSESession.js +2 -2
- package/dist/core/SSESession.js.map +1 -1
- package/dist/core/connectRequest.d.ts +38 -3
- package/dist/core/connectRequest.d.ts.map +1 -1
- package/dist/core/connectRequest.js +57 -11
- package/dist/core/connectRequest.js.map +1 -1
- package/dist/core/index.d.ts +5 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +8 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/logicalTurn.d.ts +4 -1
- package/dist/core/logicalTurn.d.ts.map +1 -1
- package/dist/core/logicalTurn.js +35 -4
- package/dist/core/logicalTurn.js.map +1 -1
- package/dist/core/modelOutput.d.ts +40 -0
- package/dist/core/modelOutput.d.ts.map +1 -0
- package/dist/core/modelOutput.js +187 -0
- package/dist/core/modelOutput.js.map +1 -0
- package/dist/core/publishedChat.d.ts +9 -0
- package/dist/core/publishedChat.d.ts.map +1 -0
- package/dist/core/publishedChat.js +13 -0
- package/dist/core/publishedChat.js.map +1 -0
- package/dist/core/types.d.ts +225 -25
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/embed/ConvaiChatElement.d.ts +103 -0
- package/dist/embed/ConvaiChatElement.d.ts.map +1 -0
- package/dist/embed/ConvaiChatElement.js +679 -0
- package/dist/embed/ConvaiChatElement.js.map +1 -0
- package/dist/embed/browser.d.ts +3 -0
- package/dist/embed/browser.d.ts.map +1 -0
- package/dist/embed/browser.js +5 -0
- package/dist/embed/browser.js.map +1 -0
- package/dist/embed/chat-embed-v1.js +200 -0
- package/dist/embed/chat-embed-v1.js.map +7 -0
- package/dist/embed/index.d.ts +9 -0
- package/dist/embed/index.d.ts.map +1 -0
- package/dist/embed/index.js +9 -0
- package/dist/embed/index.js.map +1 -0
- package/dist/embed/publicationApi.d.ts +23 -0
- package/dist/embed/publicationApi.d.ts.map +1 -0
- package/dist/embed/publicationApi.js +141 -0
- package/dist/embed/publicationApi.js.map +1 -0
- package/dist/react/components/ConvaiWidget.d.ts +11 -1
- package/dist/react/components/ConvaiWidget.d.ts.map +1 -1
- package/dist/react/components/ConvaiWidget.js +67 -11
- package/dist/react/components/ConvaiWidget.js.map +1 -1
- package/dist/react/components/rtc-widget/components/MarkdownRenderer.d.ts +7 -0
- package/dist/react/components/rtc-widget/components/MarkdownRenderer.d.ts.map +1 -1
- package/dist/react/components/rtc-widget/components/MarkdownRenderer.js +111 -17
- package/dist/react/components/rtc-widget/components/MarkdownRenderer.js.map +1 -1
- package/dist/react/components/rtc-widget/components/UserMessage.js +1 -1
- package/dist/react/components/rtc-widget/components/UserMessage.js.map +1 -1
- package/dist/react/components/rtc-widget/components/conviComponents/SettingsTray.d.ts +1 -1
- package/dist/react/components/rtc-widget/components/conviComponents/SettingsTray.d.ts.map +1 -1
- package/dist/react/components/rtc-widget/components/conviComponents/SettingsTray.js +2 -2
- package/dist/react/components/rtc-widget/components/conviComponents/SettingsTray.js.map +1 -1
- package/dist/react/hooks/useConvaiClient.d.ts +2 -0
- package/dist/react/hooks/useConvaiClient.d.ts.map +1 -1
- package/dist/react/hooks/useConvaiClient.js +8 -0
- package/dist/react/hooks/useConvaiClient.js.map +1 -1
- package/dist/react/index.d.ts +4 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +8 -1
- package/dist/react/index.js.map +1 -1
- package/dist/utils/inlineMarkdown.d.ts +22 -0
- package/dist/utils/inlineMarkdown.d.ts.map +1 -1
- package/dist/utils/inlineMarkdown.js +84 -7
- package/dist/utils/inlineMarkdown.js.map +1 -1
- package/dist/vanilla/ConvaiWidget.d.ts.map +1 -1
- package/dist/vanilla/ConvaiWidget.js +290 -43
- package/dist/vanilla/ConvaiWidget.js.map +1 -1
- package/dist/vanilla/WidgetEventSubscriptions.d.ts +15 -0
- package/dist/vanilla/WidgetEventSubscriptions.d.ts.map +1 -0
- package/dist/vanilla/WidgetEventSubscriptions.js +37 -0
- package/dist/vanilla/WidgetEventSubscriptions.js.map +1 -0
- package/dist/vanilla/index.d.ts +3 -1
- package/dist/vanilla/index.d.ts.map +1 -1
- package/dist/vanilla/index.js +3 -1
- package/dist/vanilla/index.js.map +1 -1
- package/dist/vanilla/styles.d.ts.map +1 -1
- package/dist/vanilla/styles.js +12 -0
- package/dist/vanilla/styles.js.map +1 -1
- package/dist/vanilla/types.d.ts +8 -1
- package/dist/vanilla/types.d.ts.map +1 -1
- package/dist/vanilla/types.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +48 -13
|
@@ -18,6 +18,9 @@ export declare class MessageHandler extends EventEmitter {
|
|
|
18
18
|
private totalBlendshapesReceived;
|
|
19
19
|
private blendshapeQueue;
|
|
20
20
|
private _currentEmotion;
|
|
21
|
+
private modelOutputVersion;
|
|
22
|
+
private seenModelOutputPayloads;
|
|
23
|
+
private seenModelOutputPayloadCharacters;
|
|
21
24
|
private seenBotOutputSegments;
|
|
22
25
|
private readonly _boundHandleDataReceived;
|
|
23
26
|
constructor(room?: Room);
|
|
@@ -29,6 +32,8 @@ export declare class MessageHandler extends EventEmitter {
|
|
|
29
32
|
* Enable or disable console logging for decoded incoming RTVI messages.
|
|
30
33
|
*/
|
|
31
34
|
setRtviMessageLogging(enabled: boolean): void;
|
|
35
|
+
/** Select the server-negotiated canonical output version for this session. */
|
|
36
|
+
setModelOutputVersion(version: 1 | 2): void;
|
|
32
37
|
/**
|
|
33
38
|
* Get current chat messages
|
|
34
39
|
*/
|
|
@@ -80,12 +85,18 @@ export declare class MessageHandler extends EventEmitter {
|
|
|
80
85
|
*/
|
|
81
86
|
private handleDataReceived;
|
|
82
87
|
private _handleActionResponse;
|
|
88
|
+
private _handleModelOutput;
|
|
89
|
+
private _clearModelOutputReplayLedger;
|
|
90
|
+
private _handleBotLlmTextRaw;
|
|
83
91
|
private lifecycleOwner;
|
|
84
92
|
private numberOrUndefined;
|
|
93
|
+
private transcriptionLogicalTurnId;
|
|
85
94
|
/**
|
|
86
95
|
* Public entry point for WebSocket transport to inject messages into the same pipeline.
|
|
87
96
|
*/
|
|
88
97
|
handleDataReceivedPublic(payload: Uint8Array): void;
|
|
98
|
+
/** Append a submitted text message without waiting for a server echo. */
|
|
99
|
+
addUserTextMessage(text: string): void;
|
|
89
100
|
/**
|
|
90
101
|
* Insert a message into the transcript locally. Nothing is transmitted and
|
|
91
102
|
* no LLM turn is billed -- this is for client-side lines (a greeting, a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MessageHandler.d.ts","sourceRoot":"","sources":["../../src/core/MessageHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAa,MAAM,gBAAgB,CAAC;AACjD,OAAO,EACL,WAAW,
|
|
1
|
+
{"version":3,"file":"MessageHandler.d.ts","sourceRoot":"","sources":["../../src/core/MessageHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAa,MAAM,gBAAgB,CAAC;AACjD,OAAO,EACL,WAAW,EAQZ,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAM9C,OAAO,EACL,eAAe,EAEhB,MAAM,mBAAmB,CAAC;AA8B3B;;;GAGG;AACH,qBAAa,cAAe,SAAQ,YAAY;IAC9C,OAAO,CAAC,IAAI,CAAqB;IACjC,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,qBAAqB,CAAuB;IACpD,OAAO,CAAC,iBAAiB,CAAc;IACvC,OAAO,CAAC,eAAe,CAAiB;IACxC,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,UAAU,CAAkB;IACpC,OAAO,CAAC,WAAW,CAAkB;IACrC,OAAO,CAAC,wBAAwB,CAAa;IAC7C,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,eAAe,CAAoD;IAC3E,OAAO,CAAC,kBAAkB,CAAY;IACtC,OAAO,CAAC,uBAAuB,CAAqB;IACpD,OAAO,CAAC,gCAAgC,CAAK;IAI7C,OAAO,CAAC,qBAAqB,CAA8B;IAK3D,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAG/B;gBAEE,IAAI,CAAC,EAAE,IAAI;IASvB;;OAEG;IACH,kBAAkB,IAAI,eAAe;IAIrC;;OAEG;IACH,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAI7C,8EAA8E;IAC9E,qBAAqB,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,GAAG,IAAI;IAK3C;;OAEG;IACH,eAAe,IAAI,WAAW,EAAE;IAIhC;;OAEG;IACH,oBAAoB,IAAI,MAAM;IAI9B;;OAEG;IACH,kBAAkB,IAAI,OAAO;IAI7B;;OAEG;IACH,aAAa,IAAI,OAAO;IAIxB;;OAEG;IACH,UAAU,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAIxD;;;OAGG;IACH,cAAc,IAAI,OAAO;IAIzB;;;;OAIG;IACH,wBAAwB,IAAI,IAAI;IAShC;;OAEG;IACH,OAAO,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IAUzB;;OAEG;IACH,OAAO,CAAC,cAAc;IAMtB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAMxB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAk1B1B,OAAO,CAAC,qBAAqB;IAsB7B,OAAO,CAAC,kBAAkB;IAuC1B,OAAO,CAAC,6BAA6B;IAKrC,OAAO,CAAC,oBAAoB;IAwB5B,OAAO,CAAC,cAAc;IAkBtB,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,0BAA0B;IAMlC;;OAEG;IACH,wBAAwB,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI;IAInD,yEAAyE;IACzE,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAYtC;;;;;;;OAOG;IACH,kBAAkB,CAAC,OAAO,EAAE;QAC1B,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC;QACvB,OAAO,EAAE,MAAM,CAAC;QAChB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,GAAG,WAAW;IAcf;;OAEG;IACH,KAAK,IAAI,IAAI;CAmBd"}
|
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import { RoomEvent } from "livekit-client";
|
|
2
2
|
import { EventEmitter } from "./EventEmitter.js";
|
|
3
|
+
import { extractModelOutputPayload, parseModelOutputMessage, } from "./modelOutput.js";
|
|
4
|
+
import { isValidLogicalTurnId, isValidOutputId } from "./logicalTurn.js";
|
|
3
5
|
import { BlendshapeQueue, } from "./BlendshapeQueue.js";
|
|
6
|
+
function stableJsonFingerprint(value) {
|
|
7
|
+
if (Array.isArray(value)) {
|
|
8
|
+
return `[${value.map(stableJsonFingerprint).join(",")}]`;
|
|
9
|
+
}
|
|
10
|
+
if (value && typeof value === "object") {
|
|
11
|
+
const entries = Object.entries(value)
|
|
12
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
13
|
+
.map(([key, item]) => `${JSON.stringify(key)}:${stableJsonFingerprint(item)}`);
|
|
14
|
+
return `{${entries.join(",")}}`;
|
|
15
|
+
}
|
|
16
|
+
return JSON.stringify(value);
|
|
17
|
+
}
|
|
18
|
+
const MAX_MODEL_OUTPUT_REPLAY_FINGERPRINTS = 400;
|
|
19
|
+
// Preserve exact replay matching instead of accepting hash collisions, while
|
|
20
|
+
// bounding the aggregate strings retained by a long-lived v2 session.
|
|
21
|
+
const MAX_MODEL_OUTPUT_REPLAY_RETAINED_CHARS = 4 * 1024 * 1024;
|
|
4
22
|
/**
|
|
5
23
|
* Handles incoming messages from Convai
|
|
6
24
|
* Processes various message types from the AI assistant and updates chat history
|
|
@@ -18,6 +36,9 @@ export class MessageHandler extends EventEmitter {
|
|
|
18
36
|
this.isListening = false;
|
|
19
37
|
this.totalBlendshapesReceived = 0;
|
|
20
38
|
this._currentEmotion = null;
|
|
39
|
+
this.modelOutputVersion = 1;
|
|
40
|
+
this.seenModelOutputPayloads = new Set();
|
|
41
|
+
this.seenModelOutputPayloadCharacters = 0;
|
|
21
42
|
// bot-output segments already added to the chat. The server re-sends the
|
|
22
43
|
// same segment (full text) as TTS progress advances (spoken_status
|
|
23
44
|
// in-progress → completed); only the first sighting may touch the chat.
|
|
@@ -40,6 +61,11 @@ export class MessageHandler extends EventEmitter {
|
|
|
40
61
|
setRtviMessageLogging(enabled) {
|
|
41
62
|
this.logRtviMessages = enabled;
|
|
42
63
|
}
|
|
64
|
+
/** Select the server-negotiated canonical output version for this session. */
|
|
65
|
+
setModelOutputVersion(version) {
|
|
66
|
+
this.modelOutputVersion = version;
|
|
67
|
+
this._clearModelOutputReplayLedger();
|
|
68
|
+
}
|
|
43
69
|
/**
|
|
44
70
|
* Get current chat messages
|
|
45
71
|
*/
|
|
@@ -130,6 +156,13 @@ export class MessageHandler extends EventEmitter {
|
|
|
130
156
|
// Parse JSON
|
|
131
157
|
const messageData = JSON.parse(messageString);
|
|
132
158
|
parsed = true;
|
|
159
|
+
// Canonical output has two transport envelopes. Normalize it before the
|
|
160
|
+
// legacy message switch so both LiveKit and WebSocket execute one path.
|
|
161
|
+
const modelOutputPayload = extractModelOutputPayload(messageData);
|
|
162
|
+
if (modelOutputPayload !== undefined) {
|
|
163
|
+
this._handleModelOutput(modelOutputPayload);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
133
166
|
// Extract and categorize messages for chat display
|
|
134
167
|
const timestamp = new Date().toISOString();
|
|
135
168
|
const messageId = `${messageData.type}-${Date.now()}-${Math.random()}`;
|
|
@@ -152,7 +185,8 @@ export class MessageHandler extends EventEmitter {
|
|
|
152
185
|
const sameSpeaker = (msg) => !!msg && msg.membershipId === attribution.membershipId;
|
|
153
186
|
// Handle different message types
|
|
154
187
|
switch (messageData.type) {
|
|
155
|
-
//
|
|
188
|
+
// Submitted text is appended locally by sendUserTextMessage. Ignore
|
|
189
|
+
// server echoes so each user message appears exactly once.
|
|
156
190
|
case "user_text_message":
|
|
157
191
|
case "user-llm-text":
|
|
158
192
|
case "text-input":
|
|
@@ -161,7 +195,7 @@ export class MessageHandler extends EventEmitter {
|
|
|
161
195
|
case "chat-message":
|
|
162
196
|
case "input-text":
|
|
163
197
|
case "message":
|
|
164
|
-
// Skip
|
|
198
|
+
// Skip server echoes; local text and voice transcription are handled separately.
|
|
165
199
|
break;
|
|
166
200
|
// Bot Ready - Bot is ready, connection should be true
|
|
167
201
|
case "bot-ready":
|
|
@@ -177,7 +211,19 @@ export class MessageHandler extends EventEmitter {
|
|
|
177
211
|
this.isBotResponding = true;
|
|
178
212
|
this.emit("botRespondingChange", true);
|
|
179
213
|
this.seenBotOutputSegments.clear();
|
|
180
|
-
//
|
|
214
|
+
// Lifecycle frames do not carry output_id. In v2, keep at most one
|
|
215
|
+
// unowned placeholder so concurrent starts cannot create anonymous
|
|
216
|
+
// rows that later get adopted in reverse order. Invocation-correlated
|
|
217
|
+
// chunks establish additional rows in authoritative arrival order.
|
|
218
|
+
const hasUnownedV2Starter = this.modelOutputVersion === 2 &&
|
|
219
|
+
this.chatMessages.some((message) => message.type === "bot-llm-text" &&
|
|
220
|
+
message.isStreaming &&
|
|
221
|
+
message.content === "" &&
|
|
222
|
+
message.outputId === undefined &&
|
|
223
|
+
message.logicalTurnId === undefined);
|
|
224
|
+
if (hasUnownedV2Starter)
|
|
225
|
+
break;
|
|
226
|
+
// Add initial streaming message to chat.
|
|
181
227
|
const streamingMessage = {
|
|
182
228
|
id: messageId,
|
|
183
229
|
type: "bot-llm-text",
|
|
@@ -253,53 +299,189 @@ export class MessageHandler extends EventEmitter {
|
|
|
253
299
|
case "bot-llm-text":
|
|
254
300
|
if (messageData.data?.text) {
|
|
255
301
|
const newChunk = messageData.data.text;
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
302
|
+
// Core adds this correlation metadata only for negotiated v2.
|
|
303
|
+
// Legacy frames retain their exact historical shape and behavior.
|
|
304
|
+
const outputId = this.modelOutputVersion === 2 &&
|
|
305
|
+
isValidOutputId(messageData.data.output_id)
|
|
306
|
+
? messageData.data.output_id
|
|
307
|
+
: undefined;
|
|
308
|
+
const logicalTurnId = this.modelOutputVersion === 2 &&
|
|
309
|
+
isValidLogicalTurnId(messageData.data.logical_turn_id)
|
|
310
|
+
? messageData.data.logical_turn_id
|
|
311
|
+
: undefined;
|
|
312
|
+
// A logical turn can contain multiple provider invocations (for
|
|
313
|
+
// example, the response before a tool call and the continuation
|
|
314
|
+
// after its result). Prefer the invocation id, then fall back to
|
|
315
|
+
// the older v2 logical-turn id when Core does not send one.
|
|
316
|
+
let targetMessageIndex = -1;
|
|
317
|
+
if (outputId !== undefined) {
|
|
318
|
+
for (let index = this.chatMessages.length - 1; index >= 0; index--) {
|
|
319
|
+
const message = this.chatMessages[index];
|
|
320
|
+
if (message.type === "bot-llm-text" &&
|
|
321
|
+
message.isStreaming &&
|
|
322
|
+
message.outputId === outputId &&
|
|
323
|
+
sameSpeaker(message)) {
|
|
324
|
+
targetMessageIndex = index;
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// bot-llm-started has no correlation metadata. Let one empty,
|
|
329
|
+
// unowned row adopt the first authoritative invocation id.
|
|
330
|
+
if (targetMessageIndex === -1) {
|
|
331
|
+
for (let index = this.chatMessages.length - 1; index >= 0; index--) {
|
|
332
|
+
const message = this.chatMessages[index];
|
|
333
|
+
if (message.type === "bot-llm-text" &&
|
|
334
|
+
message.isStreaming &&
|
|
335
|
+
message.content === "" &&
|
|
336
|
+
message.outputId === undefined &&
|
|
337
|
+
message.logicalTurnId === undefined &&
|
|
338
|
+
sameSpeaker(message)) {
|
|
339
|
+
targetMessageIndex = index;
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
else if (logicalTurnId !== undefined) {
|
|
346
|
+
for (let index = this.chatMessages.length - 1; index >= 0; index--) {
|
|
347
|
+
const message = this.chatMessages[index];
|
|
348
|
+
if (message.type === "bot-llm-text" &&
|
|
349
|
+
message.isStreaming &&
|
|
350
|
+
message.outputId === undefined &&
|
|
351
|
+
message.logicalTurnId === logicalTurnId &&
|
|
352
|
+
sameSpeaker(message)) {
|
|
353
|
+
targetMessageIndex = index;
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// Older v2 Core still needs the starter-row behavior that was
|
|
358
|
+
// introduced with logical-turn correlation.
|
|
359
|
+
if (targetMessageIndex === -1) {
|
|
360
|
+
const lastIndex = this.chatMessages.length - 1;
|
|
361
|
+
const lastMessage = this.chatMessages[lastIndex];
|
|
362
|
+
if (lastMessage?.type === "bot-llm-text" &&
|
|
363
|
+
lastMessage.isStreaming &&
|
|
364
|
+
lastMessage.content === "" &&
|
|
365
|
+
lastMessage.outputId === undefined &&
|
|
366
|
+
lastMessage.logicalTurnId === undefined &&
|
|
367
|
+
sameSpeaker(lastMessage)) {
|
|
368
|
+
targetMessageIndex = lastIndex;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
// Uncorrelated (v1) chunk: it belongs to the bubble bot-llm-started
|
|
374
|
+
// opened for this speaker. Only when no such bubble exists does the
|
|
375
|
+
// historical "last streaming row" rule apply -- and still only for
|
|
376
|
+
// the same speaker, so a target switch mid-turn cannot splice one
|
|
377
|
+
// member's text onto another's row.
|
|
378
|
+
// Ownership applies among uncorrelated rows only: an uncorrelated
|
|
379
|
+
// chunk must never join a row that carries an output or turn id,
|
|
380
|
+
// or a v2 correlated stream would absorb legacy text.
|
|
381
|
+
if (this.activeBotLlmMessageId) {
|
|
382
|
+
targetMessageIndex = this.chatMessages.findIndex((message) => message.id === this.activeBotLlmMessageId &&
|
|
383
|
+
message.type === "bot-llm-text" &&
|
|
384
|
+
message.isStreaming &&
|
|
385
|
+
!message.outputId &&
|
|
386
|
+
!message.logicalTurnId &&
|
|
387
|
+
sameSpeaker(message));
|
|
388
|
+
}
|
|
389
|
+
if (targetMessageIndex === -1) {
|
|
390
|
+
const lastIndex = this.chatMessages.length - 1;
|
|
391
|
+
const lastMessage = this.chatMessages[lastIndex];
|
|
392
|
+
if (lastMessage &&
|
|
393
|
+
lastMessage.type === "bot-llm-text" &&
|
|
394
|
+
lastMessage.isStreaming &&
|
|
395
|
+
!lastMessage.outputId &&
|
|
396
|
+
!lastMessage.logicalTurnId &&
|
|
397
|
+
sameSpeaker(lastMessage)) {
|
|
398
|
+
targetMessageIndex = lastIndex;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
let updatedMessage;
|
|
403
|
+
if (targetMessageIndex !== -1) {
|
|
404
|
+
const targetMessage = this.chatMessages[targetMessageIndex];
|
|
405
|
+
updatedMessage = {
|
|
406
|
+
...targetMessage,
|
|
407
|
+
content: targetMessage.content + newChunk,
|
|
408
|
+
...(outputId !== undefined &&
|
|
409
|
+
targetMessage.outputId === undefined
|
|
410
|
+
? { outputId }
|
|
411
|
+
: {}),
|
|
412
|
+
...(logicalTurnId !== undefined &&
|
|
413
|
+
targetMessage.logicalTurnId === undefined
|
|
414
|
+
? { logicalTurnId }
|
|
415
|
+
: {}),
|
|
268
416
|
};
|
|
417
|
+
this.chatMessages[targetMessageIndex] = updatedMessage;
|
|
269
418
|
}
|
|
270
419
|
else {
|
|
271
420
|
// If no streaming message exists yet, create one
|
|
272
|
-
|
|
421
|
+
updatedMessage = {
|
|
273
422
|
id: `${messageData.type}-${Date.now()}-${Math.random()}`,
|
|
274
423
|
type: "bot-llm-text",
|
|
275
424
|
content: newChunk,
|
|
276
425
|
timestamp: new Date().toISOString(),
|
|
277
426
|
isStreaming: true,
|
|
427
|
+
...(outputId ? { outputId } : {}),
|
|
428
|
+
...(logicalTurnId ? { logicalTurnId } : {}),
|
|
278
429
|
...attribution,
|
|
279
430
|
};
|
|
280
|
-
this.activeBotLlmMessageId =
|
|
281
|
-
this.chatMessages.push(
|
|
431
|
+
this.activeBotLlmMessageId = updatedMessage.id;
|
|
432
|
+
this.chatMessages.push(updatedMessage);
|
|
282
433
|
}
|
|
283
|
-
this.emit("messagesChange", this.chatMessages);
|
|
434
|
+
this.emit("messagesChange", this.chatMessages, updatedMessage);
|
|
284
435
|
}
|
|
285
436
|
break;
|
|
437
|
+
// Parallel diagnostics-only provider output. This must never affect
|
|
438
|
+
// normal chat, speech, TTS, or response lifecycle state.
|
|
439
|
+
case "bot-llm-text-raw":
|
|
440
|
+
this._handleBotLlmTextRaw(messageData.data);
|
|
441
|
+
break;
|
|
286
442
|
// Bot LLM Stopped - Complete streaming response
|
|
287
|
-
case "bot-llm-stopped":
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
443
|
+
case "bot-llm-stopped": {
|
|
444
|
+
// A v2 response can have interleaved correlated rows. The stop event
|
|
445
|
+
// is response-scoped, so none may remain open for later chunks.
|
|
446
|
+
const finalizedMessages = [];
|
|
447
|
+
if (this.modelOutputVersion === 2) {
|
|
448
|
+
for (let index = 0; index < this.chatMessages.length; index++) {
|
|
449
|
+
const message = this.chatMessages[index];
|
|
450
|
+
if ((message.type === "bot-llm-text" ||
|
|
451
|
+
message.type === "bot-output") &&
|
|
452
|
+
message.isStreaming) {
|
|
453
|
+
const finalizedMessage = {
|
|
454
|
+
...message,
|
|
455
|
+
isStreaming: false,
|
|
456
|
+
};
|
|
457
|
+
this.chatMessages[index] = finalizedMessage;
|
|
458
|
+
finalizedMessages.push(finalizedMessage);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
// v1: a stop frame finalizes the bubble this turn owns; only when no
|
|
464
|
+
// bubble was opened does the historical last-row rule apply.
|
|
465
|
+
let stopIndex = this.activeBotLlmMessageId
|
|
466
|
+
? this.chatMessages.findIndex((message) => message.id === this.activeBotLlmMessageId)
|
|
467
|
+
: -1;
|
|
468
|
+
if (stopIndex === -1)
|
|
469
|
+
stopIndex = this.chatMessages.length - 1;
|
|
470
|
+
const lastBotMsg = this.chatMessages[stopIndex];
|
|
471
|
+
if (lastBotMsg && lastBotMsg.isStreaming) {
|
|
472
|
+
const finalizedMessage = { ...lastBotMsg, isStreaming: false };
|
|
473
|
+
this.chatMessages[stopIndex] = finalizedMessage;
|
|
474
|
+
finalizedMessages.push(finalizedMessage);
|
|
475
|
+
}
|
|
297
476
|
}
|
|
298
477
|
// Clear streaming state
|
|
299
478
|
this.isBotResponding = false;
|
|
300
479
|
this.emit("botRespondingChange", false);
|
|
301
|
-
|
|
480
|
+
// Keep the full-list event at one emission per stop while carrying
|
|
481
|
+
// every v2 terminal delta for singular-message consumers.
|
|
482
|
+
this.emit("messagesChange", this.chatMessages, undefined, finalizedMessages);
|
|
302
483
|
break;
|
|
484
|
+
}
|
|
303
485
|
// Text-only Interactions API terminal marker. This is deliberately a
|
|
304
486
|
// separate event: a completed REST turn must not be represented as a
|
|
305
487
|
// synthetic bot-llm-stopped message.
|
|
@@ -323,8 +505,17 @@ export class MessageHandler extends EventEmitter {
|
|
|
323
505
|
if (messageData.data?.text) {
|
|
324
506
|
const messageContent = messageData.data.text;
|
|
325
507
|
if (messageData.data?.final) {
|
|
508
|
+
const logicalTurnId = this.transcriptionLogicalTurnId(messageData.data.logical_turn_id);
|
|
326
509
|
// Final transcription - accumulate in single bubble until bot responds
|
|
327
510
|
const lastUserMsg = this.chatMessages[this.chatMessages.length - 1];
|
|
511
|
+
// A final transcription identical to the line appended locally on send is
|
|
512
|
+
// the server echoing that send back: drop it, or the user sees it twice.
|
|
513
|
+
if (lastUserMsg?.type === "user-llm-text" &&
|
|
514
|
+
lastUserMsg.content.trim() === String(messageContent).trim()) {
|
|
515
|
+
this.userTranscription = "";
|
|
516
|
+
this.emit("userTranscriptionChange", "");
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
328
519
|
// Check if last message is an accumulating user message (isStreaming=true)
|
|
329
520
|
if (lastUserMsg &&
|
|
330
521
|
lastUserMsg.type === "user-transcription" &&
|
|
@@ -333,6 +524,7 @@ export class MessageHandler extends EventEmitter {
|
|
|
333
524
|
const accumulatedContent = lastUserMsg.content.trim() + " " + messageContent.trim();
|
|
334
525
|
this.chatMessages[this.chatMessages.length - 1] = {
|
|
335
526
|
...lastUserMsg,
|
|
527
|
+
...(logicalTurnId === undefined ? {} : { logicalTurnId }),
|
|
336
528
|
content: accumulatedContent,
|
|
337
529
|
timestamp: timestamp,
|
|
338
530
|
};
|
|
@@ -345,6 +537,7 @@ export class MessageHandler extends EventEmitter {
|
|
|
345
537
|
content: messageContent,
|
|
346
538
|
timestamp: timestamp,
|
|
347
539
|
isStreaming: true,
|
|
540
|
+
...(logicalTurnId === undefined ? {} : { logicalTurnId }),
|
|
348
541
|
};
|
|
349
542
|
this.chatMessages.push(userMessage);
|
|
350
543
|
}
|
|
@@ -468,15 +661,30 @@ export class MessageHandler extends EventEmitter {
|
|
|
468
661
|
if (messageData.data) {
|
|
469
662
|
const serverData = messageData.data;
|
|
470
663
|
switch (serverData.type) {
|
|
664
|
+
case "bot-llm-text-raw":
|
|
665
|
+
this._handleBotLlmTextRaw(serverData.data ?? serverData);
|
|
666
|
+
break;
|
|
471
667
|
case "final-user-transcription":
|
|
472
668
|
if (serverData.text) {
|
|
473
669
|
const finalText = serverData.text;
|
|
670
|
+
const logicalTurnId = this.transcriptionLogicalTurnId(serverData.logical_turn_id);
|
|
474
671
|
const lastUserMsg = this.chatMessages[this.chatMessages.length - 1];
|
|
672
|
+
// A final transcription identical to the line appended locally on send is
|
|
673
|
+
// the server echoing that send back: drop it, or the user sees it twice.
|
|
674
|
+
if (lastUserMsg?.type === "user-llm-text" &&
|
|
675
|
+
lastUserMsg.content.trim() === String(finalText).trim()) {
|
|
676
|
+
this.userTranscription = "";
|
|
677
|
+
this.emit("userTranscriptionChange", "");
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
475
680
|
if (lastUserMsg &&
|
|
476
681
|
lastUserMsg.type === "user-transcription" &&
|
|
477
682
|
lastUserMsg.isStreaming === true) {
|
|
478
683
|
this.chatMessages[this.chatMessages.length - 1] = {
|
|
479
684
|
...lastUserMsg,
|
|
685
|
+
...(logicalTurnId === undefined
|
|
686
|
+
? {}
|
|
687
|
+
: { logicalTurnId }),
|
|
480
688
|
content: finalText,
|
|
481
689
|
timestamp,
|
|
482
690
|
isStreaming: false,
|
|
@@ -489,6 +697,9 @@ export class MessageHandler extends EventEmitter {
|
|
|
489
697
|
content: finalText,
|
|
490
698
|
timestamp,
|
|
491
699
|
isStreaming: false,
|
|
700
|
+
...(logicalTurnId === undefined
|
|
701
|
+
? {}
|
|
702
|
+
: { logicalTurnId }),
|
|
492
703
|
});
|
|
493
704
|
}
|
|
494
705
|
this.emit("messagesChange", this.chatMessages);
|
|
@@ -659,6 +870,10 @@ export class MessageHandler extends EventEmitter {
|
|
|
659
870
|
}
|
|
660
871
|
}
|
|
661
872
|
_handleActionResponse(actions, messageId, timestamp) {
|
|
873
|
+
// model-output v2 is canonical. action-response remains a compatibility
|
|
874
|
+
// projection and executing both would duplicate the same operation.
|
|
875
|
+
if (this.modelOutputVersion === 2)
|
|
876
|
+
return;
|
|
662
877
|
this.emit("actionResponse", { actions });
|
|
663
878
|
const label = actions.length === 0
|
|
664
879
|
? "No action"
|
|
@@ -672,6 +887,66 @@ export class MessageHandler extends EventEmitter {
|
|
|
672
887
|
this.chatMessages.push(actionMessage);
|
|
673
888
|
this.emit("messagesChange", this.chatMessages);
|
|
674
889
|
}
|
|
890
|
+
_handleModelOutput(value) {
|
|
891
|
+
if (this.modelOutputVersion !== 2)
|
|
892
|
+
return;
|
|
893
|
+
const output = parseModelOutputMessage(value);
|
|
894
|
+
if (!output) {
|
|
895
|
+
this.emit("modelOutputProtocolError", {
|
|
896
|
+
code: "invalid_model_output",
|
|
897
|
+
message: "Core sent a malformed or unsupported model-output envelope",
|
|
898
|
+
});
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
// output_id identifies a provider invocation, not a serialized envelope.
|
|
902
|
+
// One invocation can produce distinct text and tool-call projections with
|
|
903
|
+
// the same id, and consumers must receive both in transport arrival order.
|
|
904
|
+
// Suppress only an exact transport replay of an already delivered payload.
|
|
905
|
+
const fingerprint = stableJsonFingerprint(output);
|
|
906
|
+
if (this.seenModelOutputPayloads.has(fingerprint))
|
|
907
|
+
return;
|
|
908
|
+
if (fingerprint.length > MAX_MODEL_OUTPUT_REPLAY_RETAINED_CHARS) {
|
|
909
|
+
this.emit("modelOutputProtocolError", {
|
|
910
|
+
code: "invalid_model_output",
|
|
911
|
+
message: "Core sent an oversized model-output envelope",
|
|
912
|
+
});
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
this.seenModelOutputPayloads.add(fingerprint);
|
|
916
|
+
this.seenModelOutputPayloadCharacters += fingerprint.length;
|
|
917
|
+
while (this.seenModelOutputPayloads.size > MAX_MODEL_OUTPUT_REPLAY_FINGERPRINTS ||
|
|
918
|
+
this.seenModelOutputPayloadCharacters >
|
|
919
|
+
MAX_MODEL_OUTPUT_REPLAY_RETAINED_CHARS) {
|
|
920
|
+
const oldestFingerprint = this.seenModelOutputPayloads.values().next()
|
|
921
|
+
.value;
|
|
922
|
+
if (!oldestFingerprint)
|
|
923
|
+
break;
|
|
924
|
+
this.seenModelOutputPayloads.delete(oldestFingerprint);
|
|
925
|
+
this.seenModelOutputPayloadCharacters -= oldestFingerprint.length;
|
|
926
|
+
}
|
|
927
|
+
this.emit("modelOutput", output);
|
|
928
|
+
}
|
|
929
|
+
_clearModelOutputReplayLedger() {
|
|
930
|
+
this.seenModelOutputPayloads.clear();
|
|
931
|
+
this.seenModelOutputPayloadCharacters = 0;
|
|
932
|
+
}
|
|
933
|
+
_handleBotLlmTextRaw(value) {
|
|
934
|
+
if (this.modelOutputVersion !== 2 || !value || typeof value !== "object") {
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
const data = value;
|
|
938
|
+
if (typeof data.text !== "string" ||
|
|
939
|
+
!isValidLogicalTurnId(data.logical_turn_id)) {
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
this.emit("botLlmTextRaw", {
|
|
943
|
+
text: data.text,
|
|
944
|
+
logical_turn_id: data.logical_turn_id,
|
|
945
|
+
...(isValidOutputId(data.output_id)
|
|
946
|
+
? { output_id: data.output_id }
|
|
947
|
+
: {}),
|
|
948
|
+
});
|
|
949
|
+
}
|
|
675
950
|
lifecycleOwner(data) {
|
|
676
951
|
if (!data) {
|
|
677
952
|
return {};
|
|
@@ -689,12 +964,30 @@ export class MessageHandler extends EventEmitter {
|
|
|
689
964
|
? value
|
|
690
965
|
: undefined;
|
|
691
966
|
}
|
|
967
|
+
transcriptionLogicalTurnId(value) {
|
|
968
|
+
return this.modelOutputVersion === 2 && isValidLogicalTurnId(value)
|
|
969
|
+
? value
|
|
970
|
+
: undefined;
|
|
971
|
+
}
|
|
692
972
|
/**
|
|
693
973
|
* Public entry point for WebSocket transport to inject messages into the same pipeline.
|
|
694
974
|
*/
|
|
695
975
|
handleDataReceivedPublic(payload) {
|
|
696
976
|
this.handleDataReceived(payload, null, "websocket");
|
|
697
977
|
}
|
|
978
|
+
/** Append a submitted text message without waiting for a server echo. */
|
|
979
|
+
addUserTextMessage(text) {
|
|
980
|
+
const content = text.trim();
|
|
981
|
+
if (!content)
|
|
982
|
+
return;
|
|
983
|
+
this.chatMessages.push({
|
|
984
|
+
id: `user-llm-text-${Date.now()}-${Math.random()}`,
|
|
985
|
+
type: "user-llm-text",
|
|
986
|
+
content,
|
|
987
|
+
timestamp: new Date().toISOString(),
|
|
988
|
+
});
|
|
989
|
+
this.emit("messagesChange", this.chatMessages);
|
|
990
|
+
}
|
|
698
991
|
/**
|
|
699
992
|
* Insert a message into the transcript locally. Nothing is transmitted and
|
|
700
993
|
* no LLM turn is billed -- this is for client-side lines (a greeting, a
|
|
@@ -728,6 +1021,8 @@ export class MessageHandler extends EventEmitter {
|
|
|
728
1021
|
this.isListening = false;
|
|
729
1022
|
this.totalBlendshapesReceived = 0;
|
|
730
1023
|
this._currentEmotion = null;
|
|
1024
|
+
this.seenBotOutputSegments.clear();
|
|
1025
|
+
this._clearModelOutputReplayLedger();
|
|
731
1026
|
this.blendshapeQueue.reset();
|
|
732
1027
|
this.emit("messagesChange", this.chatMessages);
|
|
733
1028
|
this.emit("userTranscriptionChange", "");
|