@assistant-ui/ai-sdk 0.0.2 → 0.0.4

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 (54) hide show
  1. package/README.md +1 -1
  2. package/dist/converters/convertMessage.d.ts.map +1 -1
  3. package/dist/converters/convertMessage.js +23 -2
  4. package/dist/converters/convertMessage.js.map +1 -1
  5. package/dist/converters/toCreateMessage.d.ts.map +1 -1
  6. package/dist/converters/toCreateMessage.js +6 -2
  7. package/dist/converters/toCreateMessage.js.map +1 -1
  8. package/dist/model-context/injectInteractableContext.d.ts +1 -1
  9. package/dist/model-context/injectInteractableContext.js +1 -1
  10. package/dist/model-context/injectInteractableContext.js.map +1 -1
  11. package/dist/model-context/injectQuoteContext.d.ts +1 -1
  12. package/dist/model-context/injectQuoteContext.js +1 -1
  13. package/dist/model-context/injectQuoteContext.js.map +1 -1
  14. package/dist/runtime/AISDKChat.js +1 -1
  15. package/dist/runtime/AISDKThreads.d.ts +10 -8
  16. package/dist/runtime/AISDKThreads.d.ts.map +1 -1
  17. package/dist/runtime/AISDKThreads.js +34 -27
  18. package/dist/runtime/AISDKThreads.js.map +1 -1
  19. package/dist/runtime/useAISDKRuntime.d.ts +18 -3
  20. package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
  21. package/dist/runtime/useAISDKRuntime.js +199 -73
  22. package/dist/runtime/useAISDKRuntime.js.map +1 -1
  23. package/dist/runtime/useChatRuntime.js +1 -1
  24. package/dist/runtime/useChatThread.d.ts +14 -1
  25. package/dist/runtime/useChatThread.d.ts.map +1 -1
  26. package/dist/runtime/useChatThread.js +11 -4
  27. package/dist/runtime/useChatThread.js.map +1 -1
  28. package/dist/runtime/useExternalHistory.d.ts.map +1 -1
  29. package/dist/runtime/useExternalHistory.js +52 -47
  30. package/dist/runtime/useExternalHistory.js.map +1 -1
  31. package/dist/runtime/useResourceCleanup.js +1 -1
  32. package/dist/usage.d.ts +1 -2
  33. package/dist/usage.d.ts.map +1 -1
  34. package/dist/usage.js +5 -7
  35. package/dist/usage.js.map +1 -1
  36. package/package.json +12 -12
  37. package/src/converters/convertMessage.test.ts +22 -0
  38. package/src/converters/convertMessage.ts +26 -2
  39. package/src/converters/toCreateMessage.ts +6 -5
  40. package/src/model-context/injectInteractableContext.ts +1 -1
  41. package/src/model-context/injectQuoteContext.ts +1 -1
  42. package/src/runtime/AISDKThreads.cloud.test.ts +11 -1
  43. package/src/runtime/AISDKThreads.test.ts +102 -0
  44. package/src/runtime/AISDKThreads.ts +16 -9
  45. package/src/runtime/useAISDKRuntime.denied-tool.test.tsx +122 -0
  46. package/src/runtime/useAISDKRuntime.test.ts +479 -10
  47. package/src/runtime/useAISDKRuntime.ts +199 -55
  48. package/src/runtime/useChatRuntime.integration.test.tsx +46 -0
  49. package/src/runtime/useChatRuntime.test.ts +16 -0
  50. package/src/runtime/useChatThread.ts +26 -0
  51. package/src/runtime/useExternalHistory.test.ts +143 -1
  52. package/src/runtime/useExternalHistory.ts +53 -56
  53. package/src/usage.test.ts +26 -8
  54. package/src/usage.ts +4 -9
@@ -7,13 +7,17 @@ import { validateUIMessages } from "ai";
7
7
  // Mock only the sibling module that requires AUI store context (not available
8
8
  // in isolation). Every other dependency — useExternalStoreRuntime,
9
9
  // useToolInvocations, the message converter — runs for real.
10
- vi.mock("./useExternalHistory", () => ({
11
- useExternalHistory: vi.fn(() => ({
12
- isLoading: false,
13
- deleteMessage: vi.fn().mockResolvedValue(undefined),
14
- })),
15
- toExportedMessageRepository: vi.fn(),
16
- }));
10
+ vi.mock("./useExternalHistory", async (importOriginal) => {
11
+ const original =
12
+ await importOriginal<typeof import("./useExternalHistory")>();
13
+ return {
14
+ ...original,
15
+ useExternalHistory: vi.fn(() => ({
16
+ isLoading: false,
17
+ deleteMessage: vi.fn().mockResolvedValue(undefined),
18
+ })),
19
+ };
20
+ });
17
21
 
18
22
  import { useExternalHistory } from "./useExternalHistory";
19
23
  import { useAISDKRuntime } from "./useAISDKRuntime";
@@ -23,6 +27,7 @@ const createChatHelpers = (messages: any[] = []) => {
23
27
  let currentMessages = [...messages];
24
28
 
25
29
  const chatHelpers: any = {
30
+ id: "chat-1",
26
31
  status: "ready",
27
32
  error: null,
28
33
  messages: currentMessages,
@@ -131,10 +136,11 @@ describe("useAISDKRuntime", () => {
131
136
  abortError.name = "AbortError";
132
137
  const chat = createChatHelpers();
133
138
  let stopCalls = 0;
139
+ let rejectStop!: (error: unknown) => void;
134
140
  chat.stop = () => {
135
141
  stopCalls += 1;
136
142
  return new Promise((_, reject) => {
137
- setTimeout(() => reject(abortError), 5);
143
+ rejectStop = reject;
138
144
  });
139
145
  };
140
146
  const consoleError = vi
@@ -143,8 +149,11 @@ describe("useAISDKRuntime", () => {
143
149
 
144
150
  try {
145
151
  const { result } = renderHook(() => useAISDKRuntime(chat));
146
- const unhandledRejections = await captureUnhandledRejections(() => {
147
- result.current.thread.cancelRun();
152
+ const unhandledRejections = await captureUnhandledRejections(async () => {
153
+ await act(async () => {
154
+ result.current.thread.cancelRun();
155
+ rejectStop(abortError);
156
+ });
148
157
  });
149
158
 
150
159
  expect(stopCalls).toBe(1);
@@ -155,6 +164,286 @@ describe("useAISDKRuntime", () => {
155
164
  }
156
165
  });
157
166
 
167
+ it("marks only the stopped output cancelled", async () => {
168
+ let resolveStop!: () => void;
169
+ const chat = createChatHelpers([
170
+ {
171
+ id: "assistant-1",
172
+ role: "assistant",
173
+ parts: [{ type: "text", text: "partial", state: "streaming" }],
174
+ },
175
+ ]);
176
+ chat.status = "streaming";
177
+ chat.stop = vi.fn(
178
+ () =>
179
+ new Promise<void>((resolve) => {
180
+ resolveStop = resolve;
181
+ }),
182
+ );
183
+
184
+ const { result, rerender } = renderHook(() => useAISDKRuntime(chat));
185
+
186
+ act(() => {
187
+ result.current.thread.cancelRun();
188
+ });
189
+ rerender();
190
+
191
+ expect(
192
+ result.current.thread.getState().messages.at(-1)?.status,
193
+ ).toMatchObject({ type: "running" });
194
+
195
+ act(() => {
196
+ chat.status = "ready";
197
+ rerender();
198
+ });
199
+
200
+ await waitFor(() => {
201
+ expect(
202
+ result.current.thread.getState().messages.at(-1)?.status,
203
+ ).toMatchObject({
204
+ type: "incomplete",
205
+ reason: "cancelled",
206
+ });
207
+ });
208
+
209
+ await act(async () => {
210
+ resolveStop();
211
+ await Promise.resolve();
212
+ });
213
+
214
+ act(() => {
215
+ chat.setMessages([
216
+ {
217
+ id: "assistant-2",
218
+ role: "assistant",
219
+ parts: [{ type: "text", text: "replacement" }],
220
+ },
221
+ ]);
222
+ rerender();
223
+ });
224
+ await waitFor(() => {
225
+ expect(
226
+ result.current.thread.getState().messages.at(-1)?.status,
227
+ ).toMatchObject({ type: "complete", reason: "unknown" });
228
+ });
229
+
230
+ act(() => {
231
+ chat.setMessages([
232
+ {
233
+ id: "assistant-1",
234
+ role: "assistant",
235
+ parts: [{ type: "text", text: "partial" }],
236
+ },
237
+ ]);
238
+ chat.status = "streaming";
239
+ rerender();
240
+ });
241
+
242
+ expect(
243
+ result.current.thread.getState().messages.at(-1)?.status,
244
+ ).toMatchObject({ type: "running" });
245
+
246
+ act(() => {
247
+ chat.status = "ready";
248
+ rerender();
249
+ });
250
+
251
+ await waitFor(() => {
252
+ expect(
253
+ result.current.thread.getState().messages.at(-1)?.status,
254
+ ).toMatchObject({
255
+ type: "complete",
256
+ reason: "unknown",
257
+ });
258
+ });
259
+ });
260
+
261
+ it("keeps the stopped output cancelled through the next turn", async () => {
262
+ const chat = createChatHelpers([
263
+ { id: "u1", role: "user", parts: [{ type: "text", text: "hi" }] },
264
+ {
265
+ id: "assistant-1",
266
+ role: "assistant",
267
+ parts: [{ type: "text", text: "partial", state: "streaming" }],
268
+ },
269
+ ]);
270
+ chat.status = "streaming";
271
+
272
+ const { result, rerender } = renderHook(() => useAISDKRuntime(chat));
273
+
274
+ await act(async () => {
275
+ result.current.thread.cancelRun();
276
+ });
277
+ act(() => {
278
+ chat.status = "ready";
279
+ rerender();
280
+ });
281
+
282
+ await waitFor(() => {
283
+ expect(
284
+ result.current.thread.getState().messages.at(-1)?.status,
285
+ ).toMatchObject({ type: "incomplete", reason: "cancelled" });
286
+ });
287
+
288
+ act(() => {
289
+ chat.setMessages([
290
+ ...chat.messages,
291
+ { id: "u2", role: "user", parts: [{ type: "text", text: "next" }] },
292
+ {
293
+ id: "assistant-2",
294
+ role: "assistant",
295
+ parts: [{ type: "text", text: "answer" }],
296
+ },
297
+ ]);
298
+ rerender();
299
+ });
300
+
301
+ await waitFor(() => {
302
+ const messages = result.current.thread.getState().messages;
303
+ expect(
304
+ messages.find((message) => message.id === "assistant-1")?.status,
305
+ ).toMatchObject({ type: "incomplete", reason: "cancelled" });
306
+ expect(messages.at(-1)?.status).toMatchObject({
307
+ type: "complete",
308
+ reason: "unknown",
309
+ });
310
+ });
311
+ });
312
+
313
+ it("retracts the cancellation when the provider picks the message back up", async () => {
314
+ const chat = createChatHelpers([
315
+ {
316
+ id: "assistant-1",
317
+ role: "assistant",
318
+ parts: [{ type: "text", text: "partial", state: "streaming" }],
319
+ },
320
+ ]);
321
+ chat.status = "streaming";
322
+
323
+ const { result, rerender } = renderHook(() => useAISDKRuntime(chat));
324
+
325
+ await act(async () => {
326
+ result.current.thread.cancelRun();
327
+ });
328
+ act(() => {
329
+ chat.status = "ready";
330
+ rerender();
331
+ });
332
+
333
+ await waitFor(() => {
334
+ expect(
335
+ result.current.thread.getState().messages.at(-1)?.status,
336
+ ).toMatchObject({ type: "incomplete", reason: "cancelled" });
337
+ });
338
+
339
+ act(() => {
340
+ chat.status = "streaming";
341
+ rerender();
342
+ });
343
+ act(() => {
344
+ chat.status = "ready";
345
+ rerender();
346
+ });
347
+
348
+ await waitFor(() => {
349
+ expect(
350
+ result.current.thread.getState().messages.at(-1)?.status,
351
+ ).toMatchObject({ type: "complete", reason: "unknown" });
352
+ });
353
+ });
354
+
355
+ it("does not mark completed output cancelled when already idle", async () => {
356
+ const chat = createChatHelpers([
357
+ {
358
+ id: "assistant-1",
359
+ role: "assistant",
360
+ parts: [{ type: "text", text: "finished" }],
361
+ },
362
+ ]);
363
+
364
+ const { result, rerender } = renderHook(() => useAISDKRuntime(chat));
365
+
366
+ act(() => {
367
+ result.current.thread.cancelRun();
368
+ rerender();
369
+ });
370
+
371
+ await waitFor(() => {
372
+ expect(
373
+ result.current.thread.getState().messages.at(-1)?.status,
374
+ ).toMatchObject({ type: "complete", reason: "unknown" });
375
+ });
376
+ });
377
+
378
+ it("marks output cancelled while a client tool is still executing", async () => {
379
+ let resolveTool!: (value: string) => void;
380
+ const execute = vi.fn(
381
+ () =>
382
+ new Promise<string>((resolve) => {
383
+ resolveTool = resolve;
384
+ }),
385
+ );
386
+ const chat = createChatHelpers();
387
+
388
+ const { result, rerender } = renderHook(() => useAISDKRuntime(chat));
389
+ const unregister = result.current.registerModelContextProvider({
390
+ getModelContext: () => ({
391
+ tools: {
392
+ weather: {
393
+ parameters: { type: "object", properties: {} },
394
+ execute,
395
+ },
396
+ },
397
+ }),
398
+ });
399
+
400
+ try {
401
+ act(() => {
402
+ chat.setMessages([
403
+ {
404
+ id: "assistant-1",
405
+ role: "assistant",
406
+ parts: [
407
+ {
408
+ type: "tool-weather",
409
+ toolCallId: "tool-1",
410
+ state: "input-available",
411
+ input: { city: "London" },
412
+ },
413
+ ],
414
+ },
415
+ ]);
416
+ rerender();
417
+ });
418
+
419
+ await waitFor(() => {
420
+ expect(execute).toHaveBeenCalledOnce();
421
+ expect(result.current.thread.getState().isRunning).toBe(true);
422
+ });
423
+
424
+ act(() => {
425
+ result.current.thread.cancelRun();
426
+ chat.setMessages([
427
+ {
428
+ id: "assistant-1",
429
+ role: "assistant",
430
+ parts: [{ type: "text", text: "stopped" }],
431
+ },
432
+ ]);
433
+ resolveTool("sunny");
434
+ rerender();
435
+ });
436
+
437
+ await waitFor(() => {
438
+ expect(
439
+ result.current.thread.getState().messages.at(-1)?.status,
440
+ ).toMatchObject({ type: "incomplete", reason: "cancelled" });
441
+ });
442
+ } finally {
443
+ unregister();
444
+ }
445
+ });
446
+
158
447
  it("reports non-AbortError cancellation failures", async () => {
159
448
  const stopError = new Error("stop failed");
160
449
  const chat = createChatHelpers();
@@ -690,6 +979,186 @@ describe("useAISDKRuntime", () => {
690
979
  expect(result.current.thread.getState().suggestions).toEqual(suggestions);
691
980
  });
692
981
 
982
+ it("imports a message tree without replacing the chat feed", async () => {
983
+ const chat = createChatHelpers();
984
+ const onBranchChange = vi.fn();
985
+ const messageRepository = {
986
+ headId: "a2",
987
+ messages: [
988
+ {
989
+ parentId: null,
990
+ message: {
991
+ id: "u1",
992
+ role: "user" as const,
993
+ parts: [{ type: "text" as const, text: "question" }],
994
+ },
995
+ },
996
+ {
997
+ parentId: "u1",
998
+ message: {
999
+ id: "a1",
1000
+ role: "assistant" as const,
1001
+ parts: [{ type: "text" as const, text: "first" }],
1002
+ },
1003
+ },
1004
+ {
1005
+ parentId: "u1",
1006
+ message: {
1007
+ id: "a2",
1008
+ role: "assistant" as const,
1009
+ parts: [{ type: "text" as const, text: "second" }],
1010
+ },
1011
+ },
1012
+ ],
1013
+ };
1014
+
1015
+ const { result, rerender } = renderHook(() =>
1016
+ useAISDKRuntime(chat, {
1017
+ messageRepository,
1018
+ unstable_onBranchChange: onBranchChange,
1019
+ }),
1020
+ );
1021
+
1022
+ await waitFor(() => {
1023
+ expect(
1024
+ result.current.thread.getState().messages.map((message) => message.id),
1025
+ ).toEqual(["u1", "a2"]);
1026
+ });
1027
+ expect(result.current.thread.getMessageById("a2").getState()).toMatchObject(
1028
+ {
1029
+ branchNumber: 2,
1030
+ branchCount: 2,
1031
+ },
1032
+ );
1033
+ expect(chat.messages.map((message: { id: string }) => message.id)).toEqual([
1034
+ "u1",
1035
+ "a2",
1036
+ ]);
1037
+
1038
+ act(() => {
1039
+ result.current.thread
1040
+ .getMessageById("a2")
1041
+ .switchToBranch({ branchId: "a1" });
1042
+ });
1043
+
1044
+ expect(onBranchChange).toHaveBeenCalledWith({
1045
+ headId: "a1",
1046
+ visibleMessageIds: ["u1", "a1"],
1047
+ });
1048
+ expect(chat.messages.map((message: { id: string }) => message.id)).toEqual([
1049
+ "u1",
1050
+ "a1",
1051
+ ]);
1052
+ expect(chat.messages).not.toEqual([]);
1053
+
1054
+ chat.messages = [
1055
+ { id: "u1", role: "user", parts: [{ type: "text", text: "question" }] },
1056
+ {
1057
+ id: "a1",
1058
+ role: "assistant",
1059
+ parts: [{ type: "text", text: "first token" }],
1060
+ },
1061
+ ];
1062
+ rerender();
1063
+
1064
+ await waitFor(() => {
1065
+ expect(textOf(result.current.thread.getState().messages.at(-1))).toBe(
1066
+ "first token",
1067
+ );
1068
+ });
1069
+ expect(result.current.thread.getMessageById("a1").getState()).toMatchObject(
1070
+ {
1071
+ branchNumber: 1,
1072
+ branchCount: 2,
1073
+ },
1074
+ );
1075
+ });
1076
+
1077
+ it("does not overwrite a nonempty chat with the repository", async () => {
1078
+ const chat = createChatHelpers([
1079
+ { id: "live", role: "user", parts: [{ type: "text", text: "keep me" }] },
1080
+ ]);
1081
+ const messageRepository = {
1082
+ headId: "a1",
1083
+ messages: [
1084
+ {
1085
+ parentId: null,
1086
+ message: {
1087
+ id: "u1",
1088
+ role: "user" as const,
1089
+ parts: [{ type: "text" as const, text: "question" }],
1090
+ },
1091
+ },
1092
+ {
1093
+ parentId: "u1",
1094
+ message: {
1095
+ id: "a1",
1096
+ role: "assistant" as const,
1097
+ parts: [{ type: "text" as const, text: "first" }],
1098
+ },
1099
+ },
1100
+ ],
1101
+ };
1102
+
1103
+ const { result } = renderHook(() =>
1104
+ useAISDKRuntime(chat, { messageRepository }),
1105
+ );
1106
+
1107
+ await waitFor(() => {
1108
+ expect(
1109
+ result.current.thread.getState().messages.map((message) => message.id),
1110
+ ).toEqual(["live"]);
1111
+ });
1112
+ expect(chat.messages.map((message: { id: string }) => message.id)).toEqual([
1113
+ "live",
1114
+ ]);
1115
+ });
1116
+
1117
+ it("does not reseed when the repository object identity changes", async () => {
1118
+ const chat = createChatHelpers();
1119
+ const makeRepository = (text: string) => ({
1120
+ headId: "a1",
1121
+ messages: [
1122
+ {
1123
+ parentId: null,
1124
+ message: {
1125
+ id: "u1",
1126
+ role: "user" as const,
1127
+ parts: [{ type: "text" as const, text: "question" }],
1128
+ },
1129
+ },
1130
+ {
1131
+ parentId: "u1",
1132
+ message: {
1133
+ id: "a1",
1134
+ role: "assistant" as const,
1135
+ parts: [{ type: "text" as const, text }],
1136
+ },
1137
+ },
1138
+ ],
1139
+ });
1140
+
1141
+ const { result, rerender } = renderHook(
1142
+ ({ repository }) =>
1143
+ useAISDKRuntime(chat, { messageRepository: repository }),
1144
+ { initialProps: { repository: makeRepository("first") } },
1145
+ );
1146
+
1147
+ await waitFor(() => {
1148
+ expect(textOf(result.current.thread.getState().messages.at(-1))).toBe(
1149
+ "first",
1150
+ );
1151
+ });
1152
+ const setMessagesCalls = chat.setMessages.mock.calls.length;
1153
+
1154
+ rerender({ repository: makeRepository("second") });
1155
+
1156
+ expect(textOf(result.current.thread.getState().messages.at(-1))).toBe(
1157
+ "first",
1158
+ );
1159
+ expect(chat.setMessages.mock.calls.length).toBe(setMessagesCalls);
1160
+ });
1161
+
693
1162
  it("calls adapters.suggestion after settle with messages and signal", async () => {
694
1163
  const generate = vi.fn().mockResolvedValue([{ prompt: "next" }]);
695
1164
  const chat = createChatHelpers([