@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/sdk/s2s.ts ADDED
@@ -0,0 +1,271 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ /**
3
+ * Speech-to-Speech WebSocket client for AssemblyAI's S2S API.
4
+ *
5
+ * Cross-runtime: accepts a WebSocket factory and Logger instead of
6
+ * importing `ws` or `@std/log` directly.
7
+ *
8
+ * @module
9
+ */
10
+ import type { Logger, S2SConfig } from "./runtime.ts";
11
+ import { consoleLogger } from "./runtime.ts";
12
+
13
+ // ─── Cross-runtime base64 helpers ───────────────────────────────────────────
14
+
15
+ function uint8ToBase64(bytes: Uint8Array): string {
16
+ const CHUNK = 0x8000; // 32KB chunks to avoid call stack limits
17
+ const parts: string[] = [];
18
+ for (let i = 0; i < bytes.length; i += CHUNK) {
19
+ parts.push(String.fromCharCode(...bytes.subarray(i, i + CHUNK)));
20
+ }
21
+ return btoa(parts.join(""));
22
+ }
23
+
24
+ function base64ToUint8(base64: string): Uint8Array {
25
+ const binary = atob(base64);
26
+ const bytes = new Uint8Array(binary.length);
27
+ for (let i = 0; i < binary.length; i++) {
28
+ bytes[i] = binary.charCodeAt(i);
29
+ }
30
+ return bytes;
31
+ }
32
+
33
+ // ─── WebSocket abstraction ──────────────────────────────────────────────────
34
+
35
+ /** Minimal WebSocket interface for the S2S client. */
36
+ export type S2sWebSocket = {
37
+ readonly readyState: number;
38
+ send(data: string): void;
39
+ close(): void;
40
+ on(event: string, handler: (...args: unknown[]) => void): void;
41
+ };
42
+
43
+ /** WebSocket readyState constant for OPEN. */
44
+ const WS_OPEN = 1;
45
+
46
+ /** Factory for creating WebSocket connections (e.g. the `ws` package). */
47
+ export type CreateS2sWebSocket = (
48
+ url: string,
49
+ opts: { headers: Record<string, string> },
50
+ ) => S2sWebSocket;
51
+
52
+ // ─── Types ──────────────────────────────────────────────────────────────────
53
+
54
+ export type S2sSessionConfig = {
55
+ system_prompt: string;
56
+ tools: S2sToolSchema[];
57
+ greeting?: string;
58
+ };
59
+
60
+ export type S2sToolSchema = {
61
+ type: "function";
62
+ name: string;
63
+ description: string;
64
+ parameters: Record<string, unknown>;
65
+ };
66
+
67
+ export type S2sToolCall = {
68
+ call_id: string;
69
+ name: string;
70
+ args: Record<string, unknown>;
71
+ };
72
+
73
+ export type S2sHandle = EventTarget & {
74
+ sendAudio(audio: Uint8Array): void;
75
+ sendToolResult(callId: string, result: string): void;
76
+ updateSession(config: S2sSessionConfig): void;
77
+ resumeSession(sessionId: string): void;
78
+ close(): void;
79
+ };
80
+
81
+ // ─── Connect ────────────────────────────────────────────────────────────────
82
+
83
+ export type ConnectS2sOptions = {
84
+ apiKey: string;
85
+ config: S2SConfig;
86
+ createWebSocket: CreateS2sWebSocket;
87
+ logger?: Logger;
88
+ };
89
+
90
+ /**
91
+ * Connect to AssemblyAI's Speech-to-Speech WebSocket API.
92
+ *
93
+ * Returns an {@linkcode S2sHandle} that extends EventTarget. Consumers
94
+ * listen for events: `ready`, `speech_started`, `speech_stopped`,
95
+ * `user_transcript_delta`, `user_transcript`, `reply_started`,
96
+ * `reply_done`, `audio`, `agent_transcript`, `tool_call`,
97
+ * `session_expired`, `error`, `close`.
98
+ */
99
+ export function connectS2s(opts: ConnectS2sOptions): Promise<S2sHandle> {
100
+ const { apiKey, config, createWebSocket, logger: log = consoleLogger } = opts;
101
+
102
+ return new Promise((resolve, reject) => {
103
+ log.debug("S2S connecting", { url: config.wssUrl });
104
+
105
+ const ws = createWebSocket(config.wssUrl, {
106
+ headers: { Authorization: `Bearer ${apiKey}` },
107
+ });
108
+
109
+ const target = new EventTarget();
110
+ let opened = false;
111
+
112
+ function send(msg: Record<string, unknown>): void {
113
+ if (ws.readyState !== WS_OPEN) return;
114
+ const type = msg.type as string;
115
+ if (type !== "input.audio") {
116
+ log.info(`S2S >> ${JSON.stringify(msg)}`);
117
+ }
118
+ ws.send(JSON.stringify(msg));
119
+ }
120
+
121
+ const handle: S2sHandle = Object.assign(target, {
122
+ sendAudio(audio: Uint8Array): void {
123
+ send({ type: "input.audio", audio: uint8ToBase64(audio) });
124
+ },
125
+
126
+ sendToolResult(callId: string, result: string): void {
127
+ send({ type: "tool.result", call_id: callId, result });
128
+ },
129
+
130
+ updateSession(sessionConfig: S2sSessionConfig): void {
131
+ send({ type: "session.update", session: sessionConfig });
132
+ },
133
+
134
+ resumeSession(sessionId: string): void {
135
+ send({ type: "session.resume", session_id: sessionId });
136
+ },
137
+
138
+ close(): void {
139
+ log.debug("S2S closing");
140
+ ws.close();
141
+ },
142
+ });
143
+
144
+ ws.on("open", () => {
145
+ opened = true;
146
+ log.info("S2S WebSocket open");
147
+ resolve(handle);
148
+ });
149
+
150
+ ws.on("message", (data: unknown) => {
151
+ let msg: Record<string, unknown>;
152
+ try {
153
+ msg = JSON.parse(String(data));
154
+ } catch {
155
+ return;
156
+ }
157
+
158
+ const type = msg.type as string;
159
+
160
+ if (type !== "reply.audio") {
161
+ log.info(`S2S << ${JSON.stringify(msg)}`);
162
+ }
163
+
164
+ switch (type) {
165
+ case "session.ready":
166
+ target.dispatchEvent(
167
+ new CustomEvent("ready", {
168
+ detail: { session_id: msg.session_id as string },
169
+ }),
170
+ );
171
+ break;
172
+ case "session.updated":
173
+ target.dispatchEvent(new CustomEvent("session_updated", { detail: msg }));
174
+ break;
175
+ case "input.speech.started":
176
+ target.dispatchEvent(new CustomEvent("speech_started"));
177
+ break;
178
+ case "input.speech.stopped":
179
+ target.dispatchEvent(new CustomEvent("speech_stopped"));
180
+ break;
181
+ case "transcript.user.delta":
182
+ target.dispatchEvent(
183
+ new CustomEvent("user_transcript_delta", {
184
+ detail: { text: msg.text as string },
185
+ }),
186
+ );
187
+ break;
188
+ case "transcript.user":
189
+ target.dispatchEvent(
190
+ new CustomEvent("user_transcript", {
191
+ detail: {
192
+ item_id: msg.item_id as string,
193
+ text: msg.text as string,
194
+ },
195
+ }),
196
+ );
197
+ break;
198
+ case "reply.started":
199
+ target.dispatchEvent(
200
+ new CustomEvent("reply_started", {
201
+ detail: { reply_id: msg.reply_id as string },
202
+ }),
203
+ );
204
+ break;
205
+ case "reply.audio": {
206
+ const audioBytes = base64ToUint8(msg.data as string);
207
+ target.dispatchEvent(new CustomEvent("audio", { detail: { audio: audioBytes } }));
208
+ break;
209
+ }
210
+ case "transcript.agent":
211
+ target.dispatchEvent(
212
+ new CustomEvent("agent_transcript", {
213
+ detail: { text: msg.text as string },
214
+ }),
215
+ );
216
+ break;
217
+ case "tool.call":
218
+ target.dispatchEvent(
219
+ new CustomEvent("tool_call", {
220
+ detail: {
221
+ call_id: msg.call_id as string,
222
+ name: msg.name as string,
223
+ args: (msg.args ?? {}) as Record<string, unknown>,
224
+ },
225
+ }),
226
+ );
227
+ break;
228
+ case "reply.done":
229
+ target.dispatchEvent(
230
+ new CustomEvent("reply_done", {
231
+ detail: { status: (msg.status as string) ?? undefined },
232
+ }),
233
+ );
234
+ break;
235
+ case "session.error": {
236
+ const code = msg.code as string;
237
+ const isExpired = code === "session_not_found" || code === "session_forbidden";
238
+ target.dispatchEvent(
239
+ new CustomEvent(isExpired ? "session_expired" : "error", {
240
+ detail: { code, message: msg.message as string },
241
+ }),
242
+ );
243
+ break;
244
+ }
245
+ }
246
+ });
247
+
248
+ ws.on("close", (code: unknown, reason: unknown) => {
249
+ log.info("S2S WebSocket closed", {
250
+ code: code as number,
251
+ reason:
252
+ reason instanceof Uint8Array ? new TextDecoder().decode(reason) : String(reason ?? ""),
253
+ });
254
+ target.dispatchEvent(new CustomEvent("close"));
255
+ });
256
+
257
+ ws.on("error", (err: unknown) => {
258
+ const errObj = err instanceof Error ? err : new Error(String(err));
259
+ log.error("S2S WebSocket error", { error: errObj.message });
260
+ if (!opened) {
261
+ reject(errObj);
262
+ } else {
263
+ target.dispatchEvent(
264
+ new CustomEvent("error", {
265
+ detail: { code: "ws_error", message: errObj.message },
266
+ }),
267
+ );
268
+ }
269
+ });
270
+ });
271
+ }
package/sdk/server.ts ADDED
@@ -0,0 +1,198 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ /**
3
+ * Self-hostable agent server.
4
+ *
5
+ * `createServer()` returns a server with a standard `fetch(Request): Response`
6
+ * handler and a Node.js `listen()` method for HTTP + WebSocket.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ import type { Kv } from "./kv.ts";
12
+ import type { Logger, S2SConfig } from "./runtime.ts";
13
+ import { consoleLogger, DEFAULT_S2S_CONFIG } from "./runtime.ts";
14
+ import type { CreateS2sWebSocket, S2sWebSocket } from "./s2s.ts";
15
+ import type { AgentDef } from "./types.ts";
16
+ import { createWintercServer, type WintercServer } from "./winterc_server.ts";
17
+
18
+ export type ServerOptions = {
19
+ /** The agent definition returned by `defineAgent()`. */
20
+ agent: AgentDef;
21
+ /** Environment variables. Defaults to `process.env`. */
22
+ env?: Record<string, string>;
23
+ /** KV store. Defaults to in-memory. */
24
+ kv?: Kv;
25
+ /** HTML to serve at `GET /`. */
26
+ clientHtml?: string;
27
+ /** Logger. Defaults to console. */
28
+ logger?: Logger;
29
+ /** S2S configuration. Defaults to AssemblyAI production. */
30
+ s2sConfig?: S2SConfig;
31
+ /** WebSocket factory for S2S connections. Auto-detected if not provided. */
32
+ createWebSocket?: CreateS2sWebSocket;
33
+ };
34
+
35
+ export type AgentServer = {
36
+ /** Standard fetch handler using web `Request`/`Response` types. */
37
+ fetch(request: Request): Promise<Response>;
38
+ /** Start listening on the given port. */
39
+ listen(port?: number): Promise<void>;
40
+ /** Stop the server. */
41
+ close(): Promise<void>;
42
+ };
43
+
44
+ /** Try to load the `ws` package for WebSocket connections. */
45
+ async function loadWsFactory(): Promise<CreateS2sWebSocket> {
46
+ try {
47
+ const mod = await import("ws");
48
+ const WS = mod.default ?? mod;
49
+ return (url: string, opts: { headers: Record<string, string> }) =>
50
+ new WS(url, { headers: opts.headers }) as unknown as S2sWebSocket;
51
+ } catch {
52
+ throw new Error(
53
+ "WebSocket factory not provided and `ws` package not found. " +
54
+ "Install `ws` (`npm install ws`) or pass `createWebSocket` option.",
55
+ );
56
+ }
57
+ }
58
+
59
+ /** Filter env to only defined string values. */
60
+ function resolveEnv(env: Record<string, string | undefined>): Record<string, string> {
61
+ const resolved: Record<string, string> = {};
62
+ for (const [key, value] of Object.entries(env)) {
63
+ if (value !== undefined) resolved[key] = value;
64
+ }
65
+ return resolved;
66
+ }
67
+
68
+ /**
69
+ * Create a self-hostable agent server.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * import { defineAgent } from "aai";
74
+ * import { createServer } from "aai/server";
75
+ *
76
+ * const agent = defineAgent({ name: "my-agent" });
77
+ * const server = createServer({ agent });
78
+ * await server.listen(3000);
79
+ * ```
80
+ */
81
+ export function createServer(options: ServerOptions): AgentServer {
82
+ const { agent, kv, clientHtml, logger = consoleLogger, s2sConfig = DEFAULT_S2S_CONFIG } = options;
83
+
84
+ const env = resolveEnv(
85
+ options.env ?? (typeof process !== "undefined" ? (process.env as Record<string, string>) : {}),
86
+ );
87
+
88
+ let wsFactory: CreateS2sWebSocket | null = options.createWebSocket ?? null;
89
+
90
+ async function getWsFactory(): Promise<CreateS2sWebSocket> {
91
+ if (!wsFactory) {
92
+ wsFactory = await loadWsFactory();
93
+ }
94
+ return wsFactory;
95
+ }
96
+
97
+ // WintercServer is created lazily after wsFactory is resolved
98
+ let winterc: WintercServer | null = null;
99
+
100
+ function getWinterc(): WintercServer {
101
+ if (!winterc) {
102
+ winterc = createWintercServer({
103
+ agent,
104
+ env,
105
+ ...(kv ? { kv } : {}),
106
+ createWebSocket: wsFactory!,
107
+ clientHtml,
108
+ logger,
109
+ s2sConfig,
110
+ });
111
+ }
112
+ return winterc;
113
+ }
114
+
115
+ let serverHandle: { shutdown(): Promise<void> } | null = null;
116
+
117
+ return {
118
+ fetch(request: Request) {
119
+ return getWinterc().fetch(request);
120
+ },
121
+
122
+ async listen(port = 3000) {
123
+ // Ensure WS factory is loaded before starting
124
+ await getWsFactory();
125
+
126
+ const http = await import("node:http");
127
+
128
+ const nodeServer = http.createServer(async (req, res) => {
129
+ try {
130
+ const protocol = (req.socket as { encrypted?: boolean }).encrypted ? "https" : "http";
131
+ const host = req.headers.host ?? `localhost:${port}`;
132
+ const url = new URL(req.url ?? "/", `${protocol}://${host}`);
133
+ const headers = new Headers();
134
+ for (const [key, val] of Object.entries(req.headers)) {
135
+ if (val) headers.set(key, Array.isArray(val) ? val[0]! : val);
136
+ }
137
+ const request = new Request(url, {
138
+ method: req.method ?? "GET",
139
+ headers,
140
+ });
141
+ const response = await getWinterc().fetch(request);
142
+ res.writeHead(response.status, Object.fromEntries(response.headers));
143
+ const body = await response.text();
144
+ res.end(body);
145
+ } catch (err: unknown) {
146
+ res.writeHead(500);
147
+ res.end(err instanceof Error ? err.message : "Internal Server Error");
148
+ }
149
+ });
150
+
151
+ // WebSocket upgrade via ws package
152
+ try {
153
+ const wsMod = await import("ws");
154
+ const WSServer = wsMod.WebSocketServer ?? wsMod.default?.Server;
155
+ if (WSServer) {
156
+ const wss = new WSServer({ noServer: true });
157
+ nodeServer.on("upgrade", (req: unknown, socket: unknown, head: unknown) => {
158
+ wss.handleUpgrade(
159
+ req as Parameters<typeof wss.handleUpgrade>[0],
160
+ socket as Parameters<typeof wss.handleUpgrade>[1],
161
+ head as Parameters<typeof wss.handleUpgrade>[2],
162
+ (ws: WebSocket) => {
163
+ const reqUrl = new URL(
164
+ (req as { url?: string }).url ?? "/",
165
+ `http://localhost:${port}`,
166
+ );
167
+ const resume = reqUrl.searchParams.has("resume");
168
+ getWinterc().handleWebSocket(ws, { skipGreeting: resume });
169
+ },
170
+ );
171
+ });
172
+ }
173
+ } catch {
174
+ logger.warn("ws package not available for Node.js WebSocket upgrade");
175
+ }
176
+
177
+ await new Promise<void>((resolve) => {
178
+ nodeServer.listen(port, () => {
179
+ logger.info(`Agent "${agent.name}" listening on http://localhost:${port}`);
180
+ resolve();
181
+ });
182
+ });
183
+
184
+ serverHandle = {
185
+ async shutdown() {
186
+ await new Promise<void>((resolve, reject) => {
187
+ nodeServer.close((err) => (err ? reject(err) : resolve()));
188
+ });
189
+ },
190
+ };
191
+ },
192
+
193
+ async close() {
194
+ await winterc?.close();
195
+ await serverHandle?.shutdown();
196
+ },
197
+ };
198
+ }