@alexkroman1/aai 0.12.3 → 1.0.2
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/.turbo/turbo-build.log +20 -0
- package/CHANGELOG.md +174 -0
- package/dist/constants-VTFoymJ-.js +47 -0
- package/dist/host/_run-code.d.ts +1 -1
- package/dist/host/_runtime-conformance.d.ts +4 -5
- package/dist/host/builtin-tools.d.ts +11 -9
- package/dist/host/runtime-barrel.d.ts +15 -0
- package/dist/{direct-executor-DRRrZUp0.js → host/runtime-barrel.js} +453 -348
- package/dist/host/runtime-config.d.ts +42 -0
- package/dist/host/runtime.d.ts +119 -35
- package/dist/host/s2s.d.ts +14 -38
- package/dist/host/server.d.ts +16 -8
- package/dist/host/session-ctx.d.ts +55 -0
- package/dist/host/session.d.ts +20 -70
- package/dist/host/tool-executor.d.ts +20 -0
- package/dist/host/unstorage-kv.d.ts +1 -1
- package/dist/host/ws-handler.d.ts +4 -2
- package/dist/index.d.ts +9 -20
- package/dist/index.js +63 -2
- package/dist/{isolate → sdk}/_internal-types.d.ts +5 -9
- package/dist/{isolate → sdk}/constants.d.ts +6 -4
- package/dist/sdk/define.d.ts +66 -0
- package/dist/{isolate → sdk}/kv.d.ts +1 -49
- package/dist/sdk/manifest-barrel.d.ts +8 -0
- package/dist/sdk/manifest-barrel.js +52 -0
- package/dist/sdk/manifest.d.ts +50 -0
- package/dist/{isolate → sdk}/protocol.d.ts +59 -36
- package/dist/sdk/protocol.js +163 -0
- package/dist/{isolate → sdk}/system-prompt.d.ts +2 -2
- package/dist/sdk/types.d.ts +201 -0
- package/dist/sdk/ws-upgrade.d.ts +5 -0
- package/dist/{system-prompt-DYAYFW99.js → system-prompt-nik_iavo.js} +10 -10
- package/dist/types-Cfx_4QDK.js +39 -0
- package/dist/ws-upgrade-BeOQ7fXL.js +30 -0
- package/exports-no-dev-deps.test.ts +62 -0
- package/host/_mock-ws.ts +185 -0
- package/host/_run-code.ts +217 -0
- package/host/_runtime-conformance.ts +143 -0
- package/host/_test-utils.ts +276 -0
- package/host/builtin-tools.test.ts +774 -0
- package/host/builtin-tools.ts +255 -0
- package/host/cleanup.test.ts +422 -0
- package/host/fixture-replay.test.ts +463 -0
- package/host/fixtures/README.md +40 -0
- package/host/fixtures/greeting-session-sequence.json +40 -0
- package/host/fixtures/reply-audio-samples.json +42 -0
- package/host/fixtures/reply-lifecycle.json +21 -0
- package/host/fixtures/session-ready.json +48 -0
- package/host/fixtures/session-updated.json +45 -0
- package/host/fixtures/simple-question-sequence.json +73 -0
- package/host/fixtures/tool-call-sequence.json +114 -0
- package/host/fixtures/tool-calls.json +11 -0
- package/host/fixtures/tool-config-session-sequence.json +51 -0
- package/host/fixtures/user-speech-recognition.json +30 -0
- package/host/fixtures/web-search-sequence.json +122 -0
- package/host/integration.test.ts +222 -0
- package/host/runtime-barrel.ts +25 -0
- package/host/runtime-config.test.ts +71 -0
- package/host/runtime-config.ts +99 -0
- package/host/runtime.test.ts +641 -0
- package/host/runtime.ts +308 -0
- package/host/s2s-fixtures.test.ts +237 -0
- package/host/s2s.test.ts +562 -0
- package/host/s2s.ts +310 -0
- package/host/server-shutdown.test.ts +76 -0
- package/host/server.test.ts +116 -0
- package/host/server.ts +223 -0
- package/host/session-ctx.ts +107 -0
- package/host/session-fixture-replay.test.ts +136 -0
- package/host/session-prompt.test.ts +77 -0
- package/host/session.test.ts +590 -0
- package/host/session.ts +370 -0
- package/host/tool-executor.test.ts +124 -0
- package/host/tool-executor.ts +80 -0
- package/host/unstorage-kv.test.ts +99 -0
- package/host/unstorage-kv.ts +69 -0
- package/host/ws-handler.test.ts +739 -0
- package/host/ws-handler.ts +255 -0
- package/index.ts +16 -0
- package/package.json +24 -72
- package/sdk/_internal-types.test.ts +34 -0
- package/sdk/_internal-types.ts +115 -0
- package/sdk/compat-fixtures/README.md +26 -0
- package/sdk/compat-fixtures/v1.json +68 -0
- package/sdk/constants.ts +77 -0
- package/sdk/define.test.ts +57 -0
- package/sdk/define.ts +88 -0
- package/sdk/kv.ts +60 -0
- package/sdk/manifest-barrel.ts +12 -0
- package/sdk/manifest.test.ts +56 -0
- package/sdk/manifest.ts +89 -0
- package/sdk/protocol-compat.test.ts +187 -0
- package/sdk/protocol-snapshot.test.ts +199 -0
- package/sdk/protocol.test.ts +170 -0
- package/sdk/protocol.ts +223 -0
- package/sdk/schema-alignment.test.ts +191 -0
- package/sdk/system-prompt.test.ts +111 -0
- package/sdk/system-prompt.ts +74 -0
- package/sdk/tsconfig.json +12 -0
- package/sdk/types-inference.test.ts +122 -0
- package/sdk/types.test.ts +14 -0
- package/sdk/types.ts +226 -0
- package/sdk/utils.test.ts +52 -0
- package/sdk/utils.ts +20 -0
- package/sdk/ws-upgrade.test.ts +48 -0
- package/sdk/ws-upgrade.ts +13 -0
- package/tsconfig.build.json +14 -0
- package/tsconfig.json +10 -0
- package/tsdown.config.ts +26 -0
- package/vitest.config.ts +17 -0
- package/dist/host/_test-utils.d.ts +0 -73
- package/dist/host/direct-executor.d.ts +0 -130
- package/dist/host/index.d.ts +0 -19
- package/dist/host/index.js +0 -165
- package/dist/host/matchers.d.ts +0 -20
- package/dist/host/matchers.js +0 -41
- package/dist/host/server.js +0 -164
- package/dist/host/testing.d.ts +0 -294
- package/dist/host/testing.js +0 -2
- package/dist/host/vite-plugin.d.ts +0 -15
- package/dist/host/vite-plugin.js +0 -83
- package/dist/isolate/_kv-utils.d.ts +0 -10
- package/dist/isolate/_utils.js +0 -17
- package/dist/isolate/hooks.d.ts +0 -44
- package/dist/isolate/hooks.js +0 -58
- package/dist/isolate/index.d.ts +0 -18
- package/dist/isolate/index.js +0 -6
- package/dist/isolate/kv.js +0 -1
- package/dist/isolate/protocol.js +0 -2
- package/dist/isolate/types.d.ts +0 -418
- package/dist/isolate/types.js +0 -175
- package/dist/protocol-rcOrz7T3.js +0 -183
- package/dist/testing-BreLdpq-.js +0 -513
- package/dist/types.test-d.d.ts +0 -7
- /package/dist/{isolate/_utils.d.ts → sdk/utils.d.ts} +0 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
+
/**
|
|
3
|
+
* WebSocket session lifecycle handler.
|
|
4
|
+
*
|
|
5
|
+
* Audio validation is handled at the host transport layer (see server.ts).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import pTimeout from "p-timeout";
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_SESSION_START_TIMEOUT_MS,
|
|
11
|
+
MAX_MESSAGE_BUFFER_SIZE,
|
|
12
|
+
WS_OPEN,
|
|
13
|
+
} from "../sdk/constants.ts";
|
|
14
|
+
import type { ClientMessage, ClientSink, ReadyConfig } from "../sdk/protocol.ts";
|
|
15
|
+
import { ClientMessageSchema, lenientParse } from "../sdk/protocol.ts";
|
|
16
|
+
import { errorDetail, errorMessage } from "../sdk/utils.ts";
|
|
17
|
+
import type { Logger } from "./runtime-config.ts";
|
|
18
|
+
import { consoleLogger } from "./runtime-config.ts";
|
|
19
|
+
import type { Session } from "./session.ts";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Minimal WebSocket interface accepted by {@link wireSessionSocket}.
|
|
23
|
+
*
|
|
24
|
+
* Satisfied by the standard `WebSocket` and the `ws` npm package's WebSocket.
|
|
25
|
+
*/
|
|
26
|
+
export type SessionWebSocket = {
|
|
27
|
+
readonly readyState: number;
|
|
28
|
+
send(data: string | ArrayBuffer | Uint8Array): void;
|
|
29
|
+
addEventListener(type: "close" | "open", listener: () => void): void;
|
|
30
|
+
addEventListener(type: "message", listener: (event: { data: unknown }) => void): void;
|
|
31
|
+
addEventListener(type: "error", listener: (event: { message?: string }) => void): void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Options for wiring a WebSocket to a session. */
|
|
35
|
+
export type WsSessionOptions = {
|
|
36
|
+
/** Map of active sessions (session is added on open, removed on close). */
|
|
37
|
+
sessions: Map<string, Session>;
|
|
38
|
+
/** Factory function to create a session for a given ID and client sink. */
|
|
39
|
+
createSession: (sessionId: string, client: ClientSink) => Session;
|
|
40
|
+
/** Protocol config sent to the client immediately on connect. */
|
|
41
|
+
readyConfig: ReadyConfig;
|
|
42
|
+
/** Additional key-value pairs included in log messages. */
|
|
43
|
+
logContext?: Record<string, string>;
|
|
44
|
+
/** Callback invoked when the WebSocket connection opens. */
|
|
45
|
+
onOpen?: () => void;
|
|
46
|
+
/** Callback invoked when the WebSocket connection closes. */
|
|
47
|
+
onClose?: () => void;
|
|
48
|
+
/** Callback invoked with the session ID after session cleanup. */
|
|
49
|
+
onSessionEnd?: (sessionId: string) => void;
|
|
50
|
+
/** Logger instance. Defaults to console. */
|
|
51
|
+
logger?: Logger;
|
|
52
|
+
/** Timeout in ms for session.start(). Defaults to 10 000 (10s). */
|
|
53
|
+
sessionStartTimeoutMs?: number;
|
|
54
|
+
/** Old session ID to resume. When set, reuses this ID instead of generating a new UUID. */
|
|
55
|
+
resumeFrom?: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Creates a {@link ClientSink} backed by a plain WebSocket.
|
|
60
|
+
*
|
|
61
|
+
* Text events are sent as JSON text frames; audio chunks are sent as
|
|
62
|
+
* binary frames (zero-copy).
|
|
63
|
+
*/
|
|
64
|
+
function createClientSink(ws: SessionWebSocket, log: Logger): ClientSink {
|
|
65
|
+
/** Send data over ws, silently dropping if the socket is not open. */
|
|
66
|
+
function safeSend(data: string | ArrayBuffer | Uint8Array): void {
|
|
67
|
+
try {
|
|
68
|
+
if (ws.readyState !== WS_OPEN) return;
|
|
69
|
+
ws.send(data);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
log.debug?.("safeSend: socket closed between readyState check and send", {
|
|
72
|
+
error: errorMessage(err),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
get open() {
|
|
79
|
+
return ws.readyState === WS_OPEN;
|
|
80
|
+
},
|
|
81
|
+
event(e) {
|
|
82
|
+
safeSend(JSON.stringify(e));
|
|
83
|
+
},
|
|
84
|
+
playAudioChunk(chunk) {
|
|
85
|
+
safeSend(chunk);
|
|
86
|
+
},
|
|
87
|
+
playAudioDone() {
|
|
88
|
+
safeSend(JSON.stringify({ type: "audio_done" }));
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function handleBinaryAudio(data: unknown, session: Session): boolean {
|
|
94
|
+
// Buffer extends Uint8Array in Node, so this catches Buffer too.
|
|
95
|
+
if (data instanceof Uint8Array) {
|
|
96
|
+
session.onAudio(data);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
if (data instanceof ArrayBuffer) {
|
|
100
|
+
session.onAudio(new Uint8Array(data));
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function handleTextMessage(
|
|
107
|
+
data: unknown,
|
|
108
|
+
session: Session,
|
|
109
|
+
log: Logger,
|
|
110
|
+
ctx: Record<string, string>,
|
|
111
|
+
sid: string,
|
|
112
|
+
): void {
|
|
113
|
+
if (typeof data !== "string") return;
|
|
114
|
+
let json: unknown;
|
|
115
|
+
try {
|
|
116
|
+
json = JSON.parse(data);
|
|
117
|
+
} catch {
|
|
118
|
+
log.warn("Invalid JSON from client", { ...ctx, sid });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const parsed = lenientParse(ClientMessageSchema, json);
|
|
123
|
+
if (!parsed.ok) {
|
|
124
|
+
if (parsed.malformed) log.warn("Invalid client message", { ...ctx, sid, error: parsed.error });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const msg: ClientMessage = parsed.data;
|
|
129
|
+
switch (msg.type) {
|
|
130
|
+
case "audio_ready":
|
|
131
|
+
session.onAudioReady();
|
|
132
|
+
break;
|
|
133
|
+
case "cancel":
|
|
134
|
+
session.onCancel();
|
|
135
|
+
break;
|
|
136
|
+
case "reset":
|
|
137
|
+
session.onReset();
|
|
138
|
+
break;
|
|
139
|
+
case "history":
|
|
140
|
+
session.onHistory(msg.messages);
|
|
141
|
+
break;
|
|
142
|
+
default:
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Attaches session lifecycle handlers to a native WebSocket using
|
|
149
|
+
* plain JSON text frames and binary audio frames.
|
|
150
|
+
*
|
|
151
|
+
* Connection flow:
|
|
152
|
+
* 1. WebSocket opens → server sends `{ type: "config", ...ReadyConfig }`
|
|
153
|
+
* 2. Client sets up audio → sends `{ type: "audio_ready" }`
|
|
154
|
+
* 3. If reconnecting → client sends `{ type: "history", messages: [...] }`
|
|
155
|
+
*/
|
|
156
|
+
export function wireSessionSocket(ws: SessionWebSocket, opts: WsSessionOptions): void {
|
|
157
|
+
const { sessions, logger: log = consoleLogger } = opts;
|
|
158
|
+
const sessionId = opts.resumeFrom ?? crypto.randomUUID();
|
|
159
|
+
const sid = sessionId.slice(0, 8);
|
|
160
|
+
const ctx = opts.logContext ?? {};
|
|
161
|
+
|
|
162
|
+
let session: Session | null = null;
|
|
163
|
+
/** Set to true once session.start() resolves. Messages arriving before
|
|
164
|
+
* this flag is set are buffered and replayed once the session is ready,
|
|
165
|
+
* preventing audio/text from being dispatched to a half-initialized session. */
|
|
166
|
+
let sessionReady = false;
|
|
167
|
+
let messageBuffer: { data: unknown }[] | null = [];
|
|
168
|
+
|
|
169
|
+
function drainBuffer(): void {
|
|
170
|
+
if (!(session && messageBuffer)) return;
|
|
171
|
+
const buf = messageBuffer;
|
|
172
|
+
messageBuffer = null;
|
|
173
|
+
for (const event of buf) {
|
|
174
|
+
const { data } = event;
|
|
175
|
+
if (handleBinaryAudio(data, session)) continue;
|
|
176
|
+
handleTextMessage(data, session, log, ctx, sid);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function onOpen(): void {
|
|
181
|
+
opts.onOpen?.();
|
|
182
|
+
log.info("Session connected", { ...ctx, sid });
|
|
183
|
+
|
|
184
|
+
const client = createClientSink(ws, log);
|
|
185
|
+
session = opts.createSession(sessionId, client);
|
|
186
|
+
sessions.set(sessionId, session);
|
|
187
|
+
|
|
188
|
+
// Send config immediately — zero RTT. Include sessionId so the client
|
|
189
|
+
// can reconnect with ?sessionId=<id> to resume a persisted session.
|
|
190
|
+
ws.send(JSON.stringify({ type: "config", ...opts.readyConfig, sessionId }));
|
|
191
|
+
|
|
192
|
+
const timeoutMs = opts.sessionStartTimeoutMs ?? DEFAULT_SESSION_START_TIMEOUT_MS;
|
|
193
|
+
const startWithTimeout = pTimeout(session.start(), {
|
|
194
|
+
milliseconds: timeoutMs,
|
|
195
|
+
message: `session.start() timed out after ${timeoutMs}ms`,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
startWithTimeout
|
|
199
|
+
.then(() => {
|
|
200
|
+
log.info("Session ready", { ...ctx, sid });
|
|
201
|
+
sessionReady = true;
|
|
202
|
+
drainBuffer();
|
|
203
|
+
})
|
|
204
|
+
.catch((err: unknown) => {
|
|
205
|
+
log.error("Session start failed", { ...ctx, sid, error: errorDetail(err) });
|
|
206
|
+
sessions.delete(sessionId);
|
|
207
|
+
session = null;
|
|
208
|
+
messageBuffer = null;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// readyState OPEN — socket already open (e.g. from ws handleUpgrade)
|
|
213
|
+
if (ws.readyState === WS_OPEN) {
|
|
214
|
+
onOpen();
|
|
215
|
+
} else {
|
|
216
|
+
ws.addEventListener("open", onOpen);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
ws.addEventListener("message", (event) => {
|
|
220
|
+
if (!session) return;
|
|
221
|
+
// Buffer messages until session.start() completes to avoid dispatching
|
|
222
|
+
// to a session whose S2S connection isn't established yet.
|
|
223
|
+
if (!sessionReady) {
|
|
224
|
+
if (messageBuffer && messageBuffer.length < MAX_MESSAGE_BUFFER_SIZE) {
|
|
225
|
+
messageBuffer.push(event);
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const { data } = event;
|
|
230
|
+
|
|
231
|
+
if (handleBinaryAudio(data, session)) return;
|
|
232
|
+
handleTextMessage(data, session, log, ctx, sid);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
ws.addEventListener("close", () => {
|
|
236
|
+
log.info("Session disconnected", { ...ctx, sid });
|
|
237
|
+
if (session) {
|
|
238
|
+
void session
|
|
239
|
+
.stop()
|
|
240
|
+
.catch((err: unknown) => {
|
|
241
|
+
log.error("Session stop failed", { ...ctx, sid, error: errorDetail(err) });
|
|
242
|
+
})
|
|
243
|
+
.finally(() => {
|
|
244
|
+
sessions.delete(sessionId);
|
|
245
|
+
opts.onSessionEnd?.(sessionId);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
opts.onClose?.();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
ws.addEventListener("error", (ev) => {
|
|
252
|
+
const msg = typeof ev.message === "string" ? ev.message : "WebSocket error";
|
|
253
|
+
log.error("WebSocket error", { ...ctx, sid, error: msg });
|
|
254
|
+
});
|
|
255
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
+
/**
|
|
3
|
+
* aai — shared fundamentals with no Node.js dependencies.
|
|
4
|
+
*
|
|
5
|
+
* Types, KV interface, utils, and constants used across
|
|
6
|
+
* aai-cli, aai-server, and aai-ui.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// biome-ignore-all lint/performance/noReExportAll: barrel file by design
|
|
10
|
+
|
|
11
|
+
export * from "./sdk/constants.ts";
|
|
12
|
+
export * from "./sdk/define.ts";
|
|
13
|
+
export * from "./sdk/kv.ts";
|
|
14
|
+
export * from "./sdk/types.ts";
|
|
15
|
+
export * from "./sdk/utils.ts";
|
|
16
|
+
export * from "./sdk/ws-upgrade.ts";
|
package/package.json
CHANGED
|
@@ -1,97 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alexkroman1/aai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"files": [
|
|
6
|
-
"dist"
|
|
7
|
-
],
|
|
8
5
|
"exports": {
|
|
9
6
|
".": {
|
|
10
7
|
"@dev/source": "./index.ts",
|
|
11
8
|
"types": "./dist/index.d.ts",
|
|
12
9
|
"import": "./dist/index.js"
|
|
13
10
|
},
|
|
14
|
-
"./isolate": {
|
|
15
|
-
"@dev/source": "./isolate/index.ts",
|
|
16
|
-
"types": "./dist/isolate/index.d.ts",
|
|
17
|
-
"import": "./dist/isolate/index.js"
|
|
18
|
-
},
|
|
19
|
-
"./host": {
|
|
20
|
-
"@dev/source": "./host/index.ts",
|
|
21
|
-
"types": "./dist/host/index.d.ts",
|
|
22
|
-
"import": "./dist/host/index.js"
|
|
23
|
-
},
|
|
24
|
-
"./server": {
|
|
25
|
-
"@dev/source": "./host/server.ts",
|
|
26
|
-
"types": "./dist/host/server.d.ts",
|
|
27
|
-
"import": "./dist/host/server.js"
|
|
28
|
-
},
|
|
29
|
-
"./types": {
|
|
30
|
-
"@dev/source": "./isolate/types.ts",
|
|
31
|
-
"types": "./dist/isolate/types.d.ts",
|
|
32
|
-
"import": "./dist/isolate/types.js"
|
|
33
|
-
},
|
|
34
|
-
"./kv": {
|
|
35
|
-
"@dev/source": "./isolate/kv.ts",
|
|
36
|
-
"types": "./dist/isolate/kv.d.ts",
|
|
37
|
-
"import": "./dist/isolate/kv.js"
|
|
38
|
-
},
|
|
39
11
|
"./protocol": {
|
|
40
|
-
"@dev/source": "./
|
|
41
|
-
"types": "./dist/
|
|
42
|
-
"import": "./dist/
|
|
43
|
-
},
|
|
44
|
-
"./testing": {
|
|
45
|
-
"@dev/source": "./host/testing.ts",
|
|
46
|
-
"types": "./dist/host/testing.d.ts",
|
|
47
|
-
"import": "./dist/host/testing.js"
|
|
12
|
+
"@dev/source": "./sdk/protocol.ts",
|
|
13
|
+
"types": "./dist/sdk/protocol.d.ts",
|
|
14
|
+
"import": "./dist/sdk/protocol.js"
|
|
48
15
|
},
|
|
49
|
-
"./
|
|
50
|
-
"@dev/source": "./host/
|
|
51
|
-
"types": "./dist/host/
|
|
52
|
-
"import": "./dist/host/
|
|
16
|
+
"./runtime": {
|
|
17
|
+
"@dev/source": "./host/runtime-barrel.ts",
|
|
18
|
+
"types": "./dist/host/runtime-barrel.d.ts",
|
|
19
|
+
"import": "./dist/host/runtime-barrel.js"
|
|
53
20
|
},
|
|
54
|
-
"./
|
|
55
|
-
"@dev/source": "./
|
|
56
|
-
"types": "./dist/
|
|
57
|
-
"import": "./dist/
|
|
58
|
-
},
|
|
59
|
-
"./utils": {
|
|
60
|
-
"@dev/source": "./isolate/_utils.ts",
|
|
61
|
-
"types": "./dist/isolate/_utils.d.ts",
|
|
62
|
-
"import": "./dist/isolate/_utils.js"
|
|
63
|
-
},
|
|
64
|
-
"./vite-plugin": {
|
|
65
|
-
"@dev/source": "./host/vite-plugin.ts",
|
|
66
|
-
"types": "./dist/host/vite-plugin.d.ts",
|
|
67
|
-
"import": "./dist/host/vite-plugin.js"
|
|
21
|
+
"./manifest": {
|
|
22
|
+
"@dev/source": "./sdk/manifest-barrel.ts",
|
|
23
|
+
"types": "./dist/sdk/manifest-barrel.d.ts",
|
|
24
|
+
"import": "./dist/sdk/manifest-barrel.js"
|
|
68
25
|
}
|
|
69
26
|
},
|
|
70
27
|
"dependencies": {
|
|
71
|
-
"
|
|
28
|
+
"escape-html": "^1.0.3",
|
|
29
|
+
"html-to-text": "^9.0.5",
|
|
30
|
+
"mime-types": "^3.0.2",
|
|
72
31
|
"nanoevents": "^9.1.0",
|
|
73
32
|
"p-timeout": "^7.0.1",
|
|
74
33
|
"unstorage": "^1.17.5",
|
|
75
34
|
"ws": "^8.20.0",
|
|
76
35
|
"zod": "^4.3.6"
|
|
77
36
|
},
|
|
78
|
-
"peerDependencies": {
|
|
79
|
-
"vitest": "^4.1.2"
|
|
80
|
-
},
|
|
81
|
-
"peerDependenciesMeta": {
|
|
82
|
-
"vitest": {
|
|
83
|
-
"optional": true
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
37
|
"devDependencies": {
|
|
38
|
+
"@types/escape-html": "^1.0.4",
|
|
39
|
+
"@types/html-to-text": "^9.0.4",
|
|
87
40
|
"@types/json-schema": "^7.0.15",
|
|
88
|
-
"@types/
|
|
41
|
+
"@types/mime-types": "^3.0.1",
|
|
42
|
+
"@types/node": "^25.5.2",
|
|
89
43
|
"@types/ws": "^8.18.1",
|
|
90
44
|
"tsdown": "^0.21.7",
|
|
91
|
-
"
|
|
45
|
+
"vitest": "^4.1.3"
|
|
92
46
|
},
|
|
93
47
|
"engines": {
|
|
94
|
-
"node": ">=
|
|
48
|
+
"node": ">=24"
|
|
95
49
|
},
|
|
96
50
|
"repository": {
|
|
97
51
|
"type": "git",
|
|
@@ -101,12 +55,10 @@
|
|
|
101
55
|
"scripts": {
|
|
102
56
|
"test": "vitest run",
|
|
103
57
|
"test:coverage": "vitest run --coverage",
|
|
104
|
-
"test:integration": "vitest run
|
|
58
|
+
"test:integration": "VITEST_INCLUDE=host/integration.test.ts,host/pentest.test.ts,host/run-code-sandbox.test.ts vitest run -c ../../vitest.slow.config.ts",
|
|
105
59
|
"check:integration": "pnpm run test:integration",
|
|
106
60
|
"build": "tsdown && tsc -p tsconfig.build.json",
|
|
107
|
-
"typecheck": "tsc --noEmit && tsc -p
|
|
108
|
-
"lint": "biome check ."
|
|
109
|
-
"check:publint": "publint",
|
|
110
|
-
"check:attw": "attw --pack --profile esm-only"
|
|
61
|
+
"typecheck": "tsc --noEmit && tsc -p sdk/tsconfig.json",
|
|
62
|
+
"lint": "biome check ."
|
|
111
63
|
}
|
|
112
64
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
+
import { expect, test } from "vitest";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { agentToolsToSchemas } from "./_internal-types.ts";
|
|
5
|
+
import type { ToolDef } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
test("agentToolsToSchemas - converts tool definitions to OpenAI schema", () => {
|
|
8
|
+
const tools: Record<string, ToolDef> = {
|
|
9
|
+
get_weather: {
|
|
10
|
+
description: "Get weather",
|
|
11
|
+
parameters: z.object({
|
|
12
|
+
city: z.string().describe("City"),
|
|
13
|
+
}),
|
|
14
|
+
execute: async () => {
|
|
15
|
+
/* noop */
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
set_alarm: {
|
|
19
|
+
description: "Set alarm",
|
|
20
|
+
parameters: z.object({
|
|
21
|
+
time: z.string(),
|
|
22
|
+
label: z.string().optional(),
|
|
23
|
+
}),
|
|
24
|
+
execute: async () => {
|
|
25
|
+
/* noop */
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
const schemas = agentToolsToSchemas(tools);
|
|
30
|
+
expect(schemas.length).toBe(2);
|
|
31
|
+
expect(schemas[0]?.name).toBe("get_weather");
|
|
32
|
+
expect(schemas[0]?.description).toBe("Get weather");
|
|
33
|
+
expect(schemas[1]?.name).toBe("set_alarm");
|
|
34
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
+
/**
|
|
3
|
+
* Internal type definitions shared by server and CLI.
|
|
4
|
+
*
|
|
5
|
+
* Note: this module is for internal use only and should not be used directly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { JSONSchema7 } from "json-schema";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import type { Message } from "./types.ts";
|
|
11
|
+
import { BuiltinToolSchema, ToolChoiceSchema, type ToolDef } from "./types.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Function signature for executing a tool by name.
|
|
15
|
+
*
|
|
16
|
+
* Used by session.ts to invoke tools, by direct-executor.ts and
|
|
17
|
+
* harness-runtime.ts to implement the execution.
|
|
18
|
+
*/
|
|
19
|
+
export type ExecuteTool = (
|
|
20
|
+
name: string,
|
|
21
|
+
args: Readonly<Record<string, unknown>>,
|
|
22
|
+
sessionId?: string,
|
|
23
|
+
messages?: readonly Message[],
|
|
24
|
+
) => Promise<string>;
|
|
25
|
+
|
|
26
|
+
// ─── AgentConfig ────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Zod schema for serializable agent configuration sent over the wire.
|
|
30
|
+
*
|
|
31
|
+
* This is the JSON-safe subset of the agent definition that can be
|
|
32
|
+
* transmitted between the worker and the host process via structured clone.
|
|
33
|
+
*/
|
|
34
|
+
export const AgentConfigSchema = z.object({
|
|
35
|
+
name: z.string().min(1),
|
|
36
|
+
systemPrompt: z.string(),
|
|
37
|
+
greeting: z.string(),
|
|
38
|
+
sttPrompt: z.string().optional(),
|
|
39
|
+
maxSteps: z.number().int().positive().optional(),
|
|
40
|
+
toolChoice: ToolChoiceSchema.optional(),
|
|
41
|
+
builtinTools: z.array(BuiltinToolSchema).readonly().optional(),
|
|
42
|
+
idleTimeoutMs: z.number().nonnegative().optional(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/** Serializable agent configuration — derived from {@link AgentConfigSchema}. */
|
|
46
|
+
export type AgentConfig = z.infer<typeof AgentConfigSchema>;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Input shape accepted by {@link toAgentConfig}. Covers both `AgentDef`
|
|
50
|
+
* (where `maxSteps` may be a function) and `IsolateConfig` (where it is
|
|
51
|
+
* always a number).
|
|
52
|
+
*/
|
|
53
|
+
export interface AgentConfigSource {
|
|
54
|
+
name: string;
|
|
55
|
+
systemPrompt: string;
|
|
56
|
+
greeting: string;
|
|
57
|
+
sttPrompt?: string | undefined;
|
|
58
|
+
maxSteps?: number | undefined;
|
|
59
|
+
toolChoice?: AgentConfig["toolChoice"] | undefined;
|
|
60
|
+
builtinTools?: Readonly<AgentConfig["builtinTools"]> | undefined;
|
|
61
|
+
idleTimeoutMs?: number | undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Extract the serializable {@link AgentConfig} subset from a source object. */
|
|
65
|
+
export function toAgentConfig(src: AgentConfigSource): AgentConfig {
|
|
66
|
+
const config: AgentConfig = {
|
|
67
|
+
name: src.name,
|
|
68
|
+
systemPrompt: src.systemPrompt,
|
|
69
|
+
greeting: src.greeting,
|
|
70
|
+
};
|
|
71
|
+
if (src.sttPrompt !== undefined) config.sttPrompt = src.sttPrompt;
|
|
72
|
+
if (src.maxSteps !== undefined) config.maxSteps = src.maxSteps;
|
|
73
|
+
if (src.toolChoice !== undefined) config.toolChoice = src.toolChoice;
|
|
74
|
+
if (src.builtinTools) config.builtinTools = [...src.builtinTools];
|
|
75
|
+
if (src.idleTimeoutMs !== undefined) config.idleTimeoutMs = src.idleTimeoutMs;
|
|
76
|
+
return config;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── ToolSchema ─────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Zod schema for serialized tool definitions sent over the wire.
|
|
83
|
+
*
|
|
84
|
+
* `parameters` must be a valid JSON Schema object (with `type`, `properties`,
|
|
85
|
+
* etc.) — the Vercel AI SDK wraps it via `jsonSchema()`.
|
|
86
|
+
*/
|
|
87
|
+
export const ToolSchemaSchema = z.object({
|
|
88
|
+
name: z.string().min(1),
|
|
89
|
+
description: z.string().min(1),
|
|
90
|
+
parameters: z.record(z.string(), z.unknown()),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
/** Serialized tool schema — derived from {@link ToolSchemaSchema}. */
|
|
94
|
+
export type ToolSchema = {
|
|
95
|
+
name: string;
|
|
96
|
+
description: string;
|
|
97
|
+
parameters: JSONSchema7;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** Empty Zod object schema used as default when tools have no parameters. */
|
|
101
|
+
export const EMPTY_PARAMS = z.object({});
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Convert agent tool definitions to JSON Schema format for wire transport.
|
|
105
|
+
*
|
|
106
|
+
* Transforms the Zod-based `parameters` of each tool into a plain JSON Schema
|
|
107
|
+
* object suitable for structured clone / JSON serialization.
|
|
108
|
+
*/
|
|
109
|
+
export function agentToolsToSchemas(tools: Readonly<Record<string, ToolDef>>): ToolSchema[] {
|
|
110
|
+
return Object.entries(tools).map(([name, def]) => ({
|
|
111
|
+
name,
|
|
112
|
+
description: def.description,
|
|
113
|
+
parameters: z.toJSONSchema(def.parameters ?? EMPTY_PARAMS) as JSONSchema7,
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Protocol Compatibility Fixtures
|
|
2
|
+
|
|
3
|
+
Pinned JSON snapshots of valid wire-format messages. Unlike inline snapshot
|
|
4
|
+
tests, these files **never auto-update** — they represent what
|
|
5
|
+
already-deployed clients and agents actually send and receive.
|
|
6
|
+
|
|
7
|
+
## When to create a new version
|
|
8
|
+
|
|
9
|
+
Create a new `v{N}.json` when you intentionally change the protocol and
|
|
10
|
+
have confirmed all deployed clients/agents have been updated. The old
|
|
11
|
+
fixture stays to protect any stragglers.
|
|
12
|
+
|
|
13
|
+
## Rules
|
|
14
|
+
|
|
15
|
+
- **Never modify** an existing fixture file after it's committed.
|
|
16
|
+
- **Never delete** a fixture unless you're certain no deployed code depends
|
|
17
|
+
on that version.
|
|
18
|
+
- One example per variant, plus examples with/without optional fields.
|
|
19
|
+
|
|
20
|
+
## What's covered
|
|
21
|
+
|
|
22
|
+
- `ServerMessage` — all server-to-client WebSocket JSON messages
|
|
23
|
+
- `ClientMessage` — all client-to-server WebSocket JSON messages
|
|
24
|
+
- `KvRequest` — KV operations (also used by the sidecar since it shares
|
|
25
|
+
the same schema)
|
|
26
|
+
- `constants` — wire-format constants (audio format, sample rates, error codes)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"created": "2026-04-07",
|
|
4
|
+
"description": "Initial baseline: all WebSocket message types and KV operations",
|
|
5
|
+
"ServerMessage": [
|
|
6
|
+
{ "type": "config", "audioFormat": "pcm16", "sampleRate": 16000, "ttsSampleRate": 24000 },
|
|
7
|
+
{
|
|
8
|
+
"type": "config",
|
|
9
|
+
"audioFormat": "pcm16",
|
|
10
|
+
"sampleRate": 16000,
|
|
11
|
+
"ttsSampleRate": 24000,
|
|
12
|
+
"sessionId": "sess_abc"
|
|
13
|
+
},
|
|
14
|
+
{ "type": "audio_done" },
|
|
15
|
+
{ "type": "speech_started" },
|
|
16
|
+
{ "type": "speech_stopped" },
|
|
17
|
+
{ "type": "user_transcript", "text": "hello" },
|
|
18
|
+
{ "type": "user_transcript", "text": "hello world" },
|
|
19
|
+
{ "type": "user_transcript", "text": "hello", "turnOrder": 1 },
|
|
20
|
+
{ "type": "agent_transcript", "text": "response" },
|
|
21
|
+
{
|
|
22
|
+
"type": "tool_call",
|
|
23
|
+
"toolCallId": "tc1",
|
|
24
|
+
"toolName": "web_search",
|
|
25
|
+
"args": { "query": "weather" }
|
|
26
|
+
},
|
|
27
|
+
{ "type": "tool_call_done", "toolCallId": "tc1", "result": "72F" },
|
|
28
|
+
{ "type": "reply_done" },
|
|
29
|
+
{ "type": "cancelled" },
|
|
30
|
+
{ "type": "reset" },
|
|
31
|
+
{ "type": "idle_timeout" },
|
|
32
|
+
{ "type": "error", "code": "stt", "message": "Speech recognition failed" },
|
|
33
|
+
{ "type": "error", "code": "internal", "message": "something went wrong" }
|
|
34
|
+
],
|
|
35
|
+
"ClientMessage": [
|
|
36
|
+
{ "type": "audio_ready" },
|
|
37
|
+
{ "type": "cancel" },
|
|
38
|
+
{ "type": "reset" },
|
|
39
|
+
{
|
|
40
|
+
"type": "history",
|
|
41
|
+
"messages": [{ "role": "user", "content": "Hello" }, { "role": "assistant", "content": "Hi" }]
|
|
42
|
+
},
|
|
43
|
+
{ "type": "history", "messages": [] }
|
|
44
|
+
],
|
|
45
|
+
"KvRequest": [
|
|
46
|
+
{ "op": "get", "key": "k1" },
|
|
47
|
+
{ "op": "set", "key": "k1", "value": "v1" },
|
|
48
|
+
{ "op": "set", "key": "k1", "value": { "nested": true } },
|
|
49
|
+
{ "op": "set", "key": "k1", "value": "v1", "expireIn": 60000 },
|
|
50
|
+
{ "op": "del", "key": "k1" }
|
|
51
|
+
],
|
|
52
|
+
"constants": {
|
|
53
|
+
"AUDIO_FORMAT": "pcm16",
|
|
54
|
+
"DEFAULT_STT_SAMPLE_RATE": 16000,
|
|
55
|
+
"DEFAULT_TTS_SAMPLE_RATE": 24000,
|
|
56
|
+
"MAX_TOOL_RESULT_CHARS": 4000,
|
|
57
|
+
"SessionErrorCodes": [
|
|
58
|
+
"stt",
|
|
59
|
+
"llm",
|
|
60
|
+
"tts",
|
|
61
|
+
"tool",
|
|
62
|
+
"protocol",
|
|
63
|
+
"connection",
|
|
64
|
+
"audio",
|
|
65
|
+
"internal"
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
}
|