ai 7.0.31 → 7.0.33

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.
@@ -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
@@ -114,7 +114,7 @@ import { dataPartsSchema, metadataSchema } from '@util/schemas';
114
114
  const tools = {
115
115
  weather: tool({
116
116
  description: 'Get weather information',
117
- parameters: z.object({
117
+ inputSchema: z.object({
118
118
  location: z.string(),
119
119
  units: z.enum(['celsius', 'fahrenheit']),
120
120
  }),
@@ -238,7 +238,7 @@ export async function POST(request) {
238
238
  tools: {
239
239
  displayWeather: {
240
240
  description: 'Display the weather for a location',
241
- parameters: z.object({
241
+ inputSchema: z.object({
242
242
  latitude: z.number(),
243
243
  longitude: z.number(),
244
244
  }),
@@ -55,7 +55,7 @@ const dataSchemas = {
55
55
  const tools = {
56
56
  weather: tool({
57
57
  description: 'Get weather info',
58
- parameters: z.object({
58
+ inputSchema: z.object({
59
59
  location: z.string(),
60
60
  }),
61
61
  execute: async ({ location }) => `Weather in ${location}: sunny`,
@@ -61,7 +61,7 @@ const dataSchemas = {
61
61
  const tools = {
62
62
  weather: tool({
63
63
  description: 'Get weather info',
64
- parameters: z.object({
64
+ inputSchema: z.object({
65
65
  location: z.string(),
66
66
  }),
67
67
  execute: async ({ location }) => `Weather in ${location}: sunny`,
@@ -121,7 +121,7 @@ const result = await generateText({
121
121
  calculate: calculateTool,
122
122
  finalAnswer: {
123
123
  description: 'Provide the final answer to the user',
124
- parameters: z.object({
124
+ inputSchema: z.object({
125
125
  answer: z.string(),
126
126
  }),
127
127
  execute: async ({ answer }) => answer,
@@ -237,7 +237,7 @@ import { z } from 'zod';
237
237
 
238
238
  const weatherTool = tool({
239
239
  description: 'Get the current weather',
240
- parameters: z.object({
240
+ inputSchema: z.object({
241
241
  location: z.string().describe('The city and state'),
242
242
  }),
243
243
  execute: async ({ location }) => {
@@ -25,7 +25,7 @@ You have two options for handling tool results:
25
25
  const tools = {
26
26
  weather: tool({
27
27
  description: 'Get the weather in a location',
28
- parameters: z.object({
28
+ inputSchema: z.object({
29
29
  location: z
30
30
  .string()
31
31
  .describe('The city and state, e.g. "San Francisco, CA"'),
@@ -20,7 +20,7 @@ export async function POST(req: Request) {
20
20
  tools: {
21
21
  weather: {
22
22
  description: 'Get the weather for a location',
23
- parameters: z.object({
23
+ inputSchema: z.object({
24
24
  location: z.string(),
25
25
  }),
26
26
  execute: async ({ location }) => {
@@ -54,7 +54,7 @@ export async function POST(req: Request) {
54
54
  tools: {
55
55
  weather: {
56
56
  description: 'Get the weather for a location',
57
- parameters: z.object({
57
+ inputSchema: z.object({
58
58
  location: z.string(),
59
59
  }),
60
60
  execute: async ({ location }) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "7.0.31",
3
+ "version": "7.0.33",
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.23",
45
+ "@ai-sdk/gateway": "4.0.25",
46
46
  "@ai-sdk/provider": "4.0.3",
47
- "@ai-sdk/provider-utils": "5.0.11"
47
+ "@ai-sdk/provider-utils": "5.0.12"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@edge-runtime/vm": "^5.0.0",
@@ -795,6 +795,10 @@ export async function generateText<
795
795
  const pendingDeferredToolCalls = new Map<string, { toolName: string }>();
796
796
 
797
797
  do {
798
+ if (steps.length > 0) {
799
+ mergedAbortSignal?.throwIfAborted();
800
+ }
801
+
798
802
  // Set up step timeout if configured
799
803
  const stepTimeoutId = setAbortTimeout({
800
804
  abortController: stepAbortController,
@@ -25,6 +25,7 @@ import {
25
25
  createDefaultDownloadFunction,
26
26
  type DownloadFunction,
27
27
  } from '../util/download/download-function';
28
+ import { mergeObjects } from '../util/merge-objects';
28
29
  import { convertToLanguageModelV4FilePart } from './file-part-data';
29
30
  import { logWarnings } from '../logger/log-warnings';
30
31
  import type { Warning } from '../types/warning';
@@ -108,7 +109,19 @@ export async function convertToLanguageModelPrompt({
108
109
 
109
110
  const lastCombinedMessage = combinedMessages.at(-1);
110
111
  if (lastCombinedMessage?.role === 'tool') {
112
+ const lastContentPart = lastCombinedMessage.content.at(-1);
113
+ if (
114
+ lastContentPart != null &&
115
+ lastCombinedMessage.providerOptions != null
116
+ ) {
117
+ lastContentPart.providerOptions = mergeObjects(
118
+ lastCombinedMessage.providerOptions,
119
+ lastContentPart.providerOptions,
120
+ );
121
+ }
122
+
111
123
  lastCombinedMessage.content.push(...message.content);
124
+ lastCombinedMessage.providerOptions = message.providerOptions;
112
125
  } else {
113
126
  combinedMessages.push(message);
114
127
  }
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
 
@@ -104,13 +104,43 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
104
104
  new TransformStream<UIMessageChunk, InferUIMessageChunk<UI_MESSAGE>>({
105
105
  async transform(chunk, controller) {
106
106
  await runUpdateMessageJob(async ({ state, write }) => {
107
+ function getCurrentStepParts() {
108
+ const parts = state.message.parts;
109
+ let currentStepStartIndex = parts.length - 1;
110
+
111
+ while (
112
+ currentStepStartIndex >= 0 &&
113
+ parts[currentStepStartIndex].type !== 'step-start'
114
+ ) {
115
+ currentStepStartIndex--;
116
+ }
117
+
118
+ return parts.slice(currentStepStartIndex + 1);
119
+ }
120
+
121
+ function getCurrentStepToolInvocations() {
122
+ return getCurrentStepParts().filter(isToolUIPart);
123
+ }
124
+
107
125
  function getToolInvocation(toolCallId: string) {
108
- const toolInvocations = state.message.parts.filter(isToolUIPart);
126
+ const toolInvocations = getCurrentStepToolInvocations();
109
127
 
110
- const toolInvocation = toolInvocations.find(
128
+ let toolInvocation = toolInvocations.find(
111
129
  invocation => invocation.toolCallId === toolCallId,
112
130
  );
113
131
 
132
+ if (toolInvocation == null) {
133
+ const parts = state.message.parts;
134
+
135
+ for (let i = parts.length - 1; i >= 0; i--) {
136
+ const part = parts[i];
137
+ if (isToolUIPart(part) && part.toolCallId === toolCallId) {
138
+ toolInvocation = part;
139
+ break;
140
+ }
141
+ }
142
+ }
143
+
114
144
  if (toolInvocation == null) {
115
145
  throw new UIMessageStreamError({
116
146
  chunkType: 'tool-invocation',
@@ -177,12 +207,15 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
177
207
  providerMetadata?: ProviderMetadata;
178
208
  }
179
209
  ),
210
+ existingPart?: ToolUIPart<InferUIMessageTools<UI_MESSAGE>>,
180
211
  ) {
181
- const part = state.message.parts.find(
182
- part =>
183
- isStaticToolUIPart(part) &&
184
- part.toolCallId === options.toolCallId,
185
- ) as ToolUIPart<InferUIMessageTools<UI_MESSAGE>> | undefined;
212
+ const part =
213
+ existingPart ??
214
+ (getCurrentStepParts().find(
215
+ part =>
216
+ isStaticToolUIPart(part) &&
217
+ part.toolCallId === options.toolCallId,
218
+ ) as ToolUIPart<InferUIMessageTools<UI_MESSAGE>> | undefined);
186
219
 
187
220
  const anyOptions = options as any;
188
221
  const anyPart = part as any;
@@ -284,12 +317,15 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
284
317
  providerMetadata?: ProviderMetadata;
285
318
  }
286
319
  ),
320
+ existingPart?: DynamicToolUIPart,
287
321
  ) {
288
- const part = state.message.parts.find(
289
- part =>
290
- part.type === 'dynamic-tool' &&
291
- part.toolCallId === options.toolCallId,
292
- ) as DynamicToolUIPart | undefined;
322
+ const part =
323
+ existingPart ??
324
+ (getCurrentStepParts().find(
325
+ part =>
326
+ part.type === 'dynamic-tool' &&
327
+ part.toolCallId === options.toolCallId,
328
+ ) as DynamicToolUIPart | undefined);
293
329
 
294
330
  const anyOptions = options as any;
295
331
  const anyPart = part as any;
@@ -540,7 +576,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
540
576
 
541
577
  case 'tool-input-start': {
542
578
  const toolInvocations =
543
- state.message.parts.filter(isStaticToolUIPart);
579
+ getCurrentStepParts().filter(isStaticToolUIPart);
544
580
 
545
581
  // add the partial tool call to the map
546
582
  state.partialToolCalls[chunk.toolCallId] = {
@@ -665,7 +701,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
665
701
  // When a part already exists for this toolCallId (e.g. from
666
702
  // tool-input-start), honour its type so we update in place
667
703
  // instead of creating a duplicate with a mismatched type.
668
- const existingPart = state.message.parts
704
+ const existingPart = getCurrentStepParts()
669
705
  .filter(isToolUIPart)
670
706
  .find(p => p.toolCallId === chunk.toolCallId);
671
707
  const isDynamic =
@@ -756,31 +792,37 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
756
792
  const toolInvocation = getToolInvocation(chunk.toolCallId);
757
793
 
758
794
  if (toolInvocation.type === 'dynamic-tool') {
759
- updateDynamicToolPart({
760
- toolCallId: chunk.toolCallId,
761
- toolName: toolInvocation.toolName,
762
- state: 'output-available',
763
- input: (toolInvocation as any).input,
764
- output: chunk.output,
765
- preliminary: chunk.preliminary,
766
- providerExecuted: chunk.providerExecuted,
767
- providerMetadata: chunk.providerMetadata,
768
- title: toolInvocation.title,
769
- toolMetadata: toolInvocation.toolMetadata,
770
- });
795
+ updateDynamicToolPart(
796
+ {
797
+ toolCallId: chunk.toolCallId,
798
+ toolName: toolInvocation.toolName,
799
+ state: 'output-available',
800
+ input: (toolInvocation as any).input,
801
+ output: chunk.output,
802
+ preliminary: chunk.preliminary,
803
+ providerExecuted: chunk.providerExecuted,
804
+ providerMetadata: chunk.providerMetadata,
805
+ title: toolInvocation.title,
806
+ toolMetadata: toolInvocation.toolMetadata,
807
+ },
808
+ toolInvocation,
809
+ );
771
810
  } else {
772
- updateToolPart({
773
- toolCallId: chunk.toolCallId,
774
- toolName: getStaticToolName(toolInvocation),
775
- state: 'output-available',
776
- input: (toolInvocation as any).input,
777
- output: chunk.output,
778
- providerExecuted: chunk.providerExecuted,
779
- preliminary: chunk.preliminary,
780
- providerMetadata: chunk.providerMetadata,
781
- title: toolInvocation.title,
782
- toolMetadata: toolInvocation.toolMetadata,
783
- });
811
+ updateToolPart(
812
+ {
813
+ toolCallId: chunk.toolCallId,
814
+ toolName: getStaticToolName(toolInvocation),
815
+ state: 'output-available',
816
+ input: (toolInvocation as any).input,
817
+ output: chunk.output,
818
+ providerExecuted: chunk.providerExecuted,
819
+ preliminary: chunk.preliminary,
820
+ providerMetadata: chunk.providerMetadata,
821
+ title: toolInvocation.title,
822
+ toolMetadata: toolInvocation.toolMetadata,
823
+ },
824
+ toolInvocation as ToolUIPart<InferUIMessageTools<UI_MESSAGE>>,
825
+ );
784
826
  }
785
827
 
786
828
  write();
@@ -791,30 +833,36 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
791
833
  const toolInvocation = getToolInvocation(chunk.toolCallId);
792
834
 
793
835
  if (toolInvocation.type === 'dynamic-tool') {
794
- updateDynamicToolPart({
795
- toolCallId: chunk.toolCallId,
796
- toolName: toolInvocation.toolName,
797
- state: 'output-error',
798
- input: (toolInvocation as any).input,
799
- errorText: chunk.errorText,
800
- providerExecuted: chunk.providerExecuted,
801
- providerMetadata: chunk.providerMetadata,
802
- title: toolInvocation.title,
803
- toolMetadata: toolInvocation.toolMetadata,
804
- });
836
+ updateDynamicToolPart(
837
+ {
838
+ toolCallId: chunk.toolCallId,
839
+ toolName: toolInvocation.toolName,
840
+ state: 'output-error',
841
+ input: (toolInvocation as any).input,
842
+ errorText: chunk.errorText,
843
+ providerExecuted: chunk.providerExecuted,
844
+ providerMetadata: chunk.providerMetadata,
845
+ title: toolInvocation.title,
846
+ toolMetadata: toolInvocation.toolMetadata,
847
+ },
848
+ toolInvocation,
849
+ );
805
850
  } else {
806
- updateToolPart({
807
- toolCallId: chunk.toolCallId,
808
- toolName: getStaticToolName(toolInvocation),
809
- state: 'output-error',
810
- input: (toolInvocation as any).input,
811
- rawInput: (toolInvocation as any).rawInput,
812
- errorText: chunk.errorText,
813
- providerExecuted: chunk.providerExecuted,
814
- providerMetadata: chunk.providerMetadata,
815
- title: toolInvocation.title,
816
- toolMetadata: toolInvocation.toolMetadata,
817
- });
851
+ updateToolPart(
852
+ {
853
+ toolCallId: chunk.toolCallId,
854
+ toolName: getStaticToolName(toolInvocation),
855
+ state: 'output-error',
856
+ input: (toolInvocation as any).input,
857
+ rawInput: (toolInvocation as any).rawInput,
858
+ errorText: chunk.errorText,
859
+ providerExecuted: chunk.providerExecuted,
860
+ providerMetadata: chunk.providerMetadata,
861
+ title: toolInvocation.title,
862
+ toolMetadata: toolInvocation.toolMetadata,
863
+ },
864
+ toolInvocation as ToolUIPart<InferUIMessageTools<UI_MESSAGE>>,
865
+ );
818
866
  }
819
867
 
820
868
  write();