ai 7.0.101 → 7.0.102
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/CHANGELOG.md +30 -0
- package/dist/index.d.ts +82 -20
- package/dist/index.js +2010 -330
- package/dist/index.js.map +1 -1
- package/dist/internal/index.js +1 -1
- package/docs/03-agents/07-workflow-agent.mdx +1 -1
- package/docs/03-ai-sdk-core/36-realtime.mdx +41 -18
- package/docs/04-ai-sdk-ui/21-transport.mdx +1 -1
- package/docs/07-reference/02-ai-sdk-ui/05-use-realtime.mdx +364 -48
- package/docs/07-reference/04-ai-sdk-workflow/02-workflow-chat-transport.mdx +4 -4
- package/docs/07-reference/05-ai-sdk-errors/index.mdx +131 -36
- package/package.json +12 -12
- package/src/generate-text/execute-tools-from-stream.ts +7 -0
- package/src/generate-text/stream-text.ts +3 -1
- package/src/realtime/__fixtures__/fake-live-websocket.ts +71 -0
- package/src/realtime/__fixtures__/fake-realtime.ts +36 -0
- package/src/realtime/__fixtures__/fake-webrtc.ts +133 -0
- package/src/realtime/browser-realtime-audio.ts +107 -10
- package/src/realtime/browser-realtime-live-websocket.ts +247 -0
- package/src/realtime/browser-realtime-transport.ts +235 -69
- package/src/realtime/browser-realtime-webrtc.ts +582 -0
- package/src/realtime/encode-realtime-frame.ts +33 -0
- package/src/realtime/index.ts +1 -0
- package/src/realtime/realtime-attempt.ts +45 -0
- package/src/realtime/realtime-command-tracker.ts +81 -0
- package/src/realtime/realtime-event-channel.ts +170 -0
- package/src/realtime/realtime-event-reducer.ts +3 -0
- package/src/realtime/realtime-session-state.ts +65 -0
- package/src/realtime/realtime-session.ts +768 -218
- package/src/realtime/realtime-types.ts +1 -1
- package/src/realtime/validate-realtime-setup.ts +37 -0
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RealtimeEventChannel } from './realtime-event-channel';
|
|
2
|
+
import {
|
|
3
|
+
assertRealtimeFrameBudget,
|
|
4
|
+
encodeRealtimeFrame,
|
|
5
|
+
} from './encode-realtime-frame';
|
|
2
6
|
import type {
|
|
3
7
|
RealtimeClientEvent,
|
|
4
8
|
RealtimeModel,
|
|
@@ -9,135 +13,297 @@ export type BrowserRealtimeTransportOptions = {
|
|
|
9
13
|
model: RealtimeModel;
|
|
10
14
|
onServerEvent: (event: RealtimeServerEvent) => void | Promise<void>;
|
|
11
15
|
onError: (error: Error) => void;
|
|
12
|
-
|
|
16
|
+
onFatalError?: (error: Error, drain?: Promise<void>) => void;
|
|
17
|
+
onClosing?: () => void;
|
|
18
|
+
onClose: (error?: Error) => void;
|
|
13
19
|
};
|
|
14
20
|
|
|
21
|
+
function getCloseError(event: CloseEvent): Error | undefined {
|
|
22
|
+
if (event.code === 1000 && event.wasClean) return undefined;
|
|
23
|
+
return new Error(
|
|
24
|
+
`Realtime WebSocket closed unexpectedly (code ${event.code}${
|
|
25
|
+
event.reason === '' ? '' : `: ${event.reason}`
|
|
26
|
+
})`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
15
30
|
export class BrowserRealtimeTransport {
|
|
16
31
|
private readonly model: RealtimeModel;
|
|
32
|
+
private readonly continuous: boolean;
|
|
17
33
|
private readonly onServerEvent: BrowserRealtimeTransportOptions['onServerEvent'];
|
|
18
34
|
private readonly onError: BrowserRealtimeTransportOptions['onError'];
|
|
19
35
|
private readonly onClose: BrowserRealtimeTransportOptions['onClose'];
|
|
20
36
|
private ws: WebSocket | null = null;
|
|
21
|
-
private
|
|
37
|
+
private codec: RealtimeEventChannel | undefined;
|
|
38
|
+
private epoch = 0;
|
|
39
|
+
private drainTimer?: ReturnType<typeof setTimeout>;
|
|
40
|
+
private failing = false;
|
|
41
|
+
private closing = false;
|
|
22
42
|
|
|
23
|
-
constructor(options: BrowserRealtimeTransportOptions) {
|
|
43
|
+
constructor(private readonly options: BrowserRealtimeTransportOptions) {
|
|
24
44
|
this.model = options.model;
|
|
45
|
+
this.continuous = options.model.capabilities?.conversation === 'continuous';
|
|
25
46
|
this.onServerEvent = options.onServerEvent;
|
|
26
47
|
this.onError = options.onError;
|
|
27
48
|
this.onClose = options.onClose;
|
|
28
49
|
}
|
|
29
50
|
|
|
51
|
+
get isOpen(): boolean {
|
|
52
|
+
return (
|
|
53
|
+
!this.closing && !this.failing && this.ws?.readyState === WebSocket.OPEN
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
30
57
|
connect({
|
|
58
|
+
mode,
|
|
31
59
|
token,
|
|
32
60
|
url,
|
|
33
61
|
onOpen,
|
|
34
|
-
|
|
35
|
-
|
|
62
|
+
protocols,
|
|
63
|
+
}: (
|
|
64
|
+
| { mode: 'client-secret'; token: string; protocols?: never }
|
|
65
|
+
| { mode: 'relay'; token?: never; protocols?: string[] }
|
|
66
|
+
) & {
|
|
36
67
|
url: string;
|
|
37
|
-
onOpen: () => void
|
|
68
|
+
onOpen: () => void | Promise<void>;
|
|
38
69
|
}): void {
|
|
39
|
-
this.
|
|
40
|
-
this.ws = null;
|
|
70
|
+
this.disconnect();
|
|
41
71
|
|
|
42
|
-
const
|
|
43
|
-
|
|
72
|
+
const epoch = this.epoch;
|
|
73
|
+
if (
|
|
74
|
+
mode !== 'relay' &&
|
|
75
|
+
(mode !== 'client-secret' ||
|
|
76
|
+
typeof token !== 'string' ||
|
|
77
|
+
token.trim() === '')
|
|
78
|
+
)
|
|
79
|
+
throw new Error(
|
|
80
|
+
'Realtime client-secret connection requires a nonempty token',
|
|
81
|
+
);
|
|
82
|
+
const wsConfig =
|
|
83
|
+
mode === 'relay'
|
|
84
|
+
? { url, protocols }
|
|
85
|
+
: this.model.getWebSocketConfig?.({ token, url });
|
|
86
|
+
if (this.epoch !== epoch) return;
|
|
87
|
+
if (wsConfig == null)
|
|
88
|
+
throw new Error('Model does not support client-secret WebSockets');
|
|
89
|
+
const finalUrl = wsConfig.url;
|
|
90
|
+
const finalProtocols = wsConfig.protocols;
|
|
91
|
+
if (this.epoch !== epoch) return;
|
|
92
|
+
try {
|
|
93
|
+
if (typeof finalUrl !== 'string' || !/^wss?:\/\//i.test(finalUrl))
|
|
94
|
+
throw new Error('Invalid realtime WebSocket URL');
|
|
95
|
+
const parsed = new URL(finalUrl);
|
|
96
|
+
if (
|
|
97
|
+
!['ws:', 'wss:'].includes(parsed.protocol) ||
|
|
98
|
+
parsed.hostname === '' ||
|
|
99
|
+
finalUrl.includes('#')
|
|
100
|
+
)
|
|
101
|
+
throw new Error('Invalid realtime WebSocket URL');
|
|
102
|
+
} catch {
|
|
103
|
+
throw new Error(
|
|
104
|
+
'Invalid realtime WebSocket URL; expected an absolute ws:// or wss:// URL with a host and no fragment',
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
if (this.epoch !== epoch) return;
|
|
108
|
+
const ws = new WebSocket(finalUrl, finalProtocols);
|
|
44
109
|
|
|
45
110
|
// Track the socket immediately (not just in `onopen`) so that calling
|
|
46
111
|
// `disconnect()` while it is still connecting actually closes it. Otherwise
|
|
47
112
|
// `close()` would be a no-op and the socket could open afterwards and fire
|
|
48
113
|
// the `onOpen` (session-update) callback against a disconnected session.
|
|
49
114
|
this.ws = ws;
|
|
115
|
+
let starting = false;
|
|
116
|
+
let connectionError: Error | undefined;
|
|
117
|
+
const codec = new RealtimeEventChannel({
|
|
118
|
+
model: this.model,
|
|
119
|
+
send: data => {
|
|
120
|
+
if (this.epoch !== epoch || this.ws !== ws || !this.isOpen)
|
|
121
|
+
throw new Error('Realtime WebSocket is not open');
|
|
122
|
+
this.sendRaw(data);
|
|
123
|
+
},
|
|
124
|
+
onEvent: this.onServerEvent,
|
|
125
|
+
onError: error => {
|
|
126
|
+
if (!starting) this.onError(error);
|
|
127
|
+
},
|
|
128
|
+
onFatalError: error => this.fail(error),
|
|
129
|
+
});
|
|
130
|
+
if (this.epoch !== epoch) {
|
|
131
|
+
codec.dispose();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
this.codec = codec;
|
|
50
135
|
|
|
51
136
|
ws.onopen = () => {
|
|
52
137
|
// Ignore a late open for a socket that has since been replaced/closed.
|
|
53
138
|
if (this.ws !== ws) return;
|
|
54
|
-
|
|
139
|
+
starting = true;
|
|
140
|
+
try {
|
|
141
|
+
void Promise.resolve(onOpen())
|
|
142
|
+
.catch(error => {
|
|
143
|
+
if (this.ws === ws)
|
|
144
|
+
this.fail(
|
|
145
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
146
|
+
);
|
|
147
|
+
})
|
|
148
|
+
.finally(() => {
|
|
149
|
+
starting = false;
|
|
150
|
+
});
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (this.epoch === epoch)
|
|
153
|
+
this.fail(error instanceof Error ? error : new Error(String(error)));
|
|
154
|
+
}
|
|
55
155
|
};
|
|
56
156
|
|
|
57
157
|
ws.onmessage = messageEvent => {
|
|
58
|
-
|
|
158
|
+
if (this.ws === ws) this.codec?.receive(messageEvent.data);
|
|
59
159
|
};
|
|
60
160
|
|
|
61
161
|
ws.onerror = () => {
|
|
62
|
-
this.
|
|
162
|
+
if (this.ws === ws) {
|
|
163
|
+
connectionError = new Error('WebSocket connection error');
|
|
164
|
+
this.failing = true;
|
|
165
|
+
}
|
|
63
166
|
};
|
|
64
167
|
|
|
65
|
-
ws.onclose =
|
|
168
|
+
ws.onclose = event => {
|
|
66
169
|
if (this.ws === ws) {
|
|
67
170
|
this.ws = null;
|
|
68
|
-
|
|
171
|
+
const closeError = getCloseError(event) ?? connectionError;
|
|
172
|
+
codec.stopWriting();
|
|
173
|
+
this.notifyClosing();
|
|
174
|
+
if (this.epoch !== epoch) return;
|
|
175
|
+
const complete = () => {
|
|
176
|
+
if (this.epoch !== epoch) return;
|
|
177
|
+
clearTimeout(this.drainTimer);
|
|
178
|
+
this.epoch++;
|
|
179
|
+
codec.dispose();
|
|
180
|
+
try {
|
|
181
|
+
this.onClose(closeError);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
this.reportCallbackError(error);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
this.drainTimer = setTimeout(complete, 1_000);
|
|
187
|
+
void this.awaitDrain(codec.finish(), complete);
|
|
69
188
|
}
|
|
70
189
|
};
|
|
71
190
|
}
|
|
72
191
|
|
|
73
192
|
disconnect(): void {
|
|
74
|
-
this.
|
|
193
|
+
this.epoch++;
|
|
194
|
+
clearTimeout(this.drainTimer);
|
|
195
|
+
const ws = this.ws;
|
|
75
196
|
this.ws = null;
|
|
197
|
+
this.codec?.dispose();
|
|
198
|
+
this.codec = undefined;
|
|
199
|
+
this.failing = false;
|
|
200
|
+
this.closing = false;
|
|
201
|
+
ws?.close();
|
|
76
202
|
}
|
|
77
203
|
|
|
78
|
-
sendEvent(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
typeof data === 'string' ||
|
|
99
|
-
data instanceof ArrayBuffer ||
|
|
100
|
-
ArrayBuffer.isView(data) ||
|
|
101
|
-
data instanceof Blob
|
|
102
|
-
) {
|
|
103
|
-
this.ws.send(data);
|
|
104
|
-
} else {
|
|
105
|
-
this.ws.send(JSON.stringify(data));
|
|
106
|
-
}
|
|
204
|
+
sendEvent(
|
|
205
|
+
event: RealtimeClientEvent,
|
|
206
|
+
shouldSend?: () => boolean,
|
|
207
|
+
automaticAudio = false,
|
|
208
|
+
): Promise<void> {
|
|
209
|
+
if (this.failing || this.closing)
|
|
210
|
+
throw new Error('Realtime connection is closed');
|
|
211
|
+
if (this.codec == null) return Promise.resolve();
|
|
212
|
+
const epoch = this.epoch;
|
|
213
|
+
const failed = (error: unknown) => {
|
|
214
|
+
if (automaticAudio && this.epoch === epoch && this.isOpen)
|
|
215
|
+
this.fail(error instanceof Error ? error : new Error(String(error)));
|
|
216
|
+
};
|
|
217
|
+
try {
|
|
218
|
+
const sent = this.codec.send(event, shouldSend);
|
|
219
|
+
void sent.catch(failed);
|
|
220
|
+
return sent;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
failed(error);
|
|
223
|
+
throw error;
|
|
107
224
|
}
|
|
108
225
|
}
|
|
109
226
|
|
|
110
|
-
|
|
111
|
-
this.
|
|
227
|
+
finish(): Promise<void> {
|
|
228
|
+
return this.codec?.finish() ?? Promise.resolve();
|
|
112
229
|
}
|
|
113
230
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
231
|
+
/** Drain accepted events when media or protocol continuity is lost. */
|
|
232
|
+
fail(error: Error): void {
|
|
233
|
+
if (this.failing) return;
|
|
234
|
+
this.failing = true;
|
|
235
|
+
const epoch = this.epoch;
|
|
236
|
+
const ws = this.ws;
|
|
237
|
+
this.ws = null;
|
|
238
|
+
this.codec?.stopWriting();
|
|
239
|
+
ws?.close();
|
|
240
|
+
this.notifyClosing();
|
|
241
|
+
if (this.epoch !== epoch) return;
|
|
242
|
+
const drain = this.finish();
|
|
243
|
+
if (this.options.onFatalError != null)
|
|
244
|
+
this.options.onFatalError(error, drain);
|
|
245
|
+
else {
|
|
246
|
+
const complete = () => {
|
|
247
|
+
if (epoch !== this.epoch) return;
|
|
248
|
+
this.disconnect();
|
|
249
|
+
try {
|
|
250
|
+
this.onClose();
|
|
251
|
+
} catch (cause) {
|
|
252
|
+
this.reportCallbackError(cause);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
this.drainTimer = setTimeout(complete, 1_000);
|
|
256
|
+
void this.awaitDrain(drain, complete);
|
|
257
|
+
this.reportCallbackError(error);
|
|
122
258
|
}
|
|
259
|
+
}
|
|
123
260
|
|
|
124
|
-
|
|
125
|
-
if (
|
|
261
|
+
private notifyClosing(): void {
|
|
262
|
+
if (this.closing) return;
|
|
263
|
+
this.closing = true;
|
|
264
|
+
try {
|
|
265
|
+
this.options.onClosing?.();
|
|
266
|
+
} catch (error) {
|
|
267
|
+
this.reportCallbackError(error);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
126
270
|
|
|
127
|
-
|
|
271
|
+
private async awaitDrain(
|
|
272
|
+
drain: Promise<void>,
|
|
273
|
+
complete: () => void,
|
|
274
|
+
): Promise<void> {
|
|
275
|
+
try {
|
|
276
|
+
await drain;
|
|
277
|
+
} catch {
|
|
278
|
+
/* Transport loss still requires finalization. */
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
complete();
|
|
282
|
+
} catch (error) {
|
|
283
|
+
this.reportCallbackError(error);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
128
286
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
287
|
+
private reportCallbackError(error: unknown): void {
|
|
288
|
+
try {
|
|
289
|
+
this.onError(error instanceof Error ? error : new Error(String(error)));
|
|
290
|
+
} catch {
|
|
291
|
+
/* Application callbacks cannot interrupt teardown. */
|
|
134
292
|
}
|
|
293
|
+
}
|
|
135
294
|
|
|
136
|
-
|
|
137
|
-
const
|
|
295
|
+
sendRaw(data: unknown): void {
|
|
296
|
+
const ws = this.ws;
|
|
297
|
+
const epoch = this.epoch;
|
|
298
|
+
if (ws == null || !this.isOpen) return;
|
|
299
|
+
const frame = encodeRealtimeFrame(data);
|
|
300
|
+
if (this.epoch !== epoch || this.ws !== ws || !this.isOpen)
|
|
301
|
+
throw new Error('Realtime connection is closed');
|
|
302
|
+
if (this.continuous) assertRealtimeFrameBudget(frame, ws.bufferedAmount);
|
|
303
|
+
ws.send(frame.data);
|
|
304
|
+
}
|
|
138
305
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
306
|
+
dispose(): void {
|
|
307
|
+
this.disconnect();
|
|
142
308
|
}
|
|
143
309
|
}
|