@assistant-ui/ai-sdk 0.0.10 → 0.0.12

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.
Files changed (124) hide show
  1. package/dist/converters/convertMessage.d.ts +8 -6
  2. package/dist/converters/convertMessage.d.ts.map +1 -1
  3. package/dist/converters/convertMessage.js +106 -28
  4. package/dist/converters/convertMessage.js.map +1 -1
  5. package/dist/converters/toolApprovalAnswers.d.ts +3 -0
  6. package/dist/converters/toolApprovalAnswers.d.ts.map +1 -0
  7. package/dist/converters/toolApprovalAnswers.js +17 -0
  8. package/dist/converters/toolApprovalAnswers.js.map +1 -0
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/index.native.d.ts +1 -1
  13. package/dist/index.native.d.ts.map +1 -1
  14. package/dist/index.native.js +1 -1
  15. package/dist/model-context/injectInteractableContext.d.ts +3 -0
  16. package/dist/model-context/injectInteractableContext.d.ts.map +1 -1
  17. package/dist/model-context/injectInteractableContext.js +3 -0
  18. package/dist/model-context/injectInteractableContext.js.map +1 -1
  19. package/dist/model-context/injectQuoteContext.d.ts +1 -0
  20. package/dist/model-context/injectQuoteContext.d.ts.map +1 -1
  21. package/dist/model-context/injectQuoteContext.js +1 -0
  22. package/dist/model-context/injectQuoteContext.js.map +1 -1
  23. package/dist/runtime/AISDKChat.js +1 -1
  24. package/dist/runtime/AISDKThreads.d.ts +5 -0
  25. package/dist/runtime/AISDKThreads.d.ts.map +1 -1
  26. package/dist/runtime/AISDKThreads.js +37 -4
  27. package/dist/runtime/AISDKThreads.js.map +1 -1
  28. package/dist/runtime/DynamicChatTransport.d.ts +49 -0
  29. package/dist/runtime/DynamicChatTransport.d.ts.map +1 -0
  30. package/dist/runtime/DynamicChatTransport.js +147 -0
  31. package/dist/runtime/DynamicChatTransport.js.map +1 -0
  32. package/dist/runtime/getResumableAdapter.d.ts +5 -0
  33. package/dist/runtime/getResumableAdapter.d.ts.map +1 -0
  34. package/dist/runtime/getResumableAdapter.js +12 -0
  35. package/dist/runtime/getResumableAdapter.js.map +1 -0
  36. package/dist/runtime/sdkIdentity.js +1 -1
  37. package/dist/runtime/toolHistoryCodec.d.ts +20 -0
  38. package/dist/runtime/toolHistoryCodec.d.ts.map +1 -0
  39. package/dist/runtime/toolHistoryCodec.js +107 -0
  40. package/dist/runtime/toolHistoryCodec.js.map +1 -0
  41. package/dist/runtime/useAISDKRuntime.d.ts +4 -1
  42. package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
  43. package/dist/runtime/useAISDKRuntime.js +154 -155
  44. package/dist/runtime/useAISDKRuntime.js.map +1 -1
  45. package/dist/runtime/useChatRuntime.d.ts +14 -2
  46. package/dist/runtime/useChatRuntime.d.ts.map +1 -1
  47. package/dist/runtime/useChatRuntime.js +12 -3
  48. package/dist/runtime/useChatRuntime.js.map +1 -1
  49. package/dist/runtime/useChatThread.d.ts +3 -2
  50. package/dist/runtime/useChatThread.d.ts.map +1 -1
  51. package/dist/runtime/useChatThread.js +56 -36
  52. package/dist/runtime/useChatThread.js.map +1 -1
  53. package/dist/runtime/useDynamicChatTransport.d.ts +4 -0
  54. package/dist/runtime/useDynamicChatTransport.d.ts.map +1 -0
  55. package/dist/runtime/useDynamicChatTransport.js +64 -0
  56. package/dist/runtime/useDynamicChatTransport.js.map +1 -0
  57. package/dist/runtime/useExternalHistory.d.ts.map +1 -1
  58. package/dist/runtime/useExternalHistory.js +11 -105
  59. package/dist/runtime/useExternalHistory.js.map +1 -1
  60. package/dist/runtime/useResourceCleanup.js +1 -1
  61. package/dist/runtime/useStreamingTiming.js +2 -2
  62. package/dist/runtime/useStreamingTiming.js.map +1 -1
  63. package/dist/tools/generativeTools.d.ts +2 -1
  64. package/dist/tools/generativeTools.d.ts.map +1 -1
  65. package/dist/tools/generativeTools.js +17 -6
  66. package/dist/tools/generativeTools.js.map +1 -1
  67. package/dist/usage.js +1 -1
  68. package/dist/utils/sliceMessagesUntil.d.ts.map +1 -1
  69. package/dist/utils/sliceMessagesUntil.js +1 -2
  70. package/dist/utils/sliceMessagesUntil.js.map +1 -1
  71. package/package.json +12 -10
  72. package/src/converters/convertMessage.test.ts +350 -2
  73. package/src/converters/convertMessage.tool-args-status.test.tsx +185 -0
  74. package/src/converters/convertMessage.ts +154 -23
  75. package/src/converters/toCreateMessage.test.ts +27 -0
  76. package/src/converters/toolApprovalAnswers.ts +27 -0
  77. package/src/index.native.ts +1 -1
  78. package/src/index.ts +1 -1
  79. package/src/model-context/injectInteractableContext.ts +3 -0
  80. package/src/model-context/injectQuoteContext.ts +1 -0
  81. package/src/runtime/AISDKChat.integration.test.tsx +57 -2
  82. package/src/runtime/AISDKThreads.cloud.test.ts +3 -0
  83. package/src/runtime/AISDKThreads.test.ts +181 -0
  84. package/src/runtime/AISDKThreads.ts +31 -4
  85. package/src/runtime/DynamicChatTransport.test.ts +203 -0
  86. package/src/runtime/DynamicChatTransport.ts +273 -0
  87. package/src/runtime/__tests__/controlled-transport.ts +3 -0
  88. package/src/runtime/getResumableAdapter.ts +16 -0
  89. package/src/runtime/toolHistoryCodec.test.ts +161 -0
  90. package/src/runtime/toolHistoryCodec.ts +207 -0
  91. package/src/runtime/useAISDKRuntime.approval.test.tsx +12 -0
  92. package/src/runtime/useAISDKRuntime.reload.test.tsx +219 -0
  93. package/src/runtime/useAISDKRuntime.test.ts +538 -5
  94. package/src/runtime/useAISDKRuntime.ts +161 -51
  95. package/src/runtime/useChatRuntime.integration.test.tsx +319 -4
  96. package/src/runtime/useChatRuntime.local-storage.test.tsx +123 -0
  97. package/src/runtime/useChatRuntime.test.ts +107 -1
  98. package/src/runtime/useChatRuntime.ts +26 -4
  99. package/src/runtime/useChatThread.binding.test.tsx +143 -0
  100. package/src/runtime/useChatThread.ts +98 -81
  101. package/src/runtime/useDynamicChatTransport.ts +26 -0
  102. package/src/runtime/useExternalHistory.test.ts +205 -0
  103. package/src/runtime/useExternalHistory.ts +14 -206
  104. package/src/runtime/useStreamingTiming.ts +2 -2
  105. package/src/tools/generativeTools.test.ts +190 -2
  106. package/src/tools/generativeTools.ts +28 -8
  107. package/src/utils/sliceMessagesUntil.test.ts +2 -6
  108. package/src/utils/sliceMessagesUntil.ts +1 -5
  109. package/dist/converters/modelContentEnvelope.d.ts +0 -14
  110. package/dist/converters/modelContentEnvelope.d.ts.map +0 -1
  111. package/dist/converters/modelContentEnvelope.js +0 -22
  112. package/dist/converters/modelContentEnvelope.js.map +0 -1
  113. package/dist/converters/toolOutputConversion.d.ts +0 -26
  114. package/dist/converters/toolOutputConversion.d.ts.map +0 -1
  115. package/dist/converters/toolOutputConversion.js +0 -31
  116. package/dist/converters/toolOutputConversion.js.map +0 -1
  117. package/dist/tools/frontendTools.d.ts +0 -30
  118. package/dist/tools/frontendTools.d.ts.map +0 -1
  119. package/dist/tools/frontendTools.js +0 -33
  120. package/dist/tools/frontendTools.js.map +0 -1
  121. package/src/converters/modelContentEnvelope.ts +0 -41
  122. package/src/converters/toolOutputConversion.ts +0 -26
  123. package/src/tools/frontendTools.test.ts +0 -205
  124. package/src/tools/frontendTools.ts +0 -83
@@ -12,10 +12,7 @@ import type {
12
12
  Unstable_ToolInteractionLog,
13
13
  } from "@assistant-ui/core";
14
14
  import { getExternalStoreMessages } from "@assistant-ui/core";
15
- import {
16
- MessageRepository,
17
- readToolInteractionLog,
18
- } from "@assistant-ui/core/internal";
15
+ import { MessageRepository } from "@assistant-ui/core/internal";
19
16
  import { useAui } from "@assistant-ui/store";
20
17
  import {
21
18
  useRef,
@@ -25,6 +22,14 @@ import {
25
22
  useCallback,
26
23
  useMemo,
27
24
  } from "react";
25
+ import {
26
+ addToolData,
27
+ collectToolApprovalResponses,
28
+ collectToolArtifacts,
29
+ collectToolInteractions,
30
+ restoreToolData,
31
+ type StoredToolApprovalResponse,
32
+ } from "./toolHistoryCodec";
28
33
 
29
34
  export const toExportedMessageRepository = <TMessage>(
30
35
  toThreadMessages: (messages: TMessage[]) => ThreadMessage[],
@@ -71,206 +76,6 @@ const isTerminalMessage = (message: ThreadMessage) =>
71
76
  message.status.type === "complete" ||
72
77
  message.status.type === "incomplete";
73
78
 
74
- const TOOL_ARTIFACTS_METADATA_KEY = "__aui_toolArtifacts";
75
- const TOOL_INTERACTIONS_METADATA_KEY = "__aui_toolInteractions";
76
- const TOOL_APPROVAL_RESPONSES_METADATA_KEY = "__aui_toolApprovalResponses";
77
-
78
- type StoredToolApprovalResponse = Omit<
79
- RespondToToolApprovalOptions,
80
- "approvalId"
81
- >;
82
-
83
- const isRecord = (value: unknown): value is Record<string, unknown> =>
84
- value !== null && typeof value === "object" && !Array.isArray(value);
85
-
86
- const collectToolArtifacts = (
87
- message: ThreadMessage,
88
- toolArtifacts: ReadonlyMap<string, unknown> | undefined,
89
- ) => {
90
- if (!toolArtifacts) return undefined;
91
- const entries = message.content.flatMap((part) => {
92
- if (part.type !== "tool-call") return [];
93
- const artifact = toolArtifacts.get(part.toolCallId);
94
- return artifact === undefined ? [] : [[part.toolCallId, artifact] as const];
95
- });
96
- return entries.length > 0 ? Object.fromEntries(entries) : undefined;
97
- };
98
-
99
- const collectToolInteractions = (
100
- message: ThreadMessage,
101
- toolInteractions:
102
- | ReadonlyMap<string, Unstable_ToolInteractionLog>
103
- | undefined,
104
- ) => {
105
- if (!toolInteractions) return undefined;
106
- const entries = message.content.flatMap((part) => {
107
- if (part.type !== "tool-call") return [];
108
- const interactions = toolInteractions.get(part.toolCallId);
109
- return interactions === undefined
110
- ? []
111
- : [[part.toolCallId, interactions] as const];
112
- });
113
- return entries.length > 0 ? Object.fromEntries(entries) : undefined;
114
- };
115
-
116
- const collectToolApprovalResponses = (
117
- message: ThreadMessage,
118
- toolApprovalResponses:
119
- | ReadonlyMap<string, RespondToToolApprovalOptions>
120
- | undefined,
121
- ) => {
122
- if (!toolApprovalResponses) return undefined;
123
- const entries = message.content.flatMap((part) => {
124
- if (part.type !== "tool-call" || !part.approval) return [];
125
- const response = toolApprovalResponses.get(part.approval.id);
126
- if (!response) return [];
127
- return [
128
- [
129
- part.approval.id,
130
- {
131
- approved: response.approved,
132
- ...(response.optionId != null && { optionId: response.optionId }),
133
- ...(response.text != null && { text: response.text }),
134
- ...(response.reason != null && { reason: response.reason }),
135
- },
136
- ] as const,
137
- ];
138
- });
139
- return entries.length > 0 ? Object.fromEntries(entries) : undefined;
140
- };
141
-
142
- const addToolData = <TMessage>(
143
- message: TMessage,
144
- toolArtifacts: Record<string, unknown> | undefined,
145
- toolInteractions: Record<string, Unstable_ToolInteractionLog> | undefined,
146
- toolApprovalResponses: Record<string, StoredToolApprovalResponse> | undefined,
147
- ): TMessage => {
148
- if (
149
- (!toolArtifacts && !toolInteractions && !toolApprovalResponses) ||
150
- !isRecord(message) ||
151
- !Array.isArray(message.parts)
152
- )
153
- return message;
154
- const toolCallIds = message.parts.flatMap((part) => {
155
- if (!isRecord(part) || typeof part.toolCallId !== "string") return [];
156
- return [part.toolCallId];
157
- });
158
- const artifacts = toolArtifacts
159
- ? Object.fromEntries(
160
- toolCallIds.flatMap((toolCallId) =>
161
- Object.hasOwn(toolArtifacts, toolCallId)
162
- ? [[toolCallId, toolArtifacts[toolCallId]] as const]
163
- : [],
164
- ),
165
- )
166
- : undefined;
167
- const interactions = toolInteractions
168
- ? Object.fromEntries(
169
- toolCallIds.flatMap((toolCallId) =>
170
- Object.hasOwn(toolInteractions, toolCallId)
171
- ? [[toolCallId, toolInteractions[toolCallId]] as const]
172
- : [],
173
- ),
174
- )
175
- : undefined;
176
- const approvalIds = message.parts.flatMap((part) => {
177
- if (!isRecord(part) || !isRecord(part.approval)) return [];
178
- const approvalId = part.approval.id;
179
- return typeof approvalId === "string" ? [approvalId] : [];
180
- });
181
- const approvalResponses = toolApprovalResponses
182
- ? Object.fromEntries(
183
- approvalIds.flatMap((approvalId) =>
184
- Object.hasOwn(toolApprovalResponses, approvalId)
185
- ? [[approvalId, toolApprovalResponses[approvalId]] as const]
186
- : [],
187
- ),
188
- )
189
- : undefined;
190
- const hasArtifacts = !!artifacts && Object.keys(artifacts).length > 0;
191
- const hasInteractions =
192
- !!interactions && Object.keys(interactions).length > 0;
193
- const hasApprovalResponses =
194
- !!approvalResponses && Object.keys(approvalResponses).length > 0;
195
- if (!hasArtifacts && !hasInteractions && !hasApprovalResponses)
196
- return message;
197
- const metadata = isRecord(message.metadata) ? message.metadata : {};
198
- return {
199
- ...message,
200
- metadata: {
201
- ...metadata,
202
- ...(hasArtifacts && { [TOOL_ARTIFACTS_METADATA_KEY]: artifacts }),
203
- ...(hasInteractions && {
204
- [TOOL_INTERACTIONS_METADATA_KEY]: interactions,
205
- }),
206
- ...(hasApprovalResponses && {
207
- [TOOL_APPROVAL_RESPONSES_METADATA_KEY]: approvalResponses,
208
- }),
209
- },
210
- } as TMessage;
211
- };
212
-
213
- const restoreToolData = <TMessage>(
214
- message: TMessage,
215
- toolArtifacts: Map<string, unknown> | undefined,
216
- toolInteractions: Map<string, Unstable_ToolInteractionLog> | undefined,
217
- toolApprovalResponses: Map<string, RespondToToolApprovalOptions> | undefined,
218
- ): TMessage => {
219
- if (!isRecord(message) || !isRecord(message.metadata)) return message;
220
- const metadata = message.metadata;
221
- const hasArtifacts = Object.hasOwn(metadata, TOOL_ARTIFACTS_METADATA_KEY);
222
- const hasInteractions = Object.hasOwn(
223
- metadata,
224
- TOOL_INTERACTIONS_METADATA_KEY,
225
- );
226
- const hasApprovalResponses = Object.hasOwn(
227
- metadata,
228
- TOOL_APPROVAL_RESPONSES_METADATA_KEY,
229
- );
230
- if (!hasArtifacts && !hasInteractions && !hasApprovalResponses)
231
- return message;
232
- const artifacts = metadata[TOOL_ARTIFACTS_METADATA_KEY];
233
- if (toolArtifacts && isRecord(artifacts)) {
234
- for (const [toolCallId, artifact] of Object.entries(artifacts)) {
235
- toolArtifacts.set(toolCallId, artifact);
236
- }
237
- }
238
- const interactions = metadata[TOOL_INTERACTIONS_METADATA_KEY];
239
- if (toolInteractions && isRecord(interactions)) {
240
- for (const [toolCallId, value] of Object.entries(interactions)) {
241
- const log = readToolInteractionLog(value);
242
- if (log) toolInteractions.set(toolCallId, log);
243
- }
244
- }
245
- const approvalResponses = metadata[TOOL_APPROVAL_RESPONSES_METADATA_KEY];
246
- if (toolApprovalResponses && isRecord(approvalResponses)) {
247
- for (const [approvalId, value] of Object.entries(approvalResponses)) {
248
- if (!isRecord(value) || typeof value.approved !== "boolean") continue;
249
- toolApprovalResponses.set(approvalId, {
250
- approvalId,
251
- approved: value.approved,
252
- ...(typeof value.optionId === "string" && {
253
- optionId: value.optionId,
254
- }),
255
- ...(typeof value.text === "string" && { text: value.text }),
256
- ...(typeof value.reason === "string" && { reason: value.reason }),
257
- });
258
- }
259
- }
260
- const {
261
- [TOOL_ARTIFACTS_METADATA_KEY]: _,
262
- [TOOL_INTERACTIONS_METADATA_KEY]: __,
263
- [TOOL_APPROVAL_RESPONSES_METADATA_KEY]: ___,
264
- ...restMetadata
265
- } = metadata;
266
- const { metadata: _metadata, ...restMessage } = message;
267
- return (
268
- Object.keys(restMetadata).length === 0
269
- ? restMessage
270
- : { ...restMessage, metadata: restMetadata }
271
- ) as TMessage;
272
- };
273
-
274
79
  const encodeContent = <TMessage>(
275
80
  storageFormatAdapter: MessageFormatAdapter<TMessage, any>,
276
81
  item: MessageFormatItem<TMessage>,
@@ -330,6 +135,10 @@ export const useExternalHistory = <TMessage>(
330
135
  const loadHistory = async () => {
331
136
  try {
332
137
  const repo = await formatAdapter.load();
138
+ const threadState = runtimeRef.current.thread.getState();
139
+ if (threadState.isRunning || threadState.messages.length > 0) {
140
+ return;
141
+ }
333
142
  toolArtifacts?.clear();
334
143
  toolInteractions?.clear();
335
144
  toolApprovalResponses?.clear();
@@ -390,8 +199,6 @@ export const useExternalHistory = <TMessage>(
390
199
 
391
200
  const remoteId = optionalThreadListItem()?.getState().remoteId;
392
201
  if (!remoteId) {
393
- // History loads asynchronously against the thread list item; without a
394
- // remote id there is nothing to await, so the flag settles here.
395
202
  // eslint-disable-next-line react-hooks/set-state-in-effect
396
203
  setHasLoaded(true);
397
204
  return aui.subscribe(() => {
@@ -409,6 +216,7 @@ export const useExternalHistory = <TMessage>(
409
216
  }
410
217
 
411
218
  loadedRef.current = true;
219
+ setHasLoaded(false);
412
220
  void loadHistory();
413
221
  return undefined;
414
222
  }, [
@@ -22,7 +22,7 @@ const getTextLength = (
22
22
  if (!message?.parts) return 0;
23
23
  let len = 0;
24
24
  for (const part of message.parts) {
25
- if (part.type === "text") len += part.text.length;
25
+ if (part?.type === "text") len += part.text?.length ?? 0;
26
26
  }
27
27
  return len;
28
28
  };
@@ -35,7 +35,7 @@ const getToolCallCount = (
35
35
  if (!message?.parts) return 0;
36
36
  let count = 0;
37
37
  for (const part of message.parts) {
38
- if (isToolUIPart(part)) count++;
38
+ if (typeof part?.type === "string" && isToolUIPart(part)) count++;
39
39
  }
40
40
  return count;
41
41
  };
@@ -1,7 +1,11 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
- import { defineMcpToolkit } from "@assistant-ui/core/react";
2
+ import {
3
+ defineMcpToolkit,
4
+ defineToolkit,
5
+ type ToolkitDefinition,
6
+ } from "@assistant-ui/core/react";
3
7
  import { AISDKToolkit } from "./generativeTools";
4
- import { wrapModelContentEnvelope } from "../converters/modelContentEnvelope";
8
+ import { wrapModelContentEnvelope } from "assistant-stream/internal";
5
9
 
6
10
  const mocks = vi.hoisted(() => ({
7
11
  close: vi.fn(),
@@ -130,6 +134,190 @@ describe("AISDKToolkit.tools()", () => {
130
134
  supportsDeferredResults: false,
131
135
  });
132
136
  });
137
+
138
+ it("converts unchanged provider and backend schemas once", async () => {
139
+ const toBackendJSONSchema = vi.fn(() => ({
140
+ type: "object" as const,
141
+ properties: {},
142
+ }));
143
+ const toProviderJSONSchema = vi.fn(() => ({
144
+ type: "object" as const,
145
+ properties: {},
146
+ }));
147
+ const toolkitDefinition = {
148
+ serverTool: {
149
+ type: "backend",
150
+ parameters: { toJSONSchema: toBackendJSONSchema },
151
+ execute: async () => "ok",
152
+ } as never,
153
+ providerTool: {
154
+ type: "provider",
155
+ providerId: "provider.tool",
156
+ args: {},
157
+ parameters: { toJSONSchema: toProviderJSONSchema },
158
+ } as never,
159
+ };
160
+ const firstToolkit = new AISDKToolkit({ toolkit: toolkitDefinition });
161
+ const secondToolkit = new AISDKToolkit({ toolkit: toolkitDefinition });
162
+
163
+ const first = await firstToolkit.tools();
164
+ const second = await secondToolkit.tools();
165
+
166
+ expect(toBackendJSONSchema).toHaveBeenCalledTimes(1);
167
+ expect(toProviderJSONSchema).toHaveBeenCalledTimes(1);
168
+ expect(second.serverTool).not.toBe(first.serverTool);
169
+ expect(second.providerTool).not.toBe(first.providerTool);
170
+ expect(second.serverTool!.inputSchema).not.toBe(
171
+ first.serverTool!.inputSchema,
172
+ );
173
+ expect(
174
+ (second.serverTool!.inputSchema as { jsonSchema: unknown }).jsonSchema,
175
+ ).toBe(
176
+ (first.serverTool!.inputSchema as { jsonSchema: unknown }).jsonSchema,
177
+ );
178
+ });
179
+
180
+ it("converts the current frontend tools on every call", async () => {
181
+ const toolkit = new AISDKToolkit({ toolkit: {} });
182
+
183
+ const first = await toolkit.tools({
184
+ frontend: {
185
+ firstClientTool: {
186
+ parameters: { type: "object", properties: {} },
187
+ },
188
+ },
189
+ });
190
+ const second = await toolkit.tools({
191
+ frontend: {
192
+ secondClientTool: {
193
+ parameters: { type: "object", properties: {} },
194
+ },
195
+ },
196
+ });
197
+
198
+ expect(first).toHaveProperty("firstClientTool");
199
+ expect(first).not.toHaveProperty("secondClientTool");
200
+ expect(second).toHaveProperty("secondClientTool");
201
+ expect(second).not.toHaveProperty("firstClientTool");
202
+ });
203
+
204
+ it("does not convert disabled static toolkit entries", async () => {
205
+ const toJSONSchema = vi.fn(() => ({
206
+ type: "object" as const,
207
+ properties: {},
208
+ }));
209
+ const toolkit = new AISDKToolkit({
210
+ toolkit: {
211
+ disabledServerTool: {
212
+ type: "backend",
213
+ disabled: true,
214
+ parameters: { toJSONSchema },
215
+ } as never,
216
+ disabledProviderTool: {
217
+ type: "provider",
218
+ disabled: true,
219
+ providerId: "provider.tool",
220
+ args: {},
221
+ parameters: { toJSONSchema },
222
+ } as never,
223
+ },
224
+ });
225
+
226
+ await expect(toolkit.tools()).resolves.toEqual({});
227
+ expect(toJSONSchema).not.toHaveBeenCalled();
228
+ });
229
+
230
+ it("reflects replaced toolkit entries on the next call", async () => {
231
+ const definition: ToolkitDefinition = {
232
+ serverTool: {
233
+ description: "Original tool",
234
+ parameters: { type: "object" as const, properties: {} },
235
+ execute: async () => "original",
236
+ },
237
+ };
238
+ const toolkit = new AISDKToolkit({ toolkit: defineToolkit(definition) });
239
+
240
+ const first = await toolkit.tools();
241
+ definition.serverTool = {
242
+ description: "Replacement tool",
243
+ parameters: { type: "object" as const, properties: {} },
244
+ execute: async () => "replacement",
245
+ };
246
+
247
+ const refreshed = await toolkit.tools();
248
+
249
+ expect(first.serverTool?.description).toBe("Original tool");
250
+ expect(refreshed.serverTool?.description).toBe("Replacement tool");
251
+ });
252
+
253
+ it("reconverts a replaced parameters object", async () => {
254
+ const firstConversion = vi.fn(() => ({
255
+ type: "object" as const,
256
+ properties: { first: { type: "string" } },
257
+ }));
258
+ const secondConversion = vi.fn(() => ({
259
+ type: "object" as const,
260
+ properties: { second: { type: "string" } },
261
+ }));
262
+ const definition: ToolkitDefinition = {
263
+ serverTool: {
264
+ parameters: { toJSONSchema: firstConversion },
265
+ execute: async () => "ok",
266
+ } as never,
267
+ };
268
+ const toolkit = new AISDKToolkit({ toolkit: defineToolkit(definition) });
269
+
270
+ await toolkit.tools();
271
+ definition.serverTool!.parameters = {
272
+ toJSONSchema: secondConversion,
273
+ } as never;
274
+ await toolkit.tools();
275
+
276
+ expect(firstConversion).toHaveBeenCalledOnce();
277
+ expect(secondConversion).toHaveBeenCalledOnce();
278
+ });
279
+
280
+ it("reflects in-place toolkit entry changes on the next call", async () => {
281
+ const definition: ToolkitDefinition = {
282
+ serverTool: {
283
+ description: "Original tool",
284
+ parameters: { type: "object" as const, properties: {} },
285
+ execute: async () => "ok",
286
+ },
287
+ };
288
+ const toolkit = new AISDKToolkit({ toolkit: defineToolkit(definition) });
289
+
290
+ expect((await toolkit.tools()).serverTool?.description).toBe(
291
+ "Original tool",
292
+ );
293
+
294
+ definition.serverTool!.description = "Updated tool";
295
+ expect((await toolkit.tools()).serverTool?.description).toBe(
296
+ "Updated tool",
297
+ );
298
+
299
+ definition.serverTool!.disabled = true;
300
+ expect(await toolkit.tools()).not.toHaveProperty("serverTool");
301
+ });
302
+
303
+ it("does not share mutable tool entries between calls", async () => {
304
+ const toolkit = new AISDKToolkit({
305
+ toolkit: defineToolkit({
306
+ serverTool: {
307
+ description: "Original tool",
308
+ parameters: { type: "object" as const, properties: {} },
309
+ execute: async () => "ok",
310
+ },
311
+ }),
312
+ });
313
+
314
+ const first = await toolkit.tools();
315
+ first.serverTool!.description = "Changed returned tool";
316
+
317
+ const second = await toolkit.tools();
318
+
319
+ expect(second.serverTool?.description).toBe("Original tool");
320
+ });
133
321
  });
134
322
 
135
323
  describe("AISDKToolkit", () => {
@@ -13,15 +13,13 @@ import type {
13
13
  Toolkit,
14
14
  ToolkitDefinition,
15
15
  } from "@assistant-ui/core/react";
16
- import { frontendTools, type FrontendTools } from "./frontendTools";
16
+ import { frontendTools, type FrontendTools } from "assistant-stream/ai-sdk";
17
17
  import {
18
18
  toAISDKContent,
19
19
  toAISDKDefaultOutput,
20
- } from "../converters/toolOutputConversion";
21
- import {
22
20
  unwrapModelContentEnvelope,
23
21
  type ModelContentEnvelope,
24
- } from "../converters/modelContentEnvelope";
22
+ } from "assistant-stream/internal";
25
23
 
26
24
  const EMPTY_SCHEMA = { type: "object" as const, properties: {} };
27
25
 
@@ -76,8 +74,29 @@ const withMcpConnectionTimeout = async <T>(
76
74
  }
77
75
  };
78
76
 
79
- const parametersToInputSchema = (parameters: Tool["parameters"] | undefined) =>
80
- jsonSchema(parameters ? toJSONSchema(parameters) : EMPTY_SCHEMA);
77
+ // Converted schemas are shared by parameter identity and treated as immutable.
78
+ const convertedParameterSchemas = new WeakMap<
79
+ object,
80
+ ReturnType<typeof toJSONSchema>
81
+ >();
82
+
83
+ const getOrConvertParameterSchema = (
84
+ parameters: NonNullable<Tool["parameters"]>,
85
+ ) => {
86
+ const cached = convertedParameterSchemas.get(parameters);
87
+ if (cached) return cached;
88
+
89
+ const converted = toJSONSchema(parameters);
90
+ convertedParameterSchemas.set(parameters, converted);
91
+ return converted;
92
+ };
93
+
94
+ const parametersToInputSchema = (
95
+ parameters: Tool["parameters"] | undefined,
96
+ ) => {
97
+ if (!parameters) return jsonSchema(EMPTY_SCHEMA);
98
+ return jsonSchema(getOrConvertParameterSchema(parameters));
99
+ };
81
100
 
82
101
  /**
83
102
  * @deprecated Options for the deprecated {@link generativeTools}. Use
@@ -135,6 +154,7 @@ export type AISDKToolkitToolsOptions = {
135
154
  * // In your route handler:
136
155
  * const { tools } = await req.json();
137
156
  * streamText({
157
+ * abortSignal: req.signal,
138
158
  * model,
139
159
  * messages,
140
160
  * tools: await aiToolkit.tools({ frontend: tools }),
@@ -394,7 +414,7 @@ const toAISDKToModelOutput =
394
414
  const { result, modelContent } = unwrapModelContentEnvelope(options.output);
395
415
 
396
416
  if (modelContent !== undefined) {
397
- return toAISDKContent(modelContent);
417
+ return toAISDKContent(modelContent, { taggedFileData: true });
398
418
  }
399
419
 
400
420
  if (!toModelOutput) {
@@ -405,7 +425,7 @@ const toAISDKToModelOutput =
405
425
  ...options,
406
426
  output: result,
407
427
  });
408
- return toAISDKContent(parts);
428
+ return toAISDKContent(parts, { taggedFileData: true });
409
429
  };
410
430
 
411
431
  const toServerToolSet = (toolkit: ToolkitDefinition): ToolSet =>
@@ -31,18 +31,14 @@ describe("sliceMessagesUntil", () => {
31
31
  ]);
32
32
  });
33
33
 
34
- it("extends the slice to include trailing assistant messages", () => {
34
+ it("stops at the target even when assistant messages follow it", () => {
35
35
  const messages = [
36
36
  msg("u1", "user"),
37
37
  msg("a1", "assistant"),
38
38
  msg("a2", "assistant"),
39
39
  msg("u2", "user"),
40
40
  ];
41
- expect(sliceMessagesUntil(messages, "u1").map((m) => m.id)).toEqual([
42
- "u1",
43
- "a1",
44
- "a2",
45
- ]);
41
+ expect(sliceMessagesUntil(messages, "u1").map((m) => m.id)).toEqual(["u1"]);
46
42
  });
47
43
 
48
44
  it("returns the whole array when the target is the last message", () => {
@@ -6,15 +6,11 @@ export const sliceMessagesUntil = <UI_MESSAGE extends UIMessage = UIMessage>(
6
6
  ) => {
7
7
  if (messageId == null) return [];
8
8
 
9
- let messageIdx = messages.findIndex((m) => m.id === messageId);
9
+ const messageIdx = messages.findIndex((m) => m.id === messageId);
10
10
  if (messageIdx === -1)
11
11
  throw new Error(
12
12
  `sliceMessagesUntil: Message "${messageId}" not found in AI SDK messages. This is likely an internal bug in assistant-ui.`,
13
13
  );
14
14
 
15
- while (messages[messageIdx + 1]?.role === "assistant") {
16
- messageIdx++;
17
- }
18
-
19
15
  return messages.slice(0, messageIdx + 1);
20
16
  };
@@ -1,14 +0,0 @@
1
- import type { ToolModelContentPart } from "assistant-stream";
2
- declare const ENVELOPE_KEY = "__aui_modelContent";
3
- export type ModelContentEnvelope<TResult = unknown> = {
4
- readonly [ENVELOPE_KEY]: readonly ToolModelContentPart[];
5
- readonly value: TResult;
6
- };
7
- export declare function isModelContentEnvelope<TResult = unknown>(value: TResult | ModelContentEnvelope<TResult>): value is ModelContentEnvelope<TResult>;
8
- export declare function wrapModelContentEnvelope<TResult>(result: TResult, modelContent: readonly ToolModelContentPart[]): ModelContentEnvelope<TResult>;
9
- export declare function unwrapModelContentEnvelope<TResult>(output: TResult | ModelContentEnvelope<TResult>): {
10
- result: TResult;
11
- modelContent?: readonly ToolModelContentPart[];
12
- };
13
- export {};
14
- //# sourceMappingURL=modelContentEnvelope.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"modelContentEnvelope.d.ts","sourceRoot":"","sources":["../../src/converters/modelContentEnvelope.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAE7D,QAAA,MAAM,YAAY,uBAAuB,CAAC;AAE1C,MAAM,MAAM,oBAAoB,CAAC,OAAO,GAAG,OAAO,IAAI;IACpD,QAAQ,CAAC,CAAC,YAAY,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAC;IACzD,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,wBAAgB,sBAAsB,CAAC,OAAO,GAAG,OAAO,EACtD,KAAK,EAAE,OAAO,GAAG,oBAAoB,CAAC,OAAO,CAAC,GAC7C,KAAK,IAAI,oBAAoB,CAAC,OAAO,CAAC,CAOxC;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAC9C,MAAM,EAAE,OAAO,EACf,YAAY,EAAE,SAAS,oBAAoB,EAAE,GAC5C,oBAAoB,CAAC,OAAO,CAAC,CAE/B;AAED,wBAAgB,0BAA0B,CAAC,OAAO,EAChD,MAAM,EAAE,OAAO,GAAG,oBAAoB,CAAC,OAAO,CAAC,GAC9C;IACD,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAC;CAChD,CAQA"}
@@ -1,22 +0,0 @@
1
- //#region src/converters/modelContentEnvelope.ts
2
- const ENVELOPE_KEY = "__aui_modelContent";
3
- function isModelContentEnvelope(value) {
4
- return value != null && typeof value === "object" && ENVELOPE_KEY in value && Array.isArray(value[ENVELOPE_KEY]);
5
- }
6
- function wrapModelContentEnvelope(result, modelContent) {
7
- return {
8
- [ENVELOPE_KEY]: modelContent,
9
- value: result
10
- };
11
- }
12
- function unwrapModelContentEnvelope(output) {
13
- if (isModelContentEnvelope(output)) return {
14
- result: output.value,
15
- modelContent: output[ENVELOPE_KEY]
16
- };
17
- return { result: output };
18
- }
19
- //#endregion
20
- export { isModelContentEnvelope, unwrapModelContentEnvelope, wrapModelContentEnvelope };
21
-
22
- //# sourceMappingURL=modelContentEnvelope.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"modelContentEnvelope.js","names":["ToolModelContentPart","ENVELOPE_KEY","ModelContentEnvelope","value","TResult","isModelContentEnvelope","Array","isArray","Record","wrapModelContentEnvelope","result","modelContent","unwrapModelContentEnvelope","output"],"sources":["../../src/converters/modelContentEnvelope.ts"],"sourcesContent":["import type { ToolModelContentPart } from \"assistant-stream\";\n\nconst ENVELOPE_KEY = \"__aui_modelContent\";\n\nexport type ModelContentEnvelope<TResult = unknown> = {\n readonly [ENVELOPE_KEY]: readonly ToolModelContentPart[];\n readonly value: TResult;\n};\n\nexport function isModelContentEnvelope<TResult = unknown>(\n value: TResult | ModelContentEnvelope<TResult>,\n): value is ModelContentEnvelope<TResult> {\n return (\n value != null &&\n typeof value === \"object\" &&\n ENVELOPE_KEY in value &&\n Array.isArray((value as Record<string, unknown>)[ENVELOPE_KEY])\n );\n}\n\nexport function wrapModelContentEnvelope<TResult>(\n result: TResult,\n modelContent: readonly ToolModelContentPart[],\n): ModelContentEnvelope<TResult> {\n return { [ENVELOPE_KEY]: modelContent, value: result };\n}\n\nexport function unwrapModelContentEnvelope<TResult>(\n output: TResult | ModelContentEnvelope<TResult>,\n): {\n result: TResult;\n modelContent?: readonly ToolModelContentPart[];\n} {\n if (isModelContentEnvelope(output)) {\n return {\n result: output.value,\n modelContent: output[ENVELOPE_KEY],\n };\n }\n return { result: output };\n}\n"],"mappings":";AAEA,MAAMC,eAAe;AAOrB,SAAgBI,uBACdF,OACwC;CACxC,OACEA,SAAS,QACT,OAAOA,UAAU,YACjBF,gBAAgBE,SAChBG,MAAMC,QAASJ,MAAkCF,aAAa;AAElE;AAEA,SAAgBQ,yBACdC,QACAC,cAC+B;CAC/B,OAAO;GAAGV,eAAeU;EAAcR,OAAOO;CAAO;AACvD;AAEA,SAAgBE,2BACdC,QAIA;CACA,IAAIR,uBAAuBQ,MAAM,GAC/B,OAAO;EACLH,QAAQG,OAAOV;EACfQ,cAAcE,OAAOZ;CACvB;CAEF,OAAO,EAAES,QAAQG,OAAO;AAC1B"}
@@ -1,26 +0,0 @@
1
- import type { JSONValue } from "ai";
2
- import type { ToolModelContentPart } from "assistant-stream";
3
- export declare const toAISDKContent: (parts: readonly ToolModelContentPart[]) => {
4
- type: "content";
5
- value: ({
6
- type: "text";
7
- text: string;
8
- } | {
9
- filename?: string;
10
- type: "file";
11
- data: {
12
- type: "data";
13
- data: string;
14
- };
15
- mediaType: string;
16
- text?: never;
17
- })[];
18
- };
19
- export declare const toAISDKDefaultOutput: (output: unknown) => {
20
- type: "text";
21
- value: string;
22
- } | {
23
- type: "json";
24
- value: JSONValue;
25
- };
26
- //# sourceMappingURL=toolOutputConversion.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"toolOutputConversion.d.ts","sourceRoot":"","sources":["../../src/converters/toolOutputConversion.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAE7D,eAAO,MAAM,cAAc,GAAI,OAAO,SAAS,oBAAoB,EAAE;;;;;;;;;;;;;;;CAiBnE,CAAC;AAEH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,OAAO;;;;;WAGM,SAAS;CAAE,CAAC"}