@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/types.ts ADDED
@@ -0,0 +1,406 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ /**
3
+ * Core type definitions for the AAI agent SDK.
4
+ *
5
+ * @module
6
+ */
7
+
8
+ import type { z } from "zod";
9
+ import type { Kv } from "./kv.ts";
10
+
11
+ /** Result of the {@linkcode AgentOptions.onBeforeStep} hook. */
12
+ export type BeforeStepResult = { activeTools?: string[] } | undefined;
13
+
14
+ /**
15
+ * Transport protocol for client-server communication.
16
+ *
17
+ * - `"websocket"` — Browser-based WebSocket connection (default).
18
+ */
19
+ export type Transport = "websocket";
20
+
21
+ /**
22
+ * Voice pipeline mode.
23
+ *
24
+ * `"s2s"` — AssemblyAI Speech-to-Speech API. Single WebSocket handles
25
+ * STT, LLM, and TTS with the lowest latency.
26
+ */
27
+ export type PipelineMode = "s2s";
28
+
29
+ /** @internal Normalize a transport value to an array of transports. */
30
+ export function normalizeTransport(
31
+ value: Transport | readonly Transport[] | undefined,
32
+ ): readonly Transport[] {
33
+ if (value === undefined) return ["websocket"];
34
+ if (typeof value === "string") return [value];
35
+ return value;
36
+ }
37
+
38
+ /**
39
+ * Identifier for a built-in server-side tool.
40
+ *
41
+ * Built-in tools run on the host process (not inside the sandboxed worker)
42
+ * and provide capabilities like web search, code execution, and API access.
43
+ *
44
+ * - `"web_search"` — Search the web for current information, facts, or news.
45
+ * - `"visit_webpage"` — Fetch a URL and return its content as Markdown.
46
+ * - `"fetch_json"` — Call a REST API endpoint and return the JSON response.
47
+ * - `"run_code"` — Execute JavaScript in a sandbox for calculations and data processing.
48
+ * - `"vector_search"` — Search the agent's RAG knowledge base for relevant documents.
49
+ */
50
+ export type BuiltinTool =
51
+ | "web_search"
52
+ | "visit_webpage"
53
+ | "fetch_json"
54
+ | "run_code"
55
+ | "vector_search";
56
+
57
+ /**
58
+ * How the LLM should select tools during a turn.
59
+ *
60
+ * - `"auto"` — The model decides whether to call a tool.
61
+ * - `"required"` — The model must call at least one tool.
62
+ * - `"none"` — Tool calling is disabled.
63
+ * - `{ type: "tool"; toolName: string }` — Force a specific tool.
64
+ */
65
+ export type ToolChoice = "auto" | "required" | "none" | { type: "tool"; toolName: string };
66
+
67
+ /**
68
+ * A single message in the conversation history.
69
+ *
70
+ * Messages are passed to tool `execute` functions via
71
+ * {@linkcode ToolContext.messages} to provide conversation context.
72
+ */
73
+ export type Message = {
74
+ /** The role of the message sender. */
75
+ role: "user" | "assistant" | "tool";
76
+ /** The text content of the message. */
77
+ content: string;
78
+ };
79
+
80
+ /**
81
+ * Context passed to tool `execute` functions.
82
+ *
83
+ * Provides access to the session environment, state, KV store, and
84
+ * conversation history from within a tool's execute handler.
85
+ *
86
+ * @typeParam S The shape of per-session state created by the agent's
87
+ * `state` factory. Defaults to `Record<string, unknown>`.
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * import { type ToolDef } from "aai";
92
+ * import { z } from "zod";
93
+ *
94
+ * const myTool: ToolDef = {
95
+ * description: "Look up a value from the KV store",
96
+ * parameters: z.object({ key: z.string() }),
97
+ * execute: async ({ key }, ctx) => {
98
+ * const value = await ctx.kv.get(key);
99
+ * return { key, value };
100
+ * },
101
+ * };
102
+ * ```
103
+ */
104
+ export type ToolContext<S = Record<string, unknown>> = {
105
+ /** Unique identifier for the current session. */
106
+ sessionId: string;
107
+ /** Environment variables declared in the agent config. */
108
+ env: Readonly<Record<string, string>>;
109
+ /** Signal that aborts when the tool execution times out. */
110
+ abortSignal?: AbortSignal;
111
+ /** Mutable per-session state created by the agent's `state` factory. */
112
+ state: S;
113
+ /** Key-value store scoped to this agent deployment. */
114
+ kv: Kv;
115
+ /** Read-only snapshot of conversation messages so far. */
116
+ messages: readonly Message[];
117
+ };
118
+
119
+ /**
120
+ * Context passed to lifecycle hooks (`onConnect`, `onTurn`, etc.).
121
+ *
122
+ * Similar to {@linkcode ToolContext} but without `messages` or `abortSignal`,
123
+ * since hooks run outside the tool execution flow.
124
+ *
125
+ * @typeParam S The shape of per-session state created by the agent's
126
+ * `state` factory. Defaults to `Record<string, unknown>`.
127
+ */
128
+ export type HookContext<S = Record<string, unknown>> = {
129
+ /** Unique identifier for the current session. */
130
+ sessionId: string;
131
+ /** Environment variables declared in the agent config. */
132
+ env: Readonly<Record<string, string>>;
133
+ /** Mutable per-session state created by the agent's `state` factory. */
134
+ state: S;
135
+ /** Key-value store scoped to this agent deployment. */
136
+ kv: Kv;
137
+ };
138
+
139
+ /**
140
+ * Definition of a custom tool that the agent can invoke.
141
+ *
142
+ * Tools are the primary way to extend agent capabilities. Each tool has a
143
+ * description (shown to the LLM), optional Zod parameters schema, and an
144
+ * `execute` function that runs inside the sandboxed worker.
145
+ *
146
+ * @typeParam P A Zod object schema describing the tool's parameters.
147
+ * Defaults to `any` so tools without parameters don't need an explicit
148
+ * type argument.
149
+ *
150
+ * @example
151
+ * ```ts
152
+ * import { type ToolDef } from "aai";
153
+ * import { z } from "zod";
154
+ *
155
+ * const weatherTool: ToolDef<typeof params> = {
156
+ * description: "Get current weather for a city",
157
+ * parameters: z.object({
158
+ * city: z.string().describe("City name"),
159
+ * }),
160
+ * execute: async ({ city }) => {
161
+ * const res = await fetch(`https://wttr.in/${city}?format=j1`);
162
+ * return await res.json();
163
+ * },
164
+ * };
165
+ *
166
+ * const params = z.object({ city: z.string() });
167
+ * ```
168
+ */
169
+ export type ToolDef<
170
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
171
+ P extends z.ZodObject<z.ZodRawShape> = any,
172
+ S = Record<string, unknown>,
173
+ > = {
174
+ /** Human-readable description shown to the LLM. */
175
+ description: string;
176
+ /** Zod schema for the tool's parameters. */
177
+ parameters?: P | undefined;
178
+ /** Function that executes the tool and returns a result. */
179
+ execute(args: z.infer<P>, ctx: ToolContext<S>): Promise<unknown> | unknown;
180
+ };
181
+
182
+ /**
183
+ * Available TTS voice identifiers (Cartesia voice UUIDs).
184
+ *
185
+ * Pass any Cartesia voice UUID as a string. The named constants below are
186
+ * provided for convenience — the type also accepts arbitrary strings to
187
+ * support custom or new voices without an SDK update.
188
+ *
189
+ * Browse all voices at https://play.cartesia.ai
190
+ *
191
+ * @default {"694f9389-aac1-45b6-b726-9d9369183238"} (Sarah)
192
+ */
193
+ export type Voice =
194
+ | "694f9389-aac1-45b6-b726-9d9369183238" // Sarah
195
+ | "a167e0f3-df7e-4d52-a9c3-f949145efdab" // Customer Support Man
196
+ | "829ccd10-f8b3-43cd-b8a0-4aeaa81f3b30" // Customer Support Lady
197
+ | "156fb8d2-335b-4950-9cb3-a2d33befec77" // Helpful Woman
198
+ | "248be419-c632-4f23-adf1-5324ed7dbf1d" // Professional Woman
199
+ | "e3827ec5-697a-4b7c-9704-1a23041bbc51" // Sweet Lady
200
+ | "79a125e8-cd45-4c13-8a67-188112f4dd22" // British Lady
201
+ | "00a77add-48d5-4ef6-8157-71e5437b282d" // Calm Lady
202
+ | "21b81c14-f85b-436d-aff5-43f2e788ecf8" // Laidback Woman
203
+ | "996a8b96-4804-46f0-8e05-3fd4ef1a87cd" // Storyteller Lady
204
+ | "bf991597-6c13-47e4-8411-91ec2de5c466" // Newslady
205
+ | "cd17ff2d-5ea4-4695-be8f-42193949b946" // Meditation Lady
206
+ | "15a9cd88-84b0-4a8b-95f2-5d583b54c72e" // Reading Lady
207
+ | "c2ac25f9-ecc4-4f56-9095-651354df60c0" // Commercial Lady
208
+ | "573e3144-a684-4e72-ac2b-9b2063a50b53" // Teacher Lady
209
+ | "34bde396-9fde-4ebf-ad03-e3a1d1155205" // New York Woman
210
+ | "b7d50908-b17c-442d-ad8d-810c63997ed9" // California Girl
211
+ | "043cfc81-d69f-4bee-ae1e-7862cb358650" // Australian Woman
212
+ | "a3520a8f-226a-428d-9fcd-b0a4711a6829" // Reflective Woman
213
+ | "d46abd1d-2d02-43e8-819f-51fb652c1c61" // Newsman
214
+ | "820a3788-2b37-4d21-847a-b65d8a68c99a" // Salesman
215
+ | "69267136-1bdc-412f-ad78-0caad210fb40" // Friendly Reading Man
216
+ | "f146dcec-e481-45be-8ad2-96e1e40e7f32" // Reading Man
217
+ | "34575e71-908f-4ab6-ab54-b08c95d6597d" // New York Man
218
+ | "ee7ea9f8-c0c1-498c-9279-764d6b56d189" // Polite Man
219
+ | "b043dea0-a007-4bbe-a708-769dc0d0c569" // Wise Man
220
+ | "63ff761f-c1e8-414b-b969-d1833d1c870c" // Confident British Man
221
+ | "95856005-0332-41b0-935f-352e296aa0df" // Classy British Man
222
+ | "bd9120b6-7761-47a6-a446-77ca49132781" // Tutorial Man
223
+ | "7360f116-6306-4e9a-b487-1235f35a0f21" // Commercial Man
224
+ | "5619d38c-cf51-4d8e-9575-48f61a280413" // Announcer Man
225
+ | "2ee87190-8f84-4925-97da-e52547f9462c" // Child
226
+ | (string & Record<never, never>);
227
+
228
+ /**
229
+ * Information about a completed agentic step, passed to the `onStep` hook.
230
+ *
231
+ * Each turn may consist of multiple steps (up to `maxSteps`). A step
232
+ * represents one LLM invocation that may include tool calls and text output.
233
+ */
234
+ export type StepInfo = {
235
+ /** 1-based step index within the current turn. */
236
+ stepNumber: number;
237
+ /** Tool calls made during this step. */
238
+ toolCalls: readonly {
239
+ toolName: string;
240
+ args: Readonly<Record<string, unknown>>;
241
+ }[];
242
+ /** LLM text output for this step. */
243
+ text: string;
244
+ };
245
+
246
+ /**
247
+ * Options passed to {@linkcode defineAgent} to configure an agent.
248
+ *
249
+ * Only `name` is required; all other fields have sensible defaults.
250
+ *
251
+ * @typeParam S The shape of per-session state returned by the `state`
252
+ * factory. Defaults to `any`.
253
+ *
254
+ * @example
255
+ * ```ts
256
+ * import { defineAgent } from "aai";
257
+ * import { z } from "zod";
258
+ *
259
+ * export default defineAgent({
260
+ * name: "research-bot",
261
+ * instructions: "You help users research topics.",
262
+ * voice: "orion",
263
+ * builtinTools: ["web_search"],
264
+ * tools: {
265
+ * summarize: {
266
+ * description: "Summarize text",
267
+ * parameters: z.object({ text: z.string() }),
268
+ * execute: ({ text }) => text.slice(0, 200) + "...",
269
+ * },
270
+ * },
271
+ * });
272
+ * ```
273
+ */
274
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
275
+ export type AgentOptions<S = any> = {
276
+ /** Display name for the agent. */
277
+ name: string;
278
+ /**
279
+ * Environment variable names the agent requires at deploy time.
280
+ *
281
+ * @default {["ASSEMBLYAI_API_KEY"]}
282
+ */
283
+ env?: readonly string[];
284
+ /**
285
+ * Transport(s) the agent supports.
286
+ *
287
+ * @default {"websocket"}
288
+ */
289
+ transport?: Transport | readonly Transport[];
290
+ /**
291
+ * Voice pipeline mode.
292
+ *
293
+ * @default {"s2s"}
294
+ */
295
+ mode?: PipelineMode;
296
+ /** System prompt for the LLM. Defaults to a built-in voice-optimized prompt. */
297
+ instructions?: string;
298
+ /** Initial spoken greeting when a session starts. */
299
+ greeting?: string;
300
+ /**
301
+ * Cartesia voice UUID for TTS. Defaults to the server's configured voice
302
+ * (Sarah) when omitted. Browse voices at https://play.cartesia.ai.
303
+ */
304
+ voice?: Voice;
305
+ /** Prompt hint for the STT model to improve transcription accuracy. */
306
+ sttPrompt?: string;
307
+ /**
308
+ * Maximum agentic loop iterations per turn. Can be a static number or
309
+ * a function that receives the hook context and returns a number.
310
+ *
311
+ * @default {5}
312
+ */
313
+ maxSteps?: number | ((ctx: HookContext<S>) => number);
314
+ /** How the LLM should choose tools. */
315
+ toolChoice?: ToolChoice;
316
+ /** Built-in tools to enable (e.g. `"web_search"`, `"run_code"`). */
317
+ builtinTools?: readonly BuiltinTool[];
318
+ /**
319
+ * Default set of active tools per turn.
320
+ *
321
+ * When set, only these tools are available to the LLM each turn.
322
+ * Can be overridden dynamically per-turn via `onBeforeStep`.
323
+ */
324
+ activeTools?: readonly string[];
325
+ /** Custom tools the agent can invoke. */
326
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
327
+ tools?: Readonly<Record<string, ToolDef<any, NoInfer<S>>>>;
328
+ /** Factory that creates fresh per-session state. Called once per connection. */
329
+ state?: () => S;
330
+ /** Called when a new session connects. */
331
+ onConnect?: (ctx: HookContext<S>) => void | Promise<void>;
332
+ /** Called when a session disconnects. */
333
+ onDisconnect?: (ctx: HookContext<S>) => void | Promise<void>;
334
+ /** Called when an unhandled error occurs. */
335
+ onError?: (error: Error, ctx?: HookContext<S>) => void;
336
+ /** Called after a complete turn (all steps finished). */
337
+ onTurn?: (text: string, ctx: HookContext<S>) => void | Promise<void>;
338
+ /** Called after each agentic step completes. */
339
+ onStep?: (step: StepInfo, ctx: HookContext<S>) => void | Promise<void>;
340
+ /**
341
+ * Called before each step; can restrict which tools are active.
342
+ *
343
+ * Return `{ activeTools: [...] }` to limit available tools for the
344
+ * upcoming step, or `void` to keep all tools active.
345
+ */
346
+ onBeforeStep?: (
347
+ stepNumber: number,
348
+ ctx: HookContext<S>,
349
+ ) => BeforeStepResult | Promise<BeforeStepResult>;
350
+ };
351
+
352
+ /**
353
+ * Default system prompt used when `instructions` is not provided.
354
+ *
355
+ * Optimized for voice-first interactions: short sentences, no visual
356
+ * formatting, confident tone, and concise answers.
357
+ */
358
+ export const DEFAULT_INSTRUCTIONS: string = `\
359
+ You are AAI, a helpful AI assistant.
360
+
361
+ Voice-First Rules:
362
+ - Optimize for natural speech. Avoid jargon unless central to the answer. \
363
+ Use short, punchy sentences.
364
+ - Never mention "search results," "sources," or "the provided text." \
365
+ Speak as if the knowledge is your own.
366
+ - No visual formatting. Do not say "bullet point," "bold," or "bracketed one." \
367
+ If you need to list items, say "First," "Next," and "Finally."
368
+ - Start with the most important information. No introductory filler.
369
+ - Be concise. Keep answers to 1-3 sentences. For complex topics, provide a high-level summary.
370
+ - Be confident. Avoid hedging phrases like "It seems that" or "I believe."
371
+ - If you don't have enough information, say so directly rather than guessing.
372
+ - Never use exclamation points. Keep your tone calm and conversational.`;
373
+
374
+ /** Default greeting spoken when a session starts. */
375
+ export const DEFAULT_GREETING: string =
376
+ "Hey there. I'm a voice assistant. What can I help you with?";
377
+
378
+ /**
379
+ * Agent definition with all defaults applied, returned by
380
+ * {@linkcode defineAgent}.
381
+ *
382
+ * Unlike {@linkcode AgentOptions}, every field here is resolved to its
383
+ * final value — no optional fields with implicit defaults remain.
384
+ */
385
+ export type AgentDef = {
386
+ name: string;
387
+ env: readonly string[];
388
+ transport: readonly Transport[];
389
+ mode?: PipelineMode | undefined;
390
+ instructions: string;
391
+ greeting: string;
392
+ voice: string;
393
+ sttPrompt?: string;
394
+ maxSteps: number | ((ctx: HookContext) => number);
395
+ toolChoice?: ToolChoice;
396
+ builtinTools?: readonly BuiltinTool[];
397
+ activeTools?: readonly string[];
398
+ tools: Readonly<Record<string, ToolDef>>;
399
+ state?: () => unknown;
400
+ onConnect?: AgentOptions["onConnect"];
401
+ onDisconnect?: AgentOptions["onDisconnect"];
402
+ onError?: AgentOptions["onError"];
403
+ onTurn?: AgentOptions["onTurn"];
404
+ onStep?: AgentOptions["onStep"];
405
+ onBeforeStep?: AgentOptions["onBeforeStep"];
406
+ };
package/sdk/vector.ts ADDED
@@ -0,0 +1,133 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ /**
3
+ * Vector store interface and in-memory implementation.
4
+ *
5
+ * @module
6
+ */
7
+
8
+ /**
9
+ * A single vector search result entry.
10
+ */
11
+ export type VectorEntry = {
12
+ /** The unique identifier for this entry. */
13
+ id: string;
14
+ /** Similarity score (higher = more similar). */
15
+ score: number;
16
+ /** The original text data stored with this entry. */
17
+ data?: string | undefined;
18
+ /** Arbitrary metadata stored with this entry. */
19
+ metadata?: Record<string, unknown> | undefined;
20
+ };
21
+
22
+ /**
23
+ * Async vector store interface used by agents.
24
+ *
25
+ * Agents access the vector store via {@linkcode ToolContext.vector} or
26
+ * {@linkcode HookContext.vector}. Backed by Upstash Vector with built-in
27
+ * embeddings — raw text is sent and vectorized server-side.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * // Inside a tool execute function:
32
+ * const myTool = {
33
+ * description: "Search knowledge base",
34
+ * execute: async (_args: unknown, ctx: { vector: VectorStore }) => {
35
+ * await ctx.vector.upsert("doc-1", "The capital of France is Paris.");
36
+ * const results = await ctx.vector.query("What is the capital of France?");
37
+ * return results;
38
+ * },
39
+ * };
40
+ * ```
41
+ */
42
+ export type VectorStore = {
43
+ /**
44
+ * Upsert a text entry into the vector store.
45
+ *
46
+ * The text is automatically embedded by the server's vector database.
47
+ *
48
+ * @param id Unique identifier for this entry.
49
+ * @param data The text content to store and embed.
50
+ * @param metadata Optional metadata to store alongside the vector.
51
+ */
52
+ upsert(id: string, data: string, metadata?: Record<string, unknown>): Promise<void>;
53
+
54
+ /**
55
+ * Query the vector store with a text string.
56
+ *
57
+ * Returns the most similar entries ranked by score.
58
+ *
59
+ * @param text The query text to search for.
60
+ * @param options Optional query parameters.
61
+ * @param options.topK Maximum number of results (default: 10).
62
+ * @param options.filter Metadata filter expression.
63
+ * @returns An array of matching {@linkcode VectorEntry} objects.
64
+ */
65
+ query(text: string, options?: { topK?: number; filter?: string }): Promise<VectorEntry[]>;
66
+
67
+ /**
68
+ * Remove entries by ID.
69
+ *
70
+ * @param ids A single ID or array of IDs to remove.
71
+ */
72
+ remove(ids: string | string[]): Promise<void>;
73
+ };
74
+
75
+ /**
76
+ * Create an in-memory vector store for testing and local development.
77
+ *
78
+ * Uses brute-force substring matching instead of real vector similarity.
79
+ * Good enough for testing the plumbing but not for production use.
80
+ *
81
+ * @returns A {@linkcode VectorStore} instance backed by in-memory storage.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * import { createMemoryVectorStore } from "aai";
86
+ *
87
+ * const vector = createMemoryVectorStore();
88
+ * await vector.upsert("doc-1", "The capital of France is Paris.");
89
+ * const results = await vector.query("France capital");
90
+ * ```
91
+ */
92
+ export function createMemoryVectorStore(): VectorStore {
93
+ const store = new Map<string, { data: string; metadata?: Record<string, unknown> | undefined }>();
94
+
95
+ return {
96
+ upsert(id: string, data: string, metadata?: Record<string, unknown>): Promise<void> {
97
+ store.set(id, { data, metadata });
98
+ return Promise.resolve();
99
+ },
100
+
101
+ query(text: string, options?: { topK?: number; filter?: string }): Promise<VectorEntry[]> {
102
+ const topK = options?.topK ?? 10;
103
+ const query = text.toLowerCase();
104
+ const results: VectorEntry[] = [];
105
+
106
+ for (const [id, entry] of store) {
107
+ const data = entry.data.toLowerCase();
108
+ // Simple substring scoring: count how many query words appear in data
109
+ const words = query.split(/\s+/).filter(Boolean);
110
+ const matches = words.filter((w) => data.includes(w)).length;
111
+ if (matches > 0) {
112
+ results.push({
113
+ id,
114
+ score: matches / Math.max(words.length, 1),
115
+ data: entry.data,
116
+ metadata: entry.metadata,
117
+ });
118
+ }
119
+ }
120
+
121
+ results.sort((a, b) => b.score - a.score);
122
+ return Promise.resolve(results.slice(0, topK));
123
+ },
124
+
125
+ remove(ids: string | string[]): Promise<void> {
126
+ const idArray = Array.isArray(ids) ? ids : [ids];
127
+ for (const id of idArray) {
128
+ store.delete(id);
129
+ }
130
+ return Promise.resolve();
131
+ },
132
+ };
133
+ }
@@ -0,0 +1,141 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ /**
3
+ * WinterTC-compatible server factory.
4
+ *
5
+ * Creates a server with `fetch(Request): Response` and `handleWebSocket(ws)`
6
+ * that can run both in-process (self-hosted) and inside a sandboxed Worker
7
+ * (platform). This is the shared core for both modes.
8
+ *
9
+ * @module
10
+ */
11
+
12
+ import { createDirectExecutor } from "./direct_executor.ts";
13
+ import type { Kv } from "./kv.ts";
14
+ import { createMemoryKv } from "./kv.ts";
15
+ import { AUDIO_FORMAT, PROTOCOL_VERSION } from "./protocol.ts";
16
+ import type { Logger, Metrics, S2SConfig } from "./runtime.ts";
17
+ import { consoleLogger, DEFAULT_S2S_CONFIG, noopMetrics } from "./runtime.ts";
18
+ import type { CreateS2sWebSocket } from "./s2s.ts";
19
+ import type { Session } from "./session.ts";
20
+ import type { AgentDef } from "./types.ts";
21
+ import { type SessionWebSocket, wireSessionSocket } from "./ws_handler.ts";
22
+
23
+ export type WintercServerOptions = {
24
+ /** The agent definition returned by `defineAgent()`. */
25
+ agent: AgentDef;
26
+ /** Environment variables. */
27
+ env: Record<string, string>;
28
+ /** KV store. Defaults to in-memory. */
29
+ kv?: Kv;
30
+ /** Vector search function. */
31
+ vectorSearch?: ((query: string, topK: number) => Promise<string>) | undefined;
32
+ /** WebSocket factory for S2S connections. */
33
+ createWebSocket: CreateS2sWebSocket;
34
+ /** HTML to serve at `GET /`. */
35
+ clientHtml?: string;
36
+ /** Logger. Defaults to console. */
37
+ logger?: Logger;
38
+ /** Metrics collector. Defaults to noop. */
39
+ metrics?: Metrics;
40
+ /** S2S configuration. Defaults to AssemblyAI production. */
41
+ s2sConfig?: S2SConfig;
42
+ };
43
+
44
+ export type WintercServer = {
45
+ /** Standard fetch handler for HTTP routes. */
46
+ fetch(request: Request): Promise<Response>;
47
+ /** Attach a WebSocket to a new session. */
48
+ handleWebSocket(ws: SessionWebSocket, opts?: { skipGreeting?: boolean }): void;
49
+ /** Stop all active sessions. */
50
+ close(): Promise<void>;
51
+ };
52
+
53
+ /**
54
+ * Create a WinterTC-compatible server from an agent definition.
55
+ *
56
+ * The returned object has a standard `fetch` handler and a `handleWebSocket`
57
+ * method. Self-hosted mode calls these directly; platform mode calls them
58
+ * from inside a sandboxed Worker via capnweb RPC.
59
+ */
60
+ export function createWintercServer(options: WintercServerOptions): WintercServer {
61
+ const {
62
+ agent,
63
+ env,
64
+ kv = createMemoryKv(),
65
+ vectorSearch,
66
+ clientHtml,
67
+ logger = consoleLogger,
68
+ metrics = noopMetrics,
69
+ s2sConfig = DEFAULT_S2S_CONFIG,
70
+ } = options;
71
+
72
+ const executor = createDirectExecutor({
73
+ agent,
74
+ env,
75
+ kv,
76
+ ...(vectorSearch ? { vectorSearch } : {}),
77
+ createWebSocket: options.createWebSocket,
78
+ logger,
79
+ metrics,
80
+ s2sConfig,
81
+ });
82
+
83
+ const sessions = new Map<string, Session>();
84
+
85
+ const readyConfig = {
86
+ protocolVersion: PROTOCOL_VERSION,
87
+ audioFormat: AUDIO_FORMAT,
88
+ sampleRate: s2sConfig.inputSampleRate,
89
+ ttsSampleRate: s2sConfig.outputSampleRate,
90
+ mode: "s2s" as const,
91
+ };
92
+
93
+ return {
94
+ async fetch(request: Request): Promise<Response> {
95
+ const url = new URL(request.url);
96
+
97
+ if (url.pathname === "/health") {
98
+ return new Response(JSON.stringify({ status: "ok", name: agent.name }), {
99
+ headers: { "Content-Type": "application/json" },
100
+ });
101
+ }
102
+
103
+ if (url.pathname === "/" && clientHtml) {
104
+ return new Response(clientHtml, {
105
+ headers: { "Content-Type": "text/html" },
106
+ });
107
+ }
108
+
109
+ if (url.pathname === "/") {
110
+ return new Response(
111
+ `<!DOCTYPE html><html><body><h1>${agent.name}</h1><p>Agent server running.</p></body></html>`,
112
+ { headers: { "Content-Type": "text/html" } },
113
+ );
114
+ }
115
+
116
+ return new Response("Not Found", { status: 404 });
117
+ },
118
+
119
+ handleWebSocket(ws: SessionWebSocket, wsOpts?: { skipGreeting?: boolean }): void {
120
+ wireSessionSocket(ws, {
121
+ sessions,
122
+ createSession: (sid, client) =>
123
+ executor.createSession({
124
+ id: sid,
125
+ agent: agent.name,
126
+ client,
127
+ skipGreeting: wsOpts?.skipGreeting ?? false,
128
+ }),
129
+ readyConfig,
130
+ logger,
131
+ });
132
+ },
133
+
134
+ async close(): Promise<void> {
135
+ for (const session of sessions.values()) {
136
+ await session.stop();
137
+ }
138
+ sessions.clear();
139
+ },
140
+ };
141
+ }