@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
@@ -0,0 +1,18 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+
3
+ import type { Signal } from "@preact/signals";
4
+ import type * as preact from "preact";
5
+ import type { SessionError } from "../types.ts";
6
+
7
+ export function ErrorBanner({
8
+ error,
9
+ }: {
10
+ error: Signal<SessionError | null>;
11
+ }): preact.JSX.Element | null {
12
+ if (!error.value) return null;
13
+ return (
14
+ <div class="mx-4 mt-3 px-3 py-2 rounded-aai border border-aai-error/40 bg-aai-error/8 text-[13px] leading-[130%] text-aai-error">
15
+ {error.value.message}
16
+ </div>
17
+ );
18
+ }
@@ -0,0 +1,21 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ import type * as preact from "preact";
3
+ import type { Message } from "../types.ts";
4
+
5
+ export function MessageBubble({ message }: { message: Message }): preact.JSX.Element {
6
+ const isUser = message.role === "user";
7
+ if (isUser) {
8
+ return (
9
+ <div class="flex flex-col w-full items-end">
10
+ <div class="max-w-[min(82%,64ch)] bg-aai-surface-faint border border-aai-border px-3 py-2 rounded-aai whitespace-pre-wrap wrap-break-word text-sm font-normal leading-[150%] text-aai-text">
11
+ {message.text}
12
+ </div>
13
+ </div>
14
+ );
15
+ }
16
+ return (
17
+ <div class="whitespace-pre-wrap wrap-break-word text-sm font-normal leading-[150%] text-aai-text">
18
+ {message.text}
19
+ </div>
20
+ );
21
+ }
@@ -0,0 +1,61 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+
3
+ import { computed, useSignalEffect } from "@preact/signals";
4
+ import type { VNode } from "preact";
5
+ import { useRef } from "preact/hooks";
6
+ import { useSession } from "../signals.ts";
7
+ import { MessageBubble } from "./message_bubble.tsx";
8
+ import { ThinkingIndicator } from "./thinking_indicator.tsx";
9
+ import { ToolCallBlock } from "./tool_call_block.tsx";
10
+ import { Transcript } from "./transcript.tsx";
11
+
12
+ export function MessageList() {
13
+ const { session } = useSession();
14
+ const scrollRef = useRef<HTMLDivElement>(null);
15
+
16
+ const showThinking = computed(() => {
17
+ if (session.state.value !== "thinking") return false;
18
+ const last = session.toolCalls.value.at(-1);
19
+ if (last?.status === "pending") return false;
20
+ const lastMsg = session.messages.value.at(-1);
21
+ return !lastMsg || lastMsg.role === "user" || !!last;
22
+ });
23
+
24
+ useSignalEffect(() => {
25
+ session.messages.value;
26
+ session.toolCalls.value;
27
+ session.userUtterance.value;
28
+ session.state.value;
29
+ scrollRef.current?.scrollIntoView({ behavior: "smooth" });
30
+ });
31
+
32
+ const messages = session.messages.value;
33
+ const toolCalls = session.toolCalls.value;
34
+
35
+ // Render each message followed by its tool calls.
36
+ const items: VNode[] = [];
37
+ let tci = 0;
38
+ for (let i = 0; i < messages.length; i++) {
39
+ items.push(<MessageBubble key={`msg-${i}`} message={messages[i]!} />);
40
+ while (tci < toolCalls.length && toolCalls[tci]!.afterMessageIndex <= i) {
41
+ items.push(<ToolCallBlock key={toolCalls[tci]!.toolCallId} toolCall={toolCalls[tci]!} />);
42
+ tci++;
43
+ }
44
+ }
45
+ // Any remaining tool calls (still pending, no following message yet).
46
+ while (tci < toolCalls.length) {
47
+ items.push(<ToolCallBlock key={toolCalls[tci]!.toolCallId} toolCall={toolCalls[tci]!} />);
48
+ tci++;
49
+ }
50
+
51
+ return (
52
+ <div role="log" class="flex-1 overflow-y-auto [scrollbar-width:none] bg-aai-surface">
53
+ <div class="flex flex-col gap-4.5 p-4">
54
+ {items}
55
+ <Transcript userUtterance={session.userUtterance} />
56
+ {showThinking.value && <ThinkingIndicator />}
57
+ <div ref={scrollRef} />
58
+ </div>
59
+ </div>
60
+ );
61
+ }
@@ -0,0 +1,17 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+
3
+ import type { Signal } from "@preact/signals";
4
+ import type * as preact from "preact";
5
+ import type { AgentState } from "../types.ts";
6
+
7
+ export function StateIndicator({ state }: { state: Signal<AgentState> }): preact.JSX.Element {
8
+ return (
9
+ <div class="inline-flex items-center justify-center gap-1.5 text-[13px] font-medium leading-[130%] text-aai-text-muted capitalize">
10
+ <div
11
+ class="w-2 h-2 rounded-full"
12
+ style={{ background: `var(--color-aai-state-${state.value})` }}
13
+ />
14
+ {state}
15
+ </div>
16
+ );
17
+ }
@@ -0,0 +1,19 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ import type * as preact from "preact";
3
+
4
+ export function ThinkingIndicator(): preact.JSX.Element {
5
+ return (
6
+ <div class="flex items-center gap-2 text-aai-text-dim text-sm font-medium min-h-5">
7
+ {[0, 0.16, 0.32].map((delay) => (
8
+ <div
9
+ key={delay}
10
+ class="w-1.5 h-1.5 rounded-full bg-aai-text-dim"
11
+ style={{
12
+ animation: "aai-bounce 1.4s infinite ease-in-out both",
13
+ animationDelay: `${delay}s`,
14
+ }}
15
+ />
16
+ ))}
17
+ </div>
18
+ );
19
+ }
@@ -0,0 +1,110 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ import type * as preact from "preact";
3
+ import { useState } from "preact/hooks";
4
+ import type { ToolCallInfo } from "../types.ts";
5
+ import {
6
+ BoltIcon,
7
+ ChatBubbleIcon,
8
+ DownloadIcon,
9
+ ExternalLinkIcon,
10
+ SearchIcon,
11
+ TerminalIcon,
12
+ } from "./tool_icons.tsx";
13
+
14
+ type IconComponent = (props: { class?: string }) => preact.JSX.Element;
15
+
16
+ type ToolConfig = {
17
+ Icon: IconComponent;
18
+ title: string;
19
+ subtitle: (args: Record<string, unknown>) => string;
20
+ };
21
+
22
+ const TOOL_CONFIG: Record<string, ToolConfig> = {
23
+ web_search: {
24
+ Icon: SearchIcon,
25
+ title: "Web Search",
26
+ subtitle: (args) => String(args.query ?? ""),
27
+ },
28
+ visit_webpage: {
29
+ Icon: ExternalLinkIcon,
30
+ title: "Visit Page",
31
+ subtitle: (args) => String(args.url ?? ""),
32
+ },
33
+ run_code: {
34
+ Icon: TerminalIcon,
35
+ title: "Run Code",
36
+ subtitle: (args) => {
37
+ const code = String(args.code ?? "");
38
+ const firstLine = code.split("\n")[0] ?? "";
39
+ return firstLine.length > 80 ? `${firstLine.slice(0, 80)}...` : firstLine;
40
+ },
41
+ },
42
+ fetch_json: {
43
+ Icon: DownloadIcon,
44
+ title: "Fetch JSON",
45
+ subtitle: (args) => String(args.url ?? ""),
46
+ },
47
+ user_input: {
48
+ Icon: ChatBubbleIcon,
49
+ title: "Asking User",
50
+ subtitle: (args) => String(args.question ?? ""),
51
+ },
52
+ };
53
+
54
+ const DEFAULT_CONFIG: ToolConfig = {
55
+ Icon: BoltIcon,
56
+ title: "",
57
+ subtitle: (args) => {
58
+ const summary = JSON.stringify(args);
59
+ return summary.length > 80 ? `${summary.slice(0, 80)}...` : summary;
60
+ },
61
+ };
62
+
63
+ function formatResult(result: string): string {
64
+ try {
65
+ return JSON.stringify(JSON.parse(result), null, 2);
66
+ } catch {
67
+ return result;
68
+ }
69
+ }
70
+
71
+ export function ToolCallBlock({ toolCall }: { toolCall: ToolCallInfo }): preact.JSX.Element {
72
+ const [isOpen, setOpen] = useState(false);
73
+ const config = TOOL_CONFIG[toolCall.toolName] ?? DEFAULT_CONFIG;
74
+ const isPending = toolCall.status === "pending";
75
+ const title = config.title || toolCall.toolName;
76
+
77
+ return (
78
+ <div class="flex flex-col">
79
+ <div
80
+ class="flex items-center gap-2 px-3 py-2 rounded-aai border border-aai-border bg-aai-surface-faint cursor-pointer select-none"
81
+ onClick={() => !isPending && setOpen(!isOpen)}
82
+ >
83
+ <config.Icon class="w-4 h-4 text-aai-text-dim shrink-0" />
84
+ <span class={`text-sm font-medium text-aai-text ${isPending ? "tool-shimmer" : ""}`}>
85
+ {title}
86
+ </span>
87
+ <span class="text-sm text-aai-text-dim truncate flex-1 min-w-0">
88
+ {config.subtitle(toolCall.args)}
89
+ </span>
90
+ {!isPending && toolCall.result && (
91
+ <span class="text-xs text-aai-text-dim shrink-0">{isOpen ? "\u25BE" : "\u25B8"}</span>
92
+ )}
93
+ </div>
94
+ {isOpen && (
95
+ <div class="border-x border-b border-aai-border rounded-b-aai bg-aai-surface max-h-64 overflow-auto">
96
+ {toolCall.toolName === "run_code" && toolCall.args.code && (
97
+ <pre class="text-xs text-aai-text p-2 whitespace-pre-wrap border-b border-aai-border font-mono">
98
+ {String(toolCall.args.code)}
99
+ </pre>
100
+ )}
101
+ {toolCall.result && (
102
+ <pre class="text-xs text-aai-text-dim p-2 whitespace-pre-wrap">
103
+ {formatResult(toolCall.result)}
104
+ </pre>
105
+ )}
106
+ </div>
107
+ )}
108
+ </div>
109
+ );
110
+ }
@@ -0,0 +1,101 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ import type * as preact from "preact";
3
+
4
+ type IconProps = { class?: string };
5
+
6
+ /** Magnifying glass icon for web_search. */
7
+ export function SearchIcon(props: IconProps): preact.JSX.Element {
8
+ return (
9
+ <svg
10
+ viewBox="0 0 16 16"
11
+ fill="none"
12
+ stroke="currentColor"
13
+ stroke-width="1.5"
14
+ class={props.class}
15
+ >
16
+ <circle cx="7" cy="7" r="4.5" />
17
+ <path d="M10.5 10.5L14 14" stroke-linecap="round" />
18
+ </svg>
19
+ );
20
+ }
21
+
22
+ /** External link icon for visit_webpage. */
23
+ export function ExternalLinkIcon(props: IconProps): preact.JSX.Element {
24
+ return (
25
+ <svg
26
+ viewBox="0 0 16 16"
27
+ fill="none"
28
+ stroke="currentColor"
29
+ stroke-width="1.5"
30
+ class={props.class}
31
+ >
32
+ <path d="M6 2H3a1 1 0 00-1 1v10a1 1 0 001 1h10a1 1 0 001-1v-3" stroke-linecap="round" />
33
+ <path d="M9 2h5v5" stroke-linecap="round" stroke-linejoin="round" />
34
+ <path d="M14 2L7 9" stroke-linecap="round" />
35
+ </svg>
36
+ );
37
+ }
38
+
39
+ /** Terminal icon for run_code. */
40
+ export function TerminalIcon(props: IconProps): preact.JSX.Element {
41
+ return (
42
+ <svg
43
+ viewBox="0 0 16 16"
44
+ fill="none"
45
+ stroke="currentColor"
46
+ stroke-width="1.5"
47
+ class={props.class}
48
+ >
49
+ <rect x="1" y="2" width="14" height="12" rx="1.5" />
50
+ <path d="M4 6l3 2.5L4 11" stroke-linecap="round" stroke-linejoin="round" />
51
+ <path d="M9 11h3" stroke-linecap="round" />
52
+ </svg>
53
+ );
54
+ }
55
+
56
+ /** Download icon for fetch_json. */
57
+ export function DownloadIcon(props: IconProps): preact.JSX.Element {
58
+ return (
59
+ <svg
60
+ viewBox="0 0 16 16"
61
+ fill="none"
62
+ stroke="currentColor"
63
+ stroke-width="1.5"
64
+ class={props.class}
65
+ >
66
+ <path d="M8 2v9" stroke-linecap="round" />
67
+ <path d="M4.5 8L8 11.5 11.5 8" stroke-linecap="round" stroke-linejoin="round" />
68
+ <path d="M2 13h12" stroke-linecap="round" />
69
+ </svg>
70
+ );
71
+ }
72
+
73
+ /** Chat bubble icon for user_input. */
74
+ export function ChatBubbleIcon(props: IconProps): preact.JSX.Element {
75
+ return (
76
+ <svg
77
+ viewBox="0 0 16 16"
78
+ fill="none"
79
+ stroke="currentColor"
80
+ stroke-width="1.5"
81
+ class={props.class}
82
+ >
83
+ <path d="M2 3a1 1 0 011-1h10a1 1 0 011 1v7a1 1 0 01-1 1H5l-3 3V3z" stroke-linejoin="round" />
84
+ </svg>
85
+ );
86
+ }
87
+
88
+ /** Bolt/lightning icon for default/unknown tools. */
89
+ export function BoltIcon(props: IconProps): preact.JSX.Element {
90
+ return (
91
+ <svg
92
+ viewBox="0 0 16 16"
93
+ fill="none"
94
+ stroke="currentColor"
95
+ stroke-width="1.5"
96
+ class={props.class}
97
+ >
98
+ <path d="M9 1L3 9h5l-1 6 6-8H8l1-6z" stroke-linejoin="round" />
99
+ </svg>
100
+ );
101
+ }
@@ -0,0 +1,20 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+
3
+ import type { Signal } from "@preact/signals";
4
+ import type * as preact from "preact";
5
+ import { ThinkingIndicator } from "./thinking_indicator.tsx";
6
+
7
+ export function Transcript({
8
+ userUtterance,
9
+ }: {
10
+ userUtterance: Signal<string | null>;
11
+ }): preact.JSX.Element | null {
12
+ if (userUtterance.value === null) return null;
13
+ return (
14
+ <div class="flex flex-col items-end w-full">
15
+ <div class="max-w-[min(82%,64ch)] whitespace-pre-wrap wrap-break-word text-sm leading-[150%] text-aai-text-muted bg-aai-surface-faint border border-aai-border px-3 py-2 rounded-aai ml-auto">
16
+ {userUtterance.value || <ThinkingIndicator />}
17
+ </div>
18
+ </div>
19
+ );
20
+ }
package/ui/audio.ts ADDED
@@ -0,0 +1,170 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ import { MIC_BUFFER_SECONDS } from "./types.ts";
3
+
4
+ /** Configuration for creating a {@linkcode VoiceIO} instance. */
5
+ export type VoiceIOOptions = {
6
+ /** Sample rate in Hz expected by the STT engine (e.g. 16000). */
7
+ sttSampleRate: number;
8
+ /** Sample rate in Hz used by the TTS engine (e.g. 22050). */
9
+ ttsSampleRate: number;
10
+ /** Source URL or data URI for the capture AudioWorklet processor. */
11
+ captureWorkletSrc: string;
12
+ /** Source URL or data URI for the playback AudioWorklet processor. */
13
+ playbackWorkletSrc: string;
14
+ /** Callback invoked with buffered PCM16 microphone data to send to the server. */
15
+ onMicData: (pcm16: ArrayBuffer) => void;
16
+ /** Callback invoked with the current playback position in samples. */
17
+ onPlaybackProgress?: (samplesPlayed: number) => void;
18
+ };
19
+
20
+ /**
21
+ * Audio I/O interface for voice capture and playback.
22
+ *
23
+ * Manages microphone capture via an AudioWorklet, resampling to the STT
24
+ * sample rate, and TTS audio playback through a second AudioWorklet. Implements
25
+ * {@linkcode AsyncDisposable} for resource cleanup.
26
+ */
27
+ export type VoiceIO = AsyncDisposable & {
28
+ /** Enqueue a PCM16 audio buffer for playback through the TTS pipeline. */
29
+ enqueue(pcm16Buffer: ArrayBuffer): void;
30
+ /** Signal that all TTS audio for the current turn has been enqueued.
31
+ * Resolves when the worklet has finished playing all buffered audio. */
32
+ done(): Promise<void>;
33
+ /** Immediately stop playback and discard any buffered TTS audio. */
34
+ flush(): void;
35
+ /** Release all audio resources (microphone, AudioContext, worklets). */
36
+ close(): Promise<void>;
37
+ };
38
+
39
+ async function loadWorklet(ctx: AudioContext, source: string): Promise<void> {
40
+ await ctx.audioWorklet.addModule(source);
41
+ }
42
+
43
+ /**
44
+ * Create a {@linkcode VoiceIO} instance that captures microphone audio and
45
+ * plays back TTS audio using the Web Audio API.
46
+ *
47
+ * The AudioContext runs at the TTS sample rate for playback fidelity.
48
+ * Captured audio is resampled to the STT rate when the rates differ.
49
+ *
50
+ * @param opts - Voice I/O configuration options.
51
+ * @returns A promise that resolves to a {@linkcode VoiceIO} handle.
52
+ * @throws If microphone access is denied or AudioWorklet registration fails.
53
+ */
54
+ export async function createVoiceIO(opts: VoiceIOOptions): Promise<VoiceIO> {
55
+ const { sttSampleRate, ttsSampleRate, captureWorkletSrc, playbackWorkletSrc, onMicData } = opts;
56
+
57
+ // Use TTS rate for the context — playback fidelity is more perceptible.
58
+ // Capture path resamples to STT rate if they differ.
59
+ const contextRate = ttsSampleRate;
60
+ const ctx = new AudioContext({
61
+ sampleRate: contextRate,
62
+ latencyHint: "playback",
63
+ });
64
+ await ctx.resume();
65
+
66
+ // Single AudioContext owns both capture and playback — required for AEC.
67
+ const stream = await navigator.mediaDevices.getUserMedia({
68
+ audio: {
69
+ sampleRate: contextRate,
70
+ echoCancellation: true,
71
+ noiseSuppression: true,
72
+ autoGainControl: true,
73
+ },
74
+ });
75
+
76
+ try {
77
+ await Promise.all([loadWorklet(ctx, captureWorkletSrc), loadWorklet(ctx, playbackWorkletSrc)]);
78
+ } catch (err: unknown) {
79
+ for (const t of stream.getTracks()) t.stop();
80
+ await ctx.close().catch(() => {});
81
+ throw err;
82
+ }
83
+
84
+ const mic = ctx.createMediaStreamSource(stream);
85
+ const capNode = new AudioWorkletNode(ctx, "capture-processor", {
86
+ channelCount: 1,
87
+ channelCountMode: "explicit",
88
+ processorOptions: { contextRate, sttSampleRate },
89
+ });
90
+ mic.connect(capNode);
91
+
92
+ // Worklet outputs PCM16 at the STT rate — just batch and send.
93
+ const chunkSizeBytes = Math.floor(sttSampleRate * MIC_BUFFER_SECONDS) * 2;
94
+ const capBuf = new Uint8Array(chunkSizeBytes * 2);
95
+ let capOffset = 0;
96
+
97
+ capNode.port.postMessage({ event: "start" });
98
+
99
+ capNode.port.onmessage = (e: MessageEvent) => {
100
+ if (e.data.event !== "chunk") return;
101
+ const chunk = new Uint8Array(e.data.buffer as ArrayBuffer);
102
+
103
+ capBuf.set(chunk, capOffset);
104
+ capOffset += chunk.byteLength;
105
+
106
+ if (capOffset >= chunkSizeBytes) {
107
+ onMicData(capBuf.slice(0, capOffset).buffer);
108
+ capOffset = 0;
109
+ }
110
+ };
111
+
112
+ let playNode: AudioWorkletNode | null = null;
113
+ let onPlaybackStop: (() => void) | null = null;
114
+ const lifecycle = new AbortController();
115
+ const { onPlaybackProgress } = opts;
116
+
117
+ function ensurePlayNode(): AudioWorkletNode {
118
+ if (playNode) return playNode;
119
+ const node = new AudioWorkletNode(ctx, "playback-processor", {
120
+ processorOptions: { sampleRate: contextRate },
121
+ });
122
+ node.connect(ctx.destination);
123
+ node.port.onmessage = (e: MessageEvent) => {
124
+ if (e.data.event === "stop") {
125
+ node.disconnect();
126
+ if (playNode === node) playNode = null;
127
+ onPlaybackStop?.();
128
+ onPlaybackStop = null;
129
+ } else if (e.data.event === "progress") {
130
+ onPlaybackProgress?.(e.data.readPos);
131
+ }
132
+ };
133
+ playNode = node;
134
+ return node;
135
+ }
136
+
137
+ const io: VoiceIO = {
138
+ enqueue(pcm16Buffer: ArrayBuffer) {
139
+ if (lifecycle.signal.aborted) return;
140
+ if (pcm16Buffer.byteLength === 0) return;
141
+ const node = ensurePlayNode();
142
+ node.port.postMessage({ event: "write", buffer: new Uint8Array(pcm16Buffer) }, [pcm16Buffer]);
143
+ },
144
+
145
+ done() {
146
+ if (!playNode) return Promise.resolve();
147
+ return new Promise<void>((resolve) => {
148
+ onPlaybackStop = resolve;
149
+ playNode!.port.postMessage({ event: "done" });
150
+ });
151
+ },
152
+
153
+ flush() {
154
+ if (playNode) playNode.port.postMessage({ event: "interrupt" });
155
+ },
156
+
157
+ async close() {
158
+ if (lifecycle.signal.aborted) return;
159
+ lifecycle.abort();
160
+ capNode.port.postMessage({ event: "stop" });
161
+ for (const t of stream.getTracks()) t.stop();
162
+ await ctx.close().catch(() => {});
163
+ },
164
+
165
+ async [Symbol.asyncDispose]() {
166
+ await io.close();
167
+ },
168
+ };
169
+ return io;
170
+ }
@@ -0,0 +1,49 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ /**
3
+ * Preact UI components for the voice agent interface.
4
+ *
5
+ * Re-exports from _components/ with explicit type annotations so JSR can
6
+ * generate .d.ts without needing to analyze source files.
7
+ */
8
+
9
+ import type { Signal } from "@preact/signals";
10
+ import type * as preact from "preact";
11
+ import { App as _App } from "./_components/app.tsx";
12
+ import { ChatView as _ChatView } from "./_components/chat_view.tsx";
13
+ import { ErrorBanner as _ErrorBanner } from "./_components/error_banner.tsx";
14
+ import { MessageBubble as _MessageBubble } from "./_components/message_bubble.tsx";
15
+ import { StateIndicator as _StateIndicator } from "./_components/state_indicator.tsx";
16
+ import { ThinkingIndicator as _ThinkingIndicator } from "./_components/thinking_indicator.tsx";
17
+ import { ToolCallBlock as _ToolCallBlock } from "./_components/tool_call_block.tsx";
18
+ import { Transcript as _Transcript } from "./_components/transcript.tsx";
19
+ import type { AgentState, Message, SessionError, ToolCallInfo } from "./types.ts";
20
+
21
+ /** Displays the current agent state as a colored indicator. */
22
+ export const StateIndicator: (props: { state: Signal<AgentState> }) => preact.JSX.Element =
23
+ _StateIndicator;
24
+
25
+ /** Displays an error message banner when an error is present. */
26
+ export const ErrorBanner: (props: {
27
+ error: Signal<SessionError | null>;
28
+ }) => preact.JSX.Element | null = _ErrorBanner;
29
+
30
+ /** Renders a single chat message bubble. */
31
+ export const MessageBubble: (props: { message: Message }) => preact.JSX.Element = _MessageBubble;
32
+
33
+ /** Renders a collapsible tool call block. */
34
+ export const ToolCallBlock: (props: { toolCall: ToolCallInfo }) => preact.JSX.Element =
35
+ _ToolCallBlock;
36
+
37
+ /** Displays the live user utterance from STT/VAD. */
38
+ export const Transcript: (props: {
39
+ userUtterance: Signal<string | null>;
40
+ }) => preact.JSX.Element | null = _Transcript;
41
+
42
+ /** Animated indicator shown while the agent is processing. */
43
+ export const ThinkingIndicator: () => preact.JSX.Element = _ThinkingIndicator;
44
+
45
+ /** Full chat view showing messages, transcript, and thinking state. */
46
+ export const ChatView: () => preact.JSX.Element = _ChatView;
47
+
48
+ /** Default top-level app component with start screen and chat view. */
49
+ export const App: () => preact.JSX.Element = _App;
@@ -0,0 +1,37 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ /**
3
+ * Preact UI components for AAI voice agents.
4
+ *
5
+ * Provides ready-made components, session context, and mount helpers.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * import { App, mount } from "@aai/ui/components";
10
+ *
11
+ * mount(App, { target: "#app" });
12
+ * ```
13
+ *
14
+ * @module
15
+ */
16
+
17
+ export {
18
+ App,
19
+ ChatView,
20
+ ErrorBanner,
21
+ MessageBubble,
22
+ StateIndicator,
23
+ ThinkingIndicator,
24
+ ToolCallBlock,
25
+ Transcript,
26
+ } from "./components.ts";
27
+ export type { MountHandle, MountOptions, MountTheme } from "./mount.tsx";
28
+
29
+ export { mount } from "./mount.tsx";
30
+ export type { MountConfig } from "./mount_context.ts";
31
+ export { useMountConfig } from "./mount_context.ts";
32
+ export type { SessionSignals } from "./signals.ts";
33
+ export {
34
+ createSessionControls,
35
+ SessionProvider,
36
+ useSession,
37
+ } from "./signals.ts";
package/ui/mod.ts ADDED
@@ -0,0 +1,48 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ /**
3
+ * Browser client library for AAI voice agents.
4
+ *
5
+ * Provides WebSocket session management, audio capture/playback,
6
+ * and Preact UI components. For narrower imports, use the sub-path exports:
7
+ *
8
+ * - `@aai/ui/session` — WebSocket session only (no Preact dependency)
9
+ * - `@aai/ui/components` — Preact components, mount helpers, and signals
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * import { App, mount } from "@aai/ui";
14
+ *
15
+ * mount(App, { target: "#app" });
16
+ * ```
17
+ *
18
+ * @module
19
+ */
20
+
21
+ export {
22
+ App,
23
+ ChatView,
24
+ createSessionControls,
25
+ ErrorBanner,
26
+ MessageBubble,
27
+ type MountConfig,
28
+ type MountHandle,
29
+ type MountOptions,
30
+ type MountTheme,
31
+ mount,
32
+ SessionProvider,
33
+ type SessionSignals,
34
+ StateIndicator,
35
+ ThinkingIndicator,
36
+ Transcript,
37
+ useMountConfig,
38
+ useSession,
39
+ } from "./components_mod.ts";
40
+ export {
41
+ type AgentState,
42
+ createVoiceSession,
43
+ type Message,
44
+ type SessionError,
45
+ type SessionErrorCode,
46
+ type SessionOptions,
47
+ type VoiceSession,
48
+ } from "./session_mod.ts";