anva-sdk 0.2.0 → 0.3.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Penguin Robotics
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,26 +1,36 @@
1
- # anva — JavaScript / TypeScript SDK
1
+ # anva-sdk — JavaScript / TypeScript
2
2
 
3
- Official JS/TS SDK for [Anva](https://anva.ai) live AI avatars.
4
- ESM, zero dependencies, typed. Node 18+ and modern browsers (keep your API
5
- key server-side; hand only the `embed_url` to clients).
6
-
7
- ```bash
8
- npm install anva-sdk
9
- ```
3
+ Install with `npm install anva-sdk@0.3.0`. REST uses Node 18+'s fetch. Realtime needs
4
+ Node 22+'s WebSocket or an injected compatible constructor.
10
5
 
11
6
  ```js
12
- import { Anva } from "anva-sdk";
13
-
7
+ import { Anva, AnvaError } from "anva-sdk";
14
8
  const client = new Anva(process.env.ANVA_KEY);
15
-
16
- const session = await client.createSession({ characterId: "char_..." });
17
- console.log(session.embed_url);
18
-
19
- await client.sendMessage(session.session_id, "Welcome!");
20
-
21
- for await (const event of client.events(session.session_id)) {
22
- console.log(event);
9
+ const capabilities = await client.capabilities();
10
+ const session = await client.createSession({
11
+ presetId:"YOUR_PRESET_ID", serviceMode:"byo_llm"
12
+ });
13
+ // Attach session.embed_url in your browser.
14
+ const stream = await client.connect(session.session_id);
15
+ try {
16
+ for await (const event of stream) {
17
+ if (event.type === "turn.request") {
18
+ // Replace this immediate sample reply with your cancellable LLM worker.
19
+ await stream.turnDelta(event.payload.turn_id, "Hello from your application.");
20
+ await stream.turnDone(event.payload.turn_id);
21
+ }
22
+ }
23
+ } finally {
24
+ stream.close();
25
+ await client.endSession(session.session_id);
23
26
  }
24
27
  ```
25
28
 
26
- Errors throw `AnvaError` with `.status` and `.code`. MIT © Penguin Robotics
29
+ Inject WebSocket on older Node: `client.connect(id, {WebSocketImpl})`. Keep keys
30
+ and sockets server-side. `AnvaError` exposes `status`, `code`, and `details`;
31
+ command errors arrive as structured events. Never log authenticated event URLs.
32
+
33
+ All five canonical modes, capability/billing discovery, REST interrupt,
34
+ presentation and PCM methods are typed in `index.d.ts`. Read the repository
35
+ README for mode availability and flow-control requirements. Preset updates use
36
+ the REST API's snake_case patch fields.
package/index.d.ts CHANGED
@@ -1,81 +1,118 @@
1
- /** Official JavaScript/TypeScript SDK for Anva (https://anva.ai). */
2
-
3
- export interface AnvaOptions {
4
- /** Override for self-hosted / testing setups. Default: https://anva.ai */
5
- baseUrl?: string;
1
+ /** Official Anva SDK. All API keys and authenticated realtime sockets belong on your server. */
2
+ export type ServiceMode = 'avatar_only' | 'byo_llm' | 'anva_light' | 'anva_expressive' | 'elevenagents_max';
3
+ export interface AnvaOptions { baseUrl?: string; }
4
+ export interface PerformanceOptions {
5
+ conversation_provider?: string;
6
+ performance_mode?: string;
7
+ elevenlabs_agent_id?: string;
8
+ dynamic_expressions?: boolean;
9
+ [key: string]: unknown;
6
10
  }
7
-
8
11
  export interface CreateSessionParams {
9
- characterId: string;
10
- /** Omit for the built-in conversation engine, or "external" to drive the LLM yourself. */
11
- llmMode?: "external";
12
+ presetId?: string;
13
+ avatarId?: string;
14
+ systemPrompt?: string;
15
+ voiceId?: string;
16
+ languageCode?: string;
17
+ serviceMode?: ServiceMode;
18
+ /** Deprecated compatibility alias. Conflicts with serviceMode are rejected by the server. */
19
+ llmMode?: string;
20
+ performanceOptions?: PerformanceOptions;
21
+ conversationProvider?: string;
22
+ performanceMode?: string;
23
+ elevenlabsAgentId?: string;
24
+ dynamicExpressions?: boolean;
12
25
  webhookUrl?: string;
13
26
  webhookSecret?: string;
14
27
  }
15
-
28
+ export interface ModeCapability { id: ServiceMode; name: string; available: boolean; reason?: string; input?: string[]; [key: string]: unknown; }
29
+ export interface Capabilities { api_version: string; modes: ModeCapability[]; audio_input?: Record<string, unknown>; [key: string]: unknown; }
30
+ export interface SessionBilling { unit: string; tokens_per_minute?: number; credits_per_minute?: number; rate_version: string; basis: string; [key: string]: unknown; }
16
31
  export interface Session {
17
32
  session_id: string;
18
33
  session_token: string;
19
- character_id: string;
34
+ instance_id: string;
35
+ preset_id?: string;
36
+ avatar_id?: string;
37
+ service_mode?: ServiceMode;
20
38
  llm_mode?: string;
39
+ billing?: SessionBilling;
21
40
  expires_at: string;
22
- /** Ready to drop into an <iframe allow="camera; microphone; autoplay">. */
23
41
  embed_url: string;
24
42
  events_ws_url: string;
25
43
  [key: string]: unknown;
26
44
  }
27
-
28
- export interface Character {
29
- id: string;
30
- name: string;
31
- visual_character_id: string;
32
- system_prompt: string;
33
- voice_id: string;
34
- language_code?: string;
35
- disabled: boolean;
36
- active: boolean;
37
- [key: string]: unknown;
38
- }
39
-
40
- export interface SessionEvent {
41
- type: string;
45
+ export interface Preset {
46
+ id: string; name: string; avatar_id: string;
47
+ /** Deprecated alias. */ visual_character_id?: string;
48
+ system_prompt: string; voice_id: string; language_code?: string;
49
+ disabled: boolean; active: boolean;
42
50
  [key: string]: unknown;
43
51
  }
44
-
45
- export interface EventsOptions {
46
- /** A WebSocket-compatible constructor (e.g. `ws` in older Node versions). */
47
- WebSocketImpl?: new (url: string) => WebSocket;
52
+ export interface Instance { id: string; [key: string]: unknown; }
53
+ export interface SessionEvent { type: string; payload?: Record<string, unknown>; [key: string]: unknown; }
54
+ export interface PresentationContext {
55
+ version: 1; demo: string; revision: number; page: string; highlight: string;
56
+ brief?: string; opening?: string;
57
+ pages: Array<{ id: string; title: string; items: Array<{id: string; text: string}> }>;
48
58
  }
49
-
59
+ export interface PresentationStart { version: 1; demo: string; revision: number; }
60
+ export interface EventsOptions { WebSocketImpl?: new (url: string) => WebSocket; }
61
+ export type PCMBytes = Uint8Array | ArrayBuffer;
50
62
  export declare class AnvaError extends Error {
51
- status: number;
52
- code: string;
63
+ constructor(status: number, code: string, message: string, details?: Record<string, unknown>);
64
+ status: number; code: string; details: Record<string, unknown>;
65
+ }
66
+ /** One active event reader per connection. Breaking iteration closes the socket. */
67
+ export declare class RealtimeSession implements AsyncIterable<SessionEvent> {
68
+ constructor(socket: WebSocket);
69
+ ready: Promise<RealtimeSession>;
70
+ closed: boolean;
71
+ send(type: string, payload?: Record<string, unknown>): Promise<void>;
72
+ message(text: string): Promise<void>;
73
+ interrupt(): Promise<void>;
74
+ updateContext(context: PresentationContext): Promise<void>;
75
+ startPresentation(start: PresentationStart): Promise<void>;
76
+ turnDelta(turnId: string, text: string): Promise<void>;
77
+ turnDone(turnId: string): Promise<void>;
78
+ turnCancel(turnId: string, reason?: string): Promise<void>;
79
+ startSpeech(turnId: string, options?: {text?: string}): Promise<void>;
80
+ appendSpeech(turnId: string, seq: number, startSample: number, pcm: PCMBytes): Promise<void>;
81
+ finishSpeech(turnId: string, totalSamples: number): Promise<void>;
82
+ cancelSpeech(turnId: string): Promise<void>;
83
+ close(): void;
84
+ [Symbol.asyncIterator](): AsyncGenerator<SessionEvent, void, void>;
53
85
  }
54
-
55
86
  export declare class Anva {
56
87
  constructor(apiKey: string, opts?: AnvaOptions);
57
- apiKey: string;
58
- baseUrl: string;
59
-
88
+ apiKey: string; baseUrl: string;
60
89
  createSession(params: CreateSessionParams): Promise<Session>;
61
90
  getSession(sessionId: string): Promise<Record<string, unknown>>;
62
91
  endSession(sessionId: string): Promise<Record<string, unknown>>;
63
- sendMessage(sessionId: string, text: string): Promise<{ status: string }>;
64
- interrupt(sessionId: string): Promise<{ status: string }>;
65
- triggerAction(sessionId: string, name: string): Promise<{ status: string }>;
92
+ sendMessage(sessionId: string, text: string): Promise<{status: string}>;
93
+ interrupt(sessionId: string): Promise<{status: string}>;
94
+ triggerAction(sessionId: string, name: string): Promise<{status: string}>;
66
95
  eventsUrl(sessionId: string): string;
67
- events(sessionId: string, opts?: EventsOptions): AsyncGenerator<SessionEvent, void, void>;
68
-
69
- listCharacters(): Promise<{ characters: Character[] }>;
70
- createCharacter(params: {
71
- name: string;
72
- visualCharacterId?: string;
73
- systemPrompt?: string;
74
- voiceId?: string;
75
- languageCode?: string;
76
- }): Promise<Character>;
77
- getCharacter(characterId: string): Promise<Character>;
78
- deleteCharacter(characterId: string): Promise<{ id: string; status: string }>;
96
+ connect(sessionId: string, options?: EventsOptions): Promise<RealtimeSession>;
97
+ events(sessionId: string, options?: EventsOptions): AsyncGenerator<SessionEvent, void, void>;
98
+ capabilities(): Promise<Capabilities>;
99
+ billing(): Promise<Record<string, unknown>>;
100
+ listAvatars(): Promise<Record<string, unknown>>;
101
+ listVoices(): Promise<Record<string, unknown>>;
102
+ listLanguages(): Promise<Record<string, unknown>>;
103
+ updateContext(sessionId: string, context: PresentationContext): Promise<Record<string, unknown>>;
104
+ startPresentation(sessionId: string, start: PresentationStart): Promise<Record<string, unknown>>;
105
+ speech(sessionId: string, type: string, payload: Record<string, unknown>): Promise<Record<string, unknown>>;
106
+ startSpeech(sessionId: string, turnId: string, options?: {text?: string}): Promise<Record<string, unknown>>;
107
+ appendSpeech(sessionId: string, turnId: string, seq: number, startSample: number, pcm: PCMBytes): Promise<Record<string, unknown>>;
108
+ finishSpeech(sessionId: string, turnId: string, totalSamples: number): Promise<Record<string, unknown>>;
109
+ cancelSpeech(sessionId: string, turnId: string): Promise<Record<string, unknown>>;
110
+ listPresets(): Promise<{presets: Preset[]}>;
111
+ createPreset(params: {name: string; avatarId?: string; visualCharacterId?: string; systemPrompt?: string; voiceId?: string; languageCode?: string}): Promise<Preset>;
112
+ /** Patch uses the REST API's snake_case field names. */
113
+ updatePreset(presetId: string, patch: Record<string, unknown>): Promise<Preset>;
114
+ getPreset(presetId: string): Promise<Preset>;
115
+ deletePreset(presetId: string): Promise<{id: string; status: string}>;
116
+ listInstances(): Promise<{instances: Instance[]}>;
79
117
  }
80
-
81
118
  export default Anva;
package/index.js CHANGED
@@ -1,24 +1,28 @@
1
1
  /**
2
2
  * Official JavaScript/TypeScript SDK for Anva (https://anva.ai).
3
3
  *
4
- * import { Anva } from "anva";
4
+ * import { Anva } from "anva-sdk";
5
5
  * const client = new Anva(process.env.ANVA_KEY);
6
6
  * const session = await client.createSession({ presetId: "..." });
7
7
  * // put session.embed_url in an <iframe allow="camera; microphone; autoplay">
8
8
  *
9
- * Works in Node 18+ (global fetch/WebSocket) and modern browsers — but keep
9
+ * Works in Node 18+ (supply a WebSocket implementation before Node 22) and modern browsers — but keep
10
10
  * your API key server-side; mint sessions on your backend and hand the
11
11
  * embed_url to the client.
12
12
  */
13
13
 
14
+ import { RealtimeSession, speechStart, speechAppend } from "./realtime.js";
15
+ export { RealtimeSession } from "./realtime.js";
16
+
14
17
  const DEFAULT_BASE_URL = "https://anva.ai";
15
18
 
16
19
  export class AnvaError extends Error {
17
- constructor(status, code, message) {
20
+ constructor(status, code, message, details = {}) {
18
21
  super(`${code}: ${message} (HTTP ${status})`);
19
22
  this.name = "AnvaError";
20
23
  this.status = status;
21
24
  this.code = code;
25
+ this.details = details;
22
26
  }
23
27
  }
24
28
 
@@ -48,9 +52,9 @@ export class Anva {
48
52
  * webhookUrl?: string, webhookSecret?: string}} params
49
53
  */
50
54
  createSession(params = {}) {
51
- const { presetId, avatarId, systemPrompt, voiceId, languageCode, llmMode, webhookUrl, webhookSecret } = params;
52
- if (!presetId && !avatarId) {
53
- throw new Error("createSession requires either presetId (embed) or avatarId (advanced persona)");
55
+ const { presetId, avatarId, systemPrompt, voiceId, languageCode, llmMode, serviceMode, performanceOptions, conversationProvider, performanceMode, elevenlabsAgentId, dynamicExpressions, webhookUrl, webhookSecret } = params;
56
+ if ((!presetId && !avatarId) || (presetId && avatarId)) {
57
+ throw new Error("createSession requires exactly one of presetId or avatarId");
54
58
  }
55
59
  const body = {};
56
60
  if (presetId) body.preset_id = presetId;
@@ -58,7 +62,13 @@ export class Anva {
58
62
  if (systemPrompt) body.system_prompt = systemPrompt;
59
63
  if (voiceId) body.voice_id = voiceId;
60
64
  if (languageCode) body.language_code = languageCode;
61
- if (llmMode) body.llm_mode = llmMode;
65
+ if (llmMode !== undefined) body.llm_mode = llmMode;
66
+ if (serviceMode !== undefined) body.service_mode = serviceMode;
67
+ if (performanceOptions !== undefined) body.performance_options = performanceOptions;
68
+ if (conversationProvider !== undefined) body.conversation_provider = conversationProvider;
69
+ if (performanceMode !== undefined) body.performance_mode = performanceMode;
70
+ if (elevenlabsAgentId !== undefined) body.elevenlabs_agent_id = elevenlabsAgentId;
71
+ if (dynamicExpressions !== undefined) body.dynamic_expressions = dynamicExpressions;
62
72
  if (webhookUrl) body.webhook_url = webhookUrl;
63
73
  if (webhookSecret) body.webhook_secret = webhookSecret;
64
74
  return this._request("POST", "/api/v2/sessions", body);
@@ -72,7 +82,8 @@ export class Anva {
72
82
  return this._request("DELETE", `/api/v2/sessions/${enc(sessionId)}`);
73
83
  }
74
84
 
75
- /** Have the avatar speak `text` to the user. */
85
+ /** Send `text` as a user message; the avatar hears it and replies (it does
86
+ * NOT speak `text` verbatim). For external replies, use serviceMode:"byo_llm" and turnDelta/turnDone on a realtime connection. */
76
87
  sendMessage(sessionId, text) {
77
88
  return this._request("POST", `/api/v2/sessions/${enc(sessionId)}/messages`, { text });
78
89
  }
@@ -92,54 +103,29 @@ export class Anva {
92
103
  return `${ws}/api/v2/sessions/${enc(sessionId)}/events?api_key=${encodeURIComponent(this.apiKey)}`;
93
104
  }
94
105
 
95
- /**
96
- * Async-iterate live events (transcripts, state changes):
97
- *
98
- * for await (const event of client.events(sessionId)) { ... }
99
- */
100
- async *events(sessionId, { WebSocketImpl } = {}) {
106
+ /** Open a single socket for both events and turn/presentation/speech commands. */
107
+ async connect(sessionId, { WebSocketImpl } = {}) {
101
108
  const WS = WebSocketImpl || globalThis.WebSocket;
102
- if (!WS) throw new Error("no WebSocket implementation available — pass { WebSocketImpl }");
103
- const ws = new WS(this.eventsUrl(sessionId));
104
- const queue = [];
105
- let notify = null;
106
- let done = false;
107
- let failure = null;
108
- ws.onmessage = (m) => {
109
- try {
110
- queue.push(JSON.parse(m.data));
111
- } catch {
112
- /* non-JSON frame */
113
- }
114
- if (notify) notify();
115
- };
116
- ws.onclose = () => {
117
- done = true;
118
- if (notify) notify();
119
- };
120
- ws.onerror = () => {
121
- failure = new Error("events socket error");
122
- done = true;
123
- if (notify) notify();
124
- };
125
- try {
126
- while (!done || queue.length) {
127
- if (!queue.length) {
128
- await new Promise((resolve) => (notify = resolve));
129
- notify = null;
130
- continue;
131
- }
132
- yield queue.shift();
133
- }
134
- if (failure) throw failure;
135
- } finally {
136
- try {
137
- ws.close();
138
- } catch {
139
- /* already closed */
140
- }
141
- }
142
- }
109
+ if (!WS) throw new Error('No WebSocket implementation; pass { WebSocketImpl }');
110
+ const stream = new RealtimeSession(new WS(this.eventsUrl(sessionId)));
111
+ try { await stream.ready; return stream; } catch (error) { stream.close(); throw error; }
112
+ }
113
+ async *events(sessionId, options = {}) {
114
+ const stream = await this.connect(sessionId, options);
115
+ try { yield* stream; } finally { stream.close(); }
116
+ }
117
+ capabilities() { return this._request('GET', '/api/v2/capabilities'); }
118
+ billing() { return this._request('GET', '/api/v2/billing'); }
119
+ listAvatars() { return this._request('GET', '/api/v2/avatars'); }
120
+ listVoices() { return this._request('GET', '/api/v2/voices'); }
121
+ listLanguages() { return this._request('GET', '/api/v2/languages'); }
122
+ updateContext(sessionId, context) { return this._request('POST', `/api/v2/sessions/${enc(sessionId)}/context`, context); }
123
+ startPresentation(sessionId, start) { return this._request('POST', `/api/v2/sessions/${enc(sessionId)}/presentation`, start); }
124
+ speech(sessionId, type, payload) { return this._request('POST', `/api/v2/sessions/${enc(sessionId)}/speech`, { type, payload }); }
125
+ startSpeech(sessionId, turnId, { text } = {}) { return this.speech(sessionId, 'speech.start', speechStart(turnId, text)); }
126
+ appendSpeech(sessionId, turnId, seq, startSample, pcm) { return this.speech(sessionId, 'speech.append', speechAppend(turnId, seq, startSample, pcm)); }
127
+ finishSpeech(sessionId, turnId, totalSamples) { return this.speech(sessionId, 'speech.done', { turn_id: turnId, total_samples: totalSamples }); }
128
+ cancelSpeech(sessionId, turnId) { return this.speech(sessionId, 'speech.cancel', { turn_id: turnId }); }
143
129
 
144
130
  // -- presets --------------------------------------------------------------
145
131
 
@@ -147,9 +133,9 @@ export class Anva {
147
133
  return this._request("GET", "/api/v2/presets");
148
134
  }
149
135
 
150
- createPreset({ name, visualCharacterId, systemPrompt, voiceId, languageCode }) {
136
+ createPreset({ name, avatarId, visualCharacterId, systemPrompt, voiceId, languageCode }) {
151
137
  const body = { name };
152
- if (visualCharacterId) body.visual_character_id = visualCharacterId;
138
+ if (avatarId || visualCharacterId) body.avatar_id = avatarId || visualCharacterId;
153
139
  if (systemPrompt) body.system_prompt = systemPrompt;
154
140
  if (voiceId) body.voice_id = voiceId;
155
141
  if (languageCode) body.language_code = languageCode;
@@ -160,6 +146,10 @@ export class Anva {
160
146
  return this._request("GET", `/api/v2/presets/${enc(presetId)}`);
161
147
  }
162
148
 
149
+ updatePreset(presetId, patch) {
150
+ return this._request("PATCH", `/api/v2/presets/${enc(presetId)}`, patch);
151
+ }
152
+
163
153
  deletePreset(presetId) {
164
154
  return this._request("DELETE", `/api/v2/presets/${enc(presetId)}`);
165
155
  }
@@ -178,7 +168,7 @@ export class Anva {
178
168
  headers: {
179
169
  Authorization: `Bearer ${this.apiKey}`,
180
170
  "Content-Type": "application/json",
181
- "User-Agent": "anva-js/0.2.0",
171
+ "User-Agent": "anva-js/0.3.0",
182
172
  },
183
173
  body: body === undefined ? undefined : JSON.stringify(body),
184
174
  });
@@ -190,8 +180,8 @@ export class Anva {
190
180
  payload = { message: raw.slice(0, 300) };
191
181
  }
192
182
  if (!res.ok) {
193
- const err = payload.error || payload;
194
- throw new AnvaError(res.status, err.code || "request_failed", err.message || "request failed");
183
+ const err = payload?.error || payload || {};
184
+ throw new AnvaError(res.status, err.code || "request_failed", err.message || "request failed", err);
195
185
  }
196
186
  return payload;
197
187
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anva-sdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Official JavaScript/TypeScript SDK for Anva \u2014 live AI avatars for your product.",
5
5
  "keywords": [
6
6
  "avatar",
@@ -30,9 +30,13 @@
30
30
  "files": [
31
31
  "index.js",
32
32
  "index.d.ts",
33
- "README.md"
33
+ "README.md",
34
+ "realtime.js"
34
35
  ],
35
36
  "engines": {
36
37
  "node": ">=18"
38
+ },
39
+ "scripts": {
40
+ "test": "node --test test/*.test.js"
37
41
  }
38
- }
42
+ }
package/realtime.js ADDED
@@ -0,0 +1,78 @@
1
+ /** A single bidirectional events connection. Keep the API key on your server. */
2
+ export class RealtimeSession {
3
+ constructor(socket) {
4
+ this.socket = socket;
5
+ this.queue = [];
6
+ this.waiters = [];
7
+ this.closed = false;
8
+ this.closeRequested = false;
9
+ this.failure = null;
10
+ this.ready = new Promise((resolve, reject) => {
11
+ this.resolveReady = resolve;
12
+ this.rejectReady = reject;
13
+ });
14
+ socket.onopen = () => this.resolveReady(this);
15
+ socket.onmessage = event => {
16
+ try { this.queue.push(JSON.parse(event.data)); } catch { return; }
17
+ this.wake();
18
+ };
19
+ socket.onclose = event => {
20
+ this.closed = true;
21
+ if (event.code !== 1000 && event.code !== undefined) this.failure ||= new Error(`Events socket closed (${event.code})`);
22
+ this.rejectReady(this.failure || new Error('Events socket closed before opening'));
23
+ this.wake();
24
+ };
25
+ socket.onerror = () => {
26
+ this.failure = new Error('Events socket error');
27
+ this.closed = true;
28
+ this.rejectReady(this.failure);
29
+ this.close();
30
+ this.wake();
31
+ };
32
+ if (socket.readyState === 1) this.resolveReady(this);
33
+ }
34
+ wake() { for (const resolve of this.waiters.splice(0)) resolve(); }
35
+ async send(type, payload = {}) {
36
+ await this.ready;
37
+ if (this.closed || this.socket.readyState !== 1) throw new Error('Events socket is not open');
38
+ this.socket.send(JSON.stringify({ type, payload }));
39
+ }
40
+ message(text) { return this.send('message', { text }); }
41
+ interrupt() { return this.send('interrupt'); }
42
+ updateContext(context) { return this.send('context.update', context); }
43
+ startPresentation(start) { return this.send('presentation.start', start); }
44
+ turnDelta(turnId, text) { return this.send('turn.delta', { turn_id: turnId, text }); }
45
+ turnDone(turnId) { return this.send('turn.done', { turn_id: turnId }); }
46
+ turnCancel(turnId, reason = '') { return this.send('turn.cancel', { turn_id: turnId, reason }); }
47
+ startSpeech(turnId, { text } = {}) { return this.send('speech.start', speechStart(turnId, text)); }
48
+ appendSpeech(turnId, seq, startSample, pcm) { return this.send('speech.append', speechAppend(turnId, seq, startSample, pcm)); }
49
+ finishSpeech(turnId, totalSamples) { return this.send('speech.done', { turn_id: turnId, total_samples: totalSamples }); }
50
+ cancelSpeech(turnId) { return this.send('speech.cancel', { turn_id: turnId }); }
51
+ close() {
52
+ if (this.closeRequested) return;
53
+ this.closeRequested = true;
54
+ this.closed = true;
55
+ this.rejectReady(new Error('Events socket closed before opening'));
56
+ try { this.socket.close(1000); } finally { this.wake(); }
57
+ }
58
+ async *[Symbol.asyncIterator]() {
59
+ await this.ready;
60
+ try {
61
+ while (!this.closed || this.queue.length) {
62
+ if (!this.queue.length) { await new Promise(resolve => this.waiters.push(resolve)); continue; }
63
+ yield this.queue.shift();
64
+ }
65
+ if (this.failure) throw this.failure;
66
+ } finally { this.close(); }
67
+ }
68
+ }
69
+ export const speechStart = (turnId, text) => ({ turn_id: turnId, codec: 'pcm_s16le', sample_rate: 24000, channels: 1, ...(text === undefined ? {} : { text }) });
70
+ export function speechAppend(turnId, seq, startSample, pcm) {
71
+ const bytes = pcm instanceof Uint8Array ? pcm : pcm instanceof ArrayBuffer ? new Uint8Array(pcm) : null;
72
+ if (!bytes || bytes.length === 0 || bytes.length % 2 || bytes.length > 24000) throw new Error('PCM chunk must contain 1–12000 signed 16-bit samples');
73
+ if (!Number.isSafeInteger(seq) || seq < 0 || !Number.isSafeInteger(startSample) || startSample < 0) throw new Error('seq and startSample must be nonnegative integers');
74
+ let binary = '';
75
+ for (const byte of bytes) binary += String.fromCharCode(byte);
76
+ const data = typeof globalThis.btoa === 'function' ? globalThis.btoa(binary) : globalThis.Buffer.from(bytes).toString('base64');
77
+ return { turn_id: turnId, seq, start_sample: startSample, data };
78
+ }