@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.
- package/.turbo/turbo-build.log +11 -9
- package/CHANGELOG.md +16 -0
- package/dist/{_internal-types-CrnTi9Ew.js → _internal-types-CfOAbK6V.js} +22 -35
- package/dist/constants-y68COEGj.js +29 -0
- package/dist/host/_base64.d.ts +2 -0
- package/dist/host/_mock-ws.d.ts +0 -61
- package/dist/host/_pipeline-test-fakes.d.ts +7 -4
- package/dist/host/_run-code.d.ts +0 -25
- package/dist/host/_runtime-conformance.d.ts +3 -34
- package/dist/host/memory-vector.d.ts +0 -11
- package/dist/host/providers/resolve-kv.d.ts +0 -7
- package/dist/host/providers/resolve-vector.d.ts +0 -8
- package/dist/host/providers/stt/assemblyai.d.ts +0 -14
- package/dist/host/providers/stt/deepgram.d.ts +2 -14
- package/dist/host/providers/stt/soniox.d.ts +0 -22
- package/dist/host/providers/tts/rime.d.ts +10 -31
- package/dist/host/runtime-barrel.js +628 -642
- package/dist/host/runtime-config.d.ts +9 -6
- package/dist/host/runtime.d.ts +3 -0
- package/dist/host/to-vercel-tools.d.ts +3 -33
- package/dist/host/transports/openai-realtime-transport.d.ts +43 -0
- package/dist/host/unstorage-kv.d.ts +0 -26
- package/dist/index.js +3 -3
- package/dist/openai-realtime-cjPAHMMx.js +10 -0
- package/dist/sdk/_internal-types.d.ts +6 -55
- package/dist/sdk/allowed-hosts.d.ts +4 -3
- package/dist/sdk/constants.d.ts +4 -29
- package/dist/sdk/define.d.ts +7 -4
- package/dist/sdk/kv.d.ts +13 -37
- package/dist/sdk/manifest-barrel.js +1 -1
- package/dist/sdk/manifest.d.ts +8 -2
- package/dist/sdk/protocol.js +1 -1
- package/dist/sdk/providers/s2s/openai-realtime.d.ts +17 -0
- package/dist/sdk/providers/s2s-barrel.d.ts +9 -0
- package/dist/sdk/providers/s2s-barrel.js +2 -0
- package/dist/sdk/providers/tts/rime.d.ts +1 -1
- package/dist/sdk/providers.d.ts +6 -2
- package/dist/sdk/types.d.ts +7 -1
- package/dist/{types-KUgezM6u.js → types-DOWVZhb9.js} +1 -7
- package/dist/{ws-upgrade-BeOQ7fXL.js → ws-upgrade-CG8-by1n.js} +2 -3
- package/host/_base64.ts +9 -0
- package/host/_mock-ws.ts +0 -65
- package/host/_pipeline-test-fakes.ts +19 -31
- package/host/_run-code.ts +10 -53
- package/host/_runtime-conformance.ts +3 -44
- package/host/_test-utils.ts +20 -42
- package/host/builtin-tools.test.ts +127 -222
- package/host/builtin-tools.ts +6 -10
- package/host/cleanup.test.ts +30 -73
- package/host/integration/pipeline-reference.integration.test.ts +12 -17
- package/host/integration.test.ts +0 -7
- package/host/memory-vector.test.ts +3 -1
- package/host/memory-vector.ts +16 -21
- package/host/pinecone-vector.test.ts +14 -17
- package/host/pinecone-vector.ts +10 -19
- package/host/providers/providers.test-d.ts +5 -3
- package/host/providers/resolve-kv.ts +23 -41
- package/host/providers/resolve-vector.ts +3 -12
- package/host/providers/resolve.test.ts +15 -28
- package/host/providers/resolve.ts +24 -24
- package/host/providers/stt/assemblyai.test.ts +2 -14
- package/host/providers/stt/assemblyai.ts +12 -35
- package/host/providers/stt/deepgram.test.ts +23 -83
- package/host/providers/stt/deepgram.ts +15 -40
- package/host/providers/stt/elevenlabs.test.ts +26 -38
- package/host/providers/stt/elevenlabs.ts +10 -9
- package/host/providers/stt/soniox.test.ts +35 -85
- package/host/providers/stt/soniox.ts +8 -53
- package/host/providers/tts/cartesia.test.ts +19 -58
- package/host/providers/tts/cartesia.ts +36 -66
- package/host/providers/tts/rime.test.ts +12 -38
- package/host/providers/tts/rime.ts +23 -86
- package/host/runtime-config.test.ts +9 -9
- package/host/runtime-config.ts +16 -22
- package/host/runtime.test.ts +111 -73
- package/host/runtime.ts +138 -86
- package/host/s2s.test.ts +92 -191
- package/host/s2s.ts +56 -53
- package/host/server-shutdown.test.ts +9 -30
- package/host/server.test.ts +2 -13
- package/host/server.ts +85 -100
- package/host/session-core.test.ts +15 -30
- package/host/session-core.ts +10 -13
- package/host/session-prompt.test.ts +1 -5
- package/host/to-vercel-tools.test.ts +53 -72
- package/host/to-vercel-tools.ts +9 -39
- package/host/tool-executor.test.ts +25 -51
- package/host/tool-executor.ts +18 -12
- package/host/transports/openai-realtime-transport.test.ts +371 -0
- package/host/transports/openai-realtime-transport.ts +319 -0
- package/host/transports/pipeline-transport.test.ts +125 -298
- package/host/transports/pipeline-transport.ts +20 -68
- package/host/transports/s2s-transport-fixtures.test.ts +31 -92
- package/host/transports/s2s-transport.test.ts +65 -134
- package/host/transports/s2s-transport.ts +15 -43
- package/host/transports/types.test.ts +4 -8
- package/host/unstorage-kv.test.ts +3 -2
- package/host/unstorage-kv.ts +5 -35
- package/host/ws-handler.test.ts +72 -176
- package/host/ws-handler.ts +6 -12
- package/package.json +6 -1
- package/sdk/__snapshots__/exports.test.ts.snap +7 -0
- package/sdk/__snapshots__/schema-shapes.test.ts.snap +1 -0
- package/sdk/_internal-types.test.ts +6 -9
- package/sdk/_internal-types.ts +16 -57
- package/sdk/_test-matchers.ts +25 -15
- package/sdk/allowed-hosts.test.ts +50 -114
- package/sdk/allowed-hosts.ts +8 -14
- package/sdk/constants.ts +5 -52
- package/sdk/define.test.ts +7 -6
- package/sdk/define.ts +7 -3
- package/sdk/exports.test.ts +6 -1
- package/sdk/kv.ts +13 -37
- package/sdk/manifest.test-d.ts +5 -0
- package/sdk/manifest.test.ts +61 -9
- package/sdk/manifest.ts +11 -11
- package/sdk/protocol-compat.test.ts +66 -98
- package/sdk/protocol-snapshot.test.ts +2 -16
- package/sdk/protocol.test.ts +13 -22
- package/sdk/providers/s2s/openai-realtime.ts +36 -0
- package/sdk/providers/s2s-barrel.ts +12 -0
- package/sdk/providers/tts/rime.ts +1 -1
- package/sdk/providers.ts +24 -5
- package/sdk/schema-alignment.test.ts +25 -73
- package/sdk/schema-shapes.test.ts +1 -29
- package/sdk/system-prompt.test.ts +0 -1
- package/sdk/system-prompt.ts +17 -19
- package/sdk/types-inference.test.ts +10 -36
- package/sdk/types.ts +7 -0
- package/sdk/ws-upgrade.test.ts +24 -23
- package/sdk/ws-upgrade.ts +2 -3
- package/tsdown.config.ts +8 -11
- package/dist/constants-C2nirZUI.js +0 -54
package/host/runtime-config.ts
CHANGED
|
@@ -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):
|
|
38
|
-
return (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
|
-
|
|
51
|
-
|
|
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"),
|
package/host/runtime.test.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
|
368
|
-
|
|
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
|
|
396
|
-
|
|
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
|
-
|
|
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,
|
|
409
|
+
logger: makeLogger(),
|
|
410
|
+
shutdownTimeoutMs: 50,
|
|
428
411
|
});
|
|
429
|
-
|
|
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
|
-
|
|
452
|
-
|
|
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
|
|
524
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
541
|
-
expect(mockWs.addEventListener).toHaveBeenCalled();
|
|
511
|
+
expect(ws.addEventListener).toHaveBeenCalled();
|
|
542
512
|
});
|
|
543
513
|
|
|
544
514
|
test("startSession works with no options", () => {
|
|
545
|
-
const
|
|
546
|
-
const
|
|
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(
|
|
554
|
-
expect(
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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,
|