ai 7.0.134 → 7.0.136
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 +24 -0
- package/dist/index.d.ts +234 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +616 -57
- package/dist/index.js.map +1 -1
- package/dist/internal/index.d.ts +6 -2
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +1 -1
- package/docs/03-agents/04-loop-control.mdx +2 -0
- package/docs/03-ai-sdk-core/15-tools-and-tool-calling.mdx +2 -2
- package/docs/03-ai-sdk-core/17-mcp-events.mdx +6 -2
- package/docs/03-ai-sdk-core/25-settings.mdx +4 -2
- package/docs/03-ai-sdk-core/30-embeddings.mdx +27 -0
- package/docs/03-ai-sdk-core/32-decisions.mdx +3 -2
- package/docs/04-ai-sdk-ui/21-transport.mdx +73 -0
- package/docs/07-reference/01-ai-sdk-core/01-generate-text.mdx +2 -2
- package/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx +2 -2
- package/docs/07-reference/01-ai-sdk-core/05-embed.mdx +14 -0
- package/docs/07-reference/01-ai-sdk-core/06-embed-many.mdx +14 -0
- package/docs/07-reference/01-ai-sdk-core/16-tool-loop-agent.mdx +5 -5
- package/docs/07-reference/01-ai-sdk-core/23-create-mcp-client.mdx +2 -2
- package/docs/07-reference/01-ai-sdk-core/24-mcp-events.mdx +123 -5
- package/docs/07-reference/02-ai-sdk-ui/51-websocket-chat-transport.mdx +393 -0
- package/docs/07-reference/02-ai-sdk-ui/index.mdx +6 -0
- package/package.json +12 -12
- package/src/agent/default-stop-condition.ts +33 -0
- package/src/agent/tool-loop-agent.ts +2 -2
- package/src/embed/embed-events.ts +6 -0
- package/src/embed/embed-many.ts +18 -0
- package/src/embed/embed.ts +16 -0
- package/src/embed/validate-embedding-dimensions.ts +14 -0
- package/src/generate-text/stream-text.ts +31 -24
- package/src/prompt/request-options.ts +2 -2
- package/src/ui/chat-transport.ts +16 -0
- package/src/ui/chat.ts +123 -34
- package/src/ui/index.ts +13 -0
- package/src/ui/process-ui-message-stream.ts +6 -2
- package/src/ui/websocket-chat-transport.ts +987 -0
- package/src/util/serial-job-executor.ts +22 -5
|
@@ -99,9 +99,11 @@ for the definition fields. Pagination is explicit; this method does not fetch al
|
|
|
99
99
|
## Managed Subscriptions
|
|
100
100
|
|
|
101
101
|
Configure `experimental_events: { adapter }` to delegate subscription lifecycle
|
|
102
|
-
operations to a backend. The adapter
|
|
103
|
-
from `@ai-sdk/mcp
|
|
104
|
-
|
|
102
|
+
operations to a backend. The `adapter` configuration requires an
|
|
103
|
+
`Experimental_MCPEventAdapter` from `@ai-sdk/mcp`. Its
|
|
104
|
+
`createAdapter({ transport })` method returns `Experimental_MCPEventOperations` bound
|
|
105
|
+
to the authorized account, endpoint and destination by your application or
|
|
106
|
+
integration package.
|
|
105
107
|
|
|
106
108
|
```ts
|
|
107
109
|
import { createMCPClient } from '@ai-sdk/mcp';
|
|
@@ -143,7 +145,123 @@ try {
|
|
|
143
145
|
Use the same authorized account for discovery and subscription management.
|
|
144
146
|
Never derive account identity, destination or authorization from model arguments.
|
|
145
147
|
|
|
146
|
-
### Adapter
|
|
148
|
+
### Using an Adapter Helper
|
|
149
|
+
|
|
150
|
+
A backend SDK or application helper can return `Experimental_MCPEventAdapter`
|
|
151
|
+
directly. Pass the helper's result as `adapter`; you do not need another object
|
|
152
|
+
wrapper or an inline `createAdapter` callback:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import { createMCPClient } from '@ai-sdk/mcp';
|
|
156
|
+
import { createBackendEventsAdapter } from './my-events-adapter';
|
|
157
|
+
|
|
158
|
+
const client = await createMCPClient({
|
|
159
|
+
transport: { type: 'http', url: 'https://service.example/mcp' },
|
|
160
|
+
experimental_events: {
|
|
161
|
+
adapter: createBackendEventsAdapter({
|
|
162
|
+
accountId: authenticatedUser.id,
|
|
163
|
+
destination: { path: '/hooks' },
|
|
164
|
+
}),
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The helper holds the authorized account and delivery configuration. AI SDK supplies
|
|
170
|
+
the transport type and URL to its `createAdapter` method internally, so the application
|
|
171
|
+
configures that URL only once. When a backend SDK exports this helper, import it
|
|
172
|
+
from that SDK instead of an application module. Use the same authorized account
|
|
173
|
+
for the MCP transport and the backend's subscription operations.
|
|
174
|
+
|
|
175
|
+
### Implementing an Adapter Helper
|
|
176
|
+
|
|
177
|
+
Backend integration authors implement `createAdapter` inside their helper:
|
|
178
|
+
|
|
179
|
+
```ts filename="my-events-adapter.ts"
|
|
180
|
+
import type { Experimental_MCPEventAdapter } from '@ai-sdk/mcp';
|
|
181
|
+
import { createBackendEventOperations } from './my-events-backend';
|
|
182
|
+
|
|
183
|
+
type BackendEventsConfig = {
|
|
184
|
+
accountId: string;
|
|
185
|
+
destination: { path: string };
|
|
186
|
+
mcpUrl?: string;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export function createBackendEventsAdapter(
|
|
190
|
+
config: BackendEventsConfig,
|
|
191
|
+
): Experimental_MCPEventAdapter {
|
|
192
|
+
return {
|
|
193
|
+
createAdapter({ transport }) {
|
|
194
|
+
return createBackendEventOperations({
|
|
195
|
+
...config,
|
|
196
|
+
mcpUrl:
|
|
197
|
+
config.mcpUrl ??
|
|
198
|
+
(transport.type === 'custom' ? undefined : transport.url),
|
|
199
|
+
});
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
These helpers and `BackendEventsConfig` are illustrative application or backend
|
|
206
|
+
SDK code, not AI SDK exports. `createBackendEventOperations` returns
|
|
207
|
+
`Experimental_MCPEventOperations` bound to the authorized account, endpoint
|
|
208
|
+
and destination. The helper above lets an explicit `mcpUrl` override the transport
|
|
209
|
+
URL for backend operations; it does not change the MCP client's transport or
|
|
210
|
+
catalog discovery. The backend still authorizes the selected endpoint.
|
|
211
|
+
|
|
212
|
+
The AI SDK adapter contract is:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
interface MCPEventAdapter {
|
|
216
|
+
createAdapter(context: {
|
|
217
|
+
transport: { type: 'http' | 'sse'; url: string } | { type: 'custom' };
|
|
218
|
+
}): MCPEventOperations;
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The interface is exported as `Experimental_MCPEventAdapter`. The client
|
|
223
|
+
calls `createAdapter` once, before starting the transport, and uses the returned
|
|
224
|
+
operations for managed subscriptions. The return value must be an object with
|
|
225
|
+
callable `subscribe`, `getSubscription`, `listSubscriptions`, and `unsubscribe`
|
|
226
|
+
methods. Nullish values, missing or non-callable methods, and Promise/thenable
|
|
227
|
+
results reject `createMCPClient` with `MCPClientError` before the transport starts.
|
|
228
|
+
Errors thrown by `createAdapter` also reject initialization without starting the
|
|
229
|
+
transport. Bound operations are no longer accepted directly in
|
|
230
|
+
`experimental_events.adapter`. To migrate existing subscription operations, wrap them explicitly:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
experimental_events: {
|
|
234
|
+
adapter: {
|
|
235
|
+
createAdapter: () => existingOperations,
|
|
236
|
+
},
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Only reuse bound operations when their account, endpoint and destination are correct
|
|
241
|
+
for every client using them. The former subscription operations interface is now
|
|
242
|
+
exported as `Experimental_MCPEventOperations`; `Experimental_MCPEventAdapter`
|
|
243
|
+
names the configured integration with `createAdapter`.
|
|
244
|
+
|
|
245
|
+
For HTTP and SSE transport configurations, the context contains a fresh
|
|
246
|
+
`transport: { type, url }` object. `url` is the configured string, including its
|
|
247
|
+
path and query, not a resolved redirect target. Custom `MCPTransport` instances
|
|
248
|
+
receive `transport: { type: 'custom' }`, with no `url` property; arbitrary
|
|
249
|
+
properties on the instance are not inspected.
|
|
250
|
+
|
|
251
|
+
This is a description of the transport, not the full configuration or live
|
|
252
|
+
instance. Headers, auth providers, fetch functions, session callbacks and
|
|
253
|
+
`start`/`send`/`close` methods are not passed to the adapter. The `transport`
|
|
254
|
+
grouping leaves room for other client context without mixing it with transport
|
|
255
|
+
metadata. No negotiated protocol version is supplied before initialization.
|
|
256
|
+
|
|
257
|
+
Return separately bound operations for each client instead of mutating shared
|
|
258
|
+
endpoint state. Adapters may prefer their own explicit endpoint override or
|
|
259
|
+
resolve a configured endpoint when `transport.type` is `'custom'`. That policy belongs to
|
|
260
|
+
the adapter; it does not change the client's transport or catalog discovery.
|
|
261
|
+
The managed backend must still authorize an endpoint before sending credentials
|
|
262
|
+
there. Receiving the transport URL does not grant that authorization.
|
|
263
|
+
|
|
264
|
+
### Managed Operations Contract
|
|
147
265
|
|
|
148
266
|
```ts
|
|
149
267
|
import type { JSONObject } from '@ai-sdk/provider';
|
|
@@ -184,7 +302,7 @@ These types are exported as `Experimental_ManagedSubscription` and
|
|
|
184
302
|
a positive `ttlMs`, an ISO deadline in `expiresAt`, or `expiresAt: null` to
|
|
185
303
|
monitor until stopped. The backend validates these values and enforces its limits.
|
|
186
304
|
|
|
187
|
-
`
|
|
305
|
+
`Experimental_MCPEventOperations` has four operations:
|
|
188
306
|
|
|
189
307
|
| Operation | Input | Result |
|
|
190
308
|
| ------------------- | ------------------------------------------------- | ---------------------------------- |
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: WebSocketChatTransport
|
|
3
|
+
description: API Reference for the WebSocketChatTransport class.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# `WebSocketChatTransport`
|
|
7
|
+
|
|
8
|
+
A [`ChatTransport`](/docs/ai-sdk-ui/transport) implementation that sends chat
|
|
9
|
+
requests and receives `UIMessageChunk` streams over one persistent WebSocket
|
|
10
|
+
connection.
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { useChat } from '@ai-sdk/react';
|
|
14
|
+
import { WebSocketChatTransport } from 'ai';
|
|
15
|
+
import { useMemo } from 'react';
|
|
16
|
+
|
|
17
|
+
export default function Chat() {
|
|
18
|
+
const transport = useMemo(
|
|
19
|
+
() => new WebSocketChatTransport({ url: '/api/chat' }),
|
|
20
|
+
[],
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const { messages, sendMessage, stop } = useChat({ transport });
|
|
24
|
+
|
|
25
|
+
// ... render the chat
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Import
|
|
30
|
+
|
|
31
|
+
<Snippet text={`import { WebSocketChatTransport } from "ai"`} prompt={false} />
|
|
32
|
+
|
|
33
|
+
## Constructor
|
|
34
|
+
|
|
35
|
+
<PropertiesTable
|
|
36
|
+
content={[
|
|
37
|
+
{
|
|
38
|
+
name: 'url',
|
|
39
|
+
type: 'string',
|
|
40
|
+
isRequired: true,
|
|
41
|
+
description:
|
|
42
|
+
'The WebSocket endpoint. Relative URLs are resolved against the browser location. HTTP and HTTPS URLs are converted to WS and WSS.',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'protocols',
|
|
46
|
+
type: 'string | string[]',
|
|
47
|
+
isOptional: true,
|
|
48
|
+
description: 'WebSocket subprotocols sent during the handshake.',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'params',
|
|
52
|
+
type: 'Resolvable<Record<string, string>>',
|
|
53
|
+
isOptional: true,
|
|
54
|
+
description:
|
|
55
|
+
'Query parameters resolved and appended whenever a new connection is opened.',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'headers',
|
|
59
|
+
type: 'Resolvable<Record<string, string> | Headers>',
|
|
60
|
+
isOptional: true,
|
|
61
|
+
description:
|
|
62
|
+
'Headers included inside each application-level send or resume frame. These are not WebSocket handshake headers.',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'body',
|
|
66
|
+
type: 'Resolvable<object>',
|
|
67
|
+
isOptional: true,
|
|
68
|
+
description:
|
|
69
|
+
'Additional application data merged with per-request body data and included in send and resume frames.',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'webSocket',
|
|
73
|
+
type: 'WebSocketConstructor',
|
|
74
|
+
isOptional: true,
|
|
75
|
+
description:
|
|
76
|
+
'A custom WebSocket constructor. Use this for Node.js WebSocket implementations or tests. Defaults to globalThis.WebSocket.',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'prepareSendMessagesRequest',
|
|
80
|
+
type: 'PrepareWebSocketChatTransportSendMessagesRequest',
|
|
81
|
+
isOptional: true,
|
|
82
|
+
description:
|
|
83
|
+
'Transforms the messages, headers, or body before a send frame is transmitted.',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'prepareReconnectToStreamRequest',
|
|
87
|
+
type: 'PrepareWebSocketChatTransportReconnectToStreamRequest',
|
|
88
|
+
isOptional: true,
|
|
89
|
+
description:
|
|
90
|
+
'Transforms the headers or body before a resume frame is transmitted.',
|
|
91
|
+
},
|
|
92
|
+
]}
|
|
93
|
+
/>
|
|
94
|
+
|
|
95
|
+
`headers` supplied to `sendMessage`, `regenerate`, or an automatic tool
|
|
96
|
+
follow-up override transport-level headers with the same lower-cased name.
|
|
97
|
+
Per-request `body` properties override transport-level body properties.
|
|
98
|
+
Preparation callbacks receive the merged values; values they return replace
|
|
99
|
+
the corresponding merged value.
|
|
100
|
+
|
|
101
|
+
## Methods
|
|
102
|
+
|
|
103
|
+
### `sendMessages()`
|
|
104
|
+
|
|
105
|
+
Opens the socket when necessary, sends a `send` frame, and returns the
|
|
106
|
+
correlated `ReadableStream<UIMessageChunk>`. Multiple requests can share the
|
|
107
|
+
same connection and are routed by `requestId`.
|
|
108
|
+
|
|
109
|
+
### `reconnectToStream()`
|
|
110
|
+
|
|
111
|
+
Sends a `resume` frame. The promise resolves as follows:
|
|
112
|
+
|
|
113
|
+
- `start`, the first `chunk`, or `end` resolves to a readable stream.
|
|
114
|
+
- `no-active` resolves to `null`.
|
|
115
|
+
- `error`, cancellation, or a connection failure rejects the promise if no
|
|
116
|
+
stream has been returned yet.
|
|
117
|
+
|
|
118
|
+
Servers should send `start` as soon as they confirm that a resumable stream is
|
|
119
|
+
active, then replay the retained response from sequence zero. The transport
|
|
120
|
+
omits `lastSequence` because Chat starts a fresh message parser when resuming:
|
|
121
|
+
text, reasoning, and tool deltas need their preceding chunks. Retain and replay
|
|
122
|
+
the original UI `start` chunk with a stable `messageId` so Chat replaces the
|
|
123
|
+
partial assistant message instead of appending another one. The transport
|
|
124
|
+
rejects gaps and ignores duplicate sequence numbers within each response
|
|
125
|
+
stream, including the replay.
|
|
126
|
+
|
|
127
|
+
### `close()`
|
|
128
|
+
|
|
129
|
+
Closes the current socket and errors active streams. A later send or resume
|
|
130
|
+
request opens a new connection.
|
|
131
|
+
|
|
132
|
+
The code that creates a transport owns its lifecycle. React and Vue `useChat`
|
|
133
|
+
do not close caller-supplied transports during replacement or cleanup, because
|
|
134
|
+
one transport can be shared by multiple chats. Call `close()` after its final
|
|
135
|
+
consumer is gone.
|
|
136
|
+
|
|
137
|
+
If a directly managed `Chat` instance exclusively owns the transport, call
|
|
138
|
+
`dispose()`:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
await chat.dispose();
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
For a shared transport, stop or discard individual chats without disposing
|
|
145
|
+
their shared transport, then call `transport.close()` after all consumers are
|
|
146
|
+
finished.
|
|
147
|
+
|
|
148
|
+
## Wire Protocol
|
|
149
|
+
|
|
150
|
+
Each frame is a JSON object with a `type` and a correlated `requestId`.
|
|
151
|
+
|
|
152
|
+
### Client-to-server frames
|
|
153
|
+
|
|
154
|
+
| Type | Purpose |
|
|
155
|
+
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
156
|
+
| `send` | Starts a new or regenerated turn and includes `id`, `messages`, `trigger`, `messageId`, request `headers`, `body`, and `metadata`. |
|
|
157
|
+
| `resume` | Requests the retained response for a chat `id`, with request `headers`, `body`, and `metadata`. `lastSequence` is omitted to request a complete replay. |
|
|
158
|
+
| `abort` | Cancels the operation identified by `requestId`. |
|
|
159
|
+
|
|
160
|
+
Use `WebSocketChatTransportRequest<UI_MESSAGE>`,
|
|
161
|
+
`WebSocketChatTransportSendRequest`, `WebSocketChatTransportResumeRequest`,
|
|
162
|
+
and `WebSocketChatTransportAbortRequest` to type these frames.
|
|
163
|
+
|
|
164
|
+
### Server-to-client frames
|
|
165
|
+
|
|
166
|
+
| Type | Purpose |
|
|
167
|
+
| ----------- | ----------------------------------------------------------------------------- |
|
|
168
|
+
| `start` | Confirms that a resume request has an active stream. |
|
|
169
|
+
| `chunk` | Carries a monotonic `sequence` and one valid `UIMessageChunk` in `chunk`. |
|
|
170
|
+
| `end` | Closes the correlated response stream. |
|
|
171
|
+
| `error` | Errors the correlated stream with the optional `errorText`. |
|
|
172
|
+
| `no-active` | Resolves a resume request to `null`; for a send request it closes the stream. |
|
|
173
|
+
|
|
174
|
+
Use `WebSocketChatTransportResponse` to type server frames.
|
|
175
|
+
|
|
176
|
+
Responses for unknown or completed request IDs are ignored. Malformed frames,
|
|
177
|
+
unknown response types, and invalid UI message chunks are treated as protocol
|
|
178
|
+
errors: the connection is closed and all active streams are errored.
|
|
179
|
+
|
|
180
|
+
Validate every untrusted client frame before using its fields:
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
const validated = await safeValidateWebSocketChatTransportRequest({
|
|
184
|
+
value: parsed.value,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (!validated.success) {
|
|
188
|
+
socket.close(1008, 'Invalid frame');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const frame = validated.data;
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Server Example
|
|
196
|
+
|
|
197
|
+
The server owns authentication, authorization, message validation, model
|
|
198
|
+
execution, resumable stream storage, and cancellation. The following Node.js
|
|
199
|
+
outline forwards a `streamText` UI message stream:
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
import { WebSocket, WebSocketServer } from 'ws';
|
|
203
|
+
import {
|
|
204
|
+
convertToModelMessages,
|
|
205
|
+
safeValidateWebSocketChatTransportRequest,
|
|
206
|
+
streamText,
|
|
207
|
+
toUIMessageStream,
|
|
208
|
+
validateUIMessages,
|
|
209
|
+
type UIMessage,
|
|
210
|
+
type UIMessageChunk,
|
|
211
|
+
type WebSocketChatTransportResponse,
|
|
212
|
+
} from 'ai';
|
|
213
|
+
import { generateId, safeParseJSON } from '@ai-sdk/provider-utils';
|
|
214
|
+
__PROVIDER_IMPORT__;
|
|
215
|
+
|
|
216
|
+
const server = new WebSocketServer({ port: 8080 });
|
|
217
|
+
|
|
218
|
+
type RetainedStream = {
|
|
219
|
+
abortController: AbortController;
|
|
220
|
+
chunks: Array<{ sequence: number; chunk: UIMessageChunk }>;
|
|
221
|
+
subscribers: Map<WebSocket, string>;
|
|
222
|
+
ended: boolean;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// Scope retained streams to an authenticated user. Use durable storage when
|
|
226
|
+
// reconnects can land in another process.
|
|
227
|
+
const streamsByUser = new Map<string, Map<string, RetainedStream>>();
|
|
228
|
+
|
|
229
|
+
server.on('connection', (socket, request) => {
|
|
230
|
+
const userId = authenticateAndValidateOrigin(request);
|
|
231
|
+
const streams =
|
|
232
|
+
streamsByUser.get(userId) ?? new Map<string, RetainedStream>();
|
|
233
|
+
streamsByUser.set(userId, streams);
|
|
234
|
+
|
|
235
|
+
socket.on('message', async data => {
|
|
236
|
+
const parsed = await safeParseJSON({ text: data.toString() });
|
|
237
|
+
if (!parsed.success) {
|
|
238
|
+
socket.close(1008, 'Invalid frame');
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const validated =
|
|
243
|
+
await safeValidateWebSocketChatTransportRequest<UIMessage>({
|
|
244
|
+
value: parsed.value,
|
|
245
|
+
});
|
|
246
|
+
if (!validated.success) {
|
|
247
|
+
socket.close(1008, 'Invalid frame');
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const frame = validated.data;
|
|
251
|
+
|
|
252
|
+
const send = (response: WebSocketChatTransportResponse) => {
|
|
253
|
+
socket.send(JSON.stringify(response));
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
if (frame.type === 'abort') {
|
|
257
|
+
for (const stream of streams.values()) {
|
|
258
|
+
if (stream.subscribers.get(socket) === frame.requestId) {
|
|
259
|
+
stream.subscribers.delete(socket);
|
|
260
|
+
stream.abortController.abort();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (frame.type === 'resume') {
|
|
267
|
+
const stream = streams.get(frame.id);
|
|
268
|
+
if (stream == null) {
|
|
269
|
+
send({ type: 'no-active', requestId: frame.requestId });
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
send({ type: 'start', requestId: frame.requestId });
|
|
274
|
+
if (!stream.ended) {
|
|
275
|
+
stream.subscribers.set(socket, frame.requestId);
|
|
276
|
+
}
|
|
277
|
+
for (const stored of stream.chunks) {
|
|
278
|
+
if (stored.sequence > (frame.lastSequence ?? -1)) {
|
|
279
|
+
send({
|
|
280
|
+
type: 'chunk',
|
|
281
|
+
requestId: frame.requestId,
|
|
282
|
+
...stored,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (stream.ended) {
|
|
287
|
+
send({ type: 'end', requestId: frame.requestId });
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (streams.get(frame.id)?.ended === false) {
|
|
293
|
+
send({
|
|
294
|
+
type: 'error',
|
|
295
|
+
requestId: frame.requestId,
|
|
296
|
+
errorText: 'A response is already active for this chat.',
|
|
297
|
+
});
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const abortController = new AbortController();
|
|
302
|
+
const stream: RetainedStream = {
|
|
303
|
+
abortController,
|
|
304
|
+
chunks: [],
|
|
305
|
+
subscribers: new Map([[socket, frame.requestId]]),
|
|
306
|
+
ended: false,
|
|
307
|
+
};
|
|
308
|
+
streams.set(frame.id, stream);
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const messages = await validateUIMessages({
|
|
312
|
+
messages: frame.messages,
|
|
313
|
+
});
|
|
314
|
+
const result = streamText({
|
|
315
|
+
model: __MODEL__,
|
|
316
|
+
messages: await convertToModelMessages(messages),
|
|
317
|
+
abortSignal: abortController.signal,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
for await (const chunk of toUIMessageStream({
|
|
321
|
+
stream: result.stream,
|
|
322
|
+
originalMessages: messages,
|
|
323
|
+
generateMessageId: generateId,
|
|
324
|
+
})) {
|
|
325
|
+
const stored = { sequence: stream.chunks.length, chunk };
|
|
326
|
+
stream.chunks.push(stored);
|
|
327
|
+
for (const [subscriber, requestId] of stream.subscribers) {
|
|
328
|
+
subscriber.send(
|
|
329
|
+
JSON.stringify({ type: 'chunk', requestId, ...stored }),
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
stream.ended = true;
|
|
334
|
+
for (const [subscriber, requestId] of stream.subscribers) {
|
|
335
|
+
subscriber.send(JSON.stringify({ type: 'end', requestId }));
|
|
336
|
+
}
|
|
337
|
+
} catch (error) {
|
|
338
|
+
// Failed responses cannot resume. Remove the retained entry so a retry
|
|
339
|
+
// can start a new response and resume requests receive no-active.
|
|
340
|
+
streams.delete(frame.id);
|
|
341
|
+
// Log the internal error server-side, but do not expose it to the client.
|
|
342
|
+
console.error(error);
|
|
343
|
+
for (const [subscriber, requestId] of stream.subscribers) {
|
|
344
|
+
subscriber.send(
|
|
345
|
+
JSON.stringify({
|
|
346
|
+
type: 'error',
|
|
347
|
+
requestId,
|
|
348
|
+
errorText: 'Chat request failed.',
|
|
349
|
+
}),
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
} finally {
|
|
353
|
+
stream.subscribers.clear();
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
socket.on('close', () => {
|
|
358
|
+
for (const stream of streams.values()) {
|
|
359
|
+
stream.subscribers.delete(socket);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
This outline keeps completed chunks in memory to make the replay behavior
|
|
366
|
+
clear. Production servers should expire completed streams and store resumable
|
|
367
|
+
state durably when reconnects can reach another process. If response setup or
|
|
368
|
+
streaming throws, the outline removes that response from retained state before
|
|
369
|
+
reporting the error. A later resume returns `no-active`, and a new send can retry
|
|
370
|
+
the chat.
|
|
371
|
+
|
|
372
|
+
## Authentication and Deployment
|
|
373
|
+
|
|
374
|
+
- Use WSS in production.
|
|
375
|
+
- Browser WebSockets cannot set arbitrary handshake headers. Prefer secure
|
|
376
|
+
cookies, short-lived query tokens, or subprotocols.
|
|
377
|
+
- Validate the handshake origin, authorize chat IDs, validate every frame and
|
|
378
|
+
UI message, limit frame sizes, and rate-limit connections.
|
|
379
|
+
- Do not depend on process memory for reconnection when your deployment can
|
|
380
|
+
restart or move the connection to another process.
|
|
381
|
+
- The transport does not automatically replay interrupted `send` requests,
|
|
382
|
+
because replay can duplicate model output or tool side effects.
|
|
383
|
+
|
|
384
|
+
`WebSocketChatTransport` is separate from the experimental Realtime APIs.
|
|
385
|
+
Realtime transports implement provider realtime sessions; this transport
|
|
386
|
+
carries the provider-independent AI SDK UI message protocol used by `useChat`.
|
|
387
|
+
|
|
388
|
+
## Client-side Tool Follow-ups
|
|
389
|
+
|
|
390
|
+
When `sendAutomaticallyWhen` triggers after client-side tool output is added,
|
|
391
|
+
the next turn reuses the same WebSocket. This avoids another HTTP connection,
|
|
392
|
+
but the current `ChatTransport` contract remains turn-based: the tool output is
|
|
393
|
+
not injected into a model generation that is still active.
|
|
@@ -70,6 +70,12 @@ AI SDK UI contains the following hooks:
|
|
|
70
70
|
'Transform a stream of UIMessageChunk objects into an AsyncIterableStream of UIMessage objects.',
|
|
71
71
|
href: '/docs/reference/ai-sdk-ui/read-ui-message-stream',
|
|
72
72
|
},
|
|
73
|
+
{
|
|
74
|
+
title: 'WebSocketChatTransport',
|
|
75
|
+
description:
|
|
76
|
+
'Send correlated chat requests and UI message streams over a persistent WebSocket connection.',
|
|
77
|
+
href: '/docs/reference/ai-sdk-ui/websocket-chat-transport',
|
|
78
|
+
},
|
|
73
79
|
{
|
|
74
80
|
title: 'experimental_MCPAppRenderer',
|
|
75
81
|
description: 'Render interactive MCP App tool UIs in React.',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.136",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AI SDK by Vercel - build apps like ChatGPT, Claude, Gemini, and more with a single interface for any model using the Vercel AI Gateway or go direct to OpenAI, Anthropic, Google, or any other model provider.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -42,20 +42,20 @@
|
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@ai-sdk/gateway": "4.0.
|
|
46
|
-
"@ai-sdk/provider": "4.0.
|
|
47
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
45
|
+
"@ai-sdk/gateway": "4.0.110",
|
|
46
|
+
"@ai-sdk/provider": "4.0.26",
|
|
47
|
+
"@ai-sdk/provider-utils": "5.0.58"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@ai-sdk/amazon-bedrock": "5.0.
|
|
51
|
-
"@ai-sdk/deepseek": "3.0.
|
|
52
|
-
"@ai-sdk/google": "4.0.
|
|
53
|
-
"@ai-sdk/groq": "4.0.
|
|
54
|
-
"@ai-sdk/huggingface": "2.0.
|
|
55
|
-
"@ai-sdk/moonshotai": "3.0.
|
|
56
|
-
"@ai-sdk/openai": "4.0.
|
|
50
|
+
"@ai-sdk/amazon-bedrock": "5.0.113",
|
|
51
|
+
"@ai-sdk/deepseek": "3.0.63",
|
|
52
|
+
"@ai-sdk/google": "4.0.93",
|
|
53
|
+
"@ai-sdk/groq": "4.0.59",
|
|
54
|
+
"@ai-sdk/huggingface": "2.0.67",
|
|
55
|
+
"@ai-sdk/moonshotai": "3.0.67",
|
|
56
|
+
"@ai-sdk/openai": "4.0.91",
|
|
57
57
|
"@ai-sdk/test-server": "2.0.4",
|
|
58
|
-
"@ai-sdk/xai": "5.0.
|
|
58
|
+
"@ai-sdk/xai": "5.0.20",
|
|
59
59
|
"@edge-runtime/vm": "^5.0.0",
|
|
60
60
|
"@smithy/eventstream-codec": "^4.3.3",
|
|
61
61
|
"@smithy/util-utf8": "^4.3.3",
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { logWarnings } from '../logger/log-warnings';
|
|
2
|
+
import type { StopCondition } from '../generate-text/stop-condition';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Stop conditions are only evaluated when the tool loop can otherwise continue,
|
|
6
|
+
* so logging here avoids warning when a call finishes naturally at the limit.
|
|
7
|
+
*/
|
|
8
|
+
export function createDefaultStopCondition(
|
|
9
|
+
stepCount: number,
|
|
10
|
+
): StopCondition<any, any> {
|
|
11
|
+
return ({ steps }) => {
|
|
12
|
+
if (steps.length !== stepCount) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { provider, modelId } = steps[steps.length - 1].model;
|
|
17
|
+
logWarnings({
|
|
18
|
+
provider,
|
|
19
|
+
model: modelId,
|
|
20
|
+
warnings: [
|
|
21
|
+
{
|
|
22
|
+
type: 'other',
|
|
23
|
+
message:
|
|
24
|
+
`The tool loop stopped because it reached the default stopWhen condition, isStepCount(${stepCount}). ` +
|
|
25
|
+
'To allow more steps, set stopWhen to isStepCount(...) with a higher limit or provide a custom stop condition. ' +
|
|
26
|
+
'Learn more: https://ai-sdk.dev/docs/agents/loop-control',
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return true;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -13,7 +13,6 @@ import type {
|
|
|
13
13
|
} from '../generate-text/generate-text-events';
|
|
14
14
|
import type { GenerateTextResult } from '../generate-text/generate-text-result';
|
|
15
15
|
import type { Output } from '../generate-text/output';
|
|
16
|
-
import { isStepCount } from '../generate-text/stop-condition';
|
|
17
16
|
import { streamText } from '../generate-text/stream-text';
|
|
18
17
|
import type { StreamTextResult } from '../generate-text/stream-text-result';
|
|
19
18
|
import type { Prompt } from '../prompt';
|
|
@@ -24,6 +23,7 @@ import type {
|
|
|
24
23
|
AgentStreamParameters,
|
|
25
24
|
} from './agent';
|
|
26
25
|
import type { ToolLoopAgentSettings } from './tool-loop-agent-settings';
|
|
26
|
+
import { createDefaultStopCondition } from './default-stop-condition';
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* A tool loop agent is an agent that runs tools in a loop. In each step,
|
|
@@ -129,7 +129,7 @@ export class ToolLoopAgent<
|
|
|
129
129
|
|
|
130
130
|
const baseCallArgs = {
|
|
131
131
|
...settingsWithoutCallbacks,
|
|
132
|
-
stopWhen: this.settings.stopWhen ??
|
|
132
|
+
stopWhen: this.settings.stopWhen ?? createDefaultStopCondition(20),
|
|
133
133
|
...options,
|
|
134
134
|
};
|
|
135
135
|
|
|
@@ -27,6 +27,9 @@ export type EmbedStartEvent<RUNTIME_CONTEXT extends Context = Context> = {
|
|
|
27
27
|
/** The value(s) being embedded. A string for embed, an array for embedMany. */
|
|
28
28
|
readonly value: string | Array<string>;
|
|
29
29
|
|
|
30
|
+
/** Requested output dimensions, when specified. */
|
|
31
|
+
readonly dimensions?: number;
|
|
32
|
+
|
|
30
33
|
/** Maximum number of retries for failed requests. */
|
|
31
34
|
readonly maxRetries: number;
|
|
32
35
|
|
|
@@ -104,6 +107,9 @@ export type EmbeddingModelCallStartEvent = {
|
|
|
104
107
|
|
|
105
108
|
/** The values being embedded in this particular model call. */
|
|
106
109
|
readonly values: Array<string>;
|
|
110
|
+
|
|
111
|
+
/** Requested output dimensions, when specified. */
|
|
112
|
+
readonly dimensions?: number;
|
|
107
113
|
};
|
|
108
114
|
|
|
109
115
|
/**
|