| /** | ||
| * The operation-level outcome of a UI message stream. | ||
| * | ||
| * This is separate from model finish reasons and individual stream chunks. | ||
| * Fatal stream-processing failures override outcomes declared by the stream | ||
| * owner. | ||
| */ | ||
| export type UIMessageStreamOutcome = | ||
| | { status: 'completed' } | ||
| | { status: 'failed'; error?: unknown } | ||
| | { status: 'aborted' } | ||
| | { status: 'unknown' }; |
+5
-5
| { | ||
| "name": "ai", | ||
| "version": "7.0.82", | ||
| "version": "7.0.83", | ||
| "type": "module", | ||
@@ -50,5 +50,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.", | ||
| "devDependencies": { | ||
| "@ai-sdk/amazon-bedrock": "5.0.65", | ||
| "@ai-sdk/deepseek": "3.0.34", | ||
| "@ai-sdk/google": "4.0.53", | ||
| "@ai-sdk/amazon-bedrock": "5.0.66", | ||
| "@ai-sdk/deepseek": "3.0.35", | ||
| "@ai-sdk/google": "4.0.54", | ||
| "@ai-sdk/groq": "4.0.33", | ||
@@ -58,3 +58,3 @@ "@ai-sdk/huggingface": "2.0.39", | ||
| "@ai-sdk/open-responses": "2.0.34", | ||
| "@ai-sdk/openai": "4.0.49", | ||
| "@ai-sdk/openai": "4.0.50", | ||
| "@ai-sdk/test-server": "2.0.1", | ||
@@ -61,0 +61,0 @@ "@ai-sdk/xai": "4.0.47", |
@@ -24,3 +24,3 @@ import type { | ||
| } from '../ui/ui-messages'; | ||
| import { validateUIMessages } from '../ui/validate-ui-messages'; | ||
| import { validateUIMessagesForAgent } from '../ui/validate-ui-messages'; | ||
| import { | ||
@@ -81,3 +81,3 @@ createAsyncIterableStream, | ||
| > { | ||
| const validatedMessages = await validateUIMessages<UI_MESSAGE>({ | ||
| const validatedMessages = await validateUIMessagesForAgent<UI_MESSAGE>({ | ||
| messages: uiMessages, | ||
@@ -84,0 +84,0 @@ // tools are compatible; the casting is required because the context param is |
@@ -9,5 +9,6 @@ import { | ||
| import type { UIMessageStreamOnEndCallback } from './ui-message-stream-on-end-callback'; | ||
| import type { UIMessageStreamOutcome } from './ui-message-stream-outcome'; | ||
| import type { UIMessageStreamOnStepEndCallback } from './ui-message-stream-on-step-end-callback'; | ||
| import type { UIMessageStreamOnStepFinishCallback } from './ui-message-stream-on-step-finish-callback'; | ||
| import type { UIMessageStreamWriter } from './ui-message-stream-writer'; | ||
| import type { UIMessageStreamWriterWithOutcome } from './ui-message-stream-writer'; | ||
@@ -40,3 +41,3 @@ /** | ||
| execute: (options: { | ||
| writer: UIMessageStreamWriter<UI_MESSAGE>; | ||
| writer: UIMessageStreamWriterWithOutcome<UI_MESSAGE>; | ||
| }) => Promise<void> | void; | ||
@@ -77,2 +78,3 @@ onError?: (error: unknown) => string; | ||
| const ongoingStreamPromises: Promise<void>[] = []; | ||
| let outcome: UIMessageStreamOutcome = { status: 'unknown' }; | ||
@@ -93,2 +95,38 @@ const stream = new ReadableStream({ | ||
| function setOutcome(newOutcome: UIMessageStreamOutcome) { | ||
| if (outcome.status === 'unknown' && newOutcome.status !== 'unknown') { | ||
| outcome = newOutcome; | ||
| } | ||
| } | ||
| function failOutcome(error: unknown) { | ||
| outcome = { status: 'failed', error }; | ||
| } | ||
| function safeError(error: unknown) { | ||
| try { | ||
| controller.error(error); | ||
| } catch { | ||
| // suppress errors when the stream has been closed | ||
| } | ||
| } | ||
| function handleError(error: unknown) { | ||
| failOutcome(error); | ||
| let errorText: string; | ||
| try { | ||
| errorText = onError(error); | ||
| } catch (onErrorError) { | ||
| failOutcome(onErrorError); | ||
| safeError(onErrorError); | ||
| return; | ||
| } | ||
| safeEnqueue({ | ||
| type: 'error', | ||
| errorText, | ||
| } as InferUIMessageChunk<UI_MESSAGE>); | ||
| } | ||
| try { | ||
@@ -110,9 +148,7 @@ const result = execute({ | ||
| })().catch(error => { | ||
| safeEnqueue({ | ||
| type: 'error', | ||
| errorText: onError(error), | ||
| } as InferUIMessageChunk<UI_MESSAGE>); | ||
| handleError(error); | ||
| }), | ||
| ); | ||
| }, | ||
| setOutcome, | ||
| onError, | ||
@@ -125,6 +161,3 @@ }, | ||
| result.catch(error => { | ||
| safeEnqueue({ | ||
| type: 'error', | ||
| errorText: onError(error), | ||
| } as InferUIMessageChunk<UI_MESSAGE>); | ||
| handleError(error); | ||
| }), | ||
@@ -134,6 +167,3 @@ ); | ||
| } catch (error) { | ||
| safeEnqueue({ | ||
| type: 'error', | ||
| errorText: onError(error), | ||
| } as InferUIMessageChunk<UI_MESSAGE>); | ||
| handleError(error); | ||
| } | ||
@@ -145,8 +175,7 @@ | ||
| // from callbacks. | ||
| const waitForStreams: Promise<void> = new Promise(async resolve => { | ||
| const waitForStreams: Promise<void> = (async () => { | ||
| while (ongoingStreamPromises.length > 0) { | ||
| await ongoingStreamPromises.shift(); | ||
| } | ||
| resolve(); | ||
| }); | ||
| })(); | ||
@@ -168,3 +197,4 @@ waitForStreams.finally(() => { | ||
| onError, | ||
| getOutcome: () => outcome, | ||
| }); | ||
| } |
@@ -11,2 +11,3 @@ import { | ||
| import type { UIMessageStreamOnEndCallback } from './ui-message-stream-on-end-callback'; | ||
| import type { UIMessageStreamOutcome } from './ui-message-stream-outcome'; | ||
| import type { UIMessageStreamOnStepEndCallback } from './ui-message-stream-on-step-end-callback'; | ||
@@ -24,2 +25,3 @@ import type { UIMessageStreamOnStepFinishCallback } from './ui-message-stream-on-step-finish-callback'; | ||
| stream, | ||
| getOutcome, | ||
| }: { | ||
@@ -59,2 +61,7 @@ stream: ReadableStream<InferUIMessageChunk<UI_MESSAGE>>; | ||
| onFinish?: UIMessageStreamOnEndCallback<UI_MESSAGE>; | ||
| /** | ||
| * Returns the operation-level outcome declared by the stream owner. | ||
| */ | ||
| getOutcome?: () => UIMessageStreamOutcome; | ||
| }): ReadableStream<InferUIMessageChunk<UI_MESSAGE>> { | ||
@@ -72,3 +79,10 @@ // last message is only relevant for assistant messages | ||
| let isAborted = false; | ||
| let hasProcessingFailure = false; | ||
| let processingError: unknown; | ||
| const recordProcessingFailure = (error: unknown) => { | ||
| hasProcessingFailure = true; | ||
| processingError = error; | ||
| }; | ||
| const idInjectedStream = stream.pipeThrough( | ||
@@ -80,17 +94,27 @@ new TransformStream< | ||
| transform(chunk, controller) { | ||
| // when there is no messageId in the start chunk, | ||
| // but the user checked for persistence, | ||
| // inject the messageId into the chunk | ||
| if (chunk.type === 'start') { | ||
| const startChunk = chunk as UIMessageChunk & { type: 'start' }; | ||
| if (startChunk.messageId == null && messageId != null) { | ||
| startChunk.messageId = messageId; | ||
| try { | ||
| let outputChunk = chunk; | ||
| // when there is no messageId in the start chunk, | ||
| // but the user checked for persistence, | ||
| // inject the messageId into the chunk | ||
| if (chunk.type === 'start') { | ||
| const startChunk = chunk as UIMessageChunk & { type: 'start' }; | ||
| if (startChunk.messageId == null && messageId != null) { | ||
| outputChunk = { | ||
| ...startChunk, | ||
| messageId, | ||
| } as InferUIMessageChunk<UI_MESSAGE>; | ||
| } | ||
| } | ||
| } | ||
| if (chunk.type === 'abort') { | ||
| isAborted = true; | ||
| if (chunk.type === 'abort') { | ||
| isAborted = true; | ||
| } | ||
| controller.enqueue(outputChunk); | ||
| } catch (error) { | ||
| recordProcessingFailure(error); | ||
| throw error; | ||
| } | ||
| controller.enqueue(chunk); | ||
| }, | ||
@@ -121,3 +145,8 @@ }), | ||
| ) => { | ||
| await job({ state, write: () => {} }); | ||
| try { | ||
| await job({ state, write: () => {} }); | ||
| } catch (error) { | ||
| recordProcessingFailure(error); | ||
| throw error; | ||
| } | ||
| }; | ||
@@ -134,5 +163,13 @@ | ||
| const isContinuation = state.message.id === lastMessage?.id; | ||
| const declaredOutcome = getOutcome?.() ?? { status: 'unknown' }; | ||
| const outcome: UIMessageStreamOutcome = hasProcessingFailure | ||
| ? { status: 'failed', error: processingError } | ||
| : declaredOutcome.status === 'unknown' && isAborted | ||
| ? { status: 'aborted' } | ||
| : declaredOutcome; | ||
| await resolvedOnEnd({ | ||
| isAborted, | ||
| isAborted: isAborted || outcome.status === 'aborted', | ||
| isContinuation, | ||
| outcome, | ||
| responseMessage: state.message as UI_MESSAGE, | ||
@@ -139,0 +176,0 @@ messages: [ |
@@ -19,4 +19,8 @@ export { createUIMessageStream } from './create-ui-message-stream'; | ||
| export type { UIMessageStreamOnFinishCallback } from './ui-message-stream-on-finish-callback'; | ||
| export type { UIMessageStreamOutcome } from './ui-message-stream-outcome'; | ||
| export type { UIMessageStreamOnStepEndCallback } from './ui-message-stream-on-step-end-callback'; | ||
| export type { UIMessageStreamOnStepFinishCallback } from './ui-message-stream-on-step-finish-callback'; | ||
| export type { UIMessageStreamWriter } from './ui-message-stream-writer'; | ||
| export type { | ||
| UIMessageStreamWriter, | ||
| UIMessageStreamWriterWithOutcome, | ||
| } from './ui-message-stream-writer'; |
@@ -10,2 +10,3 @@ import type { ToolSet } from '@ai-sdk/provider-utils'; | ||
| import type { InferUIMessageChunk } from './ui-message-chunks'; | ||
| import type { UIMessageStreamOutcome } from './ui-message-stream-outcome'; | ||
| import { toUIMessageChunk } from './to-ui-message-chunk'; | ||
@@ -41,2 +42,22 @@ | ||
| > { | ||
| let outcome: UIMessageStreamOutcome = { status: 'unknown' }; | ||
| let hasFatalFailure = false; | ||
| const setSourceOutcome = (newOutcome: UIMessageStreamOutcome) => { | ||
| if ( | ||
| !hasFatalFailure && | ||
| outcome.status !== 'completed' && | ||
| outcome.status !== 'aborted' && | ||
| newOutcome.status !== 'unknown' && | ||
| (outcome.status === 'unknown' || newOutcome.status !== 'failed') | ||
| ) { | ||
| outcome = newOutcome; | ||
| } | ||
| }; | ||
| const failOutcome = (error: unknown) => { | ||
| hasFatalFailure = true; | ||
| outcome = { status: 'failed', error }; | ||
| }; | ||
| const responseMessageId = | ||
@@ -50,33 +71,93 @@ generateMessageId != null | ||
| const uiMessageChunkStream = stream.pipeThrough( | ||
| new TransformStream({ | ||
| transform: async (part, controller) => { | ||
| const messageMetadataValue = messageMetadata?.({ part }); | ||
| const sourceReader = stream.getReader(); | ||
| let sourceReaderReleased = false; | ||
| let sourceStreamCancelled = false; | ||
| const uiMessageChunk = toUIMessageChunk(part, { | ||
| tools, | ||
| sendReasoning, | ||
| sendSources, | ||
| sendStart, | ||
| sendFinish, | ||
| onError, | ||
| messageMetadata: messageMetadataValue, | ||
| responseMessageId, | ||
| }); | ||
| const releaseSourceReader = () => { | ||
| if (!sourceReaderReleased) { | ||
| sourceReader.releaseLock(); | ||
| sourceReaderReleased = true; | ||
| } | ||
| }; | ||
| if (uiMessageChunk != null) { | ||
| controller.enqueue(uiMessageChunk); | ||
| const sourceStream = new ReadableStream<TextStreamPart<TOOLS>>({ | ||
| async pull(controller) { | ||
| try { | ||
| const { done, value } = await sourceReader.read(); | ||
| if (done) { | ||
| releaseSourceReader(); | ||
| if (!sourceStreamCancelled) { | ||
| controller.close(); | ||
| } | ||
| } else { | ||
| controller.enqueue(value); | ||
| } | ||
| } catch (error) { | ||
| releaseSourceReader(); | ||
| if (!sourceStreamCancelled) { | ||
| failOutcome(error); | ||
| controller.error(error); | ||
| } | ||
| } | ||
| }, | ||
| // start and finish events already include metadata in the converted | ||
| // chunk; for other part types emit a separate message-metadata chunk | ||
| if ( | ||
| messageMetadataValue != null && | ||
| part.type !== 'start' && | ||
| part.type !== 'finish' | ||
| ) { | ||
| controller.enqueue({ | ||
| type: 'message-metadata', | ||
| async cancel(reason) { | ||
| sourceStreamCancelled = true; | ||
| if (sourceReaderReleased) { | ||
| return; | ||
| } | ||
| try { | ||
| await sourceReader.cancel(reason); | ||
| } finally { | ||
| releaseSourceReader(); | ||
| } | ||
| }, | ||
| }); | ||
| const uiMessageChunkStream = sourceStream.pipeThrough( | ||
| new TransformStream({ | ||
| transform: async (part, controller) => { | ||
| try { | ||
| const messageMetadataValue = messageMetadata?.({ part }); | ||
| const uiMessageChunk = toUIMessageChunk(part, { | ||
| tools, | ||
| sendReasoning, | ||
| sendSources, | ||
| sendStart, | ||
| sendFinish, | ||
| onError, | ||
| messageMetadata: messageMetadataValue, | ||
| responseMessageId, | ||
| }); | ||
| if (uiMessageChunk != null) { | ||
| controller.enqueue(uiMessageChunk); | ||
| } | ||
| // start and finish events already include metadata in the converted | ||
| // chunk; for other part types emit a separate message-metadata chunk | ||
| if ( | ||
| messageMetadataValue != null && | ||
| part.type !== 'start' && | ||
| part.type !== 'finish' | ||
| ) { | ||
| controller.enqueue({ | ||
| type: 'message-metadata', | ||
| messageMetadata: messageMetadataValue, | ||
| }); | ||
| } | ||
| if (part.type === 'finish') { | ||
| setSourceOutcome({ status: 'completed' }); | ||
| } else if (part.type === 'abort') { | ||
| setSourceOutcome({ status: 'aborted' }); | ||
| } else if (part.type === 'error') { | ||
| setSourceOutcome({ status: 'failed', error: part.error }); | ||
| } | ||
| } catch (error) { | ||
| failOutcome(error); | ||
| throw error; | ||
| } | ||
@@ -93,3 +174,4 @@ }, | ||
| onError, | ||
| getOutcome: () => outcome, | ||
| }); | ||
| } |
| import type { FinishReason } from '../types/language-model'; | ||
| import type { UIMessage } from '../ui/ui-messages'; | ||
| import type { UIMessageStreamOutcome } from './ui-message-stream-outcome'; | ||
@@ -23,2 +24,8 @@ export type UIMessageStreamOnEndCallback<UI_MESSAGE extends UIMessage> = | ||
| /** | ||
| * The operation-level outcome of the stream. Fatal stream-processing | ||
| * failures override outcomes declared by the stream owner. | ||
| */ | ||
| outcome: UIMessageStreamOutcome; | ||
| /** | ||
| * The message that was sent to the client as a response | ||
@@ -25,0 +32,0 @@ * (including the original message if it was extended). |
| import type { UIMessage } from '../ui'; | ||
| import type { ErrorHandler } from '../util/error-handler'; | ||
| import type { InferUIMessageChunk } from './ui-message-chunks'; | ||
| import type { UIMessageStreamOutcome } from './ui-message-stream-outcome'; | ||
@@ -25,1 +26,15 @@ export interface UIMessageStreamWriter< | ||
| } | ||
| export interface UIMessageStreamWriterWithOutcome< | ||
| UI_MESSAGE extends UIMessage = UIMessage, | ||
| > extends UIMessageStreamWriter<UI_MESSAGE> { | ||
| /** | ||
| * Declares the operation-level outcome of the composed stream. | ||
| * | ||
| * The first outcome declared through this method is retained. Fatal | ||
| * execution, merge, error-handling, or downstream processing failures | ||
| * override declared outcomes. Declaring an outcome does not write a chunk or | ||
| * close the stream. | ||
| */ | ||
| setOutcome(outcome: UIMessageStreamOutcome): void; | ||
| } |
@@ -14,3 +14,3 @@ import type { Context, Tool, ToolSet } from '@ai-sdk/provider-utils'; | ||
| } from './ui-messages'; | ||
| import { validateUIMessages } from './validate-ui-messages'; | ||
| import { validateUIMessagesForAgent } from './validate-ui-messages'; | ||
@@ -97,3 +97,3 @@ /** | ||
| // Validate the incoming UI messages | ||
| const validatedMessages = await validateUIMessages<UI_MESSAGE>({ | ||
| const validatedMessages = await validateUIMessagesForAgent<UI_MESSAGE>({ | ||
| messages, | ||
@@ -100,0 +100,0 @@ // tools are compatible; the casting is required because the context param is |
@@ -39,2 +39,3 @@ import { isToolUIPart, type UIMessage } from './ui-messages'; | ||
| part.state === 'output-error' || | ||
| part.state === 'output-denied' || | ||
| part.state === 'approval-responded', | ||
@@ -41,0 +42,0 @@ ) |
| import { TypeValidationError, type JSONObject } from '@ai-sdk/provider'; | ||
| import { | ||
| lazySchema, | ||
| safeValidateTypes, | ||
| validateTypes, | ||
@@ -16,2 +17,3 @@ zodSchema, | ||
| DataUIPart, | ||
| DynamicToolUIPart, | ||
| InferUIMessageData, | ||
@@ -30,2 +32,21 @@ InferUIMessageTools, | ||
| function isEmptyObject(value: unknown): value is Record<string, never> { | ||
| return ( | ||
| value != null && | ||
| typeof value === 'object' && | ||
| !Array.isArray(value) && | ||
| Object.keys(value).length === 0 | ||
| ); | ||
| } | ||
| function asDynamicToolPart(toolPart: ToolUIPart): DynamicToolUIPart { | ||
| const { type, ...part } = toolPart; | ||
| return { | ||
| ...part, | ||
| type: 'dynamic-tool', | ||
| toolName: type.slice(5), | ||
| } as DynamicToolUIPart; | ||
| } | ||
| const uiMessagesSchema = lazySchema(() => | ||
@@ -389,13 +410,3 @@ zodSchema( | ||
| /** | ||
| * Validates a list of UI messages like `validateUIMessages`, | ||
| * but instead of throwing it returns `{ success: true, data }` | ||
| * or `{ success: false, error }`. | ||
| */ | ||
| export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>({ | ||
| messages, | ||
| metadataSchema, | ||
| dataSchemas, | ||
| tools, | ||
| }: { | ||
| type ValidateUIMessagesOptions<UI_MESSAGE extends UIMessage> = { | ||
| messages: unknown; | ||
@@ -414,3 +425,17 @@ metadataSchema?: FlexibleSchema<UIMessage['metadata']>; | ||
| }; | ||
| }): Promise<SafeValidateUIMessagesResult<UI_MESSAGE>> { | ||
| }; | ||
| async function safeValidateUIMessagesInternal<UI_MESSAGE extends UIMessage>( | ||
| { | ||
| messages, | ||
| metadataSchema, | ||
| dataSchemas, | ||
| tools, | ||
| }: ValidateUIMessagesOptions<UI_MESSAGE>, | ||
| { | ||
| convertMissingTerminalToolsToDynamic, | ||
| }: { | ||
| convertMissingTerminalToolsToDynamic: boolean; | ||
| }, | ||
| ): Promise<SafeValidateUIMessagesResult<UI_MESSAGE>> { | ||
| try { | ||
@@ -446,3 +471,6 @@ if (messages == null) { | ||
| if (dataSchemas || tools) { | ||
| const shouldValidateToolParts = | ||
| tools != null || convertMissingTerminalToolsToDynamic; | ||
| if (dataSchemas || shouldValidateToolParts) { | ||
| for (const [msgIdx, message] of validatedMessages.entries()) { | ||
@@ -483,3 +511,3 @@ for (const [partIdx, part] of message.parts.entries()) { | ||
| // Tool part validation | ||
| if (tools && part.type.startsWith('tool-')) { | ||
| if (shouldValidateToolParts && part.type.startsWith('tool-')) { | ||
| const toolPart = part as ToolUIPart< | ||
@@ -489,10 +517,17 @@ InferUIMessageTools<UI_MESSAGE> | ||
| const toolName = toolPart.type.slice(5); | ||
| const tool = getOwn(tools, toolName); | ||
| const tool = tools == null ? undefined : getOwn(tools, toolName); | ||
| const isTerminal = | ||
| toolPart.state === 'output-available' || | ||
| toolPart.state === 'output-error' || | ||
| toolPart.state === 'output-denied'; | ||
| if ( | ||
| !tool && | ||
| (toolPart.state === 'output-available' || | ||
| toolPart.state === 'output-error' || | ||
| toolPart.state === 'output-denied') | ||
| ) { | ||
| if (!tool && isTerminal) { | ||
| if (tools != null || convertMissingTerminalToolsToDynamic) { | ||
| // Persisted terminal history can reference tools that are no | ||
| // longer registered. Normalize those parts so callers do not | ||
| // receive unvalidated values under current static tool types. | ||
| message.parts[partIdx] = asDynamicToolPart( | ||
| toolPart, | ||
| ) as (typeof message.parts)[number]; | ||
| } | ||
| continue; | ||
@@ -517,15 +552,49 @@ } | ||
| const inputValidationContext = { | ||
| field: `messages[${msgIdx}].parts[${partIdx}].input`, | ||
| entityName: toolName, | ||
| entityId: toolPart.toolCallId, | ||
| }; | ||
| let convertToDynamic = false; | ||
| // Tool input validation | ||
| // Note: input is intentionally not re-validated for terminal states. | ||
| // Terminal tool calls can keep invalid or incomplete input, and | ||
| // re-validating it on replay would crash follow-up messages. | ||
| if (toolPart.state === 'input-available') { | ||
| if (toolPart.state === 'output-error') { | ||
| // Failed calls can retain invalid input. Keep them loadable, but | ||
| // expose incompatible input as unknown instead of the current | ||
| // static tool input type. | ||
| if (toolPart.input !== undefined) { | ||
| const result = await safeValidateTypes({ | ||
| value: toolPart.input, | ||
| schema: tool.inputSchema, | ||
| context: inputValidationContext, | ||
| }); | ||
| convertToDynamic = !result.success; | ||
| } | ||
| } else if (toolPart.state === 'output-available') { | ||
| const result = await safeValidateTypes({ | ||
| value: toolPart.input, | ||
| schema: tool.inputSchema, | ||
| context: inputValidationContext, | ||
| }); | ||
| if (!result.success) { | ||
| // Empty terminal input can represent aborted or incomplete | ||
| // history whose input was never streamed. Preserve it without | ||
| // claiming that it matches the current static input type. | ||
| if (isEmptyObject(toolPart.input)) { | ||
| convertToDynamic = true; | ||
| } else { | ||
| throw result.error; | ||
| } | ||
| } | ||
| } else if ( | ||
| toolPart.state === 'input-available' || | ||
| toolPart.state === 'approval-requested' || | ||
| toolPart.state === 'approval-responded' || | ||
| toolPart.state === 'output-denied' | ||
| ) { | ||
| await validateTypes({ | ||
| value: toolPart.input, | ||
| schema: tool.inputSchema, | ||
| context: { | ||
| field: `messages[${msgIdx}].parts[${partIdx}].input`, | ||
| entityName: toolName, | ||
| entityId: toolPart.toolCallId, | ||
| }, | ||
| context: inputValidationContext, | ||
| }); | ||
@@ -546,2 +615,8 @@ } | ||
| } | ||
| if (convertToDynamic) { | ||
| message.parts[partIdx] = asDynamicToolPart( | ||
| toolPart, | ||
| ) as (typeof message.parts)[number]; | ||
| } | ||
| } | ||
@@ -567,2 +642,15 @@ } | ||
| /** | ||
| * Validates a list of UI messages like `validateUIMessages`, | ||
| * but instead of throwing it returns `{ success: true, data }` | ||
| * or `{ success: false, error }`. | ||
| */ | ||
| export async function safeValidateUIMessages<UI_MESSAGE extends UIMessage>( | ||
| options: ValidateUIMessagesOptions<UI_MESSAGE>, | ||
| ): Promise<SafeValidateUIMessagesResult<UI_MESSAGE>> { | ||
| return safeValidateUIMessagesInternal(options, { | ||
| convertMissingTerminalToolsToDynamic: false, | ||
| }); | ||
| } | ||
| /** | ||
| * Validates a list of UI messages. | ||
@@ -574,27 +662,20 @@ * | ||
| */ | ||
| export async function validateUIMessages<UI_MESSAGE extends UIMessage>({ | ||
| messages, | ||
| metadataSchema, | ||
| dataSchemas, | ||
| tools, | ||
| }: { | ||
| messages: unknown; | ||
| metadataSchema?: FlexibleSchema<UIMessage['metadata']>; | ||
| dataSchemas?: { | ||
| [NAME in keyof InferUIMessageData<UI_MESSAGE> & string]?: FlexibleSchema< | ||
| InferUIMessageData<UI_MESSAGE>[NAME] | ||
| >; | ||
| }; | ||
| tools?: { | ||
| [NAME in keyof InferUIMessageTools<UI_MESSAGE> & string]?: Tool< | ||
| InferUIMessageTools<UI_MESSAGE>[NAME]['input'], | ||
| InferUIMessageTools<UI_MESSAGE>[NAME]['output'] | ||
| >; | ||
| }; | ||
| }): Promise<Array<UI_MESSAGE>> { | ||
| const response = await safeValidateUIMessages({ | ||
| messages, | ||
| metadataSchema, | ||
| dataSchemas, | ||
| tools, | ||
| export async function validateUIMessages<UI_MESSAGE extends UIMessage>( | ||
| options: ValidateUIMessagesOptions<UI_MESSAGE>, | ||
| ): Promise<Array<UI_MESSAGE>> { | ||
| const response = await safeValidateUIMessages(options); | ||
| if (!response.success) throw response.error; | ||
| return response.data; | ||
| } | ||
| export async function validateUIMessagesForAgent<UI_MESSAGE extends UIMessage>( | ||
| options: ValidateUIMessagesOptions<UI_MESSAGE>, | ||
| ): Promise<Array<UI_MESSAGE>> { | ||
| const response = await safeValidateUIMessagesInternal(options, { | ||
| // Agent tool sets can include ephemeral tools (for example, tools from a | ||
| // disconnected MCP server), so terminal history is converted to dynamic | ||
| // tool parts when those tools are no longer registered. | ||
| convertMissingTerminalToolsToDynamic: true, | ||
| }); | ||
@@ -601,0 +682,0 @@ |
@@ -28,5 +28,10 @@ import { createResolvablePromise } from './create-resolvable-promise'; | ||
| let isClosed = false; | ||
| let isCancelled = false; | ||
| let waitForNewStream = createResolvablePromise<void>(); | ||
| const terminate = () => { | ||
| if (isCancelled) { | ||
| return; | ||
| } | ||
| isClosed = true; | ||
@@ -44,2 +49,6 @@ waitForNewStream.resolve(); | ||
| const processPull = async () => { | ||
| if (isCancelled) { | ||
| return; | ||
| } | ||
| // Case 1: Outer stream is closed and no more inner streams | ||
@@ -64,2 +73,6 @@ if (isClosed && innerStreams.length === 0) { | ||
| if (isCancelled) { | ||
| return; | ||
| } | ||
| if (done) { | ||
@@ -81,2 +94,6 @@ // Case 3: Current inner stream is done | ||
| } catch (error) { | ||
| if (isCancelled) { | ||
| return; | ||
| } | ||
| // Case 5: Current inner stream throws an error | ||
@@ -97,2 +114,6 @@ currentStream.onError?.(error); | ||
| async cancel() { | ||
| isCancelled = true; | ||
| isClosed = true; | ||
| waitForNewStream.resolve(); | ||
| for (const { reader, onCancel } of innerStreams) { | ||
@@ -103,3 +124,2 @@ onCancel?.(); | ||
| innerStreams = []; | ||
| isClosed = true; | ||
| }, | ||
@@ -114,2 +134,8 @@ }), | ||
| ) => { | ||
| if (isCancelled) { | ||
| callbacks?.onCancel?.(); | ||
| void innerStream.cancel().catch(() => {}); | ||
| return; | ||
| } | ||
| if (isClosed) { | ||
@@ -131,2 +157,6 @@ throw new Error('Cannot add inner stream: outer stream is closed'); | ||
| close: () => { | ||
| if (isCancelled) { | ||
| return; | ||
| } | ||
| isClosed = true; | ||
@@ -133,0 +163,0 @@ waitForNewStream.resolve(); |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
6846874
0.45%636
0.16%71537
0.67%