ai 7.0.69 → 7.0.71
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 +21 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +100 -17
- package/dist/index.js.map +1 -1
- package/dist/internal/index.js +1 -1
- package/docs/03-agents/07-workflow-agent.mdx +28 -10
- package/docs/03-ai-sdk-harnesses/02-harness-agent.mdx +58 -0
- package/docs/04-ai-sdk-ui/21-transport.mdx +6 -2
- package/docs/04-ai-sdk-ui/50-stream-protocol.mdx +16 -0
- package/docs/07-reference/04-ai-sdk-workflow/01-workflow-agent.mdx +15 -1
- package/docs/07-reference/04-ai-sdk-workflow/02-workflow-chat-transport.mdx +25 -11
- package/package.json +3 -3
- package/src/generate-object/stream-object.ts +67 -4
- package/src/generate-text/execute-tools-from-stream.ts +5 -0
- package/src/generate-text/generate-text.ts +12 -8
- package/src/generate-text/is-tool-execution-allowed-finish-reason.ts +7 -0
- package/src/generate-text/stream-text.ts +14 -9
- package/src/ui/convert-to-model-messages.ts +1 -1
- package/src/ui/process-ui-message-stream.ts +17 -0
- package/src/ui-message-stream/ui-message-chunks.ts +9 -0
package/dist/internal/index.js
CHANGED
|
@@ -36,10 +36,10 @@ For simpler use cases that don't need durability, use [`ToolLoopAgent`](/docs/ag
|
|
|
36
36
|
## Installation
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
|
-
npm install @ai-sdk/workflow workflow
|
|
39
|
+
npm install @ai-sdk/workflow workflow@beta
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
`@ai-sdk/workflow` requires the `ai` package and `zod`
|
|
42
|
+
`@ai-sdk/workflow` requires Workflow 5, which is currently available under the `beta` tag, as well as the `ai` package and `zod` peer dependencies. The `workflow` package provides the Workflow DevKit runtime (`getWritable`, `'use workflow'`, `'use step'`).
|
|
43
43
|
|
|
44
44
|
## Creating a WorkflowAgent
|
|
45
45
|
|
|
@@ -185,6 +185,11 @@ return createUIMessageStreamResponse({
|
|
|
185
185
|
});
|
|
186
186
|
```
|
|
187
187
|
|
|
188
|
+
The transform also forwards `reset-step` events emitted by `WorkflowAgent` on
|
|
189
|
+
retries.
|
|
190
|
+
Clients remove partial parts from the failed model-call step before processing
|
|
191
|
+
the retried output.
|
|
192
|
+
|
|
188
193
|
## Resumable Streaming with WorkflowChatTransport
|
|
189
194
|
|
|
190
195
|
Workflow functions can time out or be interrupted by network failures. `WorkflowChatTransport` is a [`ChatTransport`](/docs/ai-sdk-ui/transport) implementation that handles these interruptions automatically — it detects when a stream ends without a `finish` event and reconnects to resume from where it left off.
|
|
@@ -202,7 +207,6 @@ export default function Chat() {
|
|
|
202
207
|
new WorkflowChatTransport({
|
|
203
208
|
api: '/api/chat',
|
|
204
209
|
maxConsecutiveErrors: 5,
|
|
205
|
-
initialStartIndex: -50, // On page refresh, fetch last 50 chunks
|
|
206
210
|
}),
|
|
207
211
|
[],
|
|
208
212
|
);
|
|
@@ -236,6 +240,7 @@ export async function POST(request: Request) {
|
|
|
236
240
|
|
|
237
241
|
```ts filename="app/api/chat/[runId]/stream/route.ts"
|
|
238
242
|
import { createModelCallToUIChunkTransform } from '@ai-sdk/workflow';
|
|
243
|
+
import { createUIMessageStreamResponse } from 'ai';
|
|
239
244
|
import type { NextRequest } from 'next/server';
|
|
240
245
|
import { getRun } from 'workflow/api';
|
|
241
246
|
|
|
@@ -247,23 +252,36 @@ export async function GET(
|
|
|
247
252
|
const startIndex = Number(
|
|
248
253
|
new URL(request.url).searchParams.get('startIndex') ?? '0',
|
|
249
254
|
);
|
|
255
|
+
if (!Number.isSafeInteger(startIndex) || startIndex < 0) {
|
|
256
|
+
return Response.json(
|
|
257
|
+
{ error: 'startIndex must be a non-negative safe integer' },
|
|
258
|
+
{ status: 400 },
|
|
259
|
+
);
|
|
260
|
+
}
|
|
250
261
|
|
|
251
262
|
const run = await getRun(runId);
|
|
252
263
|
const readable = run
|
|
253
|
-
.getReadable({ startIndex })
|
|
254
|
-
.pipeThrough(
|
|
264
|
+
.getReadable({ startIndex: 0 })
|
|
265
|
+
.pipeThrough(
|
|
266
|
+
createModelCallToUIChunkTransform({ uiStartIndex: startIndex }),
|
|
267
|
+
);
|
|
255
268
|
|
|
256
|
-
return
|
|
269
|
+
return createUIMessageStreamResponse({
|
|
270
|
+
stream: readable,
|
|
257
271
|
headers: {
|
|
258
|
-
'Content-Type': 'text/event-stream',
|
|
259
|
-
'Cache-Control': 'no-cache',
|
|
260
|
-
Connection: 'keep-alive',
|
|
261
272
|
'x-workflow-run-id': runId,
|
|
262
273
|
},
|
|
263
274
|
});
|
|
264
275
|
}
|
|
265
276
|
```
|
|
266
277
|
|
|
278
|
+
`WorkflowChatTransport` counts `UIMessageChunk` objects, while the durable
|
|
279
|
+
`WorkflowAgent` stream stores raw `ModelCallStreamPart` objects. Replay the raw
|
|
280
|
+
stream from index `0` and apply the non-negative UI cursor in
|
|
281
|
+
`createModelCallToUIChunkTransform()` as shown above. Negative start indexes
|
|
282
|
+
require a durable stream that already stores `UIMessageChunk` objects and
|
|
283
|
+
cannot be used with this raw-to-UI conversion.
|
|
284
|
+
|
|
267
285
|
For the full API reference, see [`WorkflowChatTransport`](/docs/reference/ai-sdk-workflow/workflow-chat-transport).
|
|
268
286
|
|
|
269
287
|
## Tools as Workflow Steps
|
|
@@ -589,7 +607,7 @@ export type MyAgentUIMessage = InferWorkflowAgentUIMessage<typeof myAgent>;
|
|
|
589
607
|
Install the new package alongside `workflow`:
|
|
590
608
|
|
|
591
609
|
```bash
|
|
592
|
-
npm install @ai-sdk/workflow
|
|
610
|
+
npm install @ai-sdk/workflow workflow@beta
|
|
593
611
|
```
|
|
594
612
|
|
|
595
613
|
### Write `ModelCallStreamPart`, not `UIMessageChunk`
|
|
@@ -484,6 +484,64 @@ try {
|
|
|
484
484
|
}
|
|
485
485
|
```
|
|
486
486
|
|
|
487
|
+
### Basic Sandbox Sessions Without Network Control
|
|
488
|
+
|
|
489
|
+
The following example demonstrates how the basic-session API works. If your
|
|
490
|
+
project can expose a full network sandbox session, passing that session is
|
|
491
|
+
strongly recommended. Pass a restricted basic session only when your project
|
|
492
|
+
cannot expose the full network session.
|
|
493
|
+
|
|
494
|
+
A basic sandbox session only exposes filesystem and process APIs. The agent
|
|
495
|
+
still leaves the sandbox lifecycle to the caller. For a bridge-backed harness,
|
|
496
|
+
configure the bridge port and its externally reachable endpoint on the adapter
|
|
497
|
+
because the basic session cannot resolve them.
|
|
498
|
+
|
|
499
|
+
```ts
|
|
500
|
+
import { HarnessAgent, prepareSandboxForHarness } from '@ai-sdk/harness/agent';
|
|
501
|
+
import { createClaudeCode } from '@ai-sdk/harness-claude-code';
|
|
502
|
+
import { createVercelSandbox } from '@ai-sdk/sandbox-vercel';
|
|
503
|
+
import { Sandbox } from '@vercel/sandbox';
|
|
504
|
+
|
|
505
|
+
const sandbox = await Sandbox.create({
|
|
506
|
+
runtime: 'node24',
|
|
507
|
+
ports: [4000],
|
|
508
|
+
});
|
|
509
|
+
const sandboxProvider = createVercelSandbox({ sandbox });
|
|
510
|
+
const sandboxSession = await sandboxProvider.createSession();
|
|
511
|
+
const portEndpoint = await sandboxSession.getPortEndpoint({
|
|
512
|
+
port: 4000,
|
|
513
|
+
protocol: 'ws',
|
|
514
|
+
});
|
|
515
|
+
const restrictedSandboxSession = sandboxSession.restricted();
|
|
516
|
+
const claudeCode = createClaudeCode({ port: 4000, portEndpoint });
|
|
517
|
+
|
|
518
|
+
await prepareSandboxForHarness({
|
|
519
|
+
session: restrictedSandboxSession,
|
|
520
|
+
harnesses: [claudeCode],
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
const agent = new HarnessAgent({ harness: claudeCode });
|
|
524
|
+
const session = await agent.createSession({
|
|
525
|
+
sandboxSession: restrictedSandboxSession,
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
try {
|
|
529
|
+
const result = await agent.stream({
|
|
530
|
+
session,
|
|
531
|
+
prompt: 'Create a short TODO.md for this repository.',
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
for await (const part of result.stream) {
|
|
535
|
+
if (part.type === 'text-delta') {
|
|
536
|
+
process.stdout.write(part.text);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
} finally {
|
|
540
|
+
await session.destroy();
|
|
541
|
+
await sandbox.stop();
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
|
|
487
545
|
## Next Steps
|
|
488
546
|
|
|
489
547
|
- [Tools](/docs/ai-sdk-harnesses/tools) for built-in and host-executed tools.
|
|
@@ -171,7 +171,6 @@ export default function Chat() {
|
|
|
171
171
|
new WorkflowChatTransport({
|
|
172
172
|
api: '/api/chat',
|
|
173
173
|
maxConsecutiveErrors: 5,
|
|
174
|
-
initialStartIndex: -50, // On page refresh, fetch last 50 chunks
|
|
175
174
|
onChatEnd: ({ chatId, chunkIndex }) => {
|
|
176
175
|
console.log(`Chat complete: ${chunkIndex} chunks`);
|
|
177
176
|
},
|
|
@@ -188,10 +187,15 @@ export default function Chat() {
|
|
|
188
187
|
Key features:
|
|
189
188
|
|
|
190
189
|
- **Automatic reconnection**: Detects interrupted streams (no `finish` event) and reconnects via GET to `{api}/{runId}/stream`
|
|
191
|
-
- **Page refresh recovery**: `initialStartIndex`
|
|
190
|
+
- **Page refresh recovery**: `initialStartIndex` controls where the initial reconnection begins
|
|
192
191
|
- **Configurable retries**: `maxConsecutiveErrors` controls how many consecutive reconnection failures to tolerate
|
|
193
192
|
- **Lifecycle callbacks**: `onChatSendMessage` and `onChatEnd` for tracking chat state
|
|
194
193
|
|
|
194
|
+
Negative `initialStartIndex` values can fetch only the tail when the durable
|
|
195
|
+
server stream already stores `UIMessageChunk` objects. For raw `WorkflowAgent`
|
|
196
|
+
streams, use a non-negative cursor and follow the server-side conversion in the
|
|
197
|
+
WorkflowAgent guide.
|
|
198
|
+
|
|
195
199
|
For the full API reference, see [`WorkflowChatTransport`](/docs/reference/ai-sdk-workflow/workflow-chat-transport). For server-side endpoint setup, see the [WorkflowAgent guide](/docs/agents/workflow-agent#resumable-streaming-with-workflowchattransport).
|
|
196
200
|
|
|
197
201
|
## Building Custom Transports
|
|
@@ -442,6 +442,22 @@ data: {"type":"finish-step"}
|
|
|
442
442
|
|
|
443
443
|
```
|
|
444
444
|
|
|
445
|
+
### Reset Step Part
|
|
446
|
+
|
|
447
|
+
Removes all message parts received since the most recent `start-step` part. If
|
|
448
|
+
there is no step boundary, it removes all parts from the current message. This
|
|
449
|
+
is useful when a streamed step is retried and partial output from the failed
|
|
450
|
+
attempt must be invalidated before replacement output is sent.
|
|
451
|
+
|
|
452
|
+
Format: Server-Sent Event with JSON object
|
|
453
|
+
|
|
454
|
+
Example:
|
|
455
|
+
|
|
456
|
+
```
|
|
457
|
+
data: {"type":"reset-step"}
|
|
458
|
+
|
|
459
|
+
```
|
|
460
|
+
|
|
445
461
|
### Finish Message Part
|
|
446
462
|
|
|
447
463
|
A part indicating the completion of a message.
|
|
@@ -696,7 +696,7 @@ Returns a `Promise<WorkflowAgentStreamResult>` with the following properties:
|
|
|
696
696
|
|
|
697
697
|
## Utilities
|
|
698
698
|
|
|
699
|
-
### `createModelCallToUIChunkTransform()`
|
|
699
|
+
### `createModelCallToUIChunkTransform(options?)`
|
|
700
700
|
|
|
701
701
|
Creates a `TransformStream` that converts raw `ModelCallStreamPart` chunks (written by the agent to the `writable` stream) into `UIMessageChunk` objects suitable for client consumption.
|
|
702
702
|
|
|
@@ -708,6 +708,20 @@ return createUIMessageStreamResponse({
|
|
|
708
708
|
});
|
|
709
709
|
```
|
|
710
710
|
|
|
711
|
+
When resuming with a `WorkflowChatTransport` cursor, replay the raw workflow
|
|
712
|
+
stream from index `0` and pass the non-negative UI chunk index to the transform:
|
|
713
|
+
|
|
714
|
+
```ts
|
|
715
|
+
const readable = run
|
|
716
|
+
.getReadable({ startIndex: 0 })
|
|
717
|
+
.pipeThrough(createModelCallToUIChunkTransform({ uiStartIndex: startIndex }));
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
`uiStartIndex` must be a non-negative safe integer. Raw model stream parts and
|
|
721
|
+
UI message chunks are not one-to-one, so do not pass a UI chunk index to
|
|
722
|
+
`getReadable`. Negative tail indexes require a durable stream that already
|
|
723
|
+
stores `UIMessageChunk` objects.
|
|
724
|
+
|
|
711
725
|
### `toUIMessageChunk()`
|
|
712
726
|
|
|
713
727
|
Converts a single `ModelCallStreamPart` to a `UIMessageChunk`. Returns `undefined` for parts that don't map to UI chunks.
|
|
@@ -20,7 +20,6 @@ export default function Chat() {
|
|
|
20
20
|
transport: new WorkflowChatTransport({
|
|
21
21
|
api: '/api/chat',
|
|
22
22
|
maxConsecutiveErrors: 5,
|
|
23
|
-
initialStartIndex: -50,
|
|
24
23
|
}),
|
|
25
24
|
});
|
|
26
25
|
|
|
@@ -67,7 +66,7 @@ export default function Chat() {
|
|
|
67
66
|
type: 'number',
|
|
68
67
|
isOptional: true,
|
|
69
68
|
description:
|
|
70
|
-
'Default chunk index to start from when reconnecting. Negative values read from the end of
|
|
69
|
+
'Default chunk index to start from when reconnecting. Negative values read from the end of a durable UIMessageChunk stream (e.g., -50 fetches the last 50 chunks), useful for resuming after a page refresh without replaying the full conversation. Raw ModelCallStreamPart streams do not support negative UI chunk indexes. Can be overridden per-call via reconnectToStream options. Default: 0.',
|
|
71
70
|
},
|
|
72
71
|
{
|
|
73
72
|
name: 'onChatSendMessage',
|
|
@@ -183,7 +182,7 @@ const stream = await transport.reconnectToStream({
|
|
|
183
182
|
type: 'number',
|
|
184
183
|
isOptional: true,
|
|
185
184
|
description:
|
|
186
|
-
"Override the start index for this reconnection. Negative values read from the end
|
|
185
|
+
"Override the start index for this reconnection. Negative values read from the end when the server's durable stream and tail-index header use the same UIMessageChunk index space. When omitted, falls back to the constructor's initialStartIndex.",
|
|
187
186
|
},
|
|
188
187
|
]}
|
|
189
188
|
/>
|
|
@@ -209,6 +208,10 @@ When `initialStartIndex` is negative (e.g., `-50`), the transport sends it as-is
|
|
|
209
208
|
|
|
210
209
|
If the header is missing or invalid, the transport falls back to replaying from the beginning (`startIndex=0`).
|
|
211
210
|
|
|
211
|
+
Negative indexes require a durable server stream whose stored objects are
|
|
212
|
+
already `UIMessageChunk` objects. The raw `WorkflowAgent` conversion shown
|
|
213
|
+
below supports non-negative indexes only.
|
|
214
|
+
|
|
212
215
|
## Server Requirements
|
|
213
216
|
|
|
214
217
|
For `WorkflowChatTransport` to work, your server must provide two endpoints:
|
|
@@ -262,7 +265,7 @@ export default function Chat() {
|
|
|
262
265
|
}
|
|
263
266
|
```
|
|
264
267
|
|
|
265
|
-
### With Callbacks
|
|
268
|
+
### With Callbacks
|
|
266
269
|
|
|
267
270
|
```tsx
|
|
268
271
|
'use client';
|
|
@@ -277,7 +280,6 @@ export default function Chat() {
|
|
|
277
280
|
new WorkflowChatTransport({
|
|
278
281
|
api: '/api/chat',
|
|
279
282
|
maxConsecutiveErrors: 5,
|
|
280
|
-
initialStartIndex: -50, // Resume from last 50 chunks on page refresh
|
|
281
283
|
onChatSendMessage: response => {
|
|
282
284
|
const runId = response.headers.get('x-workflow-run-id');
|
|
283
285
|
console.log('Workflow run started:', runId);
|
|
@@ -318,6 +320,7 @@ export async function POST(request: Request) {
|
|
|
318
320
|
|
|
319
321
|
```ts filename="app/api/chat/[runId]/stream/route.ts"
|
|
320
322
|
import { createModelCallToUIChunkTransform } from '@ai-sdk/workflow';
|
|
323
|
+
import { createUIMessageStreamResponse } from 'ai';
|
|
321
324
|
import type { NextRequest } from 'next/server';
|
|
322
325
|
import { getRun } from 'workflow/api';
|
|
323
326
|
|
|
@@ -329,19 +332,30 @@ export async function GET(
|
|
|
329
332
|
const startIndex = Number(
|
|
330
333
|
new URL(request.url).searchParams.get('startIndex') ?? '0',
|
|
331
334
|
);
|
|
335
|
+
if (!Number.isSafeInteger(startIndex) || startIndex < 0) {
|
|
336
|
+
return Response.json(
|
|
337
|
+
{ error: 'startIndex must be a non-negative safe integer' },
|
|
338
|
+
{ status: 400 },
|
|
339
|
+
);
|
|
340
|
+
}
|
|
332
341
|
|
|
333
342
|
const run = await getRun(runId);
|
|
334
343
|
const readable = run
|
|
335
|
-
.getReadable({ startIndex })
|
|
336
|
-
.pipeThrough(
|
|
344
|
+
.getReadable({ startIndex: 0 })
|
|
345
|
+
.pipeThrough(
|
|
346
|
+
createModelCallToUIChunkTransform({ uiStartIndex: startIndex }),
|
|
347
|
+
);
|
|
337
348
|
|
|
338
|
-
return
|
|
349
|
+
return createUIMessageStreamResponse({
|
|
350
|
+
stream: readable,
|
|
339
351
|
headers: {
|
|
340
|
-
'Content-Type': 'text/event-stream',
|
|
341
|
-
'Cache-Control': 'no-cache',
|
|
342
|
-
Connection: 'keep-alive',
|
|
343
352
|
'x-workflow-run-id': runId,
|
|
344
353
|
},
|
|
345
354
|
});
|
|
346
355
|
}
|
|
347
356
|
```
|
|
357
|
+
|
|
358
|
+
This `WorkflowAgent` endpoint replays raw `ModelCallStreamPart` objects from
|
|
359
|
+
index `0`, then applies the transport's non-negative cursor after converting
|
|
360
|
+
them to `UIMessageChunk` objects. Negative start indexes require a durable
|
|
361
|
+
stream whose stored objects are already `UIMessageChunk` objects.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.71",
|
|
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,9 +42,9 @@
|
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@ai-sdk/gateway": "4.0.
|
|
45
|
+
"@ai-sdk/gateway": "4.0.57",
|
|
46
46
|
"@ai-sdk/provider": "4.0.7",
|
|
47
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
47
|
+
"@ai-sdk/provider-utils": "5.0.28"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@edge-runtime/vm": "^5.0.0",
|
|
@@ -68,6 +68,12 @@ import { validateObjectGenerationInput } from './validate-object-generation-inpu
|
|
|
68
68
|
|
|
69
69
|
const originalGenerateId = createIdGenerator({ prefix: 'aiobj', size: 24 });
|
|
70
70
|
|
|
71
|
+
async function markPromiseAsHandled<T>(promise: Promise<T>): Promise<void> {
|
|
72
|
+
try {
|
|
73
|
+
await promise;
|
|
74
|
+
} catch {}
|
|
75
|
+
}
|
|
76
|
+
|
|
71
77
|
/**
|
|
72
78
|
* Callback that is set using the `onError` option.
|
|
73
79
|
*
|
|
@@ -540,7 +546,10 @@ class DefaultStreamObjectResult<
|
|
|
540
546
|
controller.enqueue(chunk);
|
|
541
547
|
|
|
542
548
|
if (chunk.type === 'error') {
|
|
543
|
-
|
|
549
|
+
void notify({
|
|
550
|
+
event: { error: wrapGatewayError(chunk.error) },
|
|
551
|
+
callbacks: onError,
|
|
552
|
+
});
|
|
544
553
|
}
|
|
545
554
|
},
|
|
546
555
|
});
|
|
@@ -656,6 +665,7 @@ class DefaultStreamObjectResult<
|
|
|
656
665
|
let providerMetadata: ProviderMetadata | undefined;
|
|
657
666
|
let object: RESULT | undefined;
|
|
658
667
|
let error: unknown | undefined;
|
|
668
|
+
let terminalError: { error: unknown } | undefined;
|
|
659
669
|
let msToFirstChunk: number | undefined = undefined;
|
|
660
670
|
|
|
661
671
|
let accumulatedText = '';
|
|
@@ -751,19 +761,35 @@ class DefaultStreamObjectResult<
|
|
|
751
761
|
break;
|
|
752
762
|
}
|
|
753
763
|
|
|
764
|
+
case 'error': {
|
|
765
|
+
if (terminalError === undefined) {
|
|
766
|
+
const wrappedError = wrapGatewayError(chunk.error);
|
|
767
|
+
terminalError = { error: wrappedError };
|
|
768
|
+
error = wrappedError;
|
|
769
|
+
finishReason = 'error';
|
|
770
|
+
self.rejectResultPromises(wrappedError);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
controller.enqueue(chunk);
|
|
774
|
+
break;
|
|
775
|
+
}
|
|
776
|
+
|
|
754
777
|
case 'finish': {
|
|
755
778
|
if (textDelta !== '') {
|
|
756
779
|
controller.enqueue({ type: 'text-delta', textDelta });
|
|
757
780
|
}
|
|
758
781
|
|
|
759
|
-
finishReason =
|
|
782
|
+
finishReason =
|
|
783
|
+
terminalError === undefined
|
|
784
|
+
? chunk.finishReason.unified
|
|
785
|
+
: 'error';
|
|
760
786
|
|
|
761
787
|
usage = asLanguageModelUsage(chunk.usage);
|
|
762
788
|
providerMetadata = chunk.providerMetadata;
|
|
763
789
|
|
|
764
790
|
controller.enqueue({
|
|
765
791
|
...chunk,
|
|
766
|
-
finishReason
|
|
792
|
+
finishReason,
|
|
767
793
|
usage,
|
|
768
794
|
response: fullResponse,
|
|
769
795
|
});
|
|
@@ -774,6 +800,10 @@ class DefaultStreamObjectResult<
|
|
|
774
800
|
model: model.modelId,
|
|
775
801
|
});
|
|
776
802
|
|
|
803
|
+
if (terminalError !== undefined) {
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
806
|
+
|
|
777
807
|
self._usage.resolve(usage);
|
|
778
808
|
self._providerMetadata.resolve(providerMetadata);
|
|
779
809
|
self._warnings.resolve(warnings);
|
|
@@ -867,9 +897,19 @@ class DefaultStreamObjectResult<
|
|
|
867
897
|
}),
|
|
868
898
|
);
|
|
869
899
|
|
|
870
|
-
stitchableStream.addStream(transformedStream
|
|
900
|
+
stitchableStream.addStream(transformedStream, {
|
|
901
|
+
onError(error) {
|
|
902
|
+
const wrappedError = wrapGatewayError(error);
|
|
903
|
+
self.rejectResultPromises(wrappedError);
|
|
904
|
+
void notify({
|
|
905
|
+
event: { error: wrappedError },
|
|
906
|
+
callbacks: onError,
|
|
907
|
+
});
|
|
908
|
+
},
|
|
909
|
+
});
|
|
871
910
|
})()
|
|
872
911
|
.catch(async error => {
|
|
912
|
+
self.rejectResultPromises(error);
|
|
873
913
|
await telemetryDispatcher.onError?.({ callId, error });
|
|
874
914
|
|
|
875
915
|
stitchableStream.addStream(
|
|
@@ -888,6 +928,29 @@ class DefaultStreamObjectResult<
|
|
|
888
928
|
this.outputStrategy = outputStrategy;
|
|
889
929
|
}
|
|
890
930
|
|
|
931
|
+
private rejectResultPromises(error: unknown) {
|
|
932
|
+
this.rejectResultPromise({ delayedPromise: this._object, error });
|
|
933
|
+
this.rejectResultPromise({ delayedPromise: this._usage, error });
|
|
934
|
+
this.rejectResultPromise({ delayedPromise: this._providerMetadata, error });
|
|
935
|
+
this.rejectResultPromise({ delayedPromise: this._warnings, error });
|
|
936
|
+
this.rejectResultPromise({ delayedPromise: this._request, error });
|
|
937
|
+
this.rejectResultPromise({ delayedPromise: this._response, error });
|
|
938
|
+
this.rejectResultPromise({ delayedPromise: this._finishReason, error });
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
private rejectResultPromise<T>({
|
|
942
|
+
delayedPromise,
|
|
943
|
+
error,
|
|
944
|
+
}: {
|
|
945
|
+
delayedPromise: DelayedPromise<T>;
|
|
946
|
+
error: unknown;
|
|
947
|
+
}) {
|
|
948
|
+
if (delayedPromise.isPending()) {
|
|
949
|
+
delayedPromise.reject(error);
|
|
950
|
+
markPromiseAsHandled(delayedPromise.promise);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
891
954
|
get object() {
|
|
892
955
|
return this._object.promise;
|
|
893
956
|
}
|
|
@@ -11,6 +11,7 @@ import type { TimeoutConfiguration } from '../prompt/request-options';
|
|
|
11
11
|
import type { Telemetry, TelemetryDispatcher } from '../telemetry/telemetry';
|
|
12
12
|
import { getOwn } from '../util/get-own';
|
|
13
13
|
import { executeToolCall } from './execute-tool-call';
|
|
14
|
+
import { isToolExecutionAllowedFinishReason } from './is-tool-execution-allowed-finish-reason';
|
|
14
15
|
import { resolveToolApproval } from './resolve-tool-approval';
|
|
15
16
|
import type { LanguageModelStreamPart } from './stream-language-model-call';
|
|
16
17
|
import { maybeSignApproval } from './tool-approval-signature';
|
|
@@ -197,6 +198,10 @@ export function executeToolsFromStream<
|
|
|
197
198
|
}
|
|
198
199
|
|
|
199
200
|
case 'model-call-end': {
|
|
201
|
+
if (!isToolExecutionAllowedFinishReason(chunk.finishReason)) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
200
205
|
await Promise.all(
|
|
201
206
|
toolCallsToExecute.map(async toolCall => {
|
|
202
207
|
try {
|
|
@@ -79,6 +79,7 @@ import type {
|
|
|
79
79
|
} from './generate-text-events';
|
|
80
80
|
import type { GenerateTextResult } from './generate-text-result';
|
|
81
81
|
import { DefaultGeneratedFile } from './generated-file';
|
|
82
|
+
import { isToolExecutionAllowedFinishReason } from './is-tool-execution-allowed-finish-reason';
|
|
82
83
|
import type {
|
|
83
84
|
OnLanguageModelCallEndCallback,
|
|
84
85
|
OnLanguageModelCallStartCallback,
|
|
@@ -1259,7 +1260,12 @@ export async function generateText<
|
|
|
1259
1260
|
);
|
|
1260
1261
|
const toolExecutionMs: Record<string, number> = {};
|
|
1261
1262
|
|
|
1262
|
-
if (
|
|
1263
|
+
if (
|
|
1264
|
+
stepExecutionTools != null &&
|
|
1265
|
+
isToolExecutionAllowedFinishReason(
|
|
1266
|
+
currentModelResponse.finishReason.unified,
|
|
1267
|
+
)
|
|
1268
|
+
) {
|
|
1263
1269
|
const toolExecutionResults = await executeTools({
|
|
1264
1270
|
toolCalls: clientToolCalls.filter(
|
|
1265
1271
|
toolCall =>
|
|
@@ -1432,13 +1438,11 @@ export async function generateText<
|
|
|
1432
1438
|
}
|
|
1433
1439
|
}
|
|
1434
1440
|
} while (
|
|
1435
|
-
// Continue
|
|
1436
|
-
//
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
clientToolCalls.length) ||
|
|
1441
|
-
pendingDeferredToolCalls.size > 0) &&
|
|
1441
|
+
// Continue only after all client tool calls have been executed or denied,
|
|
1442
|
+
// and if there are client results or pending deferred provider results.
|
|
1443
|
+
clientToolOutputs.length + deniedToolApprovalResponses.length ===
|
|
1444
|
+
clientToolCalls.length &&
|
|
1445
|
+
(clientToolCalls.length > 0 || pendingDeferredToolCalls.size > 0) &&
|
|
1442
1446
|
// continue until a stop condition is met:
|
|
1443
1447
|
!(await isStopConditionMet({ stopConditions, steps }))
|
|
1444
1448
|
);
|
|
@@ -1193,7 +1193,10 @@ class DefaultStreamTextResult<
|
|
|
1193
1193
|
|
|
1194
1194
|
const { part } = chunk;
|
|
1195
1195
|
|
|
1196
|
-
await
|
|
1196
|
+
await notify({
|
|
1197
|
+
event: { chunk: part },
|
|
1198
|
+
callbacks: onChunk,
|
|
1199
|
+
});
|
|
1197
1200
|
|
|
1198
1201
|
if (part.type === 'error') {
|
|
1199
1202
|
const error = wrapGatewayError(part.error);
|
|
@@ -1202,7 +1205,10 @@ class DefaultStreamTextResult<
|
|
|
1202
1205
|
recordedNoOutputError = error;
|
|
1203
1206
|
}
|
|
1204
1207
|
|
|
1205
|
-
await
|
|
1208
|
+
await notify({
|
|
1209
|
+
event: { error },
|
|
1210
|
+
callbacks: onError,
|
|
1211
|
+
});
|
|
1206
1212
|
}
|
|
1207
1213
|
|
|
1208
1214
|
if (
|
|
@@ -2422,13 +2428,12 @@ class DefaultStreamTextResult<
|
|
|
2422
2428
|
cleanupStepTimeouts();
|
|
2423
2429
|
|
|
2424
2430
|
if (
|
|
2425
|
-
// Continue
|
|
2426
|
-
//
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
deniedToolApprovalResponses.length) ||
|
|
2431
|
+
// Continue only after all client tool calls have been executed or denied,
|
|
2432
|
+
// and if there are client results or pending deferred provider results.
|
|
2433
|
+
clientToolCalls.length ===
|
|
2434
|
+
clientToolOutputs.length +
|
|
2435
|
+
deniedToolApprovalResponses.length &&
|
|
2436
|
+
(clientToolCalls.length > 0 ||
|
|
2432
2437
|
pendingDeferredToolCalls.size > 0) &&
|
|
2433
2438
|
// continue until a stop condition is met:
|
|
2434
2439
|
!(await isStopConditionMet({
|
|
@@ -63,7 +63,7 @@ export async function convertToModelMessages<UI_MESSAGE extends UIMessage>(
|
|
|
63
63
|
part =>
|
|
64
64
|
!isToolUIPart(part) ||
|
|
65
65
|
part.state === 'approval-responded' ||
|
|
66
|
-
part.state === 'output-available' ||
|
|
66
|
+
(part.state === 'output-available' && part.preliminary !== true) ||
|
|
67
67
|
part.state === 'output-error' ||
|
|
68
68
|
part.state === 'output-denied',
|
|
69
69
|
),
|
|
@@ -887,6 +887,23 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
887
887
|
break;
|
|
888
888
|
}
|
|
889
889
|
|
|
890
|
+
case 'reset-step': {
|
|
891
|
+
const currentStepParts = getCurrentStepParts();
|
|
892
|
+
|
|
893
|
+
state.activeTextParts = createIdMap();
|
|
894
|
+
state.activeReasoningParts = createIdMap();
|
|
895
|
+
state.partialToolCalls = createIdMap();
|
|
896
|
+
|
|
897
|
+
if (currentStepParts.length > 0) {
|
|
898
|
+
state.message.parts.splice(
|
|
899
|
+
state.message.parts.length - currentStepParts.length,
|
|
900
|
+
currentStepParts.length,
|
|
901
|
+
);
|
|
902
|
+
write();
|
|
903
|
+
}
|
|
904
|
+
break;
|
|
905
|
+
}
|
|
906
|
+
|
|
890
907
|
case 'start': {
|
|
891
908
|
if (chunk.messageId != null) {
|
|
892
909
|
state.message.id = chunk.messageId;
|
|
@@ -183,6 +183,9 @@ export const uiMessageChunkSchema = lazySchema(() =>
|
|
|
183
183
|
z.looseObject({
|
|
184
184
|
type: z.literal('finish-step'),
|
|
185
185
|
}),
|
|
186
|
+
z.looseObject({
|
|
187
|
+
type: z.literal('reset-step'),
|
|
188
|
+
}),
|
|
186
189
|
z.looseObject({
|
|
187
190
|
type: z.literal('start'),
|
|
188
191
|
messageId: z.string().optional(),
|
|
@@ -378,6 +381,12 @@ export type UIMessageChunk<
|
|
|
378
381
|
| {
|
|
379
382
|
type: 'finish-step';
|
|
380
383
|
}
|
|
384
|
+
| {
|
|
385
|
+
/**
|
|
386
|
+
* Removes all message parts added during the current step.
|
|
387
|
+
*/
|
|
388
|
+
type: 'reset-step';
|
|
389
|
+
}
|
|
381
390
|
| {
|
|
382
391
|
type: 'start';
|
|
383
392
|
messageId?: string;
|