@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.
- package/dist/_internal-types.d.ts +49 -22
- package/dist/_internal-types.js +43 -1
- package/dist/_mock-ws.d.ts +1 -2
- package/dist/_run-code.d.ts +31 -0
- package/dist/_session-ctx.d.ts +73 -0
- package/dist/_session-otel.d.ts +43 -0
- package/dist/_session-persist.d.ts +30 -0
- package/dist/_ssrf.d.ts +30 -0
- package/dist/_ssrf.js +123 -0
- package/dist/_utils.d.ts +25 -0
- package/dist/_utils.js +54 -1
- package/dist/builtin-tools.d.ts +5 -34
- package/dist/direct-executor-Ca0wt5H0.js +572 -0
- package/dist/direct-executor.d.ts +34 -5
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -2
- package/dist/kv.d.ts +30 -38
- package/dist/kv.js +19 -86
- package/dist/matchers.d.ts +20 -0
- package/dist/matchers.js +41 -0
- package/dist/memory-tools.d.ts +39 -0
- package/dist/middleware-core.d.ts +47 -0
- package/dist/middleware-core.js +107 -0
- package/dist/middleware.d.ts +37 -0
- package/dist/protocol.d.ts +44 -24
- package/dist/protocol.js +34 -14
- package/dist/runtime.d.ts +26 -2
- package/dist/runtime.js +44 -7
- package/dist/s2s.d.ts +19 -29
- package/dist/s2s.js +117 -87
- package/dist/server.d.ts +31 -3
- package/dist/server.js +102 -28
- package/dist/session-BkN9u0ni.js +683 -0
- package/dist/session.d.ts +55 -28
- package/dist/session.js +2 -312
- package/dist/sqlite-kv.d.ts +34 -0
- package/dist/sqlite-kv.js +133 -0
- package/dist/sqlite-vector.d.ts +58 -0
- package/dist/sqlite-vector.js +149 -0
- package/dist/system-prompt.d.ts +21 -0
- package/dist/telemetry.d.ts +49 -0
- package/dist/telemetry.js +95 -0
- package/dist/testing-MRl3SXsI.js +519 -0
- package/dist/testing.d.ts +299 -0
- package/dist/testing.js +2 -0
- package/dist/types.d.ts +324 -39
- package/dist/types.js +62 -9
- package/dist/vector.d.ts +18 -22
- package/dist/vector.js +41 -48
- package/dist/worker-entry.d.ts +11 -3
- package/dist/worker-entry.js +19 -8
- package/dist/ws-handler.d.ts +7 -3
- package/dist/ws-handler.js +64 -12
- package/package.json +55 -8
- package/dist/_mock-ws.js +0 -158
- package/dist/builtin-tools.js +0 -270
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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 {
|
|
2
|
-
|
|
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;
|