@assistant-ui/ai-sdk 0.0.1

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 (167) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -0
  3. package/dist/adapters/aiSDKFormatAdapter.d.ts +8 -0
  4. package/dist/adapters/aiSDKFormatAdapter.d.ts.map +1 -0
  5. package/dist/adapters/aiSDKFormatAdapter.js +26 -0
  6. package/dist/adapters/aiSDKFormatAdapter.js.map +1 -0
  7. package/dist/adapters/vercelAttachmentAdapter.d.ts +6 -0
  8. package/dist/adapters/vercelAttachmentAdapter.d.ts.map +1 -0
  9. package/dist/adapters/vercelAttachmentAdapter.js +37 -0
  10. package/dist/adapters/vercelAttachmentAdapter.js.map +1 -0
  11. package/dist/aiSDKExtras.d.ts +11 -0
  12. package/dist/aiSDKExtras.d.ts.map +1 -0
  13. package/dist/aiSDKExtras.js +7 -0
  14. package/dist/aiSDKExtras.js.map +1 -0
  15. package/dist/converters/convertMessage.d.ts +28 -0
  16. package/dist/converters/convertMessage.d.ts.map +1 -0
  17. package/dist/converters/convertMessage.js +249 -0
  18. package/dist/converters/convertMessage.js.map +1 -0
  19. package/dist/converters/modelContentEnvelope.d.ts +16 -0
  20. package/dist/converters/modelContentEnvelope.d.ts.map +1 -0
  21. package/dist/converters/modelContentEnvelope.js +22 -0
  22. package/dist/converters/modelContentEnvelope.js.map +1 -0
  23. package/dist/converters/toCreateMessage.d.ts +7 -0
  24. package/dist/converters/toCreateMessage.d.ts.map +1 -0
  25. package/dist/converters/toCreateMessage.js +58 -0
  26. package/dist/converters/toCreateMessage.js.map +1 -0
  27. package/dist/converters/toolOutputConversion.d.ts +29 -0
  28. package/dist/converters/toolOutputConversion.d.ts.map +1 -0
  29. package/dist/converters/toolOutputConversion.js +31 -0
  30. package/dist/converters/toolOutputConversion.js.map +1 -0
  31. package/dist/hooks.d.ts +19 -0
  32. package/dist/hooks.d.ts.map +1 -0
  33. package/dist/hooks.js +20 -0
  34. package/dist/hooks.js.map +1 -0
  35. package/dist/index.d.ts +14 -0
  36. package/dist/index.js +13 -0
  37. package/dist/index.native.d.ts +12 -0
  38. package/dist/index.native.js +11 -0
  39. package/dist/model-context/injectInteractableContext.d.ts +37 -0
  40. package/dist/model-context/injectInteractableContext.d.ts.map +1 -0
  41. package/dist/model-context/injectInteractableContext.js +52 -0
  42. package/dist/model-context/injectInteractableContext.js.map +1 -0
  43. package/dist/model-context/injectQuoteContext.d.ts +27 -0
  44. package/dist/model-context/injectQuoteContext.d.ts.map +1 -0
  45. package/dist/model-context/injectQuoteContext.js +51 -0
  46. package/dist/model-context/injectQuoteContext.js.map +1 -0
  47. package/dist/runtime/AISDKChat.d.ts +16 -0
  48. package/dist/runtime/AISDKChat.d.ts.map +1 -0
  49. package/dist/runtime/AISDKChat.js +69 -0
  50. package/dist/runtime/AISDKChat.js.map +1 -0
  51. package/dist/runtime/AISDKThreads.d.ts +45 -0
  52. package/dist/runtime/AISDKThreads.d.ts.map +1 -0
  53. package/dist/runtime/AISDKThreads.js +203 -0
  54. package/dist/runtime/AISDKThreads.js.map +1 -0
  55. package/dist/runtime/useAISDKRuntime.d.ts +49 -0
  56. package/dist/runtime/useAISDKRuntime.d.ts.map +1 -0
  57. package/dist/runtime/useAISDKRuntime.js +266 -0
  58. package/dist/runtime/useAISDKRuntime.js.map +1 -0
  59. package/dist/runtime/useChatRuntime.d.ts +13 -0
  60. package/dist/runtime/useChatRuntime.d.ts.map +1 -0
  61. package/dist/runtime/useChatRuntime.js +52 -0
  62. package/dist/runtime/useChatRuntime.js.map +1 -0
  63. package/dist/runtime/useChatThread.d.ts +77 -0
  64. package/dist/runtime/useChatThread.d.ts.map +1 -0
  65. package/dist/runtime/useChatThread.js +134 -0
  66. package/dist/runtime/useChatThread.js.map +1 -0
  67. package/dist/runtime/useExternalHistory.d.ts +11 -0
  68. package/dist/runtime/useExternalHistory.d.ts.map +1 -0
  69. package/dist/runtime/useExternalHistory.js +278 -0
  70. package/dist/runtime/useExternalHistory.js.map +1 -0
  71. package/dist/runtime/useResourceCleanup.d.ts +5 -0
  72. package/dist/runtime/useResourceCleanup.d.ts.map +1 -0
  73. package/dist/runtime/useResourceCleanup.js +47 -0
  74. package/dist/runtime/useResourceCleanup.js.map +1 -0
  75. package/dist/runtime/useStreamingTiming.d.ts +15 -0
  76. package/dist/runtime/useStreamingTiming.d.ts.map +1 -0
  77. package/dist/runtime/useStreamingTiming.js +38 -0
  78. package/dist/runtime/useStreamingTiming.js.map +1 -0
  79. package/dist/tools/frontendTools.d.ts +33 -0
  80. package/dist/tools/frontendTools.d.ts.map +1 -0
  81. package/dist/tools/frontendTools.js +33 -0
  82. package/dist/tools/frontendTools.js.map +1 -0
  83. package/dist/tools/generativeTools.d.ts +73 -0
  84. package/dist/tools/generativeTools.d.ts.map +1 -0
  85. package/dist/tools/generativeTools.js +245 -0
  86. package/dist/tools/generativeTools.js.map +1 -0
  87. package/dist/tools/mcp-stdio.node.d.ts +2 -0
  88. package/dist/tools/mcp-stdio.node.js +2 -0
  89. package/dist/tools/mcp-stdio.unsupported.d.ts +6 -0
  90. package/dist/tools/mcp-stdio.unsupported.d.ts.map +1 -0
  91. package/dist/tools/mcp-stdio.unsupported.js +11 -0
  92. package/dist/tools/mcp-stdio.unsupported.js.map +1 -0
  93. package/dist/transport/AssistantChatTransport.d.ts +23 -0
  94. package/dist/transport/AssistantChatTransport.d.ts.map +1 -0
  95. package/dist/transport/AssistantChatTransport.js +134 -0
  96. package/dist/transport/AssistantChatTransport.js.map +1 -0
  97. package/dist/transport/resumable.d.ts +38 -0
  98. package/dist/transport/resumable.d.ts.map +1 -0
  99. package/dist/transport/resumable.js +113 -0
  100. package/dist/transport/resumable.js.map +1 -0
  101. package/dist/usage.d.ts +19 -0
  102. package/dist/usage.d.ts.map +1 -0
  103. package/dist/usage.js +127 -0
  104. package/dist/usage.js.map +1 -0
  105. package/dist/utils/getVercelAIMessages.d.ts +7 -0
  106. package/dist/utils/getVercelAIMessages.d.ts.map +1 -0
  107. package/dist/utils/getVercelAIMessages.js +9 -0
  108. package/dist/utils/getVercelAIMessages.js.map +1 -0
  109. package/dist/utils/sliceMessagesUntil.d.ts +6 -0
  110. package/dist/utils/sliceMessagesUntil.d.ts.map +1 -0
  111. package/dist/utils/sliceMessagesUntil.js +12 -0
  112. package/dist/utils/sliceMessagesUntil.js.map +1 -0
  113. package/package.json +100 -0
  114. package/src/__tests__/standalone.e2e.ts +59 -0
  115. package/src/adapters/aiSDKFormatAdapter.ts +41 -0
  116. package/src/adapters/vercelAttachmentAdapter.test.ts +29 -0
  117. package/src/adapters/vercelAttachmentAdapter.ts +36 -0
  118. package/src/aiSDKExtras.ts +11 -0
  119. package/src/converters/convertMessage.test.ts +1130 -0
  120. package/src/converters/convertMessage.ts +457 -0
  121. package/src/converters/modelContentEnvelope.ts +41 -0
  122. package/src/converters/toCreateMessage.test.ts +596 -0
  123. package/src/converters/toCreateMessage.ts +99 -0
  124. package/src/converters/toolOutputConversion.ts +26 -0
  125. package/src/hooks.ts +23 -0
  126. package/src/index.native.ts +21 -0
  127. package/src/index.ts +29 -0
  128. package/src/model-context/injectInteractableContext.test.ts +167 -0
  129. package/src/model-context/injectInteractableContext.ts +61 -0
  130. package/src/model-context/injectQuoteContext.test.ts +33 -0
  131. package/src/model-context/injectQuoteContext.ts +62 -0
  132. package/src/runtime/AISDKChat.integration.test.tsx +47 -0
  133. package/src/runtime/AISDKChat.react-compiler.test.ts +49 -0
  134. package/src/runtime/AISDKChat.test.ts +193 -0
  135. package/src/runtime/AISDKChat.ts +47 -0
  136. package/src/runtime/AISDKThreads.cloud.test.ts +136 -0
  137. package/src/runtime/AISDKThreads.test.ts +548 -0
  138. package/src/runtime/AISDKThreads.ts +223 -0
  139. package/src/runtime/__tests__/controlled-transport.ts +43 -0
  140. package/src/runtime/useAISDKRuntime.approval.test.tsx +69 -0
  141. package/src/runtime/useAISDKRuntime.test.ts +1021 -0
  142. package/src/runtime/useAISDKRuntime.tool-output.test.tsx +113 -0
  143. package/src/runtime/useAISDKRuntime.ts +524 -0
  144. package/src/runtime/useChatOnTap.test.ts +58 -0
  145. package/src/runtime/useChatRuntime.integration.test.tsx +67 -0
  146. package/src/runtime/useChatRuntime.test.ts +522 -0
  147. package/src/runtime/useChatRuntime.ts +49 -0
  148. package/src/runtime/useChatThread.ts +286 -0
  149. package/src/runtime/useExternalHistory.test.ts +1289 -0
  150. package/src/runtime/useExternalHistory.ts +435 -0
  151. package/src/runtime/useResourceCleanup.ts +32 -0
  152. package/src/runtime/useStreamingTiming.ts +68 -0
  153. package/src/tools/frontendTools.test.ts +205 -0
  154. package/src/tools/frontendTools.ts +83 -0
  155. package/src/tools/generativeTools.test.ts +785 -0
  156. package/src/tools/generativeTools.ts +470 -0
  157. package/src/tools/mcp-stdio.node.ts +1 -0
  158. package/src/tools/mcp-stdio.unsupported.ts +12 -0
  159. package/src/transport/AssistantChatTransport.test.ts +300 -0
  160. package/src/transport/AssistantChatTransport.ts +204 -0
  161. package/src/transport/resumable.test.ts +262 -0
  162. package/src/transport/resumable.ts +161 -0
  163. package/src/usage.test.ts +186 -0
  164. package/src/usage.ts +174 -0
  165. package/src/utils/getVercelAIMessages.ts +11 -0
  166. package/src/utils/sliceMessagesUntil.test.ts +55 -0
  167. package/src/utils/sliceMessagesUntil.ts +20 -0
@@ -0,0 +1,548 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import type { AssistantCloud } from "assistant-cloud";
5
+ import { flushTapSync } from "@assistant-ui/tap";
6
+ import { AuiConfig, createAssistantClient } from "@assistant-ui/store/client";
7
+ import { AISDKThreads } from "./AISDKThreads";
8
+ import { AssistantChatTransport } from "../transport/AssistantChatTransport";
9
+ import {
10
+ createCancellableTransport,
11
+ createControlledTransport,
12
+ } from "./__tests__/controlled-transport";
13
+
14
+ const textReply = (text: string) =>
15
+ [
16
+ { type: "start" },
17
+ { type: "text-start", id: "t1" },
18
+ { type: "text-delta", id: "t1", delta: text },
19
+ { type: "text-end", id: "t1" },
20
+ { type: "finish" },
21
+ ] as never[];
22
+
23
+ const threadText = (aui: ReturnType<typeof createAssistantClient>) =>
24
+ aui
25
+ .getClient()
26
+ .thread.getState()
27
+ .messages.map((m) =>
28
+ m.content.map((part) => (part.type === "text" ? part.text : "")).join(""),
29
+ );
30
+
31
+ describe("AISDKThreads", () => {
32
+ it("runs one chat per thread and keeps histories isolated across switches", async () => {
33
+ const { transport, emit, close } = createControlledTransport();
34
+ const handle = createAssistantClient(
35
+ AuiConfig({ threads: AISDKThreads({ transport }) }),
36
+ );
37
+ handle.subscribe(() => {});
38
+ const aui = handle.getClient();
39
+
40
+ expect(aui.threads.getState().threadIds).toEqual(["main"]);
41
+
42
+ flushTapSync(() => aui.composer.setText("first question"));
43
+ flushTapSync(() => aui.composer.send());
44
+ await vi.waitFor(() => {
45
+ expect(
46
+ handle.getClient().thread.getState().messages.length,
47
+ ).toBeGreaterThan(0);
48
+ });
49
+ emit(...textReply("first answer"));
50
+ close();
51
+ await vi.waitFor(() => {
52
+ expect(threadText(handle as never)).toEqual([
53
+ "first question",
54
+ "first answer",
55
+ ]);
56
+ expect(handle.getClient().thread.getState().isRunning).toBe(false);
57
+ });
58
+
59
+ flushTapSync(() => aui.threads.switchToNewThread());
60
+ const state = handle.getClient().threads.getState();
61
+ expect(state.threadIds).toHaveLength(2);
62
+ expect(state.mainThreadId).not.toBe("main");
63
+ expect(handle.getClient().thread.getState().messages).toHaveLength(0);
64
+
65
+ flushTapSync(() => handle.getClient().threads.switchToThread("main"));
66
+ expect(handle.getClient().threads.getState().mainThreadId).toBe("main");
67
+ expect(threadText(handle as never)).toEqual([
68
+ "first question",
69
+ "first answer",
70
+ ]);
71
+
72
+ handle.destroy();
73
+ });
74
+
75
+ it("keeps a switched-away thread streaming in the background", async () => {
76
+ const { transport, emit, close } = createControlledTransport();
77
+ const handle = createAssistantClient(
78
+ AuiConfig({ threads: AISDKThreads({ transport: () => transport }) }),
79
+ );
80
+ handle.subscribe(() => {});
81
+ const aui = handle.getClient();
82
+
83
+ flushTapSync(() => aui.composer.setText("stream me"));
84
+ flushTapSync(() => aui.composer.send());
85
+ await vi.waitFor(() => {
86
+ expect(
87
+ handle.getClient().thread.getState().messages.length,
88
+ ).toBeGreaterThan(0);
89
+ });
90
+ emit(
91
+ { type: "start" },
92
+ { type: "text-start", id: "t1" },
93
+ { type: "text-delta", id: "t1", delta: "partial" },
94
+ );
95
+
96
+ flushTapSync(() => aui.threads.switchToNewThread());
97
+ expect(handle.getClient().thread.getState().messages).toHaveLength(0);
98
+
99
+ emit(
100
+ { type: "text-delta", id: "t1", delta: " and finished" },
101
+ { type: "text-end", id: "t1" },
102
+ { type: "finish" },
103
+ );
104
+ close();
105
+
106
+ flushTapSync(() => handle.getClient().threads.switchToThread("main"));
107
+ await vi.waitFor(() => {
108
+ expect(threadText(handle as never)).toEqual([
109
+ "stream me",
110
+ "partial and finished",
111
+ ]);
112
+ expect(handle.getClient().thread.getState().isRunning).toBe(false);
113
+ });
114
+
115
+ handle.destroy();
116
+ });
117
+
118
+ it("stops all in-flight chats when its client is destroyed", async () => {
119
+ const chats: ReturnType<typeof createCancellableTransport>[] = [];
120
+ const handle = createAssistantClient(
121
+ AuiConfig({
122
+ threads: AISDKThreads({
123
+ transport: () => {
124
+ const chat = createCancellableTransport();
125
+ chats.push(chat);
126
+ return chat.transport;
127
+ },
128
+ }),
129
+ }),
130
+ );
131
+ handle.subscribe(() => {});
132
+ const aui = handle.getClient();
133
+
134
+ try {
135
+ flushTapSync(() => aui.composer.setText("first"));
136
+ flushTapSync(() => aui.composer.send());
137
+ await vi.waitFor(() => {
138
+ expect(aui.thread.getState().isRunning).toBe(true);
139
+ });
140
+
141
+ flushTapSync(() => aui.threads.switchToNewThread());
142
+ flushTapSync(() => handle.getClient().composer.setText("second"));
143
+ flushTapSync(() => handle.getClient().composer.send());
144
+ await vi.waitFor(() => {
145
+ expect(chats).toHaveLength(2);
146
+ expect(handle.getClient().thread.getState().isRunning).toBe(true);
147
+ });
148
+ } finally {
149
+ handle.destroy();
150
+ }
151
+
152
+ await vi.waitFor(() => {
153
+ expect(chats.map((chat) => chat.getCancelCount())).toEqual([1, 1]);
154
+ });
155
+ });
156
+
157
+ it("wires model context and per-thread transports onto the wire", async () => {
158
+ const bodies: unknown[] = [];
159
+ const fetchStub = vi.fn(async (_url: unknown, init?: RequestInit) => {
160
+ bodies.push(JSON.parse(String(init?.body)));
161
+ return new Response(
162
+ new ReadableStream({
163
+ start(c) {
164
+ c.enqueue(
165
+ new TextEncoder().encode(
166
+ 'data: {"type":"start"}\n\ndata: [DONE]\n\n',
167
+ ),
168
+ );
169
+ c.close();
170
+ },
171
+ }),
172
+ { headers: { "Content-Type": "text/event-stream" } },
173
+ );
174
+ });
175
+ vi.stubGlobal("fetch", fetchStub);
176
+ try {
177
+ const transports: AssistantChatTransport<never>[] = [];
178
+ const handle = createAssistantClient(
179
+ AuiConfig({
180
+ threads: AISDKThreads({
181
+ transport: () => {
182
+ const transport = new AssistantChatTransport();
183
+ transports.push(transport as AssistantChatTransport<never>);
184
+ return transport;
185
+ },
186
+ }),
187
+ }),
188
+ );
189
+ handle.subscribe(() => {});
190
+ const aui = handle.getClient();
191
+
192
+ flushTapSync(() =>
193
+ aui.modelContext.register({
194
+ getModelContext: () => ({ system: "wired system prompt" }),
195
+ }),
196
+ );
197
+ flushTapSync(() => aui.composer.setText("hello"));
198
+ flushTapSync(() => aui.composer.send());
199
+ await vi.waitFor(() => expect(fetchStub).toHaveBeenCalledTimes(1));
200
+ expect(bodies[0]).toMatchObject({ system: "wired system prompt" });
201
+
202
+ flushTapSync(() => aui.threads.switchToNewThread());
203
+ await vi.waitFor(() => expect(transports.length).toBe(2));
204
+ expect(transports[0]).not.toBe(transports[1]);
205
+
206
+ handle.destroy();
207
+ } finally {
208
+ vi.unstubAllGlobals();
209
+ }
210
+ });
211
+
212
+ it("bridges model context on the default transport path", async () => {
213
+ const bodies: unknown[] = [];
214
+ const fetchStub = vi.fn(async (_url: unknown, init?: RequestInit) => {
215
+ bodies.push(JSON.parse(String(init?.body)));
216
+ return new Response(
217
+ new ReadableStream({
218
+ start(c) {
219
+ c.enqueue(
220
+ new TextEncoder().encode(
221
+ 'data: {"type":"start"}\n\ndata: [DONE]\n\n',
222
+ ),
223
+ );
224
+ c.close();
225
+ },
226
+ }),
227
+ { headers: { "Content-Type": "text/event-stream" } },
228
+ );
229
+ });
230
+ vi.stubGlobal("fetch", fetchStub);
231
+ try {
232
+ const handle = createAssistantClient(
233
+ AuiConfig({ threads: AISDKThreads() }),
234
+ );
235
+ handle.subscribe(() => {});
236
+ const aui = handle.getClient();
237
+
238
+ flushTapSync(() =>
239
+ aui.modelContext.register({
240
+ getModelContext: () => ({ system: "default path system" }),
241
+ }),
242
+ );
243
+ flushTapSync(() => aui.composer.setText("hello"));
244
+ flushTapSync(() => aui.composer.send());
245
+ await vi.waitFor(() => expect(fetchStub).toHaveBeenCalledTimes(1));
246
+ expect(bodies[0]).toMatchObject({ system: "default path system" });
247
+
248
+ handle.destroy();
249
+ } finally {
250
+ vi.unstubAllGlobals();
251
+ }
252
+ });
253
+
254
+ it("forwards ChatInit callbacks to each thread's chat", async () => {
255
+ const { transport, emit, close } = createControlledTransport();
256
+ const onFinish = vi.fn();
257
+ const handle = createAssistantClient(
258
+ AuiConfig({
259
+ threads: AISDKThreads({ transport: () => transport, onFinish }),
260
+ }),
261
+ );
262
+ handle.subscribe(() => {});
263
+ const aui = handle.getClient();
264
+
265
+ flushTapSync(() => aui.composer.setText("hi"));
266
+ flushTapSync(() => aui.composer.send());
267
+ await vi.waitFor(() => {
268
+ expect(
269
+ handle.getClient().thread.getState().messages.length,
270
+ ).toBeGreaterThan(0);
271
+ });
272
+ emit(...textReply("done"));
273
+ close();
274
+ await vi.waitFor(() => expect(onFinish).toHaveBeenCalledTimes(1));
275
+
276
+ handle.destroy();
277
+ });
278
+
279
+ it("posts each thread's own id as the chat id", async () => {
280
+ const bodies: unknown[] = [];
281
+ const fetchStub = vi.fn(async (_url: unknown, init?: RequestInit) => {
282
+ bodies.push(JSON.parse(String(init?.body)));
283
+ return new Response(
284
+ new ReadableStream({
285
+ start(c) {
286
+ c.enqueue(
287
+ new TextEncoder().encode(
288
+ 'data: {"type":"start"}\n\ndata: [DONE]\n\n',
289
+ ),
290
+ );
291
+ c.close();
292
+ },
293
+ }),
294
+ { headers: { "Content-Type": "text/event-stream" } },
295
+ );
296
+ });
297
+ vi.stubGlobal("fetch", fetchStub);
298
+ try {
299
+ const handle = createAssistantClient(
300
+ AuiConfig({ threads: AISDKThreads() }),
301
+ );
302
+ handle.subscribe(() => {});
303
+ const aui = handle.getClient();
304
+
305
+ flushTapSync(() => aui.composer.setText("hello"));
306
+ flushTapSync(() => aui.composer.send());
307
+ await vi.waitFor(() => expect(fetchStub).toHaveBeenCalledTimes(1));
308
+ expect(bodies[0]).toMatchObject({ id: "main" });
309
+
310
+ flushTapSync(() => aui.threads.switchToNewThread());
311
+ const newThreadId = handle.getClient().threads.getState().mainThreadId;
312
+ flushTapSync(() => handle.getClient().composer.setText("hi again"));
313
+ flushTapSync(() => handle.getClient().composer.send());
314
+ await vi.waitFor(() => expect(fetchStub).toHaveBeenCalledTimes(2));
315
+ expect(bodies[1]).toMatchObject({ id: newThreadId });
316
+
317
+ handle.destroy();
318
+ } finally {
319
+ vi.unstubAllGlobals();
320
+ }
321
+ });
322
+
323
+ it("uses assistant-cloud for listing and lifecycle actions", async () => {
324
+ const cloudThread = (id: string) => ({
325
+ id,
326
+ title: id,
327
+ is_archived: false,
328
+ last_message_at: null,
329
+ external_id: null,
330
+ metadata: null,
331
+ });
332
+ const list = vi.fn(async () => ({
333
+ threads: [cloudThread("cloud-1"), cloudThread("cloud-2")],
334
+ }));
335
+ const create = vi.fn(async () => ({ thread_id: "cloud-created" }));
336
+ const deleteThread = vi.fn(async () => {});
337
+ const cloud = {
338
+ threads: {
339
+ list,
340
+ create,
341
+ update: vi.fn(async () => {}),
342
+ delete: deleteThread,
343
+ get: vi.fn(async (id: string) => cloudThread(id)),
344
+ messages: { list: vi.fn(async () => ({ messages: [] })) },
345
+ },
346
+ runs: { stream: vi.fn() },
347
+ } as unknown as AssistantCloud;
348
+ const handle = createAssistantClient(
349
+ AuiConfig({ threads: AISDKThreads({ cloud }) }),
350
+ );
351
+ handle.subscribe(() => {});
352
+ const aui = handle.getClient();
353
+ try {
354
+ await aui.threads.getLoadThreadsPromise();
355
+ await vi.waitFor(() => {
356
+ expect(aui.threads.getState().threadIds).toEqual([
357
+ "cloud-1",
358
+ "cloud-2",
359
+ ]);
360
+ });
361
+
362
+ flushTapSync(() => aui.threads.switchToThread("cloud-1"));
363
+ await vi.waitFor(() => {
364
+ expect(aui.threads.getState().mainThreadId).toBe("cloud-1");
365
+ });
366
+
367
+ flushTapSync(() => aui.threads.switchToThread("cloud-2"));
368
+ await vi.waitFor(() => {
369
+ expect(aui.threads.getState().mainThreadId).toBe("cloud-2");
370
+ });
371
+
372
+ flushTapSync(() => aui.threads.switchToNewThread());
373
+ const newThreadId = handle.getClient().threads.getState().mainThreadId;
374
+ await handle.getClient().threads.item("main").initialize();
375
+ expect(create).toHaveBeenCalledWith(
376
+ expect.objectContaining({ external_id: undefined }),
377
+ );
378
+ await vi.waitFor(() => {
379
+ expect(
380
+ handle.getClient().threads.item({ id: newThreadId }).getState()
381
+ .remoteId,
382
+ ).toBe("cloud-created");
383
+ });
384
+
385
+ await handle.getClient().threads.item({ id: "cloud-1" }).delete();
386
+ expect(deleteThread).toHaveBeenCalledWith("cloud-1");
387
+ } finally {
388
+ handle.destroy();
389
+ }
390
+ });
391
+
392
+ it("persists a settled cloud run to the departing thread after a switch", async () => {
393
+ const cloudThread = (id: string) => ({
394
+ id,
395
+ title: id,
396
+ is_archived: false,
397
+ last_message_at: null,
398
+ external_id: null,
399
+ metadata: null,
400
+ });
401
+ const create = vi.fn(async () => ({ message_id: "remote-message-1" }));
402
+ const cloud = {
403
+ threads: {
404
+ list: vi.fn(async () => ({
405
+ threads: [cloudThread("t1"), cloudThread("t2")],
406
+ })),
407
+ create: vi.fn(async () => ({ thread_id: "should-not-create" })),
408
+ update: vi.fn(),
409
+ delete: vi.fn(),
410
+ get: vi.fn(async (id: string) => cloudThread(id)),
411
+ messages: {
412
+ list: vi.fn(async () => ({ messages: [] })),
413
+ create,
414
+ update: vi.fn(),
415
+ },
416
+ },
417
+ runs: { stream: vi.fn(), report: vi.fn() },
418
+ telemetry: { enabled: false },
419
+ } as unknown as AssistantCloud;
420
+ const { transport, emit, close } = createControlledTransport();
421
+ const handle = createAssistantClient(
422
+ AuiConfig({
423
+ threads: AISDKThreads({
424
+ cloud,
425
+ threadId: "t1",
426
+ transport,
427
+ }),
428
+ }),
429
+ );
430
+ handle.subscribe(() => {});
431
+ const aui = handle.getClient();
432
+ try {
433
+ await aui.threads.getLoadThreadsPromise();
434
+ await vi.waitFor(() => {
435
+ expect(handle.getClient().threads.getState().mainThreadId).toBe("t1");
436
+ });
437
+ await vi.waitFor(() => {
438
+ expect(handle.getClient().thread.getState().isLoading).toBe(false);
439
+ });
440
+
441
+ flushTapSync(() => handle.getClient().composer.setText("hello"));
442
+ flushTapSync(() => handle.getClient().composer.send());
443
+ await vi.waitFor(() => {
444
+ expect(handle.getClient().thread.getState().isRunning).toBe(true);
445
+ });
446
+
447
+ const switched = new Promise<void>((resolve) => {
448
+ const unsubscribe = handle.subscribe(() => {
449
+ const thread = handle.getClient().thread.getState();
450
+ if (thread.isRunning || thread.messages.length < 2) return;
451
+ unsubscribe();
452
+ flushTapSync(() => handle.getClient().threads.switchToThread("t2"));
453
+ resolve();
454
+ });
455
+ });
456
+ emit(...textReply("answer"));
457
+ close();
458
+ await switched;
459
+ await vi.waitFor(() => {
460
+ expect(handle.getClient().threads.getState().mainThreadId).toBe("t2");
461
+ });
462
+ await vi.waitFor(() => {
463
+ expect(create).toHaveBeenCalled();
464
+ });
465
+ const threadIds = create.mock.calls.map((call) => call[0]);
466
+ expect(threadIds).toContain("t1");
467
+ expect(threadIds).not.toContain("t2");
468
+ } finally {
469
+ handle.destroy();
470
+ }
471
+ });
472
+
473
+ it("persists a new cloud draft to the departing thread after a switch", async () => {
474
+ const cloudThread = (id: string) => ({
475
+ id,
476
+ title: id,
477
+ is_archived: false,
478
+ last_message_at: null,
479
+ external_id: null,
480
+ metadata: null,
481
+ });
482
+ const createMessage = vi.fn(async () => ({
483
+ message_id: "remote-message-1",
484
+ }));
485
+ const cloud = {
486
+ threads: {
487
+ list: vi.fn(async () => ({ threads: [cloudThread("t1")] })),
488
+ create: vi.fn(async () => ({ thread_id: "cloud-created" })),
489
+ update: vi.fn(),
490
+ delete: vi.fn(),
491
+ get: vi.fn(async (id: string) => cloudThread(id)),
492
+ messages: {
493
+ list: vi.fn(async () => ({ messages: [] })),
494
+ create: createMessage,
495
+ update: vi.fn(),
496
+ },
497
+ },
498
+ runs: { stream: vi.fn(), report: vi.fn() },
499
+ telemetry: { enabled: false },
500
+ } as unknown as AssistantCloud;
501
+ const { transport, emit, close } = createControlledTransport();
502
+ const handle = createAssistantClient(
503
+ AuiConfig({
504
+ threads: AISDKThreads({
505
+ cloud,
506
+ transport,
507
+ }),
508
+ }),
509
+ );
510
+ handle.subscribe(() => {});
511
+ try {
512
+ await handle.getClient().threads.getLoadThreadsPromise();
513
+ await vi.waitFor(() => {
514
+ expect(handle.getClient().thread.getState().isLoading).toBe(false);
515
+ });
516
+
517
+ flushTapSync(() => handle.getClient().composer.setText("hello"));
518
+ flushTapSync(() => handle.getClient().composer.send());
519
+ await vi.waitFor(() => {
520
+ expect(handle.getClient().thread.getState().isRunning).toBe(true);
521
+ });
522
+
523
+ const switched = new Promise<void>((resolve) => {
524
+ const unsubscribe = handle.subscribe(() => {
525
+ const thread = handle.getClient().thread.getState();
526
+ if (thread.isRunning || thread.messages.length < 2) return;
527
+ unsubscribe();
528
+ flushTapSync(() => handle.getClient().threads.switchToThread("t1"));
529
+ resolve();
530
+ });
531
+ });
532
+ emit(...textReply("answer"));
533
+ close();
534
+ await switched;
535
+ await vi.waitFor(() => {
536
+ expect(handle.getClient().threads.getState().mainThreadId).toBe("t1");
537
+ });
538
+ await vi.waitFor(() => {
539
+ expect(createMessage).toHaveBeenCalled();
540
+ });
541
+ const threadIds = createMessage.mock.calls.map((call) => call[0]);
542
+ expect(threadIds).toContain("cloud-created");
543
+ expect(threadIds).not.toContain("t1");
544
+ } finally {
545
+ handle.destroy();
546
+ }
547
+ });
548
+ });