Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/ai/src/agent/infer-agent-ui-message.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
DataUIPart,
DynamicToolUIPart,
FileUIPart,
InvalidToolUIPart,
ReasoningFileUIPart,
ReasoningUIPart,
SourceDocumentUIPart,
Expand Down Expand Up @@ -34,6 +35,7 @@ describe('InferAgentUIMessage', () => {
| ReasoningUIPart
// No static tools, so no ToolUIPart
| DynamicToolUIPart
| InvalidToolUIPart
| SourceUrlUIPart
| SourceDocumentUIPart
| FileUIPart
Expand Down
2 changes: 1 addition & 1 deletion packages/ai/src/generate-text/execute-tool-call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export async function executeToolCall<
type: 'tool-result',
output: part.output,
preliminary: true,
});
} as TypedToolResult<TOOLS>);
} else {
output = part.output;
}
Expand Down
12 changes: 11 additions & 1 deletion packages/ai/src/generate-text/generate-text-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import { InferCompleteOutput } from './output-utils';
import { ReasoningFileOutput, ReasoningOutput } from './reasoning-output';
import { ResponseMessage } from './response-message';
import { StepResult } from './step-result';
import { DynamicToolCall, StaticToolCall, TypedToolCall } from './tool-call';
import {
DynamicToolCall,
InvalidToolCall,
StaticToolCall,
TypedToolCall,
} from './tool-call';
import {
DynamicToolResult,
StaticToolResult,
Expand Down Expand Up @@ -74,6 +79,11 @@ export interface GenerateTextResult<
*/
readonly dynamicToolCalls: Array<DynamicToolCall>;

/**
* The invalid tool calls from the last step (tool not found or bad input).
*/
readonly invalidToolCalls: Array<InvalidToolCall>;

/**
* The results of the tool calls from the last step.
*/
Expand Down
28 changes: 5 additions & 23 deletions packages/ai/src/generate-text/generate-text.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,8 @@ describe('generateText', () => {
// test type inference
if (
result.toolCalls[0].toolName === 'tool1' &&
!result.toolCalls[0].dynamic
!result.toolCalls[0].dynamic &&
!result.toolCalls[0].invalid
) {
assertType<string>(result.toolCalls[0].input.value);
}
Expand Down Expand Up @@ -4459,7 +4460,8 @@ describe('generateText', () => {
// test type inference
if (
result.toolCalls[0].toolName === 'tool1' &&
!result.toolCalls[0].dynamic
!result.toolCalls[0].dynamic &&
!result.toolCalls[0].invalid
) {
assertType<string>(result.toolCalls[0].input.value);
}
Expand Down Expand Up @@ -6652,7 +6654,6 @@ describe('generateText', () => {
expect(result.content).toMatchInlineSnapshot(`
[
{
"dynamic": true,
"error": [AI_InvalidToolInputError: Invalid input for tool cityAttractions: AI_TypeValidationError: Type validation failed: Value: {"cities":"San Francisco"}.
Error message: [
{
Expand All @@ -6670,31 +6671,12 @@ describe('generateText', () => {
"invalid": true,
"providerExecuted": undefined,
"providerMetadata": undefined,
"rawInput": "{ "cities": "San Francisco" }",
"title": undefined,
"toolCallId": "call-1",
"toolName": "cityAttractions",
"type": "tool-call",
},
{
"dynamic": true,
"error": "AI_InvalidToolInputError: Invalid input for tool cityAttractions: AI_TypeValidationError: Type validation failed: Value: {"cities":"San Francisco"}.
Error message: [
{
"expected": "string",
"code": "invalid_type",
"path": [
"city"
],
"message": "Invalid input: expected string, received undefined"
}
]",
"input": {
"cities": "San Francisco",
},
"toolCallId": "call-1",
"toolName": "cityAttractions",
"type": "tool-error",
},
]
`);
});
Expand Down
23 changes: 5 additions & 18 deletions packages/ai/src/generate-text/generate-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import type {
} from '@ai-sdk/provider-utils';
import {
createIdGenerator,
getErrorMessage,
IdGenerator,
ProviderOptions,
withUserAgentSuffix,
Expand Down Expand Up @@ -837,25 +836,8 @@ export async function generateText<
}
}

// insert error tool outputs for invalid tool calls:
// TODO AI SDK 6: invalid inputs should not require output parts
const invalidToolCalls = stepToolCalls.filter(
toolCall => toolCall.invalid && toolCall.dynamic,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi-step tool call loop terminates prematurely when invalid tool calls are present because clientToolCalls includes them but clientToolOutputs doesn't, causing the count comparison clientToolOutputs.length === clientToolCalls.length to be false.

Fix on Vercel

);

clientToolOutputs = [];

for (const toolCall of invalidToolCalls) {
clientToolOutputs.push({
type: 'tool-error',
toolCallId: toolCall.toolCallId,
toolName: toolCall.toolName,
input: toolCall.input,
error: getErrorMessage(toolCall.error!),
dynamic: true,
});
}

// execute client tool calls:
clientToolCalls = stepToolCalls.filter(
toolCall => !toolCall.providerExecuted,
Expand Down Expand Up @@ -1059,6 +1041,7 @@ export async function generateText<
toolCalls: lastStep.toolCalls,
staticToolCalls: lastStep.staticToolCalls,
dynamicToolCalls: lastStep.dynamicToolCalls,
invalidToolCalls: lastStep.invalidToolCalls,
toolResults: lastStep.toolResults,
staticToolResults: lastStep.staticToolResults,
dynamicToolResults: lastStep.dynamicToolResults,
Expand Down Expand Up @@ -1220,6 +1203,10 @@ class DefaultGenerateTextResult<
return this.finalStep.dynamicToolCalls;
}

get invalidToolCalls() {
return this.finalStep.invalidToolCalls;
}

get toolResults() {
return this.finalStep.toolResults;
}
Expand Down
1 change: 1 addition & 0 deletions packages/ai/src/generate-text/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export type {
export type { ToolApprovalRequestOutput } from './tool-approval-request-output';
export type {
DynamicToolCall,
InvalidToolCall,
StaticToolCall,
TypedToolCall,
} from './tool-call';
Expand Down
14 changes: 8 additions & 6 deletions packages/ai/src/generate-text/parse-tool-call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import { InvalidToolInputError } from '../error/invalid-tool-input-error';
import { NoSuchToolError } from '../error/no-such-tool-error';
import { ToolCallRepairError } from '../error/tool-call-repair-error';
import { DynamicToolCall, TypedToolCall } from './tool-call';
import { DynamicToolCall, InvalidToolCall, TypedToolCall } from './tool-call';
import { ToolCallRepairFunction } from './tool-call-repair-function';
import type { ToolSet } from '@ai-sdk/provider-utils';

Expand Down Expand Up @@ -78,23 +78,25 @@ export async function parseToolCall<TOOLS extends ToolSet>({
return await doParseToolCall({ toolCall: repairedToolCall, tools });
}
} catch (error) {
// use parsed input when possible
const parsedInput = await safeParseJSON({ text: toolCall.input });
const input = parsedInput.success ? parsedInput.value : toolCall.input;

// TODO AI SDK 6: special invalid tool call parts
const tool = tools?.[toolCall.toolName];
const isDynamic = tool?.type === 'dynamic' || toolCall.dynamic;

return {
type: 'tool-call',
toolCallId: toolCall.toolCallId,
toolName: toolCall.toolName,
rawInput: toolCall.input,
input,
dynamic: true,
invalid: true,
...(isDynamic ? { dynamic: true } : {}),
error,
title: tools?.[toolCall.toolName]?.title,
title: tool?.title,
providerExecuted: toolCall.providerExecuted,
providerMetadata: toolCall.providerMetadata,
};
} satisfies InvalidToolCall;
}
}

Expand Down
23 changes: 20 additions & 3 deletions packages/ai/src/generate-text/step-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ import {
convertFromReasoningOutputs,
} from './reasoning-output';
import { ResponseMessage } from './response-message';
import { DynamicToolCall, StaticToolCall, TypedToolCall } from './tool-call';
import {
DynamicToolCall,
InvalidToolCall,
StaticToolCall,
TypedToolCall,
} from './tool-call';
import {
DynamicToolResult,
StaticToolResult,
Expand Down Expand Up @@ -118,6 +123,11 @@ export type StepResult<
*/
readonly dynamicToolCalls: Array<DynamicToolCall>;

/**
* The invalid tool calls from the last step (tool not found or bad input).
*/
readonly invalidToolCalls: Array<InvalidToolCall>;

/**
* The results of the tool calls.
*/
Expand Down Expand Up @@ -291,13 +301,20 @@ export class DefaultStepResult<
get staticToolCalls() {
return this.toolCalls.filter(
(toolCall): toolCall is StaticToolCall<TOOLS> =>
toolCall.dynamic !== true,
toolCall.invalid !== true && toolCall.dynamic !== true,
);
}

get dynamicToolCalls() {
return this.toolCalls.filter(
(toolCall): toolCall is DynamicToolCall => toolCall.dynamic === true,
(toolCall): toolCall is DynamicToolCall =>
toolCall.invalid !== true && toolCall.dynamic === true,
);
}

get invalidToolCalls() {
return this.toolCalls.filter(
(toolCall): toolCall is InvalidToolCall => toolCall.invalid === true,
);
}

Expand Down
14 changes: 0 additions & 14 deletions packages/ai/src/generate-text/stream-language-model-call.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
getErrorMessage,
LanguageModelV4Prompt,
LanguageModelV4StreamPart,
SharedV4Headers,
Expand Down Expand Up @@ -347,19 +346,6 @@ function createLanguageModelV4StreamPartToLanguageModelStreamPartTransform<

toolCallsByToolCallId.set(toolCall.toolCallId, toolCall);
controller.enqueue(toolCall);

if (toolCall.invalid) {
controller.enqueue({
type: 'tool-error',
toolCallId: toolCall.toolCallId,
toolName: toolCall.toolName,
input: toolCall.input,
error: getErrorMessage(toolCall.error!),
dynamic: true,
title: toolCall.title,
});
break;
}
} catch (error) {
controller.enqueue({ type: 'error', error });
}
Expand Down
14 changes: 13 additions & 1 deletion packages/ai/src/generate-text/stream-text-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ import { ReasoningFileOutput, ReasoningOutput } from './reasoning-output';
import { ResponseMessage } from './response-message';
import { StepResult } from './step-result';
import { ToolApprovalRequestOutput } from './tool-approval-request-output';
import { DynamicToolCall, StaticToolCall, TypedToolCall } from './tool-call';
import {
DynamicToolCall,
InvalidToolCall,
StaticToolCall,
TypedToolCall,
} from './tool-call';
import { TypedToolError } from './tool-error';
import { StaticToolOutputDenied } from './tool-output-denied';
import {
Expand Down Expand Up @@ -174,6 +179,13 @@ export interface StreamTextResult<
*/
readonly dynamicToolCalls: PromiseLike<DynamicToolCall[]>;

/**
* The invalid tool calls from the last step (tool not found or bad input).
*
* Automatically consumes the stream.
*/
readonly invalidToolCalls: PromiseLike<InvalidToolCall[]>;

/**
* The static tool results that have been generated in the last step.
*
Expand Down
6 changes: 5 additions & 1 deletion packages/ai/src/generate-text/stream-text.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13898,7 +13898,11 @@ describe('streamText', () => {
}

// assuming test arg structure:
if (chunk.type === 'tool-call' && !chunk.dynamic) {
if (
chunk.type === 'tool-call' &&
!chunk.dynamic &&
!chunk.invalid
) {
chunk.input = {
...chunk.input,
value: chunk.input.value.toUpperCase(),
Expand Down
13 changes: 8 additions & 5 deletions packages/ai/src/generate-text/stream-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1188,6 +1188,7 @@ class DefaultStreamTextResult<
toolCalls: finalStep.toolCalls,
staticToolCalls: finalStep.staticToolCalls,
dynamicToolCalls: finalStep.dynamicToolCalls,
invalidToolCalls: finalStep.invalidToolCalls,
toolResults: finalStep.toolResults,
staticToolResults: finalStep.staticToolResults,
dynamicToolResults: finalStep.dynamicToolResults,
Expand Down Expand Up @@ -2083,6 +2084,10 @@ class DefaultStreamTextResult<
return this.finalStep.then(step => step.dynamicToolCalls);
}

get invalidToolCalls() {
return this.finalStep.then(step => step.invalidToolCalls);
}

get toolResults() {
return this.finalStep.then(step => step.toolResults);
}
Expand Down Expand Up @@ -2258,11 +2263,9 @@ class DefaultStreamTextResult<
})
: undefined;

// TODO simplify once dynamic is no longer needed for invalid tool inputs
const isDynamic = (part: { toolName: string; dynamic?: boolean }) => {
const tool = this.tools?.[part.toolName];

// provider-executed, dynamic tools are not listed in the tools object
if (tool == null) {
return part.dynamic;
}
Expand Down Expand Up @@ -2436,8 +2439,6 @@ class DefaultStreamTextResult<
}

case 'tool-call': {
const dynamic = isDynamic(part);

if (part.invalid) {
controller.enqueue({
type: 'tool-input-error',
Expand All @@ -2450,11 +2451,13 @@ class DefaultStreamTextResult<
...(part.providerMetadata != null
? { providerMetadata: part.providerMetadata }
: {}),
...(dynamic != null ? { dynamic } : {}),
invalid: true,
errorText: onError(part.error),
...(part.title != null ? { title: part.title } : {}),
});
} else {
const dynamic = isDynamic(part);

controller.enqueue({
type: 'tool-input-available',
toolCallId: part.toolCallId,
Expand Down
Loading
Loading