@alexkroman1/aai 1.7.0 → 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 +16 -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 +628 -642
  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 +56 -53
  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
@@ -12,6 +12,10 @@ import { DEFAULT_STT_SAMPLE_RATE, DEFAULT_TTS_SAMPLE_RATE } from "../sdk/constan
12
12
  /** Structured context attached to log messages. */
13
13
  export type LogContext = Record<string, unknown>;
14
14
 
15
+ type LogLevel = "info" | "warn" | "error" | "debug";
16
+
17
+ type LogFn = (msg: string, ctx?: LogContext) => void;
18
+
15
19
  /**
16
20
  * Structured logger interface. Used by tests to suppress output and by
17
21
  * consumers to plug in custom logging backends.
@@ -27,15 +31,10 @@ export type LogContext = Record<string, unknown>;
27
31
  * createServer({ agent, logger: myLogger });
28
32
  * ```
29
33
  */
30
- export type Logger = {
31
- info(msg: string, ctx?: LogContext): void;
32
- warn(msg: string, ctx?: LogContext): void;
33
- error(msg: string, ctx?: LogContext): void;
34
- debug(msg: string, ctx?: LogContext): void;
35
- };
34
+ export type Logger = Record<LogLevel, LogFn>;
36
35
 
37
- function consoleLog(fn: typeof console.log): Logger[keyof Logger] {
38
- return (msg: string, ctx?: LogContext) => (ctx ? fn(msg, ctx) : fn(msg));
36
+ function consoleLog(fn: typeof console.log): LogFn {
37
+ return (msg, ctx) => (ctx ? fn(msg, ctx) : fn(msg));
39
38
  }
40
39
 
41
40
  /** Default console-backed logger. */
@@ -46,29 +45,24 @@ export const consoleLogger: Logger = {
46
45
  debug: consoleLog(console.debug),
47
46
  };
48
47
 
49
- /**
50
- * Structured JSON logger for production diagnostics. Each log entry is a
51
- * single-line JSON object with `timestamp`, `level`, `msg`, and any
52
- * caller-provided context fields.
53
- */
54
- function jsonLog(level: string) {
55
- return (msg: string, ctx?: LogContext): void => {
48
+ function jsonLog(level: LogLevel): LogFn {
49
+ const out = level === "error" || level === "warn" ? process.stderr : process.stdout;
50
+ return (msg, ctx) => {
56
51
  const entry: Record<string, unknown> = {
57
52
  timestamp: new Date().toISOString(),
58
53
  level,
59
54
  msg,
55
+ ...ctx,
60
56
  };
61
-
62
- if (ctx) {
63
- Object.assign(entry, ctx);
64
- }
65
-
66
- // Single-line JSON to stdout/stderr based on level.
67
- const out = level === "error" || level === "warn" ? process.stderr : process.stdout;
68
57
  out.write(`${JSON.stringify(entry)}\n`);
69
58
  };
70
59
  }
71
60
 
61
+ /**
62
+ * Structured JSON logger for production diagnostics. Each log entry is a
63
+ * single-line JSON object with `timestamp`, `level`, `msg`, and any
64
+ * caller-provided context fields.
65
+ */
72
66
  export const jsonLogger: Logger = {
73
67
  info: jsonLog("info"),
74
68
  warn: jsonLog("warn"),
@@ -4,6 +4,8 @@ import { createStorage } from "unstorage";
4
4
  import { afterEach, describe, expect, test, vi } from "vitest";
5
5
  import { z } from "zod";
6
6
  import { toAgentConfig } from "../sdk/_internal-types.ts";
7
+ import { openaiRealtime } from "../sdk/providers/s2s/openai-realtime.ts";
8
+ import type { S2sProvider } from "../sdk/providers.ts";
7
9
  import type { ToolDef } from "../sdk/types.ts";
8
10
  import {
9
11
  createFakeLanguageModel,
@@ -14,9 +16,22 @@ import { CONFORMANCE_AGENT, testRuntime } from "./_runtime-conformance.ts";
14
16
  import { flush, makeAgent, makeClientSink, makeMockHandle, silentLogger } from "./_test-utils.ts";
15
17
  import { createRuntime } from "./runtime.ts";
16
18
  import { executeToolCall } from "./tool-executor.ts";
19
+ import type { OpenaiRealtimeWebSocket } from "./transports/openai-realtime-transport.ts";
17
20
  import { _internals } from "./transports/s2s-transport.ts";
18
21
  import { createUnstorageKv } from "./unstorage-kv.ts";
19
22
 
23
+ function makeLogger() {
24
+ return { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
25
+ }
26
+
27
+ function makeMockWs() {
28
+ return {
29
+ readyState: 1,
30
+ send: vi.fn(),
31
+ addEventListener: vi.fn(),
32
+ };
33
+ }
34
+
20
35
  describe("toAgentConfig", () => {
21
36
  test("maps name, systemPrompt, greeting from AgentDef", () => {
22
37
  const config = toAgentConfig(makeAgent());
@@ -237,7 +252,7 @@ describe("executeToolCall", () => {
237
252
  throw new Error("boom");
238
253
  },
239
254
  };
240
- const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
255
+ const logger = makeLogger();
241
256
  const result = await executeToolCall("failTool", {}, { tool, env: {}, logger });
242
257
  expect(result).toContain("error");
243
258
  expect(result).toContain("boom");
@@ -276,7 +291,7 @@ describe("executeToolCall", () => {
276
291
  return "ok";
277
292
  },
278
293
  };
279
- const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
294
+ const logger = makeLogger();
280
295
  const result = await executeToolCall("kvTool", {}, { tool, env: {}, logger });
281
296
  expect(result).toContain("error");
282
297
  expect(result).toContain("KV not available");
@@ -333,7 +348,6 @@ describe("createRuntime sandbox mode", () => {
333
348
  toolSchemas: mockToolSchemas,
334
349
  });
335
350
 
336
- // Should use the provided overrides, not build its own
337
351
  expect(runtime.toolSchemas).toBe(mockToolSchemas);
338
352
  const result = await runtime.executeTool("any_tool", {}, "s1", []);
339
353
  expect(result).toBe("mocked-result");
@@ -346,37 +360,17 @@ describe("createRuntime shutdown", () => {
346
360
  vi.restoreAllMocks();
347
361
  });
348
362
 
349
- /** Helper: create a mock WS (readyState=1) that captures event listeners. */
350
- function makeMockWs() {
351
- const listeners: Record<string, Array<(...args: unknown[]) => void>> = {};
352
- return {
353
- readyState: 1,
354
- send: vi.fn(),
355
- listeners,
356
- addEventListener: vi.fn((type: string, listener: (...args: unknown[]) => void) => {
357
- if (!listeners[type]) listeners[type] = [];
358
- listeners[type].push(listener);
359
- }),
360
- };
361
- }
362
-
363
363
  test("shutdown stops active sessions gracefully", async () => {
364
364
  const mockHandle = makeMockHandle();
365
365
  const connectSpy = vi.spyOn(_internals, "connectS2s").mockResolvedValue(mockHandle);
366
366
 
367
- const agent = makeAgent();
368
- const runtime = createRuntime({ agent, env: {}, logger: silentLogger });
369
- const ws = makeMockWs();
370
-
371
- // readyState=1 means onOpen fires immediately in wireSessionSocket
372
- runtime.startSession(ws as never);
367
+ const runtime = createRuntime({ agent: makeAgent(), env: {}, logger: silentLogger });
368
+ runtime.startSession(makeMockWs() as never);
373
369
 
374
- // Wait for session.start() to resolve (fires on next tick via setTimeout)
375
370
  await vi.waitFor(() => {
376
371
  expect(connectSpy).toHaveBeenCalled();
377
372
  });
378
373
  await flush();
379
- // Give session.start() time to resolve
380
374
  await new Promise((r) => setTimeout(r, 50));
381
375
 
382
376
  await expect(runtime.shutdown()).resolves.toBeUndefined();
@@ -384,19 +378,14 @@ describe("createRuntime shutdown", () => {
384
378
  });
385
379
 
386
380
  test("shutdown warns when a session stop rejects", async () => {
387
- const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
388
381
  const mockHandle = makeMockHandle();
389
- // Make close() throw to cause session.stop() to reject
390
382
  mockHandle.close = vi.fn(() => {
391
383
  throw new Error("close failed");
392
384
  });
393
385
  const connectSpy = vi.spyOn(_internals, "connectS2s").mockResolvedValue(mockHandle);
394
386
 
395
- const agent = makeAgent();
396
- const runtime = createRuntime({ agent, env: {}, logger });
397
- const ws = makeMockWs();
398
-
399
- runtime.startSession(ws as never);
387
+ const runtime = createRuntime({ agent: makeAgent(), env: {}, logger: makeLogger() });
388
+ runtime.startSession(makeMockWs() as never);
400
389
 
401
390
  await vi.waitFor(() => {
402
391
  expect(connectSpy).toHaveBeenCalled();
@@ -404,31 +393,23 @@ describe("createRuntime shutdown", () => {
404
393
  await flush();
405
394
 
406
395
  await runtime.shutdown();
407
- // The session stop rejection should be caught and logged
408
- // (Note: whether the warn fires depends on whether stop() actually rejects
409
- // from close() throwing — session.stop() may catch it internally)
410
396
  connectSpy.mockRestore();
411
397
  });
412
398
 
413
399
  test("shutdown warns on timeout when sessions hang", async () => {
414
- const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
415
400
  const mockHandle = makeMockHandle();
416
- // Make close() hang forever so stop() never resolves
417
401
  mockHandle.close = vi.fn(() => {
418
- // intentionally do nothing — session stop will hang
402
+ /* no-op */
419
403
  });
420
404
  const connectSpy = vi.spyOn(_internals, "connectS2s").mockResolvedValue(mockHandle);
421
405
 
422
- const agent = makeAgent();
423
406
  const runtime = createRuntime({
424
- agent,
407
+ agent: makeAgent(),
425
408
  env: {},
426
- logger,
427
- shutdownTimeoutMs: 50, // Very short timeout
409
+ logger: makeLogger(),
410
+ shutdownTimeoutMs: 50,
428
411
  });
429
- const ws = makeMockWs();
430
-
431
- runtime.startSession(ws as never);
412
+ runtime.startSession(makeMockWs() as never);
432
413
 
433
414
  await vi.waitFor(() => {
434
415
  expect(connectSpy).toHaveBeenCalled();
@@ -436,7 +417,6 @@ describe("createRuntime shutdown", () => {
436
417
  await flush();
437
418
 
438
419
  await runtime.shutdown();
439
- // Whether timeout warning fires depends on internal session map population
440
420
  connectSpy.mockRestore();
441
421
  });
442
422
 
@@ -448,8 +428,9 @@ describe("createRuntime shutdown", () => {
448
428
  increment: {
449
429
  description: "Increment counter",
450
430
  execute: (_args, ctx) => {
451
- (ctx.state as { counter: number }).counter++;
452
- return String((ctx.state as { counter: number }).counter);
431
+ const state = ctx.state as { counter: number };
432
+ state.counter++;
433
+ return String(state.counter);
453
434
  },
454
435
  },
455
436
  get_state: {
@@ -460,13 +441,10 @@ describe("createRuntime shutdown", () => {
460
441
  });
461
442
  const runtime = createRuntime({ agent, env: {} });
462
443
 
463
- // First call creates state
464
444
  await runtime.executeTool("increment", {}, "s1", []);
465
- // Second call reuses same state
466
445
  await runtime.executeTool("increment", {}, "s1", []);
467
446
  const result = await runtime.executeTool("get_state", {}, "s1", []);
468
447
  expect(JSON.parse(result)).toEqual({ counter: 2 });
469
- // State factory should have been called only once
470
448
  expect(stateFactory).toHaveBeenCalledTimes(1);
471
449
  });
472
450
  });
@@ -494,7 +472,6 @@ describe("createRuntime createSession", () => {
494
472
  const agent = makeAgent();
495
473
  const runtime = createRuntime({ agent, env: {} });
496
474
  const client = makeClientSink();
497
- // Should not throw when skipGreeting is set
498
475
  const session = runtime.createSession({
499
476
  id: "test-session",
500
477
  agent: agent.name,
@@ -520,16 +497,10 @@ describe("createRuntime createSession", () => {
520
497
 
521
498
  describe("createRuntime startSession", () => {
522
499
  test("startSession wires WebSocket and passes options", () => {
523
- const agent = makeAgent();
524
- const runtime = createRuntime({ agent, env: {}, logger: silentLogger });
525
- const mockWs = {
526
- readyState: 1,
527
- send: vi.fn(),
528
- addEventListener: vi.fn(),
529
- };
500
+ const runtime = createRuntime({ agent: makeAgent(), env: {}, logger: silentLogger });
501
+ const ws = makeMockWs();
530
502
 
531
- // Should not throw
532
- runtime.startSession(mockWs as never, {
503
+ runtime.startSession(ws as never, {
533
504
  skipGreeting: true,
534
505
  resumeFrom: "prev-session",
535
506
  logContext: { userId: "u1" },
@@ -537,21 +508,15 @@ describe("createRuntime startSession", () => {
537
508
  onClose: vi.fn(),
538
509
  });
539
510
 
540
- // addEventListener should have been called to wire up the WebSocket
541
- expect(mockWs.addEventListener).toHaveBeenCalled();
511
+ expect(ws.addEventListener).toHaveBeenCalled();
542
512
  });
543
513
 
544
514
  test("startSession works with no options", () => {
545
- const agent = makeAgent();
546
- const runtime = createRuntime({ agent, env: {}, logger: silentLogger });
547
- const mockWs = {
548
- readyState: 1,
549
- send: vi.fn(),
550
- addEventListener: vi.fn(),
551
- };
515
+ const runtime = createRuntime({ agent: makeAgent(), env: {}, logger: silentLogger });
516
+ const ws = makeMockWs();
552
517
 
553
- runtime.startSession(mockWs as never);
554
- expect(mockWs.addEventListener).toHaveBeenCalled();
518
+ runtime.startSession(ws as never);
519
+ expect(ws.addEventListener).toHaveBeenCalled();
555
520
  });
556
521
  });
557
522
 
@@ -582,7 +547,6 @@ describe("createRuntime with custom options", () => {
582
547
  env: { ASSEMBLYAI_API_KEY: "test-api-key" },
583
548
  });
584
549
  const client = makeClientSink();
585
- // Should not throw — the API key gets passed to createS2sTransport internally
586
550
  const session = runtime.createSession({
587
551
  id: "test-session",
588
552
  agent: agent.name,
@@ -676,6 +640,80 @@ describe("Runtime — session routing", () => {
676
640
  await session.stop();
677
641
  connectSpy.mockRestore();
678
642
  });
643
+
644
+ test("agent.s2s = openaiRealtime() routes to OpenAI Realtime transport", async () => {
645
+ type Listener = (ev: unknown) => void;
646
+ const listeners: Record<string, Listener[]> = {
647
+ open: [],
648
+ message: [],
649
+ close: [],
650
+ error: [],
651
+ };
652
+ const fakeWs: OpenaiRealtimeWebSocket = {
653
+ readyState: 1,
654
+ send: vi.fn(),
655
+ close: vi.fn(),
656
+ addEventListener: ((type: string, fn: Listener) => {
657
+ (listeners[type] ?? []).push(fn);
658
+ }) as OpenaiRealtimeWebSocket["addEventListener"],
659
+ };
660
+ let capturedUrl: string | null = null;
661
+ let capturedOpts: { headers: Record<string, string> } | null = null;
662
+ const createOpenaiRealtimeWebSocket = vi.fn(
663
+ (url: string, wsOpts: { headers: Record<string, string> }) => {
664
+ capturedUrl = url;
665
+ capturedOpts = wsOpts;
666
+ return fakeWs;
667
+ },
668
+ );
669
+
670
+ const runtime = createRuntime({
671
+ agent: makeAgent({ s2s: openaiRealtime({ model: "gpt-realtime" }) }),
672
+ env: { OPENAI_API_KEY: "sk-test" },
673
+ logger: silentLogger,
674
+ createOpenaiRealtimeWebSocket,
675
+ });
676
+
677
+ const client = makeClientSink();
678
+ const session = runtime.createSession({
679
+ id: "sess-openai-realtime",
680
+ agent: "test-agent",
681
+ client,
682
+ });
683
+
684
+ const startP = session.start();
685
+ // Drive the WS open so transport.start() resolves
686
+ for (const fn of listeners.open ?? []) fn(undefined);
687
+ await startP;
688
+
689
+ expect(createOpenaiRealtimeWebSocket).toHaveBeenCalledTimes(1);
690
+ expect(capturedUrl).toContain("api.openai.com");
691
+ expect(capturedUrl).toContain("model=gpt-realtime");
692
+ expect(capturedOpts).toMatchObject({
693
+ headers: { Authorization: "Bearer sk-test" },
694
+ });
695
+
696
+ await session.stop();
697
+ });
698
+
699
+ test("createSession throws on unknown s2s provider kind", () => {
700
+ const runtime = createRuntime({
701
+ agent: makeAgent({
702
+ // Bypass typing for this test — descriptor with unrecognized kind:
703
+ s2s: { kind: "made-up-provider", options: {} } as unknown as S2sProvider,
704
+ }),
705
+ env: {},
706
+ logger: silentLogger,
707
+ });
708
+
709
+ expect(() =>
710
+ runtime.createSession({
711
+ id: "sess-bad",
712
+ agent: "test-agent",
713
+ client: makeClientSink(),
714
+ }),
715
+ ).toThrow(/Unknown s2s provider kind/);
716
+ });
679
717
  });
680
718
 
681
719
  // ── Shared conformance suite (same tests run against sandbox in integration) ─
package/host/runtime.ts CHANGED
@@ -15,6 +15,10 @@ import { DEFAULT_SHUTDOWN_TIMEOUT_MS } from "../sdk/constants.ts";
15
15
  import type { Kv } from "../sdk/kv.ts";
16
16
  import type { ClientSink } from "../sdk/protocol.ts";
17
17
  import { buildReadyConfig, type ReadyConfig } from "../sdk/protocol.ts";
18
+ import {
19
+ OPENAI_REALTIME_KIND,
20
+ type OpenaiRealtimeOptions,
21
+ } from "../sdk/providers/s2s/openai-realtime.ts";
18
22
  import { DEEPGRAM_KIND } from "../sdk/providers/stt/deepgram.ts";
19
23
  import { RIME_KIND } from "../sdk/providers/tts/rime.ts";
20
24
  import {
@@ -39,6 +43,11 @@ import { consoleLogger, DEFAULT_S2S_CONFIG } from "./runtime-config.ts";
39
43
  import type { CreateS2sWebSocket } from "./s2s.ts";
40
44
  import { createSessionCore, type SessionCore } from "./session-core.ts";
41
45
  import { type ExecuteTool, executeToolCall } from "./tool-executor.ts";
46
+ import {
47
+ type CreateOpenaiRealtimeWebSocket,
48
+ createOpenaiRealtimeTransport,
49
+ type OpenaiRealtimeToolSchema,
50
+ } from "./transports/openai-realtime-transport.ts";
42
51
  import { createPipelineTransport } from "./transports/pipeline-transport.ts";
43
52
  import { createS2sTransport } from "./transports/s2s-transport.ts";
44
53
  import type { Transport, TransportCallbacks } from "./transports/types.ts";
@@ -48,46 +57,27 @@ import { type SessionWebSocket, wireSessionSocket } from "./ws-handler.ts";
48
57
  // ─── Helpers ─────────────────────────────────────────────────────────────────
49
58
 
50
59
  /**
51
- * Resolve the API key env-var for the configured STT provider.
52
- *
53
- * Each STT provider uses its own env var (e.g. `ASSEMBLYAI_API_KEY`,
54
- * `DEEPGRAM_API_KEY`). We read the kind from the descriptor if it is one;
55
- * pre-resolved openers have no kind field so we fall back to AssemblyAI for
56
- * backward compatibility (openers supply their own key at open-time anyway).
60
+ * Read the descriptor `kind` if present. Pre-resolved openers (test escape
61
+ * hatch) have no `kind` field, so callers fall back to a default env var.
57
62
  */
63
+ function descriptorKind(value: object | undefined): string | undefined {
64
+ const kind = (value as { kind?: unknown } | undefined)?.kind;
65
+ return typeof kind === "string" ? kind : undefined;
66
+ }
67
+
58
68
  function resolveSttApiKey(
59
69
  stt: SttProvider | SttOpener | undefined,
60
70
  env: Record<string, string>,
61
71
  ): string {
62
- // SttProvider descriptors carry a `kind` field; SttOpener does not.
63
- const kind =
64
- stt != null && "kind" in stt && typeof (stt as SttProvider).kind === "string"
65
- ? (stt as SttProvider).kind
66
- : undefined;
67
- if (kind === DEEPGRAM_KIND) return resolveApiKey("DEEPGRAM_API_KEY", env);
68
- // Default: ASSEMBLYAI_KIND or pre-resolved opener (backward compat).
72
+ if (descriptorKind(stt) === DEEPGRAM_KIND) return resolveApiKey("DEEPGRAM_API_KEY", env);
69
73
  return resolveApiKey("ASSEMBLYAI_API_KEY", env);
70
74
  }
71
75
 
72
- /**
73
- * Resolve the API key env-var for the configured TTS provider.
74
- *
75
- * Each TTS provider uses its own env var (e.g. `CARTESIA_API_KEY`,
76
- * `RIME_API_KEY`). We read the kind from the descriptor if it is one;
77
- * pre-resolved openers have no kind field so we fall back to Cartesia for
78
- * backward compatibility (openers supply their own key at open-time anyway).
79
- */
80
76
  function resolveTtsApiKey(
81
77
  tts: TtsProvider | TtsOpener | undefined,
82
78
  env: Record<string, string>,
83
79
  ): string {
84
- // TtsProvider descriptors carry a `kind` field; TtsOpener does not.
85
- const kind =
86
- tts != null && "kind" in tts && typeof (tts as TtsProvider).kind === "string"
87
- ? (tts as TtsProvider).kind
88
- : undefined;
89
- if (kind === RIME_KIND) return resolveApiKey("RIME_API_KEY", env);
90
- // Default: CARTESIA_KIND or pre-resolved opener (backward compat).
80
+ if (descriptorKind(tts) === RIME_KIND) return resolveApiKey("RIME_API_KEY", env);
91
81
  return resolveApiKey("CARTESIA_API_KEY", env);
92
82
  }
93
83
 
@@ -173,6 +163,8 @@ export type RuntimeOptions = {
173
163
  vector?: Vector | undefined;
174
164
  /** Custom WebSocket factory for the S2S connection (useful for testing). */
175
165
  createWebSocket?: CreateS2sWebSocket | undefined;
166
+ /** Custom WebSocket factory for the OpenAI Realtime connection (testing). */
167
+ createOpenaiRealtimeWebSocket?: CreateOpenaiRealtimeWebSocket | undefined;
176
168
  logger?: Logger | undefined;
177
169
  s2sConfig?: S2SConfig | undefined;
178
170
  /**
@@ -276,6 +268,7 @@ export function createRuntime(opts: RuntimeOptions): Runtime {
276
268
  kv = createLocalKv(),
277
269
  vector,
278
270
  createWebSocket,
271
+ createOpenaiRealtimeWebSocket,
279
272
  logger = consoleLogger,
280
273
  s2sConfig = DEFAULT_S2S_CONFIG,
281
274
  sessionStartTimeoutMs,
@@ -369,25 +362,123 @@ export function createRuntime(opts: RuntimeOptions): Runtime {
369
362
  // Resolve pipeline providers once per runtime (not per session). Each
370
363
  // session reuses the same opener / LanguageModel — the opener's `open()`
371
364
  // mints the per-session stream inside.
372
- const pipelineProviders =
373
- mode === "pipeline"
374
- ? {
375
- // biome-ignore lint/style/noNonNullAssertion: mode === "pipeline" ⇒ all three set
376
- stt: resolveSttIfDescriptor(opts.stt!),
377
- // biome-ignore lint/style/noNonNullAssertion: mode === "pipeline" ⇒ all three set
378
- llm: resolveLlmIfDescriptor(opts.llm!, env),
379
- // biome-ignore lint/style/noNonNullAssertion: mode === "pipeline" ⇒ all three set
380
- tts: resolveTtsIfDescriptor(opts.tts!),
381
- }
382
- : null;
383
-
384
- function createSession(sessionOpts: {
365
+ let pipelineProviders: { stt: SttOpener; llm: LanguageModel; tts: TtsOpener } | null = null;
366
+ if (mode === "pipeline" && opts.stt && opts.llm && opts.tts) {
367
+ pipelineProviders = {
368
+ stt: resolveSttIfDescriptor(opts.stt),
369
+ llm: resolveLlmIfDescriptor(opts.llm, env),
370
+ tts: resolveTtsIfDescriptor(opts.tts),
371
+ };
372
+ }
373
+
374
+ type SessionOpts = {
385
375
  id: string;
386
376
  agent: string;
387
377
  client: ClientSink;
388
378
  skipGreeting?: boolean;
389
379
  resumeFrom?: string;
390
- }): SessionCore {
380
+ };
381
+
382
+ function buildPipelineTransport(args: {
383
+ sessionOpts: SessionOpts;
384
+ systemPrompt: string;
385
+ callbacks: TransportCallbacks;
386
+ providers: { stt: SttOpener; llm: LanguageModel; tts: TtsOpener };
387
+ }): Transport {
388
+ const { sessionOpts, systemPrompt, callbacks, providers } = args;
389
+ return createPipelineTransport({
390
+ sid: sessionOpts.id,
391
+ agent: sessionOpts.agent,
392
+ stt: providers.stt,
393
+ llm: providers.llm,
394
+ tts: providers.tts,
395
+ callbacks,
396
+ sessionConfig: {
397
+ systemPrompt,
398
+ greeting: agentConfig.greeting,
399
+ tools: toolSchemas,
400
+ },
401
+ toolSchemas,
402
+ executeTool,
403
+ providerKeys: {
404
+ stt: resolveSttApiKey(opts.stt, env),
405
+ tts: resolveTtsApiKey(opts.tts, env),
406
+ },
407
+ sttSampleRate: s2sConfig.inputSampleRate,
408
+ ttsSampleRate: s2sConfig.outputSampleRate,
409
+ maxSteps: agentConfig.maxSteps,
410
+ toolChoice: agentConfig.toolChoice,
411
+ skipGreeting: sessionOpts.skipGreeting ?? false,
412
+ logger,
413
+ });
414
+ }
415
+
416
+ function buildOpenaiRealtimeTransport(args: {
417
+ sessionOpts: SessionOpts;
418
+ systemPrompt: string;
419
+ callbacks: TransportCallbacks;
420
+ }): Transport {
421
+ const { sessionOpts, systemPrompt, callbacks } = args;
422
+ return createOpenaiRealtimeTransport({
423
+ apiKey: resolveApiKey("OPENAI_API_KEY", env),
424
+ options: (agent.s2s?.options ?? {}) as OpenaiRealtimeOptions,
425
+ sessionConfig: {
426
+ systemPrompt,
427
+ ...(agentConfig.greeting !== undefined ? { greeting: agentConfig.greeting } : {}),
428
+ tools: toolSchemas,
429
+ },
430
+ toolSchemas: toolSchemas as OpenaiRealtimeToolSchema[],
431
+ toolChoice: agentConfig.toolChoice ?? "auto",
432
+ callbacks,
433
+ sid: sessionOpts.id,
434
+ agent: sessionOpts.agent,
435
+ ...(createOpenaiRealtimeWebSocket ? { createWebSocket: createOpenaiRealtimeWebSocket } : {}),
436
+ logger,
437
+ });
438
+ }
439
+
440
+ function buildAssemblyS2sTransport(args: {
441
+ sessionOpts: SessionOpts;
442
+ systemPrompt: string;
443
+ callbacks: TransportCallbacks;
444
+ }): Transport {
445
+ const { sessionOpts, systemPrompt, callbacks } = args;
446
+ return createS2sTransport({
447
+ apiKey: env.ASSEMBLYAI_API_KEY ?? "",
448
+ s2sConfig,
449
+ sessionConfig: {
450
+ systemPrompt,
451
+ tools: toolSchemas as import("./s2s.ts").S2sToolSchema[],
452
+ ...(agentConfig.greeting !== undefined ? { greeting: agentConfig.greeting } : {}),
453
+ },
454
+ toolSchemas: toolSchemas as import("./s2s.ts").S2sToolSchema[],
455
+ callbacks,
456
+ sid: sessionOpts.id,
457
+ agent: sessionOpts.agent,
458
+ ...(createWebSocket ? { createWebSocket } : {}),
459
+ logger,
460
+ });
461
+ }
462
+
463
+ function buildTransport(args: {
464
+ sessionOpts: SessionOpts;
465
+ systemPrompt: string;
466
+ callbacks: TransportCallbacks;
467
+ }): Transport {
468
+ if (pipelineProviders) {
469
+ return buildPipelineTransport({ ...args, providers: pipelineProviders });
470
+ }
471
+ if (agent.s2s !== undefined) {
472
+ const kind = descriptorKind(agent.s2s);
473
+ if (kind === OPENAI_REALTIME_KIND) {
474
+ return buildOpenaiRealtimeTransport(args);
475
+ }
476
+ throw new Error(`Unknown s2s provider kind: ${kind ?? "<missing>"}`);
477
+ }
478
+ return buildAssemblyS2sTransport(args);
479
+ }
480
+
481
+ function createSession(sessionOpts: SessionOpts): SessionCore {
391
482
  sinkMap.set(sessionOpts.id, sessionOpts.client);
392
483
 
393
484
  const isPipeline = Boolean(pipelineProviders);
@@ -426,50 +517,11 @@ export function createRuntime(opts: RuntimeOptions): Runtime {
426
517
  onSpeechStopped: () => bindCore().onSpeechStopped(),
427
518
  };
428
519
 
429
- let transport: Transport;
430
- if (pipelineProviders) {
431
- transport = createPipelineTransport({
432
- sid: sessionOpts.id,
433
- agent: sessionOpts.agent,
434
- stt: pipelineProviders.stt,
435
- llm: pipelineProviders.llm,
436
- tts: pipelineProviders.tts,
437
- callbacks,
438
- sessionConfig: {
439
- systemPrompt,
440
- greeting: agentConfig.greeting,
441
- tools: toolSchemas,
442
- },
443
- toolSchemas,
444
- executeTool,
445
- providerKeys: {
446
- stt: resolveSttApiKey(opts.stt, env),
447
- tts: resolveTtsApiKey(opts.tts, env),
448
- },
449
- sttSampleRate: s2sConfig.inputSampleRate,
450
- ttsSampleRate: s2sConfig.outputSampleRate,
451
- maxSteps: agentConfig.maxSteps,
452
- toolChoice: agentConfig.toolChoice,
453
- skipGreeting: sessionOpts.skipGreeting ?? false,
454
- logger,
455
- });
456
- } else {
457
- transport = createS2sTransport({
458
- apiKey: env.ASSEMBLYAI_API_KEY ?? "",
459
- s2sConfig,
460
- sessionConfig: {
461
- systemPrompt,
462
- tools: toolSchemas as import("./s2s.ts").S2sToolSchema[],
463
- ...(agentConfig.greeting !== undefined ? { greeting: agentConfig.greeting } : {}),
464
- },
465
- toolSchemas: toolSchemas as import("./s2s.ts").S2sToolSchema[],
466
- callbacks,
467
- sid: sessionOpts.id,
468
- agent: sessionOpts.agent,
469
- ...(createWebSocket ? { createWebSocket } : {}),
470
- logger,
471
- });
472
- }
520
+ const transport = buildTransport({
521
+ sessionOpts,
522
+ systemPrompt,
523
+ callbacks,
524
+ });
473
525
 
474
526
  core = createSessionCore({
475
527
  id: sessionOpts.id,