ai 6.0.270 → 6.0.272
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 +22 -0
- package/dist/index.d.mts +126 -49
- package/dist/index.d.ts +126 -49
- package/dist/index.js +609 -394
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +545 -328
- package/dist/index.mjs.map +1 -1
- package/dist/internal/index.js +1 -1
- package/dist/internal/index.mjs +1 -1
- package/docs/03-ai-sdk-core/15-tools-and-tool-calling.mdx +26 -1
- package/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx +2 -2
- package/docs/07-reference/02-ai-sdk-ui/40-create-ui-message-stream.mdx +33 -5
- package/docs/07-reference/05-ai-sdk-errors/ai-tool-choice-violation-error.mdx +38 -0
- package/docs/07-reference/05-ai-sdk-errors/index.mdx +1 -0
- package/package.json +3 -3
- package/src/agent/create-agent-ui-stream.ts +2 -2
- package/src/agent/tool-loop-agent-settings.ts +9 -0
- package/src/error/index.ts +1 -0
- package/src/error/tool-choice-violation-error.ts +80 -0
- package/src/generate-text/generate-text.ts +25 -1
- package/src/generate-text/stream-text.ts +84 -1
- package/src/ui/direct-chat-transport.ts +2 -2
- package/src/ui/validate-ui-messages.ts +136 -55
- package/src/ui-message-stream/create-ui-message-stream.ts +47 -17
- package/src/ui-message-stream/handle-ui-message-stream-finish.ts +58 -15
- package/src/ui-message-stream/index.ts +5 -1
- package/src/ui-message-stream/ui-message-stream-on-finish-callback.ts +7 -0
- package/src/ui-message-stream/ui-message-stream-outcome.ts +12 -0
- package/src/ui-message-stream/ui-message-stream-writer.ts +15 -0
package/dist/internal/index.js
CHANGED
|
@@ -164,7 +164,7 @@ function detectMediaType({
|
|
|
164
164
|
var import_provider_utils2 = require("@ai-sdk/provider-utils");
|
|
165
165
|
|
|
166
166
|
// src/version.ts
|
|
167
|
-
var VERSION = true ? "6.0.
|
|
167
|
+
var VERSION = true ? "6.0.272" : "0.0.0-test";
|
|
168
168
|
|
|
169
169
|
// src/util/download/download.ts
|
|
170
170
|
var download = async ({
|
package/dist/internal/index.mjs
CHANGED
|
@@ -218,6 +218,30 @@ const paymentTool = tool({
|
|
|
218
218
|
|
|
219
219
|
In this example, only transactions over $1000 require approval. Smaller transactions execute automatically.
|
|
220
220
|
|
|
221
|
+
### Securing Approval Requests
|
|
222
|
+
|
|
223
|
+
When your application rebuilds a conversation from client-provided messages, a
|
|
224
|
+
client could forge an approval response. For tools that perform sensitive
|
|
225
|
+
operations, set `experimental_toolApprovalSecret` to cryptographically bind
|
|
226
|
+
each approval to the server that issued it:
|
|
227
|
+
|
|
228
|
+
```ts highlight="7"
|
|
229
|
+
import { ToolLoopAgent } from 'ai';
|
|
230
|
+
|
|
231
|
+
const agent = new ToolLoopAgent({
|
|
232
|
+
model: __MODEL__,
|
|
233
|
+
tools: { runCommand },
|
|
234
|
+
experimental_toolApprovalSecret: process.env.TOOL_APPROVAL_SECRET,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const result = await agent.generate({ messages });
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The server HMAC-signs each approval request and verifies the signature when an
|
|
241
|
+
approval is replayed. A forged or tampered approval is rejected before the tool
|
|
242
|
+
executes. Keep the secret on the server and use the same value on every
|
|
243
|
+
instance that can handle a later approval response.
|
|
244
|
+
|
|
221
245
|
### Tool Execution Approval with useChat
|
|
222
246
|
|
|
223
247
|
When using `useChat`, the approval flow is handled through UI state. See [Chatbot Tool Usage](/docs/ai-sdk-ui/chatbot-tool-usage#tool-execution-approval) for details on handling approvals in your UI with `addToolApprovalResponse`.
|
|
@@ -811,11 +835,12 @@ async function generateSomething(prompt: string): Promise<{
|
|
|
811
835
|
|
|
812
836
|
## Handling Errors
|
|
813
837
|
|
|
814
|
-
The AI SDK has
|
|
838
|
+
The AI SDK has four tool-call related errors:
|
|
815
839
|
|
|
816
840
|
- [`NoSuchToolError`](/docs/reference/ai-sdk-errors/ai-no-such-tool-error): the model tries to call a tool that is not defined in the tools object
|
|
817
841
|
- [`InvalidToolInputError`](/docs/reference/ai-sdk-errors/ai-invalid-tool-input-error): the model calls a tool with inputs that do not match the tool's input schema
|
|
818
842
|
- [`ToolCallRepairError`](/docs/reference/ai-sdk-errors/ai-tool-call-repair-error): an error that occurred during tool call repair
|
|
843
|
+
- [`ToolChoiceViolationError`](/docs/reference/ai-sdk-errors/ai-tool-choice-violation-error): the model response does not satisfy a required or specifically selected tool choice
|
|
819
844
|
|
|
820
845
|
When tool execution fails (errors thrown by your tool's `execute` function), the AI SDK adds them as `tool-error` content parts to enable automated LLM roundtrips in multi-step scenarios.
|
|
821
846
|
|
|
@@ -3509,9 +3509,9 @@ To see `streamText` in action, check out [these examples](#examples).
|
|
|
3509
3509
|
},
|
|
3510
3510
|
{
|
|
3511
3511
|
name: 'onFinish',
|
|
3512
|
-
type: '(options: { messages: UIMessage[]; isContinuation: boolean; responseMessage: UIMessage; isAborted: boolean; }) => void',
|
|
3512
|
+
type: '(options: { messages: UIMessage[]; isContinuation: boolean; responseMessage: UIMessage; isAborted: boolean; outcome: UIMessageStreamOutcome; finishReason?: FinishReason; }) => PromiseLike<void> | void',
|
|
3513
3513
|
isOptional: true,
|
|
3514
|
-
description: 'Callback function called when the stream finishes. Provides the updated
|
|
3514
|
+
description: 'Callback function called when the stream finishes. Provides the updated messages, continuation and abort state, model finish reason, and operation-level outcome.',
|
|
3515
3515
|
},
|
|
3516
3516
|
{
|
|
3517
3517
|
name: 'messageMetadata',
|
|
@@ -47,16 +47,32 @@ const stream = createUIMessageStream({
|
|
|
47
47
|
prompt: 'Write a haiku about AI',
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
writer.merge(
|
|
50
|
+
writer.merge(
|
|
51
|
+
result.toUIMessageStream({
|
|
52
|
+
onFinish: ({ outcome }) => {
|
|
53
|
+
// The composer decides that the model stream outcome is also the
|
|
54
|
+
// aggregate stream outcome.
|
|
55
|
+
writer.setOutcome(outcome);
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
51
59
|
},
|
|
52
60
|
onError: error => `Custom error: ${error.message}`,
|
|
53
61
|
originalMessages: existingMessages,
|
|
54
|
-
onFinish: ({ messages, isContinuation, responseMessage }) => {
|
|
62
|
+
onFinish: ({ messages, isContinuation, outcome, responseMessage }) => {
|
|
55
63
|
console.log('Stream finished with messages:', messages);
|
|
64
|
+
console.log('Stream outcome:', outcome.status);
|
|
56
65
|
},
|
|
57
66
|
});
|
|
58
67
|
```
|
|
59
68
|
|
|
69
|
+
`setOutcome` records the composer's policy without writing a chunk or closing
|
|
70
|
+
the stream. The first outcome declared through `setOutcome` is retained, but a
|
|
71
|
+
fatal execution, merge, error-handling, or downstream processing failure makes
|
|
72
|
+
the final `onFinish` outcome `failed`. Individual `error` chunks do not change the
|
|
73
|
+
outcome by themselves. When merging multiple child streams, aggregate their
|
|
74
|
+
outcomes and call `setOutcome` once.
|
|
75
|
+
|
|
60
76
|
## API Signature
|
|
61
77
|
|
|
62
78
|
### Parameters
|
|
@@ -65,12 +81,12 @@ const stream = createUIMessageStream({
|
|
|
65
81
|
content={[
|
|
66
82
|
{
|
|
67
83
|
name: 'execute',
|
|
68
|
-
type: '(options: { writer:
|
|
84
|
+
type: '(options: { writer: UIMessageStreamWriterWithOutcome }) => Promise<void> | void',
|
|
69
85
|
description:
|
|
70
86
|
'A function that receives a writer instance and can use it to write UI message chunks to the stream.',
|
|
71
87
|
properties: [
|
|
72
88
|
{
|
|
73
|
-
type: '
|
|
89
|
+
type: 'UIMessageStreamWriterWithOutcome',
|
|
74
90
|
parameters: [
|
|
75
91
|
{
|
|
76
92
|
name: 'write',
|
|
@@ -83,6 +99,12 @@ const stream = createUIMessageStream({
|
|
|
83
99
|
description:
|
|
84
100
|
'Merges the contents of another UI message stream into this stream.',
|
|
85
101
|
},
|
|
102
|
+
{
|
|
103
|
+
name: 'setOutcome',
|
|
104
|
+
type: '(outcome: UIMessageStreamOutcome) => void',
|
|
105
|
+
description:
|
|
106
|
+
"Declares the operation-level outcome of the composed stream. The first outcome declared through this method is retained, while fatal execution, merge, error-handling, or downstream processing failures override declarations. Supported statuses are 'completed', 'failed', 'aborted', and 'unknown'. Declaring an outcome does not write a chunk or close the stream.",
|
|
107
|
+
},
|
|
86
108
|
{
|
|
87
109
|
name: 'onError',
|
|
88
110
|
type: '(error: unknown) => string',
|
|
@@ -107,7 +129,7 @@ const stream = createUIMessageStream({
|
|
|
107
129
|
},
|
|
108
130
|
{
|
|
109
131
|
name: 'onFinish',
|
|
110
|
-
type: '(options: { messages: UIMessage[]; isContinuation: boolean; isAborted: boolean; responseMessage: UIMessage; finishReason?: FinishReason }) => PromiseLike<void> | void',
|
|
132
|
+
type: '(options: { messages: UIMessage[]; isContinuation: boolean; isAborted: boolean; outcome: UIMessageStreamOutcome; responseMessage: UIMessage; finishReason?: FinishReason }) => PromiseLike<void> | void',
|
|
111
133
|
description:
|
|
112
134
|
'A callback function that is called when the stream finishes.',
|
|
113
135
|
properties: [
|
|
@@ -130,6 +152,12 @@ const stream = createUIMessageStream({
|
|
|
130
152
|
type: 'boolean',
|
|
131
153
|
description: 'Indicates whether the stream was aborted.',
|
|
132
154
|
},
|
|
155
|
+
{
|
|
156
|
+
name: 'outcome',
|
|
157
|
+
type: "UIMessageStreamOutcome = { status: 'completed' } | { status: 'failed'; error?: unknown } | { status: 'aborted' } | { status: 'unknown' }",
|
|
158
|
+
description:
|
|
159
|
+
'The operation-level outcome of the stream. It reflects the stream owner declaration unless a fatal stream-processing failure occurs, and is separate from model finish reasons and individual error chunks.',
|
|
160
|
+
},
|
|
133
161
|
{
|
|
134
162
|
name: 'responseMessage',
|
|
135
163
|
type: 'UIMessage',
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: AI_ToolChoiceViolationError
|
|
3
|
+
description: Learn how to handle AI_ToolChoiceViolationError
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# AI_ToolChoiceViolationError
|
|
7
|
+
|
|
8
|
+
This error occurs when a model response does not satisfy an enforced tool
|
|
9
|
+
choice. It is thrown when `toolChoice` is set to `'required'` but the response
|
|
10
|
+
contains no structured tool call, or when a specifically selected tool was not
|
|
11
|
+
called.
|
|
12
|
+
|
|
13
|
+
The error does not automatically interpret text or reasoning as an executable
|
|
14
|
+
tool call. You can inspect `content` to implement opt-in recovery, including
|
|
15
|
+
schema validation before executing any recovered call.
|
|
16
|
+
|
|
17
|
+
## Properties
|
|
18
|
+
|
|
19
|
+
- `toolChoice`: The effective tool choice that the response did not satisfy
|
|
20
|
+
- `finishReason`: The reason why the model finished generating the response
|
|
21
|
+
- `provider`: The provider that returned the response
|
|
22
|
+
- `modelId`: The model that returned the response
|
|
23
|
+
- `content`: The normalized content returned by the model
|
|
24
|
+
- `message`: The error message
|
|
25
|
+
|
|
26
|
+
## Checking for this Error
|
|
27
|
+
|
|
28
|
+
You can check if an error is an instance of `AI_ToolChoiceViolationError` using:
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { ToolChoiceViolationError } from 'ai';
|
|
32
|
+
|
|
33
|
+
if (ToolChoiceViolationError.isInstance(error)) {
|
|
34
|
+
const serializedCall = error.content.find(part => part.type === 'text')?.text;
|
|
35
|
+
|
|
36
|
+
// Parse and validate serializedCall before treating it as a tool call.
|
|
37
|
+
}
|
|
38
|
+
```
|
|
@@ -33,6 +33,7 @@ collapsed: true
|
|
|
33
33
|
- [AI_RetryError](/docs/reference/ai-sdk-errors/ai-retry-error)
|
|
34
34
|
- [AI_ToolCallNotFoundForApprovalError](/docs/reference/ai-sdk-errors/ai-tool-call-not-found-for-approval-error)
|
|
35
35
|
- [AI_ToolCallRepairError](/docs/reference/ai-sdk-errors/ai-tool-call-repair-error)
|
|
36
|
+
- [AI_ToolChoiceViolationError](/docs/reference/ai-sdk-errors/ai-tool-choice-violation-error)
|
|
36
37
|
- [AI_TooManyEmbeddingValuesForCallError](/docs/reference/ai-sdk-errors/ai-too-many-embedding-values-for-call-error)
|
|
37
38
|
- [AI_TypeValidationError](/docs/reference/ai-sdk-errors/ai-type-validation-error)
|
|
38
39
|
- [AI_UIMessageStreamError](/docs/reference/ai-sdk-errors/ai-ui-message-stream-error)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.272",
|
|
4
4
|
"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.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@opentelemetry/api": "^1.9.0",
|
|
48
|
-
"@ai-sdk/gateway": "3.0.
|
|
48
|
+
"@ai-sdk/gateway": "3.0.185",
|
|
49
49
|
"@ai-sdk/provider": "3.0.15",
|
|
50
|
-
"@ai-sdk/provider-utils": "4.0.
|
|
50
|
+
"@ai-sdk/provider-utils": "4.0.50"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@edge-runtime/vm": "^5.0.0",
|
|
@@ -8,7 +8,7 @@ import type { TimeoutConfiguration } from '../prompt/call-settings';
|
|
|
8
8
|
import type { InferUIMessageChunk } from '../ui-message-stream';
|
|
9
9
|
import { convertToModelMessages } from '../ui/convert-to-model-messages';
|
|
10
10
|
import type { InferUITools, UIMessage } from '../ui/ui-messages';
|
|
11
|
-
import {
|
|
11
|
+
import { validateUIMessagesForAgent } from '../ui/validate-ui-messages';
|
|
12
12
|
import type { AsyncIterableStream } from '../util/async-iterable-stream';
|
|
13
13
|
import type { Agent } from './agent';
|
|
14
14
|
import type { ToolLoopAgentOnStepFinishCallback } from './tool-loop-agent-settings';
|
|
@@ -58,7 +58,7 @@ export async function createAgentUIStream<
|
|
|
58
58
|
InferUIMessageChunk<UIMessage<MESSAGE_METADATA, never, InferUITools<TOOLS>>>
|
|
59
59
|
>
|
|
60
60
|
> {
|
|
61
|
-
const validatedMessages = await
|
|
61
|
+
const validatedMessages = await validateUIMessagesForAgent<
|
|
62
62
|
UIMessage<MESSAGE_METADATA, never, InferUITools<TOOLS>>
|
|
63
63
|
>({
|
|
64
64
|
messages: uiMessages,
|
|
@@ -119,6 +119,13 @@ export type ToolLoopAgentSettings<
|
|
|
119
119
|
*/
|
|
120
120
|
onFinish?: ToolLoopAgentOnFinishCallback<NoInfer<TOOLS>>;
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Secret for HMAC-signing tool approval requests. When set, the server
|
|
124
|
+
* signs each approval request at issuance and verifies the signature when
|
|
125
|
+
* the approval is replayed, preventing client-forged approvals.
|
|
126
|
+
*/
|
|
127
|
+
experimental_toolApprovalSecret?: string | Uint8Array;
|
|
128
|
+
|
|
122
129
|
/**
|
|
123
130
|
* Additional provider-specific options. They are passed through
|
|
124
131
|
* to the provider from the AI SDK and enable provider-specific
|
|
@@ -185,6 +192,7 @@ export type ToolLoopAgentSettings<
|
|
|
185
192
|
| 'stopWhen'
|
|
186
193
|
| 'experimental_telemetry'
|
|
187
194
|
| 'activeTools'
|
|
195
|
+
| 'experimental_toolApprovalSecret'
|
|
188
196
|
| 'providerOptions'
|
|
189
197
|
| 'experimental_context'
|
|
190
198
|
| 'experimental_download'
|
|
@@ -209,6 +217,7 @@ export type ToolLoopAgentSettings<
|
|
|
209
217
|
| 'stopWhen'
|
|
210
218
|
| 'experimental_telemetry'
|
|
211
219
|
| 'activeTools'
|
|
220
|
+
| 'experimental_toolApprovalSecret'
|
|
212
221
|
| 'providerOptions'
|
|
213
222
|
| 'experimental_context'
|
|
214
223
|
| 'experimental_download'
|
package/src/error/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ export { NoSpeechGeneratedError } from './no-speech-generated-error';
|
|
|
28
28
|
export { NoTranscriptGeneratedError } from './no-transcript-generated-error';
|
|
29
29
|
export { NoVideoGeneratedError } from './no-video-generated-error';
|
|
30
30
|
export { NoSuchToolError } from './no-such-tool-error';
|
|
31
|
+
export { ToolChoiceViolationError } from './tool-choice-violation-error';
|
|
31
32
|
export { ToolCallRepairError } from './tool-call-repair-error';
|
|
32
33
|
export { UnsupportedModelVersionError } from './unsupported-model-version-error';
|
|
33
34
|
export { UIMessageStreamError } from './ui-message-stream-error';
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AISDKError,
|
|
3
|
+
type LanguageModelV3Content,
|
|
4
|
+
type LanguageModelV3ToolChoice,
|
|
5
|
+
} from '@ai-sdk/provider';
|
|
6
|
+
import type { FinishReason } from '../types/language-model';
|
|
7
|
+
|
|
8
|
+
const name = 'AI_ToolChoiceViolationError';
|
|
9
|
+
const marker = `vercel.ai.error.${name}`;
|
|
10
|
+
const symbol = Symbol.for(marker);
|
|
11
|
+
|
|
12
|
+
type EnforcedToolChoice = Extract<
|
|
13
|
+
LanguageModelV3ToolChoice,
|
|
14
|
+
{ type: 'required' } | { type: 'tool' }
|
|
15
|
+
>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Thrown when a model response does not satisfy an enforced tool choice.
|
|
19
|
+
*/
|
|
20
|
+
export class ToolChoiceViolationError extends AISDKError {
|
|
21
|
+
private readonly [symbol] = true; // used in isInstance
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The tool choice that the model response did not satisfy.
|
|
25
|
+
*/
|
|
26
|
+
readonly toolChoice: EnforcedToolChoice;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Reason why the model finished generating the response.
|
|
30
|
+
*/
|
|
31
|
+
readonly finishReason: FinishReason;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The provider that returned the response.
|
|
35
|
+
*/
|
|
36
|
+
readonly provider: string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The model that returned the response.
|
|
40
|
+
*/
|
|
41
|
+
readonly modelId: string;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The normalized content returned by the model.
|
|
45
|
+
*
|
|
46
|
+
* This can be inspected to recover a tool call that the provider returned as
|
|
47
|
+
* text or reasoning instead of a structured tool call.
|
|
48
|
+
*/
|
|
49
|
+
readonly content: Array<LanguageModelV3Content>;
|
|
50
|
+
|
|
51
|
+
constructor({
|
|
52
|
+
toolChoice,
|
|
53
|
+
finishReason,
|
|
54
|
+
provider,
|
|
55
|
+
modelId,
|
|
56
|
+
content,
|
|
57
|
+
message = toolChoice.type === 'required'
|
|
58
|
+
? 'Model response did not contain a tool call even though tool choice was required.'
|
|
59
|
+
: `Model response did not contain a call to the required tool '${toolChoice.toolName}'.`,
|
|
60
|
+
}: {
|
|
61
|
+
toolChoice: EnforcedToolChoice;
|
|
62
|
+
finishReason: FinishReason;
|
|
63
|
+
provider: string;
|
|
64
|
+
modelId: string;
|
|
65
|
+
content: Array<LanguageModelV3Content>;
|
|
66
|
+
message?: string;
|
|
67
|
+
}) {
|
|
68
|
+
super({ name, message });
|
|
69
|
+
|
|
70
|
+
this.toolChoice = toolChoice;
|
|
71
|
+
this.finishReason = finishReason;
|
|
72
|
+
this.provider = provider;
|
|
73
|
+
this.modelId = modelId;
|
|
74
|
+
this.content = content;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static isInstance(error: unknown): error is ToolChoiceViolationError {
|
|
78
|
+
return AISDKError.hasMarker(error, marker);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
type ProviderOptions,
|
|
12
12
|
} from '@ai-sdk/provider-utils';
|
|
13
13
|
import type { Tracer } from '@opentelemetry/api';
|
|
14
|
-
import { NoOutputGeneratedError } from '../error';
|
|
14
|
+
import { NoOutputGeneratedError, ToolChoiceViolationError } from '../error';
|
|
15
15
|
import { notify } from '../util/notify';
|
|
16
16
|
import { logWarnings } from '../logger/log-warnings';
|
|
17
17
|
import { resolveLanguageModel } from '../model/resolve-model';
|
|
@@ -961,6 +961,30 @@ export async function generateText<
|
|
|
961
961
|
}),
|
|
962
962
|
),
|
|
963
963
|
);
|
|
964
|
+
|
|
965
|
+
const enforcedToolChoice =
|
|
966
|
+
stepToolChoice?.type === 'required' ||
|
|
967
|
+
stepToolChoice?.type === 'tool'
|
|
968
|
+
? stepToolChoice
|
|
969
|
+
: undefined;
|
|
970
|
+
|
|
971
|
+
if (
|
|
972
|
+
enforcedToolChoice != null &&
|
|
973
|
+
!stepToolCalls.some(
|
|
974
|
+
toolCall =>
|
|
975
|
+
enforcedToolChoice.type === 'required' ||
|
|
976
|
+
toolCall.toolName === enforcedToolChoice.toolName,
|
|
977
|
+
)
|
|
978
|
+
) {
|
|
979
|
+
throw new ToolChoiceViolationError({
|
|
980
|
+
toolChoice: enforcedToolChoice,
|
|
981
|
+
finishReason: currentModelResponse.finishReason.unified,
|
|
982
|
+
provider: stepModel.provider,
|
|
983
|
+
modelId: stepModel.modelId,
|
|
984
|
+
content: currentModelResponse.content,
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
|
|
964
988
|
const toolApprovalRequests: Record<
|
|
965
989
|
string,
|
|
966
990
|
ToolApprovalRequestOutput<TOOLS>
|
|
@@ -64,6 +64,7 @@ import type {
|
|
|
64
64
|
InferUIMessageChunk,
|
|
65
65
|
UIMessageChunk,
|
|
66
66
|
} from '../ui-message-stream/ui-message-chunks';
|
|
67
|
+
import type { UIMessageStreamOutcome } from '../ui-message-stream/ui-message-stream-outcome';
|
|
67
68
|
import type { UIMessageStreamResponseInit } from '../ui-message-stream/ui-message-stream-response-init';
|
|
68
69
|
import type {
|
|
69
70
|
InferUIMessageData,
|
|
@@ -2660,6 +2661,26 @@ class DefaultStreamTextResult<
|
|
|
2660
2661
|
}: UIMessageStreamOptions<UI_MESSAGE> = {}): AsyncIterableStream<
|
|
2661
2662
|
InferUIMessageChunk<UI_MESSAGE>
|
|
2662
2663
|
> {
|
|
2664
|
+
let outcome: UIMessageStreamOutcome = { status: 'unknown' };
|
|
2665
|
+
let hasFatalFailure = false;
|
|
2666
|
+
|
|
2667
|
+
const setSourceOutcome = (newOutcome: UIMessageStreamOutcome) => {
|
|
2668
|
+
if (
|
|
2669
|
+
!hasFatalFailure &&
|
|
2670
|
+
outcome.status !== 'completed' &&
|
|
2671
|
+
outcome.status !== 'aborted' &&
|
|
2672
|
+
newOutcome.status !== 'unknown' &&
|
|
2673
|
+
(outcome.status === 'unknown' || newOutcome.status !== 'failed')
|
|
2674
|
+
) {
|
|
2675
|
+
outcome = newOutcome;
|
|
2676
|
+
}
|
|
2677
|
+
};
|
|
2678
|
+
|
|
2679
|
+
const failOutcome = (error: unknown) => {
|
|
2680
|
+
hasFatalFailure = true;
|
|
2681
|
+
outcome = { status: 'failed', error };
|
|
2682
|
+
};
|
|
2683
|
+
|
|
2663
2684
|
const responseMessageId =
|
|
2664
2685
|
generateMessageId != null
|
|
2665
2686
|
? getResponseUIMessageId({
|
|
@@ -2680,7 +2701,58 @@ class DefaultStreamTextResult<
|
|
|
2680
2701
|
return tool?.type === 'dynamic' ? true : undefined;
|
|
2681
2702
|
};
|
|
2682
2703
|
|
|
2683
|
-
const
|
|
2704
|
+
const trackFatalFailures = <T>(stream: ReadableStream<T>) => {
|
|
2705
|
+
const reader = stream.getReader();
|
|
2706
|
+
let readerReleased = false;
|
|
2707
|
+
let streamCancelled = false;
|
|
2708
|
+
|
|
2709
|
+
const releaseReader = () => {
|
|
2710
|
+
if (!readerReleased) {
|
|
2711
|
+
reader.releaseLock();
|
|
2712
|
+
readerReleased = true;
|
|
2713
|
+
}
|
|
2714
|
+
};
|
|
2715
|
+
|
|
2716
|
+
return new ReadableStream<T>({
|
|
2717
|
+
async pull(controller) {
|
|
2718
|
+
try {
|
|
2719
|
+
const { done, value } = await reader.read();
|
|
2720
|
+
|
|
2721
|
+
if (done) {
|
|
2722
|
+
releaseReader();
|
|
2723
|
+
if (!streamCancelled) {
|
|
2724
|
+
controller.close();
|
|
2725
|
+
}
|
|
2726
|
+
} else {
|
|
2727
|
+
controller.enqueue(value);
|
|
2728
|
+
}
|
|
2729
|
+
} catch (error) {
|
|
2730
|
+
releaseReader();
|
|
2731
|
+
if (!streamCancelled) {
|
|
2732
|
+
failOutcome(error);
|
|
2733
|
+
controller.error(error);
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
},
|
|
2737
|
+
|
|
2738
|
+
async cancel(reason) {
|
|
2739
|
+
streamCancelled = true;
|
|
2740
|
+
if (readerReleased) {
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
try {
|
|
2745
|
+
await reader.cancel(reason);
|
|
2746
|
+
} finally {
|
|
2747
|
+
releaseReader();
|
|
2748
|
+
}
|
|
2749
|
+
},
|
|
2750
|
+
});
|
|
2751
|
+
};
|
|
2752
|
+
|
|
2753
|
+
const sourceStream = trackFatalFailures(this.fullStream);
|
|
2754
|
+
|
|
2755
|
+
const convertedStream = sourceStream.pipeThrough(
|
|
2684
2756
|
new TransformStream<
|
|
2685
2757
|
TextStreamPart<TOOLS>,
|
|
2686
2758
|
UIMessageChunk<
|
|
@@ -3021,10 +3093,20 @@ class DefaultStreamTextResult<
|
|
|
3021
3093
|
messageMetadata: messageMetadataValue,
|
|
3022
3094
|
});
|
|
3023
3095
|
}
|
|
3096
|
+
|
|
3097
|
+
if (part.type === 'finish') {
|
|
3098
|
+
setSourceOutcome({ status: 'completed' });
|
|
3099
|
+
} else if (part.type === 'abort') {
|
|
3100
|
+
setSourceOutcome({ status: 'aborted' });
|
|
3101
|
+
} else if (part.type === 'error') {
|
|
3102
|
+
setSourceOutcome({ status: 'failed', error: part.error });
|
|
3103
|
+
}
|
|
3024
3104
|
},
|
|
3025
3105
|
}),
|
|
3026
3106
|
);
|
|
3027
3107
|
|
|
3108
|
+
const baseStream = trackFatalFailures(convertedStream);
|
|
3109
|
+
|
|
3028
3110
|
return createAsyncIterableStream(
|
|
3029
3111
|
handleUIMessageStreamFinish<UI_MESSAGE>({
|
|
3030
3112
|
stream: baseStream,
|
|
@@ -3032,6 +3114,7 @@ class DefaultStreamTextResult<
|
|
|
3032
3114
|
originalMessages,
|
|
3033
3115
|
onFinish,
|
|
3034
3116
|
onError,
|
|
3117
|
+
getOutcome: () => outcome,
|
|
3035
3118
|
}),
|
|
3036
3119
|
);
|
|
3037
3120
|
}
|
|
@@ -6,7 +6,7 @@ import type { Agent } from '../agent/agent';
|
|
|
6
6
|
import type { ChatTransport } from './chat-transport';
|
|
7
7
|
import { convertToModelMessages } from './convert-to-model-messages';
|
|
8
8
|
import type { InferUITools, UIMessage } from './ui-messages';
|
|
9
|
-
import {
|
|
9
|
+
import { validateUIMessagesForAgent } from './validate-ui-messages';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Options for the `DirectChatTransport` class.
|
|
@@ -80,7 +80,7 @@ export class DirectChatTransport<
|
|
|
80
80
|
ReadableStream<UIMessageChunk>
|
|
81
81
|
> {
|
|
82
82
|
// Validate the incoming UI messages
|
|
83
|
-
const validatedMessages = await
|
|
83
|
+
const validatedMessages = await validateUIMessagesForAgent<UI_MESSAGE>({
|
|
84
84
|
messages,
|
|
85
85
|
tools: this.agent.tools,
|
|
86
86
|
});
|