@assistant-ui/ai-sdk 0.0.11 → 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 +48 -22
- package/dist/converters/convertMessage.js.map +1 -1
- 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 -109
- 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 +5 -5
- 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 +11 -10
- package/src/converters/convertMessage.test.ts +120 -2
- package/src/converters/convertMessage.tool-args-status.test.tsx +185 -0
- package/src/converters/convertMessage.ts +60 -19
- package/src/converters/toCreateMessage.test.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 +111 -0
- package/src/runtime/useExternalHistory.ts +14 -210
- package/src/runtime/useStreamingTiming.ts +2 -2
- package/src/tools/generativeTools.test.ts +1 -1
- package/src/tools/generativeTools.ts +5 -6
- 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
|
@@ -1390,7 +1390,9 @@ describe("AISDKMessageConverter", () => {
|
|
|
1390
1390
|
const call = terminal[0]?.content.find(
|
|
1391
1391
|
(part): part is any => part.type === "tool-call",
|
|
1392
1392
|
);
|
|
1393
|
-
expect(call?.args).toEqual({
|
|
1393
|
+
expect(Object.fromEntries(Object.entries(call?.args ?? {}))).toEqual({
|
|
1394
|
+
city: "NYC",
|
|
1395
|
+
});
|
|
1394
1396
|
expect(call?.result).toEqual({ temp: 70 });
|
|
1395
1397
|
});
|
|
1396
1398
|
|
|
@@ -2094,7 +2096,10 @@ describe("AISDKMessageConverter", () => {
|
|
|
2094
2096
|
const toolCall = converted[0]?.content.find(
|
|
2095
2097
|
(part): part is any => part.type === "tool-call",
|
|
2096
2098
|
);
|
|
2097
|
-
expect(toolCall?.args).toEqual({
|
|
2099
|
+
expect(Object.fromEntries(Object.entries(toolCall?.args ?? {}))).toEqual({
|
|
2100
|
+
city: "NYC",
|
|
2101
|
+
units: "F",
|
|
2102
|
+
});
|
|
2098
2103
|
expect(toolCall?.argsText).toBe('{"city":"NYC","units":"F"}');
|
|
2099
2104
|
});
|
|
2100
2105
|
|
|
@@ -2168,6 +2173,7 @@ describe("AISDKMessageConverter", () => {
|
|
|
2168
2173
|
|
|
2169
2174
|
expect(a.argsText).toBe('{"a":1,"b":2}');
|
|
2170
2175
|
expect(b.argsText).toBe('{"b":2,"a":1}');
|
|
2176
|
+
expect(Object.keys(b.args)).toEqual(["a", "b"]);
|
|
2171
2177
|
|
|
2172
2178
|
// Both entries must survive a reconversion: the key-order entries are gone
|
|
2173
2179
|
// by now, so a cache miss would re-serialize B in raw key order.
|
|
@@ -2207,4 +2213,116 @@ describe("AISDKMessageConverter", () => {
|
|
|
2207
2213
|
}
|
|
2208
2214
|
expect(stableStringifySpy).toHaveBeenCalledTimes(1);
|
|
2209
2215
|
});
|
|
2216
|
+
|
|
2217
|
+
it("skips null parts and parts without a type", () => {
|
|
2218
|
+
const converted = AISDKMessageConverter.toThreadMessages([
|
|
2219
|
+
{
|
|
2220
|
+
id: "u1",
|
|
2221
|
+
role: "user",
|
|
2222
|
+
parts: [null, { text: "no type" }, { type: "text", text: "hi" }],
|
|
2223
|
+
},
|
|
2224
|
+
{
|
|
2225
|
+
id: "a1",
|
|
2226
|
+
role: "assistant",
|
|
2227
|
+
parts: [null, { text: "no type" }, { type: "text", text: "yo" }],
|
|
2228
|
+
},
|
|
2229
|
+
] as any);
|
|
2230
|
+
|
|
2231
|
+
expect(converted[0]?.content).toMatchObject([{ type: "text", text: "hi" }]);
|
|
2232
|
+
expect(converted[0]?.attachments).toEqual([]);
|
|
2233
|
+
expect(converted[1]?.content).toMatchObject([{ type: "text", text: "yo" }]);
|
|
2234
|
+
});
|
|
2235
|
+
|
|
2236
|
+
it("joins the text parts of a system message into one text part", () => {
|
|
2237
|
+
const converted = AISDKMessageConverter.toThreadMessages([
|
|
2238
|
+
{
|
|
2239
|
+
id: "s1",
|
|
2240
|
+
role: "system",
|
|
2241
|
+
parts: [
|
|
2242
|
+
{
|
|
2243
|
+
type: "text",
|
|
2244
|
+
text: "be ",
|
|
2245
|
+
providerMetadata: { p1: { k: "a" }, p2: { k: "b" } },
|
|
2246
|
+
},
|
|
2247
|
+
{ type: "text", text: "brief", providerMetadata: { p2: { k: "c" } } },
|
|
2248
|
+
],
|
|
2249
|
+
},
|
|
2250
|
+
{ id: "s2", role: "system", parts: [] },
|
|
2251
|
+
] as any);
|
|
2252
|
+
|
|
2253
|
+
expect(converted[0]?.content).toMatchObject([
|
|
2254
|
+
{
|
|
2255
|
+
type: "text",
|
|
2256
|
+
text: "be brief",
|
|
2257
|
+
providerMetadata: { p1: { k: "a" }, p2: { k: "c" } },
|
|
2258
|
+
},
|
|
2259
|
+
]);
|
|
2260
|
+
expect(converted[1]?.content).toMatchObject([{ type: "text", text: "" }]);
|
|
2261
|
+
expect(converted[1]?.content[0]).not.toHaveProperty("providerMetadata");
|
|
2262
|
+
});
|
|
2263
|
+
|
|
2264
|
+
it("reads a user text part without text as empty text", () => {
|
|
2265
|
+
const converted = AISDKMessageConverter.toThreadMessages([
|
|
2266
|
+
{ id: "u1", role: "user", parts: [{ type: "text" }] },
|
|
2267
|
+
] as any);
|
|
2268
|
+
|
|
2269
|
+
expect(converted[0]?.content).toMatchObject([{ type: "text", text: "" }]);
|
|
2270
|
+
});
|
|
2271
|
+
|
|
2272
|
+
it("skips a file part without a url and floors a missing mediaType", () => {
|
|
2273
|
+
const converted = AISDKMessageConverter.toThreadMessages([
|
|
2274
|
+
{
|
|
2275
|
+
id: "u1",
|
|
2276
|
+
role: "user",
|
|
2277
|
+
parts: [{ type: "file", mediaType: "image/png", filename: "a.png" }],
|
|
2278
|
+
},
|
|
2279
|
+
{
|
|
2280
|
+
id: "a1",
|
|
2281
|
+
role: "assistant",
|
|
2282
|
+
parts: [
|
|
2283
|
+
{ type: "file", mediaType: "image/png" },
|
|
2284
|
+
{ type: "file", url: "https://cdn/file.bin" },
|
|
2285
|
+
{ type: "reasoning-file", mediaType: "image/png" },
|
|
2286
|
+
{ type: "reasoning-file", url: "https://cdn/thought.bin" },
|
|
2287
|
+
],
|
|
2288
|
+
},
|
|
2289
|
+
] as any);
|
|
2290
|
+
|
|
2291
|
+
expect(converted[0]?.attachments).toEqual([]);
|
|
2292
|
+
expect(converted[1]?.content).toMatchObject([
|
|
2293
|
+
{
|
|
2294
|
+
type: "file",
|
|
2295
|
+
data: "https://cdn/file.bin",
|
|
2296
|
+
mimeType: "unknown/unknown",
|
|
2297
|
+
},
|
|
2298
|
+
{
|
|
2299
|
+
type: "file",
|
|
2300
|
+
data: "https://cdn/thought.bin",
|
|
2301
|
+
mimeType: "unknown/unknown",
|
|
2302
|
+
},
|
|
2303
|
+
]);
|
|
2304
|
+
});
|
|
2305
|
+
|
|
2306
|
+
it("gives a dynamic tool call without a toolName an empty name", () => {
|
|
2307
|
+
const converted = AISDKMessageConverter.toThreadMessages([
|
|
2308
|
+
{
|
|
2309
|
+
id: "a1",
|
|
2310
|
+
role: "assistant",
|
|
2311
|
+
parts: [
|
|
2312
|
+
{
|
|
2313
|
+
type: "dynamic-tool",
|
|
2314
|
+
toolCallId: "tc-1",
|
|
2315
|
+
state: "input-available",
|
|
2316
|
+
input: {},
|
|
2317
|
+
},
|
|
2318
|
+
],
|
|
2319
|
+
},
|
|
2320
|
+
] as any);
|
|
2321
|
+
|
|
2322
|
+
expect(converted[0]?.content[0]).toMatchObject({
|
|
2323
|
+
type: "tool-call",
|
|
2324
|
+
toolCallId: "tc-1",
|
|
2325
|
+
toolName: "",
|
|
2326
|
+
});
|
|
2327
|
+
});
|
|
2210
2328
|
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
getPartialJsonObjectFieldState,
|
|
4
|
+
getPartialJsonObjectMeta,
|
|
5
|
+
} from "assistant-stream/utils";
|
|
6
|
+
import {
|
|
7
|
+
AISDKMessageConverter,
|
|
8
|
+
type AISDKMessageConverterMetadata,
|
|
9
|
+
} from "./convertMessage";
|
|
10
|
+
|
|
11
|
+
describe("AI SDK tool argument completion", () => {
|
|
12
|
+
it.each([{}, { city: "Paris", unit: "c" }])(
|
|
13
|
+
"marks input-available arguments complete before tool execution ends for %j",
|
|
14
|
+
(input) => {
|
|
15
|
+
const convert = (toolState: "input-streaming" | "input-available") => {
|
|
16
|
+
const message = AISDKMessageConverter.toThreadMessages(
|
|
17
|
+
[
|
|
18
|
+
{
|
|
19
|
+
id: "a1",
|
|
20
|
+
role: "assistant",
|
|
21
|
+
parts: [
|
|
22
|
+
{
|
|
23
|
+
type: "tool-weather",
|
|
24
|
+
toolCallId: "tc-1",
|
|
25
|
+
state: toolState,
|
|
26
|
+
input,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
true,
|
|
32
|
+
)[0]!;
|
|
33
|
+
return message.content.find((part) => part.type === "tool-call")!;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const streaming = convert("input-streaming");
|
|
37
|
+
expect(getPartialJsonObjectMeta(streaming.args)?.state).toBe("partial");
|
|
38
|
+
|
|
39
|
+
const available = convert("input-available");
|
|
40
|
+
expect(Object.fromEntries(Object.entries(available.args))).toEqual(input);
|
|
41
|
+
expect(JSON.parse(available.argsText)).toEqual(input);
|
|
42
|
+
expect(getPartialJsonObjectMeta(available.args)?.state).toBe("complete");
|
|
43
|
+
for (const key of Object.keys(input)) {
|
|
44
|
+
expect(getPartialJsonObjectFieldState(available.args, [key])).toBe(
|
|
45
|
+
"complete",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
it("keeps the streaming frontier partial until input becomes available", () => {
|
|
52
|
+
const message = AISDKMessageConverter.toThreadMessages(
|
|
53
|
+
[
|
|
54
|
+
{
|
|
55
|
+
id: "a1",
|
|
56
|
+
role: "assistant",
|
|
57
|
+
parts: [
|
|
58
|
+
{
|
|
59
|
+
type: "tool-weather",
|
|
60
|
+
toolCallId: "tc-1",
|
|
61
|
+
state: "input-streaming",
|
|
62
|
+
input: { city: "Paris", unit: "c" },
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
true,
|
|
68
|
+
)[0]!;
|
|
69
|
+
const part = message.content.find((item) => item.type === "tool-call")!;
|
|
70
|
+
expect(getPartialJsonObjectMeta(part.args)?.state).toBe("partial");
|
|
71
|
+
expect(getPartialJsonObjectFieldState(part.args, ["city"])).toBe(
|
|
72
|
+
"complete",
|
|
73
|
+
);
|
|
74
|
+
expect(getPartialJsonObjectFieldState(part.args, ["unit"])).toBe("partial");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it.each([
|
|
78
|
+
{
|
|
79
|
+
state: "approval-responded",
|
|
80
|
+
approval: { id: "approval-1", approved: true },
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
state: "output-available",
|
|
84
|
+
output: { progress: 50 },
|
|
85
|
+
preliminary: true,
|
|
86
|
+
},
|
|
87
|
+
])("marks settled $state input complete while the part runs", (tool) => {
|
|
88
|
+
const message = AISDKMessageConverter.toThreadMessages(
|
|
89
|
+
[
|
|
90
|
+
{
|
|
91
|
+
id: "a1",
|
|
92
|
+
role: "assistant",
|
|
93
|
+
parts: [
|
|
94
|
+
{
|
|
95
|
+
type: "tool-weather",
|
|
96
|
+
toolCallId: "tc-1",
|
|
97
|
+
input: { city: "Paris" },
|
|
98
|
+
...tool,
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
] as any,
|
|
103
|
+
true,
|
|
104
|
+
)[0]!;
|
|
105
|
+
const part = message.content.find((item) => item.type === "tool-call")!;
|
|
106
|
+
expect(message.status?.type).toBe("running");
|
|
107
|
+
expect(getPartialJsonObjectMeta(part.args)?.state).toBe("complete");
|
|
108
|
+
expect(getPartialJsonObjectFieldState(part.args, ["city"])).toBe(
|
|
109
|
+
"complete",
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("reuses marked arguments when the settled input object is unchanged", () => {
|
|
114
|
+
const input = { weather: { city: "Paris" } };
|
|
115
|
+
const metadata: AISDKMessageConverterMetadata = {
|
|
116
|
+
toolArgsTextCache: new WeakMap(),
|
|
117
|
+
};
|
|
118
|
+
const convert = () =>
|
|
119
|
+
AISDKMessageConverter.toThreadMessages(
|
|
120
|
+
[
|
|
121
|
+
{
|
|
122
|
+
id: "a1",
|
|
123
|
+
role: "assistant",
|
|
124
|
+
parts: [
|
|
125
|
+
{
|
|
126
|
+
type: "tool-weather",
|
|
127
|
+
toolCallId: "tc-1",
|
|
128
|
+
state: "input-available",
|
|
129
|
+
input,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
true,
|
|
135
|
+
metadata,
|
|
136
|
+
)[0]!.content.find((part) => part.type === "tool-call")!;
|
|
137
|
+
|
|
138
|
+
const first = convert();
|
|
139
|
+
const second = convert();
|
|
140
|
+
expect(first.args).toBe(second.args);
|
|
141
|
+
expect(first.args).not.toBe(input);
|
|
142
|
+
expect(first.args.weather).toBe(input.weather);
|
|
143
|
+
expect(first.argsText).toBe(second.argsText);
|
|
144
|
+
expect(getPartialJsonObjectMeta(second.args)?.state).toBe("complete");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it.each([
|
|
148
|
+
'{"__proto__":{"polluted":true}}',
|
|
149
|
+
'{"constructor":{"prototype":{"polluted":true}}}',
|
|
150
|
+
])(
|
|
151
|
+
"marks settled prototype-named input complete while the tool runs: %s",
|
|
152
|
+
(json) => {
|
|
153
|
+
const input = JSON.parse(json);
|
|
154
|
+
const message = AISDKMessageConverter.toThreadMessages(
|
|
155
|
+
[
|
|
156
|
+
{
|
|
157
|
+
id: "a1",
|
|
158
|
+
role: "assistant",
|
|
159
|
+
parts: [
|
|
160
|
+
{
|
|
161
|
+
type: "tool-weather",
|
|
162
|
+
toolCallId: "tc-1",
|
|
163
|
+
state: "input-available",
|
|
164
|
+
input,
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
true,
|
|
170
|
+
)[0]!;
|
|
171
|
+
const part = message.content.find((item) => item.type === "tool-call")!;
|
|
172
|
+
|
|
173
|
+
expect(message.status?.type).toBe("running");
|
|
174
|
+
expect(part.args).not.toBe(input);
|
|
175
|
+
expect(Object.entries(part.args)).toEqual(Object.entries(input));
|
|
176
|
+
expect(Object.getPrototypeOf(part.args)).toBe(Object.prototype);
|
|
177
|
+
expect(Object.prototype).not.toHaveProperty("polluted");
|
|
178
|
+
expect(JSON.parse(part.argsText)).toEqual(input);
|
|
179
|
+
expect(getPartialJsonObjectMeta(part.args)?.state).toBe("complete");
|
|
180
|
+
expect(
|
|
181
|
+
getPartialJsonObjectFieldState(part.args, [Object.keys(input)[0]!]),
|
|
182
|
+
).toBe("complete");
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
});
|
|
@@ -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 {
|
|
@@ -30,11 +31,12 @@ import {
|
|
|
30
31
|
} from "@assistant-ui/core";
|
|
31
32
|
import { normalizeToolApprovalAnswers } from "./toolApprovalAnswers";
|
|
32
33
|
import { stableStringifyToolArgs } from "@assistant-ui/core/internal";
|
|
34
|
+
import { markPartialJsonObjectComplete } from "assistant-stream/internal";
|
|
33
35
|
import {
|
|
34
36
|
parsePartialJsonObject,
|
|
35
37
|
type ReadonlyJSONObject,
|
|
36
38
|
} from "assistant-stream/utils";
|
|
37
|
-
import { unwrapModelContentEnvelope } from "
|
|
39
|
+
import { unwrapModelContentEnvelope } from "assistant-stream/internal";
|
|
38
40
|
|
|
39
41
|
type MessageMetadata = ThreadMessageLike["metadata"];
|
|
40
42
|
|
|
@@ -66,13 +68,15 @@ export type AISDKMessageConverterMetadata =
|
|
|
66
68
|
useExternalMessageConverter.Metadata & {
|
|
67
69
|
toolArgsKeyOrderCache?: Map<string, Map<string, string[]>>;
|
|
68
70
|
/**
|
|
69
|
-
* Frozen
|
|
70
|
-
* by call, since the text carries
|
|
71
|
-
* call/input pair skips serialization;
|
|
72
|
-
* the input is unreachable.
|
|
73
|
-
* 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.
|
|
74
75
|
*/
|
|
75
|
-
toolArgsTextCache?: WeakMap<
|
|
76
|
+
toolArgsTextCache?: WeakMap<
|
|
77
|
+
ReadonlyJSONObject,
|
|
78
|
+
Map<string, { argsText: string; args: ReadonlyJSONObject }>
|
|
79
|
+
>;
|
|
76
80
|
toolLastInputCache?: Map<string, ReadonlyJSONObject>;
|
|
77
81
|
mcpAppMetadataCache?: Map<string, McpAppMetadata>;
|
|
78
82
|
toolArtifacts?: ReadonlyMap<string, unknown>;
|
|
@@ -457,6 +461,25 @@ const uiPartStateToStatus = (
|
|
|
457
461
|
return undefined;
|
|
458
462
|
};
|
|
459
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
|
+
|
|
460
483
|
function convertParts(
|
|
461
484
|
message: UIMessage,
|
|
462
485
|
metadata: AISDKMessageConverterMetadata,
|
|
@@ -468,6 +491,7 @@ function convertParts(
|
|
|
468
491
|
const converted = message.parts
|
|
469
492
|
.filter(
|
|
470
493
|
(p) =>
|
|
494
|
+
typeof p?.type === "string" &&
|
|
471
495
|
p.type !== "step-start" &&
|
|
472
496
|
(message.role !== "user" || p.type !== "file"),
|
|
473
497
|
)
|
|
@@ -476,7 +500,7 @@ function convertParts(
|
|
|
476
500
|
const status = uiPartStateToStatus(part.state);
|
|
477
501
|
return {
|
|
478
502
|
type: "text",
|
|
479
|
-
text: part.text,
|
|
503
|
+
text: part.text ?? "",
|
|
480
504
|
...(status != null ? { status } : undefined),
|
|
481
505
|
...(part.providerMetadata != null
|
|
482
506
|
? {
|
|
@@ -501,7 +525,7 @@ function convertParts(
|
|
|
501
525
|
}
|
|
502
526
|
|
|
503
527
|
if (isToolUIPart(part)) {
|
|
504
|
-
const toolName = getToolName(part);
|
|
528
|
+
const toolName = getToolName(part) ?? "";
|
|
505
529
|
const toolCallId = part.toolCallId;
|
|
506
530
|
const argsKeyOrderCacheKey = `${message.id}:${toolCallId}`;
|
|
507
531
|
|
|
@@ -565,20 +589,31 @@ function convertParts(
|
|
|
565
589
|
// re-serializing large args while the call keeps that input. Arrival
|
|
566
590
|
// order only matters while args stream, so the key-order entry is
|
|
567
591
|
// released.
|
|
592
|
+
const inputArgs = args;
|
|
568
593
|
const frozen =
|
|
569
|
-
metadata.toolArgsTextCache?.get(
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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;
|
|
573
600
|
} else {
|
|
574
601
|
argsText = stableStringifyToolArgs(
|
|
575
602
|
metadata.toolArgsKeyOrderCache,
|
|
576
603
|
argsKeyOrderCacheKey,
|
|
577
604
|
args,
|
|
578
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);
|
|
579
614
|
metadata.toolArgsTextCache?.set(
|
|
580
|
-
|
|
581
|
-
frozen.set(argsKeyOrderCacheKey, argsText),
|
|
615
|
+
inputArgs,
|
|
616
|
+
frozen.set(argsKeyOrderCacheKey, { argsText, args }),
|
|
582
617
|
);
|
|
583
618
|
}
|
|
584
619
|
metadata.toolArgsKeyOrderCache?.delete(argsKeyOrderCacheKey);
|
|
@@ -647,10 +682,11 @@ function convertParts(
|
|
|
647
682
|
}
|
|
648
683
|
|
|
649
684
|
if (part.type === "file") {
|
|
685
|
+
if (typeof part.url !== "string") return null;
|
|
650
686
|
return {
|
|
651
687
|
type: "file",
|
|
652
688
|
data: part.url,
|
|
653
|
-
mimeType: part.mediaType,
|
|
689
|
+
mimeType: part.mediaType ?? "unknown/unknown",
|
|
654
690
|
...(part.filename != null && { filename: part.filename }),
|
|
655
691
|
} satisfies FileMessagePart;
|
|
656
692
|
}
|
|
@@ -684,10 +720,11 @@ function convertParts(
|
|
|
684
720
|
}
|
|
685
721
|
|
|
686
722
|
if (isReasoningFileUIPart(part)) {
|
|
723
|
+
if (typeof part.url !== "string") return null;
|
|
687
724
|
return {
|
|
688
725
|
type: "file",
|
|
689
726
|
data: part.url,
|
|
690
|
-
mimeType: part.mediaType,
|
|
727
|
+
mimeType: part.mediaType ?? "unknown/unknown",
|
|
691
728
|
} satisfies FileMessagePart;
|
|
692
729
|
}
|
|
693
730
|
|
|
@@ -727,7 +764,10 @@ export const AISDKMessageConverter = unstable_createMessageConverter(
|
|
|
727
764
|
createdAt,
|
|
728
765
|
content,
|
|
729
766
|
attachments: message.parts
|
|
730
|
-
?.filter(
|
|
767
|
+
?.filter(
|
|
768
|
+
(p): p is FileUIPart =>
|
|
769
|
+
p?.type === "file" && typeof p.url === "string",
|
|
770
|
+
)
|
|
731
771
|
.map((part, idx) => {
|
|
732
772
|
const mediaType = part.mediaType ?? "unknown/unknown";
|
|
733
773
|
const isImage = mediaType.startsWith("image/");
|
|
@@ -766,7 +806,8 @@ export const AISDKMessageConverter = unstable_createMessageConverter(
|
|
|
766
806
|
role: message.role,
|
|
767
807
|
id: message.id,
|
|
768
808
|
createdAt,
|
|
769
|
-
content
|
|
809
|
+
content:
|
|
810
|
+
message.role === "system" ? toSystemContent(content) : content,
|
|
770
811
|
...(message.role === "assistant" &&
|
|
771
812
|
metadata.cancelledStatusMessageIds?.has(message.id)
|
|
772
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,
|
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;
|