@alexkroman1/aai-ui 1.8.3 → 1.9.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.
Files changed (62) hide show
  1. package/dist/_colors-DJUordGv.js +19 -0
  2. package/dist/_utils-5cs73OrA.js +16 -0
  3. package/dist/_utils.d.ts +4 -0
  4. package/dist/aai-logo-B8lDmsut.js +84 -0
  5. package/dist/audio.d.ts +9 -2
  6. package/dist/audio.js +41 -24
  7. package/dist/chat-view-C1XbqDk0.js +186 -0
  8. package/dist/components/_colors.d.ts +26 -0
  9. package/dist/components/aai-logo.d.ts +7 -6
  10. package/dist/components/button.d.ts +11 -13
  11. package/dist/components/button.js +7 -16
  12. package/dist/components/chat-view.d.ts +8 -19
  13. package/dist/components/chat-view.js +2 -98
  14. package/dist/components/controls.d.ts +1 -1
  15. package/dist/components/controls.js +2 -39
  16. package/dist/components/eyebrow.d.ts +12 -0
  17. package/dist/components/message-list.d.ts +1 -1
  18. package/dist/components/message-list.js +126 -56
  19. package/dist/components/sidebar-layout.d.ts +1 -1
  20. package/dist/components/start-screen.d.ts +4 -3
  21. package/dist/components/start-screen.js +20 -14
  22. package/dist/components/text-controls.d.ts +14 -0
  23. package/dist/components/tool-call-block.d.ts +5 -3
  24. package/dist/components/tool-call-block.js +1 -1
  25. package/dist/components/url-chips.d.ts +28 -0
  26. package/dist/context.d.ts +30 -4
  27. package/dist/context.js +86 -10
  28. package/dist/controls-BngrPbOC.js +135 -0
  29. package/dist/default-client/assets/audio-Cs-6t_Wd.js +1 -0
  30. package/dist/default-client/assets/capture-processor-C19oBn4L.js +105 -0
  31. package/dist/default-client/assets/index-Bf4ZTNcx.js +73 -0
  32. package/dist/default-client/assets/index-Bzlh9i7w.css +2 -0
  33. package/dist/default-client/assets/playback-processor-BtlzAH78.js +149 -0
  34. package/dist/default-client/index.html +3 -3
  35. package/dist/define-client.d.ts +2 -2
  36. package/dist/define-client.js +18 -24
  37. package/dist/eyebrow-C6ZFuiz6.js +27 -0
  38. package/dist/hooks.js +71 -42
  39. package/dist/index.d.ts +3 -1
  40. package/dist/index.js +6 -5
  41. package/dist/session-core-D3NDaySY.js +652 -0
  42. package/dist/session-core-messages.d.ts +50 -0
  43. package/dist/session-core-types.d.ts +147 -0
  44. package/dist/session-core-upload.d.ts +16 -0
  45. package/dist/session-core.d.ts +2 -68
  46. package/dist/session-core.js +1 -466
  47. package/dist/{tool-call-block-wby_jyoY.js → tool-call-block-7f1GTG-P.js} +33 -31
  48. package/dist/types.d.ts +29 -4
  49. package/dist/types.js +10 -1
  50. package/dist/worklets/capture-processor.d.ts +2 -0
  51. package/dist/worklets/capture-processor.js +80 -25
  52. package/dist/worklets/playback-processor.d.ts +2 -0
  53. package/dist/worklets/playback-processor.js +80 -29
  54. package/package.json +14 -14
  55. package/styles.css +30 -0
  56. package/dist/_react-test-utils.d.ts +0 -86
  57. package/dist/aai-logo-BqFv6JU6.js +0 -21
  58. package/dist/default-client/assets/audio-UowwhmYo.js +0 -1
  59. package/dist/default-client/assets/capture-processor-C26lSiVr.js +0 -53
  60. package/dist/default-client/assets/index-CSyVJsy5.css +0 -2
  61. package/dist/default-client/assets/index-D1MWTIM_.js +0 -74
  62. package/dist/default-client/assets/playback-processor-dSA8Im99.js +0 -101
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Type declarations for the framework-agnostic voice session core.
3
+ *
4
+ * Split out of `session-core.ts` to keep that module focused on behaviour.
5
+ * The public types here are re-exported from `session-core.ts` for
6
+ * backwards compatibility.
7
+ */
8
+ import type { VoiceIO } from "./audio.ts";
9
+ import type { AgentState, ChatMessage, SessionError, ToolCallInfo, VoiceSessionOptions, WebSocketConstructor } from "./types.ts";
10
+ /**
11
+ * A custom event emitted by the agent via `ctx.send`.
12
+ *
13
+ * @public
14
+ */
15
+ export type CustomEvent = {
16
+ readonly id: number;
17
+ readonly event: string;
18
+ readonly data: unknown;
19
+ };
20
+ /**
21
+ * Immutable snapshot of the session state.
22
+ *
23
+ * Consumers (e.g. React hooks via `useSyncExternalStore`) read this to render.
24
+ * A new object reference is created on every state change.
25
+ *
26
+ * @public
27
+ */
28
+ export type SessionSnapshot = {
29
+ readonly state: AgentState;
30
+ /**
31
+ * False when the server declared the session text-only (`tts: none()`):
32
+ * no audio frames will arrive and replies render as text. True until the
33
+ * server's `config` message says otherwise (voice is the default).
34
+ */
35
+ readonly audioOut: boolean;
36
+ /**
37
+ * True while the microphone is live and streaming to the server. Voice
38
+ * sessions record for their whole lifetime; text-only sessions toggle this
39
+ * via `startRecording()` / `stopRecording()` (the record button).
40
+ */
41
+ readonly recording: boolean;
42
+ /**
43
+ * The WebSocket URL a program can connect to directly (the same endpoint
44
+ * this session uses), e.g. `wss://host/my-agent/websocket`. Derived from
45
+ * `platformUrl` at construction — available before connecting.
46
+ */
47
+ readonly apiUrl: string;
48
+ /**
49
+ * Monotonically increasing counter bumped whenever rendered conversation
50
+ * content changes (`messages`, `toolCalls`, or either live transcript).
51
+ * Cheap dependency for scroll-to-bottom effects — unlike summed lengths it
52
+ * never collides when the capped arrays slide.
53
+ */
54
+ readonly contentVersion: number;
55
+ readonly messages: ChatMessage[];
56
+ readonly toolCalls: ToolCallInfo[];
57
+ readonly customEvents: CustomEvent[];
58
+ readonly userTranscript: string | null;
59
+ readonly agentTranscript: string | null;
60
+ readonly error: SessionError | null;
61
+ readonly started: boolean;
62
+ readonly running: boolean;
63
+ };
64
+ /**
65
+ * A framework-agnostic voice session that manages WebSocket communication,
66
+ * audio capture/playback, and agent state transitions.
67
+ *
68
+ * Uses a subscribe/getSnapshot pattern (compatible with React's
69
+ * `useSyncExternalStore`). Implements `Disposable` for resource cleanup.
70
+ *
71
+ * @public
72
+ */
73
+ export type SessionCore = {
74
+ /** Return the current immutable state snapshot. */
75
+ getSnapshot(): SessionSnapshot;
76
+ /** Subscribe to state changes. Returns an unsubscribe function. */
77
+ subscribe(callback: () => void): () => void;
78
+ /**
79
+ * Open a WebSocket connection to the server and begin audio capture.
80
+ * @param options - Optional. `signal` is an AbortSignal that, when aborted, disconnects the session.
81
+ */
82
+ connect(options?: {
83
+ signal?: AbortSignal;
84
+ }): void;
85
+ /** Cancel the current agent turn and discard in-flight TTS audio. */
86
+ cancel(): void;
87
+ /** Clear messages, transcript, and error state without disconnecting. */
88
+ resetState(): void;
89
+ /** Reset the session: clear state and reconnect. */
90
+ reset(): void;
91
+ /** Close the WebSocket and release all audio resources. */
92
+ disconnect(): void;
93
+ /** Start the session for the first time (sets `started` and `running`). */
94
+ start(): void;
95
+ /** Toggle between connected and disconnected states. */
96
+ toggle(): void;
97
+ /**
98
+ * Start streaming microphone audio (text-only sessions). Requests mic
99
+ * access on first use. No-op in voice sessions, where the mic is always on.
100
+ */
101
+ startRecording(): void;
102
+ /** Stop streaming microphone audio (text-only sessions). No-op in voice sessions. */
103
+ stopRecording(): void;
104
+ /**
105
+ * Decode an audio file (any format the browser can decode), resample it to
106
+ * the session's STT rate, and stream it to the server for transcription.
107
+ * Text-only sessions (`tts: none()`) only — voice sessions stream the
108
+ * microphone instead and reject. Also rejects when no session is connected,
109
+ * the mic is recording, another upload is in flight, or the file cannot be
110
+ * decoded. Resolves once the audio has been handed to the socket.
111
+ */
112
+ sendAudioFile(file: Blob): Promise<void>;
113
+ /** Alias for `disconnect` for use with `using`. */
114
+ [Symbol.dispose](): void;
115
+ };
116
+ export type SessionCoreOptions = VoiceSessionOptions;
117
+ /**
118
+ * Shared mutable connection state for audio initialization.
119
+ *
120
+ * Tracks the active WebSocket, VoiceIO instance, and a generation counter
121
+ * that prevents stale async operations (e.g. a slow `initAudioCapture`) from
122
+ * assigning their results to a newer connection after a reconnect.
123
+ */
124
+ export type ConnState = {
125
+ ws: InstanceType<WebSocketConstructor> | null;
126
+ voiceIO: VoiceIO | null;
127
+ audioSetupInFlight: boolean;
128
+ /** Monotonically increasing counter bumped on each connect(). Prevents a stale
129
+ * initAudioCapture from assigning its voiceIO to a newer connection. */
130
+ generation: number;
131
+ /** Audio chunks that arrived before `voiceIO` was initialized — drained into
132
+ * the playback worklet once init completes. Closes the race between the
133
+ * server starting greeting audio (immediately on S2S connect) and the
134
+ * client awaiting mic permission + worklet registration. */
135
+ preInitAudio: Uint8Array[];
136
+ /** True if `audio_done` arrived before `voiceIO` was initialized. The done
137
+ * signal must be replayed after draining preInitAudio, or a short greeting
138
+ * buffered during mic-permission never finishes playing. */
139
+ preInitDone: boolean;
140
+ /** The server's `config` payload for the current connection — kept so
141
+ * text-only sessions can init the mic lazily (record button) and file
142
+ * uploads know the STT sample rate to resample to. */
143
+ readyConfig: {
144
+ sampleRate: number;
145
+ ttsSampleRate: number;
146
+ } | null;
147
+ };
@@ -0,0 +1,16 @@
1
+ import type { ClientMessage } from "@alexkroman1/aai/protocol";
2
+ import type { ConnState, SessionSnapshot } from "./session-core-types.ts";
3
+ /** Handle to the upload pipeline, owned by one session core. */
4
+ export type UploadSender = {
5
+ /** See {@link SessionCore.sendAudioFile}. */
6
+ sendAudioFile(file: Blob): Promise<void>;
7
+ /** True while an upload is decoding or streaming — the mic must stay off. */
8
+ inFlight(): boolean;
9
+ /** Invalidate any in-flight upload (reset / close / reconnect). */
10
+ discard(): void;
11
+ };
12
+ export declare function createUploadSender(deps: {
13
+ conn: ConnState;
14
+ getSnapshot: () => SessionSnapshot;
15
+ sendJson: (msg: ClientMessage) => void;
16
+ }): UploadSender;
@@ -1,71 +1,5 @@
1
- import type { AgentState, ChatMessage, SessionError, ToolCallInfo, VoiceSessionOptions } from "./types.ts";
2
- export type { AgentState, ChatMessage, SessionError, SessionErrorCode, ToolCallInfo, VoiceSessionOptions, WebSocketConstructor, } from "./types.ts";
3
- /**
4
- * A custom event emitted by the agent via `ctx.send`.
5
- *
6
- * @public
7
- */
8
- export type CustomEvent = {
9
- readonly id: number;
10
- readonly event: string;
11
- readonly data: unknown;
12
- };
13
- /**
14
- * Immutable snapshot of the session state.
15
- *
16
- * Consumers (e.g. React hooks via `useSyncExternalStore`) read this to render.
17
- * A new object reference is created on every state change.
18
- *
19
- * @public
20
- */
21
- export type SessionSnapshot = {
22
- readonly state: AgentState;
23
- readonly messages: ChatMessage[];
24
- readonly toolCalls: ToolCallInfo[];
25
- readonly customEvents: CustomEvent[];
26
- readonly userTranscript: string | null;
27
- readonly agentTranscript: string | null;
28
- readonly error: SessionError | null;
29
- readonly started: boolean;
30
- readonly running: boolean;
31
- };
32
- /**
33
- * A framework-agnostic voice session that manages WebSocket communication,
34
- * audio capture/playback, and agent state transitions.
35
- *
36
- * Uses a subscribe/getSnapshot pattern (compatible with React's
37
- * `useSyncExternalStore`). Implements `Disposable` for resource cleanup.
38
- *
39
- * @public
40
- */
41
- export type SessionCore = {
42
- /** Return the current immutable state snapshot. */
43
- getSnapshot(): SessionSnapshot;
44
- /** Subscribe to state changes. Returns an unsubscribe function. */
45
- subscribe(callback: () => void): () => void;
46
- /**
47
- * Open a WebSocket connection to the server and begin audio capture.
48
- * @param options - Optional. `signal` is an AbortSignal that, when aborted, disconnects the session.
49
- */
50
- connect(options?: {
51
- signal?: AbortSignal;
52
- }): void;
53
- /** Cancel the current agent turn and discard in-flight TTS audio. */
54
- cancel(): void;
55
- /** Clear messages, transcript, and error state without disconnecting. */
56
- resetState(): void;
57
- /** Reset the session: clear state and reconnect. */
58
- reset(): void;
59
- /** Close the WebSocket and release all audio resources. */
60
- disconnect(): void;
61
- /** Start the session for the first time (sets `started` and `running`). */
62
- start(): void;
63
- /** Toggle between connected and disconnected states. */
64
- toggle(): void;
65
- /** Alias for `disconnect` for use with `using`. */
66
- [Symbol.dispose](): void;
67
- };
68
- export type SessionCoreOptions = VoiceSessionOptions;
1
+ import type { SessionCore, SessionCoreOptions } from "./session-core-types.ts";
2
+ export type { CustomEvent, SessionCore, SessionCoreOptions, SessionSnapshot, } from "./session-core-types.ts";
69
3
  /**
70
4
  * Create a framework-agnostic voice session core that connects to an AAI
71
5
  * server via WebSocket.
@@ -1,467 +1,2 @@
1
- import { WS_OPEN, errorMessage } from "@alexkroman1/aai";
2
- import { ServerMessageSchema, lenientParse } from "@alexkroman1/aai/protocol";
3
- //#region session-core.ts
4
- /**
5
- * Framework-agnostic voice session core.
6
- *
7
- * Manages WebSocket communication, audio capture/playback, and agent state
8
- * transitions using a subscribe/getSnapshot pattern compatible with React's
9
- * `useSyncExternalStore` and other external store consumers.
10
- *
11
- * No dependency on React, Preact, or any UI framework.
12
- */
13
- /** Cap on `customEvents` retained in the session snapshot to avoid unbounded growth. */
14
- const MAX_CUSTOM_EVENTS = 200;
15
- /** Cap on `messages` retained in the session snapshot; mirrors host-side DEFAULT_MAX_HISTORY. */
16
- const MAX_MESSAGES = 200;
17
- /** Cap on pre-init audio chunks buffered while `voiceIO` is initializing. ~100 chunks at
18
- * typical S2S chunk sizes is well over a second of audio — far longer than init takes
19
- * in practice, but bounded against pathological cases (mic-permission stalls). */
20
- const MAX_PREINIT_AUDIO_CHUNKS = 100;
21
- function appendCapped(list, item, cap) {
22
- const next = [...list, item];
23
- return next.length > cap ? next.slice(-cap) : next;
24
- }
25
- /**
26
- * Initialize audio capture and playback after the server sends a ready config.
27
- *
28
- * Lifecycle: dynamically import audio modules -> request microphone access ->
29
- * register AudioWorklet processors -> create a `VoiceIO` instance -> send
30
- * `audio_ready` to the server -> transition state to `"listening"`.
31
- *
32
- * Uses the connection `generation` counter to detect if `connect()` was called
33
- * while awaiting async operations; if so, the stale VoiceIO is closed immediately
34
- * to prevent it from being assigned to a newer connection.
35
- *
36
- * On failure (e.g. microphone permission denied, WebSocket closed mid-setup),
37
- * sets the error state and transitions to `"disconnected"`.
38
- */
39
- async function initAudioCapture(conn, msg, deps) {
40
- if (conn.audioSetupInFlight) return;
41
- conn.audioSetupInFlight = true;
42
- const gen = conn.generation;
43
- try {
44
- const [{ createVoiceIO }, captureWorklet, playbackWorklet] = await Promise.all([
45
- import("./audio.js"),
46
- import("./worklets/capture-processor.js").then((m) => m.default),
47
- import("./worklets/playback-processor.js").then((m) => m.default)
48
- ]);
49
- const io = await createVoiceIO({
50
- sttSampleRate: msg.sampleRate,
51
- ttsSampleRate: msg.ttsSampleRate,
52
- captureWorkletSrc: captureWorklet,
53
- playbackWorkletSrc: playbackWorklet,
54
- onMicData: (pcm16) => {
55
- try {
56
- deps.sendAudio(new Uint8Array(pcm16));
57
- } catch {
58
- console.debug("[aai-ui] sendAudio dropped: connection closed");
59
- }
60
- }
61
- });
62
- if (conn.generation !== gen || !conn.ws || conn.ws.readyState !== WS_OPEN) {
63
- io.close();
64
- return;
65
- }
66
- conn.voiceIO = io;
67
- if (conn.preInitAudio.length > 0) {
68
- for (const chunk of conn.preInitAudio) io.enqueue(chunk.buffer);
69
- conn.preInitAudio = [];
70
- }
71
- deps.sendJson({ type: "audio_ready" });
72
- deps.updateState({ state: "listening" });
73
- } catch (err) {
74
- if (conn.generation !== gen || !conn.ws || conn.ws.readyState !== WS_OPEN) return;
75
- deps.updateState({
76
- state: "error",
77
- error: {
78
- code: "audio",
79
- message: `Microphone access failed: ${errorMessage(err)}`
80
- },
81
- running: false
82
- });
83
- } finally {
84
- conn.audioSetupInFlight = false;
85
- }
86
- }
87
- function buildWsUrl(platformUrl, resume, sessionId) {
88
- const wsUrl = new URL("websocket", platformUrl.endsWith("/") ? platformUrl : `${platformUrl}/`);
89
- wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
90
- if (sessionId) wsUrl.searchParams.set("sessionId", sessionId);
91
- else if (resume) wsUrl.searchParams.set("resume", "1");
92
- return wsUrl;
93
- }
94
- /**
95
- * Create a framework-agnostic voice session core that connects to an AAI
96
- * server via WebSocket.
97
- *
98
- * Uses a subscribe/getSnapshot pattern for state management, compatible with
99
- * React's `useSyncExternalStore` and other external store integrations.
100
- *
101
- * @param options - Session configuration including the platform server URL.
102
- * @returns A {@link SessionCore} handle for controlling the session.
103
- *
104
- * @public
105
- */
106
- function createSessionCore(options) {
107
- const WS = options.WebSocket ?? WebSocket;
108
- let currentSnapshot = {
109
- state: "disconnected",
110
- messages: [],
111
- toolCalls: [],
112
- customEvents: [],
113
- userTranscript: null,
114
- agentTranscript: null,
115
- error: null,
116
- started: false,
117
- running: false
118
- };
119
- const subscribers = /* @__PURE__ */ new Set();
120
- function notify() {
121
- for (const sub of subscribers) sub();
122
- }
123
- function updateState(partial) {
124
- currentSnapshot = {
125
- ...currentSnapshot,
126
- ...partial
127
- };
128
- notify();
129
- }
130
- function getSnapshot() {
131
- return currentSnapshot;
132
- }
133
- function subscribe(callback) {
134
- subscribers.add(callback);
135
- return () => {
136
- subscribers.delete(callback);
137
- };
138
- }
139
- const conn = {
140
- ws: null,
141
- voiceIO: null,
142
- audioSetupInFlight: false,
143
- generation: 0,
144
- preInitAudio: []
145
- };
146
- let connectionController = null;
147
- let hasConnected = false;
148
- function cleanupAudio() {
149
- conn.audioSetupInFlight = false;
150
- conn.voiceIO?.close();
151
- conn.voiceIO = null;
152
- conn.preInitAudio = [];
153
- }
154
- function resetState() {
155
- updateState({
156
- messages: [],
157
- toolCalls: [],
158
- customEvents: [],
159
- userTranscript: null,
160
- agentTranscript: null,
161
- error: null
162
- });
163
- }
164
- function sendJson(msg) {
165
- if (conn.ws && conn.ws.readyState === WS_OPEN) conn.ws.send(JSON.stringify(msg));
166
- }
167
- function sendAudio(bytes) {
168
- if (conn.ws && conn.ws.readyState === WS_OPEN) conn.ws.send(bytes);
169
- }
170
- const audioDeps = {
171
- sendJson,
172
- sendAudio,
173
- updateState
174
- };
175
- /** Incremented on each turn boundary -- stale async callbacks compare against this. */
176
- let handlerGeneration = 0;
177
- /** Monotonically increasing counter for custom events -- used by useEvent to deduplicate. */
178
- let customEventSeq = 0;
179
- function appendCustomEvent(name, data) {
180
- updateState({ customEvents: appendCapped(currentSnapshot.customEvents, {
181
- id: ++customEventSeq,
182
- event: name,
183
- data
184
- }, MAX_CUSTOM_EVENTS) });
185
- }
186
- function handleUserTranscriptEvent(text) {
187
- handlerGeneration++;
188
- updateState({
189
- userTranscript: null,
190
- messages: appendCapped(currentSnapshot.messages, {
191
- role: "user",
192
- content: text
193
- }, MAX_MESSAGES),
194
- state: "thinking"
195
- });
196
- }
197
- function handleAgentTranscriptEvent(text) {
198
- updateState({
199
- agentTranscript: null,
200
- messages: appendCapped(currentSnapshot.messages, {
201
- role: "assistant",
202
- content: text
203
- }, MAX_MESSAGES)
204
- });
205
- }
206
- /** Single entry point for all server->client session events. */
207
- function handleEvent(e) {
208
- if (currentSnapshot.state === "error" && e.type !== "error") updateState({
209
- state: "disconnected",
210
- error: null
211
- });
212
- switch (e.type) {
213
- case "speech_started":
214
- updateState({ userTranscript: "" });
215
- break;
216
- case "speech_stopped": break;
217
- case "user_transcript":
218
- handleUserTranscriptEvent(e.text);
219
- break;
220
- case "agent_transcript":
221
- handleAgentTranscriptEvent(e.text);
222
- break;
223
- case "tool_call":
224
- updateState({ toolCalls: [...currentSnapshot.toolCalls, {
225
- callId: e.toolCallId,
226
- name: e.toolName,
227
- args: e.args ?? {},
228
- status: "pending",
229
- afterMessageIndex: currentSnapshot.messages.length - 1
230
- }] });
231
- break;
232
- case "tool_call_done": {
233
- const tcs = currentSnapshot.toolCalls;
234
- const idx = tcs.findIndex((tc) => tc.callId === e.toolCallId);
235
- if (idx !== -1) {
236
- const updated = [...tcs];
237
- const existing = updated[idx];
238
- if (existing) updated[idx] = {
239
- ...existing,
240
- status: "done",
241
- result: e.result
242
- };
243
- updateState({ toolCalls: updated });
244
- }
245
- break;
246
- }
247
- case "reply_done":
248
- updateState({ state: "listening" });
249
- break;
250
- case "cancelled":
251
- handlerGeneration++;
252
- conn.voiceIO?.flush();
253
- updateState({
254
- userTranscript: null,
255
- agentTranscript: null,
256
- state: "listening"
257
- });
258
- break;
259
- case "reset":
260
- handlerGeneration++;
261
- conn.voiceIO?.flush();
262
- updateState({
263
- messages: [],
264
- toolCalls: [],
265
- customEvents: [],
266
- userTranscript: null,
267
- agentTranscript: null,
268
- error: null,
269
- state: "listening"
270
- });
271
- break;
272
- case "custom_event":
273
- appendCustomEvent(e.event, e.data);
274
- break;
275
- case "error":
276
- console.error("Agent error:", e.message);
277
- updateState({
278
- state: "error",
279
- error: {
280
- code: e.code,
281
- message: e.message
282
- },
283
- running: false
284
- });
285
- break;
286
- case "idle_timeout": break;
287
- default: break;
288
- }
289
- }
290
- /** Enqueue a PCM16 audio chunk for playback. Transitions state to `"speaking"` on the first chunk. */
291
- function playAudioChunk(chunk) {
292
- if (currentSnapshot.state === "disconnected" && currentSnapshot.error !== null) return;
293
- if (currentSnapshot.state !== "speaking") updateState({ state: "speaking" });
294
- if (conn.voiceIO) conn.voiceIO.enqueue(chunk.buffer);
295
- else if (conn.preInitAudio.length < MAX_PREINIT_AUDIO_CHUNKS) conn.preInitAudio.push(chunk);
296
- }
297
- /**
298
- * Signal that the server has finished sending audio for this turn.
299
- * Waits for the audio queue to drain, then transitions state to `"listening"`.
300
- * Uses the `handlerGeneration` counter to discard stale completions from interrupted turns.
301
- */
302
- function playAudioDone() {
303
- const gen = handlerGeneration;
304
- const io = conn.voiceIO;
305
- if (io) io.done().then(() => {
306
- if (handlerGeneration !== gen) return;
307
- updateState({ state: "listening" });
308
- }).catch((err) => {
309
- console.warn("Audio playback done failed:", err);
310
- });
311
- else updateState({ state: "listening" });
312
- }
313
- /**
314
- * Dispatch an incoming WebSocket message.
315
- *
316
- * Binary frames carry raw PCM16 audio chunks. Text frames are JSON-encoded
317
- * {@link ServerMessage} values validated via Zod.
318
- *
319
- * Returns the parsed config if the message is a `config` message,
320
- * otherwise `undefined`.
321
- */
322
- function handleMessage(data) {
323
- if (data instanceof ArrayBuffer) {
324
- playAudioChunk(new Uint8Array(data));
325
- return;
326
- }
327
- if (typeof data !== "string") {
328
- console.warn("session-core: non-string, non-binary frame received; dropping");
329
- return;
330
- }
331
- let raw;
332
- try {
333
- raw = JSON.parse(data);
334
- } catch {
335
- console.warn("session-core: invalid JSON; dropping");
336
- return;
337
- }
338
- const parsed = lenientParse(ServerMessageSchema, raw);
339
- if (!parsed.ok) {
340
- if (parsed.malformed) console.warn("session-core: malformed server message", parsed.error);
341
- return;
342
- }
343
- const msg = parsed.data;
344
- if (msg.type === "config") return {
345
- sampleRate: msg.sampleRate,
346
- ttsSampleRate: msg.ttsSampleRate,
347
- sid: msg.sessionId
348
- };
349
- if (msg.type === "audio_done") {
350
- playAudioDone();
351
- return;
352
- }
353
- handleEvent(msg);
354
- }
355
- function connect(opts) {
356
- updateState({
357
- state: "connecting",
358
- error: null
359
- });
360
- connectionController?.abort();
361
- cleanupAudio();
362
- conn.ws?.close();
363
- conn.ws = null;
364
- conn.generation++;
365
- const controller = new AbortController();
366
- connectionController = controller;
367
- const { signal: sig } = controller;
368
- if (opts?.signal) opts.signal.addEventListener("abort", () => disconnect(), { signal: sig });
369
- const resumeId = !hasConnected ? options.resumeSessionId : void 0;
370
- const socket = new WS(buildWsUrl(options.platformUrl, hasConnected, resumeId).toString());
371
- socket.binaryType = "arraybuffer";
372
- conn.ws = socket;
373
- socket.addEventListener("open", () => {
374
- updateState({ state: "ready" });
375
- }, { signal: sig });
376
- socket.addEventListener("message", (event) => {
377
- const config = handleMessage(event.data);
378
- if (config) {
379
- if (config.sid) options.onSessionId?.(config.sid);
380
- const isReconnect = hasConnected;
381
- hasConnected = true;
382
- initAudioCapture(conn, config, audioDeps).catch((err) => {
383
- audioDeps.updateState({
384
- state: "error",
385
- error: {
386
- code: "audio",
387
- message: `Audio capture failed: ${errorMessage(err)}`
388
- },
389
- running: false
390
- });
391
- });
392
- if (isReconnect && currentSnapshot.messages.length > 0) sendJson({
393
- type: "history",
394
- messages: currentSnapshot.messages.map((m) => ({
395
- role: m.role,
396
- content: m.content
397
- }))
398
- });
399
- }
400
- }, { signal: sig });
401
- socket.addEventListener("close", () => {
402
- if (sig.aborted) return;
403
- controller.abort();
404
- cleanupAudio();
405
- updateState({
406
- state: "disconnected",
407
- running: false
408
- });
409
- }, { signal: sig });
410
- }
411
- function cancel() {
412
- conn.voiceIO?.flush();
413
- updateState({ state: "listening" });
414
- sendJson({ type: "cancel" });
415
- }
416
- function reset() {
417
- conn.voiceIO?.flush();
418
- if (conn.ws && conn.ws.readyState === WS_OPEN) {
419
- sendJson({ type: "reset" });
420
- return;
421
- }
422
- resetState();
423
- disconnect();
424
- connect();
425
- }
426
- function disconnect() {
427
- connectionController?.abort();
428
- connectionController = null;
429
- cleanupAudio();
430
- conn.ws?.close();
431
- conn.ws = null;
432
- updateState({
433
- state: "disconnected",
434
- running: false
435
- });
436
- }
437
- function start() {
438
- updateState({
439
- started: true,
440
- running: true
441
- });
442
- connect();
443
- }
444
- function toggle() {
445
- if (currentSnapshot.running) disconnect();
446
- else {
447
- updateState({ running: true });
448
- connect();
449
- }
450
- }
451
- return {
452
- getSnapshot,
453
- subscribe,
454
- connect,
455
- cancel,
456
- resetState,
457
- reset,
458
- disconnect,
459
- start,
460
- toggle,
461
- [Symbol.dispose]() {
462
- disconnect();
463
- }
464
- };
465
- }
466
- //#endregion
1
+ import { t as createSessionCore } from "./session-core-D3NDaySY.js";
467
2
  export { createSessionCore };