@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.
Files changed (68) hide show
  1. package/README.md +64 -3
  2. package/dist/core/CharacterRoster.d.ts +84 -0
  3. package/dist/core/CharacterRoster.d.ts.map +1 -0
  4. package/dist/core/CharacterRoster.js +348 -0
  5. package/dist/core/CharacterRoster.js.map +1 -0
  6. package/dist/core/CharacterVersionManager.d.ts +100 -0
  7. package/dist/core/CharacterVersionManager.d.ts.map +1 -0
  8. package/dist/core/CharacterVersionManager.js +227 -0
  9. package/dist/core/CharacterVersionManager.js.map +1 -0
  10. package/dist/core/ConvaiClient.d.ts +109 -1
  11. package/dist/core/ConvaiClient.d.ts.map +1 -1
  12. package/dist/core/ConvaiClient.js +642 -49
  13. package/dist/core/ConvaiClient.js.map +1 -1
  14. package/dist/core/ConvaiRoomError.d.ts +48 -0
  15. package/dist/core/ConvaiRoomError.d.ts.map +1 -0
  16. package/dist/core/ConvaiRoomError.js +111 -0
  17. package/dist/core/ConvaiRoomError.js.map +1 -0
  18. package/dist/core/MessageHandler.d.ts +14 -0
  19. package/dist/core/MessageHandler.d.ts.map +1 -1
  20. package/dist/core/MessageHandler.js +111 -5
  21. package/dist/core/MessageHandler.js.map +1 -1
  22. package/dist/core/SSESession.d.ts +15 -0
  23. package/dist/core/SSESession.d.ts.map +1 -1
  24. package/dist/core/SSESession.js +92 -3
  25. package/dist/core/SSESession.js.map +1 -1
  26. package/dist/core/characterReference.d.ts +40 -0
  27. package/dist/core/characterReference.d.ts.map +1 -0
  28. package/dist/core/characterReference.js +72 -0
  29. package/dist/core/characterReference.js.map +1 -0
  30. package/dist/core/connectRequest.d.ts +1 -0
  31. package/dist/core/connectRequest.d.ts.map +1 -1
  32. package/dist/core/connectRequest.js +11 -0
  33. package/dist/core/connectRequest.js.map +1 -1
  34. package/dist/core/index.d.ts +5 -0
  35. package/dist/core/index.d.ts.map +1 -1
  36. package/dist/core/index.js +6 -0
  37. package/dist/core/index.js.map +1 -1
  38. package/dist/core/rosterRequest.d.ts +57 -0
  39. package/dist/core/rosterRequest.d.ts.map +1 -0
  40. package/dist/core/rosterRequest.js +210 -0
  41. package/dist/core/rosterRequest.js.map +1 -0
  42. package/dist/core/types.d.ts +717 -7
  43. package/dist/core/types.d.ts.map +1 -1
  44. package/dist/core/types.js.map +1 -1
  45. package/dist/react/components/rtc-widget/components/MarkdownRenderer.d.ts.map +1 -1
  46. package/dist/react/components/rtc-widget/components/MarkdownRenderer.js +39 -44
  47. package/dist/react/components/rtc-widget/components/MarkdownRenderer.js.map +1 -1
  48. package/dist/react/hooks/useConvaiClient.d.ts.map +1 -1
  49. package/dist/react/hooks/useConvaiClient.js +15 -2
  50. package/dist/react/hooks/useConvaiClient.js.map +1 -1
  51. package/dist/utils/inlineMarkdown.d.ts +38 -0
  52. package/dist/utils/inlineMarkdown.d.ts.map +1 -0
  53. package/dist/utils/inlineMarkdown.js +106 -0
  54. package/dist/utils/inlineMarkdown.js.map +1 -0
  55. package/dist/vanilla/AudioRenderer.d.ts.map +1 -1
  56. package/dist/vanilla/AudioRenderer.js +6 -0
  57. package/dist/vanilla/AudioRenderer.js.map +1 -1
  58. package/dist/vanilla/ConvaiWidget.d.ts.map +1 -1
  59. package/dist/vanilla/ConvaiWidget.js +71 -57
  60. package/dist/vanilla/ConvaiWidget.js.map +1 -1
  61. package/dist/vanilla/index.d.ts +2 -0
  62. package/dist/vanilla/index.d.ts.map +1 -1
  63. package/dist/vanilla/index.js +2 -0
  64. package/dist/vanilla/index.js.map +1 -1
  65. package/dist/version.d.ts +1 -1
  66. package/dist/version.js +1 -1
  67. package/dist/version.js.map +1 -1
  68. 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,EAAE,WAAW,EAAoD,MAAM,SAAS,CAAC;AACxF,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;IA2gB1B,OAAO,CAAC,qBAAqB;IAmB7B,OAAO,CAAC,cAAc;IAkBtB,OAAO,CAAC,iBAAiB;IAMzB;;OAEG;IACH,wBAAwB,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI;IAInD;;OAEG;IACH,KAAK,IAAI,IAAI;CAiBd"}
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.emit("botReady");
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" && lastMsg.isStreaming) {
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
- console.log("[SDK bot-emotion]", messageData.data);
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
- console.log("[SDK bot-emotion (ws)]", serverData);
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
  */