@assistant-ui/react-ai-sdk 1.3.30 → 1.3.32
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/dist/assistant-stream/dist/core/tool/schema-utils.d.ts +15 -0
- package/dist/assistant-stream/dist/core/tool/schema-utils.d.ts.map +1 -0
- package/dist/{packages/assistant-stream → assistant-stream}/dist/core/tool/schema-utils.js +13 -4
- package/dist/assistant-stream/dist/core/tool/schema-utils.js.map +1 -0
- package/dist/{packages/assistant-stream → assistant-stream}/dist/core/tool/tool-types.d.ts +27 -1
- package/dist/assistant-stream/dist/core/tool/tool-types.d.ts.map +1 -0
- package/dist/assistant-stream/dist/index.d.ts +2 -0
- package/dist/{node_modules → assistant-stream/dist/node_modules}/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts +4 -5
- package/dist/assistant-stream/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts.map +1 -0
- package/dist/assistant-stream/dist/resumable/createResumableAssistantStreamResponse.d.ts.map +1 -0
- package/dist/assistant-stream/dist/resumable/createResumableAssistantStreamResponse.js.map +1 -0
- package/dist/assistant-stream/dist/utils/json/json-value.d.ts.map +1 -0
- package/dist/frontendTools.d.ts +33 -6
- package/dist/frontendTools.d.ts.map +1 -1
- package/dist/frontendTools.js +6 -29
- package/dist/frontendTools.js.map +1 -1
- package/dist/generativeTools.d.ts +60 -0
- package/dist/generativeTools.d.ts.map +1 -0
- package/dist/generativeTools.js +153 -0
- package/dist/generativeTools.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/modelContentEnvelope.d.ts +4 -4
- package/dist/modelContentEnvelope.d.ts.map +1 -1
- package/dist/modelContentEnvelope.js.map +1 -1
- package/dist/toolOutputConversion.d.ts +34 -0
- package/dist/toolOutputConversion.d.ts.map +1 -0
- package/dist/toolOutputConversion.js +31 -0
- package/dist/toolOutputConversion.js.map +1 -0
- package/dist/ui/resumable.d.ts +1 -1
- package/dist/ui/resumable.js +1 -1
- package/dist/ui/use-chat/AssistantChatTransport.d.ts.map +1 -1
- package/dist/ui/use-chat/AssistantChatTransport.js +2 -2
- package/dist/ui/use-chat/useAISDKRuntime.d.ts +3 -16
- package/dist/ui/use-chat/useAISDKRuntime.d.ts.map +1 -1
- package/dist/ui/use-chat/useAISDKRuntime.js +10 -4
- package/dist/ui/use-chat/useAISDKRuntime.js.map +1 -1
- package/dist/ui/use-chat/useChatRuntime.d.ts +2 -3
- package/dist/ui/use-chat/useChatRuntime.d.ts.map +1 -1
- package/dist/ui/use-chat/useChatRuntime.js +4 -3
- package/dist/ui/use-chat/useChatRuntime.js.map +1 -1
- package/dist/ui/use-chat/useExternalHistory.d.ts.map +1 -1
- package/dist/ui/use-chat/useExternalHistory.js +16 -8
- package/dist/ui/use-chat/useExternalHistory.js.map +1 -1
- package/dist/ui/utils/convertMessage.d.ts +3 -2
- package/dist/ui/utils/convertMessage.d.ts.map +1 -1
- package/dist/ui/utils/convertMessage.js +3 -1
- package/dist/ui/utils/convertMessage.js.map +1 -1
- package/dist/ui/utils/toCreateMessage.js +4 -0
- package/dist/ui/utils/toCreateMessage.js.map +1 -1
- package/dist/usage.d.ts.map +1 -1
- package/package.json +8 -7
- package/src/frontendTools.test.ts +24 -0
- package/src/frontendTools.ts +7 -32
- package/src/generativeTools.test.ts +407 -0
- package/src/generativeTools.ts +310 -0
- package/src/index.ts +7 -0
- package/src/modelContentEnvelope.ts +7 -5
- package/src/toolOutputConversion.ts +29 -0
- package/src/ui/use-chat/useAISDKRuntime.test.ts +85 -0
- package/src/ui/use-chat/useAISDKRuntime.ts +21 -17
- package/src/ui/use-chat/useChatRuntime.ts +22 -21
- package/src/ui/use-chat/useExternalHistory.test.ts +60 -1
- package/src/ui/use-chat/useExternalHistory.ts +17 -8
- package/src/ui/utils/convertMessage.test.ts +25 -0
- package/src/ui/utils/convertMessage.ts +6 -0
- package/src/ui/utils/toCreateMessage.test.ts +54 -0
- package/src/ui/utils/toCreateMessage.ts +5 -0
- package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts.map +0 -1
- package/dist/packages/assistant-stream/dist/core/tool/schema-utils.d.ts +0 -1
- package/dist/packages/assistant-stream/dist/core/tool/schema-utils.js.map +0 -1
- package/dist/packages/assistant-stream/dist/core/tool/tool-types.d.ts.map +0 -1
- package/dist/packages/assistant-stream/dist/index.d.ts +0 -1
- package/dist/packages/assistant-stream/dist/resumable/createResumableAssistantStreamResponse.d.ts.map +0 -1
- package/dist/packages/assistant-stream/dist/resumable/createResumableAssistantStreamResponse.js.map +0 -1
- package/dist/packages/assistant-stream/dist/utils/json/json-value.d.ts.map +0 -1
- /package/dist/{packages/assistant-stream → assistant-stream}/dist/core/AssistantStream.d.ts +0 -0
- /package/dist/{packages/assistant-stream → assistant-stream}/dist/core/AssistantStreamChunk.d.ts +0 -0
- /package/dist/{packages/assistant-stream → assistant-stream}/dist/core/accumulators/AssistantMessageStream.d.ts +0 -0
- /package/dist/{packages/assistant-stream → assistant-stream}/dist/core/accumulators/assistant-message-accumulator.d.ts +0 -0
- /package/dist/{packages/assistant-stream → assistant-stream}/dist/core/modules/assistant-stream.d.ts +0 -0
- /package/dist/{packages/assistant-stream → assistant-stream}/dist/core/modules/text.d.ts +0 -0
- /package/dist/{packages/assistant-stream → assistant-stream}/dist/core/modules/tool-call.d.ts +0 -0
- /package/dist/{packages/assistant-stream → assistant-stream}/dist/core/serialization/PlainText.d.ts +0 -0
- /package/dist/{packages/assistant-stream → assistant-stream}/dist/core/serialization/assistant-transport/AssistantTransport.d.ts +0 -0
- /package/dist/{packages/assistant-stream → assistant-stream}/dist/core/serialization/data-stream/DataStream.d.ts +0 -0
- /package/dist/{packages/assistant-stream → assistant-stream}/dist/core/serialization/ui-message-stream/UIMessageStream.d.ts +0 -0
- /package/dist/{packages/assistant-stream → assistant-stream}/dist/core/tool/ToolExecutionStream.d.ts +0 -0
- /package/dist/{packages/assistant-stream → assistant-stream}/dist/core/tool/toolResultStream.d.ts +0 -0
- /package/dist/{packages/assistant-stream → assistant-stream}/dist/core/utils/stream/AssistantMetaTransformStream.d.ts +0 -0
- /package/dist/{packages/assistant-stream → assistant-stream}/dist/core/utils/stream/AssistantTransformStream.d.ts +0 -0
- /package/dist/{packages/assistant-stream → assistant-stream}/dist/core/utils/types.d.ts +0 -0
- /package/dist/{packages/assistant-stream → assistant-stream}/dist/resumable/createResumableAssistantStreamResponse.d.ts +0 -0
- /package/dist/{packages/assistant-stream → assistant-stream}/dist/resumable/createResumableAssistantStreamResponse.js +0 -0
- /package/dist/{packages/assistant-stream → assistant-stream}/dist/resumable/index.d.ts +0 -0
- /package/dist/{packages/assistant-stream → assistant-stream}/dist/utils/json/json-value.d.ts +0 -0
- /package/dist/{packages/assistant-stream → assistant-stream}/dist/utils.d.ts +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"convertMessage.js","names":["unstable_createMessageConverter"],"sources":["../../../src/ui/utils/convertMessage.ts"],"sourcesContent":["import { isToolUIPart, getToolName, type UIMessage } from \"ai\";\nimport {\n createMessageConverter as unstable_createMessageConverter,\n type useExternalMessageConverter,\n} from \"@assistant-ui/core/react\";\nimport {\n isMcpAppUri,\n type ReasoningMessagePart,\n type ToolCallMessagePart,\n type TextMessagePart,\n type DataMessagePart,\n type SourceMessagePart,\n type SourceProviderMetadata,\n type FileMessagePart,\n type ThreadMessageLike,\n type McpAppMetadata,\n} from \"@assistant-ui/core\";\nimport type { ReadonlyJSONObject } from \"assistant-stream/utils\";\nimport { unwrapModelContentEnvelope } from \"../../modelContentEnvelope\";\n\ntype MessageMetadata = ThreadMessageLike[\"metadata\"];\nexport type AISDKMessageConverterMetadata =\n useExternalMessageConverter.Metadata & {\n toolArgsKeyOrderCache?: Map<string, Map<string, string[]>>;\n toolLastInputCache?: Map<string, ReadonlyJSONObject>;\n mcpAppMetadataCache?: Map<string, McpAppMetadata>;\n };\n\nfunction stripClosingDelimiters(json: string): string {\n return json.replace(/[}\\]\"]+$/, \"\");\n}\n\nconst MCP_APP_METADATA_CACHE_MAX = 100;\n\nfunction extractMcpAppMetadata(\n part: unknown,\n cache: Map<string, McpAppMetadata> | undefined,\n): McpAppMetadata | undefined {\n if (!part || typeof part !== \"object\") return undefined;\n const meta = (part as { callProviderMetadata?: unknown })\n .callProviderMetadata;\n const mcp =\n meta && typeof meta === \"object\"\n ? (meta as { mcp?: unknown }).mcp\n : undefined;\n const app =\n mcp && typeof mcp === \"object\" ? (mcp as { app?: unknown }).app : undefined;\n let a: Record<string, unknown>;\n if (app && typeof app === \"object\") {\n a = app as Record<string, unknown>;\n } else {\n // MCP-UI tools (e.g. xmcp) surface the UI pointer as\n // result._meta[\"ui/resourceUri\"] rather than in callProviderMetadata.\n const output = (part as { output?: unknown }).output;\n const outMeta =\n output && typeof output === \"object\"\n ? (output as { _meta?: unknown })._meta\n : undefined;\n const uiResourceUri =\n outMeta && typeof outMeta === \"object\"\n ? (outMeta as Record<string, unknown>)[\"ui/resourceUri\"]\n : undefined;\n if (typeof uiResourceUri !== \"string\") return undefined;\n a = { resourceUri: uiResourceUri };\n }\n if (typeof a[\"resourceUri\"] !== \"string\") return undefined;\n if (!isMcpAppUri(a[\"resourceUri\"])) return undefined;\n const cached = cache?.get(a[\"resourceUri\"]);\n if (cached) {\n cache!.delete(a[\"resourceUri\"]);\n cache!.set(a[\"resourceUri\"], cached);\n return cached;\n }\n const out: { -readonly [K in keyof McpAppMetadata]: McpAppMetadata[K] } = {\n resourceUri: a[\"resourceUri\"],\n };\n if (typeof a[\"mimeType\"] === \"string\") out.mimeType = a[\"mimeType\"];\n if (Array.isArray(a[\"visibility\"])) {\n out.visibility = a[\"visibility\"].filter(\n (v): v is \"model\" | \"app\" => v === \"model\" || v === \"app\",\n );\n }\n if (cache) {\n if (cache.size >= MCP_APP_METADATA_CACHE_MAX) {\n const oldest = cache.keys().next().value;\n if (oldest !== undefined) cache.delete(oldest);\n }\n cache.set(a[\"resourceUri\"], out);\n }\n return out;\n}\n\nconst hasOwn = (value: object, key: string) => Object.hasOwn(value, key);\n\nconst stabilizeToolArgsValue = (\n value: unknown,\n path: string,\n keyOrderByPath: Map<string, string[]>,\n): unknown => {\n if (Array.isArray(value)) {\n return value.map((item, idx) =>\n stabilizeToolArgsValue(item, `${path}[${idx}]`, keyOrderByPath),\n );\n }\n\n if (value && typeof value === \"object\") {\n const record = value as Record<string, unknown>;\n const currentKeys = Object.keys(record);\n const previousOrder = keyOrderByPath.get(path) ?? [];\n const previousOrderSet = new Set(previousOrder);\n const nextOrder = [\n ...previousOrder.filter((key) => hasOwn(record, key)),\n ...currentKeys.filter((key) => !previousOrderSet.has(key)),\n ];\n keyOrderByPath.set(path, nextOrder);\n\n return Object.fromEntries(\n nextOrder.map((key) => [\n key,\n stabilizeToolArgsValue(record[key], `${path}.${key}`, keyOrderByPath),\n ]),\n );\n }\n\n return value;\n};\n\nfunction stableStringifyToolArgs(\n keyOrderCache: Map<string, Map<string, string[]>> | undefined,\n cacheKey: string,\n args: ReadonlyJSONObject,\n): string {\n const keyOrderByPath = keyOrderCache?.get(cacheKey) ?? new Map();\n keyOrderCache?.set(cacheKey, keyOrderByPath);\n\n const stableArgs = stabilizeToolArgsValue(\n args,\n \"$\",\n keyOrderByPath,\n ) as ReadonlyJSONObject;\n return JSON.stringify(stableArgs);\n}\n\nfunction getToolApprovalAndInterrupt(\n part: {\n approval?:\n | {\n id: string;\n approved?: boolean;\n reason?: string;\n isAutomatic?: boolean;\n }\n | undefined;\n },\n toolStatus: { type: string; payload?: unknown } | undefined,\n): {\n approval?: NonNullable<ToolCallMessagePart[\"approval\"]>;\n interrupt?: NonNullable<ToolCallMessagePart[\"interrupt\"]>;\n} {\n if (part.approval && typeof part.approval.id === \"string\") {\n const { id, approved, reason, isAutomatic } = part.approval;\n return {\n approval: {\n id,\n ...(typeof approved === \"boolean\" && { approved }),\n ...(typeof reason === \"string\" && { reason }),\n ...(isAutomatic === true && { isAutomatic: true }),\n },\n };\n }\n\n if (toolStatus?.type === \"interrupt\") {\n return {\n interrupt: toolStatus.payload as NonNullable<\n ToolCallMessagePart[\"interrupt\"]\n >,\n };\n }\n\n return {};\n}\n\ntype MessageContent = Exclude<ThreadMessageLike[\"content\"], string>;\n\nfunction convertParts(\n message: UIMessage,\n metadata: AISDKMessageConverterMetadata,\n): MessageContent {\n if (!message.parts || message.parts.length === 0) {\n return [];\n }\n\n const converted = message.parts\n .filter(\n (p) =>\n p.type !== \"step-start\" &&\n (message.role !== \"user\" || p.type !== \"file\"),\n )\n .map((part) => {\n if (part.type === \"text\") {\n return {\n type: \"text\",\n text: part.text,\n } satisfies TextMessagePart;\n }\n\n if (part.type === \"reasoning\") {\n return {\n type: \"reasoning\",\n text: part.text,\n } satisfies ReasoningMessagePart;\n }\n\n if (isToolUIPart(part)) {\n const toolName = getToolName(part);\n const toolCallId = part.toolCallId;\n const argsKeyOrderCacheKey = `${message.id}:${toolCallId}`;\n\n const rawInput = part.input as ReadonlyJSONObject | null | undefined;\n let args: ReadonlyJSONObject;\n if (\n rawInput != null &&\n typeof rawInput === \"object\" &&\n !Array.isArray(rawInput)\n ) {\n args = rawInput;\n metadata.toolLastInputCache?.set(argsKeyOrderCacheKey, args);\n } else {\n args = metadata.toolLastInputCache?.get(argsKeyOrderCacheKey) ?? {};\n }\n\n let result: unknown;\n let modelContent: ToolCallMessagePart[\"modelContent\"];\n let isError = false;\n\n if (part.state === \"output-available\") {\n const unwrapped = unwrapModelContentEnvelope(part.output);\n result = unwrapped.result;\n modelContent = unwrapped.modelContent;\n } else if (part.state === \"output-error\") {\n isError = true;\n result = { error: part.errorText };\n } else if (part.state === \"output-denied\") {\n isError = true;\n result = {\n error:\n (part as { approval?: { reason?: string } }).approval?.reason ||\n \"Tool approval denied\",\n };\n }\n\n let argsText = stableStringifyToolArgs(\n metadata.toolArgsKeyOrderCache,\n argsKeyOrderCacheKey,\n args,\n );\n if (part.state === \"input-streaming\") {\n // strip closing delimiters added by the AI SDK's fix-json\n argsText = stripClosingDelimiters(argsText);\n } else {\n metadata.toolArgsKeyOrderCache?.delete(argsKeyOrderCacheKey);\n if (\n part.state === \"output-available\" ||\n part.state === \"output-error\" ||\n part.state === \"output-denied\"\n ) {\n metadata.toolLastInputCache?.delete(argsKeyOrderCacheKey);\n }\n }\n\n const toolStatus = metadata.toolStatuses?.[toolCallId];\n const mcpApp = extractMcpAppMetadata(\n part,\n metadata.mcpAppMetadataCache,\n );\n return {\n type: \"tool-call\",\n toolName,\n toolCallId,\n argsText,\n args,\n result,\n isError,\n ...(modelContent !== undefined && { modelContent }),\n ...(mcpApp && { mcp: { app: mcpApp } }),\n ...getToolApprovalAndInterrupt(part, toolStatus),\n } satisfies ToolCallMessagePart;\n }\n\n if (part.type === \"source-url\") {\n return {\n type: \"source\",\n sourceType: \"url\",\n id: part.sourceId,\n url: part.url,\n ...(part.title != null ? { title: part.title } : undefined),\n ...(part.providerMetadata != null\n ? {\n providerMetadata:\n part.providerMetadata as SourceProviderMetadata,\n }\n : undefined),\n } satisfies SourceMessagePart;\n }\n\n if (part.type === \"file\") {\n return {\n type: \"file\",\n data: part.url,\n mimeType: part.mediaType,\n ...(part.filename != null && { filename: part.filename }),\n } satisfies FileMessagePart;\n }\n\n if (part.type === \"source-document\") {\n return {\n type: \"source\",\n sourceType: \"document\",\n id: part.sourceId,\n title: part.title,\n mediaType: part.mediaType,\n ...(part.filename != null ? { filename: part.filename } : undefined),\n ...(part.providerMetadata != null\n ? {\n providerMetadata:\n part.providerMetadata as SourceProviderMetadata,\n }\n : undefined),\n } satisfies SourceMessagePart;\n }\n\n if (part.type.startsWith(\"data-\")) {\n return {\n type: \"data\",\n name: part.type.substring(5),\n data: (part as any).data,\n } satisfies DataMessagePart;\n }\n\n console.warn(`Unsupported message part type: ${part.type}`);\n return null;\n })\n .filter(Boolean) as MessageContent[number][];\n\n const seenToolCallIds = new Set<string>();\n return converted.filter((part) => {\n if (part.type === \"tool-call\" && part.toolCallId != null) {\n if (seenToolCallIds.has(part.toolCallId)) return false;\n seenToolCallIds.add(part.toolCallId);\n }\n return true;\n });\n}\n\nexport const AISDKMessageConverter = unstable_createMessageConverter(\n (message: UIMessage, metadata: AISDKMessageConverterMetadata) => {\n const createdAt = new Date();\n const content = convertParts(message, metadata);\n\n switch (message.role) {\n case \"user\":\n return {\n role: \"user\",\n id: message.id,\n createdAt,\n content,\n attachments: message.parts\n ?.filter((p) => p.type === \"file\")\n .map((part, idx) => ({\n id: idx.toString(),\n type: part.mediaType.startsWith(\"image/\") ? \"image\" : \"file\",\n name: part.filename ?? \"file\",\n content: [\n part.mediaType.startsWith(\"image/\")\n ? {\n type: \"image\",\n image: part.url,\n filename: part.filename!,\n }\n : {\n type: \"file\",\n filename: part.filename!,\n data: part.url,\n mimeType: part.mediaType,\n },\n ],\n contentType: part.mediaType ?? \"unknown/unknown\",\n status: { type: \"complete\" as const },\n })),\n metadata: message.metadata as MessageMetadata,\n };\n\n case \"system\":\n case \"assistant\": {\n const timing = metadata.messageTiming?.[message.id];\n return {\n role: message.role,\n id: message.id,\n createdAt,\n content,\n metadata: {\n ...(message.metadata as MessageMetadata),\n ...(timing && { timing }),\n },\n };\n }\n\n default:\n console.warn(`Unsupported message role: ${message.role}`);\n return [];\n }\n },\n);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"convertMessage.js","names":["unstable_createMessageConverter"],"sources":["../../../src/ui/utils/convertMessage.ts"],"sourcesContent":["import { isToolUIPart, getToolName, type UIMessage } from \"ai\";\nimport {\n createMessageConverter as unstable_createMessageConverter,\n type useExternalMessageConverter,\n} from \"@assistant-ui/core/react\";\nimport {\n isMcpAppUri,\n type ReasoningMessagePart,\n type ToolCallMessagePart,\n type TextMessagePart,\n type DataMessagePart,\n type SourceMessagePart,\n type SourceProviderMetadata,\n type FileMessagePart,\n type ThreadMessageLike,\n type McpAppMetadata,\n} from \"@assistant-ui/core\";\nimport type { ReadonlyJSONObject } from \"assistant-stream/utils\";\nimport { unwrapModelContentEnvelope } from \"../../modelContentEnvelope\";\n\ntype MessageMetadata = ThreadMessageLike[\"metadata\"];\nexport type AISDKMessageConverterMetadata =\n useExternalMessageConverter.Metadata & {\n toolArgsKeyOrderCache?: Map<string, Map<string, string[]>>;\n toolLastInputCache?: Map<string, ReadonlyJSONObject>;\n mcpAppMetadataCache?: Map<string, McpAppMetadata>;\n /** Id of the currently-streaming message, flagged optimistic (#4037). */\n optimisticMessageId?: string | undefined;\n };\n\nfunction stripClosingDelimiters(json: string): string {\n return json.replace(/[}\\]\"]+$/, \"\");\n}\n\nconst MCP_APP_METADATA_CACHE_MAX = 100;\n\nfunction extractMcpAppMetadata(\n part: unknown,\n cache: Map<string, McpAppMetadata> | undefined,\n): McpAppMetadata | undefined {\n if (!part || typeof part !== \"object\") return undefined;\n const meta = (part as { callProviderMetadata?: unknown })\n .callProviderMetadata;\n const mcp =\n meta && typeof meta === \"object\"\n ? (meta as { mcp?: unknown }).mcp\n : undefined;\n const app =\n mcp && typeof mcp === \"object\" ? (mcp as { app?: unknown }).app : undefined;\n let a: Record<string, unknown>;\n if (app && typeof app === \"object\") {\n a = app as Record<string, unknown>;\n } else {\n // MCP-UI tools (e.g. xmcp) surface the UI pointer as\n // result._meta[\"ui/resourceUri\"] rather than in callProviderMetadata.\n const output = (part as { output?: unknown }).output;\n const outMeta =\n output && typeof output === \"object\"\n ? (output as { _meta?: unknown })._meta\n : undefined;\n const uiResourceUri =\n outMeta && typeof outMeta === \"object\"\n ? (outMeta as Record<string, unknown>)[\"ui/resourceUri\"]\n : undefined;\n if (typeof uiResourceUri !== \"string\") return undefined;\n a = { resourceUri: uiResourceUri };\n }\n if (typeof a[\"resourceUri\"] !== \"string\") return undefined;\n if (!isMcpAppUri(a[\"resourceUri\"])) return undefined;\n const cached = cache?.get(a[\"resourceUri\"]);\n if (cached) {\n cache!.delete(a[\"resourceUri\"]);\n cache!.set(a[\"resourceUri\"], cached);\n return cached;\n }\n const out: { -readonly [K in keyof McpAppMetadata]: McpAppMetadata[K] } = {\n resourceUri: a[\"resourceUri\"],\n };\n if (typeof a[\"mimeType\"] === \"string\") out.mimeType = a[\"mimeType\"];\n if (Array.isArray(a[\"visibility\"])) {\n out.visibility = a[\"visibility\"].filter(\n (v): v is \"model\" | \"app\" => v === \"model\" || v === \"app\",\n );\n }\n if (cache) {\n if (cache.size >= MCP_APP_METADATA_CACHE_MAX) {\n const oldest = cache.keys().next().value;\n if (oldest !== undefined) cache.delete(oldest);\n }\n cache.set(a[\"resourceUri\"], out);\n }\n return out;\n}\n\nconst hasOwn = (value: object, key: string) => Object.hasOwn(value, key);\n\nconst stabilizeToolArgsValue = (\n value: unknown,\n path: string,\n keyOrderByPath: Map<string, string[]>,\n): unknown => {\n if (Array.isArray(value)) {\n return value.map((item, idx) =>\n stabilizeToolArgsValue(item, `${path}[${idx}]`, keyOrderByPath),\n );\n }\n\n if (value && typeof value === \"object\") {\n const record = value as Record<string, unknown>;\n const currentKeys = Object.keys(record);\n const previousOrder = keyOrderByPath.get(path) ?? [];\n const previousOrderSet = new Set(previousOrder);\n const nextOrder = [\n ...previousOrder.filter((key) => hasOwn(record, key)),\n ...currentKeys.filter((key) => !previousOrderSet.has(key)),\n ];\n keyOrderByPath.set(path, nextOrder);\n\n return Object.fromEntries(\n nextOrder.map((key) => [\n key,\n stabilizeToolArgsValue(record[key], `${path}.${key}`, keyOrderByPath),\n ]),\n );\n }\n\n return value;\n};\n\nfunction stableStringifyToolArgs(\n keyOrderCache: Map<string, Map<string, string[]>> | undefined,\n cacheKey: string,\n args: ReadonlyJSONObject,\n): string {\n const keyOrderByPath = keyOrderCache?.get(cacheKey) ?? new Map();\n keyOrderCache?.set(cacheKey, keyOrderByPath);\n\n const stableArgs = stabilizeToolArgsValue(\n args,\n \"$\",\n keyOrderByPath,\n ) as ReadonlyJSONObject;\n return JSON.stringify(stableArgs);\n}\n\nfunction getToolApprovalAndInterrupt(\n part: {\n approval?:\n | {\n id: string;\n approved?: boolean;\n reason?: string;\n isAutomatic?: boolean;\n }\n | undefined;\n },\n toolStatus: { type: string; payload?: unknown } | undefined,\n): {\n approval?: NonNullable<ToolCallMessagePart[\"approval\"]>;\n interrupt?: NonNullable<ToolCallMessagePart[\"interrupt\"]>;\n} {\n if (part.approval && typeof part.approval.id === \"string\") {\n const { id, approved, reason, isAutomatic } = part.approval;\n return {\n approval: {\n id,\n ...(typeof approved === \"boolean\" && { approved }),\n ...(typeof reason === \"string\" && { reason }),\n ...(isAutomatic === true && { isAutomatic: true }),\n },\n };\n }\n\n if (toolStatus?.type === \"interrupt\") {\n return {\n interrupt: toolStatus.payload as NonNullable<\n ToolCallMessagePart[\"interrupt\"]\n >,\n };\n }\n\n return {};\n}\n\ntype MessageContent = Exclude<ThreadMessageLike[\"content\"], string>;\n\nfunction convertParts(\n message: UIMessage,\n metadata: AISDKMessageConverterMetadata,\n): MessageContent {\n if (!message.parts || message.parts.length === 0) {\n return [];\n }\n\n const converted = message.parts\n .filter(\n (p) =>\n p.type !== \"step-start\" &&\n (message.role !== \"user\" || p.type !== \"file\"),\n )\n .map((part) => {\n if (part.type === \"text\") {\n return {\n type: \"text\",\n text: part.text,\n } satisfies TextMessagePart;\n }\n\n if (part.type === \"reasoning\") {\n return {\n type: \"reasoning\",\n text: part.text,\n } satisfies ReasoningMessagePart;\n }\n\n if (isToolUIPart(part)) {\n const toolName = getToolName(part);\n const toolCallId = part.toolCallId;\n const argsKeyOrderCacheKey = `${message.id}:${toolCallId}`;\n\n const rawInput = part.input as ReadonlyJSONObject | null | undefined;\n let args: ReadonlyJSONObject;\n if (\n rawInput != null &&\n typeof rawInput === \"object\" &&\n !Array.isArray(rawInput)\n ) {\n args = rawInput;\n metadata.toolLastInputCache?.set(argsKeyOrderCacheKey, args);\n } else {\n args = metadata.toolLastInputCache?.get(argsKeyOrderCacheKey) ?? {};\n }\n\n let result: unknown;\n let modelContent: ToolCallMessagePart[\"modelContent\"];\n let isError = false;\n\n if (part.state === \"output-available\") {\n const unwrapped = unwrapModelContentEnvelope(part.output);\n result = unwrapped.result;\n modelContent = unwrapped.modelContent;\n } else if (part.state === \"output-error\") {\n isError = true;\n result = { error: part.errorText };\n } else if (part.state === \"output-denied\") {\n isError = true;\n result = {\n error:\n (part as { approval?: { reason?: string } }).approval?.reason ||\n \"Tool approval denied\",\n };\n }\n\n let argsText = stableStringifyToolArgs(\n metadata.toolArgsKeyOrderCache,\n argsKeyOrderCacheKey,\n args,\n );\n if (part.state === \"input-streaming\") {\n // strip closing delimiters added by the AI SDK's fix-json\n argsText = stripClosingDelimiters(argsText);\n } else {\n metadata.toolArgsKeyOrderCache?.delete(argsKeyOrderCacheKey);\n if (\n part.state === \"output-available\" ||\n part.state === \"output-error\" ||\n part.state === \"output-denied\"\n ) {\n metadata.toolLastInputCache?.delete(argsKeyOrderCacheKey);\n }\n }\n\n const toolStatus = metadata.toolStatuses?.[toolCallId];\n const mcpApp = extractMcpAppMetadata(\n part,\n metadata.mcpAppMetadataCache,\n );\n return {\n type: \"tool-call\",\n toolName,\n toolCallId,\n argsText,\n args,\n result,\n isError,\n ...(modelContent !== undefined && { modelContent }),\n ...(mcpApp && { mcp: { app: mcpApp } }),\n ...getToolApprovalAndInterrupt(part, toolStatus),\n } satisfies ToolCallMessagePart;\n }\n\n if (part.type === \"source-url\") {\n return {\n type: \"source\",\n sourceType: \"url\",\n id: part.sourceId,\n url: part.url,\n ...(part.title != null ? { title: part.title } : undefined),\n ...(part.providerMetadata != null\n ? {\n providerMetadata:\n part.providerMetadata as SourceProviderMetadata,\n }\n : undefined),\n } satisfies SourceMessagePart;\n }\n\n if (part.type === \"file\") {\n return {\n type: \"file\",\n data: part.url,\n mimeType: part.mediaType,\n ...(part.filename != null && { filename: part.filename }),\n } satisfies FileMessagePart;\n }\n\n if (part.type === \"source-document\") {\n return {\n type: \"source\",\n sourceType: \"document\",\n id: part.sourceId,\n title: part.title,\n mediaType: part.mediaType,\n ...(part.filename != null ? { filename: part.filename } : undefined),\n ...(part.providerMetadata != null\n ? {\n providerMetadata:\n part.providerMetadata as SourceProviderMetadata,\n }\n : undefined),\n } satisfies SourceMessagePart;\n }\n\n if (part.type.startsWith(\"data-\")) {\n return {\n type: \"data\",\n name: part.type.substring(5),\n data: (part as any).data,\n } satisfies DataMessagePart;\n }\n\n console.warn(`Unsupported message part type: ${part.type}`);\n return null;\n })\n .filter(Boolean) as MessageContent[number][];\n\n const seenToolCallIds = new Set<string>();\n return converted.filter((part) => {\n if (part.type === \"tool-call\" && part.toolCallId != null) {\n if (seenToolCallIds.has(part.toolCallId)) return false;\n seenToolCallIds.add(part.toolCallId);\n }\n return true;\n });\n}\n\nexport const AISDKMessageConverter = unstable_createMessageConverter(\n (message: UIMessage, metadata: AISDKMessageConverterMetadata) => {\n const createdAt = new Date();\n const content = convertParts(message, metadata);\n\n switch (message.role) {\n case \"user\":\n return {\n role: \"user\",\n id: message.id,\n createdAt,\n content,\n attachments: message.parts\n ?.filter((p) => p.type === \"file\")\n .map((part, idx) => ({\n id: idx.toString(),\n type: part.mediaType.startsWith(\"image/\") ? \"image\" : \"file\",\n name: part.filename ?? \"file\",\n content: [\n part.mediaType.startsWith(\"image/\")\n ? {\n type: \"image\",\n image: part.url,\n filename: part.filename!,\n }\n : {\n type: \"file\",\n filename: part.filename!,\n data: part.url,\n mimeType: part.mediaType,\n },\n ],\n contentType: part.mediaType ?? \"unknown/unknown\",\n status: { type: \"complete\" as const },\n })),\n metadata: message.metadata as MessageMetadata,\n };\n\n case \"system\":\n case \"assistant\": {\n const timing = metadata.messageTiming?.[message.id];\n const isOptimistic =\n message.role === \"assistant\" &&\n message.id === metadata.optimisticMessageId;\n return {\n role: message.role,\n id: message.id,\n createdAt,\n content,\n metadata: {\n ...(message.metadata as MessageMetadata),\n ...(timing && { timing }),\n ...(isOptimistic && { isOptimistic: true }),\n },\n };\n }\n\n default:\n console.warn(`Unsupported message role: ${message.role}`);\n return [];\n }\n },\n);\n"],"mappings":"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"}
|
|
@@ -21,6 +21,10 @@ const toCreateMessage = (message) => {
|
|
|
21
21
|
mediaType: part.mimeType,
|
|
22
22
|
...part.filename && { filename: part.filename }
|
|
23
23
|
};
|
|
24
|
+
case "data": return {
|
|
25
|
+
type: `data-${part.name}`,
|
|
26
|
+
data: part.data
|
|
27
|
+
};
|
|
24
28
|
default: throw new Error(`Unsupported part type: ${part.type}`);
|
|
25
29
|
}
|
|
26
30
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toCreateMessage.js","names":[],"sources":["../../../src/ui/utils/toCreateMessage.ts"],"sourcesContent":["import type { AppendMessage } from \"@assistant-ui/core\";\nimport type {\n CreateUIMessage,\n UIDataTypes,\n UIMessage,\n UIMessagePart,\n UITools,\n} from \"ai\";\n\nexport const toCreateMessage = <UI_MESSAGE extends UIMessage = UIMessage>(\n message: AppendMessage,\n): CreateUIMessage<UI_MESSAGE> => {\n const inputParts = [\n ...message.content.filter((c) => c.type !== \"file\"),\n ...(message.attachments?.flatMap((a) =>\n a.content.map((c) => ({\n ...c,\n filename: a.name,\n })),\n ) ?? []),\n ];\n\n const parts = inputParts.map((part): UIMessagePart<UIDataTypes, UITools> => {\n switch (part.type) {\n case \"text\":\n return {\n type: \"text\",\n text: part.text,\n };\n case \"image\":\n return {\n type: \"file\",\n url: part.image,\n ...(part.filename && { filename: part.filename }),\n mediaType: \"image/png\",\n };\n case \"file\":\n return {\n type: \"file\",\n url: part.data,\n mediaType: part.mimeType,\n ...(part.filename && { filename: part.filename }),\n };\n default:\n throw new Error(`Unsupported part type: ${part.type}`);\n }\n });\n\n return {\n role: message.role,\n parts,\n metadata: message.metadata,\n } satisfies CreateUIMessage<UIMessage> as CreateUIMessage<UI_MESSAGE>;\n};\n"],"mappings":";AASA,MAAa,mBACX,YACgC;CAWhC,MAAM,QAAQ,CATZ,GAAG,QAAQ,QAAQ,QAAQ,MAAM,EAAE,SAAS,MAAM,GAClD,GAAI,QAAQ,aAAa,SAAS,MAChC,EAAE,QAAQ,KAAK,OAAO;EACpB,GAAG;EACH,UAAU,EAAE;CACd,EAAE,CACJ,KAAK,CAAC,CAGe,EAAE,KAAK,SAA8C;EAC1E,QAAQ,KAAK,MAAb;GACE,KAAK,QACH,OAAO;IACL,MAAM;IACN,MAAM,KAAK;GACb;GACF,KAAK,SACH,OAAO;IACL,MAAM;IACN,KAAK,KAAK;IACV,GAAI,KAAK,YAAY,EAAE,UAAU,KAAK,SAAS;IAC/C,WAAW;GACb;GACF,KAAK,QACH,OAAO;IACL,MAAM;IACN,KAAK,KAAK;IACV,WAAW,KAAK;IAChB,GAAI,KAAK,YAAY,EAAE,UAAU,KAAK,SAAS;GACjD;GACF,SACE,MAAM,IAAI,MAAM,0BAA0B,KAAK,MAAM;EACzD;CACF,CAAC;CAED,OAAO;EACL,MAAM,QAAQ;EACd;EACA,UAAU,QAAQ;CACpB;AACF"}
|
|
1
|
+
{"version":3,"file":"toCreateMessage.js","names":[],"sources":["../../../src/ui/utils/toCreateMessage.ts"],"sourcesContent":["import type { AppendMessage } from \"@assistant-ui/core\";\nimport type {\n CreateUIMessage,\n UIDataTypes,\n UIMessage,\n UIMessagePart,\n UITools,\n} from \"ai\";\n\nexport const toCreateMessage = <UI_MESSAGE extends UIMessage = UIMessage>(\n message: AppendMessage,\n): CreateUIMessage<UI_MESSAGE> => {\n const inputParts = [\n ...message.content.filter((c) => c.type !== \"file\"),\n ...(message.attachments?.flatMap((a) =>\n a.content.map((c) => ({\n ...c,\n filename: a.name,\n })),\n ) ?? []),\n ];\n\n const parts = inputParts.map((part): UIMessagePart<UIDataTypes, UITools> => {\n switch (part.type) {\n case \"text\":\n return {\n type: \"text\",\n text: part.text,\n };\n case \"image\":\n return {\n type: \"file\",\n url: part.image,\n ...(part.filename && { filename: part.filename }),\n mediaType: \"image/png\",\n };\n case \"file\":\n return {\n type: \"file\",\n url: part.data,\n mediaType: part.mimeType,\n ...(part.filename && { filename: part.filename }),\n };\n case \"data\":\n return {\n type: `data-${part.name}`,\n data: part.data,\n };\n default:\n throw new Error(`Unsupported part type: ${part.type}`);\n }\n });\n\n return {\n role: message.role,\n parts,\n metadata: message.metadata,\n } satisfies CreateUIMessage<UIMessage> as CreateUIMessage<UI_MESSAGE>;\n};\n"],"mappings":";AASA,MAAa,mBACX,YACgC;CAWhC,MAAM,QAAQ,CATZ,GAAG,QAAQ,QAAQ,QAAQ,MAAM,EAAE,SAAS,MAAM,GAClD,GAAI,QAAQ,aAAa,SAAS,MAChC,EAAE,QAAQ,KAAK,OAAO;EACpB,GAAG;EACH,UAAU,EAAE;CACd,EAAE,CACJ,KAAK,CAAC,CAGe,EAAE,KAAK,SAA8C;EAC1E,QAAQ,KAAK,MAAb;GACE,KAAK,QACH,OAAO;IACL,MAAM;IACN,MAAM,KAAK;GACb;GACF,KAAK,SACH,OAAO;IACL,MAAM;IACN,KAAK,KAAK;IACV,GAAI,KAAK,YAAY,EAAE,UAAU,KAAK,SAAS;IAC/C,WAAW;GACb;GACF,KAAK,QACH,OAAO;IACL,MAAM;IACN,KAAK,KAAK;IACV,WAAW,KAAK;IAChB,GAAI,KAAK,YAAY,EAAE,UAAU,KAAK,SAAS;GACjD;GACF,KAAK,QACH,OAAO;IACL,MAAM,QAAQ,KAAK;IACnB,MAAM,KAAK;GACb;GACF,SACE,MAAM,IAAI,MAAM,0BAA0B,KAAK,MAAM;EACzD;CACF,CAAC;CAED,OAAO;EACL,MAAM,QAAQ;EACd;EACA,UAAU,QAAQ;CACpB;AACF"}
|
package/dist/usage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usage.d.ts","names":[],"sources":["../src/usage.ts"],"mappings":";KAGY,gBAAA;EACV,WAAA;EACA,WAAA;EACA,YAAA;EACA,eAAA;EACA,iBAAA;AAAA;AAAA,UAGe,4BAAA;EACf,IAAA;EACA,QAAQ;AAAA;AAAA,iBAoGM,0BAAA,CACd,OAAA,EAAS,4BAAA,eACR,gBAAgB;AAAA,iBAeH,yBAAA,CACd,QAAA,WAAmB,4BAAA,iBAClB,gBAAgB;AAAA,iBAmBH,mBAAA,
|
|
1
|
+
{"version":3,"file":"usage.d.ts","names":[],"sources":["../src/usage.ts"],"mappings":";KAGY,gBAAA;EACV,WAAA;EACA,WAAA;EACA,YAAA;EACA,eAAA;EACA,iBAAA;AAAA;AAAA,UAGe,4BAAA;EACf,IAAA;EACA,QAAQ;AAAA;AAAA,iBAoGM,0BAAA,CACd,OAAA,EAAS,4BAAA,eACR,gBAAgB;AAAA,iBAeH,yBAAA,CACd,QAAA,WAAmB,4BAAA,iBAClB,gBAAgB;AAAA,iBAmBH,mBAAA,IAAuB,gBAAgB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/react-ai-sdk",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.32",
|
|
4
4
|
"description": "Vercel AI SDK adapter for assistant-ui",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-sdk",
|
|
@@ -30,10 +30,11 @@
|
|
|
30
30
|
],
|
|
31
31
|
"sideEffects": false,
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@ai-sdk/
|
|
34
|
-
"@
|
|
35
|
-
"@assistant-ui/
|
|
36
|
-
"
|
|
33
|
+
"@ai-sdk/mcp": "^1.0.45",
|
|
34
|
+
"@ai-sdk/react": "^3.0.195",
|
|
35
|
+
"@assistant-ui/core": "^0.2.10",
|
|
36
|
+
"@assistant-ui/store": "^0.2.13",
|
|
37
|
+
"ai": "^6.0.193",
|
|
37
38
|
"assistant-cloud": "*"
|
|
38
39
|
},
|
|
39
40
|
"peerDependencies": {
|
|
@@ -53,8 +54,8 @@
|
|
|
53
54
|
"jsdom": "^29.1.1",
|
|
54
55
|
"react": "^19.2.6",
|
|
55
56
|
"vitest": "^4.1.7",
|
|
56
|
-
"
|
|
57
|
-
"assistant-
|
|
57
|
+
"assistant-stream": "0.3.20",
|
|
58
|
+
"@assistant-ui/x-buildutils": "0.0.11"
|
|
58
59
|
},
|
|
59
60
|
"publishConfig": {
|
|
60
61
|
"access": "public",
|
|
@@ -125,4 +125,28 @@ describe("frontendTools", () => {
|
|
|
125
125
|
|
|
126
126
|
expect(output).toEqual({ type: "text", value: "hello" });
|
|
127
127
|
});
|
|
128
|
+
|
|
129
|
+
it("forwards providerOptions verbatim when present", () => {
|
|
130
|
+
const tools = frontendTools({
|
|
131
|
+
getWeather: {
|
|
132
|
+
description: "Get the weather",
|
|
133
|
+
parameters: { type: "object", properties: {} },
|
|
134
|
+
providerOptions: { anthropic: { deferLoading: true } },
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(tools.getWeather?.providerOptions).toEqual({
|
|
139
|
+
anthropic: { deferLoading: true },
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("omits providerOptions when absent", () => {
|
|
144
|
+
const tools = frontendTools({
|
|
145
|
+
getWeather: {
|
|
146
|
+
parameters: { type: "object", properties: {} },
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(tools.getWeather).not.toHaveProperty("providerOptions");
|
|
151
|
+
});
|
|
128
152
|
});
|
package/src/frontendTools.ts
CHANGED
|
@@ -1,43 +1,17 @@
|
|
|
1
1
|
import { jsonSchema, type ToolSet } from "ai";
|
|
2
|
-
import type {
|
|
3
|
-
import type { ToolModelContentPart } from "assistant-stream";
|
|
2
|
+
import type { ToolJSONSchema } from "assistant-stream";
|
|
4
3
|
import { unwrapModelContentEnvelope } from "./modelContentEnvelope";
|
|
4
|
+
import { toAISDKContent, toAISDKDefaultOutput } from "./toolOutputConversion";
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
value: parts.map((part) => {
|
|
9
|
-
if (part.type === "text") {
|
|
10
|
-
return { type: "text" as const, text: part.text };
|
|
11
|
-
}
|
|
12
|
-
const isImage = part.mediaType.startsWith("image/");
|
|
13
|
-
return isImage
|
|
14
|
-
? {
|
|
15
|
-
type: "image-data" as const,
|
|
16
|
-
data: part.data,
|
|
17
|
-
mediaType: part.mediaType,
|
|
18
|
-
}
|
|
19
|
-
: {
|
|
20
|
-
type: "file-data" as const,
|
|
21
|
-
data: part.data,
|
|
22
|
-
mediaType: part.mediaType,
|
|
23
|
-
...(part.filename !== undefined && { filename: part.filename }),
|
|
24
|
-
};
|
|
25
|
-
}),
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const defaultToModelOutput = ({ output }: { output: unknown }) => {
|
|
29
|
-
const { modelContent } = unwrapModelContentEnvelope(output);
|
|
6
|
+
export const defaultToModelOutput = ({ output }: { output: unknown }) => {
|
|
7
|
+
const { result, modelContent } = unwrapModelContentEnvelope(output);
|
|
30
8
|
if (modelContent !== undefined) {
|
|
31
9
|
return toAISDKContent(modelContent);
|
|
32
10
|
}
|
|
33
|
-
return
|
|
34
|
-
? { type: "text" as const, value: output }
|
|
35
|
-
: { type: "json" as const, value: (output ?? null) as any };
|
|
11
|
+
return toAISDKDefaultOutput(result);
|
|
36
12
|
};
|
|
37
13
|
|
|
38
|
-
export const frontendTools = (
|
|
39
|
-
tools: Record<string, { description?: string; parameters: JSONSchema7 }>,
|
|
40
|
-
): ToolSet =>
|
|
14
|
+
export const frontendTools = (tools: Record<string, ToolJSONSchema>): ToolSet =>
|
|
41
15
|
Object.fromEntries(
|
|
42
16
|
Object.entries(tools).map(([name, t]) => [
|
|
43
17
|
name,
|
|
@@ -45,6 +19,7 @@ export const frontendTools = (
|
|
|
45
19
|
...(t.description !== undefined && { description: t.description }),
|
|
46
20
|
inputSchema: jsonSchema(t.parameters),
|
|
47
21
|
toModelOutput: defaultToModelOutput,
|
|
22
|
+
...(t.providerOptions && { providerOptions: t.providerOptions }),
|
|
48
23
|
},
|
|
49
24
|
]),
|
|
50
25
|
) as ToolSet;
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { AISDKToolkit, generativeTools } from "./generativeTools";
|
|
3
|
+
import { wrapModelContentEnvelope } from "./modelContentEnvelope";
|
|
4
|
+
|
|
5
|
+
const mocks = vi.hoisted(() => ({
|
|
6
|
+
close: vi.fn(),
|
|
7
|
+
tools: vi.fn(),
|
|
8
|
+
createMCPClient: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock("@ai-sdk/mcp", () => ({
|
|
12
|
+
createMCPClient: mocks.createMCPClient,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock("@ai-sdk/mcp/mcp-stdio", () => ({
|
|
16
|
+
Experimental_StdioMCPTransport: vi.fn((config) => ({
|
|
17
|
+
type: "stdio",
|
|
18
|
+
config,
|
|
19
|
+
})),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
describe("generativeTools", () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
mocks.close.mockReset();
|
|
25
|
+
mocks.tools.mockReset();
|
|
26
|
+
mocks.createMCPClient.mockReset();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("merges frontend tools with toolkit tools", () => {
|
|
30
|
+
const toolSet = generativeTools({
|
|
31
|
+
frontendTools: {
|
|
32
|
+
clientTool: {
|
|
33
|
+
parameters: { type: "object", properties: {} },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
toolkit: {
|
|
37
|
+
serverTool: {
|
|
38
|
+
type: "backend",
|
|
39
|
+
description: "Server tool",
|
|
40
|
+
parameters: { type: "object", properties: {} },
|
|
41
|
+
execute: async () => "ok",
|
|
42
|
+
} as never,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(toolSet.clientTool).toBeDefined();
|
|
47
|
+
expect(toolSet.serverTool?.description).toBe("Server tool");
|
|
48
|
+
expect(toolSet.serverTool?.execute).toBeTypeOf("function");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("keeps a flat toolkit tool named tools", () => {
|
|
52
|
+
const toolSet = generativeTools({
|
|
53
|
+
toolkit: {
|
|
54
|
+
tools: {
|
|
55
|
+
type: "backend",
|
|
56
|
+
description: "Actually a tool, not config",
|
|
57
|
+
parameters: { type: "object", properties: {} },
|
|
58
|
+
execute: async () => "ok",
|
|
59
|
+
} as never,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(toolSet.tools?.description).toBe("Actually a tool, not config");
|
|
64
|
+
expect(toolSet.tools?.execute).toBeTypeOf("function");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("rejects MCP entries because they require pooled clients", () => {
|
|
68
|
+
expect(() =>
|
|
69
|
+
generativeTools({
|
|
70
|
+
toolkit: {
|
|
71
|
+
docs: {
|
|
72
|
+
type: "mcp",
|
|
73
|
+
server: { type: "http", url: "http://localhost:3001/mcp" },
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
).toThrow(/requires AISDKToolkit/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("converts provider tools without an execute function", () => {
|
|
81
|
+
const toolSet = generativeTools({
|
|
82
|
+
toolkit: {
|
|
83
|
+
web_search: {
|
|
84
|
+
type: "provider",
|
|
85
|
+
providerId: "openai.web_search_preview",
|
|
86
|
+
args: { searchContextSize: "low" },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(toolSet.web_search).toMatchObject({
|
|
92
|
+
type: "provider",
|
|
93
|
+
id: "openai.web_search_preview",
|
|
94
|
+
args: { searchContextSize: "low" },
|
|
95
|
+
});
|
|
96
|
+
expect(toolSet.web_search).not.toHaveProperty("inputSchema");
|
|
97
|
+
expect(toolSet.web_search).not.toHaveProperty("execute");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("forwards provider tool parameters and providerOptions when present", () => {
|
|
101
|
+
const toolSet = generativeTools({
|
|
102
|
+
toolkit: {
|
|
103
|
+
web_search: {
|
|
104
|
+
type: "provider",
|
|
105
|
+
providerId: "openai.web_search_preview",
|
|
106
|
+
args: { searchContextSize: "low" },
|
|
107
|
+
parameters: {
|
|
108
|
+
type: "object",
|
|
109
|
+
properties: {
|
|
110
|
+
query: { type: "string" },
|
|
111
|
+
},
|
|
112
|
+
required: ["query"],
|
|
113
|
+
},
|
|
114
|
+
providerOptions: {
|
|
115
|
+
openai: { rankingOptions: { scoreThreshold: 0.5 } },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(toolSet.web_search).toMatchObject({
|
|
122
|
+
type: "provider",
|
|
123
|
+
id: "openai.web_search_preview",
|
|
124
|
+
args: { searchContextSize: "low" },
|
|
125
|
+
providerOptions: {
|
|
126
|
+
openai: { rankingOptions: { scoreThreshold: 0.5 } },
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
expect(toolSet.web_search).toHaveProperty("inputSchema");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("forwards explicit false supportsDeferredResults", () => {
|
|
133
|
+
const toolSet = generativeTools({
|
|
134
|
+
toolkit: {
|
|
135
|
+
web_search: {
|
|
136
|
+
type: "provider",
|
|
137
|
+
providerId: "openai.web_search_preview",
|
|
138
|
+
args: { searchContextSize: "low" },
|
|
139
|
+
supportsDeferredResults: false,
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(toolSet.web_search).toMatchObject({
|
|
145
|
+
supportsDeferredResults: false,
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("AISDKToolkit", () => {
|
|
151
|
+
beforeEach(() => {
|
|
152
|
+
mocks.close.mockReset();
|
|
153
|
+
mocks.tools.mockReset();
|
|
154
|
+
mocks.createMCPClient.mockReset();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("loads MCP tools through pooled clients", async () => {
|
|
158
|
+
mocks.tools.mockResolvedValue({ echo: { inputSchema: {} } });
|
|
159
|
+
mocks.createMCPClient.mockResolvedValue({
|
|
160
|
+
tools: mocks.tools,
|
|
161
|
+
close: mocks.close,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const toolkit = new AISDKToolkit({
|
|
165
|
+
toolkit: {
|
|
166
|
+
local: {
|
|
167
|
+
type: "mcp",
|
|
168
|
+
server: { type: "http", url: "http://localhost:3001/mcp" },
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await expect(toolkit.tools()).resolves.toHaveProperty("echo");
|
|
174
|
+
await toolkit.tools();
|
|
175
|
+
|
|
176
|
+
expect(mocks.createMCPClient).toHaveBeenCalledTimes(1);
|
|
177
|
+
expect(mocks.createMCPClient).toHaveBeenCalledWith({
|
|
178
|
+
transport: {
|
|
179
|
+
type: "http",
|
|
180
|
+
url: "http://localhost:3001/mcp",
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
expect(mocks.tools).toHaveBeenCalledTimes(2);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("closes pooled MCP clients", async () => {
|
|
187
|
+
mocks.tools.mockResolvedValue({});
|
|
188
|
+
mocks.createMCPClient.mockResolvedValue({
|
|
189
|
+
tools: mocks.tools,
|
|
190
|
+
close: mocks.close,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const toolkit = new AISDKToolkit({
|
|
194
|
+
toolkit: {
|
|
195
|
+
local: {
|
|
196
|
+
type: "mcp",
|
|
197
|
+
server: { type: "sse", url: "http://localhost:3001/sse" },
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await toolkit.tools();
|
|
203
|
+
await toolkit.close();
|
|
204
|
+
|
|
205
|
+
expect(mocks.close).toHaveBeenCalledTimes(1);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("clears pooled MCP clients even when initialization fails", async () => {
|
|
209
|
+
const error = new Error("connect failed");
|
|
210
|
+
const closeError = new Error("close failed");
|
|
211
|
+
const close = vi.fn().mockRejectedValue(closeError);
|
|
212
|
+
mocks.tools.mockResolvedValue({});
|
|
213
|
+
mocks.createMCPClient
|
|
214
|
+
.mockResolvedValueOnce({
|
|
215
|
+
tools: mocks.tools,
|
|
216
|
+
close,
|
|
217
|
+
})
|
|
218
|
+
.mockRejectedValueOnce(error);
|
|
219
|
+
|
|
220
|
+
const toolkit = new AISDKToolkit({
|
|
221
|
+
toolkit: {
|
|
222
|
+
first: {
|
|
223
|
+
type: "mcp",
|
|
224
|
+
server: { type: "http", url: "http://localhost:3001/mcp" },
|
|
225
|
+
},
|
|
226
|
+
second: {
|
|
227
|
+
type: "mcp",
|
|
228
|
+
server: { type: "http", url: "http://localhost:3002/mcp" },
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const toolsPromise = toolkit.tools();
|
|
234
|
+
await expect(toolkit.close()).rejects.toMatchObject({
|
|
235
|
+
errors: [error, closeError],
|
|
236
|
+
});
|
|
237
|
+
await expect(toolsPromise).rejects.toThrow(error);
|
|
238
|
+
expect(close).toHaveBeenCalledTimes(1);
|
|
239
|
+
|
|
240
|
+
await expect(toolkit.close()).resolves.toBeUndefined();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("evicts failed MCP client initialization so later calls can retry", async () => {
|
|
244
|
+
const error = new Error("connect failed");
|
|
245
|
+
mocks.createMCPClient.mockRejectedValueOnce(error).mockResolvedValueOnce({
|
|
246
|
+
tools: vi.fn().mockResolvedValue({ echo: { inputSchema: {} } }),
|
|
247
|
+
close: mocks.close,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const toolkit = new AISDKToolkit({
|
|
251
|
+
toolkit: {
|
|
252
|
+
local: {
|
|
253
|
+
type: "mcp",
|
|
254
|
+
server: { type: "http", url: "http://localhost:3001/mcp" },
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
await expect(toolkit.tools()).rejects.toThrow(error);
|
|
260
|
+
await expect(toolkit.tools()).resolves.toHaveProperty("echo");
|
|
261
|
+
expect(mocks.createMCPClient).toHaveBeenCalledTimes(2);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("rejects duplicate MCP tool names", async () => {
|
|
265
|
+
mocks.createMCPClient
|
|
266
|
+
.mockResolvedValueOnce({
|
|
267
|
+
tools: vi.fn().mockResolvedValue({ echo: { inputSchema: {} } }),
|
|
268
|
+
close: mocks.close,
|
|
269
|
+
})
|
|
270
|
+
.mockResolvedValueOnce({
|
|
271
|
+
tools: vi.fn().mockResolvedValue({ echo: { inputSchema: {} } }),
|
|
272
|
+
close: mocks.close,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const toolkit = new AISDKToolkit({
|
|
276
|
+
toolkit: {
|
|
277
|
+
first: {
|
|
278
|
+
type: "mcp",
|
|
279
|
+
server: { type: "http", url: "http://localhost:3001/mcp" },
|
|
280
|
+
},
|
|
281
|
+
second: {
|
|
282
|
+
type: "mcp",
|
|
283
|
+
server: { type: "http", url: "http://localhost:3002/mcp" },
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
await expect(toolkit.tools()).rejects.toThrow(
|
|
289
|
+
/MCP tool name collision: "echo"/,
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("includes provider tools alongside MCP tools", async () => {
|
|
294
|
+
mocks.tools.mockResolvedValue({ echo: { inputSchema: {} } });
|
|
295
|
+
mocks.createMCPClient.mockResolvedValue({
|
|
296
|
+
tools: mocks.tools,
|
|
297
|
+
close: mocks.close,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const toolkit = new AISDKToolkit({
|
|
301
|
+
toolkit: {
|
|
302
|
+
local: {
|
|
303
|
+
type: "mcp",
|
|
304
|
+
server: { type: "http", url: "http://localhost:3001/mcp" },
|
|
305
|
+
},
|
|
306
|
+
web_search: {
|
|
307
|
+
type: "provider",
|
|
308
|
+
providerId: "openai.web_search_preview",
|
|
309
|
+
args: { searchContextSize: "low" },
|
|
310
|
+
supportsDeferredResults: false,
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
await expect(toolkit.tools()).resolves.toMatchObject({
|
|
316
|
+
echo: { inputSchema: {} },
|
|
317
|
+
web_search: {
|
|
318
|
+
type: "provider",
|
|
319
|
+
id: "openai.web_search_preview",
|
|
320
|
+
args: { searchContextSize: "low" },
|
|
321
|
+
supportsDeferredResults: false,
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
describe("generativeTools toModelOutput", () => {
|
|
328
|
+
const createWeatherTools = (toModelOutput?: any) =>
|
|
329
|
+
generativeTools({
|
|
330
|
+
toolkit: {
|
|
331
|
+
get_weather: {
|
|
332
|
+
...(toModelOutput && { toModelOutput }),
|
|
333
|
+
},
|
|
334
|
+
} as any,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("adapts assistant-ui model content parts to the AI SDK tool output shape", async () => {
|
|
338
|
+
const tools = createWeatherTools(({ output }: any) => [
|
|
339
|
+
{ type: "text", text: `Weather card displayed: ${output.location}` },
|
|
340
|
+
]);
|
|
341
|
+
|
|
342
|
+
const output = await tools.get_weather!.toModelOutput!({
|
|
343
|
+
toolCallId: "tc-weather",
|
|
344
|
+
input: {},
|
|
345
|
+
output: { location: "San Francisco" },
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
expect(output).toEqual({
|
|
349
|
+
type: "content",
|
|
350
|
+
value: [{ type: "text", text: "Weather card displayed: San Francisco" }],
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("uses stored model content envelopes without re-running the custom projector", async () => {
|
|
355
|
+
let called = false;
|
|
356
|
+
const tools = createWeatherTools(() => {
|
|
357
|
+
called = true;
|
|
358
|
+
return [{ type: "text", text: "recomputed" }];
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const output = await tools.get_weather!.toModelOutput!({
|
|
362
|
+
toolCallId: "tc-weather",
|
|
363
|
+
input: {},
|
|
364
|
+
output: wrapModelContentEnvelope({ location: "San Francisco" }, [
|
|
365
|
+
{ type: "text", text: "cached weather receipt" },
|
|
366
|
+
]),
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
expect(called).toBe(false);
|
|
370
|
+
expect(output).toEqual({
|
|
371
|
+
type: "content",
|
|
372
|
+
value: [{ type: "text", text: "cached weather receipt" }],
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("falls back to default model output when no custom projector is defined", async () => {
|
|
377
|
+
const tools = createWeatherTools();
|
|
378
|
+
|
|
379
|
+
const output = await tools.get_weather!.toModelOutput!({
|
|
380
|
+
toolCallId: "tc-weather",
|
|
381
|
+
input: {},
|
|
382
|
+
output: { location: "San Francisco" },
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
expect(output).toEqual({
|
|
386
|
+
type: "json",
|
|
387
|
+
value: { location: "San Francisco" },
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("uses stored model content envelopes when no custom projector is defined", async () => {
|
|
392
|
+
const tools = createWeatherTools();
|
|
393
|
+
|
|
394
|
+
const output = await tools.get_weather!.toModelOutput!({
|
|
395
|
+
toolCallId: "tc-weather",
|
|
396
|
+
input: {},
|
|
397
|
+
output: wrapModelContentEnvelope({ location: "San Francisco" }, [
|
|
398
|
+
{ type: "text", text: "cached weather receipt" },
|
|
399
|
+
]),
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
expect(output).toEqual({
|
|
403
|
+
type: "content",
|
|
404
|
+
value: [{ type: "text", text: "cached weather receipt" }],
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
});
|