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.
- package/CHANGELOG.md +23 -0
- package/dist/index.d.ts +62 -24
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +101 -31
- package/dist/index.js.map +1 -1
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +32 -12
- package/dist/internal/index.js.map +1 -1
- package/docs/03-ai-sdk-core/16-mcp-tools.mdx +25 -0
- package/docs/03-ai-sdk-core/19-tool-search.mdx +40 -4
- package/docs/03-ai-sdk-core/35-image-generation.mdx +5 -0
- package/docs/03-ai-sdk-core/36-transcription.mdx +42 -38
- package/docs/03-ai-sdk-core/37-speech.mdx +4 -0
- package/docs/04-ai-sdk-ui/02-chatbot.mdx +2 -1
- package/docs/07-reference/01-ai-sdk-core/17-create-agent-ui-stream.mdx +7 -0
- package/docs/07-reference/01-ai-sdk-core/18-create-agent-ui-stream-response.mdx +7 -0
- package/docs/07-reference/01-ai-sdk-core/18-pipe-agent-ui-stream-to-response.mdx +7 -0
- package/docs/07-reference/01-ai-sdk-core/23-tool-search.mdx +62 -6
- package/docs/07-reference/02-ai-sdk-ui/01-use-chat.mdx +13 -0
- package/docs/07-reference/04-ai-sdk-workflow/01-workflow-agent.mdx +60 -3
- package/package.json +4 -4
- package/src/agent/create-agent-ui-stream-response.ts +16 -4
- package/src/agent/create-agent-ui-stream.ts +16 -2
- package/src/agent/pipe-agent-ui-stream-to-response.ts +16 -4
- package/src/generate-text/validate-tool-approvals.ts +5 -1
- package/src/tool-search/prepare-tool-search.ts +49 -29
- package/src/tool-search/tool-search.ts +58 -4
- package/src/ui/chat.ts +36 -5
- 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(
|
|
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 {
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
.
|
|
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(
|
|
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
|
-
{
|
|
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
|
|
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)
|
|
626
|
-
|
|
627
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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);
|