@alexkroman1/aai 0.12.2 → 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.
Files changed (135) hide show
  1. package/.turbo/turbo-build.log +20 -0
  2. package/CHANGELOG.md +174 -0
  3. package/dist/constants-VTFoymJ-.js +47 -0
  4. package/dist/host/_run-code.d.ts +4 -2
  5. package/dist/host/_runtime-conformance.d.ts +4 -5
  6. package/dist/host/builtin-tools.d.ts +11 -7
  7. package/dist/host/runtime-barrel.d.ts +15 -0
  8. package/dist/{direct-executor-ZUU0Ke4j.js → host/runtime-barrel.js} +463 -345
  9. package/dist/host/runtime-config.d.ts +42 -0
  10. package/dist/host/runtime.d.ts +119 -35
  11. package/dist/host/s2s.d.ts +14 -38
  12. package/dist/host/server.d.ts +16 -8
  13. package/dist/host/session-ctx.d.ts +55 -0
  14. package/dist/host/session.d.ts +21 -70
  15. package/dist/host/tool-executor.d.ts +20 -0
  16. package/dist/host/unstorage-kv.d.ts +1 -1
  17. package/dist/host/ws-handler.d.ts +4 -2
  18. package/dist/index.d.ts +9 -20
  19. package/dist/index.js +63 -2
  20. package/dist/{isolate → sdk}/_internal-types.d.ts +6 -10
  21. package/dist/{isolate → sdk}/constants.d.ts +6 -4
  22. package/dist/sdk/define.d.ts +66 -0
  23. package/dist/{isolate → sdk}/kv.d.ts +1 -49
  24. package/dist/sdk/manifest-barrel.d.ts +8 -0
  25. package/dist/sdk/manifest-barrel.js +52 -0
  26. package/dist/sdk/manifest.d.ts +50 -0
  27. package/dist/{isolate → sdk}/protocol.d.ts +59 -36
  28. package/dist/sdk/protocol.js +163 -0
  29. package/dist/{isolate → sdk}/system-prompt.d.ts +3 -2
  30. package/dist/sdk/types.d.ts +201 -0
  31. package/dist/sdk/ws-upgrade.d.ts +5 -0
  32. package/dist/{system-prompt-CVJSQJiA.js → system-prompt-nik_iavo.js} +11 -10
  33. package/dist/types-Cfx_4QDK.js +39 -0
  34. package/dist/ws-upgrade-BeOQ7fXL.js +30 -0
  35. package/exports-no-dev-deps.test.ts +62 -0
  36. package/host/_mock-ws.ts +185 -0
  37. package/host/_run-code.ts +217 -0
  38. package/host/_runtime-conformance.ts +143 -0
  39. package/host/_test-utils.ts +276 -0
  40. package/host/builtin-tools.test.ts +774 -0
  41. package/host/builtin-tools.ts +255 -0
  42. package/host/cleanup.test.ts +422 -0
  43. package/host/fixture-replay.test.ts +463 -0
  44. package/host/fixtures/README.md +40 -0
  45. package/host/fixtures/greeting-session-sequence.json +40 -0
  46. package/host/fixtures/reply-audio-samples.json +42 -0
  47. package/host/fixtures/reply-lifecycle.json +21 -0
  48. package/host/fixtures/session-ready.json +48 -0
  49. package/host/fixtures/session-updated.json +45 -0
  50. package/host/fixtures/simple-question-sequence.json +73 -0
  51. package/host/fixtures/tool-call-sequence.json +114 -0
  52. package/host/fixtures/tool-calls.json +11 -0
  53. package/host/fixtures/tool-config-session-sequence.json +51 -0
  54. package/host/fixtures/user-speech-recognition.json +30 -0
  55. package/host/fixtures/web-search-sequence.json +122 -0
  56. package/host/integration.test.ts +222 -0
  57. package/host/runtime-barrel.ts +25 -0
  58. package/host/runtime-config.test.ts +71 -0
  59. package/host/runtime-config.ts +99 -0
  60. package/host/runtime.test.ts +641 -0
  61. package/host/runtime.ts +308 -0
  62. package/host/s2s-fixtures.test.ts +237 -0
  63. package/host/s2s.test.ts +562 -0
  64. package/host/s2s.ts +310 -0
  65. package/host/server-shutdown.test.ts +76 -0
  66. package/host/server.test.ts +116 -0
  67. package/host/server.ts +223 -0
  68. package/host/session-ctx.ts +107 -0
  69. package/host/session-fixture-replay.test.ts +136 -0
  70. package/host/session-prompt.test.ts +77 -0
  71. package/host/session.test.ts +590 -0
  72. package/host/session.ts +370 -0
  73. package/host/tool-executor.test.ts +124 -0
  74. package/host/tool-executor.ts +80 -0
  75. package/host/unstorage-kv.test.ts +99 -0
  76. package/host/unstorage-kv.ts +69 -0
  77. package/host/ws-handler.test.ts +739 -0
  78. package/host/ws-handler.ts +255 -0
  79. package/index.ts +16 -0
  80. package/package.json +28 -72
  81. package/sdk/_internal-types.test.ts +34 -0
  82. package/sdk/_internal-types.ts +115 -0
  83. package/sdk/compat-fixtures/README.md +26 -0
  84. package/sdk/compat-fixtures/v1.json +68 -0
  85. package/sdk/constants.ts +77 -0
  86. package/sdk/define.test.ts +57 -0
  87. package/sdk/define.ts +88 -0
  88. package/sdk/kv.ts +60 -0
  89. package/sdk/manifest-barrel.ts +12 -0
  90. package/sdk/manifest.test.ts +56 -0
  91. package/sdk/manifest.ts +89 -0
  92. package/sdk/protocol-compat.test.ts +187 -0
  93. package/sdk/protocol-snapshot.test.ts +199 -0
  94. package/sdk/protocol.test.ts +170 -0
  95. package/sdk/protocol.ts +223 -0
  96. package/sdk/schema-alignment.test.ts +191 -0
  97. package/sdk/system-prompt.test.ts +111 -0
  98. package/sdk/system-prompt.ts +74 -0
  99. package/sdk/tsconfig.json +12 -0
  100. package/sdk/types-inference.test.ts +122 -0
  101. package/sdk/types.test.ts +14 -0
  102. package/sdk/types.ts +226 -0
  103. package/sdk/utils.test.ts +52 -0
  104. package/sdk/utils.ts +20 -0
  105. package/sdk/ws-upgrade.test.ts +48 -0
  106. package/sdk/ws-upgrade.ts +13 -0
  107. package/tsconfig.build.json +14 -0
  108. package/tsconfig.json +10 -0
  109. package/tsdown.config.ts +26 -0
  110. package/vitest.config.ts +17 -0
  111. package/dist/host/_test-utils.d.ts +0 -73
  112. package/dist/host/direct-executor.d.ts +0 -128
  113. package/dist/host/index.d.ts +0 -18
  114. package/dist/host/index.js +0 -165
  115. package/dist/host/matchers.d.ts +0 -20
  116. package/dist/host/matchers.js +0 -41
  117. package/dist/host/server.js +0 -164
  118. package/dist/host/testing.d.ts +0 -294
  119. package/dist/host/testing.js +0 -2
  120. package/dist/host/vite-plugin.d.ts +0 -15
  121. package/dist/host/vite-plugin.js +0 -83
  122. package/dist/isolate/_kv-utils.d.ts +0 -10
  123. package/dist/isolate/_utils.js +0 -17
  124. package/dist/isolate/hooks.d.ts +0 -44
  125. package/dist/isolate/hooks.js +0 -58
  126. package/dist/isolate/index.d.ts +0 -18
  127. package/dist/isolate/index.js +0 -6
  128. package/dist/isolate/kv.js +0 -1
  129. package/dist/isolate/protocol.js +0 -2
  130. package/dist/isolate/types.d.ts +0 -418
  131. package/dist/isolate/types.js +0 -175
  132. package/dist/protocol-rcOrz7T3.js +0 -183
  133. package/dist/testing-Bb2B5Uob.js +0 -513
  134. package/dist/types.test-d.d.ts +0 -7
  135. /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.12.2",
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": "./isolate/protocol.ts",
41
- "types": "./dist/isolate/protocol.d.ts",
42
- "import": "./dist/isolate/protocol.js"
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
- "./testing/matchers": {
50
- "@dev/source": "./host/matchers.ts",
51
- "types": "./dist/host/matchers.d.ts",
52
- "import": "./dist/host/matchers.js"
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
- "./hooks": {
55
- "@dev/source": "./isolate/hooks.ts",
56
- "types": "./dist/isolate/hooks.d.ts",
57
- "import": "./dist/isolate/hooks.js"
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
- "hookable": "^6.1.0",
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.1"
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/node": "^25.5.0",
41
+ "@types/mime-types": "^3.0.1",
42
+ "@types/node": "^25.5.2",
89
43
  "@types/ws": "^8.18.1",
90
- "tsdown": "^0.21.5",
91
- "vite": "^8.0.3"
44
+ "tsdown": "^0.21.7",
45
+ "vitest": "^4.1.3"
92
46
  },
93
47
  "engines": {
94
- "node": ">=22.6"
48
+ "node": ">=24"
95
49
  },
96
50
  "repository": {
97
51
  "type": "git",
@@ -99,10 +53,12 @@
99
53
  "directory": "packages/aai"
100
54
  },
101
55
  "scripts": {
56
+ "test": "vitest run",
57
+ "test:coverage": "vitest run --coverage",
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",
59
+ "check:integration": "pnpm run test:integration",
102
60
  "build": "tsdown && tsc -p tsconfig.build.json",
103
- "typecheck": "tsc --noEmit && tsc -p isolate/tsconfig.json",
104
- "lint": "biome check .",
105
- "check:publint": "publint",
106
- "check:attw": "attw --pack --profile esm-only"
61
+ "typecheck": "tsc --noEmit && tsc -p sdk/tsconfig.json",
62
+ "lint": "biome check ."
107
63
  }
108
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
+ }