@alexkroman1/aai 1.4.5 → 1.5.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.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +19 -0
- package/dist/{_internal-types-3p3OJZPb.js → _internal-types-DFL07G3f.js} +2 -0
- package/dist/assemblyai-C969QGi4.js +35 -0
- package/dist/cartesia-BfQPOQ7Y.js +37 -0
- package/dist/host/_pipeline-test-fakes.d.ts +3 -1
- package/dist/host/providers/stt/deepgram.d.ts +28 -0
- package/dist/host/providers/tts/cartesia.d.ts +1 -1
- package/dist/host/providers/tts/rime.d.ts +44 -0
- package/dist/host/runtime-barrel.d.ts +4 -2
- package/dist/host/runtime-barrel.js +1434 -1209
- package/dist/host/runtime.d.ts +2 -2
- package/dist/host/s2s.d.ts +16 -16
- package/dist/host/session-core.d.ts +37 -0
- package/dist/host/transports/pipeline-transport.d.ts +48 -0
- package/dist/host/transports/s2s-transport.d.ts +19 -0
- package/dist/host/transports/types.d.ts +45 -0
- package/dist/host/ws-handler.d.ts +14 -10
- package/dist/sdk/_internal-types.d.ts +2 -0
- package/dist/sdk/manifest-barrel.js +1 -1
- package/dist/sdk/protocol.d.ts +6 -5
- package/dist/sdk/providers/llm-barrel.js +1 -1
- package/dist/sdk/providers/stt/deepgram.d.ts +35 -0
- package/dist/sdk/providers/stt-barrel.d.ts +1 -0
- package/dist/sdk/providers/stt-barrel.js +2 -2
- package/dist/sdk/providers/tts/cartesia.d.ts +12 -4
- package/dist/sdk/providers/tts/rime.d.ts +42 -0
- package/dist/sdk/providers/tts-barrel.d.ts +1 -0
- package/dist/sdk/providers/tts-barrel.js +2 -2
- package/host/_pipeline-test-fakes.ts +6 -3
- package/host/_test-utils.ts +209 -128
- package/host/builtin-tools.ts +1 -0
- package/host/cleanup.test.ts +25 -298
- package/host/integration/pipeline-reference.integration.test.ts +30 -35
- package/host/providers/resolve.ts +10 -2
- package/host/providers/stt/deepgram.test.ts +229 -0
- package/host/providers/stt/deepgram.ts +172 -0
- package/host/providers/tts/cartesia.ts +7 -3
- package/host/providers/tts/rime.test.ts +251 -0
- package/host/providers/tts/rime.ts +322 -0
- package/host/runtime-barrel.ts +4 -2
- package/host/runtime.test.ts +16 -47
- package/host/runtime.ts +131 -23
- package/host/s2s.test.ts +122 -131
- package/host/s2s.ts +44 -52
- package/host/session-core.test.ts +257 -0
- package/host/session-core.ts +262 -0
- package/host/to-vercel-tools.test.ts +9 -1
- package/host/transports/pipeline-transport.test.ts +653 -0
- package/host/transports/pipeline-transport.ts +532 -0
- package/host/{fixture-replay.test.ts → transports/s2s-transport-fixtures.test.ts} +76 -106
- package/host/transports/s2s-transport.test.ts +56 -0
- package/host/transports/s2s-transport.ts +116 -0
- package/host/transports/types.test.ts +22 -0
- package/host/transports/types.ts +51 -0
- package/host/ws-handler.test.ts +324 -242
- package/host/ws-handler.ts +56 -59
- package/package.json +2 -1
- package/sdk/__snapshots__/exports.test.ts.snap +3 -3
- package/sdk/__snapshots__/schema-shapes.test.ts.snap +1 -0
- package/sdk/_internal-types.ts +3 -0
- package/sdk/protocol-compat.test.ts +8 -0
- package/sdk/protocol.ts +6 -5
- package/sdk/providers/stt/deepgram.ts +43 -0
- package/sdk/providers/stt-barrel.ts +2 -0
- package/sdk/providers/tts/cartesia.ts +15 -5
- package/sdk/providers/tts/rime.ts +52 -0
- package/sdk/providers/tts-barrel.ts +2 -0
- package/sdk/schema-alignment.test.ts +18 -6
- package/dist/assemblyai-Cxg9eobY.js +0 -18
- package/dist/cartesia-DwDk2tEu.js +0 -10
- package/dist/host/pipeline-session-ctx.d.ts +0 -24
- package/dist/host/pipeline-session.d.ts +0 -52
- package/dist/host/session-ctx.d.ts +0 -73
- package/dist/host/session.d.ts +0 -62
- package/host/pipeline-session-ctx.test.ts +0 -31
- package/host/pipeline-session-ctx.ts +0 -36
- package/host/pipeline-session.test.ts +0 -672
- package/host/pipeline-session.ts +0 -533
- package/host/s2s-fixtures.test.ts +0 -237
- package/host/session-ctx.test.ts +0 -387
- package/host/session-ctx.ts +0 -134
- package/host/session-fixture-replay.test.ts +0 -128
- package/host/session.test.ts +0 -634
- package/host/session.ts +0 -412
- /package/dist/{anthropic-BrUCPKUc.js → anthropic-CcLZygAr.js} +0 -0
package/host/cleanup.test.ts
CHANGED
|
@@ -7,18 +7,10 @@
|
|
|
7
7
|
* error, and reset to prevent memory leaks in long-running processes.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { describe, expect, test, vi } from "vitest";
|
|
11
11
|
import { MockWebSocket } from "./_mock-ws.ts";
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
makeMockHandle,
|
|
15
|
-
makeSessionOpts,
|
|
16
|
-
makeStubSession,
|
|
17
|
-
silentLogger,
|
|
18
|
-
} from "./_test-utils.ts";
|
|
19
|
-
import type { S2sHandle } from "./s2s.ts";
|
|
20
|
-
import type { Session } from "./session.ts";
|
|
21
|
-
import { _internals, createS2sSession, type S2sSessionOptions } from "./session.ts";
|
|
12
|
+
import { makeMockCore, silentLogger } from "./_test-utils.ts";
|
|
13
|
+
import type { SessionCore } from "./session-core.ts";
|
|
22
14
|
import { wireSessionSocket } from "./ws-handler.ts";
|
|
23
15
|
|
|
24
16
|
const defaultConfig = { audioFormat: "pcm16" as const, sampleRate: 16_000, ttsSampleRate: 24_000 };
|
|
@@ -27,13 +19,13 @@ const defaultConfig = { audioFormat: "pcm16" as const, sampleRate: 16_000, ttsSa
|
|
|
27
19
|
|
|
28
20
|
describe("wireSessionSocket resource cleanup", () => {
|
|
29
21
|
test("session.stop() is called exactly once on normal close", async () => {
|
|
30
|
-
const
|
|
22
|
+
const core = makeMockCore();
|
|
31
23
|
const ws = new MockWebSocket("ws://test");
|
|
32
24
|
ws.readyState = MockWebSocket.OPEN;
|
|
33
25
|
|
|
34
26
|
wireSessionSocket(ws, {
|
|
35
27
|
sessions: new Map(),
|
|
36
|
-
createSession: () =>
|
|
28
|
+
createSession: () => core,
|
|
37
29
|
readyConfig: defaultConfig,
|
|
38
30
|
logger: silentLogger,
|
|
39
31
|
});
|
|
@@ -41,21 +33,20 @@ describe("wireSessionSocket resource cleanup", () => {
|
|
|
41
33
|
ws.close();
|
|
42
34
|
|
|
43
35
|
await vi.waitFor(() => {
|
|
44
|
-
expect(
|
|
36
|
+
expect(core.stop).toHaveBeenCalledOnce();
|
|
45
37
|
});
|
|
46
38
|
});
|
|
47
39
|
|
|
48
40
|
test("session is removed from sessions map even when stop() rejects", async () => {
|
|
49
|
-
const sessions = new Map<string,
|
|
50
|
-
const
|
|
51
|
-
session.stop = vi.fn(() => Promise.reject(new Error("stop failed")));
|
|
41
|
+
const sessions = new Map<string, SessionCore>();
|
|
42
|
+
const core = makeMockCore({ stop: vi.fn(() => Promise.reject(new Error("stop failed"))) });
|
|
52
43
|
|
|
53
44
|
const ws = new MockWebSocket("ws://test");
|
|
54
45
|
ws.readyState = MockWebSocket.OPEN;
|
|
55
46
|
|
|
56
47
|
wireSessionSocket(ws, {
|
|
57
48
|
sessions,
|
|
58
|
-
createSession: () =>
|
|
49
|
+
createSession: () => core,
|
|
59
50
|
readyConfig: defaultConfig,
|
|
60
51
|
logger: silentLogger,
|
|
61
52
|
});
|
|
@@ -69,43 +60,42 @@ describe("wireSessionSocket resource cleanup", () => {
|
|
|
69
60
|
});
|
|
70
61
|
|
|
71
62
|
test("message buffer is cleared when start() fails", async () => {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
const sessions = new Map<string, Session>();
|
|
63
|
+
const core = makeMockCore({ start: vi.fn(() => Promise.reject(new Error("start failed"))) });
|
|
64
|
+
const sessions = new Map<string, SessionCore>();
|
|
75
65
|
|
|
76
66
|
const ws = new MockWebSocket("ws://test");
|
|
77
67
|
ws.readyState = MockWebSocket.OPEN;
|
|
78
68
|
|
|
79
69
|
wireSessionSocket(ws, {
|
|
80
70
|
sessions,
|
|
81
|
-
createSession: () =>
|
|
71
|
+
createSession: () => core,
|
|
82
72
|
readyConfig: defaultConfig,
|
|
83
73
|
logger: silentLogger,
|
|
84
74
|
});
|
|
85
75
|
|
|
86
|
-
// Send
|
|
87
|
-
ws.simulateMessage(
|
|
76
|
+
// Send a binary frame while start is failing (string frames are now dropped as non-binary)
|
|
77
|
+
ws.simulateMessage(new ArrayBuffer(4));
|
|
88
78
|
|
|
89
79
|
await vi.waitFor(() => {
|
|
90
80
|
expect(sessions.size).toBe(0);
|
|
91
81
|
});
|
|
92
82
|
|
|
93
83
|
// Session is null, further messages should be silently ignored (no throw)
|
|
94
|
-
ws.simulateMessage(JSON.stringify({ type: "audio_ready" }));
|
|
95
84
|
ws.simulateMessage(new ArrayBuffer(4));
|
|
96
85
|
});
|
|
97
86
|
|
|
98
87
|
test("multiple rapid closes don't double-invoke stop()", async () => {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
88
|
+
const core = makeMockCore({
|
|
89
|
+
stop: vi.fn(() => new Promise<void>((r) => setTimeout(r, 50))),
|
|
90
|
+
});
|
|
91
|
+
const sessions = new Map<string, SessionCore>();
|
|
102
92
|
|
|
103
93
|
const ws = new MockWebSocket("ws://test");
|
|
104
94
|
ws.readyState = MockWebSocket.OPEN;
|
|
105
95
|
|
|
106
96
|
wireSessionSocket(ws, {
|
|
107
97
|
sessions,
|
|
108
|
-
createSession: () =>
|
|
98
|
+
createSession: () => core,
|
|
109
99
|
readyConfig: defaultConfig,
|
|
110
100
|
logger: silentLogger,
|
|
111
101
|
});
|
|
@@ -115,18 +105,18 @@ describe("wireSessionSocket resource cleanup", () => {
|
|
|
115
105
|
// Even if close event fires again, stop should only be called once
|
|
116
106
|
// because the session reference is captured on first close
|
|
117
107
|
await vi.waitFor(() => {
|
|
118
|
-
expect(
|
|
108
|
+
expect(core.stop).toHaveBeenCalledOnce();
|
|
119
109
|
});
|
|
120
110
|
});
|
|
121
111
|
|
|
122
112
|
test("close before open does not throw or leak", () => {
|
|
123
113
|
const ws = new MockWebSocket("ws://test");
|
|
124
114
|
ws.readyState = MockWebSocket.CONNECTING;
|
|
125
|
-
const sessions = new Map<string,
|
|
115
|
+
const sessions = new Map<string, SessionCore>();
|
|
126
116
|
|
|
127
117
|
wireSessionSocket(ws, {
|
|
128
118
|
sessions,
|
|
129
|
-
createSession: () =>
|
|
119
|
+
createSession: () => makeMockCore(),
|
|
130
120
|
readyConfig: defaultConfig,
|
|
131
121
|
logger: silentLogger,
|
|
132
122
|
});
|
|
@@ -137,286 +127,23 @@ describe("wireSessionSocket resource cleanup", () => {
|
|
|
137
127
|
});
|
|
138
128
|
|
|
139
129
|
test("error event after close does not throw", async () => {
|
|
140
|
-
const
|
|
130
|
+
const core = makeMockCore();
|
|
141
131
|
const ws = new MockWebSocket("ws://test");
|
|
142
132
|
ws.readyState = MockWebSocket.OPEN;
|
|
143
133
|
|
|
144
134
|
wireSessionSocket(ws, {
|
|
145
135
|
sessions: new Map(),
|
|
146
|
-
createSession: () =>
|
|
136
|
+
createSession: () => core,
|
|
147
137
|
readyConfig: defaultConfig,
|
|
148
138
|
logger: silentLogger,
|
|
149
139
|
});
|
|
150
140
|
|
|
151
141
|
ws.close();
|
|
152
142
|
await vi.waitFor(() => {
|
|
153
|
-
expect(
|
|
143
|
+
expect(core.stop).toHaveBeenCalled();
|
|
154
144
|
});
|
|
155
145
|
|
|
156
146
|
// Error after close should not throw
|
|
157
147
|
ws.dispatchEvent(new Event("error"));
|
|
158
148
|
});
|
|
159
149
|
});
|
|
160
|
-
|
|
161
|
-
// ─── createS2sSession cleanup tests ──────────────────────────────────────────
|
|
162
|
-
|
|
163
|
-
describe("createS2sSession resource cleanup", () => {
|
|
164
|
-
let connectSpy: ReturnType<typeof vi.spyOn>;
|
|
165
|
-
let mockHandle: ReturnType<typeof makeMockHandle>;
|
|
166
|
-
|
|
167
|
-
function setup(overrides?: Partial<S2sSessionOptions>) {
|
|
168
|
-
mockHandle = makeMockHandle();
|
|
169
|
-
connectSpy = vi.spyOn(_internals, "connectS2s").mockResolvedValue(mockHandle);
|
|
170
|
-
const client = makeClient();
|
|
171
|
-
const opts = makeSessionOpts({ client, ...overrides });
|
|
172
|
-
const session = createS2sSession(opts);
|
|
173
|
-
return { session, client, opts, mockHandle };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
afterEach(() => {
|
|
177
|
-
connectSpy?.mockRestore();
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
test("stop() closes S2S handle and waits for in-flight turn", async () => {
|
|
181
|
-
let resolveToolCall!: (value: string) => void;
|
|
182
|
-
const executeTool = vi.fn(
|
|
183
|
-
() =>
|
|
184
|
-
new Promise<string>((r) => {
|
|
185
|
-
resolveToolCall = r;
|
|
186
|
-
}),
|
|
187
|
-
);
|
|
188
|
-
const { session, mockHandle } = setup({ executeTool });
|
|
189
|
-
await session.start();
|
|
190
|
-
|
|
191
|
-
// Start a tool call
|
|
192
|
-
mockHandle._fire("replyStarted", { replyId: "r1" });
|
|
193
|
-
mockHandle._fire("event", { type: "tool_call", toolCallId: "c1", toolName: "t1", args: {} });
|
|
194
|
-
await vi.waitFor(() => expect(executeTool).toHaveBeenCalled());
|
|
195
|
-
|
|
196
|
-
// Stop while tool is in-flight
|
|
197
|
-
const stopPromise = session.stop();
|
|
198
|
-
resolveToolCall("done");
|
|
199
|
-
await stopPromise;
|
|
200
|
-
|
|
201
|
-
expect(mockHandle.close).toHaveBeenCalled();
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
test("onReset clears pendingTools and conversation messages", async () => {
|
|
205
|
-
const executeTool = vi.fn(async () => "result");
|
|
206
|
-
const { session, mockHandle } = setup({ executeTool });
|
|
207
|
-
await session.start();
|
|
208
|
-
|
|
209
|
-
// Accumulate some tool calls
|
|
210
|
-
mockHandle._fire("replyStarted", { replyId: "r1" });
|
|
211
|
-
mockHandle._fire("event", { type: "tool_call", toolCallId: "c1", toolName: "t1", args: {} });
|
|
212
|
-
await session.waitForTurn();
|
|
213
|
-
|
|
214
|
-
// Send a user transcript to add conversation messages
|
|
215
|
-
mockHandle._fire("event", { type: "user_transcript", text: "Hello" });
|
|
216
|
-
|
|
217
|
-
// Reset — should clear pending tools and conversation
|
|
218
|
-
session.onReset();
|
|
219
|
-
|
|
220
|
-
// Verify old handle was closed
|
|
221
|
-
expect(mockHandle.close).toHaveBeenCalled();
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
test("onReset invalidates currentReplyId to discard stale tool results", async () => {
|
|
225
|
-
let resolveToolCall!: (value: string) => void;
|
|
226
|
-
const executeTool = vi.fn(
|
|
227
|
-
() =>
|
|
228
|
-
new Promise<string>((r) => {
|
|
229
|
-
resolveToolCall = r;
|
|
230
|
-
}),
|
|
231
|
-
);
|
|
232
|
-
const handles: ReturnType<typeof makeMockHandle>[] = [];
|
|
233
|
-
const spy = vi.spyOn(_internals, "connectS2s").mockImplementation(async () => {
|
|
234
|
-
const h = makeMockHandle();
|
|
235
|
-
handles.push(h);
|
|
236
|
-
return h;
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
const client = makeClient();
|
|
240
|
-
const session = createS2sSession(makeSessionOpts({ client, executeTool }));
|
|
241
|
-
await session.start();
|
|
242
|
-
|
|
243
|
-
// biome-ignore lint/style/noNonNullAssertion: test assertions after length check
|
|
244
|
-
const firstHandle = handles[0]!;
|
|
245
|
-
|
|
246
|
-
// Start a tool call on the first handle
|
|
247
|
-
firstHandle._fire("replyStarted", { replyId: "r1" });
|
|
248
|
-
firstHandle._fire("event", { type: "tool_call", toolCallId: "c1", toolName: "t1", args: {} });
|
|
249
|
-
await vi.waitFor(() => expect(executeTool).toHaveBeenCalled());
|
|
250
|
-
|
|
251
|
-
// Reset while tool is in-flight
|
|
252
|
-
session.onReset();
|
|
253
|
-
|
|
254
|
-
// Tool finishes late — result should be discarded due to generation mismatch
|
|
255
|
-
resolveToolCall("stale-result");
|
|
256
|
-
await session.waitForTurn();
|
|
257
|
-
|
|
258
|
-
// New handle should not receive the stale result
|
|
259
|
-
const newHandle = handles[1];
|
|
260
|
-
expect(newHandle?.sendToolResult).not.toHaveBeenCalled();
|
|
261
|
-
|
|
262
|
-
spy.mockRestore();
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
test("stop() is safe to call without start()", async () => {
|
|
266
|
-
const client = makeClient();
|
|
267
|
-
const session = createS2sSession(makeSessionOpts({ client }));
|
|
268
|
-
// stop() without start() — should not throw
|
|
269
|
-
await session.stop();
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
test("stop() prevents orphaned S2S connection when called during start()", async () => {
|
|
273
|
-
let resolveConnect!: (value: S2sHandle) => void;
|
|
274
|
-
const handle = makeMockHandle();
|
|
275
|
-
const spy = vi.spyOn(_internals, "connectS2s").mockImplementation(
|
|
276
|
-
() =>
|
|
277
|
-
new Promise((r) => {
|
|
278
|
-
resolveConnect = r as (value: S2sHandle) => void;
|
|
279
|
-
}),
|
|
280
|
-
);
|
|
281
|
-
|
|
282
|
-
const client = makeClient();
|
|
283
|
-
const session = createS2sSession(makeSessionOpts({ client }));
|
|
284
|
-
|
|
285
|
-
const startPromise = session.start();
|
|
286
|
-
const stopPromise = session.stop();
|
|
287
|
-
|
|
288
|
-
// Connection resolves after stop — handle must be closed immediately
|
|
289
|
-
resolveConnect(handle);
|
|
290
|
-
await startPromise;
|
|
291
|
-
await stopPromise;
|
|
292
|
-
|
|
293
|
-
expect(handle.close).toHaveBeenCalled();
|
|
294
|
-
spy.mockRestore();
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
test("S2S error event closes handle and emits error to client", async () => {
|
|
298
|
-
const { session, client, mockHandle } = setup();
|
|
299
|
-
await session.start();
|
|
300
|
-
|
|
301
|
-
mockHandle._fire("error", new Error("S2S crashed"));
|
|
302
|
-
|
|
303
|
-
expect(mockHandle.close).toHaveBeenCalled();
|
|
304
|
-
expect(client.events).toContainEqual({
|
|
305
|
-
type: "error",
|
|
306
|
-
code: "internal",
|
|
307
|
-
message: "S2S crashed",
|
|
308
|
-
});
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
test("S2S close event nullifies the handle reference", async () => {
|
|
312
|
-
const { session, mockHandle } = setup();
|
|
313
|
-
await session.start();
|
|
314
|
-
|
|
315
|
-
// Simulate S2S WebSocket close
|
|
316
|
-
mockHandle._fire("close", 1000, "normal");
|
|
317
|
-
|
|
318
|
-
// Sending audio after close should not throw (no-ops via ?. on null s2s)
|
|
319
|
-
session.onAudio(new Uint8Array([1, 2, 3]));
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
test("sessionExpired event closes the S2S handle", async () => {
|
|
323
|
-
const { session, mockHandle } = setup();
|
|
324
|
-
await session.start();
|
|
325
|
-
|
|
326
|
-
mockHandle._fire("sessionExpired");
|
|
327
|
-
// The handler calls handle.close() directly
|
|
328
|
-
expect(mockHandle.close).toHaveBeenCalled();
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
test("rapid resets close all stale connections", async () => {
|
|
332
|
-
const handles: ReturnType<typeof makeMockHandle>[] = [];
|
|
333
|
-
const resolvers: ((h: S2sHandle) => void)[] = [];
|
|
334
|
-
|
|
335
|
-
const spy = vi.spyOn(_internals, "connectS2s").mockImplementation(
|
|
336
|
-
() =>
|
|
337
|
-
new Promise<S2sHandle>((resolve) => {
|
|
338
|
-
const h = makeMockHandle();
|
|
339
|
-
handles.push(h);
|
|
340
|
-
resolvers.push(resolve as (value: S2sHandle) => void);
|
|
341
|
-
}),
|
|
342
|
-
);
|
|
343
|
-
|
|
344
|
-
const client = makeClient();
|
|
345
|
-
const session = createS2sSession(makeSessionOpts({ client }));
|
|
346
|
-
|
|
347
|
-
const startPromise = session.start();
|
|
348
|
-
session.onReset();
|
|
349
|
-
session.onReset();
|
|
350
|
-
|
|
351
|
-
expect(resolvers.length).toBe(3);
|
|
352
|
-
|
|
353
|
-
// Resolve in order — first two are stale
|
|
354
|
-
// biome-ignore lint/style/noNonNullAssertion: test assertions after length check
|
|
355
|
-
resolvers[0]?.(handles[0]!);
|
|
356
|
-
// biome-ignore lint/style/noNonNullAssertion: test assertions after length check
|
|
357
|
-
resolvers[1]?.(handles[1]!);
|
|
358
|
-
// biome-ignore lint/style/noNonNullAssertion: test assertions after length check
|
|
359
|
-
resolvers[2]?.(handles[2]!);
|
|
360
|
-
|
|
361
|
-
await startPromise;
|
|
362
|
-
|
|
363
|
-
await vi.waitFor(() => {
|
|
364
|
-
expect(handles[0]?.close).toHaveBeenCalled();
|
|
365
|
-
expect(handles[1]?.close).toHaveBeenCalled();
|
|
366
|
-
});
|
|
367
|
-
expect(handles[2]?.close).not.toHaveBeenCalled();
|
|
368
|
-
|
|
369
|
-
spy.mockRestore();
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
test("concurrent tool calls all complete before stop() resolves", async () => {
|
|
373
|
-
const resolvers: ((value: string) => void)[] = [];
|
|
374
|
-
const executeTool = vi.fn(
|
|
375
|
-
() =>
|
|
376
|
-
new Promise<string>((r) => {
|
|
377
|
-
resolvers.push(r);
|
|
378
|
-
}),
|
|
379
|
-
);
|
|
380
|
-
const { session, mockHandle } = setup({ executeTool });
|
|
381
|
-
await session.start();
|
|
382
|
-
|
|
383
|
-
mockHandle._fire("replyStarted", { replyId: "r1" });
|
|
384
|
-
mockHandle._fire("event", { type: "tool_call", toolCallId: "c1", toolName: "t1", args: {} });
|
|
385
|
-
mockHandle._fire("event", { type: "tool_call", toolCallId: "c2", toolName: "t2", args: {} });
|
|
386
|
-
|
|
387
|
-
await vi.waitFor(() => expect(executeTool).toHaveBeenCalledTimes(2));
|
|
388
|
-
|
|
389
|
-
// Stop while both tools are in-flight
|
|
390
|
-
const stopPromise = session.stop();
|
|
391
|
-
|
|
392
|
-
// Resolve both tools
|
|
393
|
-
resolvers[0]?.("result-1");
|
|
394
|
-
resolvers[1]?.("result-2");
|
|
395
|
-
|
|
396
|
-
await stopPromise;
|
|
397
|
-
// If we get here, turnPromise was properly awaited
|
|
398
|
-
expect(mockHandle.close).toHaveBeenCalled();
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
test("connectS2s failure does not leak resources", async () => {
|
|
402
|
-
const spy = vi.spyOn(_internals, "connectS2s").mockRejectedValue(new Error("network error"));
|
|
403
|
-
const client = makeClient();
|
|
404
|
-
const session = createS2sSession(makeSessionOpts({ client }));
|
|
405
|
-
|
|
406
|
-
await session.start();
|
|
407
|
-
|
|
408
|
-
// Client should get error event
|
|
409
|
-
expect(client.events).toContainEqual(
|
|
410
|
-
expect.objectContaining({
|
|
411
|
-
type: "error",
|
|
412
|
-
code: "internal",
|
|
413
|
-
message: "network error",
|
|
414
|
-
}),
|
|
415
|
-
);
|
|
416
|
-
|
|
417
|
-
// stop() should not throw even after failed start
|
|
418
|
-
await session.stop();
|
|
419
|
-
|
|
420
|
-
spy.mockRestore();
|
|
421
|
-
});
|
|
422
|
-
});
|
|
@@ -24,11 +24,10 @@ import { dirname, join } from "node:path";
|
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
25
|
import { openai } from "@ai-sdk/openai";
|
|
26
26
|
import { describe, expect, test } from "vitest";
|
|
27
|
-
import type {
|
|
28
|
-
import type { ClientEvent, ClientSink } from "../../sdk/protocol.ts";
|
|
29
|
-
import { createPipelineSession } from "../pipeline-session.ts";
|
|
27
|
+
import type { ClientSink } from "../../sdk/protocol.ts";
|
|
30
28
|
import { openAssemblyAI } from "../providers/stt/assemblyai.ts";
|
|
31
29
|
import { openCartesia } from "../providers/tts/cartesia.ts";
|
|
30
|
+
import { createRuntime } from "../runtime.ts";
|
|
32
31
|
import { consoleLogger } from "../runtime-config.ts";
|
|
33
32
|
|
|
34
33
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -59,47 +58,48 @@ describe.skipIf(!envReady)("pipeline integration — reference stack", () => {
|
|
|
59
58
|
);
|
|
60
59
|
}
|
|
61
60
|
const pcm = await readFile(fixturePath);
|
|
62
|
-
const
|
|
61
|
+
const userTranscripts: string[] = [];
|
|
63
62
|
const audioOut: Uint8Array[] = [];
|
|
63
|
+
let replyDone = false;
|
|
64
|
+
|
|
64
65
|
const client: ClientSink = {
|
|
65
66
|
open: true,
|
|
66
67
|
event: (e) => {
|
|
67
|
-
|
|
68
|
+
if (e.type === "user_transcript") userTranscripts.push(e.text);
|
|
69
|
+
if (e.type === "reply_done") replyDone = true;
|
|
68
70
|
},
|
|
69
71
|
playAudioChunk: (chunk) => {
|
|
70
72
|
audioOut.push(chunk);
|
|
71
73
|
},
|
|
72
|
-
playAudioDone: () =>
|
|
73
|
-
/* no-op: test asserts on audioOut chunks directly */
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const agentConfig: AgentConfig = {
|
|
78
|
-
name: "int",
|
|
79
|
-
systemPrompt: "You reply in one short sentence.",
|
|
80
|
-
greeting: "",
|
|
81
|
-
maxSteps: 1,
|
|
74
|
+
playAudioDone: () => undefined,
|
|
82
75
|
};
|
|
83
76
|
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
77
|
+
const runtime = createRuntime({
|
|
78
|
+
agent: {
|
|
79
|
+
name: "int",
|
|
80
|
+
systemPrompt: "You reply in one short sentence.",
|
|
81
|
+
greeting: "",
|
|
82
|
+
maxSteps: 1,
|
|
83
|
+
tools: {},
|
|
84
|
+
},
|
|
85
|
+
env: {
|
|
86
|
+
// biome-ignore lint/style/noNonNullAssertion: envReady guard ensures presence
|
|
87
|
+
ASSEMBLYAI_API_KEY: process.env.ASSEMBLYAI_API_KEY!,
|
|
88
|
+
// biome-ignore lint/style/noNonNullAssertion: envReady guard ensures presence
|
|
89
|
+
CARTESIA_API_KEY: process.env.CARTESIA_API_KEY!,
|
|
90
|
+
},
|
|
93
91
|
stt: openAssemblyAI({ model: "u3pro-rt" }),
|
|
94
92
|
llm: openai("gpt-4o-mini"),
|
|
95
93
|
tts: openCartesia({ voice: "694f9389-aac1-45b6-b726-9d9369183238" }),
|
|
96
|
-
// biome-ignore lint/style/noNonNullAssertion: envReady guard ensures presence
|
|
97
|
-
sttApiKey: process.env.ASSEMBLYAI_API_KEY!,
|
|
98
|
-
// biome-ignore lint/style/noNonNullAssertion: envReady guard ensures presence
|
|
99
|
-
ttsApiKey: process.env.CARTESIA_API_KEY!,
|
|
100
94
|
logger: consoleLogger,
|
|
101
95
|
});
|
|
102
96
|
|
|
97
|
+
const session = runtime.createSession({
|
|
98
|
+
id: "int-1",
|
|
99
|
+
agent: "pipeline-reference",
|
|
100
|
+
client,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
103
|
await session.start();
|
|
104
104
|
session.onAudioReady();
|
|
105
105
|
|
|
@@ -110,15 +110,10 @@ describe.skipIf(!envReady)("pipeline integration — reference stack", () => {
|
|
|
110
110
|
session.onAudio(new Uint8Array(chunk));
|
|
111
111
|
await new Promise((r) => setTimeout(r, 100));
|
|
112
112
|
}
|
|
113
|
-
await session.waitForTurn();
|
|
114
113
|
await session.stop();
|
|
115
114
|
|
|
116
|
-
|
|
117
|
-
expect(
|
|
118
|
-
expect(String((userTranscript as { text: string }).text).toLowerCase()).toContain(
|
|
119
|
-
"how are you",
|
|
120
|
-
);
|
|
121
|
-
expect(events.some((e) => e.type === "reply_done")).toBe(true);
|
|
115
|
+
expect(userTranscripts.some((t) => t.toLowerCase().includes("how are you"))).toBe(true);
|
|
116
|
+
expect(replyDone).toBe(true);
|
|
122
117
|
expect(audioOut.length).toBeGreaterThan(0);
|
|
123
118
|
}, 60_000);
|
|
124
119
|
});
|
|
@@ -17,7 +17,9 @@ import { createAnthropic } from "@ai-sdk/anthropic";
|
|
|
17
17
|
import type { LanguageModel } from "ai";
|
|
18
18
|
import { ANTHROPIC_KIND, type AnthropicOptions } from "../../sdk/providers/llm/anthropic.ts";
|
|
19
19
|
import { ASSEMBLYAI_KIND, type AssemblyAIOptions } from "../../sdk/providers/stt/assemblyai.ts";
|
|
20
|
+
import { DEEPGRAM_KIND, type DeepgramOptions } from "../../sdk/providers/stt/deepgram.ts";
|
|
20
21
|
import { CARTESIA_KIND, type CartesiaOptions } from "../../sdk/providers/tts/cartesia.ts";
|
|
22
|
+
import { RIME_KIND, type RimeOptions } from "../../sdk/providers/tts/rime.ts";
|
|
21
23
|
import type {
|
|
22
24
|
LlmProvider,
|
|
23
25
|
SttOpener,
|
|
@@ -26,7 +28,9 @@ import type {
|
|
|
26
28
|
TtsProvider,
|
|
27
29
|
} from "../../sdk/providers.ts";
|
|
28
30
|
import { openAssemblyAI } from "./stt/assemblyai.ts";
|
|
31
|
+
import { openDeepgram } from "./stt/deepgram.ts";
|
|
29
32
|
import { openCartesia } from "./tts/cartesia.ts";
|
|
33
|
+
import { openRime } from "./tts/rime.ts";
|
|
30
34
|
|
|
31
35
|
/**
|
|
32
36
|
* Look up a provider API key: agent env first (set via `aai secret put` or
|
|
@@ -42,9 +46,11 @@ export function resolveStt(descriptor: SttProvider): SttOpener {
|
|
|
42
46
|
switch (descriptor.kind) {
|
|
43
47
|
case ASSEMBLYAI_KIND:
|
|
44
48
|
return openAssemblyAI(descriptor.options as unknown as AssemblyAIOptions);
|
|
49
|
+
case DEEPGRAM_KIND:
|
|
50
|
+
return openDeepgram(descriptor.options as unknown as DeepgramOptions);
|
|
45
51
|
default:
|
|
46
52
|
throw new Error(
|
|
47
|
-
`Unknown STT provider kind: "${descriptor.kind}". Supported: ${ASSEMBLYAI_KIND}.`,
|
|
53
|
+
`Unknown STT provider kind: "${descriptor.kind}". Supported: ${ASSEMBLYAI_KIND}, ${DEEPGRAM_KIND}.`,
|
|
48
54
|
);
|
|
49
55
|
}
|
|
50
56
|
}
|
|
@@ -54,9 +60,11 @@ export function resolveTts(descriptor: TtsProvider): TtsOpener {
|
|
|
54
60
|
switch (descriptor.kind) {
|
|
55
61
|
case CARTESIA_KIND:
|
|
56
62
|
return openCartesia(descriptor.options as unknown as CartesiaOptions);
|
|
63
|
+
case RIME_KIND:
|
|
64
|
+
return openRime(descriptor.options as unknown as RimeOptions);
|
|
57
65
|
default:
|
|
58
66
|
throw new Error(
|
|
59
|
-
`Unknown TTS provider kind: "${descriptor.kind}". Supported: ${CARTESIA_KIND}.`,
|
|
67
|
+
`Unknown TTS provider kind: "${descriptor.kind}". Supported: ${CARTESIA_KIND}, ${RIME_KIND}.`,
|
|
60
68
|
);
|
|
61
69
|
}
|
|
62
70
|
}
|