@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
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed failure from a room-topology request (`connect()` with a roster, or
|
|
3
|
+
* `joinRoom()`).
|
|
4
|
+
*
|
|
5
|
+
* The runtime answers these with a named code and a `request_trace_id`, and the
|
|
6
|
+
* backend's whole troubleshooting procedure starts from that trace id — it
|
|
7
|
+
* correlates API logs, room records, membership rows, actor logs and LiveKit.
|
|
8
|
+
* A plain `Error` throws it away, so this class carries it through to the
|
|
9
|
+
* caller alongside the status and the machine-readable code.
|
|
10
|
+
*/
|
|
11
|
+
export declare class ConvaiRoomError extends Error {
|
|
12
|
+
readonly status: number;
|
|
13
|
+
/** Named code such as `ROSTER_FINGERPRINT_MISMATCH`, or null when the body carried none. */
|
|
14
|
+
readonly code: string | null;
|
|
15
|
+
/** Quote this to backend support; it correlates every log for the request. */
|
|
16
|
+
readonly requestTraceId: string | null;
|
|
17
|
+
/** The parsed response body, or the raw text when it was not JSON. */
|
|
18
|
+
readonly detail: unknown;
|
|
19
|
+
constructor(message: string, status: number, code: string | null, requestTraceId: string | null, detail: unknown);
|
|
20
|
+
/**
|
|
21
|
+
* True when retrying the identical payload can plausibly succeed.
|
|
22
|
+
*
|
|
23
|
+
* Only the two "still working on it" conflicts qualify. A fingerprint
|
|
24
|
+
* mismatch means the topology genuinely differs, and a capacity or
|
|
25
|
+
* entitlement rejection will not change on its own — retrying those just
|
|
26
|
+
* burns the room's provisioning window.
|
|
27
|
+
*/
|
|
28
|
+
get isRetryable(): boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Pull a named code out of a parsed error body.
|
|
32
|
+
*
|
|
33
|
+
* The runtime is not consistent about where it puts one: some rejections carry
|
|
34
|
+
* `{ code }`, some put the name in `detail` as a bare string, and some nest it
|
|
35
|
+
* under `detail.code`. A code is never synthesised from the status — callers
|
|
36
|
+
* branch on it, and inventing `"CONFLICT"` for every 409 would make
|
|
37
|
+
* `ROSTER_FINGERPRINT_MISMATCH` and `ROSTER_PROVISIONING_IN_PROGRESS`
|
|
38
|
+
* indistinguishable, which is exactly the distinction that decides whether to
|
|
39
|
+
* retry.
|
|
40
|
+
*/
|
|
41
|
+
export declare function extractRoomErrorCode(body: unknown): string | null;
|
|
42
|
+
/** Pull the trace id out of a parsed body, wherever the layer put it. */
|
|
43
|
+
export declare function extractRequestTraceId(body: unknown): string | null;
|
|
44
|
+
/** Human-readable message from a body, falling back to the status. */
|
|
45
|
+
export declare function extractRoomErrorMessage(body: unknown, status: number): string;
|
|
46
|
+
/** Build a `ConvaiRoomError` from a response status and its parsed body. */
|
|
47
|
+
export declare function roomErrorFromResponse(status: number, body: unknown): ConvaiRoomError;
|
|
48
|
+
//# sourceMappingURL=ConvaiRoomError.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConvaiRoomError.d.ts","sourceRoot":"","sources":["../../src/core/ConvaiRoomError.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,qBAAa,eAAgB,SAAQ,KAAK;IACxC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,4FAA4F;IAC5F,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;gBAGvB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GAAG,IAAI,EACnB,cAAc,EAAE,MAAM,GAAG,IAAI,EAC7B,MAAM,EAAE,OAAO;IAUjB;;;;;;;OAOG;IACH,IAAI,WAAW,IAAI,OAAO,CAMzB;CACF;AAKD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAgBjE;AAUD,yEAAyE;AACzE,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAUlE;AAED,sEAAsE;AACtE,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAc7E;AAED,4EAA4E;AAC5E,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,OAAO,GACZ,eAAe,CAQjB"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed failure from a room-topology request (`connect()` with a roster, or
|
|
3
|
+
* `joinRoom()`).
|
|
4
|
+
*
|
|
5
|
+
* The runtime answers these with a named code and a `request_trace_id`, and the
|
|
6
|
+
* backend's whole troubleshooting procedure starts from that trace id — it
|
|
7
|
+
* correlates API logs, room records, membership rows, actor logs and LiveKit.
|
|
8
|
+
* A plain `Error` throws it away, so this class carries it through to the
|
|
9
|
+
* caller alongside the status and the machine-readable code.
|
|
10
|
+
*/
|
|
11
|
+
export class ConvaiRoomError extends Error {
|
|
12
|
+
constructor(message, status, code, requestTraceId, detail) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "ConvaiRoomError";
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.requestTraceId = requestTraceId;
|
|
18
|
+
this.detail = detail;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* True when retrying the identical payload can plausibly succeed.
|
|
22
|
+
*
|
|
23
|
+
* Only the two "still working on it" conflicts qualify. A fingerprint
|
|
24
|
+
* mismatch means the topology genuinely differs, and a capacity or
|
|
25
|
+
* entitlement rejection will not change on its own — retrying those just
|
|
26
|
+
* burns the room's provisioning window.
|
|
27
|
+
*/
|
|
28
|
+
get isRetryable() {
|
|
29
|
+
if (this.status !== 409)
|
|
30
|
+
return false;
|
|
31
|
+
return (this.code === "ROSTER_PROVISIONING_IN_PROGRESS" ||
|
|
32
|
+
this.code === "CONNECT_ATTEMPT_IN_PROGRESS");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Response bodies differ by layer; a code can arrive under any of these. */
|
|
36
|
+
const CODE_KEYS = ["code", "error_code", "detail_code"];
|
|
37
|
+
/**
|
|
38
|
+
* Pull a named code out of a parsed error body.
|
|
39
|
+
*
|
|
40
|
+
* The runtime is not consistent about where it puts one: some rejections carry
|
|
41
|
+
* `{ code }`, some put the name in `detail` as a bare string, and some nest it
|
|
42
|
+
* under `detail.code`. A code is never synthesised from the status — callers
|
|
43
|
+
* branch on it, and inventing `"CONFLICT"` for every 409 would make
|
|
44
|
+
* `ROSTER_FINGERPRINT_MISMATCH` and `ROSTER_PROVISIONING_IN_PROGRESS`
|
|
45
|
+
* indistinguishable, which is exactly the distinction that decides whether to
|
|
46
|
+
* retry.
|
|
47
|
+
*/
|
|
48
|
+
export function extractRoomErrorCode(body) {
|
|
49
|
+
if (typeof body === "string")
|
|
50
|
+
return looksLikeCode(body) ? body : null;
|
|
51
|
+
if (typeof body !== "object" || body === null)
|
|
52
|
+
return null;
|
|
53
|
+
const record = body;
|
|
54
|
+
for (const key of CODE_KEYS) {
|
|
55
|
+
const value = record[key];
|
|
56
|
+
if (typeof value === "string" && value)
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
const detail = record.detail;
|
|
60
|
+
if (typeof detail === "string")
|
|
61
|
+
return looksLikeCode(detail) ? detail : null;
|
|
62
|
+
if (typeof detail === "object" && detail !== null) {
|
|
63
|
+
return extractRoomErrorCode(detail);
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A code is SCREAMING_SNAKE_CASE. `detail` is also where human prose lands
|
|
69
|
+
* ("Human room capacity reached"), so shape is what tells the two apart.
|
|
70
|
+
*/
|
|
71
|
+
function looksLikeCode(value) {
|
|
72
|
+
return /^[A-Z][A-Z0-9_]{2,}$/.test(value.trim());
|
|
73
|
+
}
|
|
74
|
+
/** Pull the trace id out of a parsed body, wherever the layer put it. */
|
|
75
|
+
export function extractRequestTraceId(body) {
|
|
76
|
+
if (typeof body !== "object" || body === null)
|
|
77
|
+
return null;
|
|
78
|
+
const record = body;
|
|
79
|
+
const direct = record.request_trace_id;
|
|
80
|
+
if (typeof direct === "string" && direct)
|
|
81
|
+
return direct;
|
|
82
|
+
const detail = record.detail;
|
|
83
|
+
if (typeof detail === "object" && detail !== null) {
|
|
84
|
+
return extractRequestTraceId(detail);
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
/** Human-readable message from a body, falling back to the status. */
|
|
89
|
+
export function extractRoomErrorMessage(body, status) {
|
|
90
|
+
if (typeof body === "string" && body.trim())
|
|
91
|
+
return body.trim();
|
|
92
|
+
if (typeof body === "object" && body !== null) {
|
|
93
|
+
const record = body;
|
|
94
|
+
for (const key of ["message", "detail", "error"]) {
|
|
95
|
+
const value = record[key];
|
|
96
|
+
if (typeof value === "string" && value.trim())
|
|
97
|
+
return value.trim();
|
|
98
|
+
if (typeof value === "object" && value !== null) {
|
|
99
|
+
const nested = extractRoomErrorMessage(value, status);
|
|
100
|
+
if (!nested.startsWith("Room request failed"))
|
|
101
|
+
return nested;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return `Room request failed with HTTP ${status}`;
|
|
106
|
+
}
|
|
107
|
+
/** Build a `ConvaiRoomError` from a response status and its parsed body. */
|
|
108
|
+
export function roomErrorFromResponse(status, body) {
|
|
109
|
+
return new ConvaiRoomError(extractRoomErrorMessage(body, status), status, extractRoomErrorCode(body), extractRequestTraceId(body), body);
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=ConvaiRoomError.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConvaiRoomError.js","sourceRoot":"","sources":["../../src/core/ConvaiRoomError.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Typed failure from a room-topology request (`connect()` with a roster, or\n * `joinRoom()`).\n *\n * The runtime answers these with a named code and a `request_trace_id`, and the\n * backend's whole troubleshooting procedure starts from that trace id — it\n * correlates API logs, room records, membership rows, actor logs and LiveKit.\n * A plain `Error` throws it away, so this class carries it through to the\n * caller alongside the status and the machine-readable code.\n */\nexport class ConvaiRoomError extends Error {\n readonly status: number;\n /** Named code such as `ROSTER_FINGERPRINT_MISMATCH`, or null when the body carried none. */\n readonly code: string | null;\n /** Quote this to backend support; it correlates every log for the request. */\n readonly requestTraceId: string | null;\n /** The parsed response body, or the raw text when it was not JSON. */\n readonly detail: unknown;\n\n constructor(\n message: string,\n status: number,\n code: string | null,\n requestTraceId: string | null,\n detail: unknown,\n ) {\n super(message);\n this.name = \"ConvaiRoomError\";\n this.status = status;\n this.code = code;\n this.requestTraceId = requestTraceId;\n this.detail = detail;\n }\n\n /**\n * True when retrying the identical payload can plausibly succeed.\n *\n * Only the two \"still working on it\" conflicts qualify. A fingerprint\n * mismatch means the topology genuinely differs, and a capacity or\n * entitlement rejection will not change on its own — retrying those just\n * burns the room's provisioning window.\n */\n get isRetryable(): boolean {\n if (this.status !== 409) return false;\n return (\n this.code === \"ROSTER_PROVISIONING_IN_PROGRESS\" ||\n this.code === \"CONNECT_ATTEMPT_IN_PROGRESS\"\n );\n }\n}\n\n/** Response bodies differ by layer; a code can arrive under any of these. */\nconst CODE_KEYS = [\"code\", \"error_code\", \"detail_code\"] as const;\n\n/**\n * Pull a named code out of a parsed error body.\n *\n * The runtime is not consistent about where it puts one: some rejections carry\n * `{ code }`, some put the name in `detail` as a bare string, and some nest it\n * under `detail.code`. A code is never synthesised from the status — callers\n * branch on it, and inventing `\"CONFLICT\"` for every 409 would make\n * `ROSTER_FINGERPRINT_MISMATCH` and `ROSTER_PROVISIONING_IN_PROGRESS`\n * indistinguishable, which is exactly the distinction that decides whether to\n * retry.\n */\nexport function extractRoomErrorCode(body: unknown): string | null {\n if (typeof body === \"string\") return looksLikeCode(body) ? body : null;\n if (typeof body !== \"object\" || body === null) return null;\n\n const record = body as Record<string, unknown>;\n for (const key of CODE_KEYS) {\n const value = record[key];\n if (typeof value === \"string\" && value) return value;\n }\n\n const detail = record.detail;\n if (typeof detail === \"string\") return looksLikeCode(detail) ? detail : null;\n if (typeof detail === \"object\" && detail !== null) {\n return extractRoomErrorCode(detail);\n }\n return null;\n}\n\n/**\n * A code is SCREAMING_SNAKE_CASE. `detail` is also where human prose lands\n * (\"Human room capacity reached\"), so shape is what tells the two apart.\n */\nfunction looksLikeCode(value: string): boolean {\n return /^[A-Z][A-Z0-9_]{2,}$/.test(value.trim());\n}\n\n/** Pull the trace id out of a parsed body, wherever the layer put it. */\nexport function extractRequestTraceId(body: unknown): string | null {\n if (typeof body !== \"object\" || body === null) return null;\n const record = body as Record<string, unknown>;\n const direct = record.request_trace_id;\n if (typeof direct === \"string\" && direct) return direct;\n const detail = record.detail;\n if (typeof detail === \"object\" && detail !== null) {\n return extractRequestTraceId(detail);\n }\n return null;\n}\n\n/** Human-readable message from a body, falling back to the status. */\nexport function extractRoomErrorMessage(body: unknown, status: number): string {\n if (typeof body === \"string\" && body.trim()) return body.trim();\n if (typeof body === \"object\" && body !== null) {\n const record = body as Record<string, unknown>;\n for (const key of [\"message\", \"detail\", \"error\"]) {\n const value = record[key];\n if (typeof value === \"string\" && value.trim()) return value.trim();\n if (typeof value === \"object\" && value !== null) {\n const nested = extractRoomErrorMessage(value, status);\n if (!nested.startsWith(\"Room request failed\")) return nested;\n }\n }\n }\n return `Room request failed with HTTP ${status}`;\n}\n\n/** Build a `ConvaiRoomError` from a response status and its parsed body. */\nexport function roomErrorFromResponse(\n status: number,\n body: unknown,\n): ConvaiRoomError {\n return new ConvaiRoomError(\n extractRoomErrorMessage(body, status),\n status,\n extractRoomErrorCode(body),\n extractRequestTraceId(body),\n body,\n );\n}\n"]}
|
|
@@ -86,6 +86,20 @@ export declare class MessageHandler extends EventEmitter {
|
|
|
86
86
|
* Public entry point for WebSocket transport to inject messages into the same pipeline.
|
|
87
87
|
*/
|
|
88
88
|
handleDataReceivedPublic(payload: Uint8Array): void;
|
|
89
|
+
/**
|
|
90
|
+
* Insert a message into the transcript locally. Nothing is transmitted and
|
|
91
|
+
* no LLM turn is billed -- this is for client-side lines (a greeting, a
|
|
92
|
+
* system notice, a replayed history entry) that must appear in the chat.
|
|
93
|
+
*
|
|
94
|
+
* Subscribers see it through the same `messagesChange` as a live message,
|
|
95
|
+
* so React and the widgets pick it up with no special casing.
|
|
96
|
+
*/
|
|
97
|
+
appendLocalMessage(message: {
|
|
98
|
+
sender: "user" | "bot";
|
|
99
|
+
content: string;
|
|
100
|
+
membershipId?: string;
|
|
101
|
+
characterId?: string;
|
|
102
|
+
}): ChatMessage;
|
|
89
103
|
/**
|
|
90
104
|
* Reset message state
|
|
91
105
|
*/
|
|
@@ -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,
|
|
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,EAKZ,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EACL,eAAe,EAEhB,MAAM,mBAAmB,CAAC;AAS3B;;;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;IAI3E,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;;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;IA4mB1B,OAAO,CAAC,qBAAqB;IAmB7B,OAAO,CAAC,cAAc;IAkBtB,OAAO,CAAC,iBAAiB;IAMzB;;OAEG;IACH,wBAAwB,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI;IAInD;;;;;;;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;CAiBd"}
|
|
@@ -133,6 +133,23 @@ export class MessageHandler extends EventEmitter {
|
|
|
133
133
|
// Extract and categorize messages for chat display
|
|
134
134
|
const timestamp = new Date().toISOString();
|
|
135
135
|
const messageId = `${messageData.type}-${Date.now()}-${Math.random()}`;
|
|
136
|
+
// Multi-character routing identity, carried inline on every
|
|
137
|
+
// character-originated event. Undefined in single-character sessions, so
|
|
138
|
+
// these spread to nothing and every existing message keeps its shape.
|
|
139
|
+
const attribution = {
|
|
140
|
+
...(messageData.data?.membership_id && {
|
|
141
|
+
membershipId: messageData.data.membership_id,
|
|
142
|
+
}),
|
|
143
|
+
...(messageData.data?.character_id && {
|
|
144
|
+
characterId: messageData.data.character_id,
|
|
145
|
+
}),
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Two instances must never share a streaming bubble. Without this a
|
|
149
|
+
* target switch mid-turn splices one character's text onto another's,
|
|
150
|
+
* because the merge would only be checking `isStreaming`.
|
|
151
|
+
*/
|
|
152
|
+
const sameSpeaker = (msg) => !!msg && msg.membershipId === attribution.membershipId;
|
|
136
153
|
// Handle different message types
|
|
137
154
|
switch (messageData.type) {
|
|
138
155
|
// User text messages - Skip these, only allow user-transcription
|
|
@@ -148,7 +165,12 @@ export class MessageHandler extends EventEmitter {
|
|
|
148
165
|
break;
|
|
149
166
|
// Bot Ready - Bot is ready, connection should be true
|
|
150
167
|
case "bot-ready":
|
|
151
|
-
this
|
|
168
|
+
// `about` carries this instance's routing identity in a roster room,
|
|
169
|
+
// and the same fields are also flattened onto `data`. Prefer `about`,
|
|
170
|
+
// fall back to the flattened copy, and pass undefined for a
|
|
171
|
+
// single-character session.
|
|
172
|
+
this.emit("botReady", messageData.data?.about ??
|
|
173
|
+
(messageData.data?.membership_id ? messageData.data : undefined));
|
|
152
174
|
break;
|
|
153
175
|
// Bot LLM Started - Begin streaming response
|
|
154
176
|
case "bot-llm-started":
|
|
@@ -162,6 +184,7 @@ export class MessageHandler extends EventEmitter {
|
|
|
162
184
|
content: "",
|
|
163
185
|
timestamp: timestamp,
|
|
164
186
|
isStreaming: true,
|
|
187
|
+
...attribution,
|
|
165
188
|
};
|
|
166
189
|
this.activeBotLlmMessageId = messageId;
|
|
167
190
|
this.chatMessages.push(streamingMessage);
|
|
@@ -205,7 +228,9 @@ export class MessageHandler extends EventEmitter {
|
|
|
205
228
|
lastMsg.content.includes(newText)) {
|
|
206
229
|
break;
|
|
207
230
|
}
|
|
208
|
-
if (lastMsg?.type === "bot-output" &&
|
|
231
|
+
if (lastMsg?.type === "bot-output" &&
|
|
232
|
+
lastMsg.isStreaming &&
|
|
233
|
+
sameSpeaker(lastMsg)) {
|
|
209
234
|
this.chatMessages[this.chatMessages.length - 1] = {
|
|
210
235
|
...lastMsg,
|
|
211
236
|
content: lastMsg.content + newText,
|
|
@@ -218,6 +243,7 @@ export class MessageHandler extends EventEmitter {
|
|
|
218
243
|
content: newText,
|
|
219
244
|
timestamp: new Date().toISOString(),
|
|
220
245
|
isStreaming: true,
|
|
246
|
+
...attribution,
|
|
221
247
|
});
|
|
222
248
|
}
|
|
223
249
|
this.emit("messagesChange", this.chatMessages);
|
|
@@ -231,7 +257,8 @@ export class MessageHandler extends EventEmitter {
|
|
|
231
257
|
const activeIndex = this.activeBotLlmMessageId
|
|
232
258
|
? this.chatMessages.findIndex((message) => message.id === this.activeBotLlmMessageId &&
|
|
233
259
|
message.type === "bot-llm-text" &&
|
|
234
|
-
message.isStreaming
|
|
260
|
+
message.isStreaming &&
|
|
261
|
+
sameSpeaker(message))
|
|
235
262
|
: -1;
|
|
236
263
|
if (activeIndex >= 0) {
|
|
237
264
|
const activeMessage = this.chatMessages[activeIndex];
|
|
@@ -248,6 +275,7 @@ export class MessageHandler extends EventEmitter {
|
|
|
248
275
|
content: newChunk,
|
|
249
276
|
timestamp: new Date().toISOString(),
|
|
250
277
|
isStreaming: true,
|
|
278
|
+
...attribution,
|
|
251
279
|
};
|
|
252
280
|
this.activeBotLlmMessageId = streamingMsg.id;
|
|
253
281
|
this.chatMessages.push(streamingMsg);
|
|
@@ -272,6 +300,24 @@ export class MessageHandler extends EventEmitter {
|
|
|
272
300
|
this.emit("botRespondingChange", false);
|
|
273
301
|
this.emit("messagesChange", this.chatMessages);
|
|
274
302
|
break;
|
|
303
|
+
// Text-only Interactions API terminal marker. This is deliberately a
|
|
304
|
+
// separate event: a completed REST turn must not be represented as a
|
|
305
|
+
// synthetic bot-llm-stopped message.
|
|
306
|
+
case "bot-turn-completed":
|
|
307
|
+
const completedIndex = this.activeBotLlmMessageId
|
|
308
|
+
? this.chatMessages.findIndex((message) => message.id === this.activeBotLlmMessageId)
|
|
309
|
+
: -1;
|
|
310
|
+
if (completedIndex >= 0 && this.chatMessages[completedIndex].isStreaming) {
|
|
311
|
+
this.chatMessages[completedIndex] = {
|
|
312
|
+
...this.chatMessages[completedIndex],
|
|
313
|
+
isStreaming: false,
|
|
314
|
+
};
|
|
315
|
+
this.emit("messagesChange", this.chatMessages);
|
|
316
|
+
}
|
|
317
|
+
this.isBotResponding = false;
|
|
318
|
+
this.emit("botRespondingChange", false);
|
|
319
|
+
this.emit("botTurnCompleted", messageData.data ?? {});
|
|
320
|
+
break;
|
|
275
321
|
// User Transcription Messages
|
|
276
322
|
case "user-transcription":
|
|
277
323
|
if (messageData.data?.text) {
|
|
@@ -321,7 +367,8 @@ export class MessageHandler extends EventEmitter {
|
|
|
321
367
|
emotion: messageData.data.emotion,
|
|
322
368
|
scale: messageData.data.scale,
|
|
323
369
|
};
|
|
324
|
-
|
|
370
|
+
if (this.logRtviMessages)
|
|
371
|
+
console.log("[SDK bot-emotion]", messageData.data);
|
|
325
372
|
this.emit("emotionChange", this._currentEmotion);
|
|
326
373
|
const emotionMessage = {
|
|
327
374
|
id: messageId,
|
|
@@ -395,6 +442,14 @@ export class MessageHandler extends EventEmitter {
|
|
|
395
442
|
this.emit("metrics", messageData.data);
|
|
396
443
|
}
|
|
397
444
|
break;
|
|
445
|
+
// Per-turn usage/cost breakdown. Informational only: the server streams
|
|
446
|
+
// it once the client opts in with usage-toggle, and it never affects
|
|
447
|
+
// server-side usage tracking or billing.
|
|
448
|
+
case "usage-update":
|
|
449
|
+
if (messageData.data) {
|
|
450
|
+
this.emit("usageUpdate", messageData.data);
|
|
451
|
+
}
|
|
452
|
+
break;
|
|
398
453
|
// Behavior Tree Response Messages
|
|
399
454
|
case "behavior-tree-response":
|
|
400
455
|
if (messageData.data?.narrative_section_id) {
|
|
@@ -448,7 +503,8 @@ export class MessageHandler extends EventEmitter {
|
|
|
448
503
|
emotion: serverData.emotion,
|
|
449
504
|
scale: serverData.scale,
|
|
450
505
|
};
|
|
451
|
-
|
|
506
|
+
if (this.logRtviMessages)
|
|
507
|
+
console.log("[SDK bot-emotion (ws)]", serverData);
|
|
452
508
|
this.emit("emotionChange", this._currentEmotion);
|
|
453
509
|
}
|
|
454
510
|
break;
|
|
@@ -535,6 +591,35 @@ export class MessageHandler extends EventEmitter {
|
|
|
535
591
|
this.emit("serverResponse", serverResponse);
|
|
536
592
|
break;
|
|
537
593
|
}
|
|
594
|
+
/**
|
|
595
|
+
* Lifecycle of one roster member. Emitted when a member is added
|
|
596
|
+
* at runtime, becomes ready, or fails after /connect already
|
|
597
|
+
* returned 200. `bot-ready` stays the readiness authority --
|
|
598
|
+
* a `character-status` of "ready" must not enable interaction on
|
|
599
|
+
* its own -- but this is the only way a background dispatch
|
|
600
|
+
* failure is ever reported.
|
|
601
|
+
*/
|
|
602
|
+
case "character-status": {
|
|
603
|
+
const status = {
|
|
604
|
+
status: serverData.status ?? "starting",
|
|
605
|
+
membershipId: serverData.membership_id ?? "",
|
|
606
|
+
characterId: serverData.character_id ?? "",
|
|
607
|
+
characterSessionId: serverData.character_session_id ?? null,
|
|
608
|
+
participantIdentity: serverData.participant_identity ??
|
|
609
|
+
(serverData.membership_id
|
|
610
|
+
? `character:${serverData.membership_id}`
|
|
611
|
+
: ""),
|
|
612
|
+
roomSessionId: serverData.room_session_id ?? null,
|
|
613
|
+
rosterEpoch: typeof serverData.roster_epoch === "number"
|
|
614
|
+
? serverData.roster_epoch
|
|
615
|
+
: null,
|
|
616
|
+
failureCode: serverData.failure_code ?? null,
|
|
617
|
+
};
|
|
618
|
+
// Unattributable status is dropped rather than guessed at.
|
|
619
|
+
if (status.membershipId)
|
|
620
|
+
this.emit("characterStatus", status);
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
538
623
|
// interaction-created: session IDs for analytics/tracking
|
|
539
624
|
case "interaction-created": {
|
|
540
625
|
const interactionCreated = {
|
|
@@ -610,6 +695,27 @@ export class MessageHandler extends EventEmitter {
|
|
|
610
695
|
handleDataReceivedPublic(payload) {
|
|
611
696
|
this.handleDataReceived(payload, null, "websocket");
|
|
612
697
|
}
|
|
698
|
+
/**
|
|
699
|
+
* Insert a message into the transcript locally. Nothing is transmitted and
|
|
700
|
+
* no LLM turn is billed -- this is for client-side lines (a greeting, a
|
|
701
|
+
* system notice, a replayed history entry) that must appear in the chat.
|
|
702
|
+
*
|
|
703
|
+
* Subscribers see it through the same `messagesChange` as a live message,
|
|
704
|
+
* so React and the widgets pick it up with no special casing.
|
|
705
|
+
*/
|
|
706
|
+
appendLocalMessage(message) {
|
|
707
|
+
const chatMessage = {
|
|
708
|
+
id: `local-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`,
|
|
709
|
+
type: message.sender === "user" ? "user-llm-text" : "bot-llm-text",
|
|
710
|
+
content: message.content,
|
|
711
|
+
timestamp: new Date().toISOString(),
|
|
712
|
+
...(message.membershipId && { membershipId: message.membershipId }),
|
|
713
|
+
...(message.characterId && { characterId: message.characterId }),
|
|
714
|
+
};
|
|
715
|
+
this.chatMessages.push(chatMessage);
|
|
716
|
+
this.emit("messagesChange", this.chatMessages);
|
|
717
|
+
return chatMessage;
|
|
718
|
+
}
|
|
613
719
|
/**
|
|
614
720
|
* Reset message state
|
|
615
721
|
*/
|