ai 7.0.124 → 7.0.127

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.
Files changed (29) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/index.d.ts +62 -24
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +101 -31
  5. package/dist/index.js.map +1 -1
  6. package/dist/internal/index.d.ts.map +1 -1
  7. package/dist/internal/index.js +32 -12
  8. package/dist/internal/index.js.map +1 -1
  9. package/docs/03-ai-sdk-core/16-mcp-tools.mdx +25 -0
  10. package/docs/03-ai-sdk-core/19-tool-search.mdx +40 -4
  11. package/docs/03-ai-sdk-core/35-image-generation.mdx +5 -0
  12. package/docs/03-ai-sdk-core/36-transcription.mdx +42 -38
  13. package/docs/03-ai-sdk-core/37-speech.mdx +4 -0
  14. package/docs/04-ai-sdk-ui/02-chatbot.mdx +2 -1
  15. package/docs/07-reference/01-ai-sdk-core/17-create-agent-ui-stream.mdx +7 -0
  16. package/docs/07-reference/01-ai-sdk-core/18-create-agent-ui-stream-response.mdx +7 -0
  17. package/docs/07-reference/01-ai-sdk-core/18-pipe-agent-ui-stream-to-response.mdx +7 -0
  18. package/docs/07-reference/01-ai-sdk-core/23-tool-search.mdx +62 -6
  19. package/docs/07-reference/02-ai-sdk-ui/01-use-chat.mdx +13 -0
  20. package/docs/07-reference/04-ai-sdk-workflow/01-workflow-agent.mdx +60 -3
  21. package/package.json +4 -4
  22. package/src/agent/create-agent-ui-stream-response.ts +16 -4
  23. package/src/agent/create-agent-ui-stream.ts +16 -2
  24. package/src/agent/pipe-agent-ui-stream-to-response.ts +16 -4
  25. package/src/generate-text/validate-tool-approvals.ts +5 -1
  26. package/src/tool-search/prepare-tool-search.ts +49 -29
  27. package/src/tool-search/tool-search.ts +58 -4
  28. package/src/ui/chat.ts +36 -5
  29. package/src/ui-message-stream/create-ui-message-stream.ts +29 -4
@@ -15,8 +15,9 @@ import type { UIMessageStreamOptions } from '../generate-text/stream-text-result
15
15
  import type { TimeoutConfiguration } from '../prompt/request-options';
16
16
  import { pipeUIMessageStreamToResponse } from '../ui-message-stream';
17
17
  import type { UIMessageStreamResponseInit } from '../ui-message-stream/ui-message-stream-response-init';
18
- import type { InferUITools, UIMessage } from '../ui/ui-messages';
18
+ import type { InferUITools, UIMessage, UIDataTypes } from '../ui/ui-messages';
19
19
  import type { Agent } from './agent';
20
+ import type { convertToModelMessages } from '../ui/convert-to-model-messages';
20
21
  import { createAgentUIStream } from './create-agent-ui-stream';
21
22
 
22
23
  /**
@@ -25,6 +26,7 @@ import { createAgentUIStream } from './create-agent-ui-stream';
25
26
  * @param response - The Node.js ServerResponse object to pipe to.
26
27
  * @param agent - The agent to run.
27
28
  * @param uiMessages - The input UI messages.
29
+ * @param convertDataPart - Optional function to convert custom data parts to text or file model message parts.
28
30
  * @param abortSignal - Abort signal. Optional.
29
31
  * @param timeout - Timeout in milliseconds. Optional.
30
32
  * @param experimental_sandbox - The sandbox environment that is passed through to tool execution. Optional.
@@ -44,6 +46,11 @@ export async function pipeAgentUIStreamToResponse<
44
46
  RUNTIME_CONTEXT extends Context = Context,
45
47
  OUTPUT extends Output = never,
46
48
  MESSAGE_METADATA = unknown,
49
+ UI_MESSAGE extends UIMessage<
50
+ MESSAGE_METADATA,
51
+ UIDataTypes,
52
+ InferUITools<TOOLS>
53
+ > = UIMessage<MESSAGE_METADATA, UIDataTypes, InferUITools<TOOLS>>,
47
54
  >({
48
55
  response,
49
56
  headers,
@@ -56,6 +63,13 @@ export async function pipeAgentUIStreamToResponse<
56
63
  response: ServerResponse;
57
64
  agent: Agent<CALL_OPTIONS, TOOLS, RUNTIME_CONTEXT, OUTPUT>;
58
65
  uiMessages: unknown[];
66
+ /**
67
+ * Converts custom UI data parts to text or file model message parts.
68
+ * Data parts are ignored when omitted or when the callback returns undefined.
69
+ */
70
+ convertDataPart?: NonNullable<
71
+ Parameters<typeof convertToModelMessages<UI_MESSAGE>>[1]
72
+ >['convertDataPart'];
59
73
  abortSignal?: AbortSignal;
60
74
  timeout?: TimeoutConfiguration<TOOLS>;
61
75
  experimental_sandbox?: SandboxSession;
@@ -65,9 +79,7 @@ export async function pipeAgentUIStreamToResponse<
65
79
  /** @deprecated Use `onStepEnd` instead. */
66
80
  onStepFinish?: GenerateTextOnStepFinishCallback<TOOLS>;
67
81
  } & UIMessageStreamResponseInit &
68
- UIMessageStreamOptions<
69
- UIMessage<MESSAGE_METADATA, never, InferUITools<TOOLS>>
70
- >): Promise<void> {
82
+ UIMessageStreamOptions<UI_MESSAGE>): Promise<void> {
71
83
  return pipeUIMessageStreamToResponse({
72
84
  response,
73
85
  headers,
@@ -117,9 +117,13 @@ export async function validateApprovedToolApprovals<
117
117
  refineToolInput,
118
118
  });
119
119
 
120
+ // Clone both inputs into the current realm before comparing them.
121
+ const revalidatedInput = structuredClone(revalidatedToolCall.input);
122
+ const approvedInput = structuredClone(toolCall.input);
123
+
120
124
  // Revalidation must never change the operation that was approved,
121
125
  // including when older or projected history omits the schema input.
122
- if (!isDeepEqualData(revalidatedToolCall.input, toolCall.input)) {
126
+ if (!isDeepEqualData(revalidatedInput, approvedInput)) {
123
127
  validationError = new Error(
124
128
  'Approved tool input does not match the validated schema output.',
125
129
  );
@@ -11,7 +11,11 @@ import {
11
11
  } from '../generate-text/tool-caller-configuration';
12
12
  import { resolveToolDescription } from '../prompt/prepare-tools';
13
13
  import { getOwn } from '../util/get-own';
14
- import { isToolSearch } from './tool-search';
14
+ import {
15
+ getToolSearchFunction,
16
+ getToolSearchMaxResults,
17
+ isToolSearch,
18
+ } from './tool-search';
15
19
 
16
20
  /** Create discovery state for one generation, never for a shared tool instance. */
17
21
  export function createToolSearchState({
@@ -90,41 +94,57 @@ export function createToolSearchState({
90
94
  searchName,
91
95
  {
92
96
  ...tool,
93
- execute: ({ query }: { query: string }) => {
97
+ execute: async ({ query }: { query: string }) => {
98
+ const availableTools = candidates.map(([name, candidate]) => {
99
+ const description = resolveToolDescription({
100
+ tool: candidate,
101
+ toolName: name,
102
+ toolsContext,
103
+ experimental_sandbox,
104
+ });
105
+ return {
106
+ name,
107
+ ...(description == null ? {} : { description }),
108
+ };
109
+ });
110
+ const toolsByName = new Map(
111
+ availableTools.map(tool => [tool.name, tool]),
112
+ );
113
+ const customSearch = getToolSearchFunction(tool);
94
114
  const terms = [...new Set(tokenize(query))];
95
- const matches = candidates
96
- .map(([name, candidate]) => {
97
- const description = resolveToolDescription({
98
- tool: candidate,
99
- toolName: name,
100
- toolsContext,
101
- experimental_sandbox,
102
- });
103
- const nameTerms = tokenize(name);
104
- const descriptionTerms = tokenize(description ?? '');
105
- const score = terms.reduce(
106
- (score, term) =>
107
- score +
108
- (nameTerms.includes(term) ? 2 : 0) +
109
- (descriptionTerms.includes(term) ? 1 : 0),
110
- 0,
111
- );
112
- return { name, description, score };
115
+ const rankedNames = customSearch
116
+ ? await customSearch({
117
+ query,
118
+ tools: availableTools.map(tool => ({ ...tool })),
119
+ })
120
+ : availableTools
121
+ .map(({ name, description }) => {
122
+ const nameTerms = tokenize(name);
123
+ const descriptionTerms = tokenize(description ?? '');
124
+ const score = terms.reduce(
125
+ (score, term) =>
126
+ score +
127
+ (nameTerms.includes(term) ? 2 : 0) +
128
+ (descriptionTerms.includes(term) ? 1 : 0),
129
+ 0,
130
+ );
131
+ return { name, score };
132
+ })
133
+ .filter(match => match.score > 0)
134
+ .sort((a, b) => b.score - a.score)
135
+ .map(match => match.name);
136
+ const matches = [...new Set(rankedNames)]
137
+ .flatMap(name => {
138
+ const match = toolsByName.get(name);
139
+ return match == null ? [] : [match];
113
140
  })
114
- .filter(match => match.score > 0)
115
- .sort((a, b) => b.score - a.score)
116
- .slice(0, 5);
141
+ .slice(0, getToolSearchMaxResults(tool));
117
142
 
118
143
  for (const { name } of matches) {
119
144
  discovered.add(name);
120
145
  }
121
146
 
122
- return {
123
- tools: matches.map(({ name, description }) => ({
124
- name,
125
- ...(description == null ? {} : { description }),
126
- })),
127
- };
147
+ return { tools: matches };
128
148
  },
129
149
  },
130
150
  ];
@@ -1,6 +1,16 @@
1
1
  import { jsonSchema, tool, type Tool } from '@ai-sdk/provider-utils';
2
+ import { InvalidArgumentError } from '../error/invalid-argument-error';
2
3
 
3
4
  const toolSearchSymbol = Symbol.for('vercel.ai.toolSearch');
5
+ const toolSearchFunctionSymbol = Symbol.for('vercel.ai.toolSearch.search');
6
+ const toolSearchMaxResultsSymbol = Symbol.for(
7
+ 'vercel.ai.toolSearch.maxResults',
8
+ );
9
+
10
+ type ToolSearchFunction = (options: {
11
+ query: string;
12
+ tools: Array<{ name: string; description?: string }>;
13
+ }) => string[] | PromiseLike<string[]>;
4
14
 
5
15
  type ToolSearchInput = { query: string };
6
16
  type ToolSearchOutput = {
@@ -12,13 +22,36 @@ type ToolSearchOutput = {
12
22
  * binds the search registry and makes matches available on the next step.
13
23
  * Use directly or through code mode with `toolDiscovery: 'conversation'`.
14
24
  */
15
- export function toolSearch(): Tool<ToolSearchInput, ToolSearchOutput> & {
25
+ export function toolSearch({
26
+ search,
27
+ maxResults = 5,
28
+ }: {
29
+ /**
30
+ * Search eligible deferred tools and return their names in ranked order.
31
+ * Supports synchronous and asynchronous callbacks. Unknown names and
32
+ * duplicates are ignored before applying the result limit.
33
+ */
34
+ search?: ToolSearchFunction;
35
+ /**
36
+ * Maximum number of matching tools returned per search.
37
+ *
38
+ * @default 5
39
+ */
40
+ maxResults?: number;
41
+ } = {}): Tool<ToolSearchInput, ToolSearchOutput> & {
16
42
  type: 'function';
17
43
  } {
44
+ if (!Number.isSafeInteger(maxResults) || maxResults < 1) {
45
+ throw new InvalidArgumentError({
46
+ parameter: 'maxResults',
47
+ value: maxResults,
48
+ message: 'maxResults must be a positive safe integer.',
49
+ });
50
+ }
51
+
18
52
  return Object.assign(
19
53
  tool({
20
- description:
21
- 'Search for tools by keywords in their names and descriptions. Returns up to five matching tools. Matches become available on the next model step, after this execution finishes. Wait for their tool definitions before calling the discovered tools. If no tools match, try different keywords.',
54
+ description: `${search == null ? 'Search for tools by keywords in their names and descriptions.' : 'Search for tools matching a query.'} Returns up to ${maxResults === 5 ? 'five' : maxResults} matching tools. Matches become available on the next model step, after this execution finishes. Wait for their tool definitions before calling the discovered tools. If no tools match, try different keywords.`,
22
55
  inputSchema: jsonSchema<ToolSearchInput>({
23
56
  type: 'object',
24
57
  properties: { query: { type: 'string', minLength: 1 } },
@@ -48,10 +81,31 @@ export function toolSearch(): Tool<ToolSearchInput, ToolSearchOutput> & {
48
81
  throw new Error('toolSearch must be bound by an AI SDK generation.');
49
82
  },
50
83
  }),
51
- { type: 'function' as const, [toolSearchSymbol]: true },
84
+ {
85
+ type: 'function' as const,
86
+ [toolSearchSymbol]: true,
87
+ [toolSearchFunctionSymbol]: search,
88
+ [toolSearchMaxResultsSymbol]: maxResults,
89
+ },
52
90
  );
53
91
  }
54
92
 
93
+ export function getToolSearchMaxResults(tool: Tool): number {
94
+ return (
95
+ (tool as Tool & { [toolSearchMaxResultsSymbol]?: number })[
96
+ toolSearchMaxResultsSymbol
97
+ ] ?? 5
98
+ );
99
+ }
100
+
101
+ export function getToolSearchFunction(
102
+ tool: Tool,
103
+ ): ToolSearchFunction | undefined {
104
+ return (tool as Tool & { [toolSearchFunctionSymbol]?: ToolSearchFunction })[
105
+ toolSearchFunctionSymbol
106
+ ];
107
+ }
108
+
55
109
  export function isToolSearch(tool: Tool): boolean {
56
110
  return (
57
111
  (tool as Tool & { [toolSearchSymbol]?: boolean })[toolSearchSymbol] === true
package/src/ui/chat.ts CHANGED
@@ -609,7 +609,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
609
609
  });
610
610
 
611
611
  addToolOutput: ChatAddToolOutputFunction<UI_MESSAGE> = async ({
612
- state = 'output-available',
612
+ state,
613
613
  toolCallId,
614
614
  output,
615
615
  errorText,
@@ -621,10 +621,41 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
621
621
 
622
622
  const updatePart = (
623
623
  part: UIMessagePart<UIDataTypes, UITools>,
624
- ): UIMessagePart<UIDataTypes, UITools> =>
625
- isToolUIPart(part) && part.toolCallId === toolCallId
626
- ? ({ ...part, state, output, errorText } as typeof part)
627
- : part;
624
+ ): UIMessagePart<UIDataTypes, UITools> => {
625
+ if (!isToolUIPart(part) || part.toolCallId !== toolCallId) {
626
+ return part;
627
+ }
628
+
629
+ // Output states can only retain approvals that were granted.
630
+ const { approval: existingApproval, ...toolPart } = part;
631
+ const approval =
632
+ existingApproval?.approved === true
633
+ ? {
634
+ approval: {
635
+ ...existingApproval,
636
+ approved: existingApproval.approved,
637
+ },
638
+ }
639
+ : {};
640
+
641
+ return state === 'output-error'
642
+ ? {
643
+ ...toolPart,
644
+ ...approval,
645
+ state,
646
+ input: part.input,
647
+ output: undefined,
648
+ errorText,
649
+ }
650
+ : {
651
+ ...toolPart,
652
+ ...approval,
653
+ state: 'output-available',
654
+ input: part.input,
655
+ output,
656
+ errorText: undefined,
657
+ };
658
+ };
628
659
 
629
660
  // update the message to trigger an immediate UI update
630
661
  this.state.replaceMessage(messages.length - 1, {
@@ -73,12 +73,25 @@ export function createUIMessageStream<UI_MESSAGE extends UIMessage>({
73
73
  >;
74
74
 
75
75
  const ongoingStreamPromises: Promise<void>[] = [];
76
+ const activeReaders = new Set<
77
+ ReadableStreamDefaultReader<InferUIMessageChunk<UI_MESSAGE>>
78
+ >();
79
+ let isCancelled = false;
80
+ let cancelReason: unknown;
76
81
  let outcome: UIMessageStreamOutcome = { status: 'unknown' };
77
82
 
78
83
  const stream = new ReadableStream({
79
84
  start(controllerArg) {
80
85
  controller = controllerArg;
81
86
  },
87
+ async cancel(reason) {
88
+ isCancelled = true;
89
+ cancelReason = reason;
90
+
91
+ await Promise.all(
92
+ Array.from(activeReaders, reader => reader.cancel(reason)),
93
+ );
94
+ },
82
95
  });
83
96
 
84
97
  function safeEnqueue(data: InferUIMessageChunk<UI_MESSAGE>) {
@@ -132,13 +145,25 @@ export function createUIMessageStream<UI_MESSAGE extends UIMessage>({
132
145
  safeEnqueue(part);
133
146
  },
134
147
  merge(streamArg) {
148
+ if (isCancelled) {
149
+ void streamArg.cancel(cancelReason).catch(() => {});
150
+ return;
151
+ }
152
+
135
153
  ongoingStreamPromises.push(
136
154
  (async () => {
137
155
  const reader = streamArg.getReader();
138
- while (true) {
139
- const { done, value } = await reader.read();
140
- if (done) break;
141
- safeEnqueue(value);
156
+ activeReaders.add(reader);
157
+
158
+ try {
159
+ while (true) {
160
+ const { done, value } = await reader.read();
161
+ if (done) break;
162
+ safeEnqueue(value);
163
+ }
164
+ } finally {
165
+ activeReaders.delete(reader);
166
+ reader.releaseLock();
142
167
  }
143
168
  })().catch(error => {
144
169
  handleError(error);