@assistant-ui/ai-sdk 0.0.6 → 0.0.8

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 (107) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/aiSDKFormatAdapter.d.ts +2 -2
  3. package/dist/adapters/aiSDKFormatAdapter.d.ts.map +1 -0
  4. package/dist/adapters/vercelAttachmentAdapter.d.ts +2 -5
  5. package/dist/adapters/vercelAttachmentAdapter.d.ts.map +1 -1
  6. package/dist/adapters/vercelAttachmentAdapter.js +3 -3
  7. package/dist/adapters/vercelAttachmentAdapter.js.map +1 -1
  8. package/dist/aiSDKExtras.d.ts +6 -9
  9. package/dist/aiSDKExtras.d.ts.map +1 -1
  10. package/dist/converters/convertMessage.d.ts +34 -25
  11. package/dist/converters/convertMessage.d.ts.map +1 -1
  12. package/dist/converters/convertMessage.js +78 -8
  13. package/dist/converters/convertMessage.js.map +1 -1
  14. package/dist/converters/modelContentEnvelope.d.ts +10 -12
  15. package/dist/converters/modelContentEnvelope.d.ts.map +1 -1
  16. package/dist/converters/toCreateMessage.d.ts +3 -6
  17. package/dist/converters/toCreateMessage.d.ts.map +1 -1
  18. package/dist/converters/toolOutputConversion.d.ts +22 -25
  19. package/dist/converters/toolOutputConversion.d.ts.map +1 -1
  20. package/dist/hooks.d.ts +4 -7
  21. package/dist/hooks.d.ts.map +1 -1
  22. package/dist/index.d.ts +17 -14
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.native.d.ts +15 -12
  25. package/dist/index.native.d.ts.map +1 -0
  26. package/dist/model-context/injectInteractableContext.d.ts +3 -6
  27. package/dist/model-context/injectInteractableContext.d.ts.map +1 -1
  28. package/dist/model-context/injectQuoteContext.d.ts +2 -5
  29. package/dist/model-context/injectQuoteContext.d.ts.map +1 -1
  30. package/dist/runtime/AISDKChat.d.ts +4 -7
  31. package/dist/runtime/AISDKChat.d.ts.map +1 -1
  32. package/dist/runtime/AISDKChat.js +1 -1
  33. package/dist/runtime/AISDKThreads.d.ts +29 -32
  34. package/dist/runtime/AISDKThreads.d.ts.map +1 -1
  35. package/dist/runtime/AISDKThreads.js +26 -33
  36. package/dist/runtime/AISDKThreads.js.map +1 -1
  37. package/dist/runtime/sdkIdentity.d.ts +2 -5
  38. package/dist/runtime/sdkIdentity.d.ts.map +1 -1
  39. package/dist/runtime/sdkIdentity.js +1 -1
  40. package/dist/runtime/useAISDKRuntime.d.ts +72 -61
  41. package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
  42. package/dist/runtime/useAISDKRuntime.js +170 -56
  43. package/dist/runtime/useAISDKRuntime.js.map +1 -1
  44. package/dist/runtime/useChatRuntime.d.ts +8 -11
  45. package/dist/runtime/useChatRuntime.d.ts.map +1 -1
  46. package/dist/runtime/useChatRuntime.js +14 -10
  47. package/dist/runtime/useChatRuntime.js.map +1 -1
  48. package/dist/runtime/useChatThread.d.ts +100 -81
  49. package/dist/runtime/useChatThread.d.ts.map +1 -1
  50. package/dist/runtime/useChatThread.js +33 -13
  51. package/dist/runtime/useChatThread.js.map +1 -1
  52. package/dist/runtime/useExternalHistory.d.ts +8 -9
  53. package/dist/runtime/useExternalHistory.d.ts.map +1 -1
  54. package/dist/runtime/useExternalHistory.js +236 -58
  55. package/dist/runtime/useExternalHistory.js.map +1 -1
  56. package/dist/runtime/useResourceCleanup.d.ts +1 -4
  57. package/dist/runtime/useResourceCleanup.d.ts.map +1 -1
  58. package/dist/runtime/useResourceCleanup.js +38 -14
  59. package/dist/runtime/useResourceCleanup.js.map +1 -1
  60. package/dist/runtime/useStreamingTiming.d.ts +5 -8
  61. package/dist/runtime/useStreamingTiming.d.ts.map +1 -1
  62. package/dist/runtime/useStreamingTiming.js +1 -1
  63. package/dist/tools/frontendTools.d.ts +24 -27
  64. package/dist/tools/frontendTools.d.ts.map +1 -1
  65. package/dist/tools/generativeTools.d.ts +29 -32
  66. package/dist/tools/generativeTools.d.ts.map +1 -1
  67. package/dist/tools/mcp-stdio.node.d.ts +2 -2
  68. package/dist/tools/mcp-stdio.node.d.ts.map +1 -0
  69. package/dist/tools/mcp-stdio.unsupported.d.ts +2 -5
  70. package/dist/tools/mcp-stdio.unsupported.d.ts.map +1 -1
  71. package/dist/transport/AssistantChatTransport.d.ts +17 -20
  72. package/dist/transport/AssistantChatTransport.d.ts.map +1 -1
  73. package/dist/transport/resumable.d.ts +29 -32
  74. package/dist/transport/resumable.d.ts.map +1 -1
  75. package/dist/usage.d.ts +12 -15
  76. package/dist/usage.d.ts.map +1 -1
  77. package/dist/usage.js.map +1 -1
  78. package/dist/utils/getVercelAIMessages.d.ts +3 -6
  79. package/dist/utils/getVercelAIMessages.d.ts.map +1 -1
  80. package/dist/utils/sliceMessagesUntil.d.ts +2 -5
  81. package/dist/utils/sliceMessagesUntil.d.ts.map +1 -1
  82. package/package.json +16 -14
  83. package/src/adapters/vercelAttachmentAdapter.test.ts +23 -0
  84. package/src/adapters/vercelAttachmentAdapter.ts +2 -2
  85. package/src/converters/convertMessage.test.ts +616 -3
  86. package/src/converters/convertMessage.ts +187 -12
  87. package/src/index.native.ts +1 -1
  88. package/src/index.ts +1 -1
  89. package/src/runtime/AISDKChat.test.ts +4 -5
  90. package/src/runtime/AISDKThreads.cloud.test.ts +60 -2
  91. package/src/runtime/AISDKThreads.test.ts +88 -51
  92. package/src/runtime/AISDKThreads.ts +4 -13
  93. package/src/runtime/useAISDKRuntime.approval.integration.test.tsx +338 -0
  94. package/src/runtime/useAISDKRuntime.approval.test.tsx +476 -4
  95. package/src/runtime/useAISDKRuntime.test.ts +282 -8
  96. package/src/runtime/useAISDKRuntime.ts +274 -46
  97. package/src/runtime/useAISDKRuntime.voice.test.tsx +319 -0
  98. package/src/runtime/useChatRuntime.integration.test.tsx +56 -0
  99. package/src/runtime/useChatRuntime.test.ts +77 -5
  100. package/src/runtime/useChatRuntime.ts +6 -2
  101. package/src/runtime/useChatThread.transport.test.tsx +149 -0
  102. package/src/runtime/useChatThread.ts +77 -10
  103. package/src/runtime/useExternalHistory.test.ts +989 -3
  104. package/src/runtime/useExternalHistory.ts +390 -24
  105. package/src/runtime/useResourceCleanup.ts +41 -15
  106. package/src/tools/generativeTools.test.ts +7 -1
  107. package/src/usage.ts +1 -1
@@ -0,0 +1,338 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { act, renderHook, waitFor } from "@testing-library/react";
4
+ import { useChat } from "@ai-sdk/react";
5
+ import {
6
+ lastAssistantMessageIsCompleteWithApprovalResponses,
7
+ type ChatTransport,
8
+ type UIMessage,
9
+ type UIMessageChunk,
10
+ } from "ai";
11
+ import { describe, expect, it, vi } from "vitest";
12
+ import { useAISDKRuntime } from "./useAISDKRuntime";
13
+
14
+ type ApprovalHandler = NonNullable<
15
+ NonNullable<Parameters<typeof useAISDKRuntime>[1]>["onRespondToToolApproval"]
16
+ >;
17
+
18
+ const streamOf = (chunks: UIMessageChunk[]) =>
19
+ new ReadableStream<UIMessageChunk>({
20
+ start(controller) {
21
+ for (const chunk of chunks) controller.enqueue(chunk);
22
+ controller.close();
23
+ },
24
+ });
25
+
26
+ const approvalStep = (extra: UIMessageChunk[] = []): UIMessageChunk[] => [
27
+ { type: "start", messageId: "assistant-1" },
28
+ { type: "start-step" },
29
+ {
30
+ type: "tool-input-available",
31
+ toolCallId: "tool-1",
32
+ toolName: "deploy",
33
+ input: {},
34
+ },
35
+ {
36
+ type: "tool-approval-request",
37
+ approvalId: "approval-1",
38
+ toolCallId: "tool-1",
39
+ },
40
+ ...extra,
41
+ { type: "finish-step" },
42
+ { type: "finish" },
43
+ ];
44
+
45
+ const toolOutput: UIMessageChunk[] = [
46
+ { type: "start" },
47
+ { type: "tool-output-available", toolCallId: "tool-1", output: "deployed" },
48
+ { type: "finish" },
49
+ ];
50
+
51
+ const setup = async (
52
+ createHandler: (chat: () => ReturnType<typeof useChat>) => ApprovalHandler,
53
+ {
54
+ messages,
55
+ request = approvalStep(),
56
+ continuation = () => streamOf(toolOutput),
57
+ }: {
58
+ messages?: UIMessage[];
59
+ request?: UIMessageChunk[];
60
+ continuation?: () => ReadableStream<UIMessageChunk>;
61
+ } = {},
62
+ ) => {
63
+ let requests = 0;
64
+ const sendMessages = vi.fn<ChatTransport<UIMessage>["sendMessages"]>(
65
+ async () => {
66
+ requests += 1;
67
+ return requests === 1 && !messages ? streamOf(request) : continuation();
68
+ },
69
+ );
70
+ const sendAutomaticallyWhen = vi.fn(
71
+ lastAssistantMessageIsCompleteWithApprovalResponses,
72
+ );
73
+
74
+ let handler: ApprovalHandler | undefined;
75
+ const { result } = renderHook(() => {
76
+ const chat = useChat({
77
+ id: "chat-1",
78
+ ...(messages && { messages }),
79
+ transport: { sendMessages, reconnectToStream: async () => null },
80
+ sendAutomaticallyWhen,
81
+ });
82
+ return {
83
+ chat,
84
+ runtime: useAISDKRuntime(chat, {
85
+ onRespondToToolApproval: (response, context) =>
86
+ handler?.(response, context),
87
+ }),
88
+ };
89
+ });
90
+ const chat = () => result.current.chat;
91
+ handler = createHandler(chat);
92
+
93
+ if (!messages) {
94
+ await act(() => chat().sendMessage({ text: "deploy" }));
95
+ await waitFor(() => expect(chat().status).toBe("ready"));
96
+ }
97
+
98
+ const part = () =>
99
+ result.current.runtime.thread
100
+ .getMessageByIndex(1)
101
+ .getMessagePartByToolCallId("tool-1");
102
+
103
+ return {
104
+ chat,
105
+ part,
106
+ approval: () =>
107
+ (part().getState() as { approval?: Record<string, unknown> }).approval,
108
+ toolPart: () =>
109
+ chat()
110
+ .messages.flatMap((message) => message.parts)
111
+ .find((candidate) => candidate.type === "tool-deploy"),
112
+ sendMessages,
113
+ sendAutomaticallyWhen,
114
+ respond: (approved = true) =>
115
+ act(() => part().respondToToolApproval({ approved })),
116
+ };
117
+ };
118
+
119
+ describe("useAISDKRuntime tool approvals with a Chat", () => {
120
+ it("applies a host answer without writing it into the chat", async () => {
121
+ const { approval, toolPart, sendMessages, sendAutomaticallyWhen, respond } =
122
+ await setup(() => async () => {});
123
+ const automaticSendChecks = sendAutomaticallyWhen.mock.calls.length;
124
+
125
+ await respond();
126
+
127
+ expect(approval()).toMatchObject({ id: "approval-1", approved: true });
128
+ expect(toolPart()).toMatchObject({ state: "approval-requested" });
129
+ expect(sendAutomaticallyWhen).toHaveBeenCalledTimes(automaticSendChecks);
130
+ expect(sendMessages).toHaveBeenCalledTimes(1);
131
+ });
132
+
133
+ it("renders a streamed request as its approvalDescriptor declares", async () => {
134
+ const descriptor = {
135
+ prompt: "Which environment?",
136
+ display: "select",
137
+ options: [{ id: "once", kind: "allow-once", label: "Staging once" }],
138
+ scope: "deploy",
139
+ };
140
+ const handler = vi.fn<ApprovalHandler>(async () => {});
141
+ const { approval, part, toolPart } = await setup(() => handler, {
142
+ request: [
143
+ { type: "start", messageId: "assistant-1" },
144
+ { type: "start-step" },
145
+ {
146
+ type: "tool-input-available",
147
+ toolCallId: "tool-1",
148
+ toolName: "deploy",
149
+ input: {},
150
+ },
151
+ {
152
+ type: "tool-approval-request",
153
+ approvalId: "approval-1",
154
+ toolCallId: "tool-1",
155
+ approvalDescriptor: descriptor,
156
+ },
157
+ { type: "finish-step" },
158
+ { type: "finish" },
159
+ ],
160
+ });
161
+
162
+ expect(toolPart()).toMatchObject({
163
+ state: "approval-requested",
164
+ approval: { id: "approval-1", descriptor },
165
+ });
166
+ expect(approval()).toEqual({
167
+ id: "approval-1",
168
+ prompt: "Which environment?",
169
+ display: "select",
170
+ options: [{ id: "once", kind: "allow-once", label: "Staging once" }],
171
+ descriptor,
172
+ });
173
+
174
+ await act(() => part().respondToToolApproval({ optionId: "once" }));
175
+
176
+ expect(handler).toHaveBeenCalledWith(
177
+ { approvalId: "approval-1", approved: true, optionId: "once" },
178
+ expect.objectContaining({ toolCallId: "tool-1", toolName: "deploy" }),
179
+ );
180
+ expect(approval()).toMatchObject({ approved: true, optionId: "once" });
181
+ expect(toolPart()).toMatchObject({ state: "approval-requested" });
182
+ });
183
+
184
+ it("keeps a host answer out of the chat's automatic sends", async () => {
185
+ const { chat, sendMessages, respond } = await setup(() => async () => {}, {
186
+ request: approvalStep([
187
+ {
188
+ type: "tool-input-available",
189
+ toolCallId: "tool-2",
190
+ toolName: "lookup",
191
+ input: {},
192
+ },
193
+ ]),
194
+ });
195
+
196
+ await respond();
197
+ await act(async () => {
198
+ await chat().addToolOutput({
199
+ tool: "lookup",
200
+ toolCallId: "tool-2",
201
+ output: "found",
202
+ } as never);
203
+ await new Promise((resolve) => setTimeout(resolve, 0));
204
+ });
205
+
206
+ expect(sendMessages).toHaveBeenCalledTimes(1);
207
+ });
208
+
209
+ it("reopens the request when the handler throws", async () => {
210
+ const { approval, respond } = await setup(() => async () => {
211
+ throw new Error("resume failed");
212
+ });
213
+
214
+ await expect(respond()).rejects.toThrow("resume failed");
215
+
216
+ expect(approval()).toMatchObject({ id: "approval-1" });
217
+ expect(approval()).not.toHaveProperty("approved");
218
+ });
219
+
220
+ it("keeps the answer on a run the handler continues on the chat", async () => {
221
+ let stream!: ReadableStreamDefaultController<UIMessageChunk>;
222
+ const { approval, toolPart, respond } = await setup(
223
+ (chat) => async () => {
224
+ void chat().sendMessage();
225
+ },
226
+ {
227
+ continuation: () =>
228
+ new ReadableStream<UIMessageChunk>({
229
+ start(controller) {
230
+ stream = controller;
231
+ },
232
+ }),
233
+ },
234
+ );
235
+
236
+ await respond();
237
+ await act(async () => {
238
+ stream.enqueue({ type: "start" });
239
+ stream.enqueue({ type: "text-start", id: "text-1" });
240
+ stream.enqueue({ type: "text-delta", id: "text-1", delta: "Deploying" });
241
+ });
242
+ expect(approval()).toMatchObject({ id: "approval-1", approved: true });
243
+
244
+ await act(async () => {
245
+ stream.enqueue({ type: "text-end", id: "text-1" });
246
+ for (const chunk of toolOutput.slice(1)) stream.enqueue(chunk);
247
+ stream.close();
248
+ });
249
+ await waitFor(() =>
250
+ expect(toolPart()).toMatchObject({ state: "output-available" }),
251
+ );
252
+ expect(approval()).toMatchObject({ id: "approval-1", approved: true });
253
+ });
254
+
255
+ it("delivers one decision when a request is answered twice at once", async () => {
256
+ const decisions: boolean[] = [];
257
+ const releases: (() => void)[] = [];
258
+ const { approval, part } = await setup(() => async ({ approved }) => {
259
+ decisions.push(approved);
260
+ await new Promise<void>((resolve) => releases.push(resolve));
261
+ });
262
+
263
+ let approve: Promise<void> | undefined;
264
+ let deny: Promise<void> | undefined;
265
+ act(() => {
266
+ approve = part().respondToToolApproval({ approved: true });
267
+ deny = part().respondToToolApproval({ approved: false });
268
+ });
269
+ await expect(deny).rejects.toThrow(
270
+ "Tool approval approval-1 is not waiting for a response.",
271
+ );
272
+ for (const release of releases) release();
273
+ await act(() => approve);
274
+
275
+ expect(decisions).toEqual([true]);
276
+ expect(approval()).toMatchObject({ id: "approval-1", approved: true });
277
+ });
278
+
279
+ it("continues the run through the AI SDK for a request handed back", async () => {
280
+ const { toolPart, sendMessages, respond } = await setup(
281
+ () =>
282
+ (_response, { respondViaAISDK }) =>
283
+ respondViaAISDK(),
284
+ );
285
+
286
+ await respond();
287
+
288
+ await waitFor(() =>
289
+ expect(toolPart()).toMatchObject({
290
+ state: "output-available",
291
+ approval: { id: "approval-1", approved: true },
292
+ output: "deployed",
293
+ }),
294
+ );
295
+ expect(sendMessages).toHaveBeenCalledTimes(2);
296
+ });
297
+
298
+ it("leaves a request handed back to the AI SDK as the AI SDK does", async () => {
299
+ const { approval, toolPart, sendMessages, respond } = await setup(
300
+ () =>
301
+ (_response, { respondViaAISDK }) =>
302
+ respondViaAISDK(),
303
+ {
304
+ messages: [
305
+ {
306
+ id: "user-1",
307
+ role: "user",
308
+ parts: [{ type: "text", text: "deploy" }],
309
+ },
310
+ {
311
+ id: "assistant-1",
312
+ role: "assistant",
313
+ parts: [
314
+ {
315
+ type: "tool-deploy",
316
+ toolCallId: "tool-1",
317
+ state: "approval-requested",
318
+ input: {},
319
+ approval: { id: "approval-1" },
320
+ },
321
+ ],
322
+ },
323
+ {
324
+ id: "assistant-2",
325
+ role: "assistant",
326
+ parts: [{ type: "text", text: "Waiting for approval." }],
327
+ },
328
+ ],
329
+ },
330
+ );
331
+
332
+ await respond();
333
+
334
+ expect(toolPart()).toMatchObject({ state: "approval-requested" });
335
+ expect(approval()).not.toHaveProperty("approved");
336
+ expect(sendMessages).not.toHaveBeenCalled();
337
+ });
338
+ });