@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.
- package/dist/converters/convertMessage.d.ts +8 -6
- package/dist/converters/convertMessage.d.ts.map +1 -1
- package/dist/converters/convertMessage.js +106 -28
- package/dist/converters/convertMessage.js.map +1 -1
- package/dist/converters/toolApprovalAnswers.d.ts +3 -0
- package/dist/converters/toolApprovalAnswers.d.ts.map +1 -0
- package/dist/converters/toolApprovalAnswers.js +17 -0
- package/dist/converters/toolApprovalAnswers.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.native.d.ts +1 -1
- package/dist/index.native.d.ts.map +1 -1
- package/dist/index.native.js +1 -1
- package/dist/model-context/injectInteractableContext.d.ts +3 -0
- package/dist/model-context/injectInteractableContext.d.ts.map +1 -1
- package/dist/model-context/injectInteractableContext.js +3 -0
- package/dist/model-context/injectInteractableContext.js.map +1 -1
- package/dist/model-context/injectQuoteContext.d.ts +1 -0
- package/dist/model-context/injectQuoteContext.d.ts.map +1 -1
- package/dist/model-context/injectQuoteContext.js +1 -0
- package/dist/model-context/injectQuoteContext.js.map +1 -1
- package/dist/runtime/AISDKChat.js +1 -1
- package/dist/runtime/AISDKThreads.d.ts +5 -0
- package/dist/runtime/AISDKThreads.d.ts.map +1 -1
- package/dist/runtime/AISDKThreads.js +37 -4
- package/dist/runtime/AISDKThreads.js.map +1 -1
- package/dist/runtime/DynamicChatTransport.d.ts +49 -0
- package/dist/runtime/DynamicChatTransport.d.ts.map +1 -0
- package/dist/runtime/DynamicChatTransport.js +147 -0
- package/dist/runtime/DynamicChatTransport.js.map +1 -0
- package/dist/runtime/getResumableAdapter.d.ts +5 -0
- package/dist/runtime/getResumableAdapter.d.ts.map +1 -0
- package/dist/runtime/getResumableAdapter.js +12 -0
- package/dist/runtime/getResumableAdapter.js.map +1 -0
- package/dist/runtime/sdkIdentity.js +1 -1
- package/dist/runtime/toolHistoryCodec.d.ts +20 -0
- package/dist/runtime/toolHistoryCodec.d.ts.map +1 -0
- package/dist/runtime/toolHistoryCodec.js +107 -0
- package/dist/runtime/toolHistoryCodec.js.map +1 -0
- package/dist/runtime/useAISDKRuntime.d.ts +4 -1
- package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
- package/dist/runtime/useAISDKRuntime.js +154 -155
- package/dist/runtime/useAISDKRuntime.js.map +1 -1
- package/dist/runtime/useChatRuntime.d.ts +14 -2
- package/dist/runtime/useChatRuntime.d.ts.map +1 -1
- package/dist/runtime/useChatRuntime.js +12 -3
- package/dist/runtime/useChatRuntime.js.map +1 -1
- package/dist/runtime/useChatThread.d.ts +3 -2
- package/dist/runtime/useChatThread.d.ts.map +1 -1
- package/dist/runtime/useChatThread.js +56 -36
- package/dist/runtime/useChatThread.js.map +1 -1
- package/dist/runtime/useDynamicChatTransport.d.ts +4 -0
- package/dist/runtime/useDynamicChatTransport.d.ts.map +1 -0
- package/dist/runtime/useDynamicChatTransport.js +64 -0
- package/dist/runtime/useDynamicChatTransport.js.map +1 -0
- package/dist/runtime/useExternalHistory.d.ts.map +1 -1
- package/dist/runtime/useExternalHistory.js +11 -105
- package/dist/runtime/useExternalHistory.js.map +1 -1
- package/dist/runtime/useResourceCleanup.js +1 -1
- package/dist/runtime/useStreamingTiming.js +2 -2
- package/dist/runtime/useStreamingTiming.js.map +1 -1
- package/dist/tools/generativeTools.d.ts +2 -1
- package/dist/tools/generativeTools.d.ts.map +1 -1
- package/dist/tools/generativeTools.js +17 -6
- package/dist/tools/generativeTools.js.map +1 -1
- package/dist/usage.js +1 -1
- package/dist/utils/sliceMessagesUntil.d.ts.map +1 -1
- package/dist/utils/sliceMessagesUntil.js +1 -2
- package/dist/utils/sliceMessagesUntil.js.map +1 -1
- package/package.json +12 -10
- package/src/converters/convertMessage.test.ts +350 -2
- package/src/converters/convertMessage.tool-args-status.test.tsx +185 -0
- package/src/converters/convertMessage.ts +154 -23
- package/src/converters/toCreateMessage.test.ts +27 -0
- package/src/converters/toolApprovalAnswers.ts +27 -0
- package/src/index.native.ts +1 -1
- package/src/index.ts +1 -1
- package/src/model-context/injectInteractableContext.ts +3 -0
- package/src/model-context/injectQuoteContext.ts +1 -0
- package/src/runtime/AISDKChat.integration.test.tsx +57 -2
- package/src/runtime/AISDKThreads.cloud.test.ts +3 -0
- package/src/runtime/AISDKThreads.test.ts +181 -0
- package/src/runtime/AISDKThreads.ts +31 -4
- package/src/runtime/DynamicChatTransport.test.ts +203 -0
- package/src/runtime/DynamicChatTransport.ts +273 -0
- package/src/runtime/__tests__/controlled-transport.ts +3 -0
- package/src/runtime/getResumableAdapter.ts +16 -0
- package/src/runtime/toolHistoryCodec.test.ts +161 -0
- package/src/runtime/toolHistoryCodec.ts +207 -0
- package/src/runtime/useAISDKRuntime.approval.test.tsx +12 -0
- package/src/runtime/useAISDKRuntime.reload.test.tsx +219 -0
- package/src/runtime/useAISDKRuntime.test.ts +538 -5
- package/src/runtime/useAISDKRuntime.ts +161 -51
- package/src/runtime/useChatRuntime.integration.test.tsx +319 -4
- package/src/runtime/useChatRuntime.local-storage.test.tsx +123 -0
- package/src/runtime/useChatRuntime.test.ts +107 -1
- package/src/runtime/useChatRuntime.ts +26 -4
- package/src/runtime/useChatThread.binding.test.tsx +143 -0
- package/src/runtime/useChatThread.ts +98 -81
- package/src/runtime/useDynamicChatTransport.ts +26 -0
- package/src/runtime/useExternalHistory.test.ts +205 -0
- package/src/runtime/useExternalHistory.ts +14 -206
- package/src/runtime/useStreamingTiming.ts +2 -2
- package/src/tools/generativeTools.test.ts +190 -2
- package/src/tools/generativeTools.ts +28 -8
- package/src/utils/sliceMessagesUntil.test.ts +2 -6
- package/src/utils/sliceMessagesUntil.ts +1 -5
- package/dist/converters/modelContentEnvelope.d.ts +0 -14
- package/dist/converters/modelContentEnvelope.d.ts.map +0 -1
- package/dist/converters/modelContentEnvelope.js +0 -22
- package/dist/converters/modelContentEnvelope.js.map +0 -1
- package/dist/converters/toolOutputConversion.d.ts +0 -26
- package/dist/converters/toolOutputConversion.d.ts.map +0 -1
- package/dist/converters/toolOutputConversion.js +0 -31
- package/dist/converters/toolOutputConversion.js.map +0 -1
- package/dist/tools/frontendTools.d.ts +0 -30
- package/dist/tools/frontendTools.d.ts.map +0 -1
- package/dist/tools/frontendTools.js +0 -33
- package/dist/tools/frontendTools.js.map +0 -1
- package/src/converters/modelContentEnvelope.ts +0 -41
- package/src/converters/toolOutputConversion.ts +0 -26
- package/src/tools/frontendTools.test.ts +0 -205
- package/src/tools/frontendTools.ts +0 -83
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
isReasoningFileUIPart,
|
|
4
4
|
isCustomContentUIPart,
|
|
5
5
|
getToolName,
|
|
6
|
+
type FileUIPart,
|
|
6
7
|
type UIMessage,
|
|
7
8
|
} from "ai";
|
|
8
9
|
import {
|
|
@@ -13,6 +14,8 @@ import {
|
|
|
13
14
|
isMcpAppUri,
|
|
14
15
|
type ReasoningMessagePart,
|
|
15
16
|
type ToolApprovalOption,
|
|
17
|
+
type ToolApprovalQuestion,
|
|
18
|
+
type ToolApprovalQuestionOption,
|
|
16
19
|
type ToolCallMessagePart,
|
|
17
20
|
type TextMessagePart,
|
|
18
21
|
type DataMessagePart,
|
|
@@ -26,12 +29,14 @@ import {
|
|
|
26
29
|
type RespondToToolApprovalOptions,
|
|
27
30
|
type Unstable_ToolInteractionLog,
|
|
28
31
|
} from "@assistant-ui/core";
|
|
32
|
+
import { normalizeToolApprovalAnswers } from "./toolApprovalAnswers";
|
|
29
33
|
import { stableStringifyToolArgs } from "@assistant-ui/core/internal";
|
|
34
|
+
import { markPartialJsonObjectComplete } from "assistant-stream/internal";
|
|
30
35
|
import {
|
|
31
36
|
parsePartialJsonObject,
|
|
32
37
|
type ReadonlyJSONObject,
|
|
33
38
|
} from "assistant-stream/utils";
|
|
34
|
-
import { unwrapModelContentEnvelope } from "
|
|
39
|
+
import { unwrapModelContentEnvelope } from "assistant-stream/internal";
|
|
35
40
|
|
|
36
41
|
type MessageMetadata = ThreadMessageLike["metadata"];
|
|
37
42
|
|
|
@@ -63,13 +68,15 @@ export type AISDKMessageConverterMetadata =
|
|
|
63
68
|
useExternalMessageConverter.Metadata & {
|
|
64
69
|
toolArgsKeyOrderCache?: Map<string, Map<string, string[]>>;
|
|
65
70
|
/**
|
|
66
|
-
* Frozen
|
|
67
|
-
* by call, since the text carries
|
|
68
|
-
* call/input pair skips serialization;
|
|
69
|
-
* the input is unreachable.
|
|
70
|
-
* deterministic key order.
|
|
71
|
+
* Frozen text and completion-marked args keyed weakly by a settled tool
|
|
72
|
+
* call's input object, then by call, since the text carries its streamed key
|
|
73
|
+
* order. A known call/input pair skips serialization and parsing; entries
|
|
74
|
+
* become collectible once the input is unreachable.
|
|
71
75
|
*/
|
|
72
|
-
toolArgsTextCache?: WeakMap<
|
|
76
|
+
toolArgsTextCache?: WeakMap<
|
|
77
|
+
ReadonlyJSONObject,
|
|
78
|
+
Map<string, { argsText: string; args: ReadonlyJSONObject }>
|
|
79
|
+
>;
|
|
73
80
|
toolLastInputCache?: Map<string, ReadonlyJSONObject>;
|
|
74
81
|
mcpAppMetadataCache?: Map<string, McpAppMetadata>;
|
|
75
82
|
toolArtifacts?: ReadonlyMap<string, unknown>;
|
|
@@ -95,17 +102,45 @@ function extractMcpAppMetadata(
|
|
|
95
102
|
cache: Map<string, McpAppMetadata> | undefined,
|
|
96
103
|
): McpAppMetadata | undefined {
|
|
97
104
|
if (!part || typeof part !== "object") return undefined;
|
|
105
|
+
const toolMetadata = (part as { toolMetadata?: unknown }).toolMetadata;
|
|
106
|
+
const toolApp =
|
|
107
|
+
toolMetadata && typeof toolMetadata === "object"
|
|
108
|
+
? (toolMetadata as { app?: unknown }).app
|
|
109
|
+
: undefined;
|
|
98
110
|
const meta = (part as { callProviderMetadata?: unknown })
|
|
99
111
|
.callProviderMetadata;
|
|
100
112
|
const mcp =
|
|
101
113
|
meta && typeof meta === "object"
|
|
102
114
|
? (meta as { mcp?: unknown }).mcp
|
|
103
115
|
: undefined;
|
|
104
|
-
const
|
|
116
|
+
const providerApp =
|
|
105
117
|
mcp && typeof mcp === "object" ? (mcp as { app?: unknown }).app : undefined;
|
|
118
|
+
|
|
119
|
+
const apps: Record<string, unknown>[] = [];
|
|
120
|
+
for (const candidate of [toolApp, providerApp]) {
|
|
121
|
+
if (!candidate || typeof candidate !== "object") continue;
|
|
122
|
+
const candidateApp = candidate as Record<string, unknown>;
|
|
123
|
+
const resourceUri = candidateApp["resourceUri"];
|
|
124
|
+
if (typeof resourceUri !== "string" || !isMcpAppUri(resourceUri)) continue;
|
|
125
|
+
apps.push({
|
|
126
|
+
resourceUri,
|
|
127
|
+
...(typeof candidateApp["mimeType"] === "string" && {
|
|
128
|
+
mimeType: candidateApp["mimeType"],
|
|
129
|
+
}),
|
|
130
|
+
...(Array.isArray(candidateApp["visibility"]) && {
|
|
131
|
+
visibility: candidateApp["visibility"],
|
|
132
|
+
}),
|
|
133
|
+
...(candidate === providerApp &&
|
|
134
|
+
typeof candidateApp["serverId"] === "string" &&
|
|
135
|
+
candidateApp["serverId"].length > 0 && {
|
|
136
|
+
serverId: candidateApp["serverId"],
|
|
137
|
+
}),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
106
141
|
let a: Record<string, unknown>;
|
|
107
|
-
if (
|
|
108
|
-
a =
|
|
142
|
+
if (apps.length > 0) {
|
|
143
|
+
a = { ...(apps[1] ?? {}), ...apps[0]! };
|
|
109
144
|
} else {
|
|
110
145
|
// MCP-UI tools surface the pointer on result._meta: canonical nested
|
|
111
146
|
// `ui.resourceUri`, or the deprecated flat `"ui/resourceUri"` key.
|
|
@@ -221,6 +256,51 @@ const normalizeToolApprovalOptions = (
|
|
|
221
256
|
});
|
|
222
257
|
};
|
|
223
258
|
|
|
259
|
+
const normalizeToolApprovalQuestions = (
|
|
260
|
+
questions: unknown,
|
|
261
|
+
): readonly ToolApprovalQuestion[] | undefined => {
|
|
262
|
+
if (!Array.isArray(questions)) return undefined;
|
|
263
|
+
|
|
264
|
+
return questions.flatMap<ToolApprovalQuestion>((value) => {
|
|
265
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return [];
|
|
266
|
+
const question = value as Record<string, unknown>;
|
|
267
|
+
if (typeof question.id !== "string" || typeof question.prompt !== "string")
|
|
268
|
+
return [];
|
|
269
|
+
const options = Array.isArray(question.options)
|
|
270
|
+
? question.options.flatMap<ToolApprovalQuestionOption>((option) => {
|
|
271
|
+
if (!option || typeof option !== "object" || Array.isArray(option))
|
|
272
|
+
return [];
|
|
273
|
+
const { id, label, description } = option as Record<string, unknown>;
|
|
274
|
+
if (typeof id !== "string" || typeof label !== "string") return [];
|
|
275
|
+
return [
|
|
276
|
+
{
|
|
277
|
+
id,
|
|
278
|
+
label,
|
|
279
|
+
...(typeof description === "string" && { description }),
|
|
280
|
+
},
|
|
281
|
+
];
|
|
282
|
+
})
|
|
283
|
+
: undefined;
|
|
284
|
+
|
|
285
|
+
return [
|
|
286
|
+
{
|
|
287
|
+
id: question.id,
|
|
288
|
+
prompt: question.prompt,
|
|
289
|
+
...(typeof question.header === "string" && {
|
|
290
|
+
header: question.header,
|
|
291
|
+
}),
|
|
292
|
+
...(options && { options }),
|
|
293
|
+
...(typeof question.multiple === "boolean" && {
|
|
294
|
+
multiple: question.multiple,
|
|
295
|
+
}),
|
|
296
|
+
...(typeof question.allowFreeform === "boolean" && {
|
|
297
|
+
allowFreeform: question.allowFreeform,
|
|
298
|
+
}),
|
|
299
|
+
},
|
|
300
|
+
];
|
|
301
|
+
});
|
|
302
|
+
};
|
|
303
|
+
|
|
224
304
|
const APPROVAL_DESCRIPTOR_FIELDS = [
|
|
225
305
|
"prompt",
|
|
226
306
|
"display",
|
|
@@ -229,6 +309,8 @@ const APPROVAL_DESCRIPTOR_FIELDS = [
|
|
|
229
309
|
"options",
|
|
230
310
|
"optionId",
|
|
231
311
|
"text",
|
|
312
|
+
"questions",
|
|
313
|
+
"answers",
|
|
232
314
|
"resolution",
|
|
233
315
|
] as const;
|
|
234
316
|
|
|
@@ -302,6 +384,8 @@ function getToolApprovalAndInterrupt(
|
|
|
302
384
|
options,
|
|
303
385
|
optionId,
|
|
304
386
|
text,
|
|
387
|
+
questions,
|
|
388
|
+
answers,
|
|
305
389
|
...additionalApprovalFields
|
|
306
390
|
} = response
|
|
307
391
|
? {
|
|
@@ -310,11 +394,18 @@ function getToolApprovalAndInterrupt(
|
|
|
310
394
|
...(response.reason != null && { reason: response.reason }),
|
|
311
395
|
...(response.optionId != null && { optionId: response.optionId }),
|
|
312
396
|
...(response.text != null && { text: response.text }),
|
|
397
|
+
...(response.answers != null && { answers: response.answers }),
|
|
313
398
|
}
|
|
314
399
|
: approval;
|
|
315
400
|
const normalizedOptions = supportsRichToolApprovalResponses
|
|
316
401
|
? normalizeToolApprovalOptions(options)
|
|
317
402
|
: undefined;
|
|
403
|
+
const normalizedQuestions = supportsRichToolApprovalResponses
|
|
404
|
+
? normalizeToolApprovalQuestions(questions)
|
|
405
|
+
: undefined;
|
|
406
|
+
const normalizedAnswers = supportsRichToolApprovalResponses
|
|
407
|
+
? normalizeToolApprovalAnswers(answers)
|
|
408
|
+
: undefined;
|
|
318
409
|
const requestReason = additionalApprovalFields.requestReason;
|
|
319
410
|
if (typeof id === "string")
|
|
320
411
|
return {
|
|
@@ -332,12 +423,15 @@ function getToolApprovalAndInterrupt(
|
|
|
332
423
|
...(supportsRichToolApprovalResponses && {
|
|
333
424
|
...((display === "decision" ||
|
|
334
425
|
display === "select" ||
|
|
335
|
-
display === "text"
|
|
426
|
+
display === "text" ||
|
|
427
|
+
display === "questions") && { display }),
|
|
336
428
|
...(typeof allowFreeform === "boolean" && { allowFreeform }),
|
|
337
429
|
...(typeof dismissible === "boolean" && { dismissible }),
|
|
338
430
|
...(normalizedOptions && { options: normalizedOptions }),
|
|
339
431
|
...(typeof optionId === "string" && { optionId }),
|
|
340
432
|
...(typeof text === "string" && { text }),
|
|
433
|
+
...(normalizedQuestions && { questions: normalizedQuestions }),
|
|
434
|
+
...(normalizedAnswers && { answers: normalizedAnswers }),
|
|
341
435
|
}),
|
|
342
436
|
...((resolution === "cancelled" || resolution === "expired") && {
|
|
343
437
|
resolution,
|
|
@@ -367,6 +461,25 @@ const uiPartStateToStatus = (
|
|
|
367
461
|
return undefined;
|
|
368
462
|
};
|
|
369
463
|
|
|
464
|
+
const toSystemContent = (content: MessageContent): MessageContent => {
|
|
465
|
+
const text = content.filter((part) => part.type === "text");
|
|
466
|
+
if (text.length === 1) return text;
|
|
467
|
+
const providerMetadata = text.reduce<PartProviderMetadata>(
|
|
468
|
+
(merged, part) =>
|
|
469
|
+
part.providerMetadata != null
|
|
470
|
+
? { ...merged, ...part.providerMetadata }
|
|
471
|
+
: merged,
|
|
472
|
+
{},
|
|
473
|
+
);
|
|
474
|
+
return [
|
|
475
|
+
{
|
|
476
|
+
type: "text",
|
|
477
|
+
text: text.map((part) => part.text).join(""),
|
|
478
|
+
...(Object.keys(providerMetadata).length > 0 && { providerMetadata }),
|
|
479
|
+
},
|
|
480
|
+
];
|
|
481
|
+
};
|
|
482
|
+
|
|
370
483
|
function convertParts(
|
|
371
484
|
message: UIMessage,
|
|
372
485
|
metadata: AISDKMessageConverterMetadata,
|
|
@@ -378,6 +491,7 @@ function convertParts(
|
|
|
378
491
|
const converted = message.parts
|
|
379
492
|
.filter(
|
|
380
493
|
(p) =>
|
|
494
|
+
typeof p?.type === "string" &&
|
|
381
495
|
p.type !== "step-start" &&
|
|
382
496
|
(message.role !== "user" || p.type !== "file"),
|
|
383
497
|
)
|
|
@@ -386,7 +500,7 @@ function convertParts(
|
|
|
386
500
|
const status = uiPartStateToStatus(part.state);
|
|
387
501
|
return {
|
|
388
502
|
type: "text",
|
|
389
|
-
text: part.text,
|
|
503
|
+
text: part.text ?? "",
|
|
390
504
|
...(status != null ? { status } : undefined),
|
|
391
505
|
...(part.providerMetadata != null
|
|
392
506
|
? {
|
|
@@ -411,7 +525,7 @@ function convertParts(
|
|
|
411
525
|
}
|
|
412
526
|
|
|
413
527
|
if (isToolUIPart(part)) {
|
|
414
|
-
const toolName = getToolName(part);
|
|
528
|
+
const toolName = getToolName(part) ?? "";
|
|
415
529
|
const toolCallId = part.toolCallId;
|
|
416
530
|
const argsKeyOrderCacheKey = `${message.id}:${toolCallId}`;
|
|
417
531
|
|
|
@@ -475,20 +589,31 @@ function convertParts(
|
|
|
475
589
|
// re-serializing large args while the call keeps that input. Arrival
|
|
476
590
|
// order only matters while args stream, so the key-order entry is
|
|
477
591
|
// released.
|
|
592
|
+
const inputArgs = args;
|
|
478
593
|
const frozen =
|
|
479
|
-
metadata.toolArgsTextCache?.get(
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
594
|
+
metadata.toolArgsTextCache?.get(inputArgs) ??
|
|
595
|
+
new Map<string, { argsText: string; args: ReadonlyJSONObject }>();
|
|
596
|
+
const frozenEntry = frozen.get(argsKeyOrderCacheKey);
|
|
597
|
+
if (frozenEntry !== undefined) {
|
|
598
|
+
argsText = frozenEntry.argsText;
|
|
599
|
+
args = frozenEntry.args;
|
|
483
600
|
} else {
|
|
484
601
|
argsText = stableStringifyToolArgs(
|
|
485
602
|
metadata.toolArgsKeyOrderCache,
|
|
486
603
|
argsKeyOrderCacheKey,
|
|
487
604
|
args,
|
|
488
605
|
);
|
|
606
|
+
// The input is final even while execution keeps the part running.
|
|
607
|
+
// Other runtimes can synthesize complete JSON text from an
|
|
608
|
+
// accumulating snapshot, so only this converter supplies the
|
|
609
|
+
// completion signal it knows from the AI SDK part state.
|
|
610
|
+
// A complete root marker settles every field without parsing the
|
|
611
|
+
// serialized text again. Keep the SDK input's nested identities
|
|
612
|
+
// and own fields, including prototype-named JSON keys.
|
|
613
|
+
args = markPartialJsonObjectComplete(args);
|
|
489
614
|
metadata.toolArgsTextCache?.set(
|
|
490
|
-
|
|
491
|
-
frozen.set(argsKeyOrderCacheKey, argsText),
|
|
615
|
+
inputArgs,
|
|
616
|
+
frozen.set(argsKeyOrderCacheKey, { argsText, args }),
|
|
492
617
|
);
|
|
493
618
|
}
|
|
494
619
|
metadata.toolArgsKeyOrderCache?.delete(argsKeyOrderCacheKey);
|
|
@@ -557,10 +682,11 @@ function convertParts(
|
|
|
557
682
|
}
|
|
558
683
|
|
|
559
684
|
if (part.type === "file") {
|
|
685
|
+
if (typeof part.url !== "string") return null;
|
|
560
686
|
return {
|
|
561
687
|
type: "file",
|
|
562
688
|
data: part.url,
|
|
563
|
-
mimeType: part.mediaType,
|
|
689
|
+
mimeType: part.mediaType ?? "unknown/unknown",
|
|
564
690
|
...(part.filename != null && { filename: part.filename }),
|
|
565
691
|
} satisfies FileMessagePart;
|
|
566
692
|
}
|
|
@@ -594,10 +720,11 @@ function convertParts(
|
|
|
594
720
|
}
|
|
595
721
|
|
|
596
722
|
if (isReasoningFileUIPart(part)) {
|
|
723
|
+
if (typeof part.url !== "string") return null;
|
|
597
724
|
return {
|
|
598
725
|
type: "file",
|
|
599
726
|
data: part.url,
|
|
600
|
-
mimeType: part.mediaType,
|
|
727
|
+
mimeType: part.mediaType ?? "unknown/unknown",
|
|
601
728
|
} satisfies FileMessagePart;
|
|
602
729
|
}
|
|
603
730
|
|
|
@@ -637,7 +764,10 @@ export const AISDKMessageConverter = unstable_createMessageConverter(
|
|
|
637
764
|
createdAt,
|
|
638
765
|
content,
|
|
639
766
|
attachments: message.parts
|
|
640
|
-
?.filter(
|
|
767
|
+
?.filter(
|
|
768
|
+
(p): p is FileUIPart =>
|
|
769
|
+
p?.type === "file" && typeof p.url === "string",
|
|
770
|
+
)
|
|
641
771
|
.map((part, idx) => {
|
|
642
772
|
const mediaType = part.mediaType ?? "unknown/unknown";
|
|
643
773
|
const isImage = mediaType.startsWith("image/");
|
|
@@ -676,7 +806,8 @@ export const AISDKMessageConverter = unstable_createMessageConverter(
|
|
|
676
806
|
role: message.role,
|
|
677
807
|
id: message.id,
|
|
678
808
|
createdAt,
|
|
679
|
-
content
|
|
809
|
+
content:
|
|
810
|
+
message.role === "system" ? toSystemContent(content) : content,
|
|
680
811
|
...(message.role === "assistant" &&
|
|
681
812
|
metadata.cancelledStatusMessageIds?.has(message.id)
|
|
682
813
|
? {
|
|
@@ -11,6 +11,33 @@ const baseMessage = {
|
|
|
11
11
|
} as const;
|
|
12
12
|
|
|
13
13
|
describe("toCreateMessage", () => {
|
|
14
|
+
it.each([
|
|
15
|
+
[
|
|
16
|
+
"data:;base64,SGVsbG8=",
|
|
17
|
+
"data:application/octet-stream;base64,SGVsbG8=",
|
|
18
|
+
"application/octet-stream",
|
|
19
|
+
],
|
|
20
|
+
[
|
|
21
|
+
"data:;charset=utf-8,hello",
|
|
22
|
+
"data:text/plain;charset=utf-8,hello",
|
|
23
|
+
"text/plain",
|
|
24
|
+
],
|
|
25
|
+
[
|
|
26
|
+
"data:;base64,",
|
|
27
|
+
"data:application/octet-stream;base64,",
|
|
28
|
+
"application/octet-stream",
|
|
29
|
+
],
|
|
30
|
+
])("uses the resolved media type for %s", (data, url, mediaType) => {
|
|
31
|
+
const message = {
|
|
32
|
+
...baseMessage,
|
|
33
|
+
content: [{ type: "file", data, mimeType: "" }],
|
|
34
|
+
} as unknown as AppendMessage;
|
|
35
|
+
|
|
36
|
+
expect(toCreateMessage(message).parts).toEqual([
|
|
37
|
+
{ type: "file", url, mediaType },
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
|
|
14
41
|
it("converts a direct file part in message content", () => {
|
|
15
42
|
const message = {
|
|
16
43
|
...baseMessage,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ToolApprovalAnswer } from "@assistant-ui/core";
|
|
2
|
+
|
|
3
|
+
export const normalizeToolApprovalAnswers = (
|
|
4
|
+
answers: unknown,
|
|
5
|
+
): Readonly<Record<string, ToolApprovalAnswer>> | undefined => {
|
|
6
|
+
if (!answers || typeof answers !== "object" || Array.isArray(answers))
|
|
7
|
+
return undefined;
|
|
8
|
+
|
|
9
|
+
return Object.fromEntries(
|
|
10
|
+
Object.entries(answers).flatMap(([id, value]) => {
|
|
11
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
12
|
+
return [];
|
|
13
|
+
const { optionIds, text } = value as Record<string, unknown>;
|
|
14
|
+
const answer: ToolApprovalAnswer = {
|
|
15
|
+
...(Array.isArray(optionIds) && {
|
|
16
|
+
optionIds: optionIds.filter(
|
|
17
|
+
(optionId): optionId is string => typeof optionId === "string",
|
|
18
|
+
),
|
|
19
|
+
}),
|
|
20
|
+
...(typeof text === "string" && { text }),
|
|
21
|
+
};
|
|
22
|
+
return answer.optionIds?.length || answer.text !== undefined
|
|
23
|
+
? [[id, answer]]
|
|
24
|
+
: [];
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
};
|
package/src/index.native.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type {
|
|
|
12
12
|
AssistantChatResumableOptions,
|
|
13
13
|
ResumableClientStorage,
|
|
14
14
|
} from "./transport/resumable";
|
|
15
|
-
export { frontendTools, type FrontendTools } from "
|
|
15
|
+
export { frontendTools, type FrontendTools } from "assistant-stream/ai-sdk";
|
|
16
16
|
export { injectQuoteContext } from "./model-context/injectQuoteContext";
|
|
17
17
|
export { unstable_injectInteractableContext } from "./model-context/injectInteractableContext";
|
|
18
18
|
export type { ThreadTokenUsage, TokenUsageExtractableMessage } from "./usage";
|
package/src/index.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type {
|
|
|
12
12
|
AssistantChatResumableOptions,
|
|
13
13
|
ResumableClientStorage,
|
|
14
14
|
} from "./transport/resumable";
|
|
15
|
-
export { frontendTools, type FrontendTools } from "
|
|
15
|
+
export { frontendTools, type FrontendTools } from "assistant-stream/ai-sdk";
|
|
16
16
|
export { injectQuoteContext } from "./model-context/injectQuoteContext";
|
|
17
17
|
export { unstable_injectInteractableContext } from "./model-context/injectInteractableContext";
|
|
18
18
|
export type { ThreadTokenUsage, TokenUsageExtractableMessage } from "./usage";
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
* `format` must also handle entries with `partial: true`, whose `state` carries
|
|
21
21
|
* only the fields that changed since the model's last known state.
|
|
22
22
|
*
|
|
23
|
+
* @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release.
|
|
24
|
+
*
|
|
23
25
|
* @example
|
|
24
26
|
* ```ts
|
|
25
27
|
* import { convertToModelMessages, streamText } from "ai";
|
|
@@ -28,6 +30,7 @@ import {
|
|
|
28
30
|
* export async function POST(req: Request) {
|
|
29
31
|
* const { messages } = await req.json();
|
|
30
32
|
* const result = streamText({
|
|
33
|
+
* abortSignal: req.signal,
|
|
31
34
|
* model: myModel,
|
|
32
35
|
* messages: await convertToModelMessages(unstable_injectInteractableContext(messages)),
|
|
33
36
|
* });
|
|
@@ -27,6 +27,7 @@ const getQuoteText = (metadata: unknown): string | undefined => {
|
|
|
27
27
|
* export async function POST(req: Request) {
|
|
28
28
|
* const { messages } = await req.json();
|
|
29
29
|
* const result = streamText({
|
|
30
|
+
* abortSignal: req.signal,
|
|
30
31
|
* model: myModel,
|
|
31
32
|
* messages: await convertToModelMessages(injectQuoteContext(messages)),
|
|
32
33
|
* });
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { flushTapSync } from "@assistant-ui/tap";
|
|
1
2
|
// @vitest-environment jsdom
|
|
2
3
|
|
|
3
|
-
import { StrictMode, type ReactNode } from "react";
|
|
4
|
+
import { StrictMode, useLayoutEffect, type ReactNode } from "react";
|
|
4
5
|
import { act, render, waitFor } from "@testing-library/react";
|
|
5
6
|
import { AuiConfig, AuiProvider, useAui } from "@assistant-ui/store";
|
|
6
7
|
import type { ChatTransport, UIMessage } from "ai";
|
|
7
|
-
import { describe, expect, it } from "vitest";
|
|
8
|
+
import { describe, expect, it, vi } from "vitest";
|
|
8
9
|
import { AISDKChat } from "./AISDKChat";
|
|
9
10
|
import {
|
|
10
11
|
createCancellableTransport,
|
|
@@ -66,3 +67,57 @@ describe("AISDKChat legacy useAui host integration", () => {
|
|
|
66
67
|
await waitFor(() => expect(getCancelCount()).toBe(1));
|
|
67
68
|
});
|
|
68
69
|
});
|
|
70
|
+
|
|
71
|
+
describe("replacement transports", () => {
|
|
72
|
+
it("routes sends through a replacement transport", async () => {
|
|
73
|
+
const emptyStream = () =>
|
|
74
|
+
new ReadableStream({ start: (controller) => controller.close() });
|
|
75
|
+
const sendA = vi.fn(async () => emptyStream());
|
|
76
|
+
const sendB = vi.fn(async () => emptyStream());
|
|
77
|
+
const transportA: ChatTransport<UIMessage> = {
|
|
78
|
+
sendMessages: sendA,
|
|
79
|
+
reconnectToStream: vi.fn(),
|
|
80
|
+
};
|
|
81
|
+
const transportB: ChatTransport<UIMessage> = {
|
|
82
|
+
sendMessages: sendB,
|
|
83
|
+
reconnectToStream: vi.fn(),
|
|
84
|
+
};
|
|
85
|
+
let initialClient: ReturnType<typeof useAui> | undefined;
|
|
86
|
+
let currentClient: ReturnType<typeof useAui> | undefined;
|
|
87
|
+
const CaptureClient = () => {
|
|
88
|
+
const aui = useAui();
|
|
89
|
+
initialClient ??= aui;
|
|
90
|
+
currentClient = aui;
|
|
91
|
+
return null;
|
|
92
|
+
};
|
|
93
|
+
const SendOnLayout = () => {
|
|
94
|
+
const aui = useAui();
|
|
95
|
+
useLayoutEffect(() => {
|
|
96
|
+
flushTapSync(() => {
|
|
97
|
+
aui.composer.setText("hello");
|
|
98
|
+
aui.composer.send();
|
|
99
|
+
});
|
|
100
|
+
}, [aui]);
|
|
101
|
+
return null;
|
|
102
|
+
};
|
|
103
|
+
const App = ({
|
|
104
|
+
transport,
|
|
105
|
+
send = false,
|
|
106
|
+
}: {
|
|
107
|
+
transport: ChatTransport<UIMessage>;
|
|
108
|
+
send?: boolean;
|
|
109
|
+
}) => (
|
|
110
|
+
<AuiProvider config={AuiConfig({ threads: AISDKChat({ transport }) })}>
|
|
111
|
+
<CaptureClient />
|
|
112
|
+
{send && <SendOnLayout />}
|
|
113
|
+
</AuiProvider>
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const view = render(<App transport={transportA} />);
|
|
117
|
+
view.rerender(<App transport={transportB} send />);
|
|
118
|
+
|
|
119
|
+
await waitFor(() => expect(sendB).toHaveBeenCalledOnce());
|
|
120
|
+
expect(sendA).not.toHaveBeenCalled();
|
|
121
|
+
expect(currentClient).toBe(initialClient);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -79,10 +79,12 @@ import { AI_SDK_SDK } from "./sdkIdentity";
|
|
|
79
79
|
describe("AISDKThreads cloud", () => {
|
|
80
80
|
it("reloads history when switching a keyed cloud thread", async () => {
|
|
81
81
|
const cloud = {} as AssistantCloud;
|
|
82
|
+
const scopeId = "workspace-1";
|
|
82
83
|
const handle = createAssistantClient(
|
|
83
84
|
AuiConfig({
|
|
84
85
|
threads: AISDKThreads({
|
|
85
86
|
cloud,
|
|
87
|
+
scopeId,
|
|
86
88
|
threadId: "t1",
|
|
87
89
|
}),
|
|
88
90
|
}),
|
|
@@ -98,6 +100,7 @@ describe("AISDKThreads cloud", () => {
|
|
|
98
100
|
});
|
|
99
101
|
expect(mocks.useCloudThreadListAdapter).toHaveBeenCalledWith({
|
|
100
102
|
cloud,
|
|
103
|
+
scopeId,
|
|
101
104
|
sdk: AI_SDK_SDK,
|
|
102
105
|
});
|
|
103
106
|
const afterFirst = load.mock.calls.length;
|