@alexkroman1/aai 1.5.0 → 1.6.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 (50) hide show
  1. package/.turbo/turbo-build.log +17 -17
  2. package/CHANGELOG.md +18 -0
  3. package/dist/{_internal-types-3p3OJZPb.js → _internal-types-DFL07G3f.js} +2 -0
  4. package/dist/host/providers/resolve.d.ts +7 -1
  5. package/dist/host/providers/stt/elevenlabs.d.ts +16 -0
  6. package/dist/host/providers/stt/soniox.d.ts +25 -0
  7. package/dist/host/runtime-barrel.js +534 -77
  8. package/dist/sdk/_internal-types.d.ts +2 -0
  9. package/dist/sdk/manifest-barrel.js +1 -1
  10. package/dist/sdk/providers/llm/google.d.ts +22 -0
  11. package/dist/sdk/providers/llm/groq.d.ts +21 -0
  12. package/dist/sdk/providers/llm/mistral.d.ts +21 -0
  13. package/dist/sdk/providers/llm/openai.d.ts +21 -0
  14. package/dist/sdk/providers/llm/xai.d.ts +21 -0
  15. package/dist/sdk/providers/llm-barrel.d.ts +5 -0
  16. package/dist/sdk/providers/llm-barrel.js +2 -2
  17. package/dist/sdk/providers/stt/elevenlabs.d.ts +36 -0
  18. package/dist/sdk/providers/stt/soniox.d.ts +37 -0
  19. package/dist/sdk/providers/stt-barrel.d.ts +2 -0
  20. package/dist/sdk/providers/stt-barrel.js +2 -2
  21. package/dist/soniox-DCQ3GqJq.js +69 -0
  22. package/dist/xai-jfQsxxPZ.js +55 -0
  23. package/host/builtin-tools.ts +1 -0
  24. package/host/providers/resolve.test.ts +110 -0
  25. package/host/providers/resolve.ts +113 -10
  26. package/host/providers/stt/elevenlabs.test.ts +200 -0
  27. package/host/providers/stt/elevenlabs.ts +145 -0
  28. package/host/providers/stt/soniox.test.ts +338 -0
  29. package/host/providers/stt/soniox.ts +239 -0
  30. package/host/runtime.test.ts +3 -1
  31. package/host/to-vercel-tools.test.ts +9 -1
  32. package/host/transports/pipeline-transport.test.ts +93 -0
  33. package/host/transports/pipeline-transport.ts +53 -30
  34. package/host/transports/s2s-transport.test.ts +222 -2
  35. package/host/transports/s2s-transport.ts +176 -40
  36. package/package.json +35 -2
  37. package/sdk/__snapshots__/schema-shapes.test.ts.snap +1 -0
  38. package/sdk/_internal-types.ts +3 -0
  39. package/sdk/providers/llm/google.ts +30 -0
  40. package/sdk/providers/llm/groq.ts +29 -0
  41. package/sdk/providers/llm/mistral.ts +29 -0
  42. package/sdk/providers/llm/openai.ts +29 -0
  43. package/sdk/providers/llm/xai.ts +29 -0
  44. package/sdk/providers/llm-barrel.ts +10 -0
  45. package/sdk/providers/stt/elevenlabs.ts +44 -0
  46. package/sdk/providers/stt/soniox.ts +45 -0
  47. package/sdk/providers/stt-barrel.ts +4 -0
  48. package/sdk/schema-alignment.test.ts +18 -6
  49. package/dist/anthropic-CcLZygAr.js +0 -10
  50. package/dist/assemblyai-C969QGi4.js +0 -35
@@ -1,5 +1,7 @@
1
- import { describe, expect, test, vi } from "vitest";
2
- import { createS2sTransport } from "./s2s-transport.ts";
1
+ import { afterEach, describe, expect, test, vi } from "vitest";
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
5
  import type { TransportCallbacks } from "./types.ts";
4
6
 
5
7
  function makeCallbacks(): TransportCallbacks {
@@ -15,6 +17,7 @@ function makeCallbacks(): TransportCallbacks {
15
17
  onError: vi.fn(),
16
18
  onSpeechStarted: vi.fn(),
17
19
  onSpeechStopped: vi.fn(),
20
+ onSessionReady: vi.fn(),
18
21
  };
19
22
  }
20
23
 
@@ -54,3 +57,220 @@ describe("S2sTransport", () => {
54
57
  expect(close).toHaveBeenCalled();
55
58
  });
56
59
  });
60
+
61
+ // ─── Reconnect tests ────────────────────────────────────────────────────────
62
+
63
+ /** Capture the S2sCallbacks that the transport hands to connectS2s. */
64
+ function setupSpiedTransport(): {
65
+ callbacks: TransportCallbacks;
66
+ handles: S2sHandle[];
67
+ capturedCallbacks: S2sCallbacks[];
68
+ spy: ReturnType<typeof vi.spyOn>;
69
+ } {
70
+ const handles: S2sHandle[] = [];
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
+ };
86
+ }
87
+
88
+ describe("S2sTransport reconnect", () => {
89
+ afterEach(() => {
90
+ vi.restoreAllMocks();
91
+ });
92
+
93
+ test("attempts session.resume on transient close (1005) inside the resume window", async () => {
94
+ 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
+ });
106
+ await t.start();
107
+
108
+ // Establish session, start a reply, then drop the socket.
109
+ const cb1 = capturedCallbacks[0];
110
+ if (!cb1) throw new Error("expected first callbacks");
111
+ cb1.onSessionReady("sess_abc");
112
+ cb1.onReplyStarted("rep_1");
113
+ cb1.onClose(1005, "");
114
+
115
+ // Wait for the async resume() to fire connectS2s a second time.
116
+ await vi.waitFor(() => {
117
+ expect(handles.length).toBe(2);
118
+ });
119
+
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");
123
+ expect(newHandle.resumeSession).toHaveBeenCalledWith("sess_abc");
124
+
125
+ // The in-flight reply was unblocked via onCancelled, NOT a fatal error.
126
+ expect(callbacks.onCancelled).toHaveBeenCalledOnce();
127
+ expect(callbacks.onError).not.toHaveBeenCalled();
128
+ });
129
+
130
+ test("does NOT reconnect on fatal close codes (1008 unauthorized)", async () => {
131
+ 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
+ });
143
+ await t.start();
144
+
145
+ const cb1 = capturedCallbacks[0];
146
+ if (!cb1) throw new Error("expected first callbacks");
147
+ cb1.onSessionReady("sess_abc");
148
+ cb1.onReplyStarted("rep_1");
149
+ cb1.onClose(1008, "unauthorized");
150
+
151
+ // No reconnect — only one connectS2s call total.
152
+ await new Promise((resolve) => setTimeout(resolve, 5));
153
+ expect(handles.length).toBe(1);
154
+ // Fatal error surfaces, since a reply was in flight.
155
+ expect(callbacks.onError).toHaveBeenCalledWith(
156
+ "connection",
157
+ expect.stringContaining("S2S closed mid-reply"),
158
+ );
159
+ });
160
+
161
+ test("does NOT reconnect when stop() was called", async () => {
162
+ 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
+ });
174
+ await t.start();
175
+
176
+ const cb1 = capturedCallbacks[0];
177
+ if (!cb1) throw new Error("expected first callbacks");
178
+ cb1.onSessionReady("sess_abc");
179
+ await t.stop();
180
+
181
+ // Simulate the upstream's close arriving after stop() — it should be
182
+ // treated as a clean shutdown, not a transient drop worth resuming.
183
+ cb1.onClose(1005, "");
184
+
185
+ await new Promise((resolve) => setTimeout(resolve, 5));
186
+ expect(handles.length).toBe(1);
187
+ expect(callbacks.onError).not.toHaveBeenCalled();
188
+ });
189
+
190
+ test("surfaces resume failure when the resumed socket also closes", async () => {
191
+ 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
+ });
203
+ await t.start();
204
+
205
+ capturedCallbacks[0]?.onSessionReady("sess_abc");
206
+ capturedCallbacks[0]?.onReplyStarted("rep_1");
207
+ capturedCallbacks[0]?.onClose(1005, "");
208
+
209
+ await vi.waitFor(() => expect(handles.length).toBe(2));
210
+
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");
214
+ cb2.onClose(1006, "");
215
+
216
+ expect(callbacks.onError).toHaveBeenCalledWith(
217
+ "connection",
218
+ expect.stringContaining("resume failed"),
219
+ );
220
+ });
221
+
222
+ test("surfaces resume failure when server reports session_not_found", async () => {
223
+ 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
+ });
235
+ await t.start();
236
+
237
+ capturedCallbacks[0]?.onSessionReady("sess_abc");
238
+ capturedCallbacks[0]?.onClose(1005, "");
239
+
240
+ await vi.waitFor(() => expect(handles.length).toBe(2));
241
+
242
+ capturedCallbacks[1]?.onSessionExpired();
243
+
244
+ expect(callbacks.onError).toHaveBeenCalledWith(
245
+ "connection",
246
+ expect.stringContaining("session expired"),
247
+ );
248
+ });
249
+
250
+ test("after a successful resume, a later transient drop also resumes", async () => {
251
+ 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
+ });
263
+ await t.start();
264
+
265
+ // First connection establishes, drops, resumes, becomes ready again.
266
+ capturedCallbacks[0]?.onSessionReady("sess_abc");
267
+ capturedCallbacks[0]?.onClose(1005, "");
268
+ await vi.waitFor(() => expect(handles.length).toBe(2));
269
+ capturedCallbacks[1]?.onSessionReady("sess_abc");
270
+
271
+ // Second drop — should trigger another resume attempt.
272
+ capturedCallbacks[1]?.onClose(1006, "");
273
+ await vi.waitFor(() => expect(handles.length).toBe(3));
274
+ expect(handles[2]?.resumeSession).toHaveBeenCalledWith("sess_abc");
275
+ });
276
+ });
@@ -7,6 +7,7 @@ import {
7
7
  type CreateS2sWebSocket,
8
8
  connectS2s,
9
9
  defaultCreateS2sWebSocket,
10
+ type S2sCallbacks,
10
11
  type S2sHandle,
11
12
  type S2sSessionConfig,
12
13
  type S2sToolSchema,
@@ -28,11 +29,184 @@ export type S2sTransportOptions = {
28
29
  logger?: Logger;
29
30
  };
30
31
 
32
+ /**
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.
37
+ */
38
+ const TRANSIENT_CLOSE_CODES = new Set<number>([
39
+ 1005, // No Status Received (abnormal close, no frame)
40
+ 1006, // Abnormal Closure (no close frame at all)
41
+ 1011, // Internal Server Error
42
+ 3005, // Session Cancelled (unknown server error)
43
+ ]);
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
+
31
52
  export function createS2sTransport(opts: S2sTransportOptions): Transport {
32
53
  const log = opts.logger ?? consoleLogger;
33
54
  const createWs = opts.createWebSocket ?? defaultCreateS2sWebSocket;
34
55
  let handle: S2sHandle | null = null;
35
56
  let currentReplyId: string | null = null;
57
+ /** Most recent `session.ready` ID — present once the upstream session is established. */
58
+ 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
+ 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
+ */
68
+ 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
+
76
+ function buildCallbacks(): S2sCallbacks {
77
+ return {
78
+ onSessionReady: (id) => {
79
+ providerSessionId = id;
80
+ sessionReadyAt = Date.now();
81
+ if (reconnecting) {
82
+ reconnecting = false;
83
+ reconnectInFlight = false;
84
+ log.info("S2S resumed", { sid: opts.sid, sessionId: id });
85
+ }
86
+ opts.callbacks.onSessionReady?.(id);
87
+ },
88
+ onReplyStarted: (replyId) => {
89
+ currentReplyId = replyId;
90
+ opts.callbacks.onReplyStarted(replyId);
91
+ },
92
+ onReplyDone: () => {
93
+ currentReplyId = null;
94
+ opts.callbacks.onReplyDone();
95
+ },
96
+ onCancelled: () => {
97
+ currentReplyId = null;
98
+ opts.callbacks.onCancelled();
99
+ },
100
+ onAudio: (bytes) => opts.callbacks.onAudioChunk(bytes),
101
+ onUserTranscript: opts.callbacks.onUserTranscript,
102
+ onAgentTranscript: opts.callbacks.onAgentTranscript,
103
+ onToolCall: opts.callbacks.onToolCall,
104
+ onSpeechStarted: opts.callbacks.onSpeechStarted,
105
+ onSpeechStopped: opts.callbacks.onSpeechStopped,
106
+ 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.
110
+ if (reconnecting) {
111
+ reconnecting = false;
112
+ reconnectInFlight = false;
113
+ log.warn("S2S resume rejected: session expired", { sid: opts.sid });
114
+ opts.callbacks.onError("connection", "S2S resume failed: session expired");
115
+ return;
116
+ }
117
+ log.info("S2S session expired", { sid: opts.sid });
118
+ handle?.close();
119
+ },
120
+ onError: (err) => opts.callbacks.onError("internal", err.message),
121
+ onClose: (code, reason) => handleClose(code, reason),
122
+ };
123
+ }
124
+
125
+ 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;
130
+ }
131
+
132
+ function emitFatalClose(code: number, reason: string, wasReconnecting: boolean): void {
133
+ if (wasReconnecting) {
134
+ // Fresh resume socket closed before session.ready — resume failed.
135
+ reconnecting = false;
136
+ reconnectInFlight = false;
137
+ opts.callbacks.onError("connection", `S2S resume failed (code=${code})`);
138
+ return;
139
+ }
140
+ if (currentReplyId !== null) {
141
+ log.warn("S2S closed with active reply", {
142
+ sid: opts.sid,
143
+ agent: opts.agent,
144
+ activeReplyId: currentReplyId,
145
+ code,
146
+ reason,
147
+ });
148
+ opts.callbacks.onError("connection", `S2S closed mid-reply (code=${code})`);
149
+ return;
150
+ }
151
+ log.info("S2S closed", { code, reason });
152
+ }
153
+
154
+ function startResume(prevId: string, code: number, reason: string): void {
155
+ reconnectInFlight = true;
156
+ reconnecting = true;
157
+ log.warn("S2S unexpected close — attempting resume", {
158
+ sid: opts.sid,
159
+ agent: opts.agent,
160
+ code,
161
+ reason,
162
+ prevSessionId: prevId,
163
+ });
164
+ // The in-flight reply is gone; unblock SessionCore's turn promise.
165
+ if (currentReplyId !== null) {
166
+ currentReplyId = null;
167
+ opts.callbacks.onCancelled();
168
+ }
169
+ void resume(prevId).catch((err: unknown) => {
170
+ reconnecting = false;
171
+ reconnectInFlight = false;
172
+ const msg = err instanceof Error ? err.message : String(err);
173
+ log.warn("S2S resume failed", { sid: opts.sid, error: msg });
174
+ opts.callbacks.onError("connection", `S2S resume failed: ${msg}`);
175
+ });
176
+ }
177
+
178
+ function handleClose(code: number, reason: string): void {
179
+ if (closing) {
180
+ log.info("S2S closed", { code, reason });
181
+ return;
182
+ }
183
+ const wasReconnecting = reconnecting;
184
+ if (!canResumeAfter(code)) {
185
+ emitFatalClose(code, reason, wasReconnecting);
186
+ return;
187
+ }
188
+ // canResumeAfter ensures providerSessionId !== null; capture as const.
189
+ const prevId = providerSessionId;
190
+ if (prevId === null) return;
191
+ startResume(prevId, code, reason);
192
+ }
193
+
194
+ async function resume(prevSessionId: string): Promise<void> {
195
+ const newHandle = await _internals.connectS2s({
196
+ apiKey: opts.apiKey,
197
+ config: opts.s2sConfig,
198
+ createWebSocket: createWs,
199
+ logger: log,
200
+ ...(opts.sid !== undefined ? { sid: opts.sid } : {}),
201
+ callbacks: buildCallbacks(),
202
+ });
203
+ if (closing) {
204
+ newHandle.close();
205
+ return;
206
+ }
207
+ handle = newHandle;
208
+ newHandle.resumeSession(prevSessionId);
209
+ }
36
210
 
37
211
  async function start(): Promise<void> {
38
212
  handle = await _internals.connectS2s({
@@ -41,51 +215,13 @@ export function createS2sTransport(opts: S2sTransportOptions): Transport {
41
215
  createWebSocket: createWs,
42
216
  logger: log,
43
217
  sid: opts.sid,
44
- callbacks: {
45
- onSessionReady: (providerSessionId) => opts.callbacks.onSessionReady?.(providerSessionId),
46
- onReplyStarted: (replyId) => {
47
- currentReplyId = replyId;
48
- opts.callbacks.onReplyStarted(replyId);
49
- },
50
- onReplyDone: () => {
51
- currentReplyId = null;
52
- opts.callbacks.onReplyDone();
53
- },
54
- onCancelled: () => {
55
- currentReplyId = null;
56
- opts.callbacks.onCancelled();
57
- },
58
- onAudio: (bytes) => opts.callbacks.onAudioChunk(bytes),
59
- onUserTranscript: opts.callbacks.onUserTranscript,
60
- onAgentTranscript: opts.callbacks.onAgentTranscript,
61
- onToolCall: opts.callbacks.onToolCall,
62
- onSpeechStarted: opts.callbacks.onSpeechStarted,
63
- onSpeechStopped: opts.callbacks.onSpeechStopped,
64
- onSessionExpired: () => {
65
- log.info("S2S session expired", { sid: opts.sid });
66
- handle?.close();
67
- },
68
- onError: (err) => opts.callbacks.onError("internal", err.message),
69
- onClose: (code, reason) => {
70
- if (currentReplyId !== null) {
71
- log.warn("S2S closed with active reply", {
72
- sid: opts.sid,
73
- agent: opts.agent,
74
- activeReplyId: currentReplyId,
75
- code,
76
- reason,
77
- });
78
- opts.callbacks.onError("connection", `S2S closed mid-reply (code=${code})`);
79
- } else {
80
- log.info("S2S closed", { code, reason });
81
- }
82
- },
83
- },
218
+ callbacks: buildCallbacks(),
84
219
  });
85
220
  handle.updateSession(opts.sessionConfig);
86
221
  }
87
222
 
88
223
  async function stop(): Promise<void> {
224
+ closing = true;
89
225
  handle?.close();
90
226
  handle = null;
91
227
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexkroman1/aai",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -40,9 +40,9 @@
40
40
  }
41
41
  },
42
42
  "dependencies": {
43
- "@ai-sdk/anthropic": "^3.0.0",
44
43
  "@cartesia/cartesia-js": "^3.0.0",
45
44
  "@deepgram/sdk": "^5.0.0",
45
+ "@elevenlabs/elevenlabs-js": "^2.43.0",
46
46
  "ai": "^6.0.161",
47
47
  "assemblyai": "^4.30.0",
48
48
  "escape-html": "^1.0.3",
@@ -54,8 +54,41 @@
54
54
  "ws": "^8.20.0",
55
55
  "zod": "^4.3.6"
56
56
  },
57
+ "peerDependencies": {
58
+ "@ai-sdk/anthropic": "^3.0.0",
59
+ "@ai-sdk/google": "^3.0.0",
60
+ "@ai-sdk/groq": "^3.0.0",
61
+ "@ai-sdk/mistral": "^3.0.0",
62
+ "@ai-sdk/openai": "^3.0.0",
63
+ "@ai-sdk/xai": "^3.0.0"
64
+ },
65
+ "peerDependenciesMeta": {
66
+ "@ai-sdk/anthropic": {
67
+ "optional": true
68
+ },
69
+ "@ai-sdk/google": {
70
+ "optional": true
71
+ },
72
+ "@ai-sdk/groq": {
73
+ "optional": true
74
+ },
75
+ "@ai-sdk/mistral": {
76
+ "optional": true
77
+ },
78
+ "@ai-sdk/openai": {
79
+ "optional": true
80
+ },
81
+ "@ai-sdk/xai": {
82
+ "optional": true
83
+ }
84
+ },
57
85
  "devDependencies": {
86
+ "@ai-sdk/anthropic": "^3.0.0",
87
+ "@ai-sdk/google": "^3.0.0",
88
+ "@ai-sdk/groq": "^3.0.0",
89
+ "@ai-sdk/mistral": "^3.0.0",
58
90
  "@ai-sdk/openai": "^3.0.0",
91
+ "@ai-sdk/xai": "^3.0.0",
59
92
  "@types/escape-html": "^1.0.4",
60
93
  "@types/html-to-text": "^9.0.4",
61
94
  "@types/json-schema": "^7.0.15",
@@ -22,6 +22,7 @@ exports[`manifest schema shapes > ToolSchemaSchema shape 1`] = `
22
22
  "description",
23
23
  "name",
24
24
  "parameters",
25
+ "type",
25
26
  ]
26
27
  `;
27
28
 
@@ -128,6 +128,7 @@ export function toAgentConfig(src: AgentConfigSource): AgentConfig {
128
128
  * etc.) — the Vercel AI SDK wraps it via `jsonSchema()`.
129
129
  */
130
130
  export const ToolSchemaSchema = z.object({
131
+ type: z.literal("function"),
131
132
  name: z.string().min(1),
132
133
  description: z.string().min(1),
133
134
  parameters: z.record(z.string(), z.unknown()),
@@ -135,6 +136,7 @@ export const ToolSchemaSchema = z.object({
135
136
 
136
137
  /** Serialized tool schema — derived from {@link ToolSchemaSchema}. */
137
138
  export type ToolSchema = {
139
+ type: "function";
138
140
  name: string;
139
141
  description: string;
140
142
  parameters: JSONSchema7;
@@ -151,6 +153,7 @@ export const EMPTY_PARAMS = z.object({});
151
153
  */
152
154
  export function agentToolsToSchemas(tools: Readonly<Record<string, ToolDef>>): ToolSchema[] {
153
155
  return Object.entries(tools).map(([name, def]) => ({
156
+ type: "function",
154
157
  name,
155
158
  description: def.description,
156
159
  parameters: z.toJSONSchema(def.parameters ?? EMPTY_PARAMS) as JSONSchema7,
@@ -0,0 +1,30 @@
1
+ // Copyright 2026 the AAI authors. MIT license.
2
+ /**
3
+ * Google (Gemini) LLM factory — returns a pure descriptor.
4
+ *
5
+ * Users call this in place of importing from `@ai-sdk/google` directly,
6
+ * so agent bundles don't drag the Google SDK into the guest sandbox.
7
+ *
8
+ * The host-side resolver in `host/providers/resolve.ts` builds a real
9
+ * Vercel AI SDK `LanguageModel` from this descriptor during
10
+ * `createRuntime`, using `GOOGLE_GENERATIVE_AI_API_KEY` from the
11
+ * agent's env.
12
+ */
13
+
14
+ import type { LlmProvider } from "../../providers.ts";
15
+
16
+ export const GOOGLE_KIND = "google" as const;
17
+
18
+ export interface GoogleOptions {
19
+ /** Google Gemini model id, e.g. `"gemini-2.0-flash"`. */
20
+ model: string;
21
+ }
22
+
23
+ export type GoogleProvider = LlmProvider & {
24
+ readonly kind: typeof GOOGLE_KIND;
25
+ readonly options: GoogleOptions;
26
+ };
27
+
28
+ export function google(opts: GoogleOptions): GoogleProvider {
29
+ return { kind: GOOGLE_KIND, options: { ...opts } };
30
+ }
@@ -0,0 +1,29 @@
1
+ // Copyright 2026 the AAI authors. MIT license.
2
+ /**
3
+ * Groq LLM factory — returns a pure descriptor.
4
+ *
5
+ * Users call this in place of importing from `@ai-sdk/groq` directly,
6
+ * so agent bundles don't drag the Groq SDK into the guest sandbox.
7
+ *
8
+ * The host-side resolver in `host/providers/resolve.ts` builds a real
9
+ * Vercel AI SDK `LanguageModel` from this descriptor during
10
+ * `createRuntime`, using `GROQ_API_KEY` from the agent's env.
11
+ */
12
+
13
+ import type { LlmProvider } from "../../providers.ts";
14
+
15
+ export const GROQ_KIND = "groq" as const;
16
+
17
+ export interface GroqOptions {
18
+ /** Groq model id, e.g. `"llama-3.3-70b-versatile"`. */
19
+ model: string;
20
+ }
21
+
22
+ export type GroqProvider = LlmProvider & {
23
+ readonly kind: typeof GROQ_KIND;
24
+ readonly options: GroqOptions;
25
+ };
26
+
27
+ export function groq(opts: GroqOptions): GroqProvider {
28
+ return { kind: GROQ_KIND, options: { ...opts } };
29
+ }
@@ -0,0 +1,29 @@
1
+ // Copyright 2026 the AAI authors. MIT license.
2
+ /**
3
+ * Mistral LLM factory — returns a pure descriptor.
4
+ *
5
+ * Users call this in place of importing from `@ai-sdk/mistral` directly,
6
+ * so agent bundles don't drag the Mistral SDK into the guest sandbox.
7
+ *
8
+ * The host-side resolver in `host/providers/resolve.ts` builds a real
9
+ * Vercel AI SDK `LanguageModel` from this descriptor during
10
+ * `createRuntime`, using `MISTRAL_API_KEY` from the agent's env.
11
+ */
12
+
13
+ import type { LlmProvider } from "../../providers.ts";
14
+
15
+ export const MISTRAL_KIND = "mistral" as const;
16
+
17
+ export interface MistralOptions {
18
+ /** Mistral model id, e.g. `"mistral-large-latest"`. */
19
+ model: string;
20
+ }
21
+
22
+ export type MistralProvider = LlmProvider & {
23
+ readonly kind: typeof MISTRAL_KIND;
24
+ readonly options: MistralOptions;
25
+ };
26
+
27
+ export function mistral(opts: MistralOptions): MistralProvider {
28
+ return { kind: MISTRAL_KIND, options: { ...opts } };
29
+ }
@@ -0,0 +1,29 @@
1
+ // Copyright 2026 the AAI authors. MIT license.
2
+ /**
3
+ * OpenAI LLM factory — returns a pure descriptor.
4
+ *
5
+ * Users call this in place of importing from `@ai-sdk/openai` directly,
6
+ * so agent bundles don't drag the OpenAI SDK into the guest sandbox.
7
+ *
8
+ * The host-side resolver in `host/providers/resolve.ts` builds a real
9
+ * Vercel AI SDK `LanguageModel` from this descriptor during
10
+ * `createRuntime`, using `OPENAI_API_KEY` from the agent's env.
11
+ */
12
+
13
+ import type { LlmProvider } from "../../providers.ts";
14
+
15
+ export const OPENAI_KIND = "openai" as const;
16
+
17
+ export interface OpenAIOptions {
18
+ /** OpenAI model id, e.g. `"gpt-4o"`, `"gpt-4o-mini"`. */
19
+ model: string;
20
+ }
21
+
22
+ export type OpenAIProvider = LlmProvider & {
23
+ readonly kind: typeof OPENAI_KIND;
24
+ readonly options: OpenAIOptions;
25
+ };
26
+
27
+ export function openai(opts: OpenAIOptions): OpenAIProvider {
28
+ return { kind: OPENAI_KIND, options: { ...opts } };
29
+ }