@assistant-ui/react-ai-sdk 1.3.27 → 1.3.28

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.
@@ -170,6 +170,14 @@ const useAISDKRuntime = (chatHelpers, { adapters, toCreateMessage: customToCreat
170
170
  });
171
171
  },
172
172
  onResumeToolCall: (options) => toolInvocations.resume(options.toolCallId, options.payload),
173
+ onRespondToToolApproval: ({ approvalId, approved, reason }) => {
174
+ chatHelpers.addToolApprovalResponse({
175
+ id: approvalId,
176
+ approved,
177
+ ...reason != null && { reason },
178
+ options: { metadata: lastRunConfigRef.current }
179
+ });
180
+ },
173
181
  ...onResume && { onResume },
174
182
  ...suggestions && { suggestions },
175
183
  adapters: {
@@ -1 +1 @@
1
- {"version":3,"file":"useAISDKRuntime.js","names":[],"sources":["../../../src/ui/use-chat/useAISDKRuntime.ts"],"sourcesContent":["\"use client\";\n\nimport { useState, useMemo, useRef } from \"react\";\nimport type { UIMessage, useChat, CreateUIMessage } from \"@ai-sdk/react\";\nimport { isToolUIPart, generateId } from \"ai\";\nimport {\n useExternalStoreRuntime,\n useRuntimeAdapters,\n useToolInvocations,\n type ToolExecutionStatus,\n} from \"@assistant-ui/core/react\";\nimport type {\n ExternalStoreAdapter,\n ThreadHistoryAdapter,\n AssistantRuntime,\n ThreadMessage,\n ThreadSuggestion,\n MessageFormatAdapter,\n MessageFormatItem,\n MessageFormatRepository,\n AppendMessage,\n RunConfig,\n McpAppMetadata,\n} from \"@assistant-ui/core\";\nimport { getExternalStoreMessages } from \"@assistant-ui/core\";\nimport type { ReadonlyJSONObject } from \"assistant-stream/utils\";\nimport { sliceMessagesUntil } from \"../utils/sliceMessagesUntil\";\nimport { toCreateMessage } from \"../utils/toCreateMessage\";\nimport { vercelAttachmentAdapter } from \"../utils/vercelAttachmentAdapter\";\nimport { getVercelAIMessages } from \"../getVercelAIMessages\";\nimport { AISDKMessageConverter } from \"../utils/convertMessage\";\nimport { wrapModelContentEnvelope } from \"../../modelContentEnvelope\";\nimport {\n type AISDKStorageFormat,\n aiSDKV6FormatAdapter,\n} from \"../adapters/aiSDKFormatAdapter\";\nimport {\n useExternalHistory,\n toExportedMessageRepository,\n} from \"./useExternalHistory\";\nimport { useStreamingTiming } from \"./useStreamingTiming\";\n\nexport type CustomToCreateMessageFunction = <\n UI_MESSAGE extends UIMessage = UIMessage,\n>(\n message: AppendMessage,\n) => CreateUIMessage<UI_MESSAGE>;\n\nconst toUIMessage = <UI_MESSAGE extends UIMessage>(\n createMessage: CreateUIMessage<UI_MESSAGE>,\n fallbackRole: UI_MESSAGE[\"role\"],\n): UI_MESSAGE =>\n ({\n ...createMessage,\n id: createMessage.id ?? generateId(),\n role: createMessage.role ?? fallbackRole,\n }) as UI_MESSAGE;\n\nexport type AISDKRuntimeAdapter = {\n adapters?:\n | (NonNullable<ExternalStoreAdapter[\"adapters\"]> & {\n history?: ThreadHistoryAdapter | undefined;\n })\n | undefined;\n toCreateMessage?: CustomToCreateMessageFunction;\n /**\n * Whether to automatically cancel pending interactive tool calls when the user sends a new message.\n *\n * When enabled (default), the pending tool calls will be marked as failed with an error message\n * indicating the user cancelled the tool call by sending a new message.\n *\n * @default true\n */\n cancelPendingToolCallsOnSend?: boolean | undefined;\n /**\n * Called when `runtime.thread.resumeRun(config)` is invoked.\n *\n * When omitted, `resumeRun` throws `\"Runtime does not support resuming runs.\"`.\n * Provide this to bridge resume invocations into a custom replay channel\n * (for example, an SSE reconnect endpoint keyed by turn id).\n */\n onResume?: ExternalStoreAdapter[\"onResume\"];\n /**\n * Follow up suggestions to surface on the thread. Use this to drive\n * dynamic suggestions from application state, tool results, or backend\n * responses; flows into `thread.suggestions` and is rendered by\n * components that read it (such as the shadcn `ThreadFollowupSuggestions`).\n */\n suggestions?: readonly ThreadSuggestion[] | undefined;\n};\n\nexport const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(\n chatHelpers: ReturnType<typeof useChat<UI_MESSAGE>>,\n {\n adapters,\n toCreateMessage: customToCreateMessage,\n cancelPendingToolCallsOnSend = true,\n onResume,\n suggestions,\n }: AISDKRuntimeAdapter = {},\n) => {\n const contextAdapters = useRuntimeAdapters();\n const [toolStatuses, setToolStatuses] = useState<\n Record<string, ToolExecutionStatus>\n >({});\n const toolArgsKeyOrderCacheRef = useRef<Map<string, Map<string, string[]>>>(\n new Map(),\n );\n const toolLastInputCacheRef = useRef<Map<string, ReadonlyJSONObject>>(\n new Map(),\n );\n const mcpAppMetadataCacheRef = useRef<Map<string, McpAppMetadata>>(new Map());\n const lastRunConfigRef = useRef<RunConfig | undefined>(undefined);\n\n const hasExecutingTools = Object.values(toolStatuses).some(\n (s) => s?.type === \"executing\",\n );\n const isRunning =\n chatHelpers.status === \"submitted\" ||\n chatHelpers.status === \"streaming\" ||\n hasExecutingTools;\n\n const messageTiming = useStreamingTiming(chatHelpers.messages, isRunning);\n\n const messages = AISDKMessageConverter.useThreadMessages({\n isRunning,\n messages: chatHelpers.messages,\n metadata: useMemo(\n () => ({\n toolStatuses,\n messageTiming,\n toolArgsKeyOrderCache: toolArgsKeyOrderCacheRef.current,\n toolLastInputCache: toolLastInputCacheRef.current,\n mcpAppMetadataCache: mcpAppMetadataCacheRef.current,\n ...(chatHelpers.error && { error: chatHelpers.error.message }),\n }),\n [toolStatuses, messageTiming, chatHelpers.error],\n ),\n });\n\n const [runtimeRef] = useState(() => ({\n get current(): AssistantRuntime {\n return runtime;\n },\n }));\n\n const toolInvocations = useToolInvocations({\n state: {\n messages,\n isRunning,\n },\n getTools: () => runtimeRef.current.thread.getModelContext().tools,\n onResult: (command) => {\n if (command.type === \"add-tool-result\") {\n const output =\n command.modelContent !== undefined\n ? wrapModelContentEnvelope(command.result, command.modelContent)\n : command.result;\n chatHelpers.addToolResult({\n tool: command.toolName,\n toolCallId: command.toolCallId,\n output,\n options: { metadata: lastRunConfigRef.current },\n });\n }\n },\n setToolStatuses,\n });\n\n const isLoading = useExternalHistory(\n runtimeRef,\n adapters?.history ?? contextAdapters?.history,\n AISDKMessageConverter.toThreadMessages as (\n messages: UI_MESSAGE[],\n ) => ThreadMessage[],\n aiSDKV6FormatAdapter as MessageFormatAdapter<\n UI_MESSAGE,\n AISDKStorageFormat\n >,\n (messages) => {\n chatHelpers.setMessages(messages);\n },\n );\n\n const completePendingToolCalls = async () => {\n if (!cancelPendingToolCallsOnSend) return;\n\n await toolInvocations.abort();\n\n // Mark any tool without a result as cancelled (uses setMessages to avoid triggering sendAutomaticallyWhen)\n chatHelpers.setMessages((messages) => {\n const lastMessage = messages.at(-1);\n if (lastMessage?.role !== \"assistant\") return messages;\n\n let hasChanges = false;\n const parts = lastMessage.parts?.map((part) => {\n if (!isToolUIPart(part)) return part;\n if (part.state === \"output-available\" || part.state === \"output-error\")\n return part;\n\n hasChanges = true;\n return {\n ...part,\n state: \"output-error\" as const,\n errorText: \"User cancelled tool call by sending a new message.\",\n };\n });\n\n if (!hasChanges) return messages;\n return [...messages.slice(0, -1), { ...lastMessage, parts }];\n });\n };\n\n const runtime = useExternalStoreRuntime({\n isRunning,\n messages,\n setMessages: (messages) =>\n chatHelpers.setMessages(\n messages\n .map(getVercelAIMessages<UI_MESSAGE>)\n .filter(Boolean)\n .flat(),\n ),\n onImport: (messages) =>\n chatHelpers.setMessages(\n messages\n .map(getVercelAIMessages<UI_MESSAGE>)\n .filter(Boolean)\n .flat(),\n ),\n onExportExternalState: (): MessageFormatRepository<UI_MESSAGE> => {\n const exported = runtimeRef.current.thread.export();\n\n const expandedMessages: MessageFormatItem<UI_MESSAGE>[] = [];\n const lastInnerIdMap = new Map<string, string>();\n\n for (const item of exported.messages) {\n const innerMessages = getExternalStoreMessages<UI_MESSAGE>(\n item.message,\n );\n let parentId =\n item.parentId != null\n ? (lastInnerIdMap.get(item.parentId) ?? item.parentId)\n : null;\n for (const innerMessage of innerMessages) {\n expandedMessages.push({ parentId, message: innerMessage });\n parentId = aiSDKV6FormatAdapter.getId(innerMessage as UIMessage);\n }\n if (innerMessages.length > 0) {\n lastInnerIdMap.set(\n item.message.id,\n aiSDKV6FormatAdapter.getId(\n innerMessages[innerMessages.length - 1]! as UIMessage,\n ),\n );\n }\n }\n\n const result: MessageFormatRepository<UI_MESSAGE> = {\n messages: expandedMessages,\n };\n\n if (exported.headId != null) {\n result.headId = lastInnerIdMap.get(exported.headId) ?? exported.headId;\n }\n\n return result;\n },\n onLoadExternalState: (repo: MessageFormatRepository<UI_MESSAGE>) => {\n // Convert MessageFormatRepository to ExportedMessageRepository\n const exportedRepo = toExportedMessageRepository(\n AISDKMessageConverter.toThreadMessages,\n repo,\n );\n\n // Import into the thread's MessageRepository\n runtimeRef.current.thread.import(exportedRepo);\n },\n onCancel: async () => {\n chatHelpers.stop();\n await toolInvocations.abort();\n },\n onNew: async (message) => {\n const createMessage = (\n customToCreateMessage ?? toCreateMessage\n )<UI_MESSAGE>(message);\n\n if (!(message.startRun ?? message.role === \"user\")) {\n chatHelpers.setMessages((current) => [\n ...current,\n toUIMessage<UI_MESSAGE>(createMessage, message.role),\n ]);\n return;\n }\n\n lastRunConfigRef.current = message.runConfig;\n await completePendingToolCalls();\n await chatHelpers.sendMessage(createMessage, {\n metadata: message.runConfig,\n });\n },\n onEdit: async (message) => {\n const createMessage = (\n customToCreateMessage ?? toCreateMessage\n )<UI_MESSAGE>(message);\n\n if (!(message.startRun ?? message.role === \"user\")) {\n chatHelpers.setMessages((current) => [\n ...sliceMessagesUntil(current, message.parentId),\n toUIMessage<UI_MESSAGE>(createMessage, message.role),\n ]);\n return;\n }\n\n lastRunConfigRef.current = message.runConfig;\n chatHelpers.setMessages((current) =>\n sliceMessagesUntil(current, message.parentId),\n );\n await chatHelpers.sendMessage(createMessage, {\n metadata: message.runConfig,\n });\n },\n onReload: async (parentId: string | null, config) => {\n lastRunConfigRef.current = config.runConfig;\n const newMessages = sliceMessagesUntil(chatHelpers.messages, parentId);\n chatHelpers.setMessages(newMessages);\n\n await chatHelpers.regenerate({ metadata: config.runConfig });\n },\n onAddToolResult: ({ toolCallId, result, isError }) => {\n const options = { metadata: lastRunConfigRef.current };\n if (isError) {\n chatHelpers.addToolOutput({\n state: \"output-error\",\n tool: toolCallId,\n toolCallId,\n errorText:\n typeof result === \"string\" ? result : JSON.stringify(result),\n options,\n });\n } else {\n chatHelpers.addToolOutput({\n state: \"output-available\",\n tool: toolCallId,\n toolCallId,\n output: result,\n options,\n });\n }\n },\n onResumeToolCall: (options) =>\n toolInvocations.resume(options.toolCallId, options.payload),\n ...(onResume && { onResume }),\n ...(suggestions && { suggestions }),\n adapters: {\n attachments: vercelAttachmentAdapter,\n ...contextAdapters,\n ...adapters,\n },\n isLoading,\n });\n\n return runtime;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"useAISDKRuntime.js","names":[],"sources":["../../../src/ui/use-chat/useAISDKRuntime.ts"],"sourcesContent":["\"use client\";\n\nimport { useState, useMemo, useRef } from \"react\";\nimport type { UIMessage, useChat, CreateUIMessage } from \"@ai-sdk/react\";\nimport { isToolUIPart, generateId } from \"ai\";\nimport {\n useExternalStoreRuntime,\n useRuntimeAdapters,\n useToolInvocations,\n type ToolExecutionStatus,\n} from \"@assistant-ui/core/react\";\nimport type {\n ExternalStoreAdapter,\n ThreadHistoryAdapter,\n AssistantRuntime,\n ThreadMessage,\n ThreadSuggestion,\n MessageFormatAdapter,\n MessageFormatItem,\n MessageFormatRepository,\n AppendMessage,\n RunConfig,\n McpAppMetadata,\n} from \"@assistant-ui/core\";\nimport { getExternalStoreMessages } from \"@assistant-ui/core\";\nimport type { ReadonlyJSONObject } from \"assistant-stream/utils\";\nimport { sliceMessagesUntil } from \"../utils/sliceMessagesUntil\";\nimport { toCreateMessage } from \"../utils/toCreateMessage\";\nimport { vercelAttachmentAdapter } from \"../utils/vercelAttachmentAdapter\";\nimport { getVercelAIMessages } from \"../getVercelAIMessages\";\nimport { AISDKMessageConverter } from \"../utils/convertMessage\";\nimport { wrapModelContentEnvelope } from \"../../modelContentEnvelope\";\nimport {\n type AISDKStorageFormat,\n aiSDKV6FormatAdapter,\n} from \"../adapters/aiSDKFormatAdapter\";\nimport {\n useExternalHistory,\n toExportedMessageRepository,\n} from \"./useExternalHistory\";\nimport { useStreamingTiming } from \"./useStreamingTiming\";\n\nexport type CustomToCreateMessageFunction = <\n UI_MESSAGE extends UIMessage = UIMessage,\n>(\n message: AppendMessage,\n) => CreateUIMessage<UI_MESSAGE>;\n\nconst toUIMessage = <UI_MESSAGE extends UIMessage>(\n createMessage: CreateUIMessage<UI_MESSAGE>,\n fallbackRole: UI_MESSAGE[\"role\"],\n): UI_MESSAGE =>\n ({\n ...createMessage,\n id: createMessage.id ?? generateId(),\n role: createMessage.role ?? fallbackRole,\n }) as UI_MESSAGE;\n\nexport type AISDKRuntimeAdapter = {\n adapters?:\n | (NonNullable<ExternalStoreAdapter[\"adapters\"]> & {\n history?: ThreadHistoryAdapter | undefined;\n })\n | undefined;\n toCreateMessage?: CustomToCreateMessageFunction;\n /**\n * Whether to automatically cancel pending interactive tool calls when the user sends a new message.\n *\n * When enabled (default), the pending tool calls will be marked as failed with an error message\n * indicating the user cancelled the tool call by sending a new message.\n *\n * @default true\n */\n cancelPendingToolCallsOnSend?: boolean | undefined;\n /**\n * Called when `runtime.thread.resumeRun(config)` is invoked.\n *\n * When omitted, `resumeRun` throws `\"Runtime does not support resuming runs.\"`.\n * Provide this to bridge resume invocations into a custom replay channel\n * (for example, an SSE reconnect endpoint keyed by turn id).\n */\n onResume?: ExternalStoreAdapter[\"onResume\"];\n /**\n * Follow up suggestions to surface on the thread. Use this to drive\n * dynamic suggestions from application state, tool results, or backend\n * responses; flows into `thread.suggestions` and is rendered by\n * components that read it (such as the shadcn `ThreadFollowupSuggestions`).\n */\n suggestions?: readonly ThreadSuggestion[] | undefined;\n};\n\nexport const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(\n chatHelpers: ReturnType<typeof useChat<UI_MESSAGE>>,\n {\n adapters,\n toCreateMessage: customToCreateMessage,\n cancelPendingToolCallsOnSend = true,\n onResume,\n suggestions,\n }: AISDKRuntimeAdapter = {},\n) => {\n const contextAdapters = useRuntimeAdapters();\n const [toolStatuses, setToolStatuses] = useState<\n Record<string, ToolExecutionStatus>\n >({});\n const toolArgsKeyOrderCacheRef = useRef<Map<string, Map<string, string[]>>>(\n new Map(),\n );\n const toolLastInputCacheRef = useRef<Map<string, ReadonlyJSONObject>>(\n new Map(),\n );\n const mcpAppMetadataCacheRef = useRef<Map<string, McpAppMetadata>>(new Map());\n const lastRunConfigRef = useRef<RunConfig | undefined>(undefined);\n\n const hasExecutingTools = Object.values(toolStatuses).some(\n (s) => s?.type === \"executing\",\n );\n const isRunning =\n chatHelpers.status === \"submitted\" ||\n chatHelpers.status === \"streaming\" ||\n hasExecutingTools;\n\n const messageTiming = useStreamingTiming(chatHelpers.messages, isRunning);\n\n const messages = AISDKMessageConverter.useThreadMessages({\n isRunning,\n messages: chatHelpers.messages,\n metadata: useMemo(\n () => ({\n toolStatuses,\n messageTiming,\n toolArgsKeyOrderCache: toolArgsKeyOrderCacheRef.current,\n toolLastInputCache: toolLastInputCacheRef.current,\n mcpAppMetadataCache: mcpAppMetadataCacheRef.current,\n ...(chatHelpers.error && { error: chatHelpers.error.message }),\n }),\n [toolStatuses, messageTiming, chatHelpers.error],\n ),\n });\n\n const [runtimeRef] = useState(() => ({\n get current(): AssistantRuntime {\n return runtime;\n },\n }));\n\n const toolInvocations = useToolInvocations({\n state: {\n messages,\n isRunning,\n },\n getTools: () => runtimeRef.current.thread.getModelContext().tools,\n onResult: (command) => {\n if (command.type === \"add-tool-result\") {\n const output =\n command.modelContent !== undefined\n ? wrapModelContentEnvelope(command.result, command.modelContent)\n : command.result;\n chatHelpers.addToolResult({\n tool: command.toolName,\n toolCallId: command.toolCallId,\n output,\n options: { metadata: lastRunConfigRef.current },\n });\n }\n },\n setToolStatuses,\n });\n\n const isLoading = useExternalHistory(\n runtimeRef,\n adapters?.history ?? contextAdapters?.history,\n AISDKMessageConverter.toThreadMessages as (\n messages: UI_MESSAGE[],\n ) => ThreadMessage[],\n aiSDKV6FormatAdapter as MessageFormatAdapter<\n UI_MESSAGE,\n AISDKStorageFormat\n >,\n (messages) => {\n chatHelpers.setMessages(messages);\n },\n );\n\n const completePendingToolCalls = async () => {\n if (!cancelPendingToolCallsOnSend) return;\n\n await toolInvocations.abort();\n\n // Mark any tool without a result as cancelled (uses setMessages to avoid triggering sendAutomaticallyWhen)\n chatHelpers.setMessages((messages) => {\n const lastMessage = messages.at(-1);\n if (lastMessage?.role !== \"assistant\") return messages;\n\n let hasChanges = false;\n const parts = lastMessage.parts?.map((part) => {\n if (!isToolUIPart(part)) return part;\n if (part.state === \"output-available\" || part.state === \"output-error\")\n return part;\n\n hasChanges = true;\n return {\n ...part,\n state: \"output-error\" as const,\n errorText: \"User cancelled tool call by sending a new message.\",\n };\n });\n\n if (!hasChanges) return messages;\n return [...messages.slice(0, -1), { ...lastMessage, parts }];\n });\n };\n\n const runtime = useExternalStoreRuntime({\n isRunning,\n messages,\n setMessages: (messages) =>\n chatHelpers.setMessages(\n messages\n .map(getVercelAIMessages<UI_MESSAGE>)\n .filter(Boolean)\n .flat(),\n ),\n onImport: (messages) =>\n chatHelpers.setMessages(\n messages\n .map(getVercelAIMessages<UI_MESSAGE>)\n .filter(Boolean)\n .flat(),\n ),\n onExportExternalState: (): MessageFormatRepository<UI_MESSAGE> => {\n const exported = runtimeRef.current.thread.export();\n\n const expandedMessages: MessageFormatItem<UI_MESSAGE>[] = [];\n const lastInnerIdMap = new Map<string, string>();\n\n for (const item of exported.messages) {\n const innerMessages = getExternalStoreMessages<UI_MESSAGE>(\n item.message,\n );\n let parentId =\n item.parentId != null\n ? (lastInnerIdMap.get(item.parentId) ?? item.parentId)\n : null;\n for (const innerMessage of innerMessages) {\n expandedMessages.push({ parentId, message: innerMessage });\n parentId = aiSDKV6FormatAdapter.getId(innerMessage as UIMessage);\n }\n if (innerMessages.length > 0) {\n lastInnerIdMap.set(\n item.message.id,\n aiSDKV6FormatAdapter.getId(\n innerMessages[innerMessages.length - 1]! as UIMessage,\n ),\n );\n }\n }\n\n const result: MessageFormatRepository<UI_MESSAGE> = {\n messages: expandedMessages,\n };\n\n if (exported.headId != null) {\n result.headId = lastInnerIdMap.get(exported.headId) ?? exported.headId;\n }\n\n return result;\n },\n onLoadExternalState: (repo: MessageFormatRepository<UI_MESSAGE>) => {\n // Convert MessageFormatRepository to ExportedMessageRepository\n const exportedRepo = toExportedMessageRepository(\n AISDKMessageConverter.toThreadMessages,\n repo,\n );\n\n // Import into the thread's MessageRepository\n runtimeRef.current.thread.import(exportedRepo);\n },\n onCancel: async () => {\n chatHelpers.stop();\n await toolInvocations.abort();\n },\n onNew: async (message) => {\n const createMessage = (\n customToCreateMessage ?? toCreateMessage\n )<UI_MESSAGE>(message);\n\n if (!(message.startRun ?? message.role === \"user\")) {\n chatHelpers.setMessages((current) => [\n ...current,\n toUIMessage<UI_MESSAGE>(createMessage, message.role),\n ]);\n return;\n }\n\n lastRunConfigRef.current = message.runConfig;\n await completePendingToolCalls();\n await chatHelpers.sendMessage(createMessage, {\n metadata: message.runConfig,\n });\n },\n onEdit: async (message) => {\n const createMessage = (\n customToCreateMessage ?? toCreateMessage\n )<UI_MESSAGE>(message);\n\n if (!(message.startRun ?? message.role === \"user\")) {\n chatHelpers.setMessages((current) => [\n ...sliceMessagesUntil(current, message.parentId),\n toUIMessage<UI_MESSAGE>(createMessage, message.role),\n ]);\n return;\n }\n\n lastRunConfigRef.current = message.runConfig;\n chatHelpers.setMessages((current) =>\n sliceMessagesUntil(current, message.parentId),\n );\n await chatHelpers.sendMessage(createMessage, {\n metadata: message.runConfig,\n });\n },\n onReload: async (parentId: string | null, config) => {\n lastRunConfigRef.current = config.runConfig;\n const newMessages = sliceMessagesUntil(chatHelpers.messages, parentId);\n chatHelpers.setMessages(newMessages);\n\n await chatHelpers.regenerate({ metadata: config.runConfig });\n },\n onAddToolResult: ({ toolCallId, result, isError }) => {\n const options = { metadata: lastRunConfigRef.current };\n if (isError) {\n chatHelpers.addToolOutput({\n state: \"output-error\",\n tool: toolCallId,\n toolCallId,\n errorText:\n typeof result === \"string\" ? result : JSON.stringify(result),\n options,\n });\n } else {\n chatHelpers.addToolOutput({\n state: \"output-available\",\n tool: toolCallId,\n toolCallId,\n output: result,\n options,\n });\n }\n },\n onResumeToolCall: (options) =>\n toolInvocations.resume(options.toolCallId, options.payload),\n onRespondToToolApproval: ({ approvalId, approved, reason }) => {\n void chatHelpers.addToolApprovalResponse({\n id: approvalId,\n approved,\n ...(reason != null && { reason }),\n options: { metadata: lastRunConfigRef.current },\n });\n },\n ...(onResume && { onResume }),\n ...(suggestions && { suggestions }),\n adapters: {\n attachments: vercelAttachmentAdapter,\n ...contextAdapters,\n ...adapters,\n },\n isLoading,\n });\n\n return runtime;\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"convertMessage.d.ts","names":[],"sources":["../../../src/ui/utils/convertMessage.ts"],"mappings":";;;;;;KAqBY,6BAAA,GACV,2BAAA,CAA4B,QAAA;EAC1B,qBAAA,GAAwB,GAAA,SAAY,GAAA;EACpC,kBAAA,GAAqB,GAAA,SAAY,kBAAA;EACjC,mBAAA,GAAsB,GAAA,SAAY,cAAA;AAAA;AAAA,cAuUzB,qBAAA;;;;;;;;;;;;;mJAzUyC,aAAA;kJAEf,aAAA"}
1
+ {"version":3,"file":"convertMessage.d.ts","names":[],"sources":["../../../src/ui/utils/convertMessage.ts"],"mappings":";;;;;;KAqBY,6BAAA,GACV,2BAAA,CAA4B,QAAA;EAC1B,qBAAA,GAAwB,GAAA,SAAY,GAAA;EACpC,kBAAA,GAAqB,GAAA,SAAY,kBAAA;EACjC,mBAAA,GAAsB,GAAA,SAAY,cAAA;AAAA;AAAA,cAyUzB,qBAAA;;;;;;;;;;;;;mJA3UyC,aAAA;kJAEf,aAAA"}
@@ -61,31 +61,17 @@ function stableStringifyToolArgs(keyOrderCache, cacheKey, args) {
61
61
  const stableArgs = stabilizeToolArgsValue(args, "$", keyOrderByPath);
62
62
  return JSON.stringify(stableArgs);
63
63
  }
64
- /**
65
- * Resolves the interrupt fields for a tool call part.
66
- *
67
- * Two interrupt paths for tool approvals:
68
- * 1. AI SDK server-side approval: approval-requested state with part.approval payload
69
- * 2. Frontend tools: toolStatuses interrupt from context.human()
70
- */
71
- function getToolInterrupt(part, toolStatus) {
72
- if (part.state === "approval-requested" && "approval" in part) return {
73
- interrupt: {
74
- type: "human",
75
- payload: part.approval
76
- },
77
- status: {
78
- type: "requires-action",
79
- reason: "interrupt"
80
- }
81
- };
82
- if (toolStatus?.type === "interrupt") return {
83
- interrupt: toolStatus.payload,
84
- status: {
85
- type: "requires-action",
86
- reason: "interrupt"
87
- }
88
- };
64
+ function getToolApprovalAndInterrupt(part, toolStatus) {
65
+ if (part.approval && typeof part.approval.id === "string") {
66
+ const { id, approved, reason, isAutomatic } = part.approval;
67
+ return { approval: {
68
+ id,
69
+ ...typeof approved === "boolean" && { approved },
70
+ ...typeof reason === "string" && { reason },
71
+ ...isAutomatic === true && { isAutomatic: true }
72
+ } };
73
+ }
74
+ if (toolStatus?.type === "interrupt") return { interrupt: toolStatus.payload };
89
75
  return {};
90
76
  }
91
77
  function convertParts(message, metadata) {
@@ -121,7 +107,7 @@ function convertParts(message, metadata) {
121
107
  result = { error: part.errorText };
122
108
  } else if (part.state === "output-denied") {
123
109
  isError = true;
124
- result = { error: part.approval.reason || "Tool approval denied" };
110
+ result = { error: part.approval?.reason || "Tool approval denied" };
125
111
  }
126
112
  let argsText = stableStringifyToolArgs(metadata.toolArgsKeyOrderCache, argsKeyOrderCacheKey, args);
127
113
  if (part.state === "input-streaming") argsText = stripClosingDelimiters(argsText);
@@ -141,7 +127,7 @@ function convertParts(message, metadata) {
141
127
  isError,
142
128
  ...modelContent !== void 0 && { modelContent },
143
129
  ...mcpApp && { mcp: { app: mcpApp } },
144
- ...getToolInterrupt(part, toolStatus)
130
+ ...getToolApprovalAndInterrupt(part, toolStatus)
145
131
  };
146
132
  }
147
133
  if (part.type === "source-url") return {
@@ -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\n/**\n * Resolves the interrupt fields for a tool call part.\n *\n * Two interrupt paths for tool approvals:\n * 1. AI SDK server-side approval: approval-requested state with part.approval payload\n * 2. Frontend tools: toolStatuses interrupt from context.human()\n */\nfunction getToolInterrupt(\n part: { state: string; approval?: unknown },\n toolStatus: { type: string; payload?: unknown } | undefined,\n): Record<string, unknown> {\n if (part.state === \"approval-requested\" && \"approval\" in part) {\n return {\n interrupt: {\n type: \"human\" as const,\n payload: (part as { approval: unknown }).approval,\n },\n status: {\n type: \"requires-action\" as const,\n reason: \"interrupt\" as const,\n },\n };\n }\n\n if (toolStatus?.type === \"interrupt\") {\n return {\n interrupt: toolStatus.payload,\n status: {\n type: \"requires-action\" as const,\n reason: \"interrupt\" as const,\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 ...getToolInterrupt(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 };\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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/react-ai-sdk",
3
- "version": "1.3.27",
3
+ "version": "1.3.28",
4
4
  "description": "Vercel AI SDK adapter for assistant-ui",
5
5
  "keywords": [
6
6
  "ai-sdk",
@@ -31,7 +31,7 @@
31
31
  "sideEffects": false,
32
32
  "dependencies": {
33
33
  "@ai-sdk/react": "^3.0.190",
34
- "@assistant-ui/core": "^0.2.5",
34
+ "@assistant-ui/core": "^0.2.6",
35
35
  "@assistant-ui/store": "^0.2.12",
36
36
  "ai": "^6.0.188",
37
37
  "assistant-cloud": "*"
@@ -54,7 +54,7 @@
54
54
  "react": "^19.2.6",
55
55
  "vitest": "^4.1.7",
56
56
  "@assistant-ui/x-buildutils": "0.0.9",
57
- "assistant-stream": "0.3.16"
57
+ "assistant-stream": "0.3.17"
58
58
  },
59
59
  "publishConfig": {
60
60
  "access": "public",
@@ -350,6 +350,14 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
350
350
  },
351
351
  onResumeToolCall: (options) =>
352
352
  toolInvocations.resume(options.toolCallId, options.payload),
353
+ onRespondToToolApproval: ({ approvalId, approved, reason }) => {
354
+ void chatHelpers.addToolApprovalResponse({
355
+ id: approvalId,
356
+ approved,
357
+ ...(reason != null && { reason }),
358
+ options: { metadata: lastRunConfigRef.current },
359
+ });
360
+ },
353
361
  ...(onResume && { onResume }),
354
362
  ...(suggestions && { suggestions }),
355
363
  adapters: {
@@ -186,7 +186,7 @@ describe("AISDKMessageConverter", () => {
186
186
  });
187
187
  });
188
188
 
189
- it("deduplicates tool calls by toolCallId and maps interrupt states", () => {
189
+ it("deduplicates tool calls by toolCallId and surfaces approval / interrupt state", () => {
190
190
  const converted = AISDKMessageConverter.toThreadMessages(
191
191
  [
192
192
  {
@@ -212,7 +212,7 @@ describe("AISDKMessageConverter", () => {
212
212
  toolCallId: "tc-2",
213
213
  state: "approval-requested",
214
214
  input: { action: "deploy" },
215
- approval: { reason: "need human review" },
215
+ approval: { id: "appr-1" },
216
216
  },
217
217
  {
218
218
  type: "tool-human",
@@ -220,6 +220,39 @@ describe("AISDKMessageConverter", () => {
220
220
  state: "input-available",
221
221
  input: { task: "confirm" },
222
222
  },
223
+ {
224
+ type: "tool-approve",
225
+ toolCallId: "tc-4",
226
+ state: "approval-responded",
227
+ input: { action: "rollback" },
228
+ approval: { id: "appr-2", approved: true, reason: "looks ok" },
229
+ },
230
+ {
231
+ type: "tool-approve",
232
+ toolCallId: "tc-5",
233
+ state: "approval-requested",
234
+ input: { action: "auto" },
235
+ approval: { id: "appr-3", isAutomatic: true },
236
+ },
237
+ {
238
+ type: "tool-approve",
239
+ toolCallId: "tc-6",
240
+ state: "output-denied",
241
+ input: { action: "wipe" },
242
+ approval: {
243
+ id: "appr-4",
244
+ approved: false,
245
+ reason: "user denied",
246
+ },
247
+ },
248
+ {
249
+ type: "tool-approve",
250
+ toolCallId: "tc-7",
251
+ state: "output-available",
252
+ input: { action: "ship" },
253
+ output: { ok: true },
254
+ approval: { id: "appr-5", approved: true, isAutomatic: true },
255
+ },
223
256
  ],
224
257
  } as any,
225
258
  ],
@@ -237,17 +270,49 @@ describe("AISDKMessageConverter", () => {
237
270
  const toolCalls = converted[0]?.content.filter(
238
271
  (part): part is any => part.type === "tool-call",
239
272
  );
240
- expect(toolCalls).toHaveLength(3);
273
+ expect(toolCalls).toHaveLength(7);
241
274
 
242
275
  expect(toolCalls?.filter((p) => p.toolCallId === "tc-1")).toHaveLength(1);
243
- expect(toolCalls?.find((p) => p.toolCallId === "tc-2")?.status).toEqual({
244
- type: "requires-action",
245
- reason: "interrupt",
276
+
277
+ expect(toolCalls?.find((p) => p.toolCallId === "tc-2")?.approval).toEqual({
278
+ id: "appr-1",
246
279
  });
280
+ expect(
281
+ toolCalls?.find((p) => p.toolCallId === "tc-2")?.interrupt,
282
+ ).toBeUndefined();
283
+
247
284
  expect(toolCalls?.find((p) => p.toolCallId === "tc-3")?.interrupt).toEqual({
248
285
  type: "human",
249
286
  payload: { kind: "human" },
250
287
  });
288
+ expect(
289
+ toolCalls?.find((p) => p.toolCallId === "tc-3")?.approval,
290
+ ).toBeUndefined();
291
+
292
+ expect(toolCalls?.find((p) => p.toolCallId === "tc-4")?.approval).toEqual({
293
+ id: "appr-2",
294
+ approved: true,
295
+ reason: "looks ok",
296
+ });
297
+
298
+ expect(toolCalls?.find((p) => p.toolCallId === "tc-5")?.approval).toEqual({
299
+ id: "appr-3",
300
+ isAutomatic: true,
301
+ });
302
+
303
+ const denied = toolCalls?.find((p) => p.toolCallId === "tc-6");
304
+ expect(denied?.approval).toEqual({
305
+ id: "appr-4",
306
+ approved: false,
307
+ reason: "user denied",
308
+ });
309
+ expect(denied?.isError).toBe(true);
310
+
311
+ expect(toolCalls?.find((p) => p.toolCallId === "tc-7")?.approval).toEqual({
312
+ id: "appr-5",
313
+ approved: true,
314
+ isAutomatic: true,
315
+ });
251
316
  });
252
317
 
253
318
  it("strips closing delimiters from streaming tool argsText", () => {
@@ -141,37 +141,39 @@ function stableStringifyToolArgs(
141
141
  return JSON.stringify(stableArgs);
142
142
  }
143
143
 
144
- /**
145
- * Resolves the interrupt fields for a tool call part.
146
- *
147
- * Two interrupt paths for tool approvals:
148
- * 1. AI SDK server-side approval: approval-requested state with part.approval payload
149
- * 2. Frontend tools: toolStatuses interrupt from context.human()
150
- */
151
- function getToolInterrupt(
152
- part: { state: string; approval?: unknown },
144
+ function getToolApprovalAndInterrupt(
145
+ part: {
146
+ approval?:
147
+ | {
148
+ id: string;
149
+ approved?: boolean;
150
+ reason?: string;
151
+ isAutomatic?: boolean;
152
+ }
153
+ | undefined;
154
+ },
153
155
  toolStatus: { type: string; payload?: unknown } | undefined,
154
- ): Record<string, unknown> {
155
- if (part.state === "approval-requested" && "approval" in part) {
156
+ ): {
157
+ approval?: NonNullable<ToolCallMessagePart["approval"]>;
158
+ interrupt?: NonNullable<ToolCallMessagePart["interrupt"]>;
159
+ } {
160
+ if (part.approval && typeof part.approval.id === "string") {
161
+ const { id, approved, reason, isAutomatic } = part.approval;
156
162
  return {
157
- interrupt: {
158
- type: "human" as const,
159
- payload: (part as { approval: unknown }).approval,
160
- },
161
- status: {
162
- type: "requires-action" as const,
163
- reason: "interrupt" as const,
163
+ approval: {
164
+ id,
165
+ ...(typeof approved === "boolean" && { approved }),
166
+ ...(typeof reason === "string" && { reason }),
167
+ ...(isAutomatic === true && { isAutomatic: true }),
164
168
  },
165
169
  };
166
170
  }
167
171
 
168
172
  if (toolStatus?.type === "interrupt") {
169
173
  return {
170
- interrupt: toolStatus.payload,
171
- status: {
172
- type: "requires-action" as const,
173
- reason: "interrupt" as const,
174
- },
174
+ interrupt: toolStatus.payload as NonNullable<
175
+ ToolCallMessagePart["interrupt"]
176
+ >,
175
177
  };
176
178
  }
177
179
 
@@ -242,7 +244,7 @@ function convertParts(
242
244
  isError = true;
243
245
  result = {
244
246
  error:
245
- (part as { approval: { reason?: string } }).approval.reason ||
247
+ (part as { approval?: { reason?: string } }).approval?.reason ||
246
248
  "Tool approval denied",
247
249
  };
248
250
  }
@@ -281,7 +283,7 @@ function convertParts(
281
283
  isError,
282
284
  ...(modelContent !== undefined && { modelContent }),
283
285
  ...(mcpApp && { mcp: { app: mcpApp } }),
284
- ...getToolInterrupt(part, toolStatus),
286
+ ...getToolApprovalAndInterrupt(part, toolStatus),
285
287
  } satisfies ToolCallMessagePart;
286
288
  }
287
289