@assistant-ui/ai-sdk 0.0.4 → 0.0.6
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/adapters/aiSDKFormatAdapter.d.ts +2 -8
- package/dist/adapters/aiSDKFormatAdapter.js +1 -25
- package/dist/converters/convertMessage.d.ts.map +1 -1
- package/dist/converters/convertMessage.js +9 -4
- package/dist/converters/convertMessage.js.map +1 -1
- package/dist/runtime/AISDKThreads.d.ts.map +1 -1
- package/dist/runtime/AISDKThreads.js +90 -65
- package/dist/runtime/AISDKThreads.js.map +1 -1
- package/dist/runtime/sdkIdentity.d.ts +6 -0
- package/dist/runtime/sdkIdentity.d.ts.map +1 -0
- package/dist/runtime/sdkIdentity.js +9 -0
- package/dist/runtime/sdkIdentity.js.map +1 -0
- package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
- package/dist/runtime/useAISDKRuntime.js +8 -1
- package/dist/runtime/useAISDKRuntime.js.map +1 -1
- package/dist/runtime/useChatRuntime.d.ts.map +1 -1
- package/dist/runtime/useChatRuntime.js +7 -2
- package/dist/runtime/useChatRuntime.js.map +1 -1
- package/dist/runtime/useChatThread.js +1 -1
- package/dist/runtime/useChatThread.js.map +1 -1
- package/dist/runtime/useExternalHistory.d.ts.map +1 -1
- package/dist/runtime/useExternalHistory.js +4 -1
- package/dist/runtime/useExternalHistory.js.map +1 -1
- package/dist/tools/generativeTools.js +7 -4
- package/dist/tools/generativeTools.js.map +1 -1
- package/dist/usage.d.ts +8 -0
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +8 -0
- package/dist/usage.js.map +1 -1
- package/package.json +12 -12
- package/src/adapters/aiSDKFormatAdapter.ts +4 -41
- package/src/converters/convertMessage.test.ts +116 -0
- package/src/converters/convertMessage.ts +39 -18
- package/src/runtime/AISDKChat.integration.test.tsx +48 -27
- package/src/runtime/AISDKThreads.cloud.test.ts +12 -3
- package/src/runtime/AISDKThreads.test.ts +131 -8
- package/src/runtime/AISDKThreads.ts +31 -3
- package/src/runtime/__tests__/controlled-transport.ts +21 -0
- package/src/runtime/sdkIdentity.ts +9 -0
- package/src/runtime/useAISDKRuntime.test.ts +39 -1
- package/src/runtime/useAISDKRuntime.ts +17 -1
- package/src/runtime/useChatRuntime.integration.test.tsx +137 -4
- package/src/runtime/useChatRuntime.test.ts +0 -1
- package/src/runtime/useChatRuntime.ts +3 -1
- package/src/runtime/useChatThread.test.ts +74 -0
- package/src/runtime/useChatThread.ts +1 -1
- package/src/runtime/useExternalHistory.test.ts +29 -0
- package/src/runtime/useExternalHistory.ts +7 -1
- package/src/tools/generativeTools.test.ts +79 -0
- package/src/tools/generativeTools.ts +7 -8
- package/src/transport/AssistantChatTransport.test.ts +1 -9
- package/src/usage.ts +8 -0
- package/dist/adapters/aiSDKFormatAdapter.d.ts.map +0 -1
- package/dist/adapters/aiSDKFormatAdapter.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generativeTools.js","names":["jsonSchema","ToolSet","MCPClient","MCPClientConfig","createMCPClient","Experimental_StdioMCPTransport","toJSONSchema","Tool","McpServerConfig","ToolModelOutputFunction","McpToolkitToolConfig","Toolkit","ToolkitDefinition","frontendTools","FrontendTools","toAISDKContent","toAISDKDefaultOutput","unwrapModelContentEnvelope","ModelContentEnvelope","EMPTY_SCHEMA","type","const","properties","humanNotSupported","Error","neverAbort","AbortController","signal","MCPConnectionTimeoutPhase","MCPConnectionTimeoutError","createMcpConnectionTimeoutError","name","phase","timeoutMs","withMcpConnectionTimeout","promise","Promise","T","options","config","startedAt","connectionTimeout","undefined","remainingMs","Date","now","timeoutError","timeout","ReturnType","setTimeout","race","_","reject","clearTimeout","parametersToInputSchema","parameters","GenerativeToolsOptions","toolkit","AISDKToolkitOptions","AISDKToolkitToolsOptions","frontend","generativeTools","assertNoMcpToolkitTools","toProviderToolSet","toServerToolSet","AISDKToolkit","mcpClients","Map","constructor","tools","frontendToolSet","mcpToolSet","mcpTools","providerToolSet","serverToolSet","assertNoMcpToolNameCollisions","source","close","clientEntries","entries","clientNames","map","clear","clientResults","allSettled","clientPromise","clients","flatMap","result","index","status","value","closeResults","client","errors","toMcpToolkitError","reason","length","AggregateError","#mcpTools","McpToolSet","toolSets","all","Object","filter","entry","McpToolkitTool","isMcpToolkitTool","tool","mcpClient","server","catch","error","delete","toolSources","serverName","mcpTool","toolSet","toolName","isDisabledMcpTool","exposedName","prefix","existingServerName","get","set","sources","#mcpClient","existing","createPromise","toMCPClientConfig","next","then","transport","command","args","env","cwd","url","headers","redirect","ToolkitTool","Record","mcp","prototype","hasOwnProperty","call","disabled","getErrorMessage","message","String","entryName","action","cause","mcpToolName","find","AISDKToModelOutputOptions","Omit","Parameters","TArgs","TResult","output","toAISDKToModelOutput","toModelOutput","modelContent","parts","fromEntries","t","execute","description","inputSchema","providerOptions","callOptions","toolCallId","abortSignal","AbortSignal","human","ProviderToolkitTool","isProviderToolkitTool","id","providerId","supportsDeferredResults","Extract"],"sources":["../../src/tools/generativeTools.ts"],"sourcesContent":["import { jsonSchema, type ToolSet } from \"ai\";\nimport type { MCPClient, MCPClientConfig } from \"@ai-sdk/mcp\";\nimport { createMCPClient } from \"@ai-sdk/mcp\";\nimport { Experimental_StdioMCPTransport } from \"#mcp-stdio\";\nimport {\n toJSONSchema,\n type Tool,\n type McpServerConfig,\n type ToolModelOutputFunction,\n} from \"assistant-stream\";\nimport type {\n McpToolkitToolConfig,\n Toolkit,\n ToolkitDefinition,\n} from \"@assistant-ui/core/react\";\nimport { frontendTools, type FrontendTools } from \"./frontendTools\";\nimport {\n toAISDKContent,\n toAISDKDefaultOutput,\n} from \"../converters/toolOutputConversion\";\nimport {\n unwrapModelContentEnvelope,\n type ModelContentEnvelope,\n} from \"../converters/modelContentEnvelope\";\n\nconst EMPTY_SCHEMA = { type: \"object\" as const, properties: {} };\n\nconst humanNotSupported = (): never => {\n throw new Error(\n \"`human()` is not available during server-side tool execution.\",\n );\n};\n\n// AI SDK leaves `abortSignal` optional; assistant-ui's execute requires one.\nconst neverAbort = new AbortController().signal;\n\ntype MCPConnectionTimeoutPhase = \"connecting\" | \"listing tools\";\n\nclass MCPConnectionTimeoutError extends Error {}\n\nconst createMcpConnectionTimeoutError = (\n name: string,\n phase: MCPConnectionTimeoutPhase,\n timeoutMs: number,\n) =>\n new MCPConnectionTimeoutError(\n `MCP toolkit entry \"${name}\" timed out while ${phase} after ${timeoutMs}ms.`,\n );\n\nconst withMcpConnectionTimeout = async <T>(\n promise: Promise<T>,\n options: {\n name: string;\n config: McpServerConfig;\n phase: MCPConnectionTimeoutPhase;\n startedAt: number;\n },\n): Promise<T> => {\n const timeoutMs = options.config.connectionTimeout;\n if (timeoutMs === undefined) return await promise;\n const remainingMs = timeoutMs - (Date.now() - options.startedAt);\n const timeoutError = () =>\n createMcpConnectionTimeoutError(options.name, options.phase, timeoutMs);\n if (remainingMs <= 0) throw timeoutError();\n\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timeout = setTimeout(() => reject(timeoutError()), remainingMs);\n }),\n ]);\n } finally {\n if (timeout !== undefined) clearTimeout(timeout);\n }\n};\n\nconst parametersToInputSchema = (parameters: Tool[\"parameters\"] | undefined) =>\n jsonSchema(parameters ? toJSONSchema(parameters) : EMPTY_SCHEMA);\n\n/**\n * @deprecated Options for the deprecated {@link generativeTools}. Use\n * {@link AISDKToolkit} with {@link AISDKToolkitOptions} /\n * {@link AISDKToolkitToolsOptions} instead.\n */\nexport interface GenerativeToolsOptions {\n /**\n * The server build of a generative toolkit (schema + server `execute`). Typed\n * as the canonical {@link Toolkit} so callers don't need to cast; the server\n * build carries `execute`, recovered internally as {@link ToolkitDefinition}.\n */\n toolkit: Toolkit;\n /**\n * Tools uploaded by the frontend (the request body's `tools`). Merged in\n * alongside the `toolkit`; a server `execute` from `toolkit` takes precedence\n * over an uploaded entry of the same name.\n */\n frontendTools?: FrontendTools;\n}\n\nexport type AISDKToolkitOptions = {\n toolkit: Toolkit;\n};\n\nexport type AISDKToolkitToolsOptions = {\n /**\n * Tools uploaded by the frontend request body.\n */\n frontend?: FrontendTools;\n};\n\n/**\n * Builds an AI SDK `ToolSet` for server-side use with `streamText` /\n * `generateText` from a generative `toolkit` and the frontend-uploaded tools.\n *\n * Each toolkit tool's `execute` runs on the server. Pair this with the\n * `\"use generative\"` compiler: import the toolkit in a server route (where it\n * resolves to the server build — schema + `execute`, with `render` stripped) and\n * pass it here. Tools without an `execute` are still exposed to the model but\n * left for the client to fulfill. `frontendTools` lets the client contribute\n * tools that aren't in the static toolkit.\n *\n * @deprecated Use {@link AISDKToolkit} instead:\n * `new AISDKToolkit({ toolkit }).tools({ frontend })`. It is a strict superset\n * (it also opens MCP server connections), so it replaces `generativeTools`\n * everywhere. The `frontendTools` option is named `frontend` on `.tools()`, and\n * `.tools()` is async. `generativeTools` will be removed in a future version.\n *\n * @example\n * ```ts\n * // Define once at module scope so any MCP connections pool across requests.\n * const aiToolkit = new AISDKToolkit({ toolkit: docsToolkit });\n *\n * // In your route handler:\n * const { tools } = await req.json();\n * streamText({\n * model,\n * messages,\n * tools: await aiToolkit.tools({ frontend: tools }),\n * });\n * ```\n */\nexport const generativeTools = (options: GenerativeToolsOptions): ToolSet => {\n assertNoMcpToolkitTools(options.toolkit);\n return {\n ...(options.frontendTools ? frontendTools(options.frontendTools) : {}),\n // `toolkit` last so its server-side `execute` wins over an uploaded entry of\n // the same name. The cast recovers the declaration shape — the server build\n // carries `execute`, which the canonical `Toolkit` type erases.\n ...toProviderToolSet(options.toolkit),\n ...toServerToolSet(options.toolkit as ToolkitDefinition),\n };\n};\n\nexport class AISDKToolkit {\n readonly #toolkit: Toolkit;\n readonly #mcpClients = new Map<string, Promise<MCPClient>>();\n\n constructor(options: AISDKToolkitOptions) {\n this.#toolkit = options.toolkit;\n }\n\n async tools(options: AISDKToolkitToolsOptions = {}): Promise<ToolSet> {\n const frontendToolSet = options.frontend\n ? frontendTools(options.frontend)\n : {};\n const mcpToolSet = await this.#mcpTools();\n const providerToolSet = toProviderToolSet(this.#toolkit);\n const serverToolSet = toServerToolSet(this.#toolkit as ToolkitDefinition);\n\n assertNoMcpToolNameCollisions(mcpToolSet, [\n { source: \"frontend\", tools: frontendToolSet },\n { source: \"provider\", tools: providerToolSet },\n { source: \"toolkit\", tools: serverToolSet },\n ]);\n\n return {\n ...frontendToolSet,\n ...mcpToolSet.tools,\n ...providerToolSet,\n ...serverToolSet,\n };\n }\n\n async close(): Promise<void> {\n const clientEntries = [...this.#mcpClients.entries()];\n const clientNames = clientEntries.map(([name]) => name);\n this.#mcpClients.clear();\n const clientResults = await Promise.allSettled(\n clientEntries.map(([, clientPromise]) => clientPromise),\n );\n const clients = clientResults.flatMap((result, index) =>\n result.status === \"fulfilled\"\n ? [[clientNames[index]!, result.value] as const]\n : [],\n );\n const closeResults = await Promise.allSettled(\n clients.map(([, client]) => client.close()),\n );\n const errors = [\n ...clientResults.flatMap((result, index) =>\n result.status === \"rejected\"\n ? [toMcpToolkitError(clientNames[index]!, \"connect\", result.reason)]\n : [],\n ),\n ...closeResults.flatMap((result, index) =>\n result.status === \"rejected\"\n ? [toMcpToolkitError(clients[index]![0], \"close\", result.reason)]\n : [],\n ),\n ];\n if (errors.length === 1) throw errors[0];\n if (errors.length > 1) {\n throw new AggregateError(\n errors,\n \"Failed to close one or more MCP clients\",\n );\n }\n }\n\n async #mcpTools(): Promise<McpToolSet> {\n const toolSets = await Promise.all(\n Object.entries(this.#toolkit)\n .filter((entry): entry is [string, McpToolkitTool] =>\n isMcpToolkitTool(entry[1]),\n )\n .map(async ([name, tool]) => {\n const startedAt = Date.now();\n const client = await this.#mcpClient(\n name,\n tool.server,\n startedAt,\n ).catch((error: unknown) => {\n if (error instanceof MCPConnectionTimeoutError) throw error;\n throw toMcpToolkitError(name, \"connect\", error);\n });\n try {\n const tools = await withMcpConnectionTimeout(client.tools(), {\n name,\n config: tool.server,\n phase: \"listing tools\",\n startedAt,\n });\n return [name, tool, tools] as const;\n } catch (error) {\n if (error instanceof MCPConnectionTimeoutError) {\n this.#mcpClients.delete(name);\n void client.close().catch(() => {});\n throw error;\n }\n throw toMcpToolkitError(name, \"list tools\", error);\n }\n }),\n );\n\n const tools: ToolSet = {};\n const toolSources = new Map<string, string>();\n for (const [serverName, mcpTool, toolSet] of toolSets) {\n for (const [toolName, tool] of Object.entries(toolSet)) {\n if (isDisabledMcpTool(mcpTool.tools?.[toolName])) continue;\n const exposedName = `${mcpTool.prefix ?? \"\"}${toolName}`;\n const existingServerName = toolSources.get(exposedName);\n if (existingServerName) {\n throw new Error(\n `MCP tool name collision: \"${exposedName}\" is exposed by both \"${existingServerName}\" and \"${serverName}\". Rename one of the toolkit entries or expose distinct MCP tool names.`,\n );\n }\n toolSources.set(exposedName, serverName);\n tools[exposedName] = tool as ToolSet[string];\n }\n }\n return { tools, sources: toolSources };\n }\n\n #mcpClient(\n name: string,\n config: McpServerConfig,\n startedAt: number,\n ): Promise<MCPClient> {\n const existing = this.#mcpClients.get(name);\n if (existing) return existing;\n const createPromise = createMCPClient(toMCPClientConfig(config));\n let next: Promise<MCPClient>;\n next = withMcpConnectionTimeout(createPromise, {\n name,\n config,\n phase: \"connecting\",\n startedAt,\n }).catch((error) => {\n if (this.#mcpClients.get(name) === next) {\n this.#mcpClients.delete(name);\n }\n if (error instanceof MCPConnectionTimeoutError) {\n void createPromise.then((client) => client.close()).catch(() => {});\n }\n throw error;\n });\n this.#mcpClients.set(name, next);\n return next;\n }\n}\n\nconst toMCPClientConfig = (config: McpServerConfig): MCPClientConfig => {\n if (config.type === \"stdio\") {\n return {\n transport: new Experimental_StdioMCPTransport({\n command: config.command,\n ...(config.args && { args: [...config.args] }),\n ...(config.env && { env: config.env }),\n ...(config.cwd && { cwd: config.cwd }),\n }),\n };\n }\n\n return {\n transport: {\n type: config.type,\n url: config.url,\n ...(config.headers && { headers: config.headers }),\n ...(config.redirect && { redirect: config.redirect }),\n },\n };\n};\n\ntype ToolkitTool = Toolkit[string];\n\ntype McpToolkitTool = ToolkitTool & {\n type: \"mcp\";\n server: McpServerConfig;\n prefix?: string | undefined;\n tools?: Record<string, McpToolkitToolConfig> | undefined;\n};\n\ntype McpToolSet = {\n tools: ToolSet;\n sources: Map<string, string>;\n};\n\nconst assertNoMcpToolNameCollisions = (\n mcp: McpToolSet,\n toolSets: readonly { source: string; tools: ToolSet }[],\n): void => {\n for (const [toolName, serverName] of mcp.sources) {\n for (const { source, tools } of toolSets) {\n if (!Object.prototype.hasOwnProperty.call(tools, toolName)) continue;\n throw new Error(\n `MCP tool \"${toolName}\" from \"${serverName}\" conflicts with ${source} tool \"${toolName}\". Rename one of the tools so each model-visible tool name is unique.`,\n );\n }\n }\n};\n\nconst isMcpToolkitTool = (tool: ToolkitTool): tool is McpToolkitTool =>\n tool.type === \"mcp\" && !tool.disabled;\n\nconst getErrorMessage = (error: unknown): string =>\n error instanceof Error ? error.message || error.name : String(error);\n\nconst toMcpToolkitError = (\n entryName: string,\n action: \"connect\" | \"list tools\" | \"close\",\n error: unknown,\n): Error => {\n return new Error(\n `MCP toolkit entry \"${entryName}\" failed to ${action}: ${getErrorMessage(error)}`,\n { cause: error },\n );\n};\n\nconst isDisabledMcpTool = (config: McpToolkitToolConfig | undefined): boolean =>\n config?.disabled === true;\n\nconst assertNoMcpToolkitTools = (toolkit: Toolkit): void => {\n const mcpToolName = Object.entries(toolkit).find(([, tool]) =>\n isMcpToolkitTool(tool),\n )?.[0];\n if (!mcpToolName) return;\n\n throw new Error(\n `MCP toolkit entry \"${mcpToolName}\" requires AISDKToolkit. Use new AISDKToolkit({ toolkit }).tools(...) instead of generativeTools(...).`,\n );\n};\n\ntype AISDKToModelOutputOptions<TArgs, TResult> = Omit<\n Parameters<ToolModelOutputFunction<TArgs, TResult>>[0],\n \"output\"\n> & {\n output: TResult | ModelContentEnvelope<TResult>;\n};\n\nconst toAISDKToModelOutput =\n <TArgs, TResult>(toModelOutput?: ToolModelOutputFunction<TArgs, TResult>) =>\n async (options: AISDKToModelOutputOptions<TArgs, TResult>) => {\n const { result, modelContent } = unwrapModelContentEnvelope(options.output);\n\n if (modelContent !== undefined) {\n return toAISDKContent(modelContent);\n }\n\n if (!toModelOutput) {\n return toAISDKDefaultOutput(result);\n }\n\n const parts = await toModelOutput({\n ...options,\n output: result,\n });\n return toAISDKContent(parts);\n };\n\nconst toServerToolSet = (toolkit: ToolkitDefinition): ToolSet =>\n Object.fromEntries(\n Object.entries(toolkit)\n .filter(\n ([, t]) => t.type !== \"mcp\" && t.type !== \"provider\" && !t.disabled,\n )\n .map(([name, t]) => {\n const execute = t.execute;\n return [\n name,\n {\n ...(t.description !== undefined && { description: t.description }),\n inputSchema: parametersToInputSchema(t.parameters),\n toModelOutput: toAISDKToModelOutput(t.toModelOutput),\n ...(t.providerOptions && { providerOptions: t.providerOptions }),\n ...(execute && {\n execute: (\n args: unknown,\n callOptions: { toolCallId: string; abortSignal?: AbortSignal },\n ) =>\n execute(args as never, {\n toolCallId: callOptions.toolCallId,\n abortSignal: callOptions.abortSignal ?? neverAbort,\n human: humanNotSupported,\n }),\n }),\n },\n ];\n }),\n ) as ToolSet;\n\nconst toProviderToolSet = (toolkit: Toolkit): ToolSet =>\n Object.fromEntries(\n Object.entries(toolkit)\n .filter((entry): entry is [string, ProviderToolkitTool] =>\n isProviderToolkitTool(entry[1]),\n )\n .map(([name, t]) => [\n name,\n {\n type: \"provider\",\n id: t.providerId,\n args: t.args,\n ...(t.parameters && {\n inputSchema: parametersToInputSchema(t.parameters),\n }),\n ...(t.providerOptions && { providerOptions: t.providerOptions }),\n ...(t.supportsDeferredResults !== undefined && {\n supportsDeferredResults: t.supportsDeferredResults,\n }),\n },\n ]),\n ) as ToolSet;\n\ntype ProviderToolkitTool = Extract<Toolkit[string], { type: \"provider\" }>;\n\nconst isProviderToolkitTool = (\n tool: Toolkit[string],\n): tool is ProviderToolkitTool => tool.type === \"provider\" && !tool.disabled;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"generativeTools.js","names":["jsonSchema","ToolSet","MCPClient","MCPClientConfig","createMCPClient","Experimental_StdioMCPTransport","toJSONSchema","Tool","McpServerConfig","ToolModelOutputFunction","McpToolkitToolConfig","Toolkit","ToolkitDefinition","frontendTools","FrontendTools","toAISDKContent","toAISDKDefaultOutput","unwrapModelContentEnvelope","ModelContentEnvelope","EMPTY_SCHEMA","type","const","properties","humanNotSupported","Error","neverAbort","AbortController","signal","MCPConnectionTimeoutPhase","MCPConnectionTimeoutError","createMcpConnectionTimeoutError","name","phase","timeoutMs","withMcpConnectionTimeout","promise","Promise","T","options","config","startedAt","connectionTimeout","undefined","remainingMs","Date","now","timeoutError","timeout","ReturnType","setTimeout","race","_","reject","clearTimeout","parametersToInputSchema","parameters","GenerativeToolsOptions","toolkit","AISDKToolkitOptions","AISDKToolkitToolsOptions","frontend","generativeTools","assertNoMcpToolkitTools","toProviderToolSet","toServerToolSet","AISDKToolkit","mcpClients","Map","constructor","tools","frontendToolSet","mcpToolSet","mcpTools","providerToolSet","serverToolSet","assertNoMcpToolNameCollisions","source","close","clientEntries","entries","clientNames","map","clear","clientResults","allSettled","clientPromise","clients","flatMap","result","index","status","value","closeResults","client","errors","toMcpToolkitError","reason","length","AggregateError","#mcpTools","McpToolSet","toolSets","all","Object","filter","entry","McpToolkitTool","isMcpToolkitTool","tool","mcpClient","server","catch","error","get","delete","create","toolSources","serverName","mcpTool","toolSet","toolName","isDisabledMcpTool","exposedName","prefix","existingServerName","set","sources","#mcpClient","existing","createPromise","toMCPClientConfig","next","then","transport","command","args","env","cwd","url","headers","redirect","ToolkitTool","Record","mcp","prototype","hasOwnProperty","call","disabled","getErrorMessage","message","String","entryName","action","cause","mcpToolName","find","AISDKToModelOutputOptions","Omit","Parameters","TArgs","TResult","output","toAISDKToModelOutput","toModelOutput","modelContent","parts","fromEntries","t","execute","description","inputSchema","providerOptions","callOptions","toolCallId","abortSignal","AbortSignal","human","ProviderToolkitTool","isProviderToolkitTool","id","providerId","supportsDeferredResults","Extract"],"sources":["../../src/tools/generativeTools.ts"],"sourcesContent":["import { jsonSchema, type ToolSet } from \"ai\";\nimport type { MCPClient, MCPClientConfig } from \"@ai-sdk/mcp\";\nimport { createMCPClient } from \"@ai-sdk/mcp\";\nimport { Experimental_StdioMCPTransport } from \"#mcp-stdio\";\nimport {\n toJSONSchema,\n type Tool,\n type McpServerConfig,\n type ToolModelOutputFunction,\n} from \"assistant-stream\";\nimport type {\n McpToolkitToolConfig,\n Toolkit,\n ToolkitDefinition,\n} from \"@assistant-ui/core/react\";\nimport { frontendTools, type FrontendTools } from \"./frontendTools\";\nimport {\n toAISDKContent,\n toAISDKDefaultOutput,\n} from \"../converters/toolOutputConversion\";\nimport {\n unwrapModelContentEnvelope,\n type ModelContentEnvelope,\n} from \"../converters/modelContentEnvelope\";\n\nconst EMPTY_SCHEMA = { type: \"object\" as const, properties: {} };\n\nconst humanNotSupported = (): never => {\n throw new Error(\n \"`human()` is not available during server-side tool execution.\",\n );\n};\n\n// AI SDK leaves `abortSignal` optional; assistant-ui's execute requires one.\nconst neverAbort = new AbortController().signal;\n\ntype MCPConnectionTimeoutPhase = \"connecting\" | \"listing tools\";\n\nclass MCPConnectionTimeoutError extends Error {}\n\nconst createMcpConnectionTimeoutError = (\n name: string,\n phase: MCPConnectionTimeoutPhase,\n timeoutMs: number,\n) =>\n new MCPConnectionTimeoutError(\n `MCP toolkit entry \"${name}\" timed out while ${phase} after ${timeoutMs}ms.`,\n );\n\nconst withMcpConnectionTimeout = async <T>(\n promise: Promise<T>,\n options: {\n name: string;\n config: McpServerConfig;\n phase: MCPConnectionTimeoutPhase;\n startedAt: number;\n },\n): Promise<T> => {\n const timeoutMs = options.config.connectionTimeout;\n if (timeoutMs === undefined) return await promise;\n const remainingMs = timeoutMs - (Date.now() - options.startedAt);\n const timeoutError = () =>\n createMcpConnectionTimeoutError(options.name, options.phase, timeoutMs);\n if (remainingMs <= 0) throw timeoutError();\n\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timeout = setTimeout(() => reject(timeoutError()), remainingMs);\n }),\n ]);\n } finally {\n if (timeout !== undefined) clearTimeout(timeout);\n }\n};\n\nconst parametersToInputSchema = (parameters: Tool[\"parameters\"] | undefined) =>\n jsonSchema(parameters ? toJSONSchema(parameters) : EMPTY_SCHEMA);\n\n/**\n * @deprecated Options for the deprecated {@link generativeTools}. Use\n * {@link AISDKToolkit} with {@link AISDKToolkitOptions} /\n * {@link AISDKToolkitToolsOptions} instead.\n */\nexport interface GenerativeToolsOptions {\n /**\n * The server build of a generative toolkit (schema + server `execute`). Typed\n * as the canonical {@link Toolkit} so callers don't need to cast; the server\n * build carries `execute`, recovered internally as {@link ToolkitDefinition}.\n */\n toolkit: Toolkit;\n /**\n * Tools uploaded by the frontend (the request body's `tools`). Merged in\n * alongside the `toolkit`; a server `execute` from `toolkit` takes precedence\n * over an uploaded entry of the same name.\n */\n frontendTools?: FrontendTools;\n}\n\nexport type AISDKToolkitOptions = {\n toolkit: Toolkit;\n};\n\nexport type AISDKToolkitToolsOptions = {\n /**\n * Tools uploaded by the frontend request body.\n */\n frontend?: FrontendTools;\n};\n\n/**\n * Builds an AI SDK `ToolSet` for server-side use with `streamText` /\n * `generateText` from a generative `toolkit` and the frontend-uploaded tools.\n *\n * Each toolkit tool's `execute` runs on the server. Pair this with the\n * `\"use generative\"` compiler: import the toolkit in a server route (where it\n * resolves to the server build — schema + `execute`, with `render` stripped) and\n * pass it here. Tools without an `execute` are still exposed to the model but\n * left for the client to fulfill. `frontendTools` lets the client contribute\n * tools that aren't in the static toolkit.\n *\n * @deprecated Use {@link AISDKToolkit} instead:\n * `new AISDKToolkit({ toolkit }).tools({ frontend })`. It is a strict superset\n * (it also opens MCP server connections), so it replaces `generativeTools`\n * everywhere. The `frontendTools` option is named `frontend` on `.tools()`, and\n * `.tools()` is async. `generativeTools` will be removed in a future version.\n *\n * @example\n * ```ts\n * // Define once at module scope so any MCP connections pool across requests.\n * const aiToolkit = new AISDKToolkit({ toolkit: docsToolkit });\n *\n * // In your route handler:\n * const { tools } = await req.json();\n * streamText({\n * model,\n * messages,\n * tools: await aiToolkit.tools({ frontend: tools }),\n * });\n * ```\n */\nexport const generativeTools = (options: GenerativeToolsOptions): ToolSet => {\n assertNoMcpToolkitTools(options.toolkit);\n return {\n ...(options.frontendTools ? frontendTools(options.frontendTools) : {}),\n // `toolkit` last so its server-side `execute` wins over an uploaded entry of\n // the same name. The cast recovers the declaration shape — the server build\n // carries `execute`, which the canonical `Toolkit` type erases.\n ...toProviderToolSet(options.toolkit),\n ...toServerToolSet(options.toolkit as ToolkitDefinition),\n };\n};\n\nexport class AISDKToolkit {\n readonly #toolkit: Toolkit;\n readonly #mcpClients = new Map<string, Promise<MCPClient>>();\n\n constructor(options: AISDKToolkitOptions) {\n this.#toolkit = options.toolkit;\n }\n\n async tools(options: AISDKToolkitToolsOptions = {}): Promise<ToolSet> {\n const frontendToolSet = options.frontend\n ? frontendTools(options.frontend)\n : {};\n const mcpToolSet = await this.#mcpTools();\n const providerToolSet = toProviderToolSet(this.#toolkit);\n const serverToolSet = toServerToolSet(this.#toolkit as ToolkitDefinition);\n\n assertNoMcpToolNameCollisions(mcpToolSet, [\n { source: \"frontend\", tools: frontendToolSet },\n { source: \"provider\", tools: providerToolSet },\n { source: \"toolkit\", tools: serverToolSet },\n ]);\n\n return {\n ...frontendToolSet,\n ...mcpToolSet.tools,\n ...providerToolSet,\n ...serverToolSet,\n };\n }\n\n async close(): Promise<void> {\n const clientEntries = [...this.#mcpClients.entries()];\n const clientNames = clientEntries.map(([name]) => name);\n this.#mcpClients.clear();\n const clientResults = await Promise.allSettled(\n clientEntries.map(([, clientPromise]) => clientPromise),\n );\n const clients = clientResults.flatMap((result, index) =>\n result.status === \"fulfilled\"\n ? [[clientNames[index]!, result.value] as const]\n : [],\n );\n const closeResults = await Promise.allSettled(\n clients.map(([, client]) => client.close()),\n );\n const errors = [\n ...clientResults.flatMap((result, index) =>\n result.status === \"rejected\"\n ? [toMcpToolkitError(clientNames[index]!, \"connect\", result.reason)]\n : [],\n ),\n ...closeResults.flatMap((result, index) =>\n result.status === \"rejected\"\n ? [toMcpToolkitError(clients[index]![0], \"close\", result.reason)]\n : [],\n ),\n ];\n if (errors.length === 1) throw errors[0];\n if (errors.length > 1) {\n throw new AggregateError(\n errors,\n \"Failed to close one or more MCP clients\",\n );\n }\n }\n\n async #mcpTools(): Promise<McpToolSet> {\n const toolSets = await Promise.all(\n Object.entries(this.#toolkit)\n .filter((entry): entry is [string, McpToolkitTool] =>\n isMcpToolkitTool(entry[1]),\n )\n .map(async ([name, tool]) => {\n const startedAt = Date.now();\n const clientPromise = this.#mcpClient(name, tool.server, startedAt);\n const client = await clientPromise.catch((error: unknown) => {\n if (error instanceof MCPConnectionTimeoutError) throw error;\n throw toMcpToolkitError(name, \"connect\", error);\n });\n try {\n const tools = await withMcpConnectionTimeout(client.tools(), {\n name,\n config: tool.server,\n phase: \"listing tools\",\n startedAt,\n });\n return [name, tool, tools] as const;\n } catch (error) {\n if (error instanceof MCPConnectionTimeoutError) {\n if (this.#mcpClients.get(name) === clientPromise) {\n this.#mcpClients.delete(name);\n void client.close().catch(() => {});\n }\n throw error;\n }\n throw toMcpToolkitError(name, \"list tools\", error);\n }\n }),\n );\n\n const tools = Object.create(null) as ToolSet;\n const toolSources = new Map<string, string>();\n for (const [serverName, mcpTool, toolSet] of toolSets) {\n for (const [toolName, tool] of Object.entries(toolSet)) {\n if (isDisabledMcpTool(mcpTool.tools?.[toolName])) continue;\n const exposedName = `${mcpTool.prefix ?? \"\"}${toolName}`;\n const existingServerName = toolSources.get(exposedName);\n if (existingServerName) {\n throw new Error(\n `MCP tool name collision: \"${exposedName}\" is exposed by both \"${existingServerName}\" and \"${serverName}\". Rename one of the toolkit entries or expose distinct MCP tool names.`,\n );\n }\n toolSources.set(exposedName, serverName);\n tools[exposedName] = tool as ToolSet[string];\n }\n }\n return { tools, sources: toolSources };\n }\n\n #mcpClient(\n name: string,\n config: McpServerConfig,\n startedAt: number,\n ): Promise<MCPClient> {\n const existing = this.#mcpClients.get(name);\n if (existing) return existing;\n const createPromise = createMCPClient(toMCPClientConfig(config));\n let next: Promise<MCPClient>;\n next = withMcpConnectionTimeout(createPromise, {\n name,\n config,\n phase: \"connecting\",\n startedAt,\n }).catch((error) => {\n if (this.#mcpClients.get(name) === next) {\n this.#mcpClients.delete(name);\n }\n if (error instanceof MCPConnectionTimeoutError) {\n void createPromise.then((client) => client.close()).catch(() => {});\n }\n throw error;\n });\n this.#mcpClients.set(name, next);\n return next;\n }\n}\n\nconst toMCPClientConfig = (config: McpServerConfig): MCPClientConfig => {\n if (config.type === \"stdio\") {\n return {\n transport: new Experimental_StdioMCPTransport({\n command: config.command,\n ...(config.args && { args: [...config.args] }),\n ...(config.env && { env: config.env }),\n ...(config.cwd && { cwd: config.cwd }),\n }),\n };\n }\n\n return {\n transport: {\n type: config.type,\n url: config.url,\n ...(config.headers && { headers: config.headers }),\n ...(config.redirect && { redirect: config.redirect }),\n },\n };\n};\n\ntype ToolkitTool = Toolkit[string];\n\ntype McpToolkitTool = ToolkitTool & {\n type: \"mcp\";\n server: McpServerConfig;\n prefix?: string | undefined;\n tools?: Record<string, McpToolkitToolConfig> | undefined;\n};\n\ntype McpToolSet = {\n tools: ToolSet;\n sources: Map<string, string>;\n};\n\nconst assertNoMcpToolNameCollisions = (\n mcp: McpToolSet,\n toolSets: readonly { source: string; tools: ToolSet }[],\n): void => {\n for (const [toolName, serverName] of mcp.sources) {\n for (const { source, tools } of toolSets) {\n if (!Object.prototype.hasOwnProperty.call(tools, toolName)) continue;\n throw new Error(\n `MCP tool \"${toolName}\" from \"${serverName}\" conflicts with ${source} tool \"${toolName}\". Rename one of the tools so each model-visible tool name is unique.`,\n );\n }\n }\n};\n\nconst isMcpToolkitTool = (tool: ToolkitTool): tool is McpToolkitTool =>\n tool.type === \"mcp\" && !tool.disabled;\n\nconst getErrorMessage = (error: unknown): string =>\n error instanceof Error ? error.message || error.name : String(error);\n\nconst toMcpToolkitError = (\n entryName: string,\n action: \"connect\" | \"list tools\" | \"close\",\n error: unknown,\n): Error => {\n return new Error(\n `MCP toolkit entry \"${entryName}\" failed to ${action}: ${getErrorMessage(error)}`,\n { cause: error },\n );\n};\n\nconst isDisabledMcpTool = (config: McpToolkitToolConfig | undefined): boolean =>\n config?.disabled === true;\n\nconst assertNoMcpToolkitTools = (toolkit: Toolkit): void => {\n const mcpToolName = Object.entries(toolkit).find(([, tool]) =>\n isMcpToolkitTool(tool),\n )?.[0];\n if (!mcpToolName) return;\n\n throw new Error(\n `MCP toolkit entry \"${mcpToolName}\" requires AISDKToolkit. Use new AISDKToolkit({ toolkit }).tools(...) instead of generativeTools(...).`,\n );\n};\n\ntype AISDKToModelOutputOptions<TArgs, TResult> = Omit<\n Parameters<ToolModelOutputFunction<TArgs, TResult>>[0],\n \"output\"\n> & {\n output: TResult | ModelContentEnvelope<TResult>;\n};\n\nconst toAISDKToModelOutput =\n <TArgs, TResult>(toModelOutput?: ToolModelOutputFunction<TArgs, TResult>) =>\n async (options: AISDKToModelOutputOptions<TArgs, TResult>) => {\n const { result, modelContent } = unwrapModelContentEnvelope(options.output);\n\n if (modelContent !== undefined) {\n return toAISDKContent(modelContent);\n }\n\n if (!toModelOutput) {\n return toAISDKDefaultOutput(result);\n }\n\n const parts = await toModelOutput({\n ...options,\n output: result,\n });\n return toAISDKContent(parts);\n };\n\nconst toServerToolSet = (toolkit: ToolkitDefinition): ToolSet =>\n Object.fromEntries(\n Object.entries(toolkit)\n .filter(\n ([, t]) => t.type !== \"mcp\" && t.type !== \"provider\" && !t.disabled,\n )\n .map(([name, t]) => {\n const execute = t.execute;\n return [\n name,\n {\n ...(t.description !== undefined && { description: t.description }),\n inputSchema: parametersToInputSchema(t.parameters),\n toModelOutput: toAISDKToModelOutput(t.toModelOutput),\n ...(t.providerOptions && { providerOptions: t.providerOptions }),\n ...(execute && {\n execute: (\n args: unknown,\n callOptions: { toolCallId: string; abortSignal?: AbortSignal },\n ) =>\n execute(args as never, {\n toolCallId: callOptions.toolCallId,\n abortSignal: callOptions.abortSignal ?? neverAbort,\n human: humanNotSupported,\n }),\n }),\n },\n ];\n }),\n ) as ToolSet;\n\nconst toProviderToolSet = (toolkit: Toolkit): ToolSet =>\n Object.fromEntries(\n Object.entries(toolkit)\n .filter((entry): entry is [string, ProviderToolkitTool] =>\n isProviderToolkitTool(entry[1]),\n )\n .map(([name, t]) => [\n name,\n {\n type: \"provider\",\n id: t.providerId,\n args: t.args,\n ...(t.parameters && {\n inputSchema: parametersToInputSchema(t.parameters),\n }),\n ...(t.providerOptions && { providerOptions: t.providerOptions }),\n ...(t.supportsDeferredResults !== undefined && {\n supportsDeferredResults: t.supportsDeferredResults,\n }),\n },\n ]),\n ) as ToolSet;\n\ntype ProviderToolkitTool = Extract<Toolkit[string], { type: \"provider\" }>;\n\nconst isProviderToolkitTool = (\n tool: Toolkit[string],\n): tool is ProviderToolkitTool => tool.type === \"provider\" && !tool.disabled;\n"],"mappings":"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"}
|
package/dist/usage.d.ts
CHANGED
|
@@ -12,6 +12,14 @@ interface TokenUsageExtractableMessage {
|
|
|
12
12
|
metadata?: unknown;
|
|
13
13
|
}
|
|
14
14
|
declare function getThreadMessageTokenUsage(message: TokenUsageExtractableMessage | undefined): ThreadTokenUsage | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Reads token usage from the newest assistant message that reports any.
|
|
17
|
+
*
|
|
18
|
+
* A route attaches usage through the AI SDK's `messageMetadata` option. Because
|
|
19
|
+
* a thread message carries a fixed metadata shape, the converter moves every
|
|
20
|
+
* other key the route returns into `metadata.custom`, which is where this hook
|
|
21
|
+
* looks.
|
|
22
|
+
*/
|
|
15
23
|
declare function useThreadTokenUsage(): ThreadTokenUsage | undefined;
|
|
16
24
|
//#endregion
|
|
17
25
|
export { ThreadTokenUsage, TokenUsageExtractableMessage, getThreadMessageTokenUsage, useThreadTokenUsage };
|
package/dist/usage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usage.d.ts","names":[],"sources":["../src/usage.ts"],"mappings":";KAIY;EACV;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA;;iBAuHc,2BACd,SAAS,2CACR
|
|
1
|
+
{"version":3,"file":"usage.d.ts","names":[],"sources":["../src/usage.ts"],"mappings":";KAIY;EACV;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA;;iBAuHc,2BACd,SAAS,2CACR;;;;;;;;;iBAsCa,uBAAuB"}
|
package/dist/usage.js
CHANGED
|
@@ -105,6 +105,14 @@ function findLatestMessageWithUsage(messages) {
|
|
|
105
105
|
if (getThreadMessageTokenUsage(message)) return message;
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Reads token usage from the newest assistant message that reports any.
|
|
110
|
+
*
|
|
111
|
+
* A route attaches usage through the AI SDK's `messageMetadata` option. Because
|
|
112
|
+
* a thread message carries a fixed metadata shape, the converter moves every
|
|
113
|
+
* other key the route returns into `metadata.custom`, which is where this hook
|
|
114
|
+
* looks.
|
|
115
|
+
*/
|
|
108
116
|
function useThreadTokenUsage() {
|
|
109
117
|
const $ = c(2);
|
|
110
118
|
const msg = useAuiState(_temp);
|
package/dist/usage.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usage.js","names":["useMemo","useAuiState","ThreadTokenUsage","totalTokens","inputTokens","outputTokens","reasoningTokens","cachedInputTokens","TokenUsageExtractableMessage","role","metadata","UsageRecord","Record","USAGE_KEYS","const","asRecord","value","Array","isArray","undefined","asPositiveTokenCount","Number","isFinite","computeTotalTokens","usage","normalizeUsage","record","result","hasFields","key","count","outputTokenDetails","inputTokenDetails","cacheReadTokens","withComputedTotal","usageFromSteps","steps","sums","present","stepsWithUsage","stepsWithComputableTotal","step","stepTotal","getThreadMessageTokenUsage","message","topLevelUsage","customUsage","custom","findLatestMessageWithUsage","messages","idx","length","useThreadTokenUsage","$","_c","msg","_temp","t0","s","thread"],"sources":["../src/usage.ts"],"sourcesContent":["/// <reference types=\"@assistant-ui/core/react\" />\nimport { useMemo } from \"react\";\nimport { useAuiState } from \"@assistant-ui/store\";\n\nexport type ThreadTokenUsage = {\n totalTokens?: number;\n inputTokens?: number;\n outputTokens?: number;\n reasoningTokens?: number;\n cachedInputTokens?: number;\n};\n\nexport interface TokenUsageExtractableMessage {\n role?: string;\n metadata?: unknown;\n}\n\ntype UsageRecord = Record<string, unknown>;\n\nconst USAGE_KEYS = [\n \"inputTokens\",\n \"outputTokens\",\n \"reasoningTokens\",\n \"cachedInputTokens\",\n \"totalTokens\",\n] as const satisfies (keyof ThreadTokenUsage)[];\n\nfunction asRecord(value: unknown): UsageRecord | undefined {\n if (!value || typeof value !== \"object\" || Array.isArray(value))\n return undefined;\n return value as UsageRecord;\n}\n\nfunction asPositiveTokenCount(value: unknown): number | undefined {\n if (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n return undefined;\n }\n return value;\n}\n\nfunction computeTotalTokens(usage: ThreadTokenUsage): number | undefined {\n if (usage.totalTokens !== undefined) return usage.totalTokens;\n if (usage.inputTokens !== undefined && usage.outputTokens !== undefined) {\n return usage.inputTokens + usage.outputTokens;\n }\n return undefined;\n}\n\nfunction normalizeUsage(value: unknown): ThreadTokenUsage | undefined {\n const record = asRecord(value);\n if (!record) return undefined;\n\n const result: ThreadTokenUsage = {};\n let hasFields = false;\n for (const key of USAGE_KEYS) {\n const count = asPositiveTokenCount(record[key]);\n if (count !== undefined) {\n result[key] = count;\n hasFields = true;\n }\n }\n // AI SDK v7 moved these under token detail objects; v6 kept them top-level.\n if (result.reasoningTokens === undefined) {\n const count = asPositiveTokenCount(\n asRecord(record.outputTokenDetails)?.reasoningTokens,\n );\n if (count !== undefined) {\n result.reasoningTokens = count;\n hasFields = true;\n }\n }\n if (result.cachedInputTokens === undefined) {\n const count = asPositiveTokenCount(\n asRecord(record.inputTokenDetails)?.cacheReadTokens,\n );\n if (count !== undefined) {\n result.cachedInputTokens = count;\n hasFields = true;\n }\n }\n return hasFields ? result : undefined;\n}\n\nfunction withComputedTotal(\n usage: ThreadTokenUsage,\n): ThreadTokenUsage | undefined {\n const totalTokens = computeTotalTokens(usage);\n return { ...usage, ...(totalTokens !== undefined && { totalTokens }) };\n}\n\nfunction usageFromSteps(value: unknown): ThreadTokenUsage | undefined {\n const steps = Array.isArray(value) ? value : [];\n\n const sums: Record<string, number> = {};\n const present: Record<string, boolean> = {};\n let stepsWithUsage = 0;\n let stepsWithComputableTotal = 0;\n\n for (const step of steps) {\n const usage = normalizeUsage(asRecord(step)?.usage);\n if (!usage) continue;\n stepsWithUsage++;\n\n const stepTotal = computeTotalTokens(usage);\n if (stepTotal !== undefined) {\n sums.totalTokens = (sums.totalTokens ?? 0) + stepTotal;\n stepsWithComputableTotal++;\n }\n\n for (const key of USAGE_KEYS) {\n if (key === \"totalTokens\") continue;\n if (usage[key] !== undefined) {\n sums[key] = (sums[key] ?? 0) + usage[key];\n present[key] = true;\n }\n }\n }\n\n if (stepsWithUsage === 0) return undefined;\n\n const result: ThreadTokenUsage = {};\n if (stepsWithComputableTotal === stepsWithUsage) {\n result.totalTokens = sums.totalTokens!;\n }\n for (const key of USAGE_KEYS) {\n if (key === \"totalTokens\") continue;\n if (present[key]) {\n result[key] = sums[key]!;\n }\n }\n return result;\n}\n\nexport function getThreadMessageTokenUsage(\n message: TokenUsageExtractableMessage | undefined,\n): ThreadTokenUsage | undefined {\n if (!message || message.role !== \"assistant\") return undefined;\n\n const metadata = asRecord(message.metadata);\n if (!metadata) return undefined;\n\n const topLevelUsage = normalizeUsage(metadata.usage);\n if (topLevelUsage) return withComputedTotal(topLevelUsage);\n\n const customUsage = normalizeUsage(asRecord(metadata.custom)?.usage);\n if (customUsage) return withComputedTotal(customUsage);\n\n return usageFromSteps(metadata.steps);\n}\n\nfunction findLatestMessageWithUsage(\n messages: readonly TokenUsageExtractableMessage[] | undefined,\n): TokenUsageExtractableMessage | undefined {\n if (!messages) return undefined;\n\n for (let idx = messages.length - 1; idx >= 0; idx -= 1) {\n const message = messages[idx];\n if (getThreadMessageTokenUsage(message)) {\n return message;\n }\n }\n\n return undefined;\n}\n\nexport function useThreadTokenUsage(): ThreadTokenUsage | undefined {\n const msg = useAuiState((s) => findLatestMessageWithUsage(s.thread.messages));\n return useMemo(() => getThreadMessageTokenUsage(msg), [msg]);\n}\n"],"mappings":";;;;AAmBA,MAAMa,aAAa;CACjB;CACA;CACA;CACA;CACA;AAAa;AAGf,SAASE,SAASC,OAAyC;CACzD,IAAI,CAACA,SAAS,OAAOA,UAAU,YAAYC,MAAMC,QAAQF,KAAK,GAC5D,OAAOG,KAAAA;CACT,OAAOH;AACT;AAEA,SAASI,qBAAqBJ,OAAoC;CAChE,IAAI,OAAOA,UAAU,YAAY,CAACK,OAAOC,SAASN,KAAK,KAAKA,QAAQ,GAClE;CAEF,OAAOA;AACT;AAEA,SAASO,mBAAmBC,OAA6C;CACvE,IAAIA,MAAMrB,gBAAgBgB,KAAAA,GAAW,OAAOK,MAAMrB;CAClD,IAAIqB,MAAMpB,gBAAgBe,KAAAA,KAAaK,MAAMnB,iBAAiBc,KAAAA,GAC5D,OAAOK,MAAMpB,cAAcoB,MAAMnB;AAGrC;AAEA,SAASoB,eAAeT,OAA8C;CACpE,MAAMU,SAASX,SAASC,KAAK;CAC7B,IAAI,CAACU,QAAQ,OAAOP,KAAAA;CAEpB,MAAMQ,SAA2B,CAAC;CAClC,IAAIC,YAAY;CAChB,KAAK,MAAMC,OAAOhB,YAAY;EAC5B,MAAMiB,QAAQV,qBAAqBM,OAAOG,IAAI;EAC9C,IAAIC,UAAUX,KAAAA,GAAW;GACvBQ,OAAOE,OAAOC;GACdF,YAAY;EACd;CACF;CAEA,IAAID,OAAOrB,oBAAoBa,KAAAA,GAAW;EACxC,MAAMW,QAAQV,qBACZL,SAASW,OAAOK,kBAAkB,CAAC,EAAEzB,eACvC;EACA,IAAIwB,UAAUX,KAAAA,GAAW;GACvBQ,OAAOrB,kBAAkBwB;GACzBF,YAAY;EACd;CACF;CACA,IAAID,OAAOpB,sBAAsBY,KAAAA,GAAW;EAC1C,MAAMW,QAAQV,qBACZL,SAASW,OAAOM,iBAAiB,CAAC,EAAEC,eACtC;EACA,IAAIH,UAAUX,KAAAA,GAAW;GACvBQ,OAAOpB,oBAAoBuB;GAC3BF,YAAY;EACd;CACF;CACA,OAAOA,YAAYD,SAASR,KAAAA;AAC9B;AAEA,SAASe,kBACPV,OAC8B;CAC9B,MAAMrB,cAAcoB,mBAAmBC,KAAK;CAC5C,OAAO;EAAE,GAAGA;EAAO,GAAIrB,gBAAgBgB,KAAAA,KAAa,EAAEhB,YAAY;CAAG;AACvE;AAEA,SAASgC,eAAenB,OAA8C;CACpE,MAAMoB,QAAQnB,MAAMC,QAAQF,KAAK,IAAIA,QAAQ,CAAA;CAE7C,MAAMqB,OAA+B,CAAC;CACtC,MAAMC,UAAmC,CAAC;CAC1C,IAAIC,iBAAiB;CACrB,IAAIC,2BAA2B;CAE/B,KAAK,MAAMC,QAAQL,OAAO;EACxB,MAAMZ,QAAQC,eAAeV,SAAS0B,IAAI,CAAC,EAAEjB,KAAK;EAClD,IAAI,CAACA,OAAO;EACZe;EAEA,MAAMG,YAAYnB,mBAAmBC,KAAK;EAC1C,IAAIkB,cAAcvB,KAAAA,GAAW;GAC3BkB,KAAKlC,eAAekC,KAAKlC,eAAe,KAAKuC;GAC7CF;EACF;EAEA,KAAK,MAAMX,OAAOhB,YAAY;GAC5B,IAAIgB,QAAQ,eAAe;GAC3B,IAAIL,MAAMK,SAASV,KAAAA,GAAW;IAC5BkB,KAAKR,QAAQQ,KAAKR,QAAQ,KAAKL,MAAMK;IACrCS,QAAQT,OAAO;GACjB;EACF;CACF;CAEA,IAAIU,mBAAmB,GAAG,OAAOpB,KAAAA;CAEjC,MAAMQ,SAA2B,CAAC;CAClC,IAAIa,6BAA6BD,gBAC/BZ,OAAOxB,cAAckC,KAAKlC;CAE5B,KAAK,MAAM0B,OAAOhB,YAAY;EAC5B,IAAIgB,QAAQ,eAAe;EAC3B,IAAIS,QAAQT,MACVF,OAAOE,OAAOQ,KAAKR;CAEvB;CACA,OAAOF;AACT;AAEA,SAAgBgB,2BACdC,SAC8B;CAC9B,IAAI,CAACA,WAAWA,QAAQnC,SAAS,aAAa,OAAOU,KAAAA;CAErD,MAAMT,WAAWK,SAAS6B,QAAQlC,QAAQ;CAC1C,IAAI,CAACA,UAAU,OAAOS,KAAAA;CAEtB,MAAM0B,gBAAgBpB,eAAef,SAASc,KAAK;CACnD,IAAIqB,eAAe,OAAOX,kBAAkBW,aAAa;CAEzD,MAAMC,cAAcrB,eAAeV,SAASL,SAASqC,MAAM,CAAC,EAAEvB,KAAK;CACnE,IAAIsB,aAAa,OAAOZ,kBAAkBY,WAAW;CAErD,OAAOX,eAAezB,SAAS0B,KAAK;AACtC;AAEA,SAASY,2BACPC,UAC0C;CAC1C,IAAI,CAACA,UAAU,OAAO9B,KAAAA;CAEtB,KAAK,IAAI+B,MAAMD,SAASE,SAAS,GAAGD,OAAO,GAAGA,OAAO,GAAG;EACtD,MAAMN,UAAUK,SAASC;EACzB,IAAIP,2BAA2BC,OAAO,GACpC,OAAOA;CAEX;AAGF
|
|
1
|
+
{"version":3,"file":"usage.js","names":["useMemo","useAuiState","ThreadTokenUsage","totalTokens","inputTokens","outputTokens","reasoningTokens","cachedInputTokens","TokenUsageExtractableMessage","role","metadata","UsageRecord","Record","USAGE_KEYS","const","asRecord","value","Array","isArray","undefined","asPositiveTokenCount","Number","isFinite","computeTotalTokens","usage","normalizeUsage","record","result","hasFields","key","count","outputTokenDetails","inputTokenDetails","cacheReadTokens","withComputedTotal","usageFromSteps","steps","sums","present","stepsWithUsage","stepsWithComputableTotal","step","stepTotal","getThreadMessageTokenUsage","message","topLevelUsage","customUsage","custom","findLatestMessageWithUsage","messages","idx","length","useThreadTokenUsage","$","_c","msg","_temp","t0","s","thread"],"sources":["../src/usage.ts"],"sourcesContent":["/// <reference types=\"@assistant-ui/core/react\" />\nimport { useMemo } from \"react\";\nimport { useAuiState } from \"@assistant-ui/store\";\n\nexport type ThreadTokenUsage = {\n totalTokens?: number;\n inputTokens?: number;\n outputTokens?: number;\n reasoningTokens?: number;\n cachedInputTokens?: number;\n};\n\nexport interface TokenUsageExtractableMessage {\n role?: string;\n metadata?: unknown;\n}\n\ntype UsageRecord = Record<string, unknown>;\n\nconst USAGE_KEYS = [\n \"inputTokens\",\n \"outputTokens\",\n \"reasoningTokens\",\n \"cachedInputTokens\",\n \"totalTokens\",\n] as const satisfies (keyof ThreadTokenUsage)[];\n\nfunction asRecord(value: unknown): UsageRecord | undefined {\n if (!value || typeof value !== \"object\" || Array.isArray(value))\n return undefined;\n return value as UsageRecord;\n}\n\nfunction asPositiveTokenCount(value: unknown): number | undefined {\n if (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n return undefined;\n }\n return value;\n}\n\nfunction computeTotalTokens(usage: ThreadTokenUsage): number | undefined {\n if (usage.totalTokens !== undefined) return usage.totalTokens;\n if (usage.inputTokens !== undefined && usage.outputTokens !== undefined) {\n return usage.inputTokens + usage.outputTokens;\n }\n return undefined;\n}\n\nfunction normalizeUsage(value: unknown): ThreadTokenUsage | undefined {\n const record = asRecord(value);\n if (!record) return undefined;\n\n const result: ThreadTokenUsage = {};\n let hasFields = false;\n for (const key of USAGE_KEYS) {\n const count = asPositiveTokenCount(record[key]);\n if (count !== undefined) {\n result[key] = count;\n hasFields = true;\n }\n }\n // AI SDK v7 moved these under token detail objects; v6 kept them top-level.\n if (result.reasoningTokens === undefined) {\n const count = asPositiveTokenCount(\n asRecord(record.outputTokenDetails)?.reasoningTokens,\n );\n if (count !== undefined) {\n result.reasoningTokens = count;\n hasFields = true;\n }\n }\n if (result.cachedInputTokens === undefined) {\n const count = asPositiveTokenCount(\n asRecord(record.inputTokenDetails)?.cacheReadTokens,\n );\n if (count !== undefined) {\n result.cachedInputTokens = count;\n hasFields = true;\n }\n }\n return hasFields ? result : undefined;\n}\n\nfunction withComputedTotal(\n usage: ThreadTokenUsage,\n): ThreadTokenUsage | undefined {\n const totalTokens = computeTotalTokens(usage);\n return { ...usage, ...(totalTokens !== undefined && { totalTokens }) };\n}\n\nfunction usageFromSteps(value: unknown): ThreadTokenUsage | undefined {\n const steps = Array.isArray(value) ? value : [];\n\n const sums: Record<string, number> = {};\n const present: Record<string, boolean> = {};\n let stepsWithUsage = 0;\n let stepsWithComputableTotal = 0;\n\n for (const step of steps) {\n const usage = normalizeUsage(asRecord(step)?.usage);\n if (!usage) continue;\n stepsWithUsage++;\n\n const stepTotal = computeTotalTokens(usage);\n if (stepTotal !== undefined) {\n sums.totalTokens = (sums.totalTokens ?? 0) + stepTotal;\n stepsWithComputableTotal++;\n }\n\n for (const key of USAGE_KEYS) {\n if (key === \"totalTokens\") continue;\n if (usage[key] !== undefined) {\n sums[key] = (sums[key] ?? 0) + usage[key];\n present[key] = true;\n }\n }\n }\n\n if (stepsWithUsage === 0) return undefined;\n\n const result: ThreadTokenUsage = {};\n if (stepsWithComputableTotal === stepsWithUsage) {\n result.totalTokens = sums.totalTokens!;\n }\n for (const key of USAGE_KEYS) {\n if (key === \"totalTokens\") continue;\n if (present[key]) {\n result[key] = sums[key]!;\n }\n }\n return result;\n}\n\nexport function getThreadMessageTokenUsage(\n message: TokenUsageExtractableMessage | undefined,\n): ThreadTokenUsage | undefined {\n if (!message || message.role !== \"assistant\") return undefined;\n\n const metadata = asRecord(message.metadata);\n if (!metadata) return undefined;\n\n const topLevelUsage = normalizeUsage(metadata.usage);\n if (topLevelUsage) return withComputedTotal(topLevelUsage);\n\n const customUsage = normalizeUsage(asRecord(metadata.custom)?.usage);\n if (customUsage) return withComputedTotal(customUsage);\n\n return usageFromSteps(metadata.steps);\n}\n\nfunction findLatestMessageWithUsage(\n messages: readonly TokenUsageExtractableMessage[] | undefined,\n): TokenUsageExtractableMessage | undefined {\n if (!messages) return undefined;\n\n for (let idx = messages.length - 1; idx >= 0; idx -= 1) {\n const message = messages[idx];\n if (getThreadMessageTokenUsage(message)) {\n return message;\n }\n }\n\n return undefined;\n}\n\n/**\n * Reads token usage from the newest assistant message that reports any.\n *\n * A route attaches usage through the AI SDK's `messageMetadata` option. Because\n * a thread message carries a fixed metadata shape, the converter moves every\n * other key the route returns into `metadata.custom`, which is where this hook\n * looks.\n */\nexport function useThreadTokenUsage(): ThreadTokenUsage | undefined {\n const msg = useAuiState((s) => findLatestMessageWithUsage(s.thread.messages));\n return useMemo(() => getThreadMessageTokenUsage(msg), [msg]);\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/ai-sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Vercel AI SDK adapter for assistant-ui",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-sdk",
|
|
@@ -46,14 +46,14 @@
|
|
|
46
46
|
],
|
|
47
47
|
"sideEffects": false,
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@ai-sdk/mcp": "^2.0.
|
|
50
|
-
"@ai-sdk/react": "^4.0.
|
|
51
|
-
"@assistant-ui/core": "^0.3.
|
|
52
|
-
"@assistant-ui/store": "^0.3.
|
|
53
|
-
"@assistant-ui/tap": "^0.9.
|
|
54
|
-
"ai": "^7.0.
|
|
55
|
-
"assistant-cloud": "
|
|
56
|
-
"assistant-stream": "^0.3.
|
|
49
|
+
"@ai-sdk/mcp": "^2.0.45",
|
|
50
|
+
"@ai-sdk/react": "^4.0.96",
|
|
51
|
+
"@assistant-ui/core": "^0.3.19",
|
|
52
|
+
"@assistant-ui/store": "^0.3.13",
|
|
53
|
+
"@assistant-ui/tap": "^0.9.17",
|
|
54
|
+
"ai": "^7.0.93",
|
|
55
|
+
"assistant-cloud": "^0.2.1",
|
|
56
|
+
"assistant-stream": "^0.3.43"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"@types/react": "*",
|
|
@@ -68,14 +68,14 @@
|
|
|
68
68
|
}
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
|
-
"@assistant-ui/x-buildutils": "0.0.
|
|
71
|
+
"@assistant-ui/x-buildutils": "0.0.27",
|
|
72
72
|
"@testing-library/react": "^16.3.3",
|
|
73
73
|
"@types/json-schema": "^7.0.15",
|
|
74
74
|
"@types/react": "^19.2.18",
|
|
75
|
-
"@types/react-dom": "^19.2.
|
|
75
|
+
"@types/react-dom": "^19.2.7",
|
|
76
76
|
"jsdom": "^30.0.1",
|
|
77
77
|
"react": "^19.2.8",
|
|
78
|
-
"vitest": "^
|
|
78
|
+
"vitest": "^5.0.0"
|
|
79
79
|
},
|
|
80
80
|
"publishConfig": {
|
|
81
81
|
"access": "public",
|
|
@@ -1,41 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
MessageStorageEntry,
|
|
6
|
-
} from "@assistant-ui/core";
|
|
7
|
-
|
|
8
|
-
// Storage format for AI SDK messages - just the UIMessage
|
|
9
|
-
export type AISDKStorageFormat = Omit<UIMessage, "id">;
|
|
10
|
-
|
|
11
|
-
export const aiSDKV6FormatAdapter: MessageFormatAdapter<
|
|
12
|
-
UIMessage,
|
|
13
|
-
AISDKStorageFormat
|
|
14
|
-
> = {
|
|
15
|
-
format: "ai-sdk/v6",
|
|
16
|
-
|
|
17
|
-
encode({
|
|
18
|
-
message: { id, parts, ...message },
|
|
19
|
-
}: MessageFormatItem<UIMessage>): AISDKStorageFormat {
|
|
20
|
-
return {
|
|
21
|
-
...message,
|
|
22
|
-
parts,
|
|
23
|
-
};
|
|
24
|
-
},
|
|
25
|
-
|
|
26
|
-
decode(
|
|
27
|
-
stored: MessageStorageEntry<AISDKStorageFormat>,
|
|
28
|
-
): MessageFormatItem<UIMessage> {
|
|
29
|
-
return {
|
|
30
|
-
parentId: stored.parent_id,
|
|
31
|
-
message: {
|
|
32
|
-
id: stored.id,
|
|
33
|
-
...stored.content,
|
|
34
|
-
},
|
|
35
|
-
};
|
|
36
|
-
},
|
|
37
|
-
|
|
38
|
-
getId(message: UIMessage): string {
|
|
39
|
-
return message.id;
|
|
40
|
-
},
|
|
41
|
-
};
|
|
1
|
+
export {
|
|
2
|
+
aiSDKV6FormatAdapter,
|
|
3
|
+
type AISDKStorageFormat,
|
|
4
|
+
} from "assistant-cloud/ai-sdk";
|
|
@@ -48,6 +48,20 @@ describe("AISDKMessageConverter", () => {
|
|
|
48
48
|
expect(converted[0]?.metadata).not.toHaveProperty("usage");
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
+
it("keeps modality metadata at the top level", () => {
|
|
52
|
+
const converted = AISDKMessageConverter.toThreadMessages([
|
|
53
|
+
{
|
|
54
|
+
id: "a1",
|
|
55
|
+
role: "assistant",
|
|
56
|
+
parts: [{ type: "text", text: "yo" }],
|
|
57
|
+
metadata: { modality: "voice" },
|
|
58
|
+
},
|
|
59
|
+
] as any);
|
|
60
|
+
|
|
61
|
+
expect(converted[0]?.metadata.modality).toBe("voice");
|
|
62
|
+
expect(converted[0]?.metadata.custom).not.toHaveProperty("modality");
|
|
63
|
+
});
|
|
64
|
+
|
|
51
65
|
it("does not flag messages when no optimistic id is provided", () => {
|
|
52
66
|
const converted = AISDKMessageConverter.toThreadMessages([
|
|
53
67
|
{ id: "a1", role: "assistant", parts: [{ type: "text", text: "yo" }] },
|
|
@@ -394,6 +408,108 @@ describe("AISDKMessageConverter", () => {
|
|
|
394
408
|
});
|
|
395
409
|
});
|
|
396
410
|
|
|
411
|
+
it("preserves producer-defined approval fields and gives prompt precedence", () => {
|
|
412
|
+
const descriptor = { scope: "account:deploy" };
|
|
413
|
+
const converted = AISDKMessageConverter.toThreadMessages([
|
|
414
|
+
{
|
|
415
|
+
id: "a1",
|
|
416
|
+
role: "assistant",
|
|
417
|
+
parts: [
|
|
418
|
+
{
|
|
419
|
+
type: "tool-deploy",
|
|
420
|
+
toolCallId: "tc-1",
|
|
421
|
+
state: "approval-responded",
|
|
422
|
+
input: { environment: "production" },
|
|
423
|
+
approval: {
|
|
424
|
+
id: "approval-1",
|
|
425
|
+
approved: true,
|
|
426
|
+
reason: "approved by operator",
|
|
427
|
+
prompt: "Deploy to production?",
|
|
428
|
+
descriptor,
|
|
429
|
+
requestReason: "Production access requires approval",
|
|
430
|
+
signature: "signed-approval",
|
|
431
|
+
futureField: "preserved",
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
],
|
|
435
|
+
} as any,
|
|
436
|
+
]);
|
|
437
|
+
|
|
438
|
+
const toolCall = converted[0]?.content.find(
|
|
439
|
+
(part): part is any => part.type === "tool-call",
|
|
440
|
+
);
|
|
441
|
+
expect(toolCall?.approval).toEqual({
|
|
442
|
+
id: "approval-1",
|
|
443
|
+
approved: true,
|
|
444
|
+
reason: "approved by operator",
|
|
445
|
+
prompt: "Deploy to production?",
|
|
446
|
+
descriptor,
|
|
447
|
+
requestReason: "Production access requires approval",
|
|
448
|
+
signature: "signed-approval",
|
|
449
|
+
futureField: "preserved",
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("drops fields the AI SDK cannot answer and uses requestReason as the prompt", () => {
|
|
454
|
+
const converted = AISDKMessageConverter.toThreadMessages([
|
|
455
|
+
{
|
|
456
|
+
id: "a1",
|
|
457
|
+
role: "assistant",
|
|
458
|
+
parts: [
|
|
459
|
+
{
|
|
460
|
+
type: "tool-deploy",
|
|
461
|
+
toolCallId: "tc-1",
|
|
462
|
+
state: "approval-requested",
|
|
463
|
+
input: {},
|
|
464
|
+
approval: {
|
|
465
|
+
id: "approval-1",
|
|
466
|
+
display: "select",
|
|
467
|
+
allowFreeform: true,
|
|
468
|
+
options: [{ id: "once", kind: "allow-once" }],
|
|
469
|
+
optionId: "once",
|
|
470
|
+
text: "an answer",
|
|
471
|
+
resolution: "cancelled",
|
|
472
|
+
requestReason: "kept",
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
],
|
|
476
|
+
} as any,
|
|
477
|
+
]);
|
|
478
|
+
|
|
479
|
+
const toolCall = converted[0]?.content.find(
|
|
480
|
+
(part): part is any => part.type === "tool-call",
|
|
481
|
+
);
|
|
482
|
+
expect(toolCall?.approval).toEqual({
|
|
483
|
+
id: "approval-1",
|
|
484
|
+
prompt: "kept",
|
|
485
|
+
resolution: "cancelled",
|
|
486
|
+
requestReason: "kept",
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("drops a resolution the core contract does not declare", () => {
|
|
491
|
+
const converted = AISDKMessageConverter.toThreadMessages([
|
|
492
|
+
{
|
|
493
|
+
id: "a1",
|
|
494
|
+
role: "assistant",
|
|
495
|
+
parts: [
|
|
496
|
+
{
|
|
497
|
+
type: "tool-deploy",
|
|
498
|
+
toolCallId: "tc-1",
|
|
499
|
+
state: "approval-requested",
|
|
500
|
+
input: {},
|
|
501
|
+
approval: { id: "approval-1", resolution: "whatever" },
|
|
502
|
+
},
|
|
503
|
+
],
|
|
504
|
+
} as any,
|
|
505
|
+
]);
|
|
506
|
+
|
|
507
|
+
const toolCall = converted[0]?.content.find(
|
|
508
|
+
(part): part is any => part.type === "tool-call",
|
|
509
|
+
);
|
|
510
|
+
expect(toolCall?.approval).toEqual({ id: "approval-1" });
|
|
511
|
+
});
|
|
512
|
+
|
|
397
513
|
it("strips closing delimiters from streaming tool argsText", () => {
|
|
398
514
|
const converted = AISDKMessageConverter.toThreadMessages([
|
|
399
515
|
{
|
|
@@ -40,6 +40,7 @@ const THREAD_METADATA_KEYS = new Set([
|
|
|
40
40
|
"timing",
|
|
41
41
|
"submittedFeedback",
|
|
42
42
|
"isOptimistic",
|
|
43
|
+
"modality",
|
|
43
44
|
"custom",
|
|
44
45
|
]);
|
|
45
46
|
|
|
@@ -154,30 +155,50 @@ function extractMcpAppMetadata(
|
|
|
154
155
|
|
|
155
156
|
function getToolApprovalAndInterrupt(
|
|
156
157
|
part: {
|
|
157
|
-
approval?:
|
|
158
|
-
| {
|
|
159
|
-
id: string;
|
|
160
|
-
approved?: boolean;
|
|
161
|
-
reason?: string;
|
|
162
|
-
isAutomatic?: boolean;
|
|
163
|
-
}
|
|
164
|
-
| undefined;
|
|
158
|
+
approval?: Record<string, unknown> | undefined;
|
|
165
159
|
},
|
|
166
160
|
toolStatus: { type: string; payload?: unknown } | undefined,
|
|
167
161
|
): {
|
|
168
162
|
approval?: NonNullable<ToolCallMessagePart["approval"]>;
|
|
169
163
|
interrupt?: NonNullable<ToolCallMessagePart["interrupt"]>;
|
|
170
164
|
} {
|
|
171
|
-
if (part.approval
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
165
|
+
if (part.approval) {
|
|
166
|
+
// The AI SDK sends only id, approved and reason back to the server, so a
|
|
167
|
+
// request shape promising any other answer would render controls whose
|
|
168
|
+
// response cannot travel.
|
|
169
|
+
const {
|
|
170
|
+
id,
|
|
171
|
+
prompt,
|
|
172
|
+
approved,
|
|
173
|
+
reason,
|
|
174
|
+
isAutomatic,
|
|
175
|
+
resolution,
|
|
176
|
+
display,
|
|
177
|
+
allowFreeform,
|
|
178
|
+
options,
|
|
179
|
+
optionId,
|
|
180
|
+
text,
|
|
181
|
+
...additionalApprovalFields
|
|
182
|
+
} = part.approval;
|
|
183
|
+
const requestReason = additionalApprovalFields.requestReason;
|
|
184
|
+
if (typeof id === "string")
|
|
185
|
+
return {
|
|
186
|
+
approval: {
|
|
187
|
+
...additionalApprovalFields,
|
|
188
|
+
id,
|
|
189
|
+
...(typeof prompt === "string"
|
|
190
|
+
? { prompt }
|
|
191
|
+
: typeof requestReason === "string"
|
|
192
|
+
? { prompt: requestReason }
|
|
193
|
+
: {}),
|
|
194
|
+
...(typeof approved === "boolean" && { approved }),
|
|
195
|
+
...(typeof reason === "string" && { reason }),
|
|
196
|
+
...(isAutomatic === true && { isAutomatic: true }),
|
|
197
|
+
...((resolution === "cancelled" || resolution === "expired") && {
|
|
198
|
+
resolution,
|
|
199
|
+
}),
|
|
200
|
+
} as NonNullable<ToolCallMessagePart["approval"]>,
|
|
201
|
+
};
|
|
181
202
|
}
|
|
182
203
|
|
|
183
204
|
if (toolStatus?.type === "interrupt") {
|
|
@@ -1,47 +1,68 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { render, waitFor } from "@testing-library/react";
|
|
5
|
-
import { StrictMode } from "react";
|
|
3
|
+
import { StrictMode, type ReactNode } from "react";
|
|
4
|
+
import { act, render, waitFor } from "@testing-library/react";
|
|
6
5
|
import { AuiConfig, AuiProvider, useAui } from "@assistant-ui/store";
|
|
7
|
-
import {
|
|
6
|
+
import type { ChatTransport, UIMessage } from "ai";
|
|
8
7
|
import { describe, expect, it } from "vitest";
|
|
9
8
|
import { AISDKChat } from "./AISDKChat";
|
|
10
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
createCancellableTransport,
|
|
11
|
+
createStreamHarness,
|
|
12
|
+
} from "./__tests__/controlled-transport";
|
|
11
13
|
|
|
12
14
|
describe("AISDKChat React integration", () => {
|
|
13
|
-
it("
|
|
14
|
-
const { transport, getCancelCount
|
|
15
|
-
|
|
16
|
-
let isRunning = () => false;
|
|
17
|
-
|
|
18
|
-
const SendOnMount = () => {
|
|
19
|
-
const aui = useAui();
|
|
20
|
-
isRunning = () => aui.thread.getState().isRunning;
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
if (started) return;
|
|
23
|
-
started = true;
|
|
24
|
-
flushTapSync(() => aui.composer.setText("keep streaming"));
|
|
25
|
-
flushTapSync(() => aui.composer.send());
|
|
26
|
-
}, [aui]);
|
|
27
|
-
return null;
|
|
28
|
-
};
|
|
15
|
+
it("aborts the in-flight transport after a real unmount", async () => {
|
|
16
|
+
const { transport, getCancelCount } = createCancellableTransport();
|
|
17
|
+
const { Probe, send, isRunning } = createStreamHarness();
|
|
29
18
|
|
|
30
19
|
const view = render(
|
|
31
20
|
<StrictMode>
|
|
32
21
|
<AuiProvider config={AuiConfig({ threads: AISDKChat({ transport }) })}>
|
|
33
|
-
<
|
|
22
|
+
<Probe />
|
|
34
23
|
</AuiProvider>
|
|
35
24
|
</StrictMode>,
|
|
36
25
|
);
|
|
37
26
|
|
|
38
|
-
await
|
|
39
|
-
|
|
40
|
-
|
|
27
|
+
await act(async () => send());
|
|
28
|
+
await waitFor(() => expect(isRunning()).toBe(true));
|
|
29
|
+
// the Strict Mode double mount already ran a host cleanup by now
|
|
30
|
+
expect(getCancelCount()).toBe(0);
|
|
41
31
|
|
|
42
32
|
view.unmount();
|
|
43
|
-
await
|
|
33
|
+
await waitFor(() => expect(getCancelCount()).toBe(1));
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("AISDKChat legacy useAui host integration", () => {
|
|
38
|
+
const LegacyProvider = ({
|
|
39
|
+
transport,
|
|
40
|
+
children,
|
|
41
|
+
}: {
|
|
42
|
+
transport: ChatTransport<UIMessage>;
|
|
43
|
+
children: ReactNode;
|
|
44
|
+
}) => {
|
|
45
|
+
const aui = useAui(AuiConfig({ threads: AISDKChat({ transport }) }));
|
|
46
|
+
return <AuiProvider value={aui}>{children}</AuiProvider>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
it("aborts the in-flight transport after a real unmount", async () => {
|
|
50
|
+
const { transport, getCancelCount } = createCancellableTransport();
|
|
51
|
+
const { Probe, send, isRunning } = createStreamHarness();
|
|
52
|
+
|
|
53
|
+
const view = render(
|
|
54
|
+
<StrictMode>
|
|
55
|
+
<LegacyProvider transport={transport}>
|
|
56
|
+
<Probe />
|
|
57
|
+
</LegacyProvider>
|
|
58
|
+
</StrictMode>,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
await act(async () => send());
|
|
62
|
+
await waitFor(() => expect(isRunning()).toBe(true));
|
|
44
63
|
expect(getCancelCount()).toBe(0);
|
|
45
|
-
|
|
64
|
+
|
|
65
|
+
view.unmount();
|
|
66
|
+
await waitFor(() => expect(getCancelCount()).toBe(1));
|
|
46
67
|
});
|
|
47
68
|
});
|