@alexkroman1/aai 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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cli.js +3436 -0
  3. package/package.json +78 -0
  4. package/sdk/_internal_types.ts +89 -0
  5. package/sdk/_mock_ws.ts +172 -0
  6. package/sdk/_timeout.ts +24 -0
  7. package/sdk/builtin_tools.ts +309 -0
  8. package/sdk/capnweb.ts +341 -0
  9. package/sdk/define_agent.ts +70 -0
  10. package/sdk/direct_executor.ts +195 -0
  11. package/sdk/kv.ts +183 -0
  12. package/sdk/mod.ts +35 -0
  13. package/sdk/protocol.ts +313 -0
  14. package/sdk/runtime.ts +65 -0
  15. package/sdk/s2s.ts +271 -0
  16. package/sdk/server.ts +198 -0
  17. package/sdk/session.ts +438 -0
  18. package/sdk/system_prompt.ts +47 -0
  19. package/sdk/types.ts +406 -0
  20. package/sdk/vector.ts +133 -0
  21. package/sdk/winterc_server.ts +141 -0
  22. package/sdk/worker_entry.ts +99 -0
  23. package/sdk/worker_shim.ts +170 -0
  24. package/sdk/ws_handler.ts +190 -0
  25. package/templates/_shared/.env.example +5 -0
  26. package/templates/_shared/package.json +17 -0
  27. package/templates/code-interpreter/agent.ts +27 -0
  28. package/templates/code-interpreter/client.tsx +2 -0
  29. package/templates/dispatch-center/agent.ts +1536 -0
  30. package/templates/dispatch-center/client.tsx +504 -0
  31. package/templates/embedded-assets/agent.ts +49 -0
  32. package/templates/embedded-assets/client.tsx +2 -0
  33. package/templates/embedded-assets/knowledge.json +20 -0
  34. package/templates/health-assistant/agent.ts +160 -0
  35. package/templates/health-assistant/client.tsx +2 -0
  36. package/templates/infocom-adventure/agent.ts +164 -0
  37. package/templates/infocom-adventure/client.tsx +299 -0
  38. package/templates/math-buddy/agent.ts +21 -0
  39. package/templates/math-buddy/client.tsx +2 -0
  40. package/templates/memory-agent/agent.ts +74 -0
  41. package/templates/memory-agent/client.tsx +2 -0
  42. package/templates/night-owl/agent.ts +98 -0
  43. package/templates/night-owl/client.tsx +28 -0
  44. package/templates/personal-finance/agent.ts +26 -0
  45. package/templates/personal-finance/client.tsx +2 -0
  46. package/templates/simple/agent.ts +6 -0
  47. package/templates/simple/client.tsx +2 -0
  48. package/templates/smart-research/agent.ts +164 -0
  49. package/templates/smart-research/client.tsx +2 -0
  50. package/templates/support/README.md +62 -0
  51. package/templates/support/agent.ts +19 -0
  52. package/templates/support/client.tsx +2 -0
  53. package/templates/travel-concierge/agent.ts +29 -0
  54. package/templates/travel-concierge/client.tsx +2 -0
  55. package/templates/web-researcher/agent.ts +17 -0
  56. package/templates/web-researcher/client.tsx +2 -0
  57. package/ui/_components/app.tsx +37 -0
  58. package/ui/_components/chat_view.tsx +36 -0
  59. package/ui/_components/controls.tsx +32 -0
  60. package/ui/_components/error_banner.tsx +18 -0
  61. package/ui/_components/message_bubble.tsx +21 -0
  62. package/ui/_components/message_list.tsx +61 -0
  63. package/ui/_components/state_indicator.tsx +17 -0
  64. package/ui/_components/thinking_indicator.tsx +19 -0
  65. package/ui/_components/tool_call_block.tsx +110 -0
  66. package/ui/_components/tool_icons.tsx +101 -0
  67. package/ui/_components/transcript.tsx +20 -0
  68. package/ui/audio.ts +170 -0
  69. package/ui/components.ts +49 -0
  70. package/ui/components_mod.ts +37 -0
  71. package/ui/mod.ts +48 -0
  72. package/ui/mount.tsx +112 -0
  73. package/ui/mount_context.ts +19 -0
  74. package/ui/session.ts +456 -0
  75. package/ui/session_mod.ts +27 -0
  76. package/ui/signals.ts +111 -0
  77. package/ui/types.ts +50 -0
  78. package/ui/worklets/capture-processor.js +62 -0
  79. package/ui/worklets/playback-processor.js +110 -0
package/ui/mount.tsx ADDED
@@ -0,0 +1,112 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+
3
+ import type { ComponentType } from "preact";
4
+ import { render } from "preact";
5
+ import { MountConfigProvider } from "./mount_context.ts";
6
+ import { createVoiceSession, type VoiceSession } from "./session.ts";
7
+ import { createSessionControls, SessionProvider, type SessionSignals } from "./signals.ts";
8
+
9
+ /** Theme overrides for the default UI. Applied as CSS custom properties. */
10
+ export type MountTheme = {
11
+ /** Background color. Default: `#101010`. */
12
+ bg?: string;
13
+ /** Primary accent color. Default: `#fab283`. */
14
+ primary?: string;
15
+ /** Main text color. */
16
+ text?: string;
17
+ /** Surface/card color. */
18
+ surface?: string;
19
+ /** Border color. */
20
+ border?: string;
21
+ };
22
+
23
+ /** Options for {@linkcode mount}. */
24
+ export type MountOptions = {
25
+ /** CSS selector or DOM element to render into. Defaults to `"#app"`. */
26
+ target?: string | HTMLElement;
27
+ /** Base URL of the AAI platform server. Derived from `location.href` by default. */
28
+ platformUrl?: string;
29
+ /** Agent title shown in the header and start screen. */
30
+ title?: string;
31
+ /** Theme color overrides. */
32
+ theme?: MountTheme;
33
+ };
34
+
35
+ /**
36
+ * Handle returned by {@linkcode mount} for cleanup.
37
+ *
38
+ * Implements {@linkcode Disposable} so it can be used with `using`.
39
+ */
40
+ export type MountHandle = {
41
+ /** The underlying voice session. */
42
+ session: VoiceSession;
43
+ /** Reactive session controls for the mounted UI. */
44
+ signals: SessionSignals;
45
+ /** Unmount the UI, remove injected styles, and disconnect the session. */
46
+ dispose(): void;
47
+ /** Alias for {@linkcode dispose} for use with `using`. */
48
+ [Symbol.dispose](): void;
49
+ };
50
+
51
+ function resolveContainer(target: string | HTMLElement = "#app"): HTMLElement {
52
+ const el = typeof target === "string" ? document.querySelector(target) : target;
53
+ if (!el) throw new Error(`Element not found: ${target}`);
54
+ return el as HTMLElement;
55
+ }
56
+
57
+ /**
58
+ * Mount a Preact component with voice session wiring.
59
+ *
60
+ * Creates a {@linkcode VoiceSession}, wraps it in
61
+ * {@linkcode SessionSignals}, and renders the component
62
+ * inside a {@linkcode SessionProvider}.
63
+ *
64
+ * @param Component - The Preact component to render.
65
+ * @param options - Mount options (target element, platform URL).
66
+ * @returns A {@linkcode MountHandle} for cleanup.
67
+ * @throws {Error} If the target element is not found in the DOM.
68
+ */
69
+ export function mount(Component: ComponentType, options?: MountOptions): MountHandle {
70
+ const container = resolveContainer(options?.target);
71
+
72
+ const platformUrl =
73
+ options?.platformUrl ?? globalThis.location.origin + globalThis.location.pathname;
74
+ const session = createVoiceSession({ platformUrl });
75
+ const signals = createSessionControls(session);
76
+
77
+ const mountConfig = { title: options?.title, theme: options?.theme };
78
+
79
+ // Apply theme overrides as CSS custom properties on the container.
80
+ if (options?.theme) {
81
+ const t = options.theme;
82
+ const el = container as HTMLElement;
83
+ if (t.bg) el.style.setProperty("--color-aai-bg", t.bg);
84
+ if (t.primary) el.style.setProperty("--color-aai-primary", t.primary);
85
+ if (t.text) el.style.setProperty("--color-aai-text", t.text);
86
+ if (t.surface) el.style.setProperty("--color-aai-surface", t.surface);
87
+ if (t.border) el.style.setProperty("--color-aai-border", t.border);
88
+ }
89
+
90
+ render(
91
+ <MountConfigProvider value={mountConfig}>
92
+ <SessionProvider value={signals}>
93
+ <Component />
94
+ </SessionProvider>
95
+ </MountConfigProvider>,
96
+ container,
97
+ );
98
+
99
+ const handle: MountHandle = {
100
+ session,
101
+ signals,
102
+ dispose() {
103
+ render(null, container);
104
+ signals.dispose();
105
+ session.disconnect();
106
+ },
107
+ [Symbol.dispose]() {
108
+ handle.dispose();
109
+ },
110
+ };
111
+ return handle;
112
+ }
@@ -0,0 +1,19 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ import { createContext } from "preact";
3
+ import { useContext } from "preact/hooks";
4
+ import type { MountTheme } from "./mount.tsx";
5
+
6
+ /** Resolved mount-level configuration available to default UI components. */
7
+ export type MountConfig = {
8
+ title?: string | undefined;
9
+ theme?: MountTheme | undefined;
10
+ };
11
+
12
+ const Ctx = createContext<MountConfig>({});
13
+
14
+ export const MountConfigProvider = Ctx.Provider;
15
+
16
+ /** Read mount config (title, theme) from the nearest provider. */
17
+ export function useMountConfig(): MountConfig {
18
+ return useContext(Ctx);
19
+ }
package/ui/session.ts ADDED
@@ -0,0 +1,456 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+
3
+ import { batch, type Signal, signal } from "@preact/signals";
4
+ import type { ClientEvent, ClientMessage, ReadyConfig, ServerMessage } from "../sdk/protocol.ts";
5
+ import { PROTOCOL_VERSION } from "../sdk/protocol.ts";
6
+
7
+ const SUPPORTED_PROTOCOL_VERSION = PROTOCOL_VERSION;
8
+
9
+ import type { VoiceIO } from "./audio.ts";
10
+ import type { AgentState, Message, SessionError, SessionOptions, ToolCallInfo } from "./types.ts";
11
+
12
+ /**
13
+ * A reactive voice session that manages WebSocket communication,
14
+ * audio capture/playback, and agent state transitions.
15
+ *
16
+ * Uses plain JSON text frames and binary audio frames for communication
17
+ * and native WebSocket for the connection.
18
+ *
19
+ * Implements {@linkcode Disposable} for resource cleanup via `using`.
20
+ */
21
+ export type VoiceSession = {
22
+ /** Current agent state (connecting, listening, thinking, etc.). */
23
+ readonly state: Signal<AgentState>;
24
+ /** Chat message history for the session. */
25
+ readonly messages: Signal<Message[]>;
26
+ /** Active tool calls for the current turn. */
27
+ readonly toolCalls: Signal<ToolCallInfo[]>;
28
+ /**
29
+ * Live user utterance from STT/VAD.
30
+ * `null` = not speaking, `""` = speech detected but no text yet,
31
+ * non-empty string = partial/final transcript text.
32
+ */
33
+ readonly userUtterance: Signal<string | null>;
34
+ /** Current session error, or `null` if no error. */
35
+ readonly error: Signal<SessionError | null>;
36
+ /** Disconnection info, or `null` if connected. */
37
+ readonly disconnected: Signal<{ intentional: boolean } | null>;
38
+ /**
39
+ * Open a WebSocket connection to the server and begin audio capture.
40
+ *
41
+ * @param options - Optional connection options.
42
+ * @param options.signal - An AbortSignal that, when aborted, disconnects the session.
43
+ */
44
+ connect(options?: { signal?: AbortSignal }): void;
45
+ /** Cancel the current agent turn and discard in-flight TTS audio. */
46
+ cancel(): void;
47
+ /** Clear messages, transcript, and error state without disconnecting. */
48
+ resetState(): void;
49
+ /** Reset the session: clear state and reconnect. */
50
+ reset(): void;
51
+ /** Close the WebSocket and release all audio resources. */
52
+ disconnect(): void;
53
+ /** Alias for {@linkcode disconnect} for use with `using`. */
54
+ [Symbol.dispose](): void;
55
+ };
56
+
57
+ /**
58
+ * Handles server→client messages and updates reactive Preact signals
59
+ * accordingly (state transitions, transcripts, messages, audio playback).
60
+ */
61
+ /** @internal Exported for testing only. */
62
+ export class ClientHandler {
63
+ #state: Signal<AgentState>;
64
+ #messages: Signal<Message[]>;
65
+ #toolCalls: Signal<ToolCallInfo[]>;
66
+ #userUtterance: Signal<string | null>;
67
+ #error: Signal<SessionError | null>;
68
+ #voiceIO: () => VoiceIO | null;
69
+ /** Incremented on each turn boundary — stale async callbacks compare against this. */
70
+ #generation = 0;
71
+ constructor(opts: {
72
+ state: Signal<AgentState>;
73
+ messages: Signal<Message[]>;
74
+ toolCalls: Signal<ToolCallInfo[]>;
75
+ userUtterance: Signal<string | null>;
76
+ error: Signal<SessionError | null>;
77
+ voiceIO: () => VoiceIO | null;
78
+ }) {
79
+ this.#state = opts.state;
80
+ this.#messages = opts.messages;
81
+ this.#toolCalls = opts.toolCalls;
82
+ this.#userUtterance = opts.userUtterance;
83
+ this.#error = opts.error;
84
+ this.#voiceIO = opts.voiceIO;
85
+ }
86
+
87
+ /** Single entry point for all server→client session events. */
88
+ /** Sentinel value indicating speech detected but no transcript yet. */
89
+ static readonly speechActive = "\x00";
90
+
91
+ event(e: ClientEvent): void {
92
+ switch (e.type) {
93
+ case "speech_started":
94
+ this.#userUtterance.value = "";
95
+ break;
96
+ case "speech_stopped":
97
+ // VAD detected end of speech — processing will follow.
98
+ break;
99
+ case "transcript":
100
+ this.#userUtterance.value = e.text;
101
+ break;
102
+ case "turn":
103
+ this.#generation++;
104
+ batch(() => {
105
+ this.#userUtterance.value = null;
106
+ this.#messages.value = [...this.#messages.value, { role: "user", text: e.text }];
107
+ this.#state.value = "thinking";
108
+ });
109
+ break;
110
+ case "chat":
111
+ this.#messages.value = [...this.#messages.value, { role: "assistant", text: e.text }];
112
+ break;
113
+ case "tool_call_start":
114
+ this.#toolCalls.value = [
115
+ ...this.#toolCalls.value,
116
+ {
117
+ toolCallId: e.toolCallId,
118
+ toolName: e.toolName,
119
+ args: e.args,
120
+ status: "pending",
121
+ afterMessageIndex: this.#messages.value.length - 1,
122
+ },
123
+ ];
124
+ break;
125
+ case "tool_call_done": {
126
+ const tcs = this.#toolCalls.value;
127
+ const idx = tcs.findIndex((tc) => tc.toolCallId === e.toolCallId);
128
+ if (idx !== -1) {
129
+ const updated = [...tcs];
130
+ updated[idx] = { ...updated[idx]!, status: "done", result: e.result };
131
+ this.#toolCalls.value = updated;
132
+ }
133
+ break;
134
+ }
135
+ case "tts_done":
136
+ // No-audio turns (empty LLM result) still use this event
137
+ // to transition back to listening. Audio turns signal via stream end.
138
+ this.#state.value = "listening";
139
+ break;
140
+ case "cancelled":
141
+ this.#generation++;
142
+ this.#voiceIO()?.flush();
143
+ this.#userUtterance.value = null;
144
+ this.#state.value = "listening";
145
+ break;
146
+ case "reset": {
147
+ this.#generation++;
148
+ this.#voiceIO()?.flush();
149
+ batch(() => {
150
+ this.#messages.value = [];
151
+ this.#toolCalls.value = [];
152
+ this.#userUtterance.value = null;
153
+ this.#error.value = null;
154
+ this.#state.value = "listening";
155
+ });
156
+ break;
157
+ }
158
+ case "error":
159
+ console.error("Agent error:", e.message);
160
+ batch(() => {
161
+ this.#error.value = {
162
+ code: e.code,
163
+ message: e.message,
164
+ };
165
+ this.#state.value = "error";
166
+ });
167
+ break;
168
+ }
169
+ }
170
+
171
+ playAudioChunk(chunk: Uint8Array): void {
172
+ if (this.#state.value === "error") return;
173
+ if (this.#state.value !== "speaking") {
174
+ this.#state.value = "speaking";
175
+ }
176
+ this.#voiceIO()?.enqueue(chunk.buffer as ArrayBuffer);
177
+ }
178
+
179
+ playAudioDone(): void {
180
+ const gen = this.#generation;
181
+ const io = this.#voiceIO();
182
+ if (io) {
183
+ void io.done().then(() => {
184
+ if (this.#generation !== gen) return;
185
+ this.#state.value = "listening";
186
+ });
187
+ } else {
188
+ this.#state.value = "listening";
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Dispatch an incoming WebSocket message (text or binary).
194
+ *
195
+ * Returns the parsed config if the message is a `config` message,
196
+ * otherwise `null`.
197
+ */
198
+ handleMessage(data: string | ArrayBuffer): ReadyConfig | null {
199
+ // Binary frame → raw PCM16 TTS audio
200
+ if (data instanceof ArrayBuffer) {
201
+ this.playAudioChunk(new Uint8Array(data));
202
+ return null;
203
+ }
204
+
205
+ // Text frame → JSON message
206
+ let msg: ServerMessage;
207
+ try {
208
+ msg = JSON.parse(data);
209
+ } catch {
210
+ return null;
211
+ }
212
+
213
+ if (msg.type === "config") {
214
+ const { type: _, ...config } = msg;
215
+ return config as ReadyConfig;
216
+ }
217
+
218
+ if (msg.type === "audio_done") {
219
+ this.playAudioDone();
220
+ return null;
221
+ }
222
+
223
+ // All other messages are ClientEvent
224
+ this.event(msg as ClientEvent);
225
+ return null;
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Create a voice session that connects to an AAI server via WebSocket.
231
+ *
232
+ * Uses plain JSON text frames and binary audio frames for communication.
233
+ *
234
+ * @param options - Session configuration including the platform server URL.
235
+ * @returns A {@linkcode VoiceSession} handle for controlling the session.
236
+ */
237
+ export function createVoiceSession(options: SessionOptions): VoiceSession {
238
+ const state = signal<AgentState>("disconnected");
239
+ const messages = signal<Message[]>([]);
240
+ const toolCalls = signal<ToolCallInfo[]>([]);
241
+ const userUtterance = signal<string | null>(null);
242
+ const error = signal<SessionError | null>(null);
243
+ const disconnected = signal<{ intentional: boolean } | null>(null);
244
+
245
+ let ws: WebSocket | null = null;
246
+ let voiceIO: VoiceIO | null = null;
247
+ let connectionController: AbortController | null = null;
248
+ let hasConnected = false;
249
+ let audioSetupInFlight = false;
250
+ function cleanupAudio(): void {
251
+ audioSetupInFlight = false;
252
+ void voiceIO?.close();
253
+ voiceIO = null;
254
+ }
255
+
256
+ function resetState(): void {
257
+ batch(() => {
258
+ messages.value = [];
259
+ toolCalls.value = [];
260
+ userUtterance.value = null;
261
+ error.value = null;
262
+ });
263
+ }
264
+
265
+ function send(msg: ClientMessage): void {
266
+ if (ws && ws.readyState === WebSocket.OPEN) {
267
+ ws.send(JSON.stringify(msg));
268
+ }
269
+ }
270
+
271
+ function sendBinary(data: ArrayBuffer): void {
272
+ if (ws && ws.readyState === WebSocket.OPEN) {
273
+ ws.send(data);
274
+ }
275
+ }
276
+
277
+ async function handleReady(msg: ReadyConfig): Promise<void> {
278
+ if (audioSetupInFlight) return;
279
+
280
+ // Protocol version check
281
+ if (msg.protocolVersion !== SUPPORTED_PROTOCOL_VERSION) {
282
+ batch(() => {
283
+ error.value = {
284
+ code: "protocol",
285
+ message: `Server protocol v${msg.protocolVersion} is not compatible with client v${SUPPORTED_PROTOCOL_VERSION}. Please redeploy your agent.`,
286
+ };
287
+ state.value = "error";
288
+ });
289
+ return;
290
+ }
291
+
292
+ audioSetupInFlight = true;
293
+ try {
294
+ const [{ createVoiceIO }, captureWorklet, playbackWorklet] = await Promise.all([
295
+ import("./audio.ts"),
296
+ import("./worklets/capture-processor.js").then((m) => m.default as unknown as string),
297
+ import("./worklets/playback-processor.js").then((m) => m.default as unknown as string),
298
+ ]);
299
+ const io = await createVoiceIO({
300
+ sttSampleRate: msg.sampleRate,
301
+ ttsSampleRate: msg.ttsSampleRate,
302
+ captureWorkletSrc: captureWorklet,
303
+ playbackWorkletSrc: playbackWorklet,
304
+ onMicData: (pcm16: ArrayBuffer) => {
305
+ // Always stream audio — S2S handles VAD natively.
306
+ try {
307
+ sendBinary(pcm16);
308
+ } catch {
309
+ /* connection may be closed */
310
+ }
311
+ },
312
+ });
313
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
314
+ io.close();
315
+ return;
316
+ }
317
+ voiceIO = io;
318
+ send({ type: "audio_ready" });
319
+ state.value = "listening";
320
+ } catch (err: unknown) {
321
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
322
+ batch(() => {
323
+ error.value = {
324
+ code: "audio",
325
+ message: `Microphone access failed: ${err instanceof Error ? err.message : String(err)}`,
326
+ };
327
+ state.value = "error";
328
+ });
329
+ } finally {
330
+ audioSetupInFlight = false;
331
+ }
332
+ }
333
+
334
+ function connect(opts?: { signal?: AbortSignal }): void {
335
+ disconnected.value = null;
336
+ state.value = "connecting";
337
+ connectionController?.abort();
338
+ const controller = new AbortController();
339
+ connectionController = controller;
340
+ const { signal: sig } = controller;
341
+
342
+ if (opts?.signal) {
343
+ opts.signal.addEventListener("abort", () => disconnect(), {
344
+ signal: sig,
345
+ });
346
+ }
347
+
348
+ const base = options.platformUrl;
349
+ const wsUrl = new URL("websocket", base.endsWith("/") ? base : `${base}/`);
350
+ wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
351
+ if (hasConnected) wsUrl.searchParams.set("resume", "1");
352
+
353
+ const socket = new WebSocket(wsUrl.toString());
354
+ socket.binaryType = "arraybuffer";
355
+ ws = socket;
356
+
357
+ const handler = new ClientHandler({
358
+ state,
359
+ messages,
360
+ toolCalls,
361
+ userUtterance,
362
+ error,
363
+ voiceIO: () => voiceIO,
364
+ });
365
+
366
+ socket.addEventListener(
367
+ "open",
368
+ () => {
369
+ state.value = "ready";
370
+ },
371
+ { signal: sig },
372
+ );
373
+
374
+ socket.addEventListener(
375
+ "message",
376
+ (event: Event) => {
377
+ const msgEvent = event as MessageEvent;
378
+ const config = handler.handleMessage(msgEvent.data);
379
+ if (config) {
380
+ hasConnected = true;
381
+ void handleReady(config);
382
+
383
+ // Send history if reconnecting
384
+ if (hasConnected && messages.value.length > 0) {
385
+ send({
386
+ type: "history",
387
+ messages: messages.value.map((m) => ({
388
+ role: m.role,
389
+ text: m.text,
390
+ })),
391
+ });
392
+ }
393
+ }
394
+ },
395
+ { signal: sig },
396
+ );
397
+
398
+ socket.addEventListener(
399
+ "close",
400
+ () => {
401
+ if (sig.aborted) {
402
+ return;
403
+ }
404
+ controller.abort();
405
+ disconnected.value = { intentional: false };
406
+ cleanupAudio();
407
+ state.value = "disconnected";
408
+ },
409
+ { signal: sig },
410
+ );
411
+ }
412
+
413
+ function cancel(): void {
414
+ voiceIO?.flush();
415
+ state.value = "listening";
416
+ send({ type: "cancel" });
417
+ }
418
+
419
+ function reset(): void {
420
+ voiceIO?.flush();
421
+ if (ws && ws.readyState === WebSocket.OPEN) {
422
+ send({ type: "reset" });
423
+ return;
424
+ }
425
+ resetState();
426
+ disconnect();
427
+ connect();
428
+ }
429
+
430
+ function disconnect(): void {
431
+ connectionController?.abort();
432
+ connectionController = null;
433
+ cleanupAudio();
434
+ ws?.close();
435
+ ws = null;
436
+ state.value = "disconnected";
437
+ disconnected.value = { intentional: true };
438
+ }
439
+
440
+ return {
441
+ state,
442
+ messages,
443
+ toolCalls,
444
+ userUtterance,
445
+ error,
446
+ disconnected,
447
+ connect,
448
+ cancel,
449
+ resetState,
450
+ reset,
451
+ disconnect,
452
+ [Symbol.dispose]() {
453
+ disconnect();
454
+ },
455
+ };
456
+ }
@@ -0,0 +1,27 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ /**
3
+ * WebSocket session management for AAI voice agents.
4
+ *
5
+ * Provides the low-level voice session without any Preact/UI dependencies.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { createVoiceSession } from "@aai/ui/session";
10
+ *
11
+ * const session = createVoiceSession({ platformUrl: "https://example.com" });
12
+ * session.connect();
13
+ * ```
14
+ *
15
+ * @module
16
+ */
17
+
18
+ export type { VoiceSession } from "./session.ts";
19
+ export { createVoiceSession } from "./session.ts";
20
+ export type {
21
+ AgentState,
22
+ Message,
23
+ SessionError,
24
+ SessionErrorCode,
25
+ SessionOptions,
26
+ ToolCallInfo,
27
+ } from "./types.ts";