@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.
Files changed (133) hide show
  1. package/.turbo/turbo-build.log +11 -9
  2. package/CHANGELOG.md +10 -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 +619 -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 +43 -0
  22. package/dist/host/unstorage-kv.d.ts +0 -26
  23. package/dist/index.js +3 -3
  24. package/dist/openai-realtime-cjPAHMMx.js +10 -0
  25. package/dist/sdk/_internal-types.d.ts +6 -55
  26. package/dist/sdk/allowed-hosts.d.ts +4 -3
  27. package/dist/sdk/constants.d.ts +4 -29
  28. package/dist/sdk/define.d.ts +7 -4
  29. package/dist/sdk/kv.d.ts +13 -37
  30. package/dist/sdk/manifest-barrel.js +1 -1
  31. package/dist/sdk/manifest.d.ts +8 -2
  32. package/dist/sdk/protocol.js +1 -1
  33. package/dist/sdk/providers/s2s/openai-realtime.d.ts +17 -0
  34. package/dist/sdk/providers/s2s-barrel.d.ts +9 -0
  35. package/dist/sdk/providers/s2s-barrel.js +2 -0
  36. package/dist/sdk/providers/tts/rime.d.ts +1 -1
  37. package/dist/sdk/providers.d.ts +6 -2
  38. package/dist/sdk/types.d.ts +7 -1
  39. package/dist/{types-KUgezM6u.js → types-DOWVZhb9.js} +1 -7
  40. package/dist/{ws-upgrade-BeOQ7fXL.js → ws-upgrade-CG8-by1n.js} +2 -3
  41. package/host/_base64.ts +9 -0
  42. package/host/_mock-ws.ts +0 -65
  43. package/host/_pipeline-test-fakes.ts +19 -31
  44. package/host/_run-code.ts +10 -53
  45. package/host/_runtime-conformance.ts +3 -44
  46. package/host/_test-utils.ts +20 -42
  47. package/host/builtin-tools.test.ts +127 -222
  48. package/host/builtin-tools.ts +6 -10
  49. package/host/cleanup.test.ts +30 -73
  50. package/host/integration/pipeline-reference.integration.test.ts +12 -17
  51. package/host/integration.test.ts +0 -7
  52. package/host/memory-vector.test.ts +3 -1
  53. package/host/memory-vector.ts +16 -21
  54. package/host/pinecone-vector.test.ts +14 -17
  55. package/host/pinecone-vector.ts +10 -19
  56. package/host/providers/providers.test-d.ts +5 -3
  57. package/host/providers/resolve-kv.ts +23 -41
  58. package/host/providers/resolve-vector.ts +3 -12
  59. package/host/providers/resolve.test.ts +15 -28
  60. package/host/providers/resolve.ts +24 -24
  61. package/host/providers/stt/assemblyai.test.ts +2 -14
  62. package/host/providers/stt/assemblyai.ts +12 -35
  63. package/host/providers/stt/deepgram.test.ts +23 -83
  64. package/host/providers/stt/deepgram.ts +15 -40
  65. package/host/providers/stt/elevenlabs.test.ts +26 -38
  66. package/host/providers/stt/elevenlabs.ts +10 -9
  67. package/host/providers/stt/soniox.test.ts +35 -85
  68. package/host/providers/stt/soniox.ts +8 -53
  69. package/host/providers/tts/cartesia.test.ts +19 -58
  70. package/host/providers/tts/cartesia.ts +36 -66
  71. package/host/providers/tts/rime.test.ts +12 -38
  72. package/host/providers/tts/rime.ts +23 -86
  73. package/host/runtime-config.test.ts +9 -9
  74. package/host/runtime-config.ts +16 -22
  75. package/host/runtime.test.ts +111 -73
  76. package/host/runtime.ts +138 -86
  77. package/host/s2s.test.ts +92 -191
  78. package/host/s2s.ts +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 +371 -0
  90. package/host/transports/openai-realtime-transport.ts +319 -0
  91. package/host/transports/pipeline-transport.test.ts +125 -298
  92. package/host/transports/pipeline-transport.ts +20 -68
  93. package/host/transports/s2s-transport-fixtures.test.ts +31 -92
  94. package/host/transports/s2s-transport.test.ts +65 -134
  95. package/host/transports/s2s-transport.ts +15 -43
  96. package/host/transports/types.test.ts +4 -8
  97. package/host/unstorage-kv.test.ts +3 -2
  98. package/host/unstorage-kv.ts +5 -35
  99. package/host/ws-handler.test.ts +72 -176
  100. package/host/ws-handler.ts +6 -12
  101. package/package.json +6 -1
  102. package/sdk/__snapshots__/exports.test.ts.snap +7 -0
  103. package/sdk/__snapshots__/schema-shapes.test.ts.snap +1 -0
  104. package/sdk/_internal-types.test.ts +6 -9
  105. package/sdk/_internal-types.ts +16 -57
  106. package/sdk/_test-matchers.ts +25 -15
  107. package/sdk/allowed-hosts.test.ts +50 -114
  108. package/sdk/allowed-hosts.ts +8 -14
  109. package/sdk/constants.ts +5 -52
  110. package/sdk/define.test.ts +7 -6
  111. package/sdk/define.ts +7 -3
  112. package/sdk/exports.test.ts +6 -1
  113. package/sdk/kv.ts +13 -37
  114. package/sdk/manifest.test-d.ts +5 -0
  115. package/sdk/manifest.test.ts +61 -9
  116. package/sdk/manifest.ts +11 -11
  117. package/sdk/protocol-compat.test.ts +66 -98
  118. package/sdk/protocol-snapshot.test.ts +2 -16
  119. package/sdk/protocol.test.ts +13 -22
  120. package/sdk/providers/s2s/openai-realtime.ts +36 -0
  121. package/sdk/providers/s2s-barrel.ts +12 -0
  122. package/sdk/providers/tts/rime.ts +1 -1
  123. package/sdk/providers.ts +24 -5
  124. package/sdk/schema-alignment.test.ts +25 -73
  125. package/sdk/schema-shapes.test.ts +1 -29
  126. package/sdk/system-prompt.test.ts +0 -1
  127. package/sdk/system-prompt.ts +17 -19
  128. package/sdk/types-inference.test.ts +10 -36
  129. package/sdk/types.ts +7 -0
  130. package/sdk/ws-upgrade.test.ts +24 -23
  131. package/sdk/ws-upgrade.ts +2 -3
  132. package/tsdown.config.ts +8 -11
  133. package/dist/constants-C2nirZUI.js +0 -54
@@ -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 ws = Object.assign(new EventTarget(), {
42
+ const target = new EventTarget();
43
+ const ws = Object.assign(target, {
29
44
  readyState: 0,
30
45
  send,
31
46
  close,
32
- addEventListener: EventTarget.prototype.addEventListener as unknown as (
33
- type: string,
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
- (ws as unknown as EventTarget).dispatchEvent(new Event("open"));
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
- const spy = vi
73
- .spyOn(_internals, "connectS2s")
74
- .mockImplementation(async (opts: import("../s2s.ts").ConnectS2sOptions) => {
75
- capturedCallbacks.push(opts.callbacks);
76
- const h = makeMockHandle();
77
- handles.push(h);
78
- return h;
79
- });
80
- return {
81
- callbacks: makeCallbacks(),
82
- handles,
83
- capturedCallbacks,
84
- spy,
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
- // Establish session, start a reply, then drop the socket.
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
- // The new handle should have received resumeSession with the prior id.
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[0];
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[0];
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
- // Simulate the upstream's close arriving after stop() it should be
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[0]?.onSessionReady("sess_abc");
206
- capturedCallbacks[0]?.onReplyStarted("rep_1");
207
- capturedCallbacks[0]?.onClose(1005, "");
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
- // The resume socket also drops before its session.ready arrives.
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[0]?.onSessionReady("sess_abc");
238
- capturedCallbacks[0]?.onClose(1005, "");
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[1]?.onSessionExpired();
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
- // First connection establishes, drops, resumes, becomes ready again.
266
- capturedCallbacks[0]?.onSessionReady("sess_abc");
267
- capturedCallbacks[0]?.onClose(1005, "");
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
- // Second drop should trigger another resume attempt.
272
- capturedCallbacks[1]?.onClose(1006, "");
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[2]?.resumeSession).toHaveBeenCalledWith("sess_abc");
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. Per AssemblyAI's docs, sessions are
35
- * preserved for 30 s after disconnect, so resume is bounded by the window in
36
- * `RESUME_WINDOW_MS` below.
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
- * True while a `session.resume` round-trip is in flight (between sending
65
- * resume and the next `session.ready`). Used to distinguish a resume failure
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
- // The server told us the session no longer exists (most likely
108
- // session_not_found in response to our resume). Surface as fatal
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
- if (!TRANSIENT_CLOSE_CODES.has(code)) return false;
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
- if (!canResumeAfter(code)) {
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
- ...(opts.sid !== undefined ? { sid: opts.sid } : {}),
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
- // Types only; runtime check is trivial.
6
+ const noop = (): void => undefined;
7
7
  const stub: Transport = {
8
8
  start: () => Promise.resolve(),
9
9
  stop: () => Promise.resolve(),
10
- // biome-ignore lint/suspicious/noEmptyBlockStatements: intentional no-op stub
11
- sendUserAudio: () => {},
12
- // biome-ignore lint/suspicious/noEmptyBlockStatements: intentional no-op stub
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
- const opts = prefix != null ? { storage: createStorage(), prefix } : { storage: createStorage() };
9
- return createUnstorageKv(opts);
8
+ return createUnstorageKv(
9
+ prefix === undefined ? { storage: createStorage() } : { storage: createStorage(), prefix },
10
+ );
10
11
  }
11
12
 
12
13
  describe("createUnstorageKv", () => {
@@ -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
- const serialized = JSON.stringify(value);
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 storable = value as import("unstorage").StorageValue;
53
- if (setOptions?.expireIn && setOptions.expireIn > 0) {
54
- await store.setItem(key, storable, { ttl: Math.ceil(setOptions.expireIn / 1000) });
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> {