@alexkroman1/aai 0.9.2 → 0.10.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 (57) hide show
  1. package/dist/_internal-types.d.ts +49 -22
  2. package/dist/_internal-types.js +43 -1
  3. package/dist/_mock-ws.d.ts +1 -2
  4. package/dist/_run-code.d.ts +31 -0
  5. package/dist/_session-ctx.d.ts +73 -0
  6. package/dist/_session-otel.d.ts +43 -0
  7. package/dist/_session-persist.d.ts +30 -0
  8. package/dist/_ssrf.d.ts +30 -0
  9. package/dist/_ssrf.js +123 -0
  10. package/dist/_utils.d.ts +25 -0
  11. package/dist/_utils.js +54 -1
  12. package/dist/builtin-tools.d.ts +5 -34
  13. package/dist/direct-executor-Ca0wt5H0.js +572 -0
  14. package/dist/direct-executor.d.ts +34 -5
  15. package/dist/index.d.ts +2 -1
  16. package/dist/index.js +2 -2
  17. package/dist/kv.d.ts +30 -38
  18. package/dist/kv.js +19 -86
  19. package/dist/matchers.d.ts +20 -0
  20. package/dist/matchers.js +41 -0
  21. package/dist/memory-tools.d.ts +39 -0
  22. package/dist/middleware-core.d.ts +47 -0
  23. package/dist/middleware-core.js +107 -0
  24. package/dist/middleware.d.ts +37 -0
  25. package/dist/protocol.d.ts +44 -24
  26. package/dist/protocol.js +34 -14
  27. package/dist/runtime.d.ts +26 -2
  28. package/dist/runtime.js +44 -7
  29. package/dist/s2s.d.ts +19 -29
  30. package/dist/s2s.js +117 -87
  31. package/dist/server.d.ts +31 -3
  32. package/dist/server.js +102 -28
  33. package/dist/session-BkN9u0ni.js +683 -0
  34. package/dist/session.d.ts +55 -28
  35. package/dist/session.js +2 -312
  36. package/dist/sqlite-kv.d.ts +34 -0
  37. package/dist/sqlite-kv.js +133 -0
  38. package/dist/sqlite-vector.d.ts +58 -0
  39. package/dist/sqlite-vector.js +149 -0
  40. package/dist/system-prompt.d.ts +21 -0
  41. package/dist/telemetry.d.ts +49 -0
  42. package/dist/telemetry.js +95 -0
  43. package/dist/testing-MRl3SXsI.js +519 -0
  44. package/dist/testing.d.ts +299 -0
  45. package/dist/testing.js +2 -0
  46. package/dist/types.d.ts +324 -39
  47. package/dist/types.js +62 -9
  48. package/dist/vector.d.ts +18 -22
  49. package/dist/vector.js +41 -48
  50. package/dist/worker-entry.d.ts +11 -3
  51. package/dist/worker-entry.js +19 -8
  52. package/dist/ws-handler.d.ts +7 -3
  53. package/dist/ws-handler.js +64 -12
  54. package/package.json +55 -8
  55. package/dist/_mock-ws.js +0 -158
  56. package/dist/builtin-tools.js +0 -270
  57. package/dist/direct-executor.js +0 -125
package/dist/session.d.ts CHANGED
@@ -1,43 +1,50 @@
1
- /**
2
- * S2S session — relays audio between the client and AssemblyAI's
3
- * Speech-to-Speech API, intercepting only tool calls for local execution.
4
- */
1
+ /** S2S session — relays audio between client and AssemblyAI S2S API. */
5
2
  import type { AgentConfig, ToolSchema } from "./_internal-types.ts";
3
+ import { type SessionPersistence } from "./_session-persist.ts";
4
+ import type { HookInvoker } from "./middleware.ts";
6
5
  import type { ClientSink } from "./protocol.ts";
7
6
  import type { Logger, S2SConfig } from "./runtime.ts";
8
7
  import { type CreateS2sWebSocket, connectS2s } from "./s2s.ts";
9
- import { type StepInfo } from "./types.ts";
10
8
  import type { ExecuteTool } from "./worker-entry.ts";
11
- /** A voice session managing the S2S connection for one client. */
9
+ export type { S2sSessionCtx } from "./_session-ctx.ts";
10
+ export type { PersistedSession, SessionPersistence } from "./_session-persist.ts";
11
+ export { persistKey } from "./_session-persist.ts";
12
+ export type { HookInvoker, ToolInterceptResult } from "./middleware.ts";
13
+ export { buildSystemPrompt } from "./system-prompt.ts";
14
+ /**
15
+ * A voice session managing the Speech-to-Speech connection for one client.
16
+ *
17
+ * Created by {@link createS2sSession}. Each session owns a single S2S WebSocket
18
+ * connection and relays audio between the browser client and AssemblyAI.
19
+ *
20
+ * @internal Exported for use by `ws-handler.ts`, `server.ts`, and `direct-executor.ts`.
21
+ */
12
22
  export type Session = {
23
+ /** Open the S2S connection and fire the `onConnect` hook. */
13
24
  start(): Promise<void>;
25
+ /** Gracefully shut down: wait for in-flight turns, close the S2S socket, fire `onDisconnect`. */
14
26
  stop(): Promise<void>;
27
+ /** Forward raw PCM audio from the client microphone to the S2S connection. */
15
28
  onAudio(data: Uint8Array): void;
29
+ /** Called when the client has finished setting up its audio pipeline. For S2S sessions this is a no-op since the greeting comes automatically. */
16
30
  onAudioReady(): void;
31
+ /** Handle a client-initiated cancellation (barge-in). Sends a `cancelled` event. */
17
32
  onCancel(): void;
33
+ /** Reset the session: clear conversation history, bump generation counters, reconnect S2S. */
18
34
  onReset(): void;
35
+ /**
36
+ * Inject conversation history from the client (e.g. on reconnect).
37
+ * @param incoming - Messages with `{role, content}` fields.
38
+ */
19
39
  onHistory(incoming: readonly {
20
40
  role: "user" | "assistant";
21
- text: string;
41
+ content: string;
22
42
  }[]): void;
43
+ /** Returns a promise that resolves when the current in-flight turn completes, or resolves immediately if no turn is active. */
23
44
  waitForTurn(): Promise<void>;
24
45
  };
25
- /** Generic interface for invoking agent lifecycle hooks. */
26
- export type HookInvoker = {
27
- onConnect(sessionId: string, timeoutMs?: number): Promise<void>;
28
- onDisconnect(sessionId: string, timeoutMs?: number): Promise<void>;
29
- onTurn(sessionId: string, text: string, timeoutMs?: number): Promise<void>;
30
- onError(sessionId: string, error: {
31
- message: string;
32
- }, timeoutMs?: number): Promise<void>;
33
- onStep(sessionId: string, step: StepInfo, timeoutMs?: number): Promise<void>;
34
- resolveTurnConfig(sessionId: string, timeoutMs?: number): Promise<{
35
- maxSteps?: number;
36
- activeTools?: string[];
37
- } | null>;
38
- };
39
46
  /** Configuration options for creating a new session. */
40
- export type SessionOptions = {
47
+ export type S2sSessionOptions = {
41
48
  id: string;
42
49
  agent: string;
43
50
  client: ClientSink;
@@ -51,13 +58,33 @@ export type SessionOptions = {
51
58
  hookInvoker?: HookInvoker;
52
59
  skipGreeting?: boolean;
53
60
  logger?: Logger;
61
+ /** Maximum number of conversation messages to retain. Older messages are
62
+ * dropped (sliding window) to bound memory in long-running sessions.
63
+ * Defaults to 200. Set to 0 or Infinity to disable trimming. */
64
+ maxHistory?: number;
65
+ /** Persistence configuration for auto-saving/restoring session data. */
66
+ persistence?: SessionPersistence;
67
+ /** Old session ID to resume from. Loads persisted state/messages from KV
68
+ * and attempts S2S session resume. */
69
+ resumeFrom?: string;
54
70
  };
71
+ /** @internal Not part of the public API. Exposed for testing only. */
55
72
  export declare const _internals: {
56
73
  connectS2s: typeof connectS2s;
57
74
  };
58
- /** Create an S2S-backed session with the same interface as the STT+LLM+TTS session. */
59
- export declare function createS2sSession(opts: SessionOptions): Session;
60
- export declare function buildSystemPrompt(config: AgentConfig, opts: {
61
- hasTools: boolean;
62
- voice?: boolean;
63
- }): string;
75
+ /**
76
+ * Create a Speech-to-Speech backed session implementing the {@link Session} interface.
77
+ *
78
+ * Connects to AssemblyAI's S2S WebSocket, configures the system prompt and tools,
79
+ * and wires up event listeners for user transcripts, agent replies, tool calls,
80
+ * barge-ins, and session lifecycle. Manages reconnection on `onReset` via a
81
+ * `connectGeneration` guard that prevents stale connection attempts from overwriting
82
+ * newer ones during rapid resets. A `sessionAbort` AbortController is used to
83
+ * coordinate cleanup on `stop()`.
84
+ *
85
+ * @param opts - Session configuration. See {@link S2sSessionOptions} for all fields
86
+ * including the agent config, tool schemas, API key, and optional hooks.
87
+ * @returns A {@link Session} with `start`, `stop`, `onAudio`, `onReset`, and other
88
+ * lifecycle methods.
89
+ */
90
+ export declare function createS2sSession(opts: S2sSessionOptions): Session;
package/dist/session.js CHANGED
@@ -1,312 +1,2 @@
1
- import { DEFAULT_INSTRUCTIONS } from "./types.js";
2
- import { errorMessage } from "./_utils.js";
3
- import { HOOK_TIMEOUT_MS } from "./protocol.js";
4
- import { consoleLogger } from "./runtime.js";
5
- import { connectS2s, defaultCreateS2sWebSocket } from "./s2s.js";
6
- //#region session.ts
7
- const _internals = { connectS2s };
8
- /** Create an S2S-backed session with the same interface as the STT+LLM+TTS session. */
9
- function createS2sSession(opts) {
10
- const { id, agent, client, toolSchemas, apiKey, s2sConfig, executeTool, createWebSocket = defaultCreateS2sWebSocket, hookInvoker, logger: log = consoleLogger } = opts;
11
- const agentConfig = opts.skipGreeting ? {
12
- ...opts.agentConfig,
13
- greeting: ""
14
- } : opts.agentConfig;
15
- const systemPrompt = buildSystemPrompt(agentConfig, {
16
- hasTools: toolSchemas.length > 0 || (agentConfig.builtinTools?.length ?? 0) > 0,
17
- voice: true
18
- });
19
- const s2sTools = toolSchemas.map((ts) => ({
20
- type: "function",
21
- name: ts.name,
22
- description: ts.description,
23
- parameters: ts.parameters
24
- }));
25
- let s2s = null;
26
- const sessionAbort = new AbortController();
27
- let toolCallCount = 0;
28
- let turnPromise = null;
29
- let conversationMessages = [];
30
- let pendingTools = [];
31
- async function resolveTurnConfig() {
32
- if (!hookInvoker) return null;
33
- return await hookInvoker.resolveTurnConfig(id, HOOK_TIMEOUT_MS);
34
- }
35
- function fireHook(name, fn) {
36
- if (!hookInvoker) return;
37
- try {
38
- fn(hookInvoker).catch((err) => {
39
- log.warn(`${name} hook failed`, { err: errorMessage(err) });
40
- });
41
- } catch (err) {
42
- log.warn(`${name} hook failed`, { err: errorMessage(err) });
43
- }
44
- }
45
- /** Check if a tool call should be refused due to turn config limits. Returns a result string to short-circuit, or null. */
46
- function checkTurnLimits(turnConfig, name) {
47
- const maxSteps = turnConfig?.maxSteps ?? agentConfig.maxSteps;
48
- toolCallCount++;
49
- if (maxSteps !== void 0 && toolCallCount > maxSteps) {
50
- log.info("maxSteps exceeded, refusing tool call", {
51
- toolCallCount,
52
- maxSteps
53
- });
54
- return "Maximum tool steps reached. Please respond to the user now.";
55
- }
56
- if (turnConfig?.activeTools && !turnConfig.activeTools.includes(name)) {
57
- log.info("Tool filtered by activeTools", { name });
58
- return JSON.stringify({ error: `Tool "${name}" is not available at this step.` });
59
- }
60
- return null;
61
- }
62
- async function handleToolCall(detail) {
63
- const { call_id, name, args: parsedArgs } = detail;
64
- client.event({
65
- type: "tool_call_start",
66
- toolCallId: call_id,
67
- toolName: name,
68
- args: parsedArgs
69
- });
70
- let turnConfig;
71
- try {
72
- turnConfig = await resolveTurnConfig();
73
- } catch (err) {
74
- const msg = `resolveTurnConfig hook error: ${errorMessage(err)}`;
75
- log.error(msg);
76
- pendingTools.push({
77
- call_id,
78
- result: msg
79
- });
80
- client.event({
81
- type: "tool_call_done",
82
- toolCallId: call_id,
83
- result: msg
84
- });
85
- return;
86
- }
87
- const refused = checkTurnLimits(turnConfig, name);
88
- if (refused !== null) {
89
- pendingTools.push({
90
- call_id,
91
- result: refused
92
- });
93
- client.event({
94
- type: "tool_call_done",
95
- toolCallId: call_id,
96
- result: refused
97
- });
98
- return;
99
- }
100
- fireHook("onStep", (h) => h.onStep(id, {
101
- stepNumber: toolCallCount - 1,
102
- toolCalls: [{
103
- toolName: name,
104
- args: parsedArgs
105
- }],
106
- text: ""
107
- }, HOOK_TIMEOUT_MS));
108
- log.info("S2S tool call", {
109
- tool: name,
110
- call_id,
111
- args: parsedArgs,
112
- agent
113
- });
114
- let result;
115
- try {
116
- result = await executeTool(name, parsedArgs, id, conversationMessages);
117
- } catch (err) {
118
- const msg = errorMessage(err);
119
- log.error("Tool execution failed", {
120
- tool: name,
121
- error: msg
122
- });
123
- result = JSON.stringify({ error: msg });
124
- }
125
- log.info("S2S tool result", {
126
- tool: name,
127
- call_id,
128
- resultLength: result.length
129
- });
130
- pendingTools.push({
131
- call_id,
132
- result
133
- });
134
- client.event({
135
- type: "tool_call_done",
136
- toolCallId: call_id,
137
- result
138
- });
139
- }
140
- /** Wire all S2S events to the client sink, hooks, and session state. */
141
- function setupListeners(handle) {
142
- handle.on("ready", ({ session_id }) => {
143
- log.info("S2S session ready", { session_id });
144
- });
145
- handle.on("session_expired", () => {
146
- log.info("S2S session expired");
147
- handle.close();
148
- });
149
- handle.on("speech_started", () => client.event({ type: "speech_started" }));
150
- handle.on("speech_stopped", () => client.event({ type: "speech_stopped" }));
151
- handle.on("user_transcript_delta", ({ text }) => {
152
- client.event({
153
- type: "transcript",
154
- text,
155
- isFinal: false
156
- });
157
- });
158
- handle.on("user_transcript", ({ text }) => {
159
- log.info("S2S user transcript", { text });
160
- client.event({
161
- type: "transcript",
162
- text,
163
- isFinal: true
164
- });
165
- client.event({
166
- type: "turn",
167
- text
168
- });
169
- conversationMessages.push({
170
- role: "user",
171
- content: text
172
- });
173
- fireHook("onTurn", (h) => h.onTurn(id, text, HOOK_TIMEOUT_MS));
174
- });
175
- handle.on("reply_started", () => {
176
- toolCallCount = 0;
177
- });
178
- handle.on("audio", ({ audio }) => {
179
- client.playAudioChunk(audio);
180
- });
181
- handle.on("agent_transcript_delta", ({ text }) => {
182
- client.event({
183
- type: "chat_delta",
184
- text
185
- });
186
- });
187
- handle.on("agent_transcript", ({ text }) => {
188
- client.event({
189
- type: "chat",
190
- text
191
- });
192
- conversationMessages.push({
193
- role: "assistant",
194
- content: text
195
- });
196
- });
197
- handle.on("tool_call", (detail) => {
198
- const p = handleToolCall(detail).catch((err) => {
199
- log.error("Tool call handler failed", { err: errorMessage(err) });
200
- });
201
- turnPromise = (turnPromise ?? Promise.resolve()).then(() => p);
202
- });
203
- handle.on("reply_done", ({ status }) => {
204
- if (status === "interrupted") {
205
- log.info("S2S reply interrupted (barge-in)");
206
- pendingTools = [];
207
- client.event({ type: "cancelled" });
208
- } else if (pendingTools.length > 0) {
209
- for (const tool of pendingTools) s2s?.sendToolResult(tool.call_id, tool.result);
210
- pendingTools = [];
211
- } else {
212
- client.playAudioDone();
213
- client.event({ type: "tts_done" });
214
- }
215
- });
216
- handle.on("error", ({ code, message }) => {
217
- log.error("S2S error", {
218
- code,
219
- message
220
- });
221
- client.event({
222
- type: "error",
223
- code: "internal",
224
- message
225
- });
226
- handle.close();
227
- });
228
- handle.on("close", () => {
229
- log.info("S2S closed");
230
- s2s = null;
231
- });
232
- }
233
- async function connectAndSetup() {
234
- try {
235
- const handle = await _internals.connectS2s({
236
- apiKey,
237
- config: s2sConfig,
238
- createWebSocket,
239
- logger: log
240
- });
241
- setupListeners(handle);
242
- handle.updateSession({
243
- system_prompt: systemPrompt,
244
- tools: s2sTools,
245
- ...agentConfig.greeting ? { greeting: agentConfig.greeting } : {}
246
- });
247
- s2s = handle;
248
- } catch (err) {
249
- const msg = errorMessage(err);
250
- log.error("S2S connect failed", { error: msg });
251
- client.event({
252
- type: "error",
253
- code: "internal",
254
- message: msg
255
- });
256
- }
257
- }
258
- return {
259
- async start() {
260
- fireHook("onConnect", (h) => h.onConnect(id, HOOK_TIMEOUT_MS));
261
- await connectAndSetup();
262
- },
263
- async stop() {
264
- if (sessionAbort.signal.aborted) return;
265
- sessionAbort.abort();
266
- if (turnPromise) await turnPromise;
267
- s2s?.close();
268
- fireHook("onDisconnect", (h) => h.onDisconnect(id, HOOK_TIMEOUT_MS));
269
- },
270
- onAudio(data) {
271
- s2s?.sendAudio(data);
272
- },
273
- onAudioReady() {},
274
- onCancel() {
275
- client.event({ type: "cancelled" });
276
- },
277
- onReset() {
278
- conversationMessages = [];
279
- toolCallCount = 0;
280
- turnPromise = null;
281
- pendingTools = [];
282
- s2s?.close();
283
- client.event({ type: "reset" });
284
- connectAndSetup().catch((err) => {
285
- log.error("S2S reset reconnect failed", { error: errorMessage(err) });
286
- });
287
- },
288
- onHistory(incoming) {
289
- for (const msg of incoming) conversationMessages.push({
290
- role: msg.role,
291
- content: msg.text
292
- });
293
- },
294
- waitForTurn() {
295
- return turnPromise ?? Promise.resolve();
296
- }
297
- };
298
- }
299
- const VOICE_RULES = "\n\nCRITICAL OUTPUT RULES — you MUST follow these for EVERY response:\nYour response will be spoken aloud by a TTS system and displayed as plain text.\n- NEVER use markdown: no **, no *, no _, no #, no `, no [](), no ---\n- NEVER use bullet points (-, *, •) or numbered lists (1., 2.)\n- NEVER use code blocks or inline code\n- NEVER mention tools, search, APIs, or technical failures to the user. If a tool returns no results, just answer naturally without explaining why.\n- Write exactly as you would say it out loud to a friend\n- Use short conversational sentences. To list things, say \"First,\" \"Next,\" \"Finally,\"\n- Keep responses concise — 1 to 3 sentences max";
300
- function buildSystemPrompt(config, opts) {
301
- const { hasTools } = opts;
302
- const agentInstructions = config.instructions && config.instructions !== DEFAULT_INSTRUCTIONS ? `\n\nAgent-Specific Instructions:\n${config.instructions}` : "";
303
- const toolPreamble = hasTools ? "\n\nWhen you decide to use a tool, ALWAYS say a brief natural phrase BEFORE the tool call (e.g. \"Let me look that up\" or \"One moment while I check\"). This fills silence while the tool executes. Keep preambles to one short sentence." : "";
304
- return DEFAULT_INSTRUCTIONS + `\n\nToday's date is ${(/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
305
- weekday: "long",
306
- year: "numeric",
307
- month: "long",
308
- day: "numeric"
309
- })}.` + agentInstructions + toolPreamble + (opts.voice ? VOICE_RULES : "");
310
- }
311
- //#endregion
312
- export { _internals, buildSystemPrompt, createS2sSession };
1
+ import { i as persistKey, n as createS2sSession, r as buildSystemPrompt, t as _internals } from "./session-BkN9u0ni.js";
2
+ export { _internals, buildSystemPrompt, createS2sSession, persistKey };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * SQLite-backed key-value storage for local development.
3
+ *
4
+ * Persists data across restarts using a local SQLite database file.
5
+ * Uses `node:sqlite` (built into Node 22+) — no native dependencies.
6
+ * Drop-in replacement for the in-memory KV store.
7
+ */
8
+ import type { Kv } from "./kv.ts";
9
+ /**
10
+ * Options for creating a SQLite-backed KV store.
11
+ */
12
+ export type SqliteKvOptions = {
13
+ /** Path to the SQLite database file. Defaults to `.aai/local.db`. */
14
+ path?: string;
15
+ };
16
+ /**
17
+ * Create a SQLite-backed KV store for local development.
18
+ *
19
+ * Data persists to a local SQLite file (default: `.aai/local.db`).
20
+ * TTL expiration is enforced on reads and periodically cleaned up.
21
+ *
22
+ * @param options - Optional configuration. See {@link SqliteKvOptions}.
23
+ * @returns A {@link Kv} instance backed by SQLite.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * import { createSqliteKv } from "@alexkroman1/aai/sqlite-kv";
28
+ *
29
+ * const kv = createSqliteKv();
30
+ * await kv.set("greeting", "hello");
31
+ * const value = await kv.get<string>("greeting"); // "hello"
32
+ * ```
33
+ */
34
+ export declare function createSqliteKv(options?: SqliteKvOptions): Kv;
@@ -0,0 +1,133 @@
1
+ import { MAX_VALUE_SIZE, matchGlob, sortAndPaginate } from "./kv.js";
2
+ import { DatabaseSync } from "node:sqlite";
3
+ //#region sqlite-kv.ts
4
+ /**
5
+ * SQLite-backed key-value storage for local development.
6
+ *
7
+ * Persists data across restarts using a local SQLite database file.
8
+ * Uses `node:sqlite` (built into Node 22+) — no native dependencies.
9
+ * Drop-in replacement for the in-memory KV store.
10
+ */
11
+ /**
12
+ * Create a SQLite-backed KV store for local development.
13
+ *
14
+ * Data persists to a local SQLite file (default: `.aai/local.db`).
15
+ * TTL expiration is enforced on reads and periodically cleaned up.
16
+ *
17
+ * @param options - Optional configuration. See {@link SqliteKvOptions}.
18
+ * @returns A {@link Kv} instance backed by SQLite.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * import { createSqliteKv } from "@alexkroman1/aai/sqlite-kv";
23
+ *
24
+ * const kv = createSqliteKv();
25
+ * await kv.set("greeting", "hello");
26
+ * const value = await kv.get<string>("greeting"); // "hello"
27
+ * ```
28
+ */
29
+ function createSqliteKv(options) {
30
+ const db = new DatabaseSync(options?.path ?? ".aai/local.db");
31
+ db.exec("PRAGMA journal_mode = WAL");
32
+ db.exec(`
33
+ CREATE TABLE IF NOT EXISTS kv (
34
+ key TEXT PRIMARY KEY,
35
+ value TEXT NOT NULL,
36
+ expires_at INTEGER
37
+ )
38
+ `);
39
+ db.exec(`
40
+ CREATE INDEX IF NOT EXISTS idx_kv_expires_at ON kv(expires_at)
41
+ WHERE expires_at IS NOT NULL
42
+ `);
43
+ const stmtGet = db.prepare("SELECT value, expires_at FROM kv WHERE key = ?");
44
+ const stmtUpsert = db.prepare("INSERT INTO kv (key, value, expires_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at");
45
+ const stmtDelete = db.prepare("DELETE FROM kv WHERE key = ?");
46
+ const stmtDeleteExpired = db.prepare("DELETE FROM kv WHERE expires_at IS NOT NULL AND expires_at <= ?");
47
+ const stmtListPrefix = db.prepare("SELECT key, value FROM kv WHERE key >= ? AND key < ? AND (expires_at IS NULL OR expires_at > ?)");
48
+ const stmtListAll = db.prepare("SELECT key, value FROM kv WHERE expires_at IS NULL OR expires_at > ?");
49
+ const stmtKeysAll = db.prepare("SELECT key FROM kv WHERE expires_at IS NULL OR expires_at > ?");
50
+ const stmtKeysPrefix = db.prepare("SELECT key FROM kv WHERE key >= ? AND key < ? AND (expires_at IS NULL OR expires_at > ?)");
51
+ /** Compute the exclusive upper bound for a prefix scan. */
52
+ function prefixUpperBound(prefix) {
53
+ if (prefix === "") return "￿";
54
+ const last = prefix.charCodeAt(prefix.length - 1);
55
+ return prefix.slice(0, -1) + String.fromCharCode(last + 1);
56
+ }
57
+ const cleanupInterval = setInterval(() => {
58
+ stmtDeleteExpired.run(Date.now());
59
+ }, 6e4);
60
+ if (cleanupInterval.unref) cleanupInterval.unref();
61
+ return {
62
+ close() {
63
+ clearInterval(cleanupInterval);
64
+ db.close();
65
+ },
66
+ get(key) {
67
+ const now = Date.now();
68
+ const row = stmtGet.get(key);
69
+ if (!row) return Promise.resolve(null);
70
+ if (row.expires_at !== null && row.expires_at <= now) {
71
+ stmtDelete.run(key);
72
+ return Promise.resolve(null);
73
+ }
74
+ return Promise.resolve(JSON.parse(row.value));
75
+ },
76
+ set(key, value, setOptions) {
77
+ try {
78
+ const raw = JSON.stringify(value);
79
+ if (raw.length > 65536) return Promise.reject(/* @__PURE__ */ new Error(`Value exceeds max size of ${MAX_VALUE_SIZE} bytes`));
80
+ const expireIn = setOptions?.expireIn;
81
+ const expiresAt = expireIn && expireIn > 0 ? Date.now() + expireIn : null;
82
+ stmtUpsert.run(key, raw, expiresAt);
83
+ return Promise.resolve();
84
+ } catch (err) {
85
+ return Promise.reject(err);
86
+ }
87
+ },
88
+ delete(keys) {
89
+ const keyArray = Array.isArray(keys) ? keys : [keys];
90
+ for (const k of keyArray) stmtDelete.run(k);
91
+ return Promise.resolve();
92
+ },
93
+ list(prefix, listOptions) {
94
+ const now = Date.now();
95
+ let rows;
96
+ if (prefix === "") rows = stmtListAll.all(now);
97
+ else {
98
+ const upper = prefixUpperBound(prefix);
99
+ rows = stmtListPrefix.all(prefix, upper, now);
100
+ }
101
+ const entries = rows.map((row) => ({
102
+ key: row.key,
103
+ value: JSON.parse(row.value)
104
+ }));
105
+ return Promise.resolve(sortAndPaginate(entries, listOptions));
106
+ },
107
+ keys(pattern) {
108
+ const now = Date.now();
109
+ const isGlob = pattern?.includes("*");
110
+ if (!pattern) {
111
+ const keys = stmtKeysAll.all(now).map((r) => r.key);
112
+ return Promise.resolve(keys.sort((a, b) => a.localeCompare(b)));
113
+ }
114
+ if (isGlob) {
115
+ const starIdx = pattern.indexOf("*");
116
+ const prefix = pattern.slice(0, starIdx);
117
+ let rows;
118
+ if (prefix === "") rows = stmtKeysAll.all(now);
119
+ else {
120
+ const upper = prefixUpperBound(prefix);
121
+ rows = stmtKeysPrefix.all(prefix, upper, now);
122
+ }
123
+ const keys = rows.filter((r) => matchGlob(r.key, pattern)).map((r) => r.key);
124
+ return Promise.resolve(keys.sort((a, b) => a.localeCompare(b)));
125
+ }
126
+ const upper = prefixUpperBound(pattern);
127
+ const keys = stmtKeysPrefix.all(pattern, upper, now).map((r) => r.key);
128
+ return Promise.resolve(keys.sort((a, b) => a.localeCompare(b)));
129
+ }
130
+ };
131
+ }
132
+ //#endregion
133
+ export { createSqliteKv };
@@ -0,0 +1,58 @@
1
+ /**
2
+ * SQLite-backed vector store with local embeddings.
3
+ *
4
+ * Persists data across restarts using a local SQLite database file.
5
+ * Uses brute-force cosine similarity over `node:sqlite` — no native
6
+ * extensions required. Fast enough for local dev (sub-ms for <10k vectors).
7
+ * Embeddings are computed locally via `all-MiniLM-L6-v2` (384 dims) —
8
+ * no external API key required. The model is downloaded on first use
9
+ * (~86 MB) and cached in `.aai/models/`.
10
+ */
11
+ import type { VectorStore } from "./vector.ts";
12
+ /** Function that converts text into an embedding vector. */
13
+ export type EmbedFn = (text: string) => Promise<number[]>;
14
+ /**
15
+ * Options for creating a SQLite-vec backed vector store.
16
+ */
17
+ export type SqliteVecVectorStoreOptions = {
18
+ /** Path to the SQLite database file. Defaults to `.aai/vectors.db`. */
19
+ path?: string;
20
+ /** Custom embedding function. Defaults to local `all-MiniLM-L6-v2` model. */
21
+ embedFn?: EmbedFn;
22
+ /** Embedding dimensions. Must match the embedFn output. Defaults to 384. */
23
+ dimensions?: number;
24
+ /** Directory for caching downloaded models. Defaults to `.aai/models`. */
25
+ modelCacheDir?: string;
26
+ };
27
+ /**
28
+ * Create a deterministic hash-based embedding function for testing.
29
+ *
30
+ * Produces repeatable vectors where similar text yields similar embeddings.
31
+ * Not suitable for production — use the default local model instead.
32
+ *
33
+ * @param dimensions - Vector dimensions (default: 384).
34
+ */
35
+ export declare function createTestEmbedFn(dimensions?: number): EmbedFn;
36
+ /**
37
+ * Create a SQLite-backed vector store with local embeddings.
38
+ *
39
+ * Data persists to a local SQLite file (default: `.aai/vectors.db`).
40
+ * Embeddings are computed locally using `all-MiniLM-L6-v2` by default —
41
+ * no API key required. The model auto-downloads on first use (~86 MB).
42
+ *
43
+ * Vector search uses brute-force cosine similarity over all stored
44
+ * embeddings. This is fast for local dev workloads (<10k vectors).
45
+ *
46
+ * @param options - See {@link SqliteVecVectorStoreOptions}.
47
+ * @returns A {@link VectorStore} instance.
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * import { createSqliteVectorStore } from "@alexkroman1/aai/sqlite-vector";
52
+ *
53
+ * const vector = createSqliteVectorStore();
54
+ * await vector.upsert("doc-1", "The capital of France is Paris.");
55
+ * const results = await vector.query("France capital");
56
+ * ```
57
+ */
58
+ export declare function createSqliteVectorStore(options?: SqliteVecVectorStoreOptions): VectorStore;