@assistant-ui/ai-sdk 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/aiSDKFormatAdapter.d.ts +2 -8
- package/dist/adapters/aiSDKFormatAdapter.js +1 -25
- package/dist/converters/convertMessage.d.ts.map +1 -1
- package/dist/converters/convertMessage.js +1 -0
- package/dist/converters/convertMessage.js.map +1 -1
- package/dist/runtime/AISDKThreads.d.ts.map +1 -1
- package/dist/runtime/AISDKThreads.js +90 -65
- package/dist/runtime/AISDKThreads.js.map +1 -1
- package/dist/runtime/sdkIdentity.d.ts +6 -0
- package/dist/runtime/sdkIdentity.d.ts.map +1 -0
- package/dist/runtime/sdkIdentity.js +9 -0
- package/dist/runtime/sdkIdentity.js.map +1 -0
- package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
- package/dist/runtime/useAISDKRuntime.js +8 -1
- package/dist/runtime/useAISDKRuntime.js.map +1 -1
- package/dist/runtime/useChatRuntime.d.ts.map +1 -1
- package/dist/runtime/useChatRuntime.js +5 -1
- package/dist/runtime/useChatRuntime.js.map +1 -1
- package/dist/runtime/useChatThread.js +1 -1
- package/dist/runtime/useChatThread.js.map +1 -1
- package/dist/runtime/useExternalHistory.d.ts.map +1 -1
- package/dist/runtime/useExternalHistory.js +4 -1
- package/dist/runtime/useExternalHistory.js.map +1 -1
- package/package.json +6 -6
- package/src/adapters/aiSDKFormatAdapter.ts +4 -41
- package/src/converters/convertMessage.test.ts +14 -0
- package/src/converters/convertMessage.ts +1 -0
- package/src/runtime/AISDKThreads.cloud.test.ts +12 -3
- package/src/runtime/AISDKThreads.test.ts +131 -8
- package/src/runtime/AISDKThreads.ts +31 -3
- package/src/runtime/sdkIdentity.ts +9 -0
- package/src/runtime/useAISDKRuntime.test.ts +39 -1
- package/src/runtime/useAISDKRuntime.ts +17 -1
- package/src/runtime/useChatRuntime.test.ts +0 -1
- package/src/runtime/useChatRuntime.ts +2 -1
- package/src/runtime/useChatThread.test.ts +74 -0
- package/src/runtime/useChatThread.ts +1 -1
- package/src/runtime/useExternalHistory.test.ts +29 -0
- package/src/runtime/useExternalHistory.ts +4 -1
- package/src/transport/AssistantChatTransport.test.ts +1 -9
- package/dist/adapters/aiSDKFormatAdapter.d.ts.map +0 -1
- package/dist/adapters/aiSDKFormatAdapter.js.map +0 -1
|
@@ -54,23 +54,28 @@ const mocks = vi.hoisted(() => {
|
|
|
54
54
|
return { history };
|
|
55
55
|
},
|
|
56
56
|
};
|
|
57
|
-
return {
|
|
57
|
+
return {
|
|
58
|
+
adapter,
|
|
59
|
+
useCloudThreadListAdapter: vi.fn(() => adapter),
|
|
60
|
+
};
|
|
58
61
|
});
|
|
59
62
|
|
|
60
63
|
vi.mock("@assistant-ui/core/react", async (importOriginal) => ({
|
|
61
64
|
...(await importOriginal<typeof import("@assistant-ui/core/react")>()),
|
|
62
|
-
useCloudThreadListAdapter:
|
|
65
|
+
useCloudThreadListAdapter: mocks.useCloudThreadListAdapter,
|
|
63
66
|
}));
|
|
64
67
|
|
|
65
68
|
import { AISDKThreads } from "./AISDKThreads";
|
|
66
69
|
import { createCancellableTransport } from "./__tests__/controlled-transport";
|
|
70
|
+
import { AI_SDK_SDK } from "./sdkIdentity";
|
|
67
71
|
|
|
68
72
|
describe("AISDKThreads cloud", () => {
|
|
69
73
|
it("reloads history when switching a keyed cloud thread", async () => {
|
|
74
|
+
const cloud = {} as AssistantCloud;
|
|
70
75
|
const handle = createAssistantClient(
|
|
71
76
|
AuiConfig({
|
|
72
77
|
threads: AISDKThreads({
|
|
73
|
-
cloud
|
|
78
|
+
cloud,
|
|
74
79
|
threadId: "t1",
|
|
75
80
|
}),
|
|
76
81
|
}),
|
|
@@ -84,6 +89,10 @@ describe("AISDKThreads cloud", () => {
|
|
|
84
89
|
await vi.waitFor(() => {
|
|
85
90
|
expect(load).toHaveBeenCalled();
|
|
86
91
|
});
|
|
92
|
+
expect(mocks.useCloudThreadListAdapter).toHaveBeenCalledWith({
|
|
93
|
+
cloud,
|
|
94
|
+
sdk: AI_SDK_SDK,
|
|
95
|
+
});
|
|
87
96
|
const afterFirst = load.mock.calls.length;
|
|
88
97
|
flushTapSync(() => aui.threads.switchToThread("t2"));
|
|
89
98
|
await vi.waitFor(() => {
|
|
@@ -251,17 +251,28 @@ describe("AISDKThreads", () => {
|
|
|
251
251
|
}
|
|
252
252
|
});
|
|
253
253
|
|
|
254
|
-
it("forwards ChatInit callbacks to each thread's chat", async () => {
|
|
254
|
+
it("forwards ChatInit callbacks to each thread's chat from the latest render", async () => {
|
|
255
255
|
const { transport, emit, close } = createControlledTransport();
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
256
|
+
const onFinishA = vi.fn();
|
|
257
|
+
const onFinishB = vi.fn();
|
|
258
|
+
let onFinish = onFinishA;
|
|
259
|
+
const listeners = new Set<() => void>();
|
|
260
|
+
const handle = createAssistantClient({
|
|
261
|
+
getConfig: () =>
|
|
262
|
+
AuiConfig({
|
|
263
|
+
threads: AISDKThreads({ transport: () => transport, onFinish }),
|
|
264
|
+
}),
|
|
265
|
+
subscribe: (listener) => {
|
|
266
|
+
listeners.add(listener);
|
|
267
|
+
return () => listeners.delete(listener);
|
|
268
|
+
},
|
|
269
|
+
});
|
|
262
270
|
handle.subscribe(() => {});
|
|
263
271
|
const aui = handle.getClient();
|
|
264
272
|
|
|
273
|
+
onFinish = onFinishB;
|
|
274
|
+
flushTapSync(() => listeners.forEach((listener) => listener()));
|
|
275
|
+
|
|
265
276
|
flushTapSync(() => aui.composer.setText("hi"));
|
|
266
277
|
flushTapSync(() => aui.composer.send());
|
|
267
278
|
await vi.waitFor(() => {
|
|
@@ -271,11 +282,123 @@ describe("AISDKThreads", () => {
|
|
|
271
282
|
});
|
|
272
283
|
emit(...textReply("done"));
|
|
273
284
|
close();
|
|
274
|
-
await vi.waitFor(() => expect(
|
|
285
|
+
await vi.waitFor(() => expect(onFinishB).toHaveBeenCalledTimes(1));
|
|
286
|
+
expect(onFinishA).not.toHaveBeenCalled();
|
|
275
287
|
|
|
276
288
|
handle.destroy();
|
|
277
289
|
});
|
|
278
290
|
|
|
291
|
+
it("forwards the latest callbacks to a switched-away thread still streaming in the background", async () => {
|
|
292
|
+
const { transport, emit, close } = createControlledTransport();
|
|
293
|
+
const onFinishA = vi.fn();
|
|
294
|
+
const onFinishB = vi.fn();
|
|
295
|
+
let onFinish = onFinishA;
|
|
296
|
+
const listeners = new Set<() => void>();
|
|
297
|
+
const handle = createAssistantClient({
|
|
298
|
+
getConfig: () =>
|
|
299
|
+
AuiConfig({
|
|
300
|
+
threads: AISDKThreads({ transport: () => transport, onFinish }),
|
|
301
|
+
}),
|
|
302
|
+
subscribe: (listener) => {
|
|
303
|
+
listeners.add(listener);
|
|
304
|
+
return () => listeners.delete(listener);
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
handle.subscribe(() => {});
|
|
308
|
+
const aui = handle.getClient();
|
|
309
|
+
|
|
310
|
+
flushTapSync(() => aui.composer.setText("stream me"));
|
|
311
|
+
flushTapSync(() => aui.composer.send());
|
|
312
|
+
await vi.waitFor(() => {
|
|
313
|
+
expect(
|
|
314
|
+
handle.getClient().thread.getState().messages.length,
|
|
315
|
+
).toBeGreaterThan(0);
|
|
316
|
+
});
|
|
317
|
+
emit(
|
|
318
|
+
{ type: "start" },
|
|
319
|
+
{ type: "text-start", id: "t1" },
|
|
320
|
+
{ type: "text-delta", id: "t1", delta: "partial" },
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
flushTapSync(() => aui.threads.switchToNewThread());
|
|
324
|
+
onFinish = onFinishB;
|
|
325
|
+
flushTapSync(() => listeners.forEach((listener) => listener()));
|
|
326
|
+
|
|
327
|
+
emit({ type: "text-end", id: "t1" }, { type: "finish" });
|
|
328
|
+
close();
|
|
329
|
+
await vi.waitFor(() => expect(onFinishB).toHaveBeenCalledTimes(1));
|
|
330
|
+
expect(onFinishA).not.toHaveBeenCalled();
|
|
331
|
+
|
|
332
|
+
handle.destroy();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("forwards the latest callbacks to a cloud thread's chat", async () => {
|
|
336
|
+
const cloudThread = (id: string) => ({
|
|
337
|
+
id,
|
|
338
|
+
title: id,
|
|
339
|
+
is_archived: false,
|
|
340
|
+
last_message_at: null,
|
|
341
|
+
external_id: null,
|
|
342
|
+
metadata: null,
|
|
343
|
+
});
|
|
344
|
+
const cloud = {
|
|
345
|
+
threads: {
|
|
346
|
+
list: vi.fn(async () => ({ threads: [cloudThread("t1")] })),
|
|
347
|
+
create: vi.fn(),
|
|
348
|
+
update: vi.fn(),
|
|
349
|
+
delete: vi.fn(),
|
|
350
|
+
get: vi.fn(async (id: string) => cloudThread(id)),
|
|
351
|
+
messages: {
|
|
352
|
+
list: vi.fn(async () => ({ messages: [] })),
|
|
353
|
+
create: vi.fn(async () => ({ message_id: "remote-message-1" })),
|
|
354
|
+
update: vi.fn(),
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
runs: { stream: vi.fn(), report: vi.fn() },
|
|
358
|
+
telemetry: { enabled: false },
|
|
359
|
+
} as unknown as AssistantCloud;
|
|
360
|
+
const { transport, emit, close } = createControlledTransport();
|
|
361
|
+
const onFinishA = vi.fn();
|
|
362
|
+
const onFinishB = vi.fn();
|
|
363
|
+
let onFinish = onFinishA;
|
|
364
|
+
const listeners = new Set<() => void>();
|
|
365
|
+
const handle = createAssistantClient({
|
|
366
|
+
getConfig: () =>
|
|
367
|
+
AuiConfig({
|
|
368
|
+
threads: AISDKThreads({ cloud, threadId: "t1", transport, onFinish }),
|
|
369
|
+
}),
|
|
370
|
+
subscribe: (listener) => {
|
|
371
|
+
listeners.add(listener);
|
|
372
|
+
return () => listeners.delete(listener);
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
handle.subscribe(() => {});
|
|
376
|
+
try {
|
|
377
|
+
await handle.getClient().threads.getLoadThreadsPromise();
|
|
378
|
+
await vi.waitFor(() => {
|
|
379
|
+
expect(handle.getClient().threads.getState().mainThreadId).toBe("t1");
|
|
380
|
+
});
|
|
381
|
+
await vi.waitFor(() => {
|
|
382
|
+
expect(handle.getClient().thread.getState().isLoading).toBe(false);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
onFinish = onFinishB;
|
|
386
|
+
flushTapSync(() => listeners.forEach((listener) => listener()));
|
|
387
|
+
|
|
388
|
+
flushTapSync(() => handle.getClient().composer.setText("hi"));
|
|
389
|
+
flushTapSync(() => handle.getClient().composer.send());
|
|
390
|
+
await vi.waitFor(() => {
|
|
391
|
+
expect(handle.getClient().thread.getState().isRunning).toBe(true);
|
|
392
|
+
});
|
|
393
|
+
emit(...textReply("done"));
|
|
394
|
+
close();
|
|
395
|
+
await vi.waitFor(() => expect(onFinishB).toHaveBeenCalledTimes(1));
|
|
396
|
+
expect(onFinishA).not.toHaveBeenCalled();
|
|
397
|
+
} finally {
|
|
398
|
+
handle.destroy();
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
279
402
|
it("posts each thread's own id as the chat id", async () => {
|
|
280
403
|
const bodies: unknown[] = [];
|
|
281
404
|
const fetchStub = vi.fn(async (_url: unknown, init?: RequestInit) => {
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
} from "./useChatThread";
|
|
27
27
|
import { MessageRepository } from "@assistant-ui/core/internal";
|
|
28
28
|
import { useResourceCleanup } from "./useResourceCleanup";
|
|
29
|
+
import { AI_SDK_SDK } from "./sdkIdentity";
|
|
29
30
|
|
|
30
31
|
export type AISDKThreadsOptions<UI_MESSAGE extends UIMessage = UIMessage> =
|
|
31
32
|
Omit<ChatThreadOptions<UI_MESSAGE>, "id" | "transport" | "messages"> & {
|
|
@@ -62,16 +63,22 @@ type AISDKThreadChatOptions<UI_MESSAGE extends UIMessage = UIMessage> = Omit<
|
|
|
62
63
|
"cloud" | "threadId" | "onThreadIdChange"
|
|
63
64
|
>;
|
|
64
65
|
|
|
66
|
+
type ChatOptionsRef<UI_MESSAGE extends UIMessage> = {
|
|
67
|
+
current: AISDKThreadChatOptions<UI_MESSAGE> | undefined;
|
|
68
|
+
};
|
|
69
|
+
|
|
65
70
|
type ChatEntry<UI_MESSAGE extends UIMessage> = {
|
|
66
71
|
chat: Chat<UI_MESSAGE>;
|
|
67
72
|
transport: ChatTransport<UI_MESSAGE>;
|
|
68
73
|
repository: MessageRepository;
|
|
74
|
+
optionsRef: ChatOptionsRef<UI_MESSAGE>;
|
|
69
75
|
};
|
|
70
76
|
|
|
71
77
|
const createChatEntry = <UI_MESSAGE extends UIMessage>(
|
|
72
78
|
threadId: string,
|
|
73
79
|
options: AISDKThreadChatOptions<UI_MESSAGE> | undefined,
|
|
74
80
|
): ChatEntry<UI_MESSAGE> => {
|
|
81
|
+
const optionsRef: ChatOptionsRef<UI_MESSAGE> = { current: options };
|
|
75
82
|
const { chatInit } = splitChatThreadOptions(
|
|
76
83
|
options as ChatThreadOptions<UI_MESSAGE> | undefined,
|
|
77
84
|
);
|
|
@@ -84,9 +91,20 @@ const createChatEntry = <UI_MESSAGE extends UIMessage>(
|
|
|
84
91
|
? options.transport.__internal_clone()
|
|
85
92
|
: options.transport;
|
|
86
93
|
return {
|
|
87
|
-
chat: new Chat<UI_MESSAGE>({
|
|
94
|
+
chat: new Chat<UI_MESSAGE>({
|
|
95
|
+
...chatInit,
|
|
96
|
+
id: threadId,
|
|
97
|
+
transport,
|
|
98
|
+
onToolCall: (arg) => optionsRef.current?.onToolCall?.(arg),
|
|
99
|
+
onData: (arg) => optionsRef.current?.onData?.(arg),
|
|
100
|
+
onFinish: (arg) => optionsRef.current?.onFinish?.(arg),
|
|
101
|
+
onError: (arg) => optionsRef.current?.onError?.(arg),
|
|
102
|
+
sendAutomaticallyWhen: (arg) =>
|
|
103
|
+
optionsRef.current?.sendAutomaticallyWhen?.(arg) ?? false,
|
|
104
|
+
}),
|
|
88
105
|
transport,
|
|
89
106
|
repository: new MessageRepository(),
|
|
107
|
+
optionsRef,
|
|
90
108
|
};
|
|
91
109
|
};
|
|
92
110
|
|
|
@@ -116,9 +134,13 @@ const useAISDKChatThread = <UI_MESSAGE extends UIMessage = UIMessage>({
|
|
|
116
134
|
const [owned] = useState(() =>
|
|
117
135
|
cloud ? createChatEntry(threadId, options) : undefined,
|
|
118
136
|
);
|
|
119
|
-
const { chat, transport, repository } =
|
|
137
|
+
const { chat, transport, repository, optionsRef } =
|
|
120
138
|
owned ?? getOrCreateChatEntry(threadId, options, chats);
|
|
121
139
|
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (cloud) optionsRef.current = options;
|
|
142
|
+
});
|
|
143
|
+
|
|
122
144
|
useEffect(() => {
|
|
123
145
|
if (!cloud) return undefined;
|
|
124
146
|
return () => {
|
|
@@ -173,13 +195,19 @@ const useAISDKThreads = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
173
195
|
const [chats] = useState(() => new Map<string, ChatEntry<UI_MESSAGE>>());
|
|
174
196
|
const bindCloud = cloud !== undefined;
|
|
175
197
|
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
for (const { optionsRef } of chats.values()) {
|
|
200
|
+
optionsRef.current = threadOptions;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
176
204
|
useResourceCleanup(true, () => {
|
|
177
205
|
for (const { chat } of chats.values()) {
|
|
178
206
|
void chat.stop().catch(() => {});
|
|
179
207
|
}
|
|
180
208
|
});
|
|
181
209
|
|
|
182
|
-
const cloudAdapter = useCloudThreadListAdapter({ cloud });
|
|
210
|
+
const cloudAdapter = useCloudThreadListAdapter({ cloud, sdk: AI_SDK_SDK });
|
|
183
211
|
const thread = (id: string) => {
|
|
184
212
|
const element = AISDKChatThread({
|
|
185
213
|
threadId: id,
|
|
@@ -77,7 +77,6 @@ const textOf = (message: any): string =>
|
|
|
77
77
|
|
|
78
78
|
describe("useAISDKRuntime", () => {
|
|
79
79
|
beforeEach(() => {
|
|
80
|
-
vi.clearAllMocks();
|
|
81
80
|
vi.mocked(useExternalHistory).mockReturnValue({
|
|
82
81
|
isLoading: false,
|
|
83
82
|
deleteMessage: vi.fn().mockResolvedValue(undefined),
|
|
@@ -1487,4 +1486,43 @@ describe("useAISDKRuntime", () => {
|
|
|
1487
1486
|
error: chat.error,
|
|
1488
1487
|
});
|
|
1489
1488
|
});
|
|
1489
|
+
|
|
1490
|
+
it("keeps the error name and code on the failed message status", () => {
|
|
1491
|
+
const chat = createChatHelpers([
|
|
1492
|
+
{ id: "u1", role: "user", parts: [{ type: "text", text: "hi" }] },
|
|
1493
|
+
{ id: "a1", role: "assistant", parts: [{ type: "text", text: "" }] },
|
|
1494
|
+
]);
|
|
1495
|
+
chat.error = Object.assign(new Error("rate limited"), {
|
|
1496
|
+
name: "AI_APICallError",
|
|
1497
|
+
code: "rate_limited",
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1500
|
+
const { result } = renderHook(() => useAISDKRuntime(chat));
|
|
1501
|
+
|
|
1502
|
+
expect(
|
|
1503
|
+
result.current.thread.getState().messages.at(-1)?.status,
|
|
1504
|
+
).toMatchObject({
|
|
1505
|
+
type: "incomplete",
|
|
1506
|
+
reason: "error",
|
|
1507
|
+
error: { code: "rate_limited", message: "rate limited" },
|
|
1508
|
+
});
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1511
|
+
it("uses the error name as the code when the error carries none", () => {
|
|
1512
|
+
const chat = createChatHelpers([
|
|
1513
|
+
{ id: "u1", role: "user", parts: [{ type: "text", text: "hi" }] },
|
|
1514
|
+
{ id: "a1", role: "assistant", parts: [{ type: "text", text: "" }] },
|
|
1515
|
+
]);
|
|
1516
|
+
chat.error = Object.assign(new Error("upstream failed"), {
|
|
1517
|
+
name: "AI_APICallError",
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1520
|
+
const { result } = renderHook(() => useAISDKRuntime(chat));
|
|
1521
|
+
|
|
1522
|
+
expect(
|
|
1523
|
+
result.current.thread.getState().messages.at(-1)?.status,
|
|
1524
|
+
).toMatchObject({
|
|
1525
|
+
error: { code: "AI_APICallError", message: "upstream failed" },
|
|
1526
|
+
});
|
|
1527
|
+
});
|
|
1490
1528
|
});
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
MessageRepository,
|
|
48
48
|
} from "@assistant-ui/core/internal";
|
|
49
49
|
import type { ReadonlyJSONObject } from "assistant-stream/utils";
|
|
50
|
+
import type { AssistantError } from "@assistant-ui/core";
|
|
50
51
|
import { sliceMessagesUntil } from "../utils/sliceMessagesUntil";
|
|
51
52
|
import { toCreateMessage } from "../converters/toCreateMessage";
|
|
52
53
|
import { vercelAttachmentAdapter } from "../adapters/vercelAttachmentAdapter";
|
|
@@ -219,6 +220,19 @@ const useGeneratedSuggestions = (
|
|
|
219
220
|
|
|
220
221
|
const NO_CANCELLED_MESSAGE_IDS: ReadonlySet<string> = new Set();
|
|
221
222
|
|
|
223
|
+
const toChatError = (error: Error): AssistantError => {
|
|
224
|
+
const code = (error as { code?: unknown }).code;
|
|
225
|
+
return {
|
|
226
|
+
code:
|
|
227
|
+
typeof code === "string"
|
|
228
|
+
? code
|
|
229
|
+
: error.name !== "Error"
|
|
230
|
+
? error.name
|
|
231
|
+
: "unknown",
|
|
232
|
+
message: error.message,
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
|
|
222
236
|
export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
223
237
|
chatHelpers: ReturnType<typeof useChat<UI_MESSAGE>>,
|
|
224
238
|
adapter: AISDKRuntimeAdapter<UI_MESSAGE> = {},
|
|
@@ -315,7 +329,9 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
315
329
|
toolLastInputCache: toolLastInputCacheRef.current,
|
|
316
330
|
mcpAppMetadataCache: mcpAppMetadataCacheRef.current,
|
|
317
331
|
...(optimisticMessageId && { optimisticMessageId }),
|
|
318
|
-
...(chatHelpers.error && {
|
|
332
|
+
...(chatHelpers.error && {
|
|
333
|
+
error: toChatError(chatHelpers.error),
|
|
334
|
+
}),
|
|
319
335
|
...(cancelledMessageIds.size > 0 && { cancelledMessageIds }),
|
|
320
336
|
}),
|
|
321
337
|
[
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "@assistant-ui/core/react";
|
|
10
10
|
import { useAui, useAuiState } from "@assistant-ui/store";
|
|
11
11
|
import { useChatThread, type ChatThreadOptions } from "./useChatThread";
|
|
12
|
+
import { AI_SDK_SDK } from "./sdkIdentity";
|
|
12
13
|
|
|
13
14
|
export type UseChatRuntimeOptions<UI_MESSAGE extends UIMessage = UIMessage> =
|
|
14
15
|
ChatThreadOptions<UI_MESSAGE> & {
|
|
@@ -38,7 +39,7 @@ export const useChatRuntime = <UI_MESSAGE extends UIMessage = UIMessage>({
|
|
|
38
39
|
onThreadIdChange,
|
|
39
40
|
...options
|
|
40
41
|
}: UseChatRuntimeOptions<UI_MESSAGE> = {}): AssistantRuntime => {
|
|
41
|
-
const cloudAdapter = useCloudThreadListAdapter({ cloud });
|
|
42
|
+
const cloudAdapter = useCloudThreadListAdapter({ cloud, sdk: AI_SDK_SDK });
|
|
42
43
|
return useRemoteThreadListRuntime({
|
|
43
44
|
runtimeHook: function RuntimeHook() {
|
|
44
45
|
return useChatThreadRuntime(options);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { resource, useResource, flushTapSync } from "@assistant-ui/tap";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import {
|
|
7
|
+
RuntimeAdapter,
|
|
8
|
+
runtimeAdapterTransformScopes,
|
|
9
|
+
} from "@assistant-ui/core/store";
|
|
10
|
+
import {
|
|
11
|
+
attachTransformScopes,
|
|
12
|
+
AuiConfig,
|
|
13
|
+
createAssistantClient,
|
|
14
|
+
} from "@assistant-ui/store/client";
|
|
15
|
+
import { useChatThread, type ChatThreadEnvironment } from "./useChatThread";
|
|
16
|
+
import {
|
|
17
|
+
createCancellableTransport,
|
|
18
|
+
nextTask,
|
|
19
|
+
} from "./__tests__/controlled-transport";
|
|
20
|
+
|
|
21
|
+
const createHost = (
|
|
22
|
+
env: Pick<ChatThreadEnvironment, "stopOnClientDestroy">,
|
|
23
|
+
) => {
|
|
24
|
+
const useHost = (options: Parameters<typeof useChatThread>[0]) => {
|
|
25
|
+
const [threadListItem] = useState(() => ({
|
|
26
|
+
initialize: async () => ({ remoteId: "main", externalId: undefined }),
|
|
27
|
+
}));
|
|
28
|
+
const runtime = useChatThread(options, {
|
|
29
|
+
id: "main",
|
|
30
|
+
isMainThread: true,
|
|
31
|
+
getThreadListItem: () => threadListItem,
|
|
32
|
+
...env,
|
|
33
|
+
});
|
|
34
|
+
return useResource(RuntimeAdapter(runtime));
|
|
35
|
+
};
|
|
36
|
+
attachTransformScopes(useHost, runtimeAdapterTransformScopes);
|
|
37
|
+
return resource(useHost);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const streamThenDestroy = async (
|
|
41
|
+
env: Pick<ChatThreadEnvironment, "stopOnClientDestroy">,
|
|
42
|
+
) => {
|
|
43
|
+
const { transport, getCancelCount, close } = createCancellableTransport();
|
|
44
|
+
const Host = createHost(env);
|
|
45
|
+
const handle = createAssistantClient(
|
|
46
|
+
AuiConfig({ threads: Host({ transport }) }),
|
|
47
|
+
);
|
|
48
|
+
handle.subscribe(() => {});
|
|
49
|
+
const aui = handle.getClient();
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
flushTapSync(() => aui.composer.setText("stop me"));
|
|
53
|
+
flushTapSync(() => aui.composer.send());
|
|
54
|
+
await vi.waitFor(() => {
|
|
55
|
+
expect(aui.thread.getState().isRunning).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
} finally {
|
|
58
|
+
handle.destroy();
|
|
59
|
+
}
|
|
60
|
+
await nextTask();
|
|
61
|
+
const cancelCount = getCancelCount();
|
|
62
|
+
if (cancelCount === 0) close();
|
|
63
|
+
return cancelCount;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
describe("useChatThread", () => {
|
|
67
|
+
it("stops an in-flight chat on client destroy when stopOnClientDestroy is omitted", async () => {
|
|
68
|
+
expect(await streamThenDestroy({})).toBe(1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("leaves an in-flight chat running on client destroy when stopOnClientDestroy is false", async () => {
|
|
72
|
+
expect(await streamThenDestroy({ stopOnClientDestroy: false })).toBe(0);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -188,7 +188,7 @@ export const useChatThread = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
188
188
|
id,
|
|
189
189
|
isMainThread,
|
|
190
190
|
getThreadListItem,
|
|
191
|
-
stopOnClientDestroy =
|
|
191
|
+
stopOnClientDestroy = true,
|
|
192
192
|
chat: externalChat,
|
|
193
193
|
messageRepositoryInstance,
|
|
194
194
|
} = env;
|
|
@@ -780,6 +780,35 @@ describe("useExternalHistory persistence", () => {
|
|
|
780
780
|
],
|
|
781
781
|
expect.any(Object),
|
|
782
782
|
);
|
|
783
|
+
expect(reportTelemetry.mock.calls[0]![1]).toMatchObject({
|
|
784
|
+
message: expect.objectContaining({
|
|
785
|
+
id: "assistant-a",
|
|
786
|
+
status: { type: "complete", reason: "stop" },
|
|
787
|
+
}),
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
it("reports a run that failed before any assistant message", async () => {
|
|
792
|
+
const { append, reportTelemetry, runCycle, flush } =
|
|
793
|
+
createPersistenceHarness(true);
|
|
794
|
+
const failed = createAssistantMessage(
|
|
795
|
+
{
|
|
796
|
+
type: "incomplete",
|
|
797
|
+
reason: "error",
|
|
798
|
+
error: { code: "AI_APICallError", message: "upstream failed" },
|
|
799
|
+
},
|
|
800
|
+
[],
|
|
801
|
+
);
|
|
802
|
+
|
|
803
|
+
await runCycle([failed]);
|
|
804
|
+
await flush();
|
|
805
|
+
|
|
806
|
+
expect(append).not.toHaveBeenCalled();
|
|
807
|
+
expect(reportTelemetry).toHaveBeenCalledTimes(1);
|
|
808
|
+
expect(reportTelemetry).toHaveBeenCalledWith(
|
|
809
|
+
[],
|
|
810
|
+
expect.objectContaining({ message: failed }),
|
|
811
|
+
);
|
|
783
812
|
});
|
|
784
813
|
|
|
785
814
|
it("restores deferred telemetry for reloaded paused messages", async () => {
|
|
@@ -371,7 +371,10 @@ export const useExternalHistory = <TMessage>(
|
|
|
371
371
|
|
|
372
372
|
if (deferredTelemetryIds.current.has(message.id) && isTerminal) {
|
|
373
373
|
deferredTelemetryIds.current.delete(message.id);
|
|
374
|
-
adapter.reportTelemetry?.(batchItems,
|
|
374
|
+
adapter.reportTelemetry?.(batchItems, {
|
|
375
|
+
...telemetryOptions,
|
|
376
|
+
message,
|
|
377
|
+
});
|
|
375
378
|
}
|
|
376
379
|
}
|
|
377
380
|
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { UIMessage } from "ai";
|
|
2
|
-
import {
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
3
|
|
|
4
4
|
import { RESUMABLE_STREAM_ID_HEADER } from "./resumable";
|
|
5
5
|
import { AssistantChatTransport } from "./AssistantChatTransport";
|
|
@@ -26,10 +26,6 @@ const sendMessagesOptions = {
|
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
describe("AssistantChatTransport.prepareSendMessagesRequest", () => {
|
|
29
|
-
beforeEach(() => {
|
|
30
|
-
vi.clearAllMocks();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
29
|
it("passes the initialized remote thread id to prepareSendMessagesRequest as options.id", async () => {
|
|
34
30
|
const threadListItem = createThreadListItem("remote-thread-id");
|
|
35
31
|
const captured: { id: unknown } = { id: undefined };
|
|
@@ -209,10 +205,6 @@ const wrappedFetchOf = (
|
|
|
209
205
|
).fetch;
|
|
210
206
|
|
|
211
207
|
describe("AssistantChatTransport resumable fetch wrapper", () => {
|
|
212
|
-
beforeEach(() => {
|
|
213
|
-
vi.clearAllMocks();
|
|
214
|
-
});
|
|
215
|
-
|
|
216
208
|
it("passes a 204 with a non-null empty body through untouched (WebKit)", async () => {
|
|
217
209
|
const response = nullBodyStatusWithBody(204);
|
|
218
210
|
const fetchMock = vi.fn(async () => response);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"aiSDKFormatAdapter.d.ts","names":[],"sources":["../../src/adapters/aiSDKFormatAdapter.ts"],"mappings":";;;KAQY,qBAAqB,KAAK;cAEzB,sBAAsB,qBACjC,WACA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"aiSDKFormatAdapter.js","names":["UIMessage","MessageFormatAdapter","MessageFormatItem","MessageStorageEntry","AISDKStorageFormat","Omit","aiSDKV6FormatAdapter","format","encode","message","id","parts","decode","stored","parentId","parent_id","content","getId"],"sources":["../../src/adapters/aiSDKFormatAdapter.ts"],"sourcesContent":["import type { UIMessage } from \"ai\";\nimport type {\n MessageFormatAdapter,\n MessageFormatItem,\n MessageStorageEntry,\n} from \"@assistant-ui/core\";\n\n// Storage format for AI SDK messages - just the UIMessage\nexport type AISDKStorageFormat = Omit<UIMessage, \"id\">;\n\nexport const aiSDKV6FormatAdapter: MessageFormatAdapter<\n UIMessage,\n AISDKStorageFormat\n> = {\n format: \"ai-sdk/v6\",\n\n encode({\n message: { id, parts, ...message },\n }: MessageFormatItem<UIMessage>): AISDKStorageFormat {\n return {\n ...message,\n parts,\n };\n },\n\n decode(\n stored: MessageStorageEntry<AISDKStorageFormat>,\n ): MessageFormatItem<UIMessage> {\n return {\n parentId: stored.parent_id,\n message: {\n id: stored.id,\n ...stored.content,\n },\n };\n },\n\n getId(message: UIMessage): string {\n return message.id;\n },\n};\n"],"mappings":";AAUA,MAAaM,uBAGT;CACFC,QAAQ;CAERC,OAAO,EACLC,SAAS,EAAEC,IAAIC,OAAO,GAAGF,aAC0B;EACnD,OAAO;GACL,GAAGA;GACHE;EACF;CACF;CAEAC,OACEC,QAC8B;EAC9B,OAAO;GACLC,UAAUD,OAAOE;GACjBN,SAAS;IACPC,IAAIG,OAAOH;IACX,GAAGG,OAAOG;GACZ;EACF;CACF;CAEAC,MAAMR,SAA4B;EAChC,OAAOA,QAAQC;CACjB;AACF"}
|