@alexkroman1/aai 1.7.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/.turbo/turbo-build.log +11 -9
  2. package/CHANGELOG.md +10 -0
  3. package/dist/{_internal-types-CrnTi9Ew.js → _internal-types-CfOAbK6V.js} +22 -35
  4. package/dist/constants-y68COEGj.js +29 -0
  5. package/dist/host/_base64.d.ts +2 -0
  6. package/dist/host/_mock-ws.d.ts +0 -61
  7. package/dist/host/_pipeline-test-fakes.d.ts +7 -4
  8. package/dist/host/_run-code.d.ts +0 -25
  9. package/dist/host/_runtime-conformance.d.ts +3 -34
  10. package/dist/host/memory-vector.d.ts +0 -11
  11. package/dist/host/providers/resolve-kv.d.ts +0 -7
  12. package/dist/host/providers/resolve-vector.d.ts +0 -8
  13. package/dist/host/providers/stt/assemblyai.d.ts +0 -14
  14. package/dist/host/providers/stt/deepgram.d.ts +2 -14
  15. package/dist/host/providers/stt/soniox.d.ts +0 -22
  16. package/dist/host/providers/tts/rime.d.ts +10 -31
  17. package/dist/host/runtime-barrel.js +619 -630
  18. package/dist/host/runtime-config.d.ts +9 -6
  19. package/dist/host/runtime.d.ts +3 -0
  20. package/dist/host/to-vercel-tools.d.ts +3 -33
  21. package/dist/host/transports/openai-realtime-transport.d.ts +43 -0
  22. package/dist/host/unstorage-kv.d.ts +0 -26
  23. package/dist/index.js +3 -3
  24. package/dist/openai-realtime-cjPAHMMx.js +10 -0
  25. package/dist/sdk/_internal-types.d.ts +6 -55
  26. package/dist/sdk/allowed-hosts.d.ts +4 -3
  27. package/dist/sdk/constants.d.ts +4 -29
  28. package/dist/sdk/define.d.ts +7 -4
  29. package/dist/sdk/kv.d.ts +13 -37
  30. package/dist/sdk/manifest-barrel.js +1 -1
  31. package/dist/sdk/manifest.d.ts +8 -2
  32. package/dist/sdk/protocol.js +1 -1
  33. package/dist/sdk/providers/s2s/openai-realtime.d.ts +17 -0
  34. package/dist/sdk/providers/s2s-barrel.d.ts +9 -0
  35. package/dist/sdk/providers/s2s-barrel.js +2 -0
  36. package/dist/sdk/providers/tts/rime.d.ts +1 -1
  37. package/dist/sdk/providers.d.ts +6 -2
  38. package/dist/sdk/types.d.ts +7 -1
  39. package/dist/{types-KUgezM6u.js → types-DOWVZhb9.js} +1 -7
  40. package/dist/{ws-upgrade-BeOQ7fXL.js → ws-upgrade-CG8-by1n.js} +2 -3
  41. package/host/_base64.ts +9 -0
  42. package/host/_mock-ws.ts +0 -65
  43. package/host/_pipeline-test-fakes.ts +19 -31
  44. package/host/_run-code.ts +10 -53
  45. package/host/_runtime-conformance.ts +3 -44
  46. package/host/_test-utils.ts +20 -42
  47. package/host/builtin-tools.test.ts +127 -222
  48. package/host/builtin-tools.ts +6 -10
  49. package/host/cleanup.test.ts +30 -73
  50. package/host/integration/pipeline-reference.integration.test.ts +12 -17
  51. package/host/integration.test.ts +0 -7
  52. package/host/memory-vector.test.ts +3 -1
  53. package/host/memory-vector.ts +16 -21
  54. package/host/pinecone-vector.test.ts +14 -17
  55. package/host/pinecone-vector.ts +10 -19
  56. package/host/providers/providers.test-d.ts +5 -3
  57. package/host/providers/resolve-kv.ts +23 -41
  58. package/host/providers/resolve-vector.ts +3 -12
  59. package/host/providers/resolve.test.ts +15 -28
  60. package/host/providers/resolve.ts +24 -24
  61. package/host/providers/stt/assemblyai.test.ts +2 -14
  62. package/host/providers/stt/assemblyai.ts +12 -35
  63. package/host/providers/stt/deepgram.test.ts +23 -83
  64. package/host/providers/stt/deepgram.ts +15 -40
  65. package/host/providers/stt/elevenlabs.test.ts +26 -38
  66. package/host/providers/stt/elevenlabs.ts +10 -9
  67. package/host/providers/stt/soniox.test.ts +35 -85
  68. package/host/providers/stt/soniox.ts +8 -53
  69. package/host/providers/tts/cartesia.test.ts +19 -58
  70. package/host/providers/tts/cartesia.ts +36 -66
  71. package/host/providers/tts/rime.test.ts +12 -38
  72. package/host/providers/tts/rime.ts +23 -86
  73. package/host/runtime-config.test.ts +9 -9
  74. package/host/runtime-config.ts +16 -22
  75. package/host/runtime.test.ts +111 -73
  76. package/host/runtime.ts +138 -86
  77. package/host/s2s.test.ts +92 -191
  78. package/host/s2s.ts +55 -49
  79. package/host/server-shutdown.test.ts +9 -30
  80. package/host/server.test.ts +2 -13
  81. package/host/server.ts +85 -100
  82. package/host/session-core.test.ts +15 -30
  83. package/host/session-core.ts +10 -13
  84. package/host/session-prompt.test.ts +1 -5
  85. package/host/to-vercel-tools.test.ts +53 -72
  86. package/host/to-vercel-tools.ts +9 -39
  87. package/host/tool-executor.test.ts +25 -51
  88. package/host/tool-executor.ts +18 -12
  89. package/host/transports/openai-realtime-transport.test.ts +371 -0
  90. package/host/transports/openai-realtime-transport.ts +319 -0
  91. package/host/transports/pipeline-transport.test.ts +125 -298
  92. package/host/transports/pipeline-transport.ts +20 -68
  93. package/host/transports/s2s-transport-fixtures.test.ts +31 -92
  94. package/host/transports/s2s-transport.test.ts +65 -134
  95. package/host/transports/s2s-transport.ts +15 -43
  96. package/host/transports/types.test.ts +4 -8
  97. package/host/unstorage-kv.test.ts +3 -2
  98. package/host/unstorage-kv.ts +5 -35
  99. package/host/ws-handler.test.ts +72 -176
  100. package/host/ws-handler.ts +6 -12
  101. package/package.json +6 -1
  102. package/sdk/__snapshots__/exports.test.ts.snap +7 -0
  103. package/sdk/__snapshots__/schema-shapes.test.ts.snap +1 -0
  104. package/sdk/_internal-types.test.ts +6 -9
  105. package/sdk/_internal-types.ts +16 -57
  106. package/sdk/_test-matchers.ts +25 -15
  107. package/sdk/allowed-hosts.test.ts +50 -114
  108. package/sdk/allowed-hosts.ts +8 -14
  109. package/sdk/constants.ts +5 -52
  110. package/sdk/define.test.ts +7 -6
  111. package/sdk/define.ts +7 -3
  112. package/sdk/exports.test.ts +6 -1
  113. package/sdk/kv.ts +13 -37
  114. package/sdk/manifest.test-d.ts +5 -0
  115. package/sdk/manifest.test.ts +61 -9
  116. package/sdk/manifest.ts +11 -11
  117. package/sdk/protocol-compat.test.ts +66 -98
  118. package/sdk/protocol-snapshot.test.ts +2 -16
  119. package/sdk/protocol.test.ts +13 -22
  120. package/sdk/providers/s2s/openai-realtime.ts +36 -0
  121. package/sdk/providers/s2s-barrel.ts +12 -0
  122. package/sdk/providers/tts/rime.ts +1 -1
  123. package/sdk/providers.ts +24 -5
  124. package/sdk/schema-alignment.test.ts +25 -73
  125. package/sdk/schema-shapes.test.ts +1 -29
  126. package/sdk/system-prompt.test.ts +0 -1
  127. package/sdk/system-prompt.ts +17 -19
  128. package/sdk/types-inference.test.ts +10 -36
  129. package/sdk/types.ts +7 -0
  130. package/sdk/ws-upgrade.test.ts +24 -23
  131. package/sdk/ws-upgrade.ts +2 -3
  132. package/tsdown.config.ts +8 -11
  133. package/dist/constants-C2nirZUI.js +0 -54
package/host/s2s.ts CHANGED
@@ -8,12 +8,10 @@ import WsWebSocket from "ws";
8
8
  import { z } from "zod";
9
9
  import { WS_OPEN } from "../sdk/constants.ts";
10
10
  import type { ClientEvent } from "../sdk/protocol.ts";
11
+ import { base64ToUint8, uint8ToBase64 } from "./_base64.ts";
11
12
  import type { Logger, S2SConfig } from "./runtime-config.ts";
12
13
  import { consoleLogger } from "./runtime-config.ts";
13
14
 
14
- const uint8ToBase64 = (bytes: Uint8Array): string => Buffer.from(bytes).toString("base64");
15
- const base64ToUint8 = (base64: string): Uint8Array => new Uint8Array(Buffer.from(base64, "base64"));
16
-
17
15
  export type S2sWebSocket = {
18
16
  readonly readyState: number;
19
17
  send(data: string): void;
@@ -32,8 +30,7 @@ export type CreateS2sWebSocket = (
32
30
  opts: { headers: Record<string, string> },
33
31
  ) => S2sWebSocket;
34
32
 
35
- // Node's native WebSocket doesn't support custom headers.
36
- // Use the `ws` package which accepts { headers } in the constructor.
33
+ // Node's native WebSocket doesn't support custom headers; the `ws` package does.
37
34
  export const defaultCreateS2sWebSocket: CreateS2sWebSocket = (url, opts) =>
38
35
  new WsWebSocket(url, { headers: opts.headers }) as unknown as S2sWebSocket;
39
36
 
@@ -41,7 +38,12 @@ export const defaultCreateS2sWebSocket: CreateS2sWebSocket = (url, opts) =>
41
38
 
42
39
  const S2sMessageSchema = z.discriminatedUnion("type", [
43
40
  z.object({ type: z.literal("session.ready"), session_id: z.string() }).passthrough(),
44
- z.object({ type: z.literal("session.updated") }).passthrough(),
41
+ z
42
+ .object({
43
+ type: z.literal("session.updated"),
44
+ config: z.object({ id: z.string().optional() }).passthrough().optional(),
45
+ })
46
+ .passthrough(),
45
47
  z.object({ type: z.literal("input.speech.started") }),
46
48
  z.object({ type: z.literal("input.speech.stopped") }),
47
49
  z.object({ type: z.literal("transcript.user"), item_id: z.string(), text: z.string() }),
@@ -86,12 +88,14 @@ export type S2sEvent = ClientEvent & { _interrupted?: boolean };
86
88
  type DispatchState = { speechActive: boolean };
87
89
 
88
90
  type DispatchContext = {
89
- /** Logger used for diagnostic `S2S <<` arrival logs. */
90
91
  log: Logger;
91
- /** Session id threaded through diagnostic logs; omitted when undefined. */
92
92
  sid?: string;
93
93
  };
94
94
 
95
+ function sidFields(ctx: DispatchContext): { sid?: string } {
96
+ return ctx.sid !== undefined ? { sid: ctx.sid } : {};
97
+ }
98
+
95
99
  function dispatchS2sMessage(
96
100
  callbacks: S2sCallbacks,
97
101
  msg: S2sServerMessage,
@@ -103,6 +107,10 @@ function dispatchS2sMessage(
103
107
  callbacks.onSessionReady(msg.session_id);
104
108
  break;
105
109
  case "session.updated":
110
+ // The S2S API conveys the session id via `config.id` in the success
111
+ // path (no separate `session.ready` is emitted); capturing it here is
112
+ // required for resume on transient close.
113
+ if (msg.config?.id !== undefined) callbacks.onSessionReady(msg.config.id);
106
114
  break;
107
115
  case "input.speech.started":
108
116
  if (!state.speechActive) {
@@ -129,20 +137,21 @@ function dispatchS2sMessage(
129
137
  callbacks.onToolCall(msg.call_id, msg.name, msg.args);
130
138
  break;
131
139
  case "reply.done":
132
- // Log every raw reply.done arrival from the S2S service — one line per
133
- // event, before any client-facing dedup so we can cross-check which
134
- // stalled sessions actually received reply.done for their turn.
140
+ // Log every raw reply.done before client-facing dedup so we can
141
+ // cross-check which stalled sessions actually got one.
135
142
  ctx.log.info("S2S << reply.done", {
136
- ...(ctx.sid !== undefined ? { sid: ctx.sid } : {}),
143
+ ...sidFields(ctx),
137
144
  status: msg.status ?? "completed",
138
145
  });
139
- if (msg.status === "interrupted") {
140
- callbacks.onCancelled();
141
- } else {
142
- callbacks.onReplyDone();
143
- }
146
+ if (msg.status === "interrupted") callbacks.onCancelled();
147
+ else callbacks.onReplyDone();
144
148
  break;
145
149
  case "session.error":
150
+ ctx.log.warn("S2S << session.error", {
151
+ ...sidFields(ctx),
152
+ code: msg.code,
153
+ message: msg.message,
154
+ });
146
155
  if (msg.code === "session_not_found" || msg.code === "session_forbidden") {
147
156
  callbacks.onSessionExpired();
148
157
  } else {
@@ -234,12 +243,10 @@ export function connectS2s(opts: ConnectS2sOptions): Promise<S2sHandle> {
234
243
  return;
235
244
  }
236
245
  const json = JSON.stringify(msg);
237
- if (msg.type !== "input.audio") {
238
- if (msg.type === "session.update") {
239
- log.info(`S2S >> ${msg.type}`, { payload: json });
240
- } else {
241
- log.info(`S2S >> ${msg.type}`);
242
- }
246
+ if (msg.type === "session.update") {
247
+ log.info(`S2S >> ${msg.type}`, { payload: json });
248
+ } else if (msg.type !== "input.audio") {
249
+ log.info(`S2S >> ${msg.type}`);
243
250
  }
244
251
  ws.send(json);
245
252
  }
@@ -281,42 +288,41 @@ export function connectS2s(opts: ConnectS2sOptions): Promise<S2sHandle> {
281
288
  resolve(handle);
282
289
  });
283
290
 
284
- function tryParseJson(data: unknown): unknown | undefined {
285
- try {
286
- return JSON.parse(String(data));
287
- } catch {
288
- log.warn("S2S << invalid JSON", { data: String(data).slice(0, 200) });
289
- }
290
- }
291
-
292
- function handleAudioFastPath(obj: { type?: unknown; data?: unknown }): boolean {
293
- if (obj.type === "reply.audio" && typeof obj.data === "string") {
294
- callbacks.onAudio(base64ToUint8(obj.data));
295
- return true;
296
- }
297
- return false;
298
- }
299
-
300
- function logIncoming(obj: { type?: unknown }): void {
291
+ function logIncoming(type: unknown): void {
301
292
  // reply.audio and input.audio are ~95% of traffic — skip logging.
302
- if (obj.type === "reply.audio" || obj.type === "input.audio") return;
303
- // reply.done gets a richer log (sid + status) inside dispatch;
304
- // skip the generic line here to avoid a duplicate.
305
- if (obj.type === "reply.done") return;
306
- log.info(`S2S << ${obj.type}`);
293
+ // reply.done and session.error get richer logs inside dispatch;
294
+ // skip here to avoid a duplicate line.
295
+ if (
296
+ type === "reply.audio" ||
297
+ type === "input.audio" ||
298
+ type === "reply.done" ||
299
+ type === "session.error"
300
+ ) {
301
+ return;
302
+ }
303
+ log.info(`S2S << ${type}`);
307
304
  }
308
305
 
309
306
  ws.addEventListener("message", (ev) => {
310
- const raw = tryParseJson(ev.data);
311
- if (raw === undefined) return;
307
+ let raw: unknown;
308
+ try {
309
+ raw = JSON.parse(String(ev.data));
310
+ } catch {
311
+ log.warn("S2S << invalid JSON", { data: String(ev.data).slice(0, 200) });
312
+ return;
313
+ }
312
314
 
313
315
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
314
316
  log.warn("S2S << non-object JSON message", { type: typeof raw });
315
317
  return;
316
318
  }
317
319
  const obj = raw as Record<string, unknown>;
318
- logIncoming(obj);
319
- if (handleAudioFastPath(obj)) return;
320
+ logIncoming(obj.type);
321
+
322
+ if (obj.type === "reply.audio" && typeof obj.data === "string") {
323
+ callbacks.onAudio(base64ToUint8(obj.data));
324
+ return;
325
+ }
320
326
 
321
327
  const parsed = parseS2sMessage(obj);
322
328
  if (!parsed) {
@@ -1,10 +1,4 @@
1
1
  // Copyright 2025 the AAI authors. MIT license.
2
- /**
3
- * Tests for server shutdown timeout behavior.
4
- *
5
- * Creates a mock runtime with controlled shutdown behavior, then exercises
6
- * the timeout and graceful paths in close().
7
- */
8
2
 
9
3
  import { afterEach, describe, expect, test, vi } from "vitest";
10
4
  import { silentLogger } from "./_test-utils.ts";
@@ -24,35 +18,25 @@ function createMockRuntime(): Runtime {
24
18
  };
25
19
  }
26
20
 
27
- describe("server shutdown timeout", () => {
28
- let server: ReturnType<typeof createServer> | null = null;
21
+ async function startServer(): Promise<ReturnType<typeof createServer>> {
22
+ const server = createServer({ runtime: createMockRuntime(), logger: silentLogger });
23
+ await server.listen(0);
24
+ return server;
25
+ }
29
26
 
27
+ describe("server shutdown timeout", () => {
30
28
  afterEach(() => {
31
29
  mockShutdown = vi.fn<() => Promise<void>>().mockResolvedValue(undefined);
32
- server = null;
33
30
  });
34
31
 
35
32
  test("close calls runtime.shutdown()", async () => {
36
- mockShutdown = vi.fn<() => Promise<void>>().mockResolvedValue(undefined);
37
-
38
- server = createServer({
39
- runtime: createMockRuntime(),
40
- logger: silentLogger,
41
- });
42
- await server.listen(0);
43
-
33
+ const server = await startServer();
44
34
  await server.close();
45
35
  expect(mockShutdown).toHaveBeenCalledOnce();
46
36
  }, 10_000);
47
37
 
48
38
  test("close resolves quickly when runtime.shutdown() resolves", async () => {
49
- mockShutdown = vi.fn<() => Promise<void>>().mockResolvedValue(undefined);
50
-
51
- server = createServer({
52
- runtime: createMockRuntime(),
53
- logger: silentLogger,
54
- });
55
- await server.listen(0);
39
+ const server = await startServer();
56
40
 
57
41
  const start = Date.now();
58
42
  await server.close();
@@ -65,12 +49,7 @@ describe("server shutdown timeout", () => {
65
49
  test("close propagates when runtime.shutdown() rejects", async () => {
66
50
  mockShutdown = vi.fn<() => Promise<void>>().mockRejectedValue(new Error("boom"));
67
51
 
68
- server = createServer({
69
- runtime: createMockRuntime(),
70
- logger: silentLogger,
71
- });
72
- await server.listen(0);
73
-
52
+ const server = await startServer();
74
53
  await expect(server.close()).rejects.toThrow("boom");
75
54
  }, 10_000);
76
55
  });
@@ -36,16 +36,12 @@ describe("createServer", () => {
36
36
  const { runtime } = makeRuntime({ name: "health-agent" });
37
37
  server = createServer({ runtime, name: "health-agent", logger: silentLogger });
38
38
  await server.listen(0);
39
- await server.close();
40
- server = null;
41
39
  });
42
40
 
43
41
  test("listen and close lifecycle works", async () => {
44
42
  const { runtime } = makeRuntime();
45
43
  server = createServer({ runtime, logger: silentLogger });
46
44
  await server.listen(0);
47
- await server.close();
48
- server = null;
49
45
  });
50
46
 
51
47
  test("/ returns default HTML with escaped agent name", async () => {
@@ -77,11 +73,7 @@ describe("createServer", () => {
77
73
 
78
74
  test("/health returns JSON with agent name", async () => {
79
75
  const { runtime } = makeRuntime({ name: "my-agent" });
80
- server = createServer({
81
- runtime,
82
- name: "my-agent",
83
- logger: silentLogger,
84
- });
76
+ server = createServer({ runtime, name: "my-agent", logger: silentLogger });
85
77
  await server.listen(0);
86
78
 
87
79
  const res = await fetch(`http://localhost:${server.port}/health`);
@@ -91,10 +83,7 @@ describe("createServer", () => {
91
83
 
92
84
  test("404 triggers error-level logging", async () => {
93
85
  const { runtime } = makeRuntime();
94
- server = createServer({
95
- runtime,
96
- logger: silentLogger,
97
- });
86
+ server = createServer({ runtime, logger: silentLogger });
98
87
  await server.listen(0);
99
88
 
100
89
  await fetch(`http://localhost:${server.port}/nonexistent-path`);
package/host/server.ts CHANGED
@@ -51,7 +51,12 @@ export type AgentServer = {
51
51
  port: number | undefined;
52
52
  };
53
53
 
54
- // ── Static file serving ─────────────────────────────────────────────────
54
+ const JSON_HEADERS = { "Content-Type": "application/json" } as const;
55
+
56
+ function sendJson(res: http.ServerResponse, status: number, body: unknown): void {
57
+ res.writeHead(status, JSON_HEADERS);
58
+ res.end(JSON.stringify(body));
59
+ }
55
60
 
56
61
  async function serveStatic(
57
62
  dir: string,
@@ -61,8 +66,8 @@ async function serveStatic(
61
66
  const url = req.url?.split("?")[0] ?? "/";
62
67
  const filePath = path.join(dir, url === "/" ? "index.html" : url);
63
68
 
64
- // Prevent path traversal — use resolved dir + separator to avoid prefix
65
- // collisions (e.g. dir="/app/static" matching "/app/static-secrets/…").
69
+ // Use resolved dir + separator to avoid prefix collisions
70
+ // (e.g. dir="/app/static" matching "/app/static-secrets/…").
66
71
  const resolved = path.resolve(dir);
67
72
  if (!filePath.startsWith(resolved + path.sep) && filePath !== resolved) return false;
68
73
 
@@ -79,77 +84,72 @@ async function serveStatic(
79
84
  }
80
85
  }
81
86
 
82
- // ── Server ──────────────────────────────────────────────────────────────
87
+ async function readBody(req: http.IncomingMessage): Promise<string> {
88
+ let body = "";
89
+ for await (const chunk of req) body += chunk;
90
+ return body;
91
+ }
83
92
 
84
- function handleVectorPost(
93
+ async function handleVectorPost(
85
94
  vector: Vector,
86
95
  req: http.IncomingMessage,
87
96
  res: http.ServerResponse,
88
- ): void {
89
- let body = "";
90
- req.on("data", (chunk) => {
91
- body += chunk;
92
- });
93
- req.on("end", async () => {
94
- try {
95
- const json = JSON.parse(body);
96
- const parsed = VectorRequestSchema.safeParse(json);
97
- if (!parsed.success) {
98
- res.statusCode = 400;
99
- res.end(JSON.stringify({ error: parsed.error.message }));
100
- return;
101
- }
102
- const op = parsed.data;
103
- let result: unknown;
104
- switch (op.op) {
105
- case "upsert":
106
- await vector.upsert(op.id, op.text, op.metadata);
107
- result = "OK";
108
- break;
109
- case "query":
110
- result = await vector.query(op.text, {
111
- ...(op.topK !== undefined ? { topK: op.topK } : {}),
112
- ...(op.filter !== undefined ? { filter: op.filter } : {}),
113
- });
114
- break;
115
- case "delete":
116
- await vector.delete(op.ids);
117
- result = "OK";
118
- break;
119
- default:
120
- break;
97
+ ): Promise<void> {
98
+ try {
99
+ const parsed = VectorRequestSchema.safeParse(JSON.parse(await readBody(req)));
100
+ if (!parsed.success) {
101
+ sendJson(res, 400, { error: parsed.error.message });
102
+ return;
103
+ }
104
+ const op = parsed.data;
105
+ let result: unknown;
106
+ switch (op.op) {
107
+ case "upsert":
108
+ await vector.upsert(op.id, op.text, op.metadata);
109
+ result = "OK";
110
+ break;
111
+ case "query":
112
+ result = await vector.query(op.text, {
113
+ ...(op.topK !== undefined ? { topK: op.topK } : {}),
114
+ ...(op.filter !== undefined ? { filter: op.filter } : {}),
115
+ });
116
+ break;
117
+ case "delete":
118
+ await vector.delete(op.ids);
119
+ result = "OK";
120
+ break;
121
+ default: {
122
+ const _exhaustive: never = op;
123
+ return _exhaustive;
121
124
  }
122
- res.statusCode = 200;
123
- res.end(JSON.stringify({ result }));
124
- } catch (err) {
125
- res.statusCode = 500;
126
- res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
127
125
  }
128
- });
126
+ sendJson(res, 200, { result });
127
+ } catch (err) {
128
+ sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
129
+ }
129
130
  }
130
131
 
131
- function handleKvGet(kv: Kv, req: http.IncomingMessage, res: http.ServerResponse): void {
132
- const fullUrl = new URL(req.url ?? "/", "http://localhost");
133
- const key = fullUrl.searchParams.get("key");
132
+ async function handleKvGet(
133
+ kv: Kv,
134
+ req: http.IncomingMessage,
135
+ res: http.ServerResponse,
136
+ ): Promise<void> {
137
+ const key = new URL(req.url ?? "/", "http://localhost").searchParams.get("key");
134
138
  if (!key) {
135
- res.writeHead(400, { "Content-Type": "application/json" });
136
- res.end(JSON.stringify({ error: "Missing key query parameter" }));
139
+ sendJson(res, 400, { error: "Missing key query parameter" });
137
140
  return;
138
141
  }
139
- kv.get(key)
140
- .then((value) => {
141
- if (value === null) {
142
- res.writeHead(404, { "Content-Type": "application/json" });
143
- res.end("null");
144
- } else {
145
- res.writeHead(200, { "Content-Type": "application/json" });
146
- res.end(JSON.stringify(value));
147
- }
148
- })
149
- .catch(() => {
150
- res.writeHead(500, { "Content-Type": "application/json" });
151
- res.end(JSON.stringify({ error: "KV error" }));
152
- });
142
+ try {
143
+ const value = await kv.get(key);
144
+ if (value === null) {
145
+ res.writeHead(404, JSON_HEADERS);
146
+ res.end("null");
147
+ return;
148
+ }
149
+ sendJson(res, 200, value);
150
+ } catch {
151
+ sendJson(res, 500, { error: "KV error" });
152
+ }
153
153
  }
154
154
 
155
155
  /**
@@ -165,67 +165,52 @@ export function createServer(options: ServerOptions): AgentServer {
165
165
  throw new Error("clientHtml and clientDir are mutually exclusive");
166
166
  }
167
167
 
168
- // Pre-compute the default HTML page once (the agent name never changes).
169
- const escapedName = escapeHtml(name);
170
168
  const defaultHtml =
171
169
  clientHtml ??
172
- `<!DOCTYPE html><html><body><h1>${escapedName}</h1><p>Agent server running.</p></body></html>`;
170
+ `<!DOCTYPE html><html><body><h1>${escapeHtml(name)}</h1><p>Agent server running.</p></body></html>`;
171
+
172
+ async function handleRequest(
173
+ req: http.IncomingMessage,
174
+ res: http.ServerResponse,
175
+ url: string,
176
+ method: string,
177
+ ): Promise<void> {
178
+ if (clientDir && (await serveStatic(clientDir, req, res))) return;
179
+
180
+ if (method === "GET" && url === "/") {
181
+ res.writeHead(200, { "Content-Type": "text/html" });
182
+ res.end(defaultHtml);
183
+ return;
184
+ }
185
+
186
+ logger.error(`${method} ${url} 404`);
187
+ sendJson(res, 404, { error: "Not found" });
188
+ }
173
189
 
174
190
  const httpServer = http.createServer((req, res) => {
175
191
  const url = req.url?.split("?")[0] ?? "/";
176
192
  const method = req.method ?? "GET";
177
193
 
178
- // Security headers
179
194
  res.setHeader("Content-Security-Policy", AGENT_CSP);
180
195
  res.setHeader("X-Content-Type-Options", "nosniff");
181
196
  res.setHeader("X-Frame-Options", "SAMEORIGIN");
182
197
 
183
- // Health endpoint
184
198
  if (method === "GET" && url === "/health") {
185
- res.writeHead(200, { "Content-Type": "application/json" });
186
- res.end(JSON.stringify({ status: "ok", name }));
199
+ sendJson(res, 200, { status: "ok", name });
187
200
  return;
188
201
  }
189
-
190
- // KV endpoint
191
202
  if (kv && method === "GET" && url === "/kv") {
192
- handleKvGet(kv, req, res);
203
+ void handleKvGet(kv, req, res);
193
204
  return;
194
205
  }
195
-
196
- // Vector endpoint
197
206
  if (vector && method === "POST" && url === "/vector") {
198
- handleVectorPost(vector, req, res);
207
+ void handleVectorPost(vector, req, res);
199
208
  return;
200
209
  }
201
210
 
202
- // Routes that may need async handling
203
211
  void handleRequest(req, res, url, method);
204
212
  });
205
213
 
206
- async function handleRequest(
207
- req: http.IncomingMessage,
208
- res: http.ServerResponse,
209
- url: string,
210
- method: string,
211
- ): Promise<void> {
212
- // Static files from client dir
213
- if (clientDir && (await serveStatic(clientDir, req, res))) return;
214
-
215
- // Default HTML
216
- if (method === "GET" && url === "/") {
217
- res.writeHead(200, { "Content-Type": "text/html" });
218
- res.end(defaultHtml);
219
- return;
220
- }
221
-
222
- // 404
223
- logger.error(`${method} ${url} 404`);
224
- res.writeHead(404, { "Content-Type": "application/json" });
225
- res.end(JSON.stringify({ error: "Not found" }));
226
- }
227
-
228
- // WebSocket upgrade via ws
229
214
  const wss = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD_BYTES });
230
215
 
231
216
  httpServer.on("upgrade", (req, socket, head) => {
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, test, vi } from "vitest";
2
+ import type { AgentConfig } from "../sdk/_internal-types.ts";
2
3
  import type { ClientEvent, ClientSink } from "../sdk/protocol.ts";
3
4
  import { DEFAULT_SYSTEM_PROMPT } from "../sdk/types.ts";
4
5
  import { flush } from "./_test-utils.ts";
@@ -9,7 +10,7 @@ import type { Transport } from "./transports/types.ts";
9
10
  function makeSink(): {
10
11
  events: ClientEvent[];
11
12
  audioChunks: Uint8Array[];
12
- audioDoneCount: number;
13
+ readonly audioDoneCount: number;
13
14
  sink: ClientSink;
14
15
  } {
15
16
  const events: ClientEvent[] = [];
@@ -22,9 +23,7 @@ function makeSink(): {
22
23
  return audioDoneCount;
23
24
  },
24
25
  sink: {
25
- get open() {
26
- return true;
27
- },
26
+ open: true,
28
27
  event: (e) => {
29
28
  events.push(e);
30
29
  },
@@ -34,13 +33,13 @@ function makeSink(): {
34
33
  playAudioDone: () => {
35
34
  audioDoneCount++;
36
35
  },
37
- } satisfies ClientSink,
36
+ },
38
37
  };
39
38
  }
40
39
 
41
- function makeTransport(): Transport & { starts: number; stops: number } {
42
- let starts = 0,
43
- stops = 0;
40
+ function makeTransport(): Transport & { readonly starts: number; readonly stops: number } {
41
+ let starts = 0;
42
+ let stops = 0;
44
43
  return {
45
44
  start: async () => {
46
45
  starts++;
@@ -60,6 +59,10 @@ function makeTransport(): Transport & { starts: number; stops: number } {
60
59
  };
61
60
  }
62
61
 
62
+ function makeAgentConfig(overrides: Partial<AgentConfig> = {}): AgentConfig {
63
+ return { name: "test", systemPrompt: DEFAULT_SYSTEM_PROMPT, greeting: "", ...overrides };
64
+ }
65
+
63
66
  function makeCore(overrides: Partial<SessionCoreOptions> = {}): {
64
67
  core: SessionCore;
65
68
  sink: ReturnType<typeof makeSink>;
@@ -71,7 +74,7 @@ function makeCore(overrides: Partial<SessionCoreOptions> = {}): {
71
74
  id: "s-test",
72
75
  agent: "test-agent",
73
76
  client: sink.sink,
74
- agentConfig: { name: "test", systemPrompt: DEFAULT_SYSTEM_PROMPT, greeting: "" },
77
+ agentConfig: makeAgentConfig(),
75
78
  executeTool: vi.fn(async () => "ok"),
76
79
  transport,
77
80
  ...overrides,
@@ -98,12 +101,7 @@ describe("createSessionCore — lifecycle", () => {
98
101
  vi.useFakeTimers();
99
102
  try {
100
103
  const { core, sink } = makeCore({
101
- agentConfig: {
102
- name: "test",
103
- systemPrompt: DEFAULT_SYSTEM_PROMPT,
104
- greeting: "",
105
- idleTimeoutMs: 1000,
106
- } as unknown as SessionCoreOptions["agentConfig"],
104
+ agentConfig: makeAgentConfig({ idleTimeoutMs: 1000 }),
107
105
  });
108
106
  await core.start();
109
107
  await core.stop();
@@ -190,10 +188,8 @@ describe("createSessionCore — tool call pending results", () => {
190
188
  await core.start();
191
189
  core.onReplyStarted("r1");
192
190
  core.onToolCall("cid", "my_tool", {});
193
- // Let the async tool IIFE settle and push to pendingTools
194
191
  await flush();
195
192
  core.onReplyDone();
196
- // Poll until tool results are forwarded and toolCallDone fires
197
193
  await vi.waitFor(() =>
198
194
  expect(transport.sendToolResult).toHaveBeenCalledWith("cid", "tool-output"),
199
195
  );
@@ -206,12 +202,7 @@ describe("createSessionCore — idle timeout", () => {
206
202
  vi.useFakeTimers();
207
203
  try {
208
204
  const { core, sink } = makeCore({
209
- agentConfig: {
210
- name: "t",
211
- systemPrompt: DEFAULT_SYSTEM_PROMPT,
212
- greeting: "",
213
- idleTimeoutMs: 1000,
214
- } as unknown as SessionCoreOptions["agentConfig"],
205
+ agentConfig: makeAgentConfig({ name: "t", idleTimeoutMs: 1000 }),
215
206
  });
216
207
  await core.start();
217
208
  expect(sink.events.filter((e) => e.type === "idle_timeout")).toHaveLength(0);
@@ -225,12 +216,7 @@ describe("createSessionCore — idle timeout", () => {
225
216
  vi.useFakeTimers();
226
217
  try {
227
218
  const { core, sink } = makeCore({
228
- agentConfig: {
229
- name: "t",
230
- systemPrompt: DEFAULT_SYSTEM_PROMPT,
231
- greeting: "",
232
- idleTimeoutMs: 1000,
233
- } as unknown as SessionCoreOptions["agentConfig"],
219
+ agentConfig: makeAgentConfig({ name: "t", idleTimeoutMs: 1000 }),
234
220
  });
235
221
  await core.start();
236
222
  vi.advanceTimersByTime(500);
@@ -251,7 +237,6 @@ describe("createSessionCore — history", () => {
251
237
  await core.start();
252
238
  core.onHistory([{ role: "user", content: "prior" }]);
253
239
  core.onUserTranscript("now");
254
- // No direct introspection — but onReset clears history and replay should see no effect on subsequent behavior.
255
240
  core.onReset();
256
241
  });
257
242
  });