ai 7.0.69 → 7.0.70

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.
@@ -92,7 +92,7 @@ import {
92
92
  } from "@ai-sdk/provider-utils";
93
93
 
94
94
  // src/version.ts
95
- var VERSION = true ? "7.0.69" : "0.0.0-test";
95
+ var VERSION = true ? "7.0.70" : "0.0.0-test";
96
96
 
97
97
  // src/util/download/download.ts
98
98
  var download = async ({
@@ -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` as peer dependencies. The `workflow` package provides the Workflow DevKit runtime (`getWritable`, `'use workflow'`, `'use step'`).
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.
@@ -589,7 +594,7 @@ export type MyAgentUIMessage = InferWorkflowAgentUIMessage<typeof myAgent>;
589
594
  Install the new package alongside `workflow`:
590
595
 
591
596
  ```bash
592
- npm install @ai-sdk/workflow
597
+ npm install @ai-sdk/workflow workflow@beta
593
598
  ```
594
599
 
595
600
  ### Write `ModelCallStreamPart`, not `UIMessageChunk`
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "7.0.69",
3
+ "version": "7.0.70",
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.55",
45
+ "@ai-sdk/gateway": "4.0.56",
46
46
  "@ai-sdk/provider": "4.0.7",
47
- "@ai-sdk/provider-utils": "5.0.27"
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
  *
@@ -656,6 +662,7 @@ class DefaultStreamObjectResult<
656
662
  let providerMetadata: ProviderMetadata | undefined;
657
663
  let object: RESULT | undefined;
658
664
  let error: unknown | undefined;
665
+ let terminalError: { error: unknown } | undefined;
659
666
  let msToFirstChunk: number | undefined = undefined;
660
667
 
661
668
  let accumulatedText = '';
@@ -751,19 +758,35 @@ class DefaultStreamObjectResult<
751
758
  break;
752
759
  }
753
760
 
761
+ case 'error': {
762
+ if (terminalError === undefined) {
763
+ const wrappedError = wrapGatewayError(chunk.error);
764
+ terminalError = { error: wrappedError };
765
+ error = wrappedError;
766
+ finishReason = 'error';
767
+ self.rejectResultPromises(wrappedError);
768
+ }
769
+
770
+ controller.enqueue(chunk);
771
+ break;
772
+ }
773
+
754
774
  case 'finish': {
755
775
  if (textDelta !== '') {
756
776
  controller.enqueue({ type: 'text-delta', textDelta });
757
777
  }
758
778
 
759
- finishReason = chunk.finishReason.unified;
779
+ finishReason =
780
+ terminalError === undefined
781
+ ? chunk.finishReason.unified
782
+ : 'error';
760
783
 
761
784
  usage = asLanguageModelUsage(chunk.usage);
762
785
  providerMetadata = chunk.providerMetadata;
763
786
 
764
787
  controller.enqueue({
765
788
  ...chunk,
766
- finishReason: chunk.finishReason.unified,
789
+ finishReason,
767
790
  usage,
768
791
  response: fullResponse,
769
792
  });
@@ -774,6 +797,10 @@ class DefaultStreamObjectResult<
774
797
  model: model.modelId,
775
798
  });
776
799
 
800
+ if (terminalError !== undefined) {
801
+ break;
802
+ }
803
+
777
804
  self._usage.resolve(usage);
778
805
  self._providerMetadata.resolve(providerMetadata);
779
806
  self._warnings.resolve(warnings);
@@ -867,9 +894,19 @@ class DefaultStreamObjectResult<
867
894
  }),
868
895
  );
869
896
 
870
- stitchableStream.addStream(transformedStream);
897
+ stitchableStream.addStream(transformedStream, {
898
+ onError(error) {
899
+ const wrappedError = wrapGatewayError(error);
900
+ self.rejectResultPromises(wrappedError);
901
+ void notify({
902
+ event: { error: wrappedError },
903
+ callbacks: onError,
904
+ });
905
+ },
906
+ });
871
907
  })()
872
908
  .catch(async error => {
909
+ self.rejectResultPromises(error);
873
910
  await telemetryDispatcher.onError?.({ callId, error });
874
911
 
875
912
  stitchableStream.addStream(
@@ -888,6 +925,29 @@ class DefaultStreamObjectResult<
888
925
  this.outputStrategy = outputStrategy;
889
926
  }
890
927
 
928
+ private rejectResultPromises(error: unknown) {
929
+ this.rejectResultPromise({ delayedPromise: this._object, error });
930
+ this.rejectResultPromise({ delayedPromise: this._usage, error });
931
+ this.rejectResultPromise({ delayedPromise: this._providerMetadata, error });
932
+ this.rejectResultPromise({ delayedPromise: this._warnings, error });
933
+ this.rejectResultPromise({ delayedPromise: this._request, error });
934
+ this.rejectResultPromise({ delayedPromise: this._response, error });
935
+ this.rejectResultPromise({ delayedPromise: this._finishReason, error });
936
+ }
937
+
938
+ private rejectResultPromise<T>({
939
+ delayedPromise,
940
+ error,
941
+ }: {
942
+ delayedPromise: DelayedPromise<T>;
943
+ error: unknown;
944
+ }) {
945
+ if (delayedPromise.isPending()) {
946
+ delayedPromise.reject(error);
947
+ markPromiseAsHandled(delayedPromise.promise);
948
+ }
949
+ }
950
+
891
951
  get object() {
892
952
  return this._object.promise;
893
953
  }
@@ -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 (stepExecutionTools != null) {
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 if:
1436
- // 1. There are client tool calls that have all been executed or denied, OR
1437
- // 2. There are pending deferred results from provider-executed tools
1438
- ((clientToolCalls.length > 0 &&
1439
- clientToolOutputs.length + deniedToolApprovalResponses.length ===
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
  );
@@ -0,0 +1,7 @@
1
+ import type { FinishReason } from '../types/language-model';
2
+
3
+ export function isToolExecutionAllowedFinishReason(
4
+ finishReason: FinishReason,
5
+ ): boolean {
6
+ return finishReason === 'stop' || finishReason === 'tool-calls';
7
+ }
@@ -2422,13 +2422,12 @@ class DefaultStreamTextResult<
2422
2422
  cleanupStepTimeouts();
2423
2423
 
2424
2424
  if (
2425
- // Continue if:
2426
- // 1. There are client tool calls that have all been executed or denied, OR
2427
- // 2. There are pending deferred results from provider-executed tools, OR
2428
- ((clientToolCalls.length > 0 &&
2429
- clientToolCalls.length ===
2430
- clientToolOutputs.length +
2431
- deniedToolApprovalResponses.length) ||
2425
+ // Continue only after all client tool calls have been executed or denied,
2426
+ // and if there are client results or pending deferred provider results.
2427
+ clientToolCalls.length ===
2428
+ clientToolOutputs.length +
2429
+ deniedToolApprovalResponses.length &&
2430
+ (clientToolCalls.length > 0 ||
2432
2431
  pendingDeferredToolCalls.size > 0) &&
2433
2432
  // continue until a stop condition is met:
2434
2433
  !(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;