@assistant-ui/ai-sdk 0.0.7 → 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.
- package/README.md +1 -1
- package/dist/adapters/aiSDKFormatAdapter.d.ts +2 -2
- package/dist/adapters/aiSDKFormatAdapter.d.ts.map +1 -0
- package/dist/adapters/vercelAttachmentAdapter.d.ts +1 -3
- package/dist/adapters/vercelAttachmentAdapter.d.ts.map +1 -1
- package/dist/adapters/vercelAttachmentAdapter.js +3 -3
- package/dist/adapters/vercelAttachmentAdapter.js.map +1 -1
- package/dist/aiSDKExtras.d.ts +4 -6
- package/dist/aiSDKExtras.d.ts.map +1 -1
- package/dist/converters/convertMessage.d.ts +32 -24
- package/dist/converters/convertMessage.d.ts.map +1 -1
- package/dist/converters/convertMessage.js +43 -9
- package/dist/converters/convertMessage.js.map +1 -1
- package/dist/converters/modelContentEnvelope.d.ts +6 -7
- package/dist/converters/modelContentEnvelope.d.ts.map +1 -1
- package/dist/converters/toCreateMessage.d.ts +2 -4
- package/dist/converters/toCreateMessage.d.ts.map +1 -1
- package/dist/converters/toolOutputConversion.d.ts +20 -22
- package/dist/converters/toolOutputConversion.d.ts.map +1 -1
- package/dist/hooks.d.ts +2 -4
- package/dist/hooks.d.ts.map +1 -1
- package/dist/index.d.ts +17 -14
- package/dist/index.d.ts.map +1 -0
- package/dist/index.native.d.ts +15 -12
- package/dist/index.native.d.ts.map +1 -0
- package/dist/model-context/injectInteractableContext.d.ts +2 -4
- package/dist/model-context/injectInteractableContext.d.ts.map +1 -1
- package/dist/model-context/injectQuoteContext.d.ts +1 -3
- package/dist/model-context/injectQuoteContext.d.ts.map +1 -1
- package/dist/runtime/AISDKChat.d.ts +2 -4
- package/dist/runtime/AISDKChat.d.ts.map +1 -1
- package/dist/runtime/AISDKChat.js +1 -1
- package/dist/runtime/AISDKThreads.d.ts +29 -31
- package/dist/runtime/AISDKThreads.d.ts.map +1 -1
- package/dist/runtime/AISDKThreads.js +22 -23
- package/dist/runtime/AISDKThreads.js.map +1 -1
- package/dist/runtime/sdkIdentity.d.ts +1 -3
- package/dist/runtime/sdkIdentity.d.ts.map +1 -1
- package/dist/runtime/sdkIdentity.js +1 -1
- package/dist/runtime/useAISDKRuntime.d.ts +69 -70
- package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
- package/dist/runtime/useAISDKRuntime.js +112 -56
- package/dist/runtime/useAISDKRuntime.js.map +1 -1
- package/dist/runtime/useChatRuntime.d.ts +6 -8
- package/dist/runtime/useChatRuntime.d.ts.map +1 -1
- package/dist/runtime/useChatRuntime.js +14 -10
- package/dist/runtime/useChatRuntime.js.map +1 -1
- package/dist/runtime/useChatThread.d.ts +94 -89
- package/dist/runtime/useChatThread.d.ts.map +1 -1
- package/dist/runtime/useChatThread.js +4 -5
- package/dist/runtime/useChatThread.js.map +1 -1
- package/dist/runtime/useExternalHistory.d.ts +7 -7
- package/dist/runtime/useExternalHistory.d.ts.map +1 -1
- package/dist/runtime/useExternalHistory.js +229 -59
- package/dist/runtime/useExternalHistory.js.map +1 -1
- package/dist/runtime/useResourceCleanup.d.ts +1 -3
- package/dist/runtime/useResourceCleanup.d.ts.map +1 -1
- package/dist/runtime/useResourceCleanup.js +38 -14
- package/dist/runtime/useResourceCleanup.js.map +1 -1
- package/dist/runtime/useStreamingTiming.d.ts +3 -5
- package/dist/runtime/useStreamingTiming.d.ts.map +1 -1
- package/dist/runtime/useStreamingTiming.js +1 -1
- package/dist/tools/frontendTools.d.ts +21 -23
- package/dist/tools/frontendTools.d.ts.map +1 -1
- package/dist/tools/generativeTools.d.ts +24 -26
- package/dist/tools/generativeTools.d.ts.map +1 -1
- package/dist/tools/mcp-stdio.node.d.ts +2 -2
- package/dist/tools/mcp-stdio.node.d.ts.map +1 -0
- package/dist/tools/mcp-stdio.unsupported.d.ts +2 -4
- package/dist/tools/mcp-stdio.unsupported.d.ts.map +1 -1
- package/dist/transport/AssistantChatTransport.d.ts +14 -16
- package/dist/transport/AssistantChatTransport.d.ts.map +1 -1
- package/dist/transport/resumable.d.ts +25 -27
- package/dist/transport/resumable.d.ts.map +1 -1
- package/dist/usage.d.ts +8 -10
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js.map +1 -1
- package/dist/utils/getVercelAIMessages.d.ts +2 -4
- package/dist/utils/getVercelAIMessages.d.ts.map +1 -1
- package/dist/utils/sliceMessagesUntil.d.ts +1 -3
- package/dist/utils/sliceMessagesUntil.d.ts.map +1 -1
- package/package.json +9 -8
- package/src/adapters/vercelAttachmentAdapter.test.ts +23 -0
- package/src/adapters/vercelAttachmentAdapter.ts +2 -2
- package/src/converters/convertMessage.test.ts +478 -3
- package/src/converters/convertMessage.ts +100 -14
- package/src/index.native.ts +1 -1
- package/src/index.ts +1 -1
- package/src/runtime/AISDKThreads.cloud.test.ts +60 -2
- package/src/runtime/AISDKThreads.test.ts +79 -46
- package/src/runtime/AISDKThreads.ts +1 -1
- package/src/runtime/useAISDKRuntime.approval.integration.test.tsx +51 -0
- package/src/runtime/useAISDKRuntime.approval.test.tsx +225 -9
- package/src/runtime/useAISDKRuntime.test.ts +275 -5
- package/src/runtime/useAISDKRuntime.ts +141 -37
- package/src/runtime/useAISDKRuntime.voice.test.tsx +50 -1
- package/src/runtime/useChatRuntime.integration.test.tsx +56 -0
- package/src/runtime/useChatRuntime.test.ts +5 -1
- package/src/runtime/useChatRuntime.ts +6 -2
- package/src/runtime/useChatThread.transport.test.tsx +149 -0
- package/src/runtime/useChatThread.ts +33 -5
- package/src/runtime/useExternalHistory.test.ts +943 -3
- package/src/runtime/useExternalHistory.ts +366 -14
- package/src/runtime/useResourceCleanup.ts +41 -15
- package/src/usage.ts +1 -1
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
|
|
3
|
-
import { act, renderHook } from "@testing-library/react";
|
|
4
|
-
import
|
|
5
|
-
|
|
3
|
+
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
4
|
+
import {
|
|
5
|
+
bindExternalStoreMessage,
|
|
6
|
+
type ExternalStoreAdapter,
|
|
7
|
+
type ThreadAssistantMessage,
|
|
8
|
+
type ThreadMessage,
|
|
9
|
+
} from "@assistant-ui/core";
|
|
10
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
11
|
|
|
7
12
|
const mocks = vi.hoisted(() => ({
|
|
8
13
|
adapter: undefined as ExternalStoreAdapter | undefined,
|
|
14
|
+
persistToolApprovalResponses: vi.fn(),
|
|
15
|
+
threadMessages: [] as ThreadMessage[],
|
|
9
16
|
}));
|
|
10
17
|
|
|
11
18
|
vi.mock("@assistant-ui/core/react", async (importOriginal) => {
|
|
@@ -15,7 +22,11 @@ vi.mock("@assistant-ui/core/react", async (importOriginal) => {
|
|
|
15
22
|
...original,
|
|
16
23
|
useExternalStoreRuntime: vi.fn((adapter: ExternalStoreAdapter) => {
|
|
17
24
|
mocks.adapter = adapter;
|
|
18
|
-
return {
|
|
25
|
+
return {
|
|
26
|
+
thread: {
|
|
27
|
+
getState: () => ({ messages: mocks.threadMessages }),
|
|
28
|
+
},
|
|
29
|
+
} as never;
|
|
19
30
|
}),
|
|
20
31
|
useRuntimeAdapters: vi.fn(() => ({})),
|
|
21
32
|
};
|
|
@@ -29,13 +40,30 @@ vi.mock("./useExternalHistory", async (importOriginal) => {
|
|
|
29
40
|
useExternalHistory: vi.fn(() => ({
|
|
30
41
|
isLoading: false,
|
|
31
42
|
deleteMessage: vi.fn().mockResolvedValue(undefined),
|
|
43
|
+
persistToolInteractions: vi.fn().mockResolvedValue(undefined),
|
|
44
|
+
persistToolApprovalResponses: mocks.persistToolApprovalResponses,
|
|
32
45
|
})),
|
|
33
46
|
};
|
|
34
47
|
});
|
|
35
48
|
|
|
36
49
|
import { useAISDKRuntime } from "./useAISDKRuntime";
|
|
50
|
+
import { useExternalHistory } from "./useExternalHistory";
|
|
37
51
|
|
|
38
52
|
describe("useAISDKRuntime tool approvals", () => {
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
mocks.adapter = undefined;
|
|
55
|
+
mocks.threadMessages = [];
|
|
56
|
+
mocks.persistToolApprovalResponses.mockReset().mockResolvedValue(undefined);
|
|
57
|
+
vi.mocked(useExternalHistory)
|
|
58
|
+
.mockReset()
|
|
59
|
+
.mockImplementation(() => ({
|
|
60
|
+
isLoading: false,
|
|
61
|
+
deleteMessage: vi.fn().mockResolvedValue(undefined),
|
|
62
|
+
persistToolInteractions: vi.fn().mockResolvedValue(undefined),
|
|
63
|
+
persistToolApprovalResponses: mocks.persistToolApprovalResponses,
|
|
64
|
+
}));
|
|
65
|
+
});
|
|
66
|
+
|
|
39
67
|
it("forwards the AI SDK approval promise to the external-store adapter", () => {
|
|
40
68
|
const approvalPromise = Promise.resolve();
|
|
41
69
|
const addToolApprovalResponse = vi.fn(() => approvalPromise);
|
|
@@ -88,6 +116,7 @@ describe("useAISDKRuntime tool approvals", () => {
|
|
|
88
116
|
},
|
|
89
117
|
];
|
|
90
118
|
const setMessages = vi.fn();
|
|
119
|
+
const sendMessage = vi.fn();
|
|
91
120
|
const addToolApprovalResponse = vi.fn();
|
|
92
121
|
const chat = {
|
|
93
122
|
id: "chat-1",
|
|
@@ -95,7 +124,7 @@ describe("useAISDKRuntime tool approvals", () => {
|
|
|
95
124
|
error: undefined,
|
|
96
125
|
messages,
|
|
97
126
|
setMessages,
|
|
98
|
-
sendMessage
|
|
127
|
+
sendMessage,
|
|
99
128
|
regenerate: vi.fn(),
|
|
100
129
|
addToolOutput: vi.fn(),
|
|
101
130
|
addToolApprovalResponse,
|
|
@@ -116,17 +145,29 @@ describe("useAISDKRuntime tool approvals", () => {
|
|
|
116
145
|
}) => mocks.adapter?.onRespondToToolApproval?.(response),
|
|
117
146
|
setMessages,
|
|
118
147
|
addToolApprovalResponse,
|
|
148
|
+
sendMessage,
|
|
149
|
+
messages,
|
|
119
150
|
getApproval: () =>
|
|
120
151
|
mocks.adapter?.messages?.[0]?.content.find(
|
|
121
152
|
(part) => part.type === "tool-call",
|
|
122
153
|
)?.approval,
|
|
154
|
+
getToolCall: () =>
|
|
155
|
+
mocks.adapter?.messages?.[0]?.content.find(
|
|
156
|
+
(part) => part.type === "tool-call",
|
|
157
|
+
),
|
|
123
158
|
};
|
|
124
159
|
};
|
|
125
160
|
|
|
126
|
-
it("
|
|
161
|
+
it("stores a host answer after the handler resolves without starting a run", async () => {
|
|
127
162
|
const onRespondToToolApproval = vi.fn(async () => {});
|
|
128
|
-
const {
|
|
129
|
-
|
|
163
|
+
const {
|
|
164
|
+
respond,
|
|
165
|
+
setMessages,
|
|
166
|
+
addToolApprovalResponse,
|
|
167
|
+
sendMessage,
|
|
168
|
+
messages,
|
|
169
|
+
getApproval,
|
|
170
|
+
} = setupPendingApproval(onRespondToToolApproval);
|
|
130
171
|
|
|
131
172
|
const response = {
|
|
132
173
|
approvalId: "approval-1",
|
|
@@ -146,6 +187,13 @@ describe("useAISDKRuntime tool approvals", () => {
|
|
|
146
187
|
});
|
|
147
188
|
expect(addToolApprovalResponse).not.toHaveBeenCalled();
|
|
148
189
|
expect(setMessages).not.toHaveBeenCalled();
|
|
190
|
+
expect(sendMessage).not.toHaveBeenCalled();
|
|
191
|
+
expect(mocks.persistToolApprovalResponses).toHaveBeenCalledExactlyOnceWith(
|
|
192
|
+
"message-1",
|
|
193
|
+
);
|
|
194
|
+
expect(messages[0]).not.toHaveProperty(
|
|
195
|
+
"metadata.__aui_toolApprovalResponses",
|
|
196
|
+
);
|
|
149
197
|
expect(getApproval()).toEqual({
|
|
150
198
|
id: "approval-1",
|
|
151
199
|
approved: true,
|
|
@@ -155,7 +203,7 @@ describe("useAISDKRuntime tool approvals", () => {
|
|
|
155
203
|
});
|
|
156
204
|
});
|
|
157
205
|
|
|
158
|
-
it("
|
|
206
|
+
it("does not store a request the handler hands back through the AI SDK", async () => {
|
|
159
207
|
const { respond, addToolApprovalResponse } = setupPendingApproval(
|
|
160
208
|
(_response, { respondViaAISDK }) => respondViaAISDK(),
|
|
161
209
|
);
|
|
@@ -175,6 +223,27 @@ describe("useAISDKRuntime tool approvals", () => {
|
|
|
175
223
|
reason: "Not now",
|
|
176
224
|
options: { metadata: undefined },
|
|
177
225
|
});
|
|
226
|
+
expect(mocks.persistToolApprovalResponses).not.toHaveBeenCalled();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("does not store a host answer when the handler rejects", async () => {
|
|
230
|
+
const { respond, messages, getApproval } = setupPendingApproval(
|
|
231
|
+
async () => {
|
|
232
|
+
throw new Error("resume failed");
|
|
233
|
+
},
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
await expect(
|
|
237
|
+
act(async () => {
|
|
238
|
+
await respond({ approvalId: "approval-1", approved: true });
|
|
239
|
+
}),
|
|
240
|
+
).rejects.toThrow("resume failed");
|
|
241
|
+
|
|
242
|
+
expect(mocks.persistToolApprovalResponses).not.toHaveBeenCalled();
|
|
243
|
+
expect(getApproval()).toEqual({ id: "approval-1" });
|
|
244
|
+
expect(messages[0]).not.toHaveProperty(
|
|
245
|
+
"metadata.__aui_toolApprovalResponses",
|
|
246
|
+
);
|
|
178
247
|
});
|
|
179
248
|
|
|
180
249
|
it("reopens a request when a handed-back AI SDK response fails inside the handler", async () => {
|
|
@@ -254,6 +323,153 @@ describe("useAISDKRuntime tool approvals", () => {
|
|
|
254
323
|
expect(onRespondToToolApproval).toHaveBeenCalledTimes(2);
|
|
255
324
|
});
|
|
256
325
|
|
|
326
|
+
it("restores a host answer and refuses a second response", async () => {
|
|
327
|
+
const onRespondToToolApproval = vi.fn(async () => {});
|
|
328
|
+
const { respond, getApproval, messages } = setupPendingApproval(
|
|
329
|
+
onRespondToToolApproval,
|
|
330
|
+
);
|
|
331
|
+
const historyCall = vi.mocked(useExternalHistory).mock.calls.at(-1)!;
|
|
332
|
+
const toolApprovalResponses = historyCall[9] as Map<
|
|
333
|
+
string,
|
|
334
|
+
{ approvalId: string; approved: boolean; reason?: string }
|
|
335
|
+
>;
|
|
336
|
+
const onToolApprovalResponsesRestored = historyCall[10] as () => void;
|
|
337
|
+
|
|
338
|
+
await act(async () => {
|
|
339
|
+
toolApprovalResponses.set("approval-1", {
|
|
340
|
+
approvalId: "approval-1",
|
|
341
|
+
approved: true,
|
|
342
|
+
reason: "Approved by operator",
|
|
343
|
+
});
|
|
344
|
+
onToolApprovalResponsesRestored();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
await waitFor(() =>
|
|
348
|
+
expect(getApproval()).toEqual({
|
|
349
|
+
id: "approval-1",
|
|
350
|
+
approved: true,
|
|
351
|
+
reason: "Approved by operator",
|
|
352
|
+
}),
|
|
353
|
+
);
|
|
354
|
+
await expect(
|
|
355
|
+
respond({ approvalId: "approval-1", approved: true }),
|
|
356
|
+
).rejects.toThrow(
|
|
357
|
+
"Tool approval approval-1 is not waiting for a response.",
|
|
358
|
+
);
|
|
359
|
+
expect(onRespondToToolApproval).not.toHaveBeenCalled();
|
|
360
|
+
expect(messages[0]).not.toHaveProperty(
|
|
361
|
+
"metadata.__aui_toolApprovalResponses",
|
|
362
|
+
);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("reopens a restored approval when history clears its response map", async () => {
|
|
366
|
+
const onRespondToToolApproval = vi.fn(async () => {});
|
|
367
|
+
const { respond, getApproval } = setupPendingApproval(
|
|
368
|
+
onRespondToToolApproval,
|
|
369
|
+
);
|
|
370
|
+
const historyCall = vi.mocked(useExternalHistory).mock.calls.at(-1)!;
|
|
371
|
+
const toolApprovalResponses = historyCall[9] as Map<
|
|
372
|
+
string,
|
|
373
|
+
{ approvalId: string; approved: boolean }
|
|
374
|
+
>;
|
|
375
|
+
const onToolApprovalResponsesRestored = historyCall[10] as () => void;
|
|
376
|
+
|
|
377
|
+
await act(async () => {
|
|
378
|
+
toolApprovalResponses.set("approval-1", {
|
|
379
|
+
approvalId: "approval-1",
|
|
380
|
+
approved: true,
|
|
381
|
+
});
|
|
382
|
+
onToolApprovalResponsesRestored();
|
|
383
|
+
});
|
|
384
|
+
await waitFor(() =>
|
|
385
|
+
expect(getApproval()).toEqual({ id: "approval-1", approved: true }),
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
await act(async () => {
|
|
389
|
+
toolApprovalResponses.clear();
|
|
390
|
+
onToolApprovalResponsesRestored();
|
|
391
|
+
});
|
|
392
|
+
await waitFor(() => expect(getApproval()).toEqual({ id: "approval-1" }));
|
|
393
|
+
|
|
394
|
+
await act(async () => {
|
|
395
|
+
await respond({ approvalId: "approval-1", approved: true });
|
|
396
|
+
});
|
|
397
|
+
expect(onRespondToToolApproval).toHaveBeenCalledOnce();
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("clears deleted tool sidecars and its host approval reservation", async () => {
|
|
401
|
+
const onRespondToToolApproval = vi.fn(async () => {});
|
|
402
|
+
const { getApproval, getToolCall, messages, respond } =
|
|
403
|
+
setupPendingApproval(onRespondToToolApproval);
|
|
404
|
+
const threadMessage: ThreadAssistantMessage = {
|
|
405
|
+
id: "message-1",
|
|
406
|
+
role: "assistant",
|
|
407
|
+
content: [
|
|
408
|
+
{
|
|
409
|
+
type: "tool-call",
|
|
410
|
+
toolCallId: "tool-1",
|
|
411
|
+
toolName: "deploy",
|
|
412
|
+
args: {},
|
|
413
|
+
argsText: "{}",
|
|
414
|
+
result: undefined,
|
|
415
|
+
isError: false,
|
|
416
|
+
approval: { id: "approval-1" },
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
createdAt: new Date(),
|
|
420
|
+
status: { type: "requires-action", reason: "tool-calls" },
|
|
421
|
+
metadata: {
|
|
422
|
+
unstable_state: null,
|
|
423
|
+
unstable_annotations: [],
|
|
424
|
+
unstable_data: [],
|
|
425
|
+
steps: [],
|
|
426
|
+
custom: {},
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
bindExternalStoreMessage(threadMessage, messages[0]!);
|
|
430
|
+
mocks.threadMessages = [threadMessage];
|
|
431
|
+
|
|
432
|
+
await act(async () => {
|
|
433
|
+
await respond({ approvalId: "approval-1", approved: true });
|
|
434
|
+
await mocks.adapter?.onAddToolResult?.({
|
|
435
|
+
messageId: "message-1",
|
|
436
|
+
toolCallId: "tool-1",
|
|
437
|
+
toolName: "deploy",
|
|
438
|
+
result: "deployed",
|
|
439
|
+
artifact: { preview: "deployment complete" },
|
|
440
|
+
isError: false,
|
|
441
|
+
});
|
|
442
|
+
await mocks.adapter?.unstable_onRecordToolInteraction?.({
|
|
443
|
+
messageId: "message-1",
|
|
444
|
+
toolCallId: "tool-1",
|
|
445
|
+
interaction: {
|
|
446
|
+
type: "action",
|
|
447
|
+
occurredAt: 1,
|
|
448
|
+
payload: { copied: true },
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
expect(getToolCall()).toMatchObject({
|
|
453
|
+
artifact: { preview: "deployment complete" },
|
|
454
|
+
unstable_interactions: {
|
|
455
|
+
entries: [{ type: "action", occurredAt: 1, payload: { copied: true } }],
|
|
456
|
+
},
|
|
457
|
+
approval: { id: "approval-1", approved: true },
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
await act(async () => {
|
|
461
|
+
await mocks.adapter?.onDelete?.("message-1");
|
|
462
|
+
});
|
|
463
|
+
expect(getToolCall()).not.toHaveProperty("artifact");
|
|
464
|
+
expect(getToolCall()).not.toHaveProperty("unstable_interactions");
|
|
465
|
+
expect(getApproval()).toEqual({ id: "approval-1" });
|
|
466
|
+
|
|
467
|
+
await act(async () => {
|
|
468
|
+
await respond({ approvalId: "approval-1", approved: true });
|
|
469
|
+
});
|
|
470
|
+
expect(onRespondToToolApproval).toHaveBeenCalledTimes(2);
|
|
471
|
+
});
|
|
472
|
+
|
|
257
473
|
it("rejects an approval that is not waiting for a response", async () => {
|
|
258
474
|
const onRespondToToolApproval = vi.fn();
|
|
259
475
|
const { respond } = setupPendingApproval(onRespondToToolApproval);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
4
4
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import { validateUIMessages } from "ai";
|
|
6
|
+
import { ToolResponse } from "assistant-stream";
|
|
6
7
|
import type { UIMessage } from "@ai-sdk/react";
|
|
7
8
|
import type { MessageFormatRepository } from "@assistant-ui/core";
|
|
8
9
|
|
|
@@ -17,6 +18,8 @@ vi.mock("./useExternalHistory", async (importOriginal) => {
|
|
|
17
18
|
useExternalHistory: vi.fn(() => ({
|
|
18
19
|
isLoading: false,
|
|
19
20
|
deleteMessage: vi.fn().mockResolvedValue(undefined),
|
|
21
|
+
persistToolInteractions: vi.fn().mockResolvedValue(undefined),
|
|
22
|
+
persistToolApprovalResponses: vi.fn().mockResolvedValue(undefined),
|
|
20
23
|
})),
|
|
21
24
|
};
|
|
22
25
|
});
|
|
@@ -82,6 +85,8 @@ describe("useAISDKRuntime", () => {
|
|
|
82
85
|
vi.mocked(useExternalHistory).mockReturnValue({
|
|
83
86
|
isLoading: false,
|
|
84
87
|
deleteMessage: vi.fn().mockResolvedValue(undefined),
|
|
88
|
+
persistToolInteractions: vi.fn().mockResolvedValue(undefined),
|
|
89
|
+
persistToolApprovalResponses: vi.fn().mockResolvedValue(undefined),
|
|
85
90
|
});
|
|
86
91
|
});
|
|
87
92
|
|
|
@@ -121,13 +126,13 @@ describe("useAISDKRuntime", () => {
|
|
|
121
126
|
result.current.thread.append({
|
|
122
127
|
role: "user",
|
|
123
128
|
content: [{ type: "text", text: "hello" }],
|
|
124
|
-
runConfig: { custom: { model: "gpt-
|
|
129
|
+
runConfig: { custom: { model: "gpt-6-luna" } },
|
|
125
130
|
});
|
|
126
131
|
});
|
|
127
132
|
|
|
128
133
|
await waitFor(() => {
|
|
129
134
|
expect(chat.sendMessage).toHaveBeenCalledWith(expect.anything(), {
|
|
130
|
-
metadata: { custom: { model: "gpt-
|
|
135
|
+
metadata: { custom: { model: "gpt-6-luna" } },
|
|
131
136
|
});
|
|
132
137
|
});
|
|
133
138
|
});
|
|
@@ -571,7 +576,65 @@ describe("useAISDKRuntime", () => {
|
|
|
571
576
|
).resolves.toBeDefined();
|
|
572
577
|
});
|
|
573
578
|
|
|
574
|
-
it("
|
|
579
|
+
it("cancels a pending tool call left behind a staged message", async () => {
|
|
580
|
+
const chat = createChatHelpers([
|
|
581
|
+
{
|
|
582
|
+
id: "a1",
|
|
583
|
+
role: "assistant",
|
|
584
|
+
parts: [
|
|
585
|
+
{
|
|
586
|
+
type: "dynamic-tool",
|
|
587
|
+
toolName: "mcp_search",
|
|
588
|
+
toolCallId: "tc-1",
|
|
589
|
+
state: "approval-requested",
|
|
590
|
+
input: { q: "hi" },
|
|
591
|
+
approval: { id: "appr-1" },
|
|
592
|
+
},
|
|
593
|
+
],
|
|
594
|
+
},
|
|
595
|
+
]);
|
|
596
|
+
|
|
597
|
+
const { result } = renderHook(() => useAISDKRuntime(chat));
|
|
598
|
+
|
|
599
|
+
await waitFor(() => {
|
|
600
|
+
expect(result.current.thread.getState().messages.length).toBeGreaterThan(
|
|
601
|
+
0,
|
|
602
|
+
);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
act(() => {
|
|
606
|
+
result.current.thread.append({
|
|
607
|
+
role: "user",
|
|
608
|
+
content: [{ type: "text", text: "context" }],
|
|
609
|
+
startRun: false,
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
await waitFor(() => {
|
|
614
|
+
expect(chat.messages).toHaveLength(2);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
act(() => {
|
|
618
|
+
result.current.thread.append({
|
|
619
|
+
role: "user",
|
|
620
|
+
content: [{ type: "text", text: "what" }],
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
await waitFor(() => {
|
|
625
|
+
expect(chat.sendMessage).toHaveBeenCalledTimes(1);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
const part = chat.messages[0].parts[0];
|
|
629
|
+
expect(part.state).toBe("output-error");
|
|
630
|
+
expect(part.approval).toBeUndefined();
|
|
631
|
+
|
|
632
|
+
await expect(
|
|
633
|
+
validateUIMessages({ messages: chat.messages }),
|
|
634
|
+
).resolves.toBeDefined();
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it("attaches a tool artifact to the live part and forwards its result through addToolOutput", async () => {
|
|
575
638
|
const chat = createChatHelpers([
|
|
576
639
|
{
|
|
577
640
|
id: "a1",
|
|
@@ -587,7 +650,7 @@ describe("useAISDKRuntime", () => {
|
|
|
587
650
|
},
|
|
588
651
|
]);
|
|
589
652
|
|
|
590
|
-
const { result } = renderHook(() => useAISDKRuntime(chat));
|
|
653
|
+
const { result, rerender } = renderHook(() => useAISDKRuntime(chat));
|
|
591
654
|
|
|
592
655
|
await waitFor(() => {
|
|
593
656
|
expect(result.current.thread.getState().messages.length).toBeGreaterThan(
|
|
@@ -599,13 +662,26 @@ describe("useAISDKRuntime", () => {
|
|
|
599
662
|
result.current.thread
|
|
600
663
|
.getMessageById("a1")
|
|
601
664
|
.getMessagePartByToolCallId("tc-1")
|
|
602
|
-
.addToolResult(
|
|
665
|
+
.addToolResult(
|
|
666
|
+
new ToolResponse({
|
|
667
|
+
result: { temp: 72 },
|
|
668
|
+
artifact: { preview: "72°F and sunny" },
|
|
669
|
+
}),
|
|
670
|
+
);
|
|
603
671
|
});
|
|
604
672
|
|
|
605
673
|
await waitFor(() => {
|
|
606
674
|
expect(chat.addToolOutput).toHaveBeenCalledTimes(1);
|
|
607
675
|
});
|
|
608
676
|
|
|
677
|
+
const livePart = result.current.thread
|
|
678
|
+
.getMessageById("a1")
|
|
679
|
+
.getMessagePartByToolCallId("tc-1")
|
|
680
|
+
.getState();
|
|
681
|
+
expect(
|
|
682
|
+
livePart.type === "tool-call" ? livePart.artifact : undefined,
|
|
683
|
+
).toEqual({ preview: "72°F and sunny" });
|
|
684
|
+
|
|
609
685
|
expect(chat.addToolOutput).toHaveBeenCalledWith(
|
|
610
686
|
expect.objectContaining({
|
|
611
687
|
tool: "weather",
|
|
@@ -614,7 +690,90 @@ describe("useAISDKRuntime", () => {
|
|
|
614
690
|
options: { metadata: undefined },
|
|
615
691
|
}),
|
|
616
692
|
);
|
|
693
|
+
expect(chat.addToolOutput.mock.calls[0]?.[0]).not.toHaveProperty(
|
|
694
|
+
"artifact",
|
|
695
|
+
);
|
|
617
696
|
expect(chat.addToolResult).not.toHaveBeenCalled();
|
|
697
|
+
expect(
|
|
698
|
+
vi.mocked(useExternalHistory).mock.calls.at(-1)?.[5]?.get("tc-1"),
|
|
699
|
+
).toEqual({ preview: "72°F and sunny" });
|
|
700
|
+
|
|
701
|
+
chat.messages = chat.messages.map((message: UIMessage) => ({
|
|
702
|
+
...message,
|
|
703
|
+
}));
|
|
704
|
+
rerender();
|
|
705
|
+
|
|
706
|
+
await waitFor(() => {
|
|
707
|
+
const part = result.current.thread
|
|
708
|
+
.getMessageById("a1")
|
|
709
|
+
.getMessagePartByToolCallId("tc-1")
|
|
710
|
+
.getState();
|
|
711
|
+
expect(part.type === "tool-call" ? part.artifact : undefined).toEqual({
|
|
712
|
+
preview: "72°F and sunny",
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
expect(chat.messages[0]?.metadata).toBeUndefined();
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it("shows recorded tool interactions without writing them to chat messages", async () => {
|
|
719
|
+
const chat = createChatHelpers([
|
|
720
|
+
{
|
|
721
|
+
id: "a1",
|
|
722
|
+
role: "assistant",
|
|
723
|
+
parts: [
|
|
724
|
+
{
|
|
725
|
+
type: "tool-weather",
|
|
726
|
+
toolCallId: "tc-1",
|
|
727
|
+
state: "input-available",
|
|
728
|
+
input: { city: "NYC" },
|
|
729
|
+
},
|
|
730
|
+
],
|
|
731
|
+
},
|
|
732
|
+
]);
|
|
733
|
+
const persistToolInteractions = vi.fn().mockResolvedValue(undefined);
|
|
734
|
+
vi.mocked(useExternalHistory).mockReturnValue({
|
|
735
|
+
isLoading: false,
|
|
736
|
+
deleteMessage: vi.fn().mockResolvedValue(undefined),
|
|
737
|
+
persistToolInteractions,
|
|
738
|
+
persistToolApprovalResponses: vi.fn().mockResolvedValue(undefined),
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
const { result } = renderHook(() => useAISDKRuntime(chat));
|
|
742
|
+
|
|
743
|
+
await waitFor(() => {
|
|
744
|
+
expect(result.current.thread.getState().messages).toHaveLength(1);
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
await act(async () => {
|
|
748
|
+
await result.current.thread
|
|
749
|
+
.getMessageById("a1")
|
|
750
|
+
.getMessagePartByToolCallId("tc-1").unstable_recordInteraction!({
|
|
751
|
+
type: "action",
|
|
752
|
+
payload: { refresh: true },
|
|
753
|
+
});
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
await waitFor(() => {
|
|
757
|
+
const part = result.current.thread
|
|
758
|
+
.getMessageById("a1")
|
|
759
|
+
.getMessagePartByToolCallId("tc-1")
|
|
760
|
+
.getState();
|
|
761
|
+
expect(
|
|
762
|
+
part.type === "tool-call" ? part.unstable_interactions : undefined,
|
|
763
|
+
).toEqual({
|
|
764
|
+
entries: [
|
|
765
|
+
{
|
|
766
|
+
type: "action",
|
|
767
|
+
occurredAt: expect.any(Number),
|
|
768
|
+
payload: { refresh: true },
|
|
769
|
+
},
|
|
770
|
+
],
|
|
771
|
+
});
|
|
772
|
+
});
|
|
773
|
+
expect(persistToolInteractions).toHaveBeenCalledExactlyOnceWith("a1");
|
|
774
|
+
expect(chat.messages[0]?.metadata).toBeUndefined();
|
|
775
|
+
expect(chat.addToolOutput).not.toHaveBeenCalled();
|
|
776
|
+
expect(chat.sendMessage).not.toHaveBeenCalled();
|
|
618
777
|
});
|
|
619
778
|
|
|
620
779
|
it("appends a new user message without sending when startRun is false", async () => {
|
|
@@ -704,6 +863,8 @@ describe("useAISDKRuntime", () => {
|
|
|
704
863
|
vi.mocked(useExternalHistory).mockReturnValue({
|
|
705
864
|
isLoading: false,
|
|
706
865
|
deleteMessage,
|
|
866
|
+
persistToolInteractions: vi.fn().mockResolvedValue(undefined),
|
|
867
|
+
persistToolApprovalResponses: vi.fn().mockResolvedValue(undefined),
|
|
707
868
|
});
|
|
708
869
|
const chat = createChatHelpers([
|
|
709
870
|
{ id: "u1", role: "user", parts: [{ type: "text", text: "first" }] },
|
|
@@ -738,6 +899,115 @@ describe("useAISDKRuntime", () => {
|
|
|
738
899
|
]);
|
|
739
900
|
});
|
|
740
901
|
|
|
902
|
+
it("removes tool artifacts with their deleted message", async () => {
|
|
903
|
+
const deleteMessage = vi.fn().mockResolvedValue(undefined);
|
|
904
|
+
vi.mocked(useExternalHistory).mockReturnValue({
|
|
905
|
+
isLoading: false,
|
|
906
|
+
deleteMessage,
|
|
907
|
+
persistToolInteractions: vi.fn().mockResolvedValue(undefined),
|
|
908
|
+
persistToolApprovalResponses: vi.fn().mockResolvedValue(undefined),
|
|
909
|
+
});
|
|
910
|
+
const chat = createChatHelpers([
|
|
911
|
+
{
|
|
912
|
+
id: "a1",
|
|
913
|
+
role: "assistant",
|
|
914
|
+
parts: [
|
|
915
|
+
{
|
|
916
|
+
type: "tool-weather",
|
|
917
|
+
toolCallId: "tc-1",
|
|
918
|
+
state: "input-available",
|
|
919
|
+
input: { city: "NYC" },
|
|
920
|
+
},
|
|
921
|
+
],
|
|
922
|
+
},
|
|
923
|
+
]);
|
|
924
|
+
|
|
925
|
+
const { result } = renderHook(() => useAISDKRuntime(chat));
|
|
926
|
+
|
|
927
|
+
await waitFor(() => {
|
|
928
|
+
expect(result.current.thread.getState().messages).toHaveLength(1);
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
act(() => {
|
|
932
|
+
result.current.thread
|
|
933
|
+
.getMessageById("a1")
|
|
934
|
+
.getMessagePartByToolCallId("tc-1")
|
|
935
|
+
.addToolResult(
|
|
936
|
+
new ToolResponse({
|
|
937
|
+
result: { temp: 72 },
|
|
938
|
+
artifact: { preview: "72°F and sunny" },
|
|
939
|
+
}),
|
|
940
|
+
);
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
await waitFor(() => {
|
|
944
|
+
expect(chat.addToolOutput).toHaveBeenCalledTimes(1);
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
const toolArtifacts = vi.mocked(useExternalHistory).mock.calls.at(-1)?.[5];
|
|
948
|
+
expect(toolArtifacts?.get("tc-1")).toEqual({
|
|
949
|
+
preview: "72°F and sunny",
|
|
950
|
+
});
|
|
951
|
+
await act(async () => {
|
|
952
|
+
await result.current.thread.getMessageById("a1").delete();
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
expect(deleteMessage).toHaveBeenCalledWith("a1");
|
|
956
|
+
expect(toolArtifacts?.has("tc-1")).toBe(false);
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
it("removes tool interactions with their deleted message", async () => {
|
|
960
|
+
const deleteMessage = vi.fn().mockResolvedValue(undefined);
|
|
961
|
+
vi.mocked(useExternalHistory).mockReturnValue({
|
|
962
|
+
isLoading: false,
|
|
963
|
+
deleteMessage,
|
|
964
|
+
persistToolInteractions: vi.fn().mockResolvedValue(undefined),
|
|
965
|
+
persistToolApprovalResponses: vi.fn().mockResolvedValue(undefined),
|
|
966
|
+
});
|
|
967
|
+
const chat = createChatHelpers([
|
|
968
|
+
{
|
|
969
|
+
id: "a1",
|
|
970
|
+
role: "assistant",
|
|
971
|
+
parts: [
|
|
972
|
+
{
|
|
973
|
+
type: "tool-weather",
|
|
974
|
+
toolCallId: "tc-1",
|
|
975
|
+
state: "output-available",
|
|
976
|
+
input: { city: "NYC" },
|
|
977
|
+
output: { temp: 72 },
|
|
978
|
+
},
|
|
979
|
+
],
|
|
980
|
+
},
|
|
981
|
+
]);
|
|
982
|
+
|
|
983
|
+
const { result } = renderHook(() => useAISDKRuntime(chat));
|
|
984
|
+
|
|
985
|
+
await waitFor(() => {
|
|
986
|
+
expect(result.current.thread.getState().messages).toHaveLength(1);
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
await act(async () => {
|
|
990
|
+
await result.current.thread
|
|
991
|
+
.getMessageById("a1")
|
|
992
|
+
.getMessagePartByToolCallId("tc-1").unstable_recordInteraction!({
|
|
993
|
+
type: "action",
|
|
994
|
+
payload: { type: "refresh" },
|
|
995
|
+
});
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
const toolInteractions = vi
|
|
999
|
+
.mocked(useExternalHistory)
|
|
1000
|
+
.mock.calls.at(-1)?.[7];
|
|
1001
|
+
expect(toolInteractions?.has("tc-1")).toBe(true);
|
|
1002
|
+
|
|
1003
|
+
await act(async () => {
|
|
1004
|
+
await result.current.thread.getMessageById("a1").delete();
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
expect(deleteMessage).toHaveBeenCalledWith("a1");
|
|
1008
|
+
expect(toolInteractions?.has("tc-1")).toBe(false);
|
|
1009
|
+
});
|
|
1010
|
+
|
|
741
1011
|
it("edit slices history to parentId and sends the edited message", async () => {
|
|
742
1012
|
const chat = createChatHelpers([
|
|
743
1013
|
{ id: "u1", role: "user", parts: [{ type: "text", text: "first" }] },
|