ai 7.0.30 → 7.0.32

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.
@@ -1043,7 +1043,7 @@ The following tool input lifecycle hooks are available:
1043
1043
  - **`onInputDelta`**: Called for each chunk of text as the input is streamed
1044
1044
  - **`onInputAvailable`**: Called when the complete input is available and validated
1045
1045
 
1046
- `onInputStart` and `onInputDelta` are only called in streaming contexts (when using `streamText`). They are not called when using `generateText`.
1046
+ `onInputStart` is always called before `onInputAvailable`, including when using `generateText`. `onInputDelta` is only called in streaming contexts (when using `streamText`).
1047
1047
 
1048
1048
  ### Example
1049
1049
 
@@ -100,6 +100,10 @@ or a message-array `prompt`, `HarnessAgent` takes the latest user message as the
100
100
  fresh input for the turn. It does not replay the full prior conversation into
101
101
  the harness.
102
102
 
103
+ A trailing tool message is handled differently: tool approval responses and
104
+ client-provided tool results continue the unfinished harness turn that produced
105
+ the corresponding request or tool call.
106
+
103
107
  This is different from model calls, where the application usually sends the full
104
108
  message history. In chat routes, persist and resume the harness session instead
105
109
  of relying on message replay.
@@ -116,6 +120,8 @@ End every session explicitly:
116
120
  the turn is unfinished, the resume state includes the continuation state.
117
121
  - `session.suspendTurn()` is for advanced active-turn continuation across a
118
122
  process boundary.
123
+ - `session.hasUnfinishedTurn()` reports whether the current turn must be
124
+ continued or suspended before the session accepts a new prompt.
119
125
 
120
126
  Use `destroy()` for one-off scripts and tests. Use `detach()` or `stop()` for
121
127
  HTTP routes that need multi-turn continuity.
@@ -165,8 +171,10 @@ For advanced workflows that must hand off an active turn across a process
165
171
  boundary, suspend the turn and persist the continuation state:
166
172
 
167
173
  ```ts
168
- const continuationState = await session.suspendTurn();
169
- await persistContinuationState({ chatId, continuationState });
174
+ if (session.hasUnfinishedTurn()) {
175
+ const continuationState = await session.suspendTurn();
176
+ await persistContinuationState({ chatId, continuationState });
177
+ }
170
178
  ```
171
179
 
172
180
  When you only have raw continuation state from `suspendTurn()`, resume with
@@ -91,6 +91,46 @@ const agent = new HarnessAgent({
91
91
  When the harness calls `weather`, `HarnessAgent` executes the tool in your host
92
92
  process, then submits the result back to the harness runtime.
93
93
 
94
+ ## Client-Side Tools
95
+
96
+ Omit `execute` when a browser, user interaction, or another external process
97
+ provides the tool result:
98
+
99
+ ```ts
100
+ const weather = tool({
101
+ description: 'Get the current temperature for a city.',
102
+ inputSchema: z.object({ city: z.string() }),
103
+ });
104
+ ```
105
+
106
+ When the harness calls a tool without `execute`, the returned result slice ends
107
+ after the tool-call step while the underlying turn waits for a result.
108
+ `session.hasUnfinishedTurn()` remains `true`, and `session.suspendTurn()`
109
+ includes the pending tool call in its serializable continuation state.
110
+
111
+ In UI flows, pass the model messages produced after `addToolOutput` to the next
112
+ `stream()` or `generate()` call. `HarnessAgent` extracts the trailing tool
113
+ result and continues the paused turn automatically.
114
+
115
+ For direct agent calls, provide the raw result to `continueStream()` or
116
+ `continueGenerate()`:
117
+
118
+ ```ts
119
+ const continued = await agent.continueStream({
120
+ session,
121
+ toolResultContinuations: [
122
+ {
123
+ toolCallId,
124
+ output: { city: 'Paris', celsius: 12 },
125
+ },
126
+ ],
127
+ });
128
+ ```
129
+
130
+ Set `isError: true` when the external tool failed. To continue in another
131
+ process, call `suspendTurn()`, recreate the session with `continueFrom`, and
132
+ then pass the result to `continueStream()` or `continueGenerate()`.
133
+
94
134
  ## Tool Filtering
95
135
 
96
136
  Use `activeTools` or `inactiveTools` on `HarnessAgent` to control which tools the
@@ -171,7 +171,7 @@ export const weatherTool = tool({
171
171
  isOptional: true,
172
172
  type: '(options: ToolExecutionOptions<CONTEXT>) => void | PromiseLike<void>',
173
173
  description:
174
- 'Optional function that is called when the argument streaming starts. Only called when the tool is used in a streaming context.',
174
+ 'Optional function that is called when the model starts generating the tool input. In non-streaming contexts, it is called immediately before onInputAvailable.',
175
175
  },
176
176
  {
177
177
  name: 'onInputDelta',
@@ -130,7 +130,7 @@ export const customTool = dynamicTool({
130
130
  isOptional: true,
131
131
  type: '(options: ToolExecutionOptions<Context>) => void | PromiseLike<void>',
132
132
  description:
133
- 'Optional function that is called when the argument streaming starts. Only called when the tool is used in a streaming context.'
133
+ 'Optional function that is called when the model starts generating the tool input. In non-streaming contexts, it is called immediately before onInputAvailable.'
134
134
  },
135
135
  {
136
136
  name: 'onInputDelta',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "7.0.30",
3
+ "version": "7.0.32",
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.22",
45
+ "@ai-sdk/gateway": "4.0.24",
46
46
  "@ai-sdk/provider": "4.0.3",
47
- "@ai-sdk/provider-utils": "5.0.10"
47
+ "@ai-sdk/provider-utils": "5.0.11"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@edge-runtime/vm": "^5.0.0",
@@ -2,17 +2,18 @@ import type {
2
2
  ModelMessage,
3
3
  ToolApprovalRequest,
4
4
  ToolApprovalResponse,
5
+ ToolResultPart,
5
6
  ToolSet,
6
7
  } from '@ai-sdk/provider-utils';
7
8
  import { InvalidToolApprovalError } from '../error/invalid-tool-approval-error';
8
9
  import { ToolCallNotFoundForApprovalError } from '../error/tool-call-not-found-for-approval-error';
9
10
  import type { TypedToolCall } from './tool-call';
10
- import type { TypedToolResult } from './tool-result';
11
11
 
12
12
  export type CollectedToolApprovals<TOOLS extends ToolSet> = {
13
13
  approvalRequest: ToolApprovalRequest;
14
14
  approvalResponse: ToolApprovalResponse;
15
15
  toolCall: TypedToolCall<TOOLS>;
16
+ existingToolResult?: ToolResultPart;
16
17
  };
17
18
 
18
19
  /**
@@ -75,12 +76,10 @@ export function collectToolApprovals<TOOLS extends ToolSet>({
75
76
  }
76
77
 
77
78
  // gather tool results from the last tool message
78
- const toolResults: Record<string, TypedToolResult<TOOLS>> = Object.create(
79
- null,
80
- );
79
+ const toolResults: Record<string, ToolResultPart> = Object.create(null);
81
80
  for (const part of lastMessage.content) {
82
81
  if (part.type === 'tool-result') {
83
- toolResults[part.toolCallId] = part as TypedToolResult<TOOLS>;
82
+ toolResults[part.toolCallId] = part;
84
83
  }
85
84
  }
86
85
 
@@ -100,7 +99,12 @@ export function collectToolApprovals<TOOLS extends ToolSet>({
100
99
  });
101
100
  }
102
101
 
103
- if (toolResults[approvalRequest.toolCallId] != null) {
102
+ const existingToolResult = toolResults[approvalRequest.toolCallId];
103
+ if (
104
+ existingToolResult != null &&
105
+ (approvalResponse.approved ||
106
+ existingToolResult.output.type !== 'execution-denied')
107
+ ) {
104
108
  continue;
105
109
  }
106
110
 
@@ -116,6 +120,7 @@ export function collectToolApprovals<TOOLS extends ToolSet>({
116
120
  approvalRequest,
117
121
  approvalResponse,
118
122
  toolCall,
123
+ ...(existingToolResult != null ? { existingToolResult } : {}),
119
124
  };
120
125
 
121
126
  if (approvalResponse.approved) {
@@ -682,9 +682,12 @@ export async function generateText<
682
682
  ...collectedDeniedToolApprovals,
683
683
  ...revalidationDeniedToolApprovals,
684
684
  ];
685
+ const deniedToolApprovalsWithoutResults = deniedToolApprovals.filter(
686
+ toolApproval => toolApproval.existingToolResult == null,
687
+ );
685
688
 
686
689
  if (
687
- deniedToolApprovals.length > 0 ||
690
+ deniedToolApprovalsWithoutResults.length > 0 ||
688
691
  localApprovedToolApprovals.length > 0
689
692
  ) {
690
693
  const toolResults = await executeTools({
@@ -741,7 +744,7 @@ export async function generateText<
741
744
  }
742
745
 
743
746
  // add execution denied tool results for all denied tool approvals:
744
- for (const toolApproval of deniedToolApprovals) {
747
+ for (const toolApproval of deniedToolApprovalsWithoutResults) {
745
748
  toolContent.push({
746
749
  type: 'tool-result' as const,
747
750
  toolCallId: toolApproval.toolCall.toolCallId,
@@ -792,6 +795,10 @@ export async function generateText<
792
795
  const pendingDeferredToolCalls = new Map<string, { toolName: string }>();
793
796
 
794
797
  do {
798
+ if (steps.length > 0) {
799
+ mergedAbortSignal?.throwIfAborted();
800
+ }
801
+
795
802
  // Set up step timeout if configured
796
803
  const stepTimeoutId = setAbortTimeout({
797
804
  abortController: stepAbortController,
@@ -1057,6 +1064,15 @@ export async function generateText<
1057
1064
  continue;
1058
1065
  }
1059
1066
 
1067
+ if (tool.onInputStart != null) {
1068
+ await tool.onInputStart({
1069
+ toolCallId: toolCall.toolCallId,
1070
+ messages: stepMessages,
1071
+ abortSignal: mergedAbortSignal,
1072
+ context: runtimeContext,
1073
+ });
1074
+ }
1075
+
1060
1076
  if (tool?.onInputAvailable != null) {
1061
1077
  await tool.onInputAvailable({
1062
1078
  input: toolCall.input,
@@ -1637,6 +1637,10 @@ class DefaultStreamTextResult<
1637
1637
  ),
1638
1638
  ...revalidationDeniedToolApprovals,
1639
1639
  ];
1640
+ const localDeniedToolApprovalsWithoutResults =
1641
+ localDeniedToolApprovals.filter(
1642
+ toolApproval => toolApproval.existingToolResult == null,
1643
+ );
1640
1644
 
1641
1645
  const deniedProviderExecutedToolApprovals = deniedToolApprovals.filter(
1642
1646
  toolApproval => toolApproval.toolCall.providerExecuted,
@@ -1703,7 +1707,10 @@ class DefaultStreamTextResult<
1703
1707
  );
1704
1708
 
1705
1709
  // Local tool results (approved + denied) are sent as tool results:
1706
- if (toolOutputs.length > 0 || localDeniedToolApprovals.length > 0) {
1710
+ if (
1711
+ toolOutputs.length > 0 ||
1712
+ localDeniedToolApprovalsWithoutResults.length > 0
1713
+ ) {
1707
1714
  const localToolContent: ToolContent = [];
1708
1715
 
1709
1716
  // add regular tool results for approved tool calls:
@@ -1726,7 +1733,7 @@ class DefaultStreamTextResult<
1726
1733
  }
1727
1734
 
1728
1735
  // add execution denied tool results for denied local tool approvals:
1729
- for (const toolApproval of localDeniedToolApprovals) {
1736
+ for (const toolApproval of localDeniedToolApprovalsWithoutResults) {
1730
1737
  localToolContent.push({
1731
1738
  type: 'tool-result' as const,
1732
1739
  toolCallId: toolApproval.toolCall.toolCallId,
package/src/ui/chat.ts CHANGED
@@ -208,8 +208,8 @@ export interface ChatInit<UI_MESSAGE extends UIMessage> {
208
208
  * Optional callback function that is invoked when a tool call is received.
209
209
  * Intended for automatic client-side tool execution.
210
210
  *
211
- * You can optionally return a result for the tool call,
212
- * either synchronously or asynchronously.
211
+ * To add the tool output, call `addToolOutput` without awaiting it inside
212
+ * this callback. The callback's return value is not used.
213
213
  */
214
214
  onToolCall?: ChatOnToolCallCallback<UI_MESSAGE>;
215
215
 
@@ -21,29 +21,29 @@ const toolMetadataSchema: z.ZodType<JSONObject> = z.record(
21
21
  );
22
22
 
23
23
  export const uiMessageChunkSchema = lazySchema(() =>
24
- zodSchema(
24
+ zodSchema<UIMessageChunk>(
25
25
  z.union([
26
- z.strictObject({
26
+ z.looseObject({
27
27
  type: z.literal('text-start'),
28
28
  id: z.string(),
29
29
  providerMetadata: providerMetadataSchema.optional(),
30
30
  }),
31
- z.strictObject({
31
+ z.looseObject({
32
32
  type: z.literal('text-delta'),
33
33
  id: z.string(),
34
34
  delta: z.string(),
35
35
  providerMetadata: providerMetadataSchema.optional(),
36
36
  }),
37
- z.strictObject({
37
+ z.looseObject({
38
38
  type: z.literal('text-end'),
39
39
  id: z.string(),
40
40
  providerMetadata: providerMetadataSchema.optional(),
41
41
  }),
42
- z.strictObject({
42
+ z.looseObject({
43
43
  type: z.literal('error'),
44
44
  errorText: z.string(),
45
45
  }),
46
- z.strictObject({
46
+ z.looseObject({
47
47
  type: z.literal('tool-input-start'),
48
48
  toolCallId: z.string(),
49
49
  toolName: z.string(),
@@ -53,12 +53,12 @@ export const uiMessageChunkSchema = lazySchema(() =>
53
53
  dynamic: z.boolean().optional(),
54
54
  title: z.string().optional(),
55
55
  }),
56
- z.strictObject({
56
+ z.looseObject({
57
57
  type: z.literal('tool-input-delta'),
58
58
  toolCallId: z.string(),
59
59
  inputTextDelta: z.string(),
60
60
  }),
61
- z.strictObject({
61
+ z.looseObject({
62
62
  type: z.literal('tool-input-available'),
63
63
  toolCallId: z.string(),
64
64
  toolName: z.string(),
@@ -69,7 +69,7 @@ export const uiMessageChunkSchema = lazySchema(() =>
69
69
  dynamic: z.boolean().optional(),
70
70
  title: z.string().optional(),
71
71
  }),
72
- z.strictObject({
72
+ z.looseObject({
73
73
  type: z.literal('tool-input-error'),
74
74
  toolCallId: z.string(),
75
75
  toolName: z.string(),
@@ -81,14 +81,14 @@ export const uiMessageChunkSchema = lazySchema(() =>
81
81
  errorText: z.string(),
82
82
  title: z.string().optional(),
83
83
  }),
84
- z.strictObject({
84
+ z.looseObject({
85
85
  type: z.literal('tool-approval-request'),
86
86
  approvalId: z.string(),
87
87
  toolCallId: z.string(),
88
88
  isAutomatic: z.boolean().optional(),
89
89
  signature: z.string().optional(),
90
90
  }),
91
- z.strictObject({
91
+ z.looseObject({
92
92
  type: z.literal('tool-approval-response'),
93
93
  approvalId: z.string(),
94
94
  approved: z.boolean(),
@@ -96,7 +96,7 @@ export const uiMessageChunkSchema = lazySchema(() =>
96
96
  providerExecuted: z.boolean().optional(),
97
97
  providerMetadata: providerMetadataSchema.optional(),
98
98
  }),
99
- z.strictObject({
99
+ z.looseObject({
100
100
  type: z.literal('tool-output-available'),
101
101
  toolCallId: z.string(),
102
102
  output: z.unknown(),
@@ -106,7 +106,7 @@ export const uiMessageChunkSchema = lazySchema(() =>
106
106
  dynamic: z.boolean().optional(),
107
107
  preliminary: z.boolean().optional(),
108
108
  }),
109
- z.strictObject({
109
+ z.looseObject({
110
110
  type: z.literal('tool-output-error'),
111
111
  toolCallId: z.string(),
112
112
  errorText: z.string(),
@@ -115,39 +115,39 @@ export const uiMessageChunkSchema = lazySchema(() =>
115
115
  toolMetadata: toolMetadataSchema.optional(),
116
116
  dynamic: z.boolean().optional(),
117
117
  }),
118
- z.strictObject({
118
+ z.looseObject({
119
119
  type: z.literal('tool-output-denied'),
120
120
  toolCallId: z.string(),
121
121
  }),
122
- z.strictObject({
122
+ z.looseObject({
123
123
  type: z.literal('reasoning-start'),
124
124
  id: z.string(),
125
125
  providerMetadata: providerMetadataSchema.optional(),
126
126
  }),
127
- z.strictObject({
127
+ z.looseObject({
128
128
  type: z.literal('reasoning-delta'),
129
129
  id: z.string(),
130
130
  delta: z.string(),
131
131
  providerMetadata: providerMetadataSchema.optional(),
132
132
  }),
133
- z.strictObject({
133
+ z.looseObject({
134
134
  type: z.literal('reasoning-end'),
135
135
  id: z.string(),
136
136
  providerMetadata: providerMetadataSchema.optional(),
137
137
  }),
138
- z.strictObject({
138
+ z.looseObject({
139
139
  type: z.literal('custom'),
140
140
  kind: z.string().transform(value => value as `${string}.${string}`),
141
141
  providerMetadata: providerMetadataSchema.optional(),
142
142
  }),
143
- z.strictObject({
143
+ z.looseObject({
144
144
  type: z.literal('source-url'),
145
145
  sourceId: z.string(),
146
146
  url: z.string(),
147
147
  title: z.string().optional(),
148
148
  providerMetadata: providerMetadataSchema.optional(),
149
149
  }),
150
- z.strictObject({
150
+ z.looseObject({
151
151
  type: z.literal('source-document'),
152
152
  sourceId: z.string(),
153
153
  mediaType: z.string(),
@@ -155,19 +155,19 @@ export const uiMessageChunkSchema = lazySchema(() =>
155
155
  filename: z.string().optional(),
156
156
  providerMetadata: providerMetadataSchema.optional(),
157
157
  }),
158
- z.strictObject({
158
+ z.looseObject({
159
159
  type: z.literal('file'),
160
160
  url: z.string(),
161
161
  mediaType: z.string(),
162
162
  providerMetadata: providerMetadataSchema.optional(),
163
163
  }),
164
- z.strictObject({
164
+ z.looseObject({
165
165
  type: z.literal('reasoning-file'),
166
166
  url: z.string(),
167
167
  mediaType: z.string(),
168
168
  providerMetadata: providerMetadataSchema.optional(),
169
169
  }),
170
- z.strictObject({
170
+ z.looseObject({
171
171
  type: z.custom<`data-${string}`>(
172
172
  (value): value is `data-${string}` =>
173
173
  typeof value === 'string' && value.startsWith('data-'),
@@ -177,18 +177,18 @@ export const uiMessageChunkSchema = lazySchema(() =>
177
177
  data: z.unknown(),
178
178
  transient: z.boolean().optional(),
179
179
  }),
180
- z.strictObject({
180
+ z.looseObject({
181
181
  type: z.literal('start-step'),
182
182
  }),
183
- z.strictObject({
183
+ z.looseObject({
184
184
  type: z.literal('finish-step'),
185
185
  }),
186
- z.strictObject({
186
+ z.looseObject({
187
187
  type: z.literal('start'),
188
188
  messageId: z.string().optional(),
189
189
  messageMetadata: z.unknown().optional(),
190
190
  }),
191
- z.strictObject({
191
+ z.looseObject({
192
192
  type: z.literal('finish'),
193
193
  finishReason: z
194
194
  .enum([
@@ -202,11 +202,11 @@ export const uiMessageChunkSchema = lazySchema(() =>
202
202
  .optional(),
203
203
  messageMetadata: z.unknown().optional(),
204
204
  }),
205
- z.strictObject({
205
+ z.looseObject({
206
206
  type: z.literal('abort'),
207
207
  reason: z.string().optional(),
208
208
  }),
209
- z.strictObject({
209
+ z.looseObject({
210
210
  type: z.literal('message-metadata'),
211
211
  messageMetadata: z.unknown(),
212
212
  }),