@assistant-ui/ai-sdk 0.0.7 → 0.0.9

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 (115) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +1 -1
  3. package/dist/adapters/aiSDKFormatAdapter.d.ts +2 -2
  4. package/dist/adapters/aiSDKFormatAdapter.d.ts.map +1 -0
  5. package/dist/adapters/vercelAttachmentAdapter.d.ts +1 -3
  6. package/dist/adapters/vercelAttachmentAdapter.d.ts.map +1 -1
  7. package/dist/adapters/vercelAttachmentAdapter.js +3 -3
  8. package/dist/adapters/vercelAttachmentAdapter.js.map +1 -1
  9. package/dist/aiSDKExtras.d.ts +4 -6
  10. package/dist/aiSDKExtras.d.ts.map +1 -1
  11. package/dist/converters/convertMessage.d.ts +34 -24
  12. package/dist/converters/convertMessage.d.ts.map +1 -1
  13. package/dist/converters/convertMessage.js +56 -12
  14. package/dist/converters/convertMessage.js.map +1 -1
  15. package/dist/converters/modelContentEnvelope.d.ts +6 -7
  16. package/dist/converters/modelContentEnvelope.d.ts.map +1 -1
  17. package/dist/converters/toCreateMessage.d.ts +2 -4
  18. package/dist/converters/toCreateMessage.d.ts.map +1 -1
  19. package/dist/converters/toolOutputConversion.d.ts +20 -22
  20. package/dist/converters/toolOutputConversion.d.ts.map +1 -1
  21. package/dist/hooks.d.ts +2 -4
  22. package/dist/hooks.d.ts.map +1 -1
  23. package/dist/index.d.ts +17 -14
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.native.d.ts +15 -12
  26. package/dist/index.native.d.ts.map +1 -0
  27. package/dist/model-context/injectInteractableContext.d.ts +2 -4
  28. package/dist/model-context/injectInteractableContext.d.ts.map +1 -1
  29. package/dist/model-context/injectQuoteContext.d.ts +1 -3
  30. package/dist/model-context/injectQuoteContext.d.ts.map +1 -1
  31. package/dist/runtime/AISDKChat.d.ts +2 -4
  32. package/dist/runtime/AISDKChat.d.ts.map +1 -1
  33. package/dist/runtime/AISDKChat.js +1 -1
  34. package/dist/runtime/AISDKChat.js.map +1 -1
  35. package/dist/runtime/AISDKThreads.d.ts +29 -31
  36. package/dist/runtime/AISDKThreads.d.ts.map +1 -1
  37. package/dist/runtime/AISDKThreads.js +22 -23
  38. package/dist/runtime/AISDKThreads.js.map +1 -1
  39. package/dist/runtime/sdkIdentity.d.ts +1 -3
  40. package/dist/runtime/sdkIdentity.d.ts.map +1 -1
  41. package/dist/runtime/sdkIdentity.js +1 -1
  42. package/dist/runtime/useAISDKRuntime.d.ts +76 -70
  43. package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
  44. package/dist/runtime/useAISDKRuntime.js +311 -77
  45. package/dist/runtime/useAISDKRuntime.js.map +1 -1
  46. package/dist/runtime/useChatRuntime.d.ts +6 -8
  47. package/dist/runtime/useChatRuntime.d.ts.map +1 -1
  48. package/dist/runtime/useChatRuntime.js +14 -10
  49. package/dist/runtime/useChatRuntime.js.map +1 -1
  50. package/dist/runtime/useChatThread.d.ts +94 -89
  51. package/dist/runtime/useChatThread.d.ts.map +1 -1
  52. package/dist/runtime/useChatThread.js +44 -15
  53. package/dist/runtime/useChatThread.js.map +1 -1
  54. package/dist/runtime/useExternalHistory.d.ts +7 -7
  55. package/dist/runtime/useExternalHistory.d.ts.map +1 -1
  56. package/dist/runtime/useExternalHistory.js +231 -60
  57. package/dist/runtime/useExternalHistory.js.map +1 -1
  58. package/dist/runtime/useResourceCleanup.d.ts +1 -3
  59. package/dist/runtime/useResourceCleanup.d.ts.map +1 -1
  60. package/dist/runtime/useResourceCleanup.js +38 -14
  61. package/dist/runtime/useResourceCleanup.js.map +1 -1
  62. package/dist/runtime/useStreamingTiming.d.ts +3 -5
  63. package/dist/runtime/useStreamingTiming.d.ts.map +1 -1
  64. package/dist/runtime/useStreamingTiming.js +1 -1
  65. package/dist/tools/frontendTools.d.ts +21 -23
  66. package/dist/tools/frontendTools.d.ts.map +1 -1
  67. package/dist/tools/generativeTools.d.ts +24 -26
  68. package/dist/tools/generativeTools.d.ts.map +1 -1
  69. package/dist/tools/mcp-stdio.node.d.ts +2 -2
  70. package/dist/tools/mcp-stdio.node.d.ts.map +1 -0
  71. package/dist/tools/mcp-stdio.unsupported.d.ts +2 -4
  72. package/dist/tools/mcp-stdio.unsupported.d.ts.map +1 -1
  73. package/dist/transport/AssistantChatTransport.d.ts +14 -16
  74. package/dist/transport/AssistantChatTransport.d.ts.map +1 -1
  75. package/dist/transport/AssistantChatTransport.js +9 -2
  76. package/dist/transport/AssistantChatTransport.js.map +1 -1
  77. package/dist/transport/resumable.d.ts +25 -27
  78. package/dist/transport/resumable.d.ts.map +1 -1
  79. package/dist/usage.d.ts +8 -10
  80. package/dist/usage.d.ts.map +1 -1
  81. package/dist/usage.js.map +1 -1
  82. package/dist/utils/getVercelAIMessages.d.ts +2 -4
  83. package/dist/utils/getVercelAIMessages.d.ts.map +1 -1
  84. package/dist/utils/sliceMessagesUntil.d.ts +1 -3
  85. package/dist/utils/sliceMessagesUntil.d.ts.map +1 -1
  86. package/package.json +13 -9
  87. package/src/adapters/vercelAttachmentAdapter.test.ts +23 -0
  88. package/src/adapters/vercelAttachmentAdapter.ts +2 -2
  89. package/src/converters/convertMessage.test.ts +541 -3
  90. package/src/converters/convertMessage.ts +123 -15
  91. package/src/index.native.ts +1 -1
  92. package/src/index.ts +1 -1
  93. package/src/runtime/AISDKChat.ts +0 -4
  94. package/src/runtime/AISDKThreads.cloud.test.ts +60 -2
  95. package/src/runtime/AISDKThreads.test.ts +105 -46
  96. package/src/runtime/AISDKThreads.ts +1 -1
  97. package/src/runtime/useAISDKRuntime.approval.integration.test.tsx +1639 -19
  98. package/src/runtime/useAISDKRuntime.approval.test.tsx +252 -9
  99. package/src/runtime/useAISDKRuntime.fast-refresh.test.tsx +182 -0
  100. package/src/runtime/useAISDKRuntime.test.ts +275 -5
  101. package/src/runtime/useAISDKRuntime.ts +533 -60
  102. package/src/runtime/useAISDKRuntime.voice.test.tsx +50 -1
  103. package/src/runtime/useChatRuntime.fast-refresh.test.tsx +94 -0
  104. package/src/runtime/useChatRuntime.integration.test.tsx +143 -65
  105. package/src/runtime/useChatRuntime.test.ts +11 -7
  106. package/src/runtime/useChatRuntime.ts +6 -2
  107. package/src/runtime/useChatThread.test.ts +166 -2
  108. package/src/runtime/useChatThread.transport.test.tsx +152 -0
  109. package/src/runtime/useChatThread.ts +89 -22
  110. package/src/runtime/useExternalHistory.test.ts +943 -3
  111. package/src/runtime/useExternalHistory.ts +378 -15
  112. package/src/runtime/useResourceCleanup.ts +41 -15
  113. package/src/transport/AssistantChatTransport.test.ts +164 -0
  114. package/src/transport/AssistantChatTransport.ts +22 -2
  115. package/src/usage.ts +1 -1
@@ -205,6 +205,170 @@ const wrappedFetchOf = (
205
205
  ).fetch;
206
206
 
207
207
  describe("AssistantChatTransport resumable fetch wrapper", () => {
208
+ it("keeps a replacement checkpoint set while preparing an older reconnect", async () => {
209
+ const storage = createMemoryStorage("stream-old");
210
+ let finishPrepare!: () => void;
211
+ const prepareReconnectToStreamRequest = vi.fn(async () => {
212
+ await new Promise<void>((resolve) => {
213
+ finishPrepare = resolve;
214
+ });
215
+ return { headers: { "x-custom": "retained" } };
216
+ });
217
+ const fetch = vi
218
+ .fn<typeof globalThis.fetch>()
219
+ .mockResolvedValue(new Response(null, { status: 204 }));
220
+ const transport = new AssistantChatTransport({
221
+ fetch,
222
+ prepareReconnectToStreamRequest,
223
+ resumable: { storage, resumeApi: (id) => `/api/resume/${id}` },
224
+ });
225
+ const pending = transport.reconnectToStream({ chatId: "thread" });
226
+ await vi.waitFor(() =>
227
+ expect(prepareReconnectToStreamRequest).toHaveBeenCalledOnce(),
228
+ );
229
+ storage.setStreamId("stream-new");
230
+ finishPrepare();
231
+ await expect(pending).resolves.toBeNull();
232
+ expect(fetch.mock.calls[0]?.[0]).toBe("/api/resume/stream-old");
233
+ expect(
234
+ Array.from(new Headers(fetch.mock.calls[0]?.[1]?.headers).entries()),
235
+ ).toEqual([["x-custom", "retained"]]);
236
+ expect(storage.getStreamId()).toBe("stream-new");
237
+ });
238
+
239
+ it.each([
240
+ { status: 204, replaceCheckpoint: false },
241
+ { status: 204, replaceCheckpoint: true },
242
+ { status: 404, replaceCheckpoint: false },
243
+ { status: 404, replaceCheckpoint: true },
244
+ ])(
245
+ "clears only the matching checkpoint after $status (replacement: $replaceCheckpoint)",
246
+ async ({ status, replaceCheckpoint }) => {
247
+ const storage = createMemoryStorage("stream-old");
248
+ let respond!: (response: Response) => void;
249
+ const fetch = vi.fn(
250
+ () =>
251
+ new Promise<Response>((resolve) => {
252
+ respond = resolve;
253
+ }),
254
+ );
255
+ const transport = new AssistantChatTransport({
256
+ fetch,
257
+ resumable: { storage, resumeApi: "/api/resume" },
258
+ });
259
+ const pending = transport.reconnectToStream({ chatId: "thread" });
260
+ await vi.waitFor(() => expect(fetch).toHaveBeenCalledOnce());
261
+ if (replaceCheckpoint) storage.setStreamId("stream-new");
262
+ respond(
263
+ new Response(status === 404 ? "stream expired" : null, { status }),
264
+ );
265
+ if (status === 404) {
266
+ await expect(pending).rejects.toThrow("stream expired");
267
+ } else {
268
+ await expect(pending).resolves.toBeNull();
269
+ }
270
+ expect(storage.getStreamId()).toBe(
271
+ replaceCheckpoint ? "stream-new" : null,
272
+ );
273
+ },
274
+ );
275
+
276
+ it.each([undefined, "stream-old", "response-id"])(
277
+ "preserves a replacement checkpoint on a delayed successful reconnect (%s)",
278
+ async (responseId) => {
279
+ const storage = createMemoryStorage("stream-old");
280
+ let respond!: (response: Response) => void;
281
+ const fetch = vi.fn(
282
+ () =>
283
+ new Promise<Response>((resolve) => {
284
+ respond = resolve;
285
+ }),
286
+ );
287
+ const transport = new AssistantChatTransport({
288
+ fetch,
289
+ resumable: { storage, resumeApi: "/api/resume" },
290
+ });
291
+ const pending = transport.reconnectToStream({ chatId: "thread" });
292
+ await vi.waitFor(() => expect(fetch).toHaveBeenCalledOnce());
293
+ storage.setStreamId("stream-new");
294
+ respond(
295
+ new Response('data: {"type":"finish"}\n\n', {
296
+ headers: {
297
+ "content-type": "text/event-stream",
298
+ ...(responseId && { [RESUMABLE_STREAM_ID_HEADER]: responseId }),
299
+ },
300
+ }),
301
+ );
302
+ const stream = await pending;
303
+ expect(storage.getStreamId()).toBe("stream-new");
304
+ const reader = stream!.getReader();
305
+ while (!(await reader.read()).done) {}
306
+ expect(storage.getStreamId()).toBe("stream-new");
307
+ },
308
+ );
309
+
310
+ it.each([undefined, "stream-old", "response-id"])(
311
+ "preserves a checkpoint replaced while consuming a reconnect (%s)",
312
+ async (responseId) => {
313
+ const storage = createMemoryStorage("stream-old");
314
+ let controller!: ReadableStreamDefaultController<Uint8Array>;
315
+ const transport = new AssistantChatTransport({
316
+ fetch: vi.fn(
317
+ async () =>
318
+ new Response(
319
+ new ReadableStream({
320
+ start(value) {
321
+ controller = value;
322
+ },
323
+ }),
324
+ {
325
+ headers: {
326
+ "content-type": "text/event-stream",
327
+ ...(responseId && {
328
+ [RESUMABLE_STREAM_ID_HEADER]: responseId,
329
+ }),
330
+ },
331
+ },
332
+ ),
333
+ ),
334
+ resumable: { storage, resumeApi: "/api/resume" },
335
+ });
336
+ const stream = await transport.reconnectToStream({ chatId: "thread" });
337
+ expect(storage.getStreamId()).toBe(responseId ?? "stream-old");
338
+ storage.setStreamId("stream-new");
339
+ controller.enqueue(
340
+ new TextEncoder().encode('data: {"type":"finish"}\n\n'),
341
+ );
342
+ controller.close();
343
+ const reader = stream!.getReader();
344
+ while (!(await reader.read()).done) {}
345
+ expect(storage.getStreamId()).toBe("stream-new");
346
+ },
347
+ );
348
+
349
+ it.each([undefined, "stream-old", "response-id"])(
350
+ "clears the checkpoint owned by a completed reconnect (%s)",
351
+ async (responseId) => {
352
+ const storage = createMemoryStorage("stream-old");
353
+ const transport = new AssistantChatTransport({
354
+ fetch: vi.fn(
355
+ async () =>
356
+ new Response('data: {"type":"finish"}\n\n', {
357
+ headers: {
358
+ "content-type": "text/event-stream",
359
+ ...(responseId && { [RESUMABLE_STREAM_ID_HEADER]: responseId }),
360
+ },
361
+ }),
362
+ ),
363
+ resumable: { storage, resumeApi: "/api/resume" },
364
+ });
365
+ const stream = await transport.reconnectToStream({ chatId: "thread" });
366
+ const reader = stream!.getReader();
367
+ while (!(await reader.read()).done) {}
368
+ expect(storage.getStreamId()).toBeNull();
369
+ },
370
+ );
371
+
208
372
  it("passes a 204 with a non-null empty body through untouched (WebKit)", async () => {
209
373
  const response = nullBodyStatusWithBody(204);
210
374
  const fetchMock = vi.fn(async () => response);
@@ -22,6 +22,7 @@ const FINISH_MARKER = '"type":"finish"';
22
22
  const FINISH_BUFFER_LIMIT = 4096;
23
23
  const FINISH_BUFFER_TAIL = 1024;
24
24
  const RESUMABLE_THREAD_ID_HEADER = "x-assistant-ui-resumable-thread-id";
25
+ const RESUMABLE_RECONNECT_ID_HEADER = "x-assistant-ui-resumable-reconnect-id";
25
26
 
26
27
  // 101/204/205/304 are null-body statuses per the fetch spec: `new Response(body, { status })`
27
28
  // throws for them, and WebKit returns a non-null empty body, so the body check alone does not guard it.
@@ -137,10 +138,24 @@ function wrapFetchWithResumable(
137
138
  return async (input, init) => {
138
139
  const headers = new Headers(init?.headers);
139
140
  const threadId = headers.get(RESUMABLE_THREAD_ID_HEADER) ?? undefined;
141
+ const reconnectingStreamId = headers.get(RESUMABLE_RECONNECT_ID_HEADER);
142
+ const checkpointAtRequest =
143
+ reconnectingStreamId ?? resumable.storage.getStreamId(threadId);
140
144
  headers.delete(RESUMABLE_THREAD_ID_HEADER);
145
+ headers.delete(RESUMABLE_RECONNECT_ID_HEADER);
141
146
  const res = await baseFetch(input, { ...init, headers });
142
147
  const id = res.headers.get(RESUMABLE_STREAM_ID_HEADER);
143
- if (id) resumable.storage.setStreamId(id, threadId);
148
+ const ownsCheckpoint =
149
+ !reconnectingStreamId ||
150
+ resumable.storage.getStreamId(threadId) === reconnectingStreamId;
151
+ if (id && ownsCheckpoint) resumable.storage.setStreamId(id, threadId);
152
+ if (
153
+ (res.status === 204 || res.status === 404) &&
154
+ reconnectingStreamId &&
155
+ resumable.storage.getStreamId(threadId) === reconnectingStreamId
156
+ ) {
157
+ resumable.storage.clear(threadId);
158
+ }
144
159
  if (!res.body || NULL_BODY_STATUSES.has(res.status)) return res;
145
160
 
146
161
  const detectFinish = resumable.isFinishEvent ?? defaultIsFinishEvent;
@@ -153,7 +168,11 @@ function wrapFetchWithResumable(
153
168
  controller.enqueue(chunk);
154
169
  accumulator += decoder.decode(chunk, { stream: true });
155
170
  if (detectFinish(chunk, accumulator)) {
156
- if (!id || resumable.storage.getStreamId(threadId) === id) {
171
+ if (
172
+ ownsCheckpoint &&
173
+ resumable.storage.getStreamId(threadId) ===
174
+ (id ?? checkpointAtRequest)
175
+ ) {
157
176
  resumable.storage.clear(threadId);
158
177
  }
159
178
  accumulator = "";
@@ -195,6 +214,7 @@ function wrapPrepareReconnect(
195
214
  const userPrepared = await userPrepareReconnect?.({ ...options, api });
196
215
  const headers = new Headers(userPrepared?.headers ?? options.headers);
197
216
  headers.set(RESUMABLE_THREAD_ID_HEADER, options.id);
217
+ headers.set(RESUMABLE_RECONNECT_ID_HEADER, streamId);
198
218
  return {
199
219
  ...userPrepared,
200
220
  headers,
package/src/usage.ts CHANGED
@@ -1,4 +1,4 @@
1
- /// <reference types="@assistant-ui/core/react" />
1
+ /// <reference types="@assistant-ui/core/react" preserve="true" />
2
2
  import { useMemo } from "react";
3
3
  import { useAuiState } from "@assistant-ui/store";
4
4