@alexkroman1/aai 1.7.1 → 1.8.1

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 +23 -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 +670 -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 +45 -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 +139 -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 +439 -0
  90. package/host/transports/openai-realtime-transport.ts +371 -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
@@ -15,6 +15,9 @@ import type { TransportCallbacks } from "./types.ts";
15
15
 
16
16
  // ─── Helpers ─────────────────────────────────────────────────────────────────
17
17
 
18
+ type SttFake = ReturnType<typeof createFakeSttProvider>;
19
+ type TtsFake = ReturnType<typeof createFakeTtsProvider>;
20
+
18
21
  function makeCallbacks(): TransportCallbacks {
19
22
  return {
20
23
  onReplyStarted: vi.fn(),
@@ -38,15 +41,11 @@ function makeOpts(
38
41
  stt = createFakeSttProvider(),
39
42
  tts = createFakeTtsProvider(),
40
43
  callbacks = makeCallbacks(),
41
- }: {
42
- stt?: ReturnType<typeof createFakeSttProvider>;
43
- tts?: ReturnType<typeof createFakeTtsProvider>;
44
- callbacks?: TransportCallbacks;
45
- } = {},
44
+ }: { stt?: SttFake; tts?: TtsFake; callbacks?: TransportCallbacks } = {},
46
45
  ): {
47
46
  opts: PipelineTransportOptions;
48
- stt: ReturnType<typeof createFakeSttProvider>;
49
- tts: ReturnType<typeof createFakeTtsProvider>;
47
+ stt: SttFake;
48
+ tts: TtsFake;
50
49
  callbacks: TransportCallbacks;
51
50
  } {
52
51
  const opts: PipelineTransportOptions = {
@@ -56,10 +55,7 @@ function makeOpts(
56
55
  llm: createFakeLanguageModel({ script: [] }),
57
56
  tts,
58
57
  callbacks,
59
- sessionConfig: {
60
- systemPrompt: "You are a test assistant.",
61
- greeting: "",
62
- },
58
+ sessionConfig: { systemPrompt: "s", greeting: "" },
63
59
  providerKeys: { stt: "stt-key", tts: "tts-key" },
64
60
  logger: silentLogger,
65
61
  ...overrides,
@@ -67,12 +63,24 @@ function makeOpts(
67
63
  return { opts, stt, tts, callbacks };
68
64
  }
69
65
 
66
+ function firstCallArg<T>(fn: unknown): T {
67
+ // biome-ignore lint/style/noNonNullAssertion: caller asserts the spy was invoked
68
+ return (fn as ReturnType<typeof vi.fn>).mock.calls[0]![0] as T;
69
+ }
70
+
71
+ const noopToolSchema = {
72
+ type: "function" as const,
73
+ name: "lookup",
74
+ description: "Look something up.",
75
+ parameters: { type: "object" as const, properties: {}, required: [] },
76
+ };
77
+
70
78
  // ─── Tests ───────────────────────────────────────────────────────────────────
71
79
 
72
80
  describe("PipelineTransport", () => {
73
81
  describe("start()", () => {
74
82
  test("opens both STT and TTS sessions", async () => {
75
- const { opts, stt, tts } = makeOpts({ sessionConfig: { systemPrompt: "s", greeting: "" } });
83
+ const { opts, stt, tts } = makeOpts();
76
84
  const t = createPipelineTransport(opts);
77
85
  await t.start();
78
86
  expect(stt.last()).toBeDefined();
@@ -81,17 +89,11 @@ describe("PipelineTransport", () => {
81
89
  });
82
90
 
83
91
  test("passes correct keys and sample rate to STT opener", async () => {
84
- const stt = createFakeSttProvider();
85
- const { opts } = makeOpts(
86
- {
87
- stt,
88
- providerKeys: { stt: "MY_STT_KEY", tts: "t" },
89
- sttSampleRate: 8000,
90
- sttPrompt: "be brief",
91
- sessionConfig: { systemPrompt: "s", greeting: "" },
92
- },
93
- { stt },
94
- );
92
+ const { opts, stt } = makeOpts({
93
+ providerKeys: { stt: "MY_STT_KEY", tts: "t" },
94
+ sttSampleRate: 8000,
95
+ sttPrompt: "be brief",
96
+ });
95
97
  const t = createPipelineTransport(opts);
96
98
  await t.start();
97
99
  expect(stt.last()?.opts.sampleRate).toBe(8000);
@@ -101,7 +103,7 @@ describe("PipelineTransport", () => {
101
103
  });
102
104
 
103
105
  test("fires onSessionReady with the sid", async () => {
104
- const { opts, callbacks } = makeOpts({ sessionConfig: { systemPrompt: "s", greeting: "" } });
106
+ const { opts, callbacks } = makeOpts();
105
107
  const t = createPipelineTransport(opts);
106
108
  await t.start();
107
109
  expect(callbacks.onSessionReady).toHaveBeenCalledWith("test-sid");
@@ -111,38 +113,27 @@ describe("PipelineTransport", () => {
111
113
 
112
114
  describe("greeting", () => {
113
115
  test("sends greeting via ttsSession.sendText and fires onReplyStarted + onAgentTranscript + onReplyDone", async () => {
114
- const stt = createFakeSttProvider();
115
- const tts = createFakeTtsProvider();
116
- const callbacks = makeCallbacks();
117
- const { opts } = makeOpts(
118
- { sessionConfig: { systemPrompt: "s", greeting: "Hi there!" } },
119
- { stt, tts, callbacks },
120
- );
116
+ const { opts, tts, callbacks } = makeOpts({
117
+ sessionConfig: { systemPrompt: "s", greeting: "Hi there!" },
118
+ });
121
119
  const t = createPipelineTransport(opts);
122
120
  await t.start();
123
- // Greeting runs as a chained turn — waitFor covers the async flush.
124
121
  await vi.waitFor(() => {
125
122
  expect(callbacks.onReplyDone).toHaveBeenCalledOnce();
126
123
  });
127
124
  expect(tts.last()?.textChunks).toContain("Hi there!");
128
125
  expect(callbacks.onReplyStarted).toHaveBeenCalledWith(expect.stringContaining("greeting"));
129
126
  expect(callbacks.onAgentTranscript).toHaveBeenCalledWith("Hi there!", false);
130
- // onAudioDone is NOT fired by the transport — session-core's flushReply
131
- // (triggered by onReplyDone) owns the audioDone + replyDone pairing.
127
+ // onAudioDone is owned by session-core's flushReply, not the transport.
132
128
  expect(callbacks.onAudioDone).not.toHaveBeenCalled();
133
129
  await t.stop();
134
130
  });
135
131
 
136
132
  test("skipGreeting suppresses the greeting turn", async () => {
137
- const tts = createFakeTtsProvider();
138
- const callbacks = makeCallbacks();
139
- const { opts } = makeOpts(
140
- {
141
- skipGreeting: true,
142
- sessionConfig: { systemPrompt: "s", greeting: "Hello!" },
143
- },
144
- { tts, callbacks },
145
- );
133
+ const { opts, tts, callbacks } = makeOpts({
134
+ skipGreeting: true,
135
+ sessionConfig: { systemPrompt: "s", greeting: "Hello!" },
136
+ });
146
137
  const t = createPipelineTransport(opts);
147
138
  await t.start();
148
139
  await new Promise((r) => setTimeout(r, 20));
@@ -154,12 +145,7 @@ describe("PipelineTransport", () => {
154
145
 
155
146
  describe("STT → LLM turn", () => {
156
147
  test("final STT event fires onUserTranscript and onReplyStarted", async () => {
157
- const stt = createFakeSttProvider();
158
- const callbacks = makeCallbacks();
159
- const { opts } = makeOpts(
160
- { sessionConfig: { systemPrompt: "s", greeting: "" } },
161
- { stt, callbacks },
162
- );
148
+ const { opts, stt, callbacks } = makeOpts();
163
149
  const t = createPipelineTransport(opts);
164
150
  await t.start();
165
151
  stt.last()?.fireFinal("Hello agent");
@@ -171,12 +157,7 @@ describe("PipelineTransport", () => {
171
157
  });
172
158
 
173
159
  test("empty / whitespace-only final is ignored", async () => {
174
- const stt = createFakeSttProvider();
175
- const callbacks = makeCallbacks();
176
- const { opts } = makeOpts(
177
- { sessionConfig: { systemPrompt: "s", greeting: "" } },
178
- { stt, callbacks },
179
- );
160
+ const { opts, stt, callbacks } = makeOpts();
180
161
  const t = createPipelineTransport(opts);
181
162
  await t.start();
182
163
  stt.last()?.fireFinal(" ");
@@ -191,15 +172,7 @@ describe("PipelineTransport", () => {
191
172
  { type: "text", text: "I am " },
192
173
  { type: "text", text: "the answer" },
193
174
  ];
194
- const stt = createFakeSttProvider();
195
- const tts = createFakeTtsProvider();
196
- const { opts } = makeOpts(
197
- {
198
- llm: createFakeLanguageModel({ script }),
199
- sessionConfig: { systemPrompt: "s", greeting: "" },
200
- },
201
- { stt, tts },
202
- );
175
+ const { opts, stt, tts } = makeOpts({ llm: createFakeLanguageModel({ script }) });
203
176
  const t = createPipelineTransport(opts);
204
177
  await t.start();
205
178
  stt.last()?.fireFinal("what is the answer?");
@@ -211,37 +184,20 @@ describe("PipelineTransport", () => {
211
184
  });
212
185
 
213
186
  test("inserts a separator between text segments split by a mid-turn tool call", async () => {
214
- // Multi-step turn: step 1 ends with a text segment + tool-call, step 2
215
- // begins with a fresh text segment. Without the fix, the deltas fuse
216
- // into "...up.Got it" — both in the transcript and in TTS input.
217
- const stt = createFakeSttProvider();
218
- const tts = createFakeTtsProvider();
219
- const callbacks = makeCallbacks();
220
- const executeTool = vi.fn(async () => "result");
221
- const { opts } = makeOpts(
222
- {
223
- llm: createFakeLanguageModel({
224
- steps: [
225
- [
226
- { type: "text", text: "Let me look that up." },
227
- { type: "tool-call", toolCallId: "tc-1", toolName: "lookup", input: "{}" },
228
- ],
229
- [{ type: "text", text: "Got it. Here's the answer." }],
187
+ // Multi-step turn: without the separator fix, deltas fuse into "...up.Got it".
188
+ const { opts, stt, tts, callbacks } = makeOpts({
189
+ llm: createFakeLanguageModel({
190
+ steps: [
191
+ [
192
+ { type: "text", text: "Let me look that up." },
193
+ { type: "tool-call", toolCallId: "tc-1", toolName: "lookup", input: "{}" },
230
194
  ],
231
- }),
232
- executeTool,
233
- toolSchemas: [
234
- {
235
- type: "function" as const,
236
- name: "lookup",
237
- description: "Look something up.",
238
- parameters: { type: "object" as const, properties: {}, required: [] },
239
- },
195
+ [{ type: "text", text: "Got it. Here's the answer." }],
240
196
  ],
241
- sessionConfig: { systemPrompt: "s", greeting: "" },
242
- },
243
- { stt, tts, callbacks },
244
- );
197
+ }),
198
+ executeTool: vi.fn(async () => "result"),
199
+ toolSchemas: [noopToolSchema],
200
+ });
245
201
  const t = createPipelineTransport(opts);
246
202
  await t.start();
247
203
  stt.last()?.fireFinal("look it up");
@@ -259,35 +215,19 @@ describe("PipelineTransport", () => {
259
215
  });
260
216
 
261
217
  test("does not double-space when a segment boundary already carries whitespace", async () => {
262
- // Trailing space on segment 1 we must not insert an extra space.
263
- const stt = createFakeSttProvider();
264
- const tts = createFakeTtsProvider();
265
- const callbacks = makeCallbacks();
266
- const executeTool = vi.fn(async () => "result");
267
- const { opts } = makeOpts(
268
- {
269
- llm: createFakeLanguageModel({
270
- steps: [
271
- [
272
- { type: "text", text: "First sentence. " },
273
- { type: "tool-call", toolCallId: "tc-1", toolName: "lookup", input: "{}" },
274
- ],
275
- [{ type: "text", text: "Second sentence." }],
218
+ const { opts, stt, callbacks } = makeOpts({
219
+ llm: createFakeLanguageModel({
220
+ steps: [
221
+ [
222
+ { type: "text", text: "First sentence. " },
223
+ { type: "tool-call", toolCallId: "tc-1", toolName: "lookup", input: "{}" },
276
224
  ],
277
- }),
278
- executeTool,
279
- toolSchemas: [
280
- {
281
- type: "function" as const,
282
- name: "lookup",
283
- description: "Look something up.",
284
- parameters: { type: "object" as const, properties: {}, required: [] },
285
- },
225
+ [{ type: "text", text: "Second sentence." }],
286
226
  ],
287
- sessionConfig: { systemPrompt: "s", greeting: "" },
288
- },
289
- { stt, tts, callbacks },
290
- );
227
+ }),
228
+ executeTool: vi.fn(async () => "result"),
229
+ toolSchemas: [noopToolSchema],
230
+ });
291
231
  const t = createPipelineTransport(opts);
292
232
  await t.start();
293
233
  stt.last()?.fireFinal("look it up");
@@ -302,38 +242,22 @@ describe("PipelineTransport", () => {
302
242
  });
303
243
 
304
244
  test("TTS audio event is forwarded to callbacks.onAudioChunk as Uint8Array", async () => {
305
- const stt = createFakeSttProvider();
306
- const tts = createFakeTtsProvider();
307
- const callbacks = makeCallbacks();
308
- const { opts } = makeOpts(
309
- { sessionConfig: { systemPrompt: "s", greeting: "" } },
310
- { stt, tts, callbacks },
311
- );
245
+ const { opts, tts, callbacks } = makeOpts();
312
246
  const t = createPipelineTransport(opts);
313
247
  await t.start();
314
248
  const pcm = new Int16Array([100, 200, 300]);
315
249
  tts.last()?.fireAudio(pcm);
316
250
  expect(callbacks.onAudioChunk).toHaveBeenCalledOnce();
317
- // biome-ignore lint/style/noNonNullAssertion: test assertion — calledOnce proven above
318
- const arg = (callbacks.onAudioChunk as ReturnType<typeof vi.fn>).mock
319
- .calls[0]![0] as Uint8Array;
251
+ const arg = firstCallArg<Uint8Array>(callbacks.onAudioChunk);
320
252
  expect(arg).toBeInstanceOf(Uint8Array);
321
253
  expect(arg.byteLength).toBe(pcm.byteLength);
322
254
  await t.stop();
323
255
  });
324
256
 
325
257
  test("full turn: onUserTranscript → onReplyStarted → onAgentTranscript → onReplyDone (no transport-level onAudioDone)", async () => {
326
- const script: ScriptedPart[] = [{ type: "text", text: "Sure!" }];
327
- const stt = createFakeSttProvider();
328
- const tts = createFakeTtsProvider();
329
- const callbacks = makeCallbacks();
330
- const { opts } = makeOpts(
331
- {
332
- llm: createFakeLanguageModel({ script }),
333
- sessionConfig: { systemPrompt: "s", greeting: "" },
334
- },
335
- { stt, tts, callbacks },
336
- );
258
+ const { opts, stt, callbacks } = makeOpts({
259
+ llm: createFakeLanguageModel({ script: [{ type: "text", text: "Sure!" }] }),
260
+ });
337
261
  const t = createPipelineTransport(opts);
338
262
  await t.start();
339
263
  stt.last()?.fireFinal("test question");
@@ -343,23 +267,15 @@ describe("PipelineTransport", () => {
343
267
  expect(callbacks.onUserTranscript).toHaveBeenCalledWith("test question");
344
268
  expect(callbacks.onReplyStarted).toHaveBeenCalled();
345
269
  expect(callbacks.onAgentTranscript).toHaveBeenCalledWith("Sure!", false);
346
- // onAudioDone is NOT fired by the transport — session-core's flushReply
347
- // (triggered by onReplyDone) owns the audioDone + replyDone pairing.
270
+ // onAudioDone is owned by session-core's flushReply, not the transport.
348
271
  expect(callbacks.onAudioDone).not.toHaveBeenCalled();
349
272
  await t.stop();
350
273
  });
351
274
 
352
275
  test("TTS flush is called after LLM stream finishes", async () => {
353
- const script: ScriptedPart[] = [{ type: "text", text: "hi" }];
354
- const stt = createFakeSttProvider();
355
- const tts = createFakeTtsProvider();
356
- const { opts } = makeOpts(
357
- {
358
- llm: createFakeLanguageModel({ script }),
359
- sessionConfig: { systemPrompt: "s", greeting: "" },
360
- },
361
- { stt, tts },
362
- );
276
+ const { opts, stt, tts } = makeOpts({
277
+ llm: createFakeLanguageModel({ script: [{ type: "text", text: "hi" }] }),
278
+ });
363
279
  const t = createPipelineTransport(opts);
364
280
  await t.start();
365
281
  stt.last()?.fireFinal("go");
@@ -382,17 +298,12 @@ describe("PipelineTransport", () => {
382
298
  const dummyExecuteTool = async () => "{}";
383
299
 
384
300
  test("forwards toolChoice to doStream (default 'auto' when omitted)", async () => {
385
- const stt = createFakeSttProvider();
386
301
  const llm = createFakeLanguageModel({ script: [{ type: "text", text: "ok" }] });
387
- const { opts } = makeOpts(
388
- {
389
- llm,
390
- toolSchemas: dummyToolSchemas,
391
- executeTool: dummyExecuteTool,
392
- sessionConfig: { systemPrompt: "s", greeting: "" },
393
- },
394
- { stt },
395
- );
302
+ const { opts, stt } = makeOpts({
303
+ llm,
304
+ toolSchemas: dummyToolSchemas,
305
+ executeTool: dummyExecuteTool,
306
+ });
396
307
  const t = createPipelineTransport(opts);
397
308
  await t.start();
398
309
  stt.last()?.fireFinal("hi");
@@ -404,18 +315,13 @@ describe("PipelineTransport", () => {
404
315
  });
405
316
 
406
317
  test("forwards explicit toolChoice='required' to doStream", async () => {
407
- const stt = createFakeSttProvider();
408
318
  const llm = createFakeLanguageModel({ script: [{ type: "text", text: "ok" }] });
409
- const { opts } = makeOpts(
410
- {
411
- llm,
412
- toolChoice: "required",
413
- toolSchemas: dummyToolSchemas,
414
- executeTool: dummyExecuteTool,
415
- sessionConfig: { systemPrompt: "s", greeting: "" },
416
- },
417
- { stt },
418
- );
319
+ const { opts, stt } = makeOpts({
320
+ llm,
321
+ toolChoice: "required",
322
+ toolSchemas: dummyToolSchemas,
323
+ executeTool: dummyExecuteTool,
324
+ });
419
325
  const t = createPipelineTransport(opts);
420
326
  await t.start();
421
327
  stt.last()?.fireFinal("hi");
@@ -427,28 +333,17 @@ describe("PipelineTransport", () => {
427
333
  });
428
334
 
429
335
  test("maxSteps caps the doStream loop", async () => {
430
- // Script two steps that each emit a text part. With maxSteps=1 only the
431
- // first step should run; without plumbing it would default to 5 and both
432
- // would fire.
433
- const stt = createFakeSttProvider();
336
+ // Two scripted steps; maxSteps=1 must stop after the first (default would be 5).
434
337
  const llm = createFakeLanguageModel({
435
338
  steps: [[{ type: "text", text: "step1" }], [{ type: "text", text: "step2" }]],
436
339
  });
437
- const { opts } = makeOpts(
438
- {
439
- llm,
440
- maxSteps: 1,
441
- sessionConfig: { systemPrompt: "s", greeting: "" },
442
- },
443
- { stt },
444
- );
340
+ const { opts, stt } = makeOpts({ llm, maxSteps: 1 });
445
341
  const t = createPipelineTransport(opts);
446
342
  await t.start();
447
343
  stt.last()?.fireFinal("hi");
448
344
  await vi.waitFor(() => {
449
345
  expect(llm.calls.length).toBeGreaterThanOrEqual(1);
450
346
  });
451
- // Let any extra step have a chance to run.
452
347
  await new Promise((r) => setTimeout(r, 20));
453
348
  expect(llm.calls.length).toBe(1);
454
349
  await t.stop();
@@ -462,26 +357,17 @@ describe("PipelineTransport", () => {
462
357
  { type: "text", text: "how can " },
463
358
  { type: "text", text: "I help?" },
464
359
  ];
465
- const stt = createFakeSttProvider();
466
- const tts = createFakeTtsProvider();
467
- const callbacks = makeCallbacks();
468
- const { opts } = makeOpts(
469
- {
470
- llm: createFakeLanguageModel({ script, delayMs: 20 }),
471
- sessionConfig: { systemPrompt: "s", greeting: "" },
472
- },
473
- { stt, tts, callbacks },
474
- );
360
+ const { opts, stt, tts, callbacks } = makeOpts({
361
+ llm: createFakeLanguageModel({ script, delayMs: 20 }),
362
+ });
475
363
  const t = createPipelineTransport(opts);
476
364
  await t.start();
477
365
 
478
- // Start a turn, wait until TTS is receiving text (deep in AGENT_REPLYING).
479
366
  stt.last()?.fireFinal("hi there");
480
367
  await vi.waitFor(() => {
481
368
  expect(tts.last()?.textChunks.length).toBeGreaterThan(0);
482
369
  });
483
370
 
484
- // Fire barge-in partial.
485
371
  stt.last()?.firePartial("wait");
486
372
  expect(callbacks.onCancelled).toHaveBeenCalled();
487
373
  expect(tts.last()?.cancel).toHaveBeenCalled();
@@ -493,16 +379,9 @@ describe("PipelineTransport", () => {
493
379
  { type: "text", text: "some " },
494
380
  { type: "text", text: "reply" },
495
381
  ];
496
- const stt = createFakeSttProvider();
497
- const tts = createFakeTtsProvider();
498
- const callbacks = makeCallbacks();
499
- const { opts } = makeOpts(
500
- {
501
- llm: createFakeLanguageModel({ script, delayMs: 20 }),
502
- sessionConfig: { systemPrompt: "s", greeting: "" },
503
- },
504
- { stt, tts, callbacks },
505
- );
382
+ const { opts, stt, tts, callbacks } = makeOpts({
383
+ llm: createFakeLanguageModel({ script, delayMs: 20 }),
384
+ });
506
385
  const t = createPipelineTransport(opts);
507
386
  await t.start();
508
387
 
@@ -513,9 +392,8 @@ describe("PipelineTransport", () => {
513
392
 
514
393
  t.cancelReply();
515
394
  expect(tts.last()?.cancel).toHaveBeenCalled();
516
- // cancelReply() does NOT fire callbacks.onCancelled — session-core calls
517
- // client.cancelled() itself when the cancel originates from the client.
518
- // onCancelled is only fired from within the transport for barge-in (STT partial).
395
+ // cancelReply() doesn't fire onCancelled — session-core calls client.cancelled()
396
+ // itself for client-originated cancels. onCancelled fires only for STT-partial barge-in.
519
397
  expect(callbacks.onCancelled).not.toHaveBeenCalled();
520
398
  await t.stop();
521
399
  });
@@ -523,12 +401,7 @@ describe("PipelineTransport", () => {
523
401
 
524
402
  describe("stop()", () => {
525
403
  test("closes both STT and TTS sessions", async () => {
526
- const stt = createFakeSttProvider();
527
- const tts = createFakeTtsProvider();
528
- const { opts } = makeOpts(
529
- { sessionConfig: { systemPrompt: "s", greeting: "" } },
530
- { stt, tts },
531
- );
404
+ const { opts, stt, tts } = makeOpts();
532
405
  const t = createPipelineTransport(opts);
533
406
  await t.start();
534
407
  await t.stop();
@@ -537,50 +410,37 @@ describe("PipelineTransport", () => {
537
410
  });
538
411
 
539
412
  test("stop() is idempotent", async () => {
540
- const stt = createFakeSttProvider();
541
- const tts = createFakeTtsProvider();
542
- const { opts } = makeOpts(
543
- { sessionConfig: { systemPrompt: "s", greeting: "" } },
544
- { stt, tts },
545
- );
413
+ const { opts, stt } = makeOpts();
546
414
  const t = createPipelineTransport(opts);
547
415
  await t.start();
548
416
  await t.stop();
549
- await t.stop(); // should not throw or double-close
417
+ await t.stop();
550
418
  expect(stt.last()?.closed.value).toBe(true);
551
419
  });
552
420
  });
553
421
 
554
422
  describe("sendUserAudio()", () => {
555
423
  test("converts aligned Uint8Array to Int16Array and calls sttSession.sendAudio", async () => {
556
- const stt = createFakeSttProvider();
557
- const { opts } = makeOpts({ sessionConfig: { systemPrompt: "s", greeting: "" } }, { stt });
424
+ const { opts, stt } = makeOpts();
558
425
  const t = createPipelineTransport(opts);
559
426
  await t.start();
560
- const buf = new ArrayBuffer(4);
561
- const bytes = new Uint8Array(buf);
562
- bytes.set([0x01, 0x02, 0x03, 0x04]);
427
+ const bytes = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
563
428
  t.sendUserAudio(bytes);
564
429
  const sttSession = stt.last();
565
430
  expect(sttSession?.sendAudio).toHaveBeenCalledOnce();
566
- // biome-ignore lint/style/noNonNullAssertion: test assertion — calledOnce proven above
567
- const pcm = (sttSession?.sendAudio as ReturnType<typeof vi.fn>).mock
568
- .calls[0]![0] as Int16Array;
431
+ const pcm = firstCallArg<Int16Array>(sttSession?.sendAudio);
569
432
  expect(pcm).toBeInstanceOf(Int16Array);
570
433
  expect(pcm.length).toBe(2);
571
434
  await t.stop();
572
435
  });
573
436
 
574
437
  test("handles odd-length Uint8Array by copying and truncating", async () => {
575
- const stt = createFakeSttProvider();
576
- const { opts } = makeOpts({ sessionConfig: { systemPrompt: "s", greeting: "" } }, { stt });
438
+ const { opts, stt } = makeOpts();
577
439
  const t = createPipelineTransport(opts);
578
440
  await t.start();
579
- const bytes = new Uint8Array([1, 2, 3]); // 3 bytes → 1 sample
580
- t.sendUserAudio(bytes);
581
- // biome-ignore lint/style/noNonNullAssertion: test assertion — audio was sent synchronously above
582
- const pcm = (stt.last()?.sendAudio as ReturnType<typeof vi.fn>).mock
583
- .calls[0]![0] as Int16Array;
441
+ // 3 bytes 1 sample (truncates the trailing odd byte).
442
+ t.sendUserAudio(new Uint8Array([1, 2, 3]));
443
+ const pcm = firstCallArg<Int16Array>(stt.last()?.sendAudio);
584
444
  expect(pcm.length).toBe(1);
585
445
  await t.stop();
586
446
  });
@@ -588,7 +448,7 @@ describe("PipelineTransport", () => {
588
448
 
589
449
  describe("sendToolResult()", () => {
590
450
  test("is a no-op (Option A: inline tool execution)", async () => {
591
- const { opts } = makeOpts({ sessionConfig: { systemPrompt: "s", greeting: "" } });
451
+ const { opts } = makeOpts();
592
452
  const t = createPipelineTransport(opts);
593
453
  await t.start();
594
454
  expect(() => t.sendToolResult("call-1", "result")).not.toThrow();
@@ -598,7 +458,6 @@ describe("PipelineTransport", () => {
598
458
 
599
459
  describe("tool observability", () => {
600
460
  test("callbacks.onToolCall fires for each tool-call stream part", async () => {
601
- const executeTool = vi.fn(async () => "sunny");
602
461
  const script: ScriptedPart[] = [
603
462
  {
604
463
  type: "tool-call",
@@ -609,36 +468,28 @@ describe("PipelineTransport", () => {
609
468
  { type: "tool-result", toolCallId: "tc-1", toolName: "get_weather", result: "sunny" },
610
469
  { type: "text", text: "It's sunny." },
611
470
  ];
612
- const stt = createFakeSttProvider();
613
- const tts = createFakeTtsProvider();
614
- const callbacks = makeCallbacks();
615
- const { opts } = makeOpts(
616
- {
617
- llm: createFakeLanguageModel({ script }),
618
- executeTool,
619
- toolSchemas: [
620
- {
621
- type: "function" as const,
622
- name: "get_weather",
623
- description: "Look up the weather.",
624
- parameters: {
625
- type: "object" as const,
626
- properties: { city: { type: "string" } },
627
- required: ["city"],
628
- },
471
+ const { opts, stt, callbacks } = makeOpts({
472
+ llm: createFakeLanguageModel({ script }),
473
+ executeTool: vi.fn(async () => "sunny"),
474
+ toolSchemas: [
475
+ {
476
+ type: "function" as const,
477
+ name: "get_weather",
478
+ description: "Look up the weather.",
479
+ parameters: {
480
+ type: "object" as const,
481
+ properties: { city: { type: "string" } },
482
+ required: ["city"],
629
483
  },
630
- ],
631
- sessionConfig: { systemPrompt: "s", greeting: "" },
632
- },
633
- { stt, tts, callbacks },
634
- );
484
+ },
485
+ ],
486
+ });
635
487
  const t = createPipelineTransport(opts);
636
488
  await t.start();
637
489
  stt.last()?.fireFinal("how's the weather?");
638
490
  await vi.waitFor(() => {
639
491
  expect(callbacks.onReplyDone).toHaveBeenCalled();
640
492
  });
641
- // onToolCall fires for observability (Option A).
642
493
  expect(callbacks.onToolCall).toHaveBeenCalledWith("tc-1", "get_weather", expect.any(Object));
643
494
  await t.stop();
644
495
  });
@@ -646,12 +497,7 @@ describe("PipelineTransport", () => {
646
497
 
647
498
  describe("provider errors", () => {
648
499
  test("STT error fires onError('stt', ...) and terminates transport", async () => {
649
- const stt = createFakeSttProvider();
650
- const callbacks = makeCallbacks();
651
- const { opts } = makeOpts(
652
- { sessionConfig: { systemPrompt: "s", greeting: "" } },
653
- { stt, callbacks },
654
- );
500
+ const { opts, stt, callbacks } = makeOpts();
655
501
  const t = createPipelineTransport(opts);
656
502
  await t.start();
657
503
  stt.last()?.fireError("stt_stream_error", "stt failed");
@@ -660,12 +506,7 @@ describe("PipelineTransport", () => {
660
506
  });
661
507
 
662
508
  test("TTS error fires onError('tts', ...) and terminates transport", async () => {
663
- const tts = createFakeTtsProvider();
664
- const callbacks = makeCallbacks();
665
- const { opts } = makeOpts(
666
- { sessionConfig: { systemPrompt: "s", greeting: "" } },
667
- { tts, callbacks },
668
- );
509
+ const { opts, tts, callbacks } = makeOpts();
669
510
  const t = createPipelineTransport(opts);
670
511
  await t.start();
671
512
  tts.last()?.fireError("tts_stream_error", "tts failed");
@@ -674,14 +515,9 @@ describe("PipelineTransport", () => {
674
515
  });
675
516
 
676
517
  test("STT open failure fires onError('stt', ...) via reportOpenRejection", async () => {
677
- const callbacks = makeCallbacks();
678
- const { opts } = makeOpts(
679
- {
680
- stt: createFailingSttProvider("stt_connect_failed", "connect failed"),
681
- sessionConfig: { systemPrompt: "s", greeting: "" },
682
- },
683
- { callbacks },
684
- );
518
+ const { opts, callbacks } = makeOpts({
519
+ stt: createFailingSttProvider("stt_connect_failed", "connect failed"),
520
+ });
685
521
  const t = createPipelineTransport(opts);
686
522
  await t.start();
687
523
  expect(callbacks.onError).toHaveBeenCalledWith("stt", "connect failed");
@@ -689,14 +525,9 @@ describe("PipelineTransport", () => {
689
525
  });
690
526
 
691
527
  test("TTS open failure fires onError('tts', ...) via reportOpenRejection", async () => {
692
- const callbacks = makeCallbacks();
693
- const { opts } = makeOpts(
694
- {
695
- tts: createFailingTtsProvider("tts_connect_failed", "tts connect failed"),
696
- sessionConfig: { systemPrompt: "s", greeting: "" },
697
- },
698
- { callbacks },
699
- );
528
+ const { opts, callbacks } = makeOpts({
529
+ tts: createFailingTtsProvider("tts_connect_failed", "tts connect failed"),
530
+ });
700
531
  const t = createPipelineTransport(opts);
701
532
  await t.start();
702
533
  expect(callbacks.onError).toHaveBeenCalledWith("tts", "tts connect failed");
@@ -709,13 +540,12 @@ describe("PipelineTransport", () => {
709
540
  {
710
541
  stt: createFailingSttProvider("stt_connect_failed", "bad key"),
711
542
  tts,
712
- sessionConfig: { systemPrompt: "s", greeting: "" },
713
543
  },
714
544
  { tts },
715
545
  );
716
546
  const t = createPipelineTransport(opts);
717
547
  await t.start();
718
- // TTS was opened (Promise.allSettled runs both concurrently) but then closed.
548
+ // Promise.allSettled opens both concurrently; STT failure then closes TTS.
719
549
  expect(tts.last()?.closed.value).toBe(true);
720
550
  await t.stop();
721
551
  });
@@ -723,9 +553,6 @@ describe("PipelineTransport", () => {
723
553
 
724
554
  describe("history seeding", () => {
725
555
  test("sessionConfig.history is used as initial conversation messages", async () => {
726
- // History seeding is internal — we verify it indirectly by checking
727
- // that the LLM receives the correct message array.
728
- // For this test we just ensure start() doesn't throw when history is set.
729
556
  const { opts } = makeOpts({
730
557
  sessionConfig: {
731
558
  systemPrompt: "s",