@alexkroman1/aai 1.7.1 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +11 -9
- package/CHANGELOG.md +10 -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 +619 -630
- 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 +55 -49
- 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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
2
2
|
import { makeMockHandle, silentLogger } from "../_test-utils.ts";
|
|
3
|
-
import type { S2sCallbacks, S2sHandle } from "../s2s.ts";
|
|
4
|
-
import { _internals, createS2sTransport } from "./s2s-transport.ts";
|
|
3
|
+
import type { ConnectS2sOptions, S2sCallbacks, S2sHandle, S2sWebSocket } from "../s2s.ts";
|
|
4
|
+
import { _internals, createS2sTransport, type S2sTransportOptions } from "./s2s-transport.ts";
|
|
5
5
|
import type { TransportCallbacks } from "./types.ts";
|
|
6
6
|
|
|
7
7
|
function makeCallbacks(): TransportCallbacks {
|
|
@@ -21,34 +21,37 @@ function makeCallbacks(): TransportCallbacks {
|
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function makeTransportOptions(overrides: Partial<S2sTransportOptions> = {}): S2sTransportOptions {
|
|
25
|
+
return {
|
|
26
|
+
apiKey: "k",
|
|
27
|
+
s2sConfig: { wssUrl: "wss://fake", inputSampleRate: 16_000, outputSampleRate: 24_000 },
|
|
28
|
+
sessionConfig: { systemPrompt: "test", tools: [] },
|
|
29
|
+
toolSchemas: [],
|
|
30
|
+
callbacks: makeCallbacks(),
|
|
31
|
+
sid: "sid-1",
|
|
32
|
+
agent: "a",
|
|
33
|
+
logger: silentLogger,
|
|
34
|
+
...overrides,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
24
38
|
describe("S2sTransport", () => {
|
|
25
39
|
test("start() opens an S2S connection and sends session.update", async () => {
|
|
26
40
|
const send = vi.fn();
|
|
27
41
|
const close = vi.fn();
|
|
28
|
-
const
|
|
42
|
+
const target = new EventTarget();
|
|
43
|
+
const ws = Object.assign(target, {
|
|
29
44
|
readyState: 0,
|
|
30
45
|
send,
|
|
31
46
|
close,
|
|
32
|
-
addEventListener:
|
|
33
|
-
|
|
34
|
-
listener: EventListener,
|
|
35
|
-
) => void,
|
|
36
|
-
}) as unknown as import("../s2s.ts").S2sWebSocket;
|
|
47
|
+
addEventListener: target.addEventListener.bind(target),
|
|
48
|
+
}) as unknown as S2sWebSocket;
|
|
37
49
|
setTimeout(() => {
|
|
38
50
|
(ws as unknown as { readyState: number }).readyState = 1;
|
|
39
|
-
|
|
51
|
+
target.dispatchEvent(new Event("open"));
|
|
40
52
|
}, 0);
|
|
41
53
|
|
|
42
|
-
const t = createS2sTransport({
|
|
43
|
-
apiKey: "k",
|
|
44
|
-
s2sConfig: { wssUrl: "wss://fake", inputSampleRate: 16_000, outputSampleRate: 24_000 },
|
|
45
|
-
sessionConfig: { systemPrompt: "test", tools: [] },
|
|
46
|
-
toolSchemas: [],
|
|
47
|
-
callbacks: makeCallbacks(),
|
|
48
|
-
sid: "sid-1",
|
|
49
|
-
agent: "a",
|
|
50
|
-
createWebSocket: () => ws,
|
|
51
|
-
});
|
|
54
|
+
const t = createS2sTransport(makeTransportOptions({ createWebSocket: () => ws }));
|
|
52
55
|
await t.start();
|
|
53
56
|
expect(send).toHaveBeenCalled();
|
|
54
57
|
const firstSend = JSON.parse(send.mock.calls[0]?.[0] as string);
|
|
@@ -58,31 +61,27 @@ describe("S2sTransport", () => {
|
|
|
58
61
|
});
|
|
59
62
|
});
|
|
60
63
|
|
|
61
|
-
// ─── Reconnect tests ────────────────────────────────────────────────────────
|
|
62
|
-
|
|
63
64
|
/** Capture the S2sCallbacks that the transport hands to connectS2s. */
|
|
64
65
|
function setupSpiedTransport(): {
|
|
65
66
|
callbacks: TransportCallbacks;
|
|
66
67
|
handles: S2sHandle[];
|
|
67
68
|
capturedCallbacks: S2sCallbacks[];
|
|
68
|
-
spy: ReturnType<typeof vi.spyOn>;
|
|
69
69
|
} {
|
|
70
70
|
const handles: S2sHandle[] = [];
|
|
71
71
|
const capturedCallbacks: S2sCallbacks[] = [];
|
|
72
|
-
|
|
73
|
-
.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
};
|
|
72
|
+
vi.spyOn(_internals, "connectS2s").mockImplementation(async (opts: ConnectS2sOptions) => {
|
|
73
|
+
capturedCallbacks.push(opts.callbacks);
|
|
74
|
+
const h = makeMockHandle();
|
|
75
|
+
handles.push(h);
|
|
76
|
+
return h;
|
|
77
|
+
});
|
|
78
|
+
return { callbacks: makeCallbacks(), handles, capturedCallbacks };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function expectAt<T>(arr: T[], index: number, label: string): T {
|
|
82
|
+
const value = arr[index];
|
|
83
|
+
if (!value) throw new Error(`expected ${label} at index ${index}`);
|
|
84
|
+
return value;
|
|
86
85
|
}
|
|
87
86
|
|
|
88
87
|
describe("S2sTransport reconnect", () => {
|
|
@@ -92,66 +91,37 @@ describe("S2sTransport reconnect", () => {
|
|
|
92
91
|
|
|
93
92
|
test("attempts session.resume on transient close (1005) inside the resume window", async () => {
|
|
94
93
|
const { callbacks, handles, capturedCallbacks } = setupSpiedTransport();
|
|
95
|
-
|
|
96
|
-
const t = createS2sTransport({
|
|
97
|
-
apiKey: "k",
|
|
98
|
-
s2sConfig: { wssUrl: "wss://fake", inputSampleRate: 16_000, outputSampleRate: 24_000 },
|
|
99
|
-
sessionConfig: { systemPrompt: "test", tools: [] },
|
|
100
|
-
toolSchemas: [],
|
|
101
|
-
callbacks,
|
|
102
|
-
sid: "sid-1",
|
|
103
|
-
agent: "a",
|
|
104
|
-
logger: silentLogger,
|
|
105
|
-
});
|
|
94
|
+
const t = createS2sTransport(makeTransportOptions({ callbacks }));
|
|
106
95
|
await t.start();
|
|
107
96
|
|
|
108
|
-
|
|
109
|
-
const cb1 = capturedCallbacks[0];
|
|
110
|
-
if (!cb1) throw new Error("expected first callbacks");
|
|
97
|
+
const cb1 = expectAt(capturedCallbacks, 0, "first callbacks");
|
|
111
98
|
cb1.onSessionReady("sess_abc");
|
|
112
99
|
cb1.onReplyStarted("rep_1");
|
|
113
100
|
cb1.onClose(1005, "");
|
|
114
101
|
|
|
115
|
-
// Wait for the async resume() to fire connectS2s a second time.
|
|
116
102
|
await vi.waitFor(() => {
|
|
117
103
|
expect(handles.length).toBe(2);
|
|
118
104
|
});
|
|
119
105
|
|
|
120
|
-
|
|
121
|
-
const newHandle = handles[1];
|
|
122
|
-
if (!newHandle) throw new Error("expected new handle");
|
|
106
|
+
const newHandle = expectAt(handles, 1, "new handle");
|
|
123
107
|
expect(newHandle.resumeSession).toHaveBeenCalledWith("sess_abc");
|
|
124
108
|
|
|
125
|
-
// The in-flight reply was unblocked via onCancelled, NOT a fatal error.
|
|
126
109
|
expect(callbacks.onCancelled).toHaveBeenCalledOnce();
|
|
127
110
|
expect(callbacks.onError).not.toHaveBeenCalled();
|
|
128
111
|
});
|
|
129
112
|
|
|
130
113
|
test("does NOT reconnect on fatal close codes (1008 unauthorized)", async () => {
|
|
131
114
|
const { callbacks, handles, capturedCallbacks } = setupSpiedTransport();
|
|
132
|
-
|
|
133
|
-
const t = createS2sTransport({
|
|
134
|
-
apiKey: "k",
|
|
135
|
-
s2sConfig: { wssUrl: "wss://fake", inputSampleRate: 16_000, outputSampleRate: 24_000 },
|
|
136
|
-
sessionConfig: { systemPrompt: "test", tools: [] },
|
|
137
|
-
toolSchemas: [],
|
|
138
|
-
callbacks,
|
|
139
|
-
sid: "sid-1",
|
|
140
|
-
agent: "a",
|
|
141
|
-
logger: silentLogger,
|
|
142
|
-
});
|
|
115
|
+
const t = createS2sTransport(makeTransportOptions({ callbacks }));
|
|
143
116
|
await t.start();
|
|
144
117
|
|
|
145
|
-
const cb1 = capturedCallbacks
|
|
146
|
-
if (!cb1) throw new Error("expected first callbacks");
|
|
118
|
+
const cb1 = expectAt(capturedCallbacks, 0, "first callbacks");
|
|
147
119
|
cb1.onSessionReady("sess_abc");
|
|
148
120
|
cb1.onReplyStarted("rep_1");
|
|
149
121
|
cb1.onClose(1008, "unauthorized");
|
|
150
122
|
|
|
151
|
-
// No reconnect — only one connectS2s call total.
|
|
152
123
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
153
124
|
expect(handles.length).toBe(1);
|
|
154
|
-
// Fatal error surfaces, since a reply was in flight.
|
|
155
125
|
expect(callbacks.onError).toHaveBeenCalledWith(
|
|
156
126
|
"connection",
|
|
157
127
|
expect.stringContaining("S2S closed mid-reply"),
|
|
@@ -160,26 +130,14 @@ describe("S2sTransport reconnect", () => {
|
|
|
160
130
|
|
|
161
131
|
test("does NOT reconnect when stop() was called", async () => {
|
|
162
132
|
const { callbacks, handles, capturedCallbacks } = setupSpiedTransport();
|
|
163
|
-
|
|
164
|
-
const t = createS2sTransport({
|
|
165
|
-
apiKey: "k",
|
|
166
|
-
s2sConfig: { wssUrl: "wss://fake", inputSampleRate: 16_000, outputSampleRate: 24_000 },
|
|
167
|
-
sessionConfig: { systemPrompt: "test", tools: [] },
|
|
168
|
-
toolSchemas: [],
|
|
169
|
-
callbacks,
|
|
170
|
-
sid: "sid-1",
|
|
171
|
-
agent: "a",
|
|
172
|
-
logger: silentLogger,
|
|
173
|
-
});
|
|
133
|
+
const t = createS2sTransport(makeTransportOptions({ callbacks }));
|
|
174
134
|
await t.start();
|
|
175
135
|
|
|
176
|
-
const cb1 = capturedCallbacks
|
|
177
|
-
if (!cb1) throw new Error("expected first callbacks");
|
|
136
|
+
const cb1 = expectAt(capturedCallbacks, 0, "first callbacks");
|
|
178
137
|
cb1.onSessionReady("sess_abc");
|
|
179
138
|
await t.stop();
|
|
180
139
|
|
|
181
|
-
//
|
|
182
|
-
// treated as a clean shutdown, not a transient drop worth resuming.
|
|
140
|
+
// Upstream close after stop() must be treated as clean shutdown, not a transient drop.
|
|
183
141
|
cb1.onClose(1005, "");
|
|
184
142
|
|
|
185
143
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
@@ -189,28 +147,17 @@ describe("S2sTransport reconnect", () => {
|
|
|
189
147
|
|
|
190
148
|
test("surfaces resume failure when the resumed socket also closes", async () => {
|
|
191
149
|
const { callbacks, handles, capturedCallbacks } = setupSpiedTransport();
|
|
192
|
-
|
|
193
|
-
const t = createS2sTransport({
|
|
194
|
-
apiKey: "k",
|
|
195
|
-
s2sConfig: { wssUrl: "wss://fake", inputSampleRate: 16_000, outputSampleRate: 24_000 },
|
|
196
|
-
sessionConfig: { systemPrompt: "test", tools: [] },
|
|
197
|
-
toolSchemas: [],
|
|
198
|
-
callbacks,
|
|
199
|
-
sid: "sid-1",
|
|
200
|
-
agent: "a",
|
|
201
|
-
logger: silentLogger,
|
|
202
|
-
});
|
|
150
|
+
const t = createS2sTransport(makeTransportOptions({ callbacks }));
|
|
203
151
|
await t.start();
|
|
204
152
|
|
|
205
|
-
capturedCallbacks
|
|
206
|
-
|
|
207
|
-
|
|
153
|
+
const cb1 = expectAt(capturedCallbacks, 0, "first callbacks");
|
|
154
|
+
cb1.onSessionReady("sess_abc");
|
|
155
|
+
cb1.onReplyStarted("rep_1");
|
|
156
|
+
cb1.onClose(1005, "");
|
|
208
157
|
|
|
209
158
|
await vi.waitFor(() => expect(handles.length).toBe(2));
|
|
210
159
|
|
|
211
|
-
|
|
212
|
-
const cb2 = capturedCallbacks[1];
|
|
213
|
-
if (!cb2) throw new Error("expected resume callbacks");
|
|
160
|
+
const cb2 = expectAt(capturedCallbacks, 1, "resume callbacks");
|
|
214
161
|
cb2.onClose(1006, "");
|
|
215
162
|
|
|
216
163
|
expect(callbacks.onError).toHaveBeenCalledWith(
|
|
@@ -221,25 +168,17 @@ describe("S2sTransport reconnect", () => {
|
|
|
221
168
|
|
|
222
169
|
test("surfaces resume failure when server reports session_not_found", async () => {
|
|
223
170
|
const { callbacks, handles, capturedCallbacks } = setupSpiedTransport();
|
|
224
|
-
|
|
225
|
-
const t = createS2sTransport({
|
|
226
|
-
apiKey: "k",
|
|
227
|
-
s2sConfig: { wssUrl: "wss://fake", inputSampleRate: 16_000, outputSampleRate: 24_000 },
|
|
228
|
-
sessionConfig: { systemPrompt: "test", tools: [] },
|
|
229
|
-
toolSchemas: [],
|
|
230
|
-
callbacks,
|
|
231
|
-
sid: "sid-1",
|
|
232
|
-
agent: "a",
|
|
233
|
-
logger: silentLogger,
|
|
234
|
-
});
|
|
171
|
+
const t = createS2sTransport(makeTransportOptions({ callbacks }));
|
|
235
172
|
await t.start();
|
|
236
173
|
|
|
237
|
-
capturedCallbacks
|
|
238
|
-
|
|
174
|
+
const cb1 = expectAt(capturedCallbacks, 0, "first callbacks");
|
|
175
|
+
cb1.onSessionReady("sess_abc");
|
|
176
|
+
cb1.onClose(1005, "");
|
|
239
177
|
|
|
240
178
|
await vi.waitFor(() => expect(handles.length).toBe(2));
|
|
241
179
|
|
|
242
|
-
capturedCallbacks
|
|
180
|
+
const cb2 = expectAt(capturedCallbacks, 1, "resume callbacks");
|
|
181
|
+
cb2.onSessionExpired();
|
|
243
182
|
|
|
244
183
|
expect(callbacks.onError).toHaveBeenCalledWith(
|
|
245
184
|
"connection",
|
|
@@ -249,28 +188,20 @@ describe("S2sTransport reconnect", () => {
|
|
|
249
188
|
|
|
250
189
|
test("after a successful resume, a later transient drop also resumes", async () => {
|
|
251
190
|
const { callbacks, handles, capturedCallbacks } = setupSpiedTransport();
|
|
252
|
-
|
|
253
|
-
const t = createS2sTransport({
|
|
254
|
-
apiKey: "k",
|
|
255
|
-
s2sConfig: { wssUrl: "wss://fake", inputSampleRate: 16_000, outputSampleRate: 24_000 },
|
|
256
|
-
sessionConfig: { systemPrompt: "test", tools: [] },
|
|
257
|
-
toolSchemas: [],
|
|
258
|
-
callbacks,
|
|
259
|
-
sid: "sid-1",
|
|
260
|
-
agent: "a",
|
|
261
|
-
logger: silentLogger,
|
|
262
|
-
});
|
|
191
|
+
const t = createS2sTransport(makeTransportOptions({ callbacks }));
|
|
263
192
|
await t.start();
|
|
264
193
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
194
|
+
const cb1 = expectAt(capturedCallbacks, 0, "first callbacks");
|
|
195
|
+
cb1.onSessionReady("sess_abc");
|
|
196
|
+
cb1.onClose(1005, "");
|
|
268
197
|
await vi.waitFor(() => expect(handles.length).toBe(2));
|
|
269
|
-
capturedCallbacks[1]?.onSessionReady("sess_abc");
|
|
270
198
|
|
|
271
|
-
|
|
272
|
-
|
|
199
|
+
const cb2 = expectAt(capturedCallbacks, 1, "resume callbacks");
|
|
200
|
+
cb2.onSessionReady("sess_abc");
|
|
201
|
+
cb2.onClose(1006, "");
|
|
273
202
|
await vi.waitFor(() => expect(handles.length).toBe(3));
|
|
274
|
-
expect(handles
|
|
203
|
+
expect(expectAt(handles, 2, "second resume handle").resumeSession).toHaveBeenCalledWith(
|
|
204
|
+
"sess_abc",
|
|
205
|
+
);
|
|
275
206
|
});
|
|
276
207
|
});
|
|
@@ -31,9 +31,9 @@ export type S2sTransportOptions = {
|
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Close codes worth attempting `session.resume` on. These are network/server
|
|
34
|
-
* blips, not protocol or auth violations.
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* blips, not protocol or auth violations. AssemblyAI keeps the session
|
|
35
|
+
* available for 30 s after disconnect; reconnect runs immediately on close,
|
|
36
|
+
* so the resume request reliably lands inside that window.
|
|
37
37
|
*/
|
|
38
38
|
const TRANSIENT_CLOSE_CODES = new Set<number>([
|
|
39
39
|
1005, // No Status Received (abnormal close, no frame)
|
|
@@ -42,46 +42,28 @@ const TRANSIENT_CLOSE_CODES = new Set<number>([
|
|
|
42
42
|
3005, // Session Cancelled (unknown server error)
|
|
43
43
|
]);
|
|
44
44
|
|
|
45
|
-
/**
|
|
46
|
-
* AssemblyAI keeps the session alive for 30 s after disconnect; we leave a
|
|
47
|
-
* little headroom so the resume request still fits inside that window after
|
|
48
|
-
* the new WebSocket finishes opening.
|
|
49
|
-
*/
|
|
50
|
-
const RESUME_WINDOW_MS = 25_000;
|
|
51
|
-
|
|
52
45
|
export function createS2sTransport(opts: S2sTransportOptions): Transport {
|
|
53
46
|
const log = opts.logger ?? consoleLogger;
|
|
54
47
|
const createWs = opts.createWebSocket ?? defaultCreateS2sWebSocket;
|
|
55
48
|
let handle: S2sHandle | null = null;
|
|
56
49
|
let currentReplyId: string | null = null;
|
|
57
|
-
/** Most recent `session.ready` ID — present once the upstream session is established. */
|
|
58
50
|
let providerSessionId: string | null = null;
|
|
59
|
-
/** When the current session became ready; bounds the resume window. */
|
|
60
|
-
let sessionReadyAt = 0;
|
|
61
|
-
/** Set by `stop()` so a deliberate close doesn't trigger a reconnect. */
|
|
62
51
|
let closing = false;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
* (close before ready) from a normal close.
|
|
67
|
-
*/
|
|
52
|
+
// True between sending `session.resume` and the next `session.ready`.
|
|
53
|
+
// Distinguishes a resume failure (close before ready) from a normal close
|
|
54
|
+
// and prevents back-to-back reconnect loops if the resumed socket also drops.
|
|
68
55
|
let reconnecting = false;
|
|
69
|
-
/**
|
|
70
|
-
* Set when a reconnect attempt is kicked off, cleared once the resumed
|
|
71
|
-
* session's `session.ready` arrives. Prevents back-to-back reconnect loops
|
|
72
|
-
* when the freshly-resumed socket also drops before fully recovering.
|
|
73
|
-
*/
|
|
74
|
-
let reconnectInFlight = false;
|
|
75
56
|
|
|
76
57
|
function buildCallbacks(): S2sCallbacks {
|
|
77
58
|
return {
|
|
78
59
|
onSessionReady: (id) => {
|
|
60
|
+
const isFirstReady = providerSessionId === null;
|
|
79
61
|
providerSessionId = id;
|
|
80
|
-
sessionReadyAt = Date.now();
|
|
81
62
|
if (reconnecting) {
|
|
82
63
|
reconnecting = false;
|
|
83
|
-
reconnectInFlight = false;
|
|
84
64
|
log.info("S2S resumed", { sid: opts.sid, sessionId: id });
|
|
65
|
+
} else if (isFirstReady) {
|
|
66
|
+
log.info("S2S session ready", { sid: opts.sid, sessionId: id });
|
|
85
67
|
}
|
|
86
68
|
opts.callbacks.onSessionReady?.(id);
|
|
87
69
|
},
|
|
@@ -104,12 +86,10 @@ export function createS2sTransport(opts: S2sTransportOptions): Transport {
|
|
|
104
86
|
onSpeechStarted: opts.callbacks.onSpeechStarted,
|
|
105
87
|
onSpeechStopped: opts.callbacks.onSpeechStopped,
|
|
106
88
|
onSessionExpired: () => {
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
// rather than retrying — there's nothing left to resume.
|
|
89
|
+
// Server reports session no longer exists (likely session_not_found
|
|
90
|
+
// in response to our resume). Surface as fatal — nothing to resume.
|
|
110
91
|
if (reconnecting) {
|
|
111
92
|
reconnecting = false;
|
|
112
|
-
reconnectInFlight = false;
|
|
113
93
|
log.warn("S2S resume rejected: session expired", { sid: opts.sid });
|
|
114
94
|
opts.callbacks.onError("connection", "S2S resume failed: session expired");
|
|
115
95
|
return;
|
|
@@ -123,17 +103,13 @@ export function createS2sTransport(opts: S2sTransportOptions): Transport {
|
|
|
123
103
|
}
|
|
124
104
|
|
|
125
105
|
function canResumeAfter(code: number): boolean {
|
|
126
|
-
|
|
127
|
-
if (providerSessionId === null) return false;
|
|
128
|
-
if (reconnectInFlight) return false;
|
|
129
|
-
return sessionReadyAt > 0 && Date.now() - sessionReadyAt < RESUME_WINDOW_MS;
|
|
106
|
+
return TRANSIENT_CLOSE_CODES.has(code) && providerSessionId !== null && !reconnecting;
|
|
130
107
|
}
|
|
131
108
|
|
|
132
109
|
function emitFatalClose(code: number, reason: string, wasReconnecting: boolean): void {
|
|
133
110
|
if (wasReconnecting) {
|
|
134
111
|
// Fresh resume socket closed before session.ready — resume failed.
|
|
135
112
|
reconnecting = false;
|
|
136
|
-
reconnectInFlight = false;
|
|
137
113
|
opts.callbacks.onError("connection", `S2S resume failed (code=${code})`);
|
|
138
114
|
return;
|
|
139
115
|
}
|
|
@@ -152,7 +128,6 @@ export function createS2sTransport(opts: S2sTransportOptions): Transport {
|
|
|
152
128
|
}
|
|
153
129
|
|
|
154
130
|
function startResume(prevId: string, code: number, reason: string): void {
|
|
155
|
-
reconnectInFlight = true;
|
|
156
131
|
reconnecting = true;
|
|
157
132
|
log.warn("S2S unexpected close — attempting resume", {
|
|
158
133
|
sid: opts.sid,
|
|
@@ -168,7 +143,6 @@ export function createS2sTransport(opts: S2sTransportOptions): Transport {
|
|
|
168
143
|
}
|
|
169
144
|
void resume(prevId).catch((err: unknown) => {
|
|
170
145
|
reconnecting = false;
|
|
171
|
-
reconnectInFlight = false;
|
|
172
146
|
const msg = err instanceof Error ? err.message : String(err);
|
|
173
147
|
log.warn("S2S resume failed", { sid: opts.sid, error: msg });
|
|
174
148
|
opts.callbacks.onError("connection", `S2S resume failed: ${msg}`);
|
|
@@ -181,13 +155,11 @@ export function createS2sTransport(opts: S2sTransportOptions): Transport {
|
|
|
181
155
|
return;
|
|
182
156
|
}
|
|
183
157
|
const wasReconnecting = reconnecting;
|
|
184
|
-
|
|
158
|
+
const prevId = providerSessionId;
|
|
159
|
+
if (!canResumeAfter(code) || prevId === null) {
|
|
185
160
|
emitFatalClose(code, reason, wasReconnecting);
|
|
186
161
|
return;
|
|
187
162
|
}
|
|
188
|
-
// canResumeAfter ensures providerSessionId !== null; capture as const.
|
|
189
|
-
const prevId = providerSessionId;
|
|
190
|
-
if (prevId === null) return;
|
|
191
163
|
startResume(prevId, code, reason);
|
|
192
164
|
}
|
|
193
165
|
|
|
@@ -197,7 +169,7 @@ export function createS2sTransport(opts: S2sTransportOptions): Transport {
|
|
|
197
169
|
config: opts.s2sConfig,
|
|
198
170
|
createWebSocket: createWs,
|
|
199
171
|
logger: log,
|
|
200
|
-
|
|
172
|
+
sid: opts.sid,
|
|
201
173
|
callbacks: buildCallbacks(),
|
|
202
174
|
});
|
|
203
175
|
if (closing) {
|
|
@@ -3,20 +3,16 @@ import type { Transport, TransportCallbacks } from "./types.ts";
|
|
|
3
3
|
|
|
4
4
|
describe("Transport types", () => {
|
|
5
5
|
test("file compiles", () => {
|
|
6
|
-
|
|
6
|
+
const noop = (): void => undefined;
|
|
7
7
|
const stub: Transport = {
|
|
8
8
|
start: () => Promise.resolve(),
|
|
9
9
|
stop: () => Promise.resolve(),
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
sendToolResult: () => {},
|
|
14
|
-
// biome-ignore lint/suspicious/noEmptyBlockStatements: intentional no-op stub
|
|
15
|
-
cancelReply: () => {},
|
|
10
|
+
sendUserAudio: noop,
|
|
11
|
+
sendToolResult: noop,
|
|
12
|
+
cancelReply: noop,
|
|
16
13
|
};
|
|
17
14
|
expect(stub).toBeDefined();
|
|
18
15
|
|
|
19
|
-
// Ensure TransportCallbacks is referenced (type-only check).
|
|
20
16
|
type _CB = TransportCallbacks;
|
|
21
17
|
});
|
|
22
18
|
});
|
|
@@ -5,8 +5,9 @@ import { describe, expect, test } from "vitest";
|
|
|
5
5
|
import { createUnstorageKv } from "./unstorage-kv.ts";
|
|
6
6
|
|
|
7
7
|
function makeKv(prefix?: string) {
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
return createUnstorageKv(
|
|
9
|
+
prefix === undefined ? { storage: createStorage() } : { storage: createStorage(), prefix },
|
|
10
|
+
);
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
describe("createUnstorageKv", () => {
|
package/host/unstorage-kv.ts
CHANGED
|
@@ -1,40 +1,14 @@
|
|
|
1
1
|
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
-
/**
|
|
3
|
-
* Key-value store backed by unstorage.
|
|
4
|
-
*
|
|
5
|
-
* Works with any unstorage driver (memory, fs, S3/R2, etc.).
|
|
6
|
-
*/
|
|
7
2
|
|
|
8
|
-
import { prefixStorage, type Storage } from "unstorage";
|
|
3
|
+
import { prefixStorage, type Storage, type StorageValue } from "unstorage";
|
|
9
4
|
import { MAX_VALUE_SIZE } from "../sdk/constants.ts";
|
|
10
5
|
import type { Kv } from "../sdk/kv.ts";
|
|
11
6
|
|
|
12
|
-
/**
|
|
13
|
-
* Options for creating an unstorage-backed KV store.
|
|
14
|
-
*/
|
|
15
7
|
export type UnstorageKvOptions = {
|
|
16
|
-
/** Configured unstorage Storage instance. */
|
|
17
8
|
storage: Storage;
|
|
18
|
-
/** Key prefix prepended to all operations (e.g. `"agents/my-agent/kv"`). */
|
|
19
9
|
prefix?: string;
|
|
20
10
|
};
|
|
21
11
|
|
|
22
|
-
/**
|
|
23
|
-
* Create a KV store backed by any unstorage driver.
|
|
24
|
-
*
|
|
25
|
-
* @param options - See {@link UnstorageKvOptions}.
|
|
26
|
-
* @returns A {@link Kv} instance.
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* ```ts
|
|
30
|
-
* import { createStorage } from "unstorage";
|
|
31
|
-
* import { createUnstorageKv } from "@alexkroman1/aai/unstorage-kv";
|
|
32
|
-
*
|
|
33
|
-
* const kv = createUnstorageKv({ storage: createStorage() });
|
|
34
|
-
* await kv.set("greeting", "hello");
|
|
35
|
-
* const value = await kv.get<string>("greeting"); // "hello"
|
|
36
|
-
* ```
|
|
37
|
-
*/
|
|
38
12
|
export function createUnstorageKv(options: UnstorageKvOptions): Kv {
|
|
39
13
|
const store = options.prefix ? prefixStorage(options.storage, options.prefix) : options.storage;
|
|
40
14
|
|
|
@@ -45,16 +19,12 @@ export function createUnstorageKv(options: UnstorageKvOptions): Kv {
|
|
|
45
19
|
},
|
|
46
20
|
|
|
47
21
|
async set(key: string, value: unknown, setOptions?: { expireIn?: number }): Promise<void> {
|
|
48
|
-
|
|
49
|
-
if (serialized.length > MAX_VALUE_SIZE) {
|
|
22
|
+
if (JSON.stringify(value).length > MAX_VALUE_SIZE) {
|
|
50
23
|
throw new Error(`Value exceeds max size of ${MAX_VALUE_SIZE} bytes`);
|
|
51
24
|
}
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
} else {
|
|
56
|
-
await store.setItem(key, storable);
|
|
57
|
-
}
|
|
25
|
+
const expireIn = setOptions?.expireIn;
|
|
26
|
+
const ttlOption = expireIn && expireIn > 0 ? { ttl: Math.ceil(expireIn / 1000) } : undefined;
|
|
27
|
+
await store.setItem(key, value as StorageValue, ttlOption);
|
|
58
28
|
},
|
|
59
29
|
|
|
60
30
|
async delete(keys: string | string[]): Promise<void> {
|