@assistant-ui/ai-sdk 0.0.11 → 0.0.12
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/dist/converters/convertMessage.d.ts +8 -6
- package/dist/converters/convertMessage.d.ts.map +1 -1
- package/dist/converters/convertMessage.js +48 -22
- package/dist/converters/convertMessage.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.native.d.ts +1 -1
- package/dist/index.native.d.ts.map +1 -1
- package/dist/index.native.js +1 -1
- package/dist/model-context/injectInteractableContext.d.ts +3 -0
- package/dist/model-context/injectInteractableContext.d.ts.map +1 -1
- package/dist/model-context/injectInteractableContext.js +3 -0
- package/dist/model-context/injectInteractableContext.js.map +1 -1
- package/dist/model-context/injectQuoteContext.d.ts +1 -0
- package/dist/model-context/injectQuoteContext.d.ts.map +1 -1
- package/dist/model-context/injectQuoteContext.js +1 -0
- package/dist/model-context/injectQuoteContext.js.map +1 -1
- package/dist/runtime/AISDKChat.js +1 -1
- package/dist/runtime/AISDKThreads.d.ts +5 -0
- package/dist/runtime/AISDKThreads.d.ts.map +1 -1
- package/dist/runtime/AISDKThreads.js +37 -4
- package/dist/runtime/AISDKThreads.js.map +1 -1
- package/dist/runtime/DynamicChatTransport.d.ts +49 -0
- package/dist/runtime/DynamicChatTransport.d.ts.map +1 -0
- package/dist/runtime/DynamicChatTransport.js +147 -0
- package/dist/runtime/DynamicChatTransport.js.map +1 -0
- package/dist/runtime/getResumableAdapter.d.ts +5 -0
- package/dist/runtime/getResumableAdapter.d.ts.map +1 -0
- package/dist/runtime/getResumableAdapter.js +12 -0
- package/dist/runtime/getResumableAdapter.js.map +1 -0
- package/dist/runtime/sdkIdentity.js +1 -1
- package/dist/runtime/toolHistoryCodec.d.ts +20 -0
- package/dist/runtime/toolHistoryCodec.d.ts.map +1 -0
- package/dist/runtime/toolHistoryCodec.js +107 -0
- package/dist/runtime/toolHistoryCodec.js.map +1 -0
- package/dist/runtime/useAISDKRuntime.d.ts +4 -1
- package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
- package/dist/runtime/useAISDKRuntime.js +154 -155
- package/dist/runtime/useAISDKRuntime.js.map +1 -1
- package/dist/runtime/useChatRuntime.d.ts +14 -2
- package/dist/runtime/useChatRuntime.d.ts.map +1 -1
- package/dist/runtime/useChatRuntime.js +12 -3
- package/dist/runtime/useChatRuntime.js.map +1 -1
- package/dist/runtime/useChatThread.d.ts +3 -2
- package/dist/runtime/useChatThread.d.ts.map +1 -1
- package/dist/runtime/useChatThread.js +56 -36
- package/dist/runtime/useChatThread.js.map +1 -1
- package/dist/runtime/useDynamicChatTransport.d.ts +4 -0
- package/dist/runtime/useDynamicChatTransport.d.ts.map +1 -0
- package/dist/runtime/useDynamicChatTransport.js +64 -0
- package/dist/runtime/useDynamicChatTransport.js.map +1 -0
- package/dist/runtime/useExternalHistory.d.ts.map +1 -1
- package/dist/runtime/useExternalHistory.js +11 -109
- package/dist/runtime/useExternalHistory.js.map +1 -1
- package/dist/runtime/useResourceCleanup.js +1 -1
- package/dist/runtime/useStreamingTiming.js +2 -2
- package/dist/runtime/useStreamingTiming.js.map +1 -1
- package/dist/tools/generativeTools.d.ts +2 -1
- package/dist/tools/generativeTools.d.ts.map +1 -1
- package/dist/tools/generativeTools.js +5 -5
- package/dist/tools/generativeTools.js.map +1 -1
- package/dist/usage.js +1 -1
- package/dist/utils/sliceMessagesUntil.d.ts.map +1 -1
- package/dist/utils/sliceMessagesUntil.js +1 -2
- package/dist/utils/sliceMessagesUntil.js.map +1 -1
- package/package.json +11 -10
- package/src/converters/convertMessage.test.ts +120 -2
- package/src/converters/convertMessage.tool-args-status.test.tsx +185 -0
- package/src/converters/convertMessage.ts +60 -19
- package/src/converters/toCreateMessage.test.ts +27 -0
- package/src/index.native.ts +1 -1
- package/src/index.ts +1 -1
- package/src/model-context/injectInteractableContext.ts +3 -0
- package/src/model-context/injectQuoteContext.ts +1 -0
- package/src/runtime/AISDKChat.integration.test.tsx +57 -2
- package/src/runtime/AISDKThreads.cloud.test.ts +3 -0
- package/src/runtime/AISDKThreads.test.ts +181 -0
- package/src/runtime/AISDKThreads.ts +31 -4
- package/src/runtime/DynamicChatTransport.test.ts +203 -0
- package/src/runtime/DynamicChatTransport.ts +273 -0
- package/src/runtime/__tests__/controlled-transport.ts +3 -0
- package/src/runtime/getResumableAdapter.ts +16 -0
- package/src/runtime/toolHistoryCodec.test.ts +161 -0
- package/src/runtime/toolHistoryCodec.ts +207 -0
- package/src/runtime/useAISDKRuntime.approval.test.tsx +12 -0
- package/src/runtime/useAISDKRuntime.reload.test.tsx +219 -0
- package/src/runtime/useAISDKRuntime.test.ts +538 -5
- package/src/runtime/useAISDKRuntime.ts +161 -51
- package/src/runtime/useChatRuntime.integration.test.tsx +319 -4
- package/src/runtime/useChatRuntime.local-storage.test.tsx +123 -0
- package/src/runtime/useChatRuntime.test.ts +107 -1
- package/src/runtime/useChatRuntime.ts +26 -4
- package/src/runtime/useChatThread.binding.test.tsx +143 -0
- package/src/runtime/useChatThread.ts +98 -81
- package/src/runtime/useDynamicChatTransport.ts +26 -0
- package/src/runtime/useExternalHistory.test.ts +111 -0
- package/src/runtime/useExternalHistory.ts +14 -210
- package/src/runtime/useStreamingTiming.ts +2 -2
- package/src/tools/generativeTools.test.ts +1 -1
- package/src/tools/generativeTools.ts +5 -6
- package/src/utils/sliceMessagesUntil.test.ts +2 -6
- package/src/utils/sliceMessagesUntil.ts +1 -5
- package/dist/converters/modelContentEnvelope.d.ts +0 -14
- package/dist/converters/modelContentEnvelope.d.ts.map +0 -1
- package/dist/converters/modelContentEnvelope.js +0 -22
- package/dist/converters/modelContentEnvelope.js.map +0 -1
- package/dist/converters/toolOutputConversion.d.ts +0 -26
- package/dist/converters/toolOutputConversion.d.ts.map +0 -1
- package/dist/converters/toolOutputConversion.js +0 -31
- package/dist/converters/toolOutputConversion.js.map +0 -1
- package/dist/tools/frontendTools.d.ts +0 -30
- package/dist/tools/frontendTools.d.ts.map +0 -1
- package/dist/tools/frontendTools.js +0 -33
- package/dist/tools/frontendTools.js.map +0 -1
- package/src/converters/modelContentEnvelope.ts +0 -41
- package/src/converters/toolOutputConversion.ts +0 -26
- package/src/tools/frontendTools.test.ts +0 -205
- package/src/tools/frontendTools.ts +0 -83
|
@@ -5,7 +5,10 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
5
5
|
import { validateUIMessages } from "ai";
|
|
6
6
|
import { ToolResponse } from "assistant-stream";
|
|
7
7
|
import type { UIMessage } from "@ai-sdk/react";
|
|
8
|
-
import
|
|
8
|
+
import {
|
|
9
|
+
createSuggestionAdapter,
|
|
10
|
+
type MessageFormatRepository,
|
|
11
|
+
} from "@assistant-ui/core";
|
|
9
12
|
|
|
10
13
|
// Mock only the sibling module that requires AUI store context (not available
|
|
11
14
|
// in isolation). Every other dependency — useExternalStoreRuntime,
|
|
@@ -381,6 +384,31 @@ describe("useAISDKRuntime", () => {
|
|
|
381
384
|
});
|
|
382
385
|
});
|
|
383
386
|
|
|
387
|
+
it("keeps rendering through a run when the last assistant message has malformed parts", () => {
|
|
388
|
+
const chat = createChatHelpers([
|
|
389
|
+
{ id: "user-1", role: "user", parts: [{ type: "text", text: "hi" }] },
|
|
390
|
+
{
|
|
391
|
+
id: "assistant-1",
|
|
392
|
+
role: "assistant",
|
|
393
|
+
parts: [
|
|
394
|
+
null,
|
|
395
|
+
{ text: "no type" },
|
|
396
|
+
{ type: "text" },
|
|
397
|
+
{ type: "text", text: "yo" },
|
|
398
|
+
],
|
|
399
|
+
},
|
|
400
|
+
]);
|
|
401
|
+
chat.status = "submitted";
|
|
402
|
+
|
|
403
|
+
const { result, rerender } = renderHook(() => useAISDKRuntime(chat));
|
|
404
|
+
act(() => {
|
|
405
|
+
chat.status = "ready";
|
|
406
|
+
rerender();
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
expect(textOf(result.current.thread.getState().messages.at(-1))).toBe("yo");
|
|
410
|
+
});
|
|
411
|
+
|
|
384
412
|
it("marks output cancelled while a client tool is still executing", async () => {
|
|
385
413
|
let resolveTool!: (value: string) => void;
|
|
386
414
|
const execute = vi.fn(
|
|
@@ -528,6 +556,86 @@ describe("useAISDKRuntime", () => {
|
|
|
528
556
|
expect(chat.messages[0].parts[1].state).toBe("output-available");
|
|
529
557
|
});
|
|
530
558
|
|
|
559
|
+
it("sends past an assistant message with malformed parts", async () => {
|
|
560
|
+
const chat = createChatHelpers([
|
|
561
|
+
{
|
|
562
|
+
id: "a1",
|
|
563
|
+
role: "assistant",
|
|
564
|
+
parts: [
|
|
565
|
+
null,
|
|
566
|
+
{ text: "no type" },
|
|
567
|
+
{
|
|
568
|
+
type: "tool-weather",
|
|
569
|
+
toolCallId: "tc-1",
|
|
570
|
+
state: "input-available",
|
|
571
|
+
input: { city: "NYC" },
|
|
572
|
+
},
|
|
573
|
+
],
|
|
574
|
+
},
|
|
575
|
+
]);
|
|
576
|
+
|
|
577
|
+
const { result } = renderHook(() => useAISDKRuntime(chat));
|
|
578
|
+
await waitFor(() => {
|
|
579
|
+
expect(result.current.thread.getState().messages.length).toBeGreaterThan(
|
|
580
|
+
0,
|
|
581
|
+
);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
await act(async () => {
|
|
585
|
+
result.current.thread.append({
|
|
586
|
+
role: "user",
|
|
587
|
+
content: [{ type: "text", text: "continue" }],
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
await waitFor(() => {
|
|
592
|
+
expect(chat.sendMessage).toHaveBeenCalledTimes(1);
|
|
593
|
+
});
|
|
594
|
+
expect(chat.messages[0].parts.slice(0, 2)).toEqual([
|
|
595
|
+
null,
|
|
596
|
+
{ text: "no type" },
|
|
597
|
+
]);
|
|
598
|
+
expect(chat.messages[0].parts[2].state).toBe("output-error");
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it("answers a tool call in an earlier assistant message with malformed parts", async () => {
|
|
602
|
+
const chat = createChatHelpers([
|
|
603
|
+
{
|
|
604
|
+
id: "a1",
|
|
605
|
+
role: "assistant",
|
|
606
|
+
parts: [
|
|
607
|
+
null,
|
|
608
|
+
{
|
|
609
|
+
type: "tool-weather",
|
|
610
|
+
toolCallId: "tc-1",
|
|
611
|
+
state: "input-available",
|
|
612
|
+
input: { city: "NYC" },
|
|
613
|
+
},
|
|
614
|
+
],
|
|
615
|
+
},
|
|
616
|
+
{ id: "u2", role: "user", parts: [{ type: "text", text: "and?" }] },
|
|
617
|
+
{ id: "a2", role: "assistant", parts: [{ type: "text", text: "ok" }] },
|
|
618
|
+
]);
|
|
619
|
+
|
|
620
|
+
const { result } = renderHook(() => useAISDKRuntime(chat));
|
|
621
|
+
await waitFor(() => {
|
|
622
|
+
expect(result.current.thread.getState().messages.length).toBe(3);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
await act(async () => {
|
|
626
|
+
result.current.thread
|
|
627
|
+
.getMessageById("a1")
|
|
628
|
+
.getMessagePartByToolCallId("tc-1")
|
|
629
|
+
.addToolResult({ temp: 72 });
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
await waitFor(() => {
|
|
633
|
+
expect(chat.messages[0].parts[1].state).toBe("output-available");
|
|
634
|
+
});
|
|
635
|
+
expect(chat.messages[0].parts[0]).toBeNull();
|
|
636
|
+
expect(chat.messages[0].parts[1].output).toEqual({ temp: 72 });
|
|
637
|
+
});
|
|
638
|
+
|
|
531
639
|
it("strips stale approval when cancelling a tool pending approval so history stays valid", async () => {
|
|
532
640
|
const chat = createChatHelpers([
|
|
533
641
|
{
|
|
@@ -845,10 +953,9 @@ describe("useAISDKRuntime", () => {
|
|
|
845
953
|
expect(chat.sendMessage).not.toHaveBeenCalled();
|
|
846
954
|
expect(chat.messages.map((m: any) => m.id)).toEqual([
|
|
847
955
|
"u1",
|
|
848
|
-
"a1",
|
|
849
956
|
expect.any(String),
|
|
850
957
|
]);
|
|
851
|
-
expect(chat.messages[
|
|
958
|
+
expect(chat.messages[1]).toEqual(
|
|
852
959
|
expect.objectContaining({
|
|
853
960
|
role: "user",
|
|
854
961
|
parts: expect.arrayContaining([
|
|
@@ -1044,8 +1151,7 @@ describe("useAISDKRuntime", () => {
|
|
|
1044
1151
|
expect(chat.sendMessage).toHaveBeenCalledTimes(1);
|
|
1045
1152
|
});
|
|
1046
1153
|
|
|
1047
|
-
|
|
1048
|
-
expect(chat.messages.map((m: any) => m.id)).toEqual(["u1", "a1"]);
|
|
1154
|
+
expect(chat.messages.map((m: any) => m.id)).toEqual(["u1"]);
|
|
1049
1155
|
expect(chat.sendMessage).toHaveBeenCalledWith(
|
|
1050
1156
|
expect.objectContaining({ role: "user" }),
|
|
1051
1157
|
{ metadata: { custom: { temperature: 0.2 } } },
|
|
@@ -1697,6 +1803,307 @@ describe("useAISDKRuntime", () => {
|
|
|
1697
1803
|
expect(result.current.thread.getState().suggestions).toEqual([]);
|
|
1698
1804
|
});
|
|
1699
1805
|
|
|
1806
|
+
it("regenerates suggestions when the same adapter is restored", async () => {
|
|
1807
|
+
const generate = vi
|
|
1808
|
+
.fn()
|
|
1809
|
+
.mockResolvedValueOnce([{ prompt: "first" }])
|
|
1810
|
+
.mockResolvedValueOnce([{ prompt: "second" }]);
|
|
1811
|
+
const chat = createChatHelpers([
|
|
1812
|
+
{ id: "u1", role: "user", parts: [{ type: "text", text: "hi" }] },
|
|
1813
|
+
{
|
|
1814
|
+
id: "a1",
|
|
1815
|
+
role: "assistant",
|
|
1816
|
+
parts: [{ type: "text", text: "hello" }],
|
|
1817
|
+
},
|
|
1818
|
+
]);
|
|
1819
|
+
|
|
1820
|
+
const { result, rerender } = renderHook(
|
|
1821
|
+
({ status, withAdapter }) => {
|
|
1822
|
+
chat.status = status;
|
|
1823
|
+
return useAISDKRuntime(
|
|
1824
|
+
chat,
|
|
1825
|
+
withAdapter ? { adapters: { suggestion: { generate } } } : {},
|
|
1826
|
+
);
|
|
1827
|
+
},
|
|
1828
|
+
{
|
|
1829
|
+
initialProps: {
|
|
1830
|
+
status: "submitted" as string,
|
|
1831
|
+
withAdapter: true,
|
|
1832
|
+
},
|
|
1833
|
+
},
|
|
1834
|
+
);
|
|
1835
|
+
|
|
1836
|
+
rerender({ status: "ready", withAdapter: true });
|
|
1837
|
+
await waitFor(() => {
|
|
1838
|
+
expect(result.current.thread.getState().suggestions).toEqual([
|
|
1839
|
+
{ prompt: "first" },
|
|
1840
|
+
]);
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1843
|
+
rerender({ status: "ready", withAdapter: false });
|
|
1844
|
+
expect(result.current.thread.getState().suggestions).toEqual([]);
|
|
1845
|
+
|
|
1846
|
+
rerender({ status: "ready", withAdapter: true });
|
|
1847
|
+
await waitFor(() => expect(generate).toHaveBeenCalledTimes(2));
|
|
1848
|
+
await waitFor(() => {
|
|
1849
|
+
expect(result.current.thread.getState().suggestions).toEqual([
|
|
1850
|
+
{ prompt: "second" },
|
|
1851
|
+
]);
|
|
1852
|
+
});
|
|
1853
|
+
});
|
|
1854
|
+
|
|
1855
|
+
it("preserves the suggestion adapter receiver", async () => {
|
|
1856
|
+
const adapter = {
|
|
1857
|
+
prompt: "bound",
|
|
1858
|
+
async generate() {
|
|
1859
|
+
return [{ prompt: this.prompt }];
|
|
1860
|
+
},
|
|
1861
|
+
};
|
|
1862
|
+
const chat = createChatHelpers([
|
|
1863
|
+
{ id: "u1", role: "user", parts: [{ type: "text", text: "hi" }] },
|
|
1864
|
+
{
|
|
1865
|
+
id: "a1",
|
|
1866
|
+
role: "assistant",
|
|
1867
|
+
parts: [{ type: "text", text: "hello" }],
|
|
1868
|
+
},
|
|
1869
|
+
]);
|
|
1870
|
+
|
|
1871
|
+
const { result, rerender } = renderHook(
|
|
1872
|
+
({ status }) => {
|
|
1873
|
+
chat.status = status;
|
|
1874
|
+
return useAISDKRuntime(chat, {
|
|
1875
|
+
adapters: { suggestion: adapter },
|
|
1876
|
+
});
|
|
1877
|
+
},
|
|
1878
|
+
{ initialProps: { status: "submitted" as string } },
|
|
1879
|
+
);
|
|
1880
|
+
|
|
1881
|
+
rerender({ status: "ready" });
|
|
1882
|
+
await waitFor(() => {
|
|
1883
|
+
expect(result.current.thread.getState().suggestions).toEqual([
|
|
1884
|
+
{ prompt: "bound" },
|
|
1885
|
+
]);
|
|
1886
|
+
});
|
|
1887
|
+
});
|
|
1888
|
+
|
|
1889
|
+
it("keeps a keyless inline suggestion adapter stable across rerenders", async () => {
|
|
1890
|
+
let resolveGenerate!: (value: readonly { prompt: string }[]) => void;
|
|
1891
|
+
const generate = vi.fn(
|
|
1892
|
+
({ signal: _signal }: { signal?: AbortSignal }) =>
|
|
1893
|
+
new Promise<readonly { prompt: string }[]>((resolve) => {
|
|
1894
|
+
resolveGenerate = resolve;
|
|
1895
|
+
}),
|
|
1896
|
+
);
|
|
1897
|
+
const chat = createChatHelpers([
|
|
1898
|
+
{ id: "u1", role: "user", parts: [{ type: "text", text: "hi" }] },
|
|
1899
|
+
{
|
|
1900
|
+
id: "a1",
|
|
1901
|
+
role: "assistant",
|
|
1902
|
+
parts: [{ type: "text", text: "hello" }],
|
|
1903
|
+
},
|
|
1904
|
+
]);
|
|
1905
|
+
|
|
1906
|
+
const { result, rerender } = renderHook(
|
|
1907
|
+
({ status }) => {
|
|
1908
|
+
chat.status = status;
|
|
1909
|
+
return useAISDKRuntime(chat, {
|
|
1910
|
+
adapters: {
|
|
1911
|
+
suggestion: {
|
|
1912
|
+
async generate({ signal }) {
|
|
1913
|
+
return generate({
|
|
1914
|
+
...(signal !== undefined && { signal }),
|
|
1915
|
+
});
|
|
1916
|
+
},
|
|
1917
|
+
},
|
|
1918
|
+
},
|
|
1919
|
+
});
|
|
1920
|
+
},
|
|
1921
|
+
{ initialProps: { status: "submitted" as string } },
|
|
1922
|
+
);
|
|
1923
|
+
|
|
1924
|
+
rerender({ status: "ready" });
|
|
1925
|
+
await waitFor(() => expect(generate).toHaveBeenCalledOnce());
|
|
1926
|
+
const signal = generate.mock.calls[0]![0].signal;
|
|
1927
|
+
|
|
1928
|
+
rerender({ status: "ready" });
|
|
1929
|
+
expect(generate).toHaveBeenCalledOnce();
|
|
1930
|
+
expect(signal?.aborted).toBe(false);
|
|
1931
|
+
|
|
1932
|
+
resolveGenerate([{ prompt: "next" }]);
|
|
1933
|
+
await waitFor(() => {
|
|
1934
|
+
expect(result.current.thread.getState().suggestions).toEqual([
|
|
1935
|
+
{ prompt: "next" },
|
|
1936
|
+
]);
|
|
1937
|
+
});
|
|
1938
|
+
await act(async () => {
|
|
1939
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
1940
|
+
});
|
|
1941
|
+
expect(generate).toHaveBeenCalledOnce();
|
|
1942
|
+
});
|
|
1943
|
+
|
|
1944
|
+
it("does not generate for an inline adapter on mount or parent rerender", async () => {
|
|
1945
|
+
const complete = vi.fn().mockResolvedValue(["unexpected"]);
|
|
1946
|
+
const chat = createChatHelpers([
|
|
1947
|
+
{ id: "u1", role: "user", parts: [{ type: "text", text: "hi" }] },
|
|
1948
|
+
{
|
|
1949
|
+
id: "a1",
|
|
1950
|
+
role: "assistant",
|
|
1951
|
+
parts: [{ type: "text", text: "hello" }],
|
|
1952
|
+
},
|
|
1953
|
+
]);
|
|
1954
|
+
|
|
1955
|
+
const { rerender } = renderHook(() =>
|
|
1956
|
+
useAISDKRuntime(chat, {
|
|
1957
|
+
adapters: {
|
|
1958
|
+
suggestion: createSuggestionAdapter({ complete }),
|
|
1959
|
+
},
|
|
1960
|
+
}),
|
|
1961
|
+
);
|
|
1962
|
+
|
|
1963
|
+
rerender();
|
|
1964
|
+
await act(async () => {
|
|
1965
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
1966
|
+
});
|
|
1967
|
+
expect(complete).not.toHaveBeenCalled();
|
|
1968
|
+
});
|
|
1969
|
+
|
|
1970
|
+
it("restarts an inline suggestion adapter when its key changes", async () => {
|
|
1971
|
+
const firstComplete = vi.fn().mockResolvedValue(["first"]);
|
|
1972
|
+
const secondComplete = vi.fn().mockResolvedValue(["second"]);
|
|
1973
|
+
const chat = createChatHelpers([
|
|
1974
|
+
{ id: "u1", role: "user", parts: [{ type: "text", text: "hi" }] },
|
|
1975
|
+
{
|
|
1976
|
+
id: "a1",
|
|
1977
|
+
role: "assistant",
|
|
1978
|
+
parts: [{ type: "text", text: "hello" }],
|
|
1979
|
+
},
|
|
1980
|
+
]);
|
|
1981
|
+
|
|
1982
|
+
const { result, rerender } = renderHook(
|
|
1983
|
+
({ status, adapterKey, complete }) => {
|
|
1984
|
+
chat.status = status;
|
|
1985
|
+
return useAISDKRuntime(chat, {
|
|
1986
|
+
adapters: {
|
|
1987
|
+
suggestion: createSuggestionAdapter({
|
|
1988
|
+
key: adapterKey,
|
|
1989
|
+
complete,
|
|
1990
|
+
}),
|
|
1991
|
+
},
|
|
1992
|
+
});
|
|
1993
|
+
},
|
|
1994
|
+
{
|
|
1995
|
+
initialProps: {
|
|
1996
|
+
status: "submitted" as string,
|
|
1997
|
+
adapterKey: "first",
|
|
1998
|
+
complete: firstComplete,
|
|
1999
|
+
},
|
|
2000
|
+
},
|
|
2001
|
+
);
|
|
2002
|
+
|
|
2003
|
+
rerender({
|
|
2004
|
+
status: "ready",
|
|
2005
|
+
adapterKey: "first",
|
|
2006
|
+
complete: firstComplete,
|
|
2007
|
+
});
|
|
2008
|
+
await waitFor(() => {
|
|
2009
|
+
expect(result.current.thread.getState().suggestions).toEqual([
|
|
2010
|
+
{ prompt: "first" },
|
|
2011
|
+
]);
|
|
2012
|
+
});
|
|
2013
|
+
|
|
2014
|
+
rerender({
|
|
2015
|
+
status: "ready",
|
|
2016
|
+
adapterKey: "first",
|
|
2017
|
+
complete: firstComplete,
|
|
2018
|
+
});
|
|
2019
|
+
expect(firstComplete).toHaveBeenCalledOnce();
|
|
2020
|
+
|
|
2021
|
+
rerender({
|
|
2022
|
+
status: "ready",
|
|
2023
|
+
adapterKey: "second",
|
|
2024
|
+
complete: secondComplete,
|
|
2025
|
+
});
|
|
2026
|
+
await waitFor(() => expect(secondComplete).toHaveBeenCalledOnce());
|
|
2027
|
+
await waitFor(() => {
|
|
2028
|
+
expect(result.current.thread.getState().suggestions).toEqual([
|
|
2029
|
+
{ prompt: "second" },
|
|
2030
|
+
]);
|
|
2031
|
+
});
|
|
2032
|
+
});
|
|
2033
|
+
|
|
2034
|
+
it("restarts suggestion generation across stable adapter replacements", async () => {
|
|
2035
|
+
let resolveFirst!: (value: readonly { prompt: string }[]) => void;
|
|
2036
|
+
const firstGenerate = vi.fn().mockImplementation(
|
|
2037
|
+
() =>
|
|
2038
|
+
new Promise<readonly { prompt: string }[]>((resolve) => {
|
|
2039
|
+
resolveFirst = resolve;
|
|
2040
|
+
}),
|
|
2041
|
+
);
|
|
2042
|
+
let resolveSecond!: (value: readonly { prompt: string }[]) => void;
|
|
2043
|
+
const secondGenerate = vi.fn().mockImplementation(
|
|
2044
|
+
() =>
|
|
2045
|
+
new Promise<readonly { prompt: string }[]>((resolve) => {
|
|
2046
|
+
resolveSecond = resolve;
|
|
2047
|
+
}),
|
|
2048
|
+
);
|
|
2049
|
+
const thirdGenerate = vi.fn().mockResolvedValue([{ prompt: "third" }]);
|
|
2050
|
+
const firstAdapter = { key: "first", generate: firstGenerate };
|
|
2051
|
+
const secondAdapter = { key: "second", generate: secondGenerate };
|
|
2052
|
+
const thirdAdapter = { key: "third", generate: thirdGenerate };
|
|
2053
|
+
const chat = createChatHelpers([
|
|
2054
|
+
{ id: "u1", role: "user", parts: [{ type: "text", text: "hi" }] },
|
|
2055
|
+
{
|
|
2056
|
+
id: "a1",
|
|
2057
|
+
role: "assistant",
|
|
2058
|
+
parts: [{ type: "text", text: "hello" }],
|
|
2059
|
+
},
|
|
2060
|
+
]);
|
|
2061
|
+
|
|
2062
|
+
const { result, rerender } = renderHook(
|
|
2063
|
+
({ status, adapter }) => {
|
|
2064
|
+
chat.status = status;
|
|
2065
|
+
return useAISDKRuntime(chat, {
|
|
2066
|
+
adapters: { suggestion: adapter },
|
|
2067
|
+
});
|
|
2068
|
+
},
|
|
2069
|
+
{
|
|
2070
|
+
initialProps: {
|
|
2071
|
+
status: "submitted" as string,
|
|
2072
|
+
adapter: firstAdapter,
|
|
2073
|
+
},
|
|
2074
|
+
},
|
|
2075
|
+
);
|
|
2076
|
+
|
|
2077
|
+
rerender({ status: "ready", adapter: firstAdapter });
|
|
2078
|
+
await waitFor(() => expect(firstGenerate).toHaveBeenCalledOnce());
|
|
2079
|
+
const firstSignal = firstGenerate.mock.calls[0]![0].signal as AbortSignal;
|
|
2080
|
+
|
|
2081
|
+
rerender({ status: "ready", adapter: secondAdapter });
|
|
2082
|
+
|
|
2083
|
+
expect(firstSignal.aborted).toBe(true);
|
|
2084
|
+
await waitFor(() => expect(secondGenerate).toHaveBeenCalledOnce());
|
|
2085
|
+
const secondSignal = secondGenerate.mock.calls[0]![0].signal as AbortSignal;
|
|
2086
|
+
|
|
2087
|
+
rerender({ status: "ready", adapter: thirdAdapter });
|
|
2088
|
+
|
|
2089
|
+
expect(secondSignal.aborted).toBe(true);
|
|
2090
|
+
await waitFor(() => expect(thirdGenerate).toHaveBeenCalledOnce());
|
|
2091
|
+
await waitFor(() => {
|
|
2092
|
+
expect(result.current.thread.getState().suggestions).toEqual([
|
|
2093
|
+
{ prompt: "third" },
|
|
2094
|
+
]);
|
|
2095
|
+
});
|
|
2096
|
+
|
|
2097
|
+
resolveFirst([{ prompt: "stale" }]);
|
|
2098
|
+
resolveSecond([{ prompt: "also stale" }]);
|
|
2099
|
+
await act(async () => {
|
|
2100
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
2101
|
+
});
|
|
2102
|
+
expect(result.current.thread.getState().suggestions).toEqual([
|
|
2103
|
+
{ prompt: "third" },
|
|
2104
|
+
]);
|
|
2105
|
+
});
|
|
2106
|
+
|
|
1700
2107
|
it("merges consecutive assistant messages into one turn by default", async () => {
|
|
1701
2108
|
const chat = createChatHelpers([
|
|
1702
2109
|
{ id: "u1", role: "user", parts: [{ type: "text", text: "hi" }] },
|
|
@@ -1799,4 +2206,130 @@ describe("useAISDKRuntime", () => {
|
|
|
1799
2206
|
error: { code: "AI_APICallError", message: "upstream failed" },
|
|
1800
2207
|
});
|
|
1801
2208
|
});
|
|
2209
|
+
|
|
2210
|
+
it("keeps a cancelled output when the runtime remounts over its owner", async () => {
|
|
2211
|
+
let resolveStop!: () => void;
|
|
2212
|
+
const chat = createChatHelpers([
|
|
2213
|
+
{
|
|
2214
|
+
id: "assistant-1",
|
|
2215
|
+
role: "assistant",
|
|
2216
|
+
parts: [{ type: "text", text: "partial", state: "streaming" }],
|
|
2217
|
+
},
|
|
2218
|
+
]);
|
|
2219
|
+
chat.status = "streaming";
|
|
2220
|
+
chat.stop = vi.fn(
|
|
2221
|
+
() =>
|
|
2222
|
+
new Promise<void>((resolve) => {
|
|
2223
|
+
resolveStop = resolve;
|
|
2224
|
+
}),
|
|
2225
|
+
);
|
|
2226
|
+
const owner = {};
|
|
2227
|
+
|
|
2228
|
+
const first = renderHook(() =>
|
|
2229
|
+
useAISDKRuntime(chat, { unstable_hostApprovalOwner: owner }),
|
|
2230
|
+
);
|
|
2231
|
+
|
|
2232
|
+
act(() => {
|
|
2233
|
+
first.result.current.thread.cancelRun();
|
|
2234
|
+
chat.status = "ready";
|
|
2235
|
+
first.rerender();
|
|
2236
|
+
});
|
|
2237
|
+
|
|
2238
|
+
await waitFor(() => {
|
|
2239
|
+
expect(
|
|
2240
|
+
first.result.current.thread.getState().messages.at(-1)?.status,
|
|
2241
|
+
).toMatchObject({
|
|
2242
|
+
type: "incomplete",
|
|
2243
|
+
reason: "cancelled",
|
|
2244
|
+
});
|
|
2245
|
+
});
|
|
2246
|
+
|
|
2247
|
+
await act(async () => {
|
|
2248
|
+
resolveStop();
|
|
2249
|
+
await Promise.resolve();
|
|
2250
|
+
});
|
|
2251
|
+
first.unmount();
|
|
2252
|
+
|
|
2253
|
+
const second = renderHook(() =>
|
|
2254
|
+
useAISDKRuntime(chat, { unstable_hostApprovalOwner: owner }),
|
|
2255
|
+
);
|
|
2256
|
+
|
|
2257
|
+
expect(
|
|
2258
|
+
second.result.current.thread.getState().messages.at(-1)?.status,
|
|
2259
|
+
).toMatchObject({
|
|
2260
|
+
type: "incomplete",
|
|
2261
|
+
reason: "cancelled",
|
|
2262
|
+
});
|
|
2263
|
+
|
|
2264
|
+
act(() => {
|
|
2265
|
+
chat.status = "streaming";
|
|
2266
|
+
second.rerender();
|
|
2267
|
+
});
|
|
2268
|
+
await waitFor(() => {
|
|
2269
|
+
expect(
|
|
2270
|
+
second.result.current.thread.getState().messages.at(-1)?.status,
|
|
2271
|
+
).toMatchObject({ type: "running" });
|
|
2272
|
+
});
|
|
2273
|
+
|
|
2274
|
+
act(() => {
|
|
2275
|
+
chat.status = "ready";
|
|
2276
|
+
second.rerender();
|
|
2277
|
+
});
|
|
2278
|
+
await waitFor(() => {
|
|
2279
|
+
expect(
|
|
2280
|
+
second.result.current.thread.getState().messages.at(-1)?.status,
|
|
2281
|
+
).toMatchObject({ type: "complete", reason: "unknown" });
|
|
2282
|
+
});
|
|
2283
|
+
second.unmount();
|
|
2284
|
+
|
|
2285
|
+
const third = renderHook(() =>
|
|
2286
|
+
useAISDKRuntime(chat, { unstable_hostApprovalOwner: owner }),
|
|
2287
|
+
);
|
|
2288
|
+
expect(
|
|
2289
|
+
third.result.current.thread.getState().messages.at(-1)?.status,
|
|
2290
|
+
).toMatchObject({ type: "complete", reason: "unknown" });
|
|
2291
|
+
third.unmount();
|
|
2292
|
+
});
|
|
2293
|
+
|
|
2294
|
+
it("keeps a cancellation when the runtime unmounts before rendering it", async () => {
|
|
2295
|
+
let resolveStop!: () => void;
|
|
2296
|
+
const chat = createChatHelpers([
|
|
2297
|
+
{
|
|
2298
|
+
id: "assistant-1",
|
|
2299
|
+
role: "assistant",
|
|
2300
|
+
parts: [{ type: "text", text: "partial", state: "streaming" }],
|
|
2301
|
+
},
|
|
2302
|
+
]);
|
|
2303
|
+
chat.status = "streaming";
|
|
2304
|
+
chat.stop = vi.fn(
|
|
2305
|
+
() =>
|
|
2306
|
+
new Promise<void>((resolve) => {
|
|
2307
|
+
resolveStop = resolve;
|
|
2308
|
+
}),
|
|
2309
|
+
);
|
|
2310
|
+
const owner = {};
|
|
2311
|
+
const first = renderHook(() =>
|
|
2312
|
+
useAISDKRuntime(chat, { unstable_hostApprovalOwner: owner }),
|
|
2313
|
+
);
|
|
2314
|
+
|
|
2315
|
+
act(() => {
|
|
2316
|
+
first.result.current.thread.cancelRun();
|
|
2317
|
+
first.unmount();
|
|
2318
|
+
});
|
|
2319
|
+
expect(chat.stop).toHaveBeenCalledOnce();
|
|
2320
|
+
|
|
2321
|
+
chat.status = "ready";
|
|
2322
|
+
await act(async () => {
|
|
2323
|
+
resolveStop();
|
|
2324
|
+
await Promise.resolve();
|
|
2325
|
+
});
|
|
2326
|
+
|
|
2327
|
+
const second = renderHook(() =>
|
|
2328
|
+
useAISDKRuntime(chat, { unstable_hostApprovalOwner: owner }),
|
|
2329
|
+
);
|
|
2330
|
+
expect(
|
|
2331
|
+
second.result.current.thread.getState().messages.at(-1)?.status,
|
|
2332
|
+
).toMatchObject({ type: "incomplete", reason: "cancelled" });
|
|
2333
|
+
second.unmount();
|
|
2334
|
+
});
|
|
1802
2335
|
});
|