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

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/index.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- /// <reference types="@assistant-ui/core/store" />
2
1
  /// <reference types="@assistant-ui/core/react" />
3
2
  import { frontendTools } from "./frontendTools.js";
4
3
  import { useAISDKRuntime } from "./ui/use-chat/useAISDKRuntime.js";
@@ -1 +1 @@
1
- {"version":3,"file":"useAISDKRuntime.d.ts","names":[],"sources":["../../../src/ui/use-chat/useAISDKRuntime.ts"],"mappings":";;;;KA0CY,6BAAA,uBACS,SAAA,GAAY,SAAA,EAE/B,OAAA,EAAS,aAAA,KACN,eAAA,CAAgB,UAAA;AAAA,KAYT,mBAAA;EACV,QAAA,IACK,WAAA,CAAY,oBAAA;IACX,OAAA,GAAU,oBAAA;EAAA;EAGhB,eAAA,GAAkB,6BAAA;EArBa;;;;;;;;EA8B/B,4BAAA;EA5BS;;;;;AACoB;AAY/B;EAuBE,QAAA,GAAW,oBAAA;;;;;;;EAOX,WAAA,YAAuB,gBAAA;AAAA;AAAA,cAGZ,eAAA,sBAAsC,SAAA,GAAY,SAAA,EAC7D,WAAA,EAAa,UAAA,QAAkB,OAAA,CAAQ,UAAA;EACvC,QAAA;EAAA,eAAA,EAAA,qBAAA;EAAA,4BAAA;EAAA,QAAA;EAAA;AAAA,IAMG,mBAAA,KAAwB,gBAAA"}
1
+ {"version":3,"file":"useAISDKRuntime.d.ts","names":[],"sources":["../../../src/ui/use-chat/useAISDKRuntime.ts"],"mappings":";;;;KAyCY,6BAAA,uBACS,SAAA,GAAY,SAAA,EAE/B,OAAA,EAAS,aAAA,KACN,eAAA,CAAgB,UAAA;AAAA,KAYT,mBAAA;EACV,QAAA,IACK,WAAA,CAAY,oBAAA;IACX,OAAA,GAAU,oBAAA;EAAA;EAGhB,eAAA,GAAkB,6BAAA;EArBa;;;;;;;;EA8B/B,4BAAA;EA5BS;;;;;AACoB;AAY/B;EAuBE,QAAA,GAAW,oBAAA;;;;;;;EAOX,WAAA,YAAuB,gBAAA;AAAA;AAAA,cAGZ,eAAA,sBAAsC,SAAA,GAAY,SAAA,EAC7D,WAAA,EAAa,UAAA,QAAkB,OAAA,CAAQ,UAAA;EACvC,QAAA;EAAA,eAAA,EAAA,qBAAA;EAAA,4BAAA;EAAA,QAAA;EAAA;AAAA,IAMG,mBAAA,KAAwB,gBAAA"}
@@ -10,7 +10,7 @@ import { toExportedMessageRepository, useExternalHistory } from "./useExternalHi
10
10
  import { useStreamingTiming } from "./useStreamingTiming.js";
11
11
  import { generateId, isToolUIPart } from "ai";
12
12
  import { useMemo, useRef, useState } from "react";
13
- import { useExternalStoreRuntime, useRuntimeAdapters, useToolInvocations } from "@assistant-ui/core/react";
13
+ import { useExternalStoreRuntime, useRuntimeAdapters } from "@assistant-ui/core/react";
14
14
  import { getExternalStoreMessages } from "@assistant-ui/core";
15
15
  //#region src/ui/use-chat/useAISDKRuntime.ts
16
16
  const toUIMessage = (createMessage, fallbackRole) => ({
@@ -47,31 +47,11 @@ const useAISDKRuntime = (chatHelpers, { adapters, toCreateMessage: customToCreat
47
47
  const [runtimeRef] = useState(() => ({ get current() {
48
48
  return runtime;
49
49
  } }));
50
- const toolInvocations = useToolInvocations({
51
- state: {
52
- messages,
53
- isRunning
54
- },
55
- getTools: () => runtimeRef.current.thread.getModelContext().tools,
56
- onResult: (command) => {
57
- if (command.type === "add-tool-result") {
58
- const output = command.modelContent !== void 0 ? wrapModelContentEnvelope(command.result, command.modelContent) : command.result;
59
- chatHelpers.addToolResult({
60
- tool: command.toolName,
61
- toolCallId: command.toolCallId,
62
- output,
63
- options: { metadata: lastRunConfigRef.current }
64
- });
65
- }
66
- },
67
- setToolStatuses
68
- });
69
50
  const isLoading = useExternalHistory(runtimeRef, adapters?.history ?? contextAdapters?.history, AISDKMessageConverter.toThreadMessages, aiSDKV6FormatAdapter, (messages) => {
70
51
  chatHelpers.setMessages(messages);
71
52
  });
72
53
  const completePendingToolCalls = async () => {
73
54
  if (!cancelPendingToolCallsOnSend) return;
74
- await toolInvocations.abort();
75
55
  chatHelpers.setMessages((messages) => {
76
56
  const lastMessage = messages.at(-1);
77
57
  if (lastMessage?.role !== "assistant") return messages;
@@ -96,6 +76,8 @@ const useAISDKRuntime = (chatHelpers, { adapters, toCreateMessage: customToCreat
96
76
  const runtime = useExternalStoreRuntime({
97
77
  isRunning,
98
78
  messages,
79
+ unstable_enableToolInvocations: true,
80
+ setToolStatuses,
99
81
  setMessages: (messages) => chatHelpers.setMessages(messages.map(getVercelAIMessages).filter(Boolean).flat()),
100
82
  onImport: (messages) => chatHelpers.setMessages(messages.map(getVercelAIMessages).filter(Boolean).flat()),
101
83
  onExportExternalState: () => {
@@ -124,7 +106,6 @@ const useAISDKRuntime = (chatHelpers, { adapters, toCreateMessage: customToCreat
124
106
  },
125
107
  onCancel: async () => {
126
108
  chatHelpers.stop();
127
- await toolInvocations.abort();
128
109
  },
129
110
  onNew: async (message) => {
130
111
  const createMessage = (customToCreateMessage ?? toCreateMessage)(message);
@@ -152,24 +133,33 @@ const useAISDKRuntime = (chatHelpers, { adapters, toCreateMessage: customToCreat
152
133
  chatHelpers.setMessages(newMessages);
153
134
  await chatHelpers.regenerate({ metadata: config.runConfig });
154
135
  },
155
- onAddToolResult: ({ toolCallId, result, isError }) => {
136
+ onAddToolResult: ({ toolCallId, toolName, result, isError, modelContent }) => {
156
137
  const options = { metadata: lastRunConfigRef.current };
157
138
  if (isError) chatHelpers.addToolOutput({
158
139
  state: "output-error",
159
- tool: toolCallId,
140
+ tool: toolName ?? toolCallId,
160
141
  toolCallId,
161
142
  errorText: typeof result === "string" ? result : JSON.stringify(result),
162
143
  options
163
144
  });
164
- else chatHelpers.addToolOutput({
165
- state: "output-available",
166
- tool: toolCallId,
167
- toolCallId,
168
- output: result,
169
- options
145
+ else {
146
+ const output = modelContent !== void 0 ? wrapModelContentEnvelope(result, modelContent) : result;
147
+ chatHelpers.addToolResult({
148
+ tool: toolName,
149
+ toolCallId,
150
+ output,
151
+ options
152
+ });
153
+ }
154
+ },
155
+ onRespondToToolApproval: ({ approvalId, approved, reason }) => {
156
+ chatHelpers.addToolApprovalResponse({
157
+ id: approvalId,
158
+ approved,
159
+ ...reason != null && { reason },
160
+ options: { metadata: lastRunConfigRef.current }
170
161
  });
171
162
  },
172
- onResumeToolCall: (options) => toolInvocations.resume(options.toolCallId, options.payload),
173
163
  ...onResume && { onResume },
174
164
  ...suggestions && { suggestions },
175
165
  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 { useMemo, useRef, useState } from \"react\";\nimport type { UIMessage, useChat, CreateUIMessage } from \"@ai-sdk/react\";\nimport { isToolUIPart, generateId } from \"ai\";\nimport {\n useExternalStoreRuntime,\n useRuntimeAdapters,\n} from \"@assistant-ui/core/react\";\nimport type { ToolExecutionStatus } from \"@assistant-ui/core\";\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 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 // The runtime auto-aborts in-flight tool invocations when a new run\n // is dispatched (append() / startRun()). All we need to do here is\n // mark any tool without a result as cancelled in the UI message list.\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 unstable_enableToolInvocations: true,\n setToolStatuses,\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 },\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: ({\n toolCallId,\n toolName,\n result,\n isError,\n modelContent,\n }) => {\n const options = { metadata: lastRunConfigRef.current };\n if (isError) {\n chatHelpers.addToolOutput({\n state: \"output-error\",\n tool: toolName ?? toolCallId,\n toolCallId,\n errorText:\n typeof result === \"string\" ? result : JSON.stringify(result),\n options,\n });\n } else {\n const output =\n modelContent !== undefined\n ? wrapModelContentEnvelope(result, modelContent)\n : result;\n chatHelpers.addToolResult({\n tool: toolName,\n toolCallId,\n output,\n options,\n });\n }\n },\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.30",
4
4
  "description": "Vercel AI SDK adapter for assistant-ui",
5
5
  "keywords": [
6
6
  "ai-sdk",
@@ -30,10 +30,10 @@
30
30
  ],
31
31
  "sideEffects": false,
32
32
  "dependencies": {
33
- "@ai-sdk/react": "^3.0.190",
34
- "@assistant-ui/core": "^0.2.5",
33
+ "@ai-sdk/react": "^3.0.193",
34
+ "@assistant-ui/core": "^0.2.7",
35
35
  "@assistant-ui/store": "^0.2.12",
36
- "ai": "^6.0.188",
36
+ "ai": "^6.0.191",
37
37
  "assistant-cloud": "*"
38
38
  },
39
39
  "peerDependencies": {
@@ -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",
package/src/index.ts CHANGED
@@ -1,4 +1,3 @@
1
- /// <reference types="@assistant-ui/core/store" />
2
1
  /// <reference types="@assistant-ui/core/react" />
3
2
 
4
3
  export { useAISDKRuntime } from "./ui/use-chat/useAISDKRuntime";
@@ -1,14 +1,13 @@
1
1
  "use client";
2
2
 
3
- import { useState, useMemo, useRef } from "react";
3
+ import { useMemo, useRef, useState } from "react";
4
4
  import type { UIMessage, useChat, CreateUIMessage } from "@ai-sdk/react";
5
5
  import { isToolUIPart, generateId } from "ai";
6
6
  import {
7
7
  useExternalStoreRuntime,
8
8
  useRuntimeAdapters,
9
- useToolInvocations,
10
- type ToolExecutionStatus,
11
9
  } from "@assistant-ui/core/react";
10
+ import type { ToolExecutionStatus } from "@assistant-ui/core";
12
11
  import type {
13
12
  ExternalStoreAdapter,
14
13
  ThreadHistoryAdapter,
@@ -144,29 +143,6 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
144
143
  },
145
144
  }));
146
145
 
147
- const toolInvocations = useToolInvocations({
148
- state: {
149
- messages,
150
- isRunning,
151
- },
152
- getTools: () => runtimeRef.current.thread.getModelContext().tools,
153
- onResult: (command) => {
154
- if (command.type === "add-tool-result") {
155
- const output =
156
- command.modelContent !== undefined
157
- ? wrapModelContentEnvelope(command.result, command.modelContent)
158
- : command.result;
159
- chatHelpers.addToolResult({
160
- tool: command.toolName,
161
- toolCallId: command.toolCallId,
162
- output,
163
- options: { metadata: lastRunConfigRef.current },
164
- });
165
- }
166
- },
167
- setToolStatuses,
168
- });
169
-
170
146
  const isLoading = useExternalHistory(
171
147
  runtimeRef,
172
148
  adapters?.history ?? contextAdapters?.history,
@@ -185,7 +161,9 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
185
161
  const completePendingToolCalls = async () => {
186
162
  if (!cancelPendingToolCallsOnSend) return;
187
163
 
188
- await toolInvocations.abort();
164
+ // The runtime auto-aborts in-flight tool invocations when a new run
165
+ // is dispatched (append() / startRun()). All we need to do here is
166
+ // mark any tool without a result as cancelled in the UI message list.
189
167
 
190
168
  // Mark any tool without a result as cancelled (uses setMessages to avoid triggering sendAutomaticallyWhen)
191
169
  chatHelpers.setMessages((messages) => {
@@ -214,6 +192,8 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
214
192
  const runtime = useExternalStoreRuntime({
215
193
  isRunning,
216
194
  messages,
195
+ unstable_enableToolInvocations: true,
196
+ setToolStatuses,
217
197
  setMessages: (messages) =>
218
198
  chatHelpers.setMessages(
219
199
  messages
@@ -278,7 +258,6 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
278
258
  },
279
259
  onCancel: async () => {
280
260
  chatHelpers.stop();
281
- await toolInvocations.abort();
282
261
  },
283
262
  onNew: async (message) => {
284
263
  const createMessage = (
@@ -327,29 +306,44 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
327
306
 
328
307
  await chatHelpers.regenerate({ metadata: config.runConfig });
329
308
  },
330
- onAddToolResult: ({ toolCallId, result, isError }) => {
309
+ onAddToolResult: ({
310
+ toolCallId,
311
+ toolName,
312
+ result,
313
+ isError,
314
+ modelContent,
315
+ }) => {
331
316
  const options = { metadata: lastRunConfigRef.current };
332
317
  if (isError) {
333
318
  chatHelpers.addToolOutput({
334
319
  state: "output-error",
335
- tool: toolCallId,
320
+ tool: toolName ?? toolCallId,
336
321
  toolCallId,
337
322
  errorText:
338
323
  typeof result === "string" ? result : JSON.stringify(result),
339
324
  options,
340
325
  });
341
326
  } else {
342
- chatHelpers.addToolOutput({
343
- state: "output-available",
344
- tool: toolCallId,
327
+ const output =
328
+ modelContent !== undefined
329
+ ? wrapModelContentEnvelope(result, modelContent)
330
+ : result;
331
+ chatHelpers.addToolResult({
332
+ tool: toolName,
345
333
  toolCallId,
346
- output: result,
334
+ output,
347
335
  options,
348
336
  });
349
337
  }
350
338
  },
351
- onResumeToolCall: (options) =>
352
- toolInvocations.resume(options.toolCallId, options.payload),
339
+ onRespondToToolApproval: ({ approvalId, approved, reason }) => {
340
+ void chatHelpers.addToolApprovalResponse({
341
+ id: approvalId,
342
+ approved,
343
+ ...(reason != null && { reason }),
344
+ options: { metadata: lastRunConfigRef.current },
345
+ });
346
+ },
353
347
  ...(onResume && { onResume }),
354
348
  ...(suggestions && { suggestions }),
355
349
  adapters: {
@@ -1,6 +1,9 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import type { ReadonlyJSONObject } from "assistant-stream/utils";
3
- import { AISDKMessageConverter } from "./convertMessage";
3
+ import {
4
+ AISDKMessageConverter,
5
+ type AISDKMessageConverterMetadata,
6
+ } from "./convertMessage";
4
7
 
5
8
  describe("AISDKMessageConverter", () => {
6
9
  it("converts user files into attachments and keeps text content", () => {
@@ -186,7 +189,7 @@ describe("AISDKMessageConverter", () => {
186
189
  });
187
190
  });
188
191
 
189
- it("deduplicates tool calls by toolCallId and maps interrupt states", () => {
192
+ it("deduplicates tool calls by toolCallId and surfaces approval / interrupt state", () => {
190
193
  const converted = AISDKMessageConverter.toThreadMessages(
191
194
  [
192
195
  {
@@ -212,7 +215,7 @@ describe("AISDKMessageConverter", () => {
212
215
  toolCallId: "tc-2",
213
216
  state: "approval-requested",
214
217
  input: { action: "deploy" },
215
- approval: { reason: "need human review" },
218
+ approval: { id: "appr-1" },
216
219
  },
217
220
  {
218
221
  type: "tool-human",
@@ -220,6 +223,39 @@ describe("AISDKMessageConverter", () => {
220
223
  state: "input-available",
221
224
  input: { task: "confirm" },
222
225
  },
226
+ {
227
+ type: "tool-approve",
228
+ toolCallId: "tc-4",
229
+ state: "approval-responded",
230
+ input: { action: "rollback" },
231
+ approval: { id: "appr-2", approved: true, reason: "looks ok" },
232
+ },
233
+ {
234
+ type: "tool-approve",
235
+ toolCallId: "tc-5",
236
+ state: "approval-requested",
237
+ input: { action: "auto" },
238
+ approval: { id: "appr-3", isAutomatic: true },
239
+ },
240
+ {
241
+ type: "tool-approve",
242
+ toolCallId: "tc-6",
243
+ state: "output-denied",
244
+ input: { action: "wipe" },
245
+ approval: {
246
+ id: "appr-4",
247
+ approved: false,
248
+ reason: "user denied",
249
+ },
250
+ },
251
+ {
252
+ type: "tool-approve",
253
+ toolCallId: "tc-7",
254
+ state: "output-available",
255
+ input: { action: "ship" },
256
+ output: { ok: true },
257
+ approval: { id: "appr-5", approved: true, isAutomatic: true },
258
+ },
223
259
  ],
224
260
  } as any,
225
261
  ],
@@ -237,17 +273,49 @@ describe("AISDKMessageConverter", () => {
237
273
  const toolCalls = converted[0]?.content.filter(
238
274
  (part): part is any => part.type === "tool-call",
239
275
  );
240
- expect(toolCalls).toHaveLength(3);
276
+ expect(toolCalls).toHaveLength(7);
241
277
 
242
278
  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",
279
+
280
+ expect(toolCalls?.find((p) => p.toolCallId === "tc-2")?.approval).toEqual({
281
+ id: "appr-1",
246
282
  });
283
+ expect(
284
+ toolCalls?.find((p) => p.toolCallId === "tc-2")?.interrupt,
285
+ ).toBeUndefined();
286
+
247
287
  expect(toolCalls?.find((p) => p.toolCallId === "tc-3")?.interrupt).toEqual({
248
288
  type: "human",
249
289
  payload: { kind: "human" },
250
290
  });
291
+ expect(
292
+ toolCalls?.find((p) => p.toolCallId === "tc-3")?.approval,
293
+ ).toBeUndefined();
294
+
295
+ expect(toolCalls?.find((p) => p.toolCallId === "tc-4")?.approval).toEqual({
296
+ id: "appr-2",
297
+ approved: true,
298
+ reason: "looks ok",
299
+ });
300
+
301
+ expect(toolCalls?.find((p) => p.toolCallId === "tc-5")?.approval).toEqual({
302
+ id: "appr-3",
303
+ isAutomatic: true,
304
+ });
305
+
306
+ const denied = toolCalls?.find((p) => p.toolCallId === "tc-6");
307
+ expect(denied?.approval).toEqual({
308
+ id: "appr-4",
309
+ approved: false,
310
+ reason: "user denied",
311
+ });
312
+ expect(denied?.isError).toBe(true);
313
+
314
+ expect(toolCalls?.find((p) => p.toolCallId === "tc-7")?.approval).toEqual({
315
+ id: "appr-5",
316
+ approved: true,
317
+ isAutomatic: true,
318
+ });
251
319
  });
252
320
 
253
321
  it("strips closing delimiters from streaming tool argsText", () => {
@@ -273,7 +341,7 @@ describe("AISDKMessageConverter", () => {
273
341
  });
274
342
 
275
343
  it("keeps observed key order from streaming snapshots for final tool args", () => {
276
- const metadata = {
344
+ const metadata: AISDKMessageConverterMetadata = {
277
345
  toolArgsKeyOrderCache: new Map<string, Map<string, string[]>>(),
278
346
  };
279
347
 
@@ -345,7 +413,7 @@ describe("AISDKMessageConverter", () => {
345
413
  });
346
414
 
347
415
  it("merges duplicate toolCallId across assistant snapshots", () => {
348
- const metadata = {
416
+ const metadata: AISDKMessageConverterMetadata = {
349
417
  toolArgsKeyOrderCache: new Map<string, Map<string, string[]>>(),
350
418
  };
351
419
 
@@ -398,7 +466,7 @@ describe("AISDKMessageConverter", () => {
398
466
  });
399
467
 
400
468
  it("preserves last good input when AI SDK briefly emits null input", () => {
401
- const metadata = {
469
+ const metadata: AISDKMessageConverterMetadata = {
402
470
  toolArgsKeyOrderCache: new Map<string, Map<string, string[]>>(),
403
471
  toolLastInputCache: new Map<string, ReadonlyJSONObject>(),
404
472
  };
@@ -441,7 +509,7 @@ describe("AISDKMessageConverter", () => {
441
509
  });
442
510
 
443
511
  it("preserves last good input across terminal state transitions", () => {
444
- const metadata = {
512
+ const metadata: AISDKMessageConverterMetadata = {
445
513
  toolArgsKeyOrderCache: new Map<string, Map<string, string[]>>(),
446
514
  toolLastInputCache: new Map<string, ReadonlyJSONObject>(),
447
515
  };
@@ -625,7 +693,7 @@ describe("AISDKMessageConverter", () => {
625
693
  });
626
694
 
627
695
  it("memoizes MCP app metadata across conversions by resourceUri", () => {
628
- const metadata = {
696
+ const metadata: AISDKMessageConverterMetadata = {
629
697
  mcpAppMetadataCache: new Map(),
630
698
  };
631
699
 
@@ -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