@ulearning/ai-sdk-adapter 0.0.0-stage → 1.0.0-next.0
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/LICENSE +21 -0
- package/README.md +29 -2
- package/dist/adapters/aiSDKFormatAdapter.d.ts +6 -0
- package/dist/adapters/aiSDKFormatAdapter.d.ts.map +1 -0
- package/dist/adapters/aiSDKFormatAdapter.js +17 -0
- package/dist/adapters/aiSDKFormatAdapter.js.map +1 -0
- package/dist/adapters/vercelAttachmentAdapter.d.ts +3 -0
- package/dist/adapters/vercelAttachmentAdapter.d.ts.map +1 -0
- package/dist/adapters/vercelAttachmentAdapter.js +36 -0
- package/dist/adapters/vercelAttachmentAdapter.js.map +1 -0
- package/dist/aiSDKExtras.d.ts +13 -0
- package/dist/aiSDKExtras.d.ts.map +1 -0
- package/dist/aiSDKExtras.js +6 -0
- package/dist/aiSDKExtras.js.map +1 -0
- package/dist/converters/convertMessage.d.ts +44 -0
- package/dist/converters/convertMessage.d.ts.map +1 -0
- package/dist/converters/convertMessage.js +465 -0
- package/dist/converters/convertMessage.js.map +1 -0
- package/dist/converters/toCreateMessage.d.ts +4 -0
- package/dist/converters/toCreateMessage.d.ts.map +1 -0
- package/dist/converters/toCreateMessage.js +65 -0
- package/dist/converters/toCreateMessage.js.map +1 -0
- package/dist/converters/toolApprovalAnswers.d.ts +3 -0
- package/dist/converters/toolApprovalAnswers.d.ts.map +1 -0
- package/dist/converters/toolApprovalAnswers.js +16 -0
- package/dist/converters/toolApprovalAnswers.js.map +1 -0
- package/dist/hooks.d.ts +17 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +19 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/model-context/injectInteractableContext.d.ts +37 -0
- package/dist/model-context/injectInteractableContext.d.ts.map +1 -0
- package/dist/model-context/injectInteractableContext.js +54 -0
- package/dist/model-context/injectInteractableContext.js.map +1 -0
- package/dist/model-context/injectQuoteContext.d.ts +25 -0
- package/dist/model-context/injectQuoteContext.d.ts.map +1 -0
- package/dist/model-context/injectQuoteContext.js +51 -0
- package/dist/model-context/injectQuoteContext.js.map +1 -0
- package/dist/runtime/AISDKChat.d.ts +13 -0
- package/dist/runtime/AISDKChat.d.ts.map +1 -0
- package/dist/runtime/AISDKChat.js +34 -0
- package/dist/runtime/AISDKChat.js.map +1 -0
- package/dist/runtime/AISDKThreads.d.ts +24 -0
- package/dist/runtime/AISDKThreads.d.ts.map +1 -0
- package/dist/runtime/AISDKThreads.js +92 -0
- package/dist/runtime/AISDKThreads.js.map +1 -0
- package/dist/runtime/DynamicChatTransport.d.ts +50 -0
- package/dist/runtime/DynamicChatTransport.d.ts.map +1 -0
- package/dist/runtime/DynamicChatTransport.js +153 -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 +11 -0
- package/dist/runtime/getResumableAdapter.js.map +1 -0
- package/dist/runtime/toolHistoryCodec.d.ts +20 -0
- package/dist/runtime/toolHistoryCodec.d.ts.map +1 -0
- package/dist/runtime/toolHistoryCodec.js +122 -0
- package/dist/runtime/toolHistoryCodec.js.map +1 -0
- package/dist/runtime/useAISDKRuntime.d.ts +95 -0
- package/dist/runtime/useAISDKRuntime.d.ts.map +1 -0
- package/dist/runtime/useAISDKRuntime.js +754 -0
- package/dist/runtime/useAISDKRuntime.js.map +1 -0
- package/dist/runtime/useChatRuntime.d.ts +17 -0
- package/dist/runtime/useChatRuntime.d.ts.map +1 -0
- package/dist/runtime/useChatRuntime.js +50 -0
- package/dist/runtime/useChatRuntime.js.map +1 -0
- package/dist/runtime/useChatSnapshot.d.ts +6 -0
- package/dist/runtime/useChatSnapshot.d.ts.map +1 -0
- package/dist/runtime/useChatSnapshot.js +62 -0
- package/dist/runtime/useChatSnapshot.js.map +1 -0
- package/dist/runtime/useChatThread.d.ts +121 -0
- package/dist/runtime/useChatThread.d.ts.map +1 -0
- package/dist/runtime/useChatThread.js +255 -0
- package/dist/runtime/useChatThread.js.map +1 -0
- package/dist/runtime/useDynamicChatTransport.d.ts +4 -0
- package/dist/runtime/useDynamicChatTransport.d.ts.map +1 -0
- package/dist/runtime/useDynamicChatTransport.js +26 -0
- package/dist/runtime/useDynamicChatTransport.js.map +1 -0
- package/dist/runtime/useExternalHistory.d.ts +12 -0
- package/dist/runtime/useExternalHistory.d.ts.map +1 -0
- package/dist/runtime/useExternalHistory.js +556 -0
- package/dist/runtime/useExternalHistory.js.map +1 -0
- package/dist/runtime/useMessageQueue.d.ts +14 -0
- package/dist/runtime/useMessageQueue.d.ts.map +1 -0
- package/dist/runtime/useMessageQueue.js +142 -0
- package/dist/runtime/useMessageQueue.js.map +1 -0
- package/dist/runtime/useResourceCleanup.d.ts +2 -0
- package/dist/runtime/useResourceCleanup.d.ts.map +1 -0
- package/dist/runtime/useResourceCleanup.js +47 -0
- package/dist/runtime/useResourceCleanup.js.map +1 -0
- package/dist/runtime/useStreamingTiming.d.ts +12 -0
- package/dist/runtime/useStreamingTiming.d.ts.map +1 -0
- package/dist/runtime/useStreamingTiming.js +36 -0
- package/dist/runtime/useStreamingTiming.js.map +1 -0
- package/dist/tools/generativeTools.d.ts +71 -0
- package/dist/tools/generativeTools.d.ts.map +1 -0
- package/dist/tools/generativeTools.js +258 -0
- package/dist/tools/generativeTools.js.map +1 -0
- package/dist/tools/mcp-stdio.node.d.ts +2 -0
- package/dist/tools/mcp-stdio.node.d.ts.map +1 -0
- package/dist/tools/mcp-stdio.node.js +1 -0
- package/dist/tools/mcp-stdio.unsupported.d.ts +3 -0
- package/dist/tools/mcp-stdio.unsupported.d.ts.map +1 -0
- package/dist/tools/mcp-stdio.unsupported.js +10 -0
- package/dist/tools/mcp-stdio.unsupported.js.map +1 -0
- package/dist/transport/AssistantChatTransport.d.ts +20 -0
- package/dist/transport/AssistantChatTransport.d.ts.map +1 -0
- package/dist/transport/AssistantChatTransport.js +151 -0
- package/dist/transport/AssistantChatTransport.js.map +1 -0
- package/dist/transport/resumable.d.ts +36 -0
- package/dist/transport/resumable.d.ts.map +1 -0
- package/dist/transport/resumable.js +112 -0
- package/dist/transport/resumable.js.map +1 -0
- package/dist/usage.d.ts +24 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +121 -0
- package/dist/usage.js.map +1 -0
- package/dist/utils/getVercelAIMessages.d.ts +4 -0
- package/dist/utils/getVercelAIMessages.d.ts.map +1 -0
- package/dist/utils/getVercelAIMessages.js +8 -0
- package/dist/utils/getVercelAIMessages.js.map +1 -0
- package/dist/utils/sliceMessagesUntil.d.ts +3 -0
- package/dist/utils/sliceMessagesUntil.d.ts.map +1 -0
- package/dist/utils/sliceMessagesUntil.js +10 -0
- package/dist/utils/sliceMessagesUntil.js.map +1 -0
- package/package.json +96 -3
- package/src/__tests__/standalone.e2e.ts +118 -0
- package/src/adapters/aiSDKFormatAdapter.test.ts +36 -0
- package/src/adapters/aiSDKFormatAdapter.ts +18 -0
- package/src/adapters/vercelAttachmentAdapter.test.ts +52 -0
- package/src/adapters/vercelAttachmentAdapter.ts +36 -0
- package/src/aiSDKExtras.ts +31 -0
- package/src/converters/convertMessage.metadata-key.test.tsx +182 -0
- package/src/converters/convertMessage.test.ts +2428 -0
- package/src/converters/convertMessage.tool-args-status.test.tsx +185 -0
- package/src/converters/convertMessage.ts +919 -0
- package/src/converters/toCreateMessage.test.ts +716 -0
- package/src/converters/toCreateMessage.ts +110 -0
- package/src/converters/toolApprovalAnswers.ts +27 -0
- package/src/hooks.test.ts +168 -0
- package/src/hooks.ts +26 -0
- package/src/index.ts +29 -0
- package/src/model-context/injectInteractableContext.test.ts +167 -0
- package/src/model-context/injectInteractableContext.ts +64 -0
- package/src/model-context/injectQuoteContext.test.ts +33 -0
- package/src/model-context/injectQuoteContext.ts +63 -0
- package/src/runtime/AISDKChat.artifacts.test.ts +48 -0
- package/src/runtime/AISDKChat.integration.test.tsx +148 -0
- package/src/runtime/AISDKChat.test.ts +241 -0
- package/src/runtime/AISDKChat.ts +43 -0
- package/src/runtime/AISDKThreads.test.ts +811 -0
- package/src/runtime/AISDKThreads.ts +194 -0
- package/src/runtime/DynamicChatTransport.test.ts +228 -0
- package/src/runtime/DynamicChatTransport.ts +281 -0
- package/src/runtime/__tests__/controlled-transport.ts +70 -0
- package/src/runtime/__tests__/tap.ts +80 -0
- package/src/runtime/__tests__/vue.ts +46 -0
- package/src/runtime/getResumableAdapter.ts +16 -0
- package/src/runtime/toolHistoryCodec.test.ts +187 -0
- package/src/runtime/toolHistoryCodec.ts +245 -0
- package/src/runtime/useAISDKRuntime.approval.integration.test.tsx +1956 -0
- package/src/runtime/useAISDKRuntime.approval.test.tsx +580 -0
- package/src/runtime/useAISDKRuntime.denied-tool.test.tsx +122 -0
- package/src/runtime/useAISDKRuntime.fast-refresh.test.tsx +302 -0
- package/src/runtime/useAISDKRuntime.metadata-cache.test.tsx +157 -0
- package/src/runtime/useAISDKRuntime.queue.test.tsx +707 -0
- package/src/runtime/useAISDKRuntime.reload.test.tsx +217 -0
- package/src/runtime/useAISDKRuntime.test.ts +2345 -0
- package/src/runtime/useAISDKRuntime.tool-output.test.tsx +113 -0
- package/src/runtime/useAISDKRuntime.ts +1503 -0
- package/src/runtime/useAISDKRuntime.voice.test.tsx +319 -0
- package/src/runtime/useChatOnTap.test.ts +60 -0
- package/src/runtime/useChatRuntime.fast-refresh.test.tsx +83 -0
- package/src/runtime/useChatRuntime.integration.test.tsx +661 -0
- package/src/runtime/useChatRuntime.local-storage.test.tsx +114 -0
- package/src/runtime/useChatRuntime.native.test.ts +147 -0
- package/src/runtime/useChatRuntime.run-telemetry.test.tsx +226 -0
- package/src/runtime/useChatRuntime.test.ts +619 -0
- package/src/runtime/useChatRuntime.ts +80 -0
- package/src/runtime/useChatSnapshot.test.ts +60 -0
- package/src/runtime/useChatSnapshot.ts +86 -0
- package/src/runtime/useChatThread.binding.test.tsx +140 -0
- package/src/runtime/useChatThread.test.ts +993 -0
- package/src/runtime/useChatThread.transport.test.tsx +149 -0
- package/src/runtime/useChatThread.ts +533 -0
- package/src/runtime/useDynamicChatTransport.ts +26 -0
- package/src/runtime/useExternalHistory.test.ts +4187 -0
- package/src/runtime/useExternalHistory.ts +902 -0
- package/src/runtime/useMessageQueue.effects.test.tsx +88 -0
- package/src/runtime/useMessageQueue.test.tsx +256 -0
- package/src/runtime/useMessageQueue.ts +189 -0
- package/src/runtime/useResourceCleanup.ts +58 -0
- package/src/runtime/useStreamingTiming.test.tsx +96 -0
- package/src/runtime/useStreamingTiming.ts +69 -0
- package/src/tools/generativeTools.test.ts +1058 -0
- package/src/tools/generativeTools.ts +491 -0
- package/src/tools/mcp-stdio.node.ts +1 -0
- package/src/tools/mcp-stdio.unsupported.ts +12 -0
- package/src/transport/AssistantChatTransport.test.ts +631 -0
- package/src/transport/AssistantChatTransport.ts +236 -0
- package/src/transport/resumable.test.ts +262 -0
- package/src/transport/resumable.ts +162 -0
- package/src/usage.test.ts +204 -0
- package/src/usage.ts +179 -0
- package/src/utils/getVercelAIMessages.ts +11 -0
- package/src/utils/sliceMessagesUntil.test.ts +51 -0
- package/src/utils/sliceMessagesUntil.ts +16 -0
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import type { Chat } from "@ai-sdk/vue";
|
|
4
|
+
import type { ChatInit, ChatTransport, UIMessage, UIMessageChunk } from "ai";
|
|
5
|
+
import { defineComponent, h, shallowRef } from "vue";
|
|
6
|
+
import { useTapRoot } from "@ulearning/ai-tap";
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
8
|
+
import { act, render, waitFor } from "./__tests__/vue";
|
|
9
|
+
import type { UseChatRuntimeOptions } from "./useChatRuntime";
|
|
10
|
+
import type { ResumableClientStorage } from "../transport/resumable";
|
|
11
|
+
|
|
12
|
+
const mocks = vi.hoisted(() => {
|
|
13
|
+
const state = {
|
|
14
|
+
isLoadingHistory: false,
|
|
15
|
+
threadId: "thread-id",
|
|
16
|
+
mainThreadId: "thread-id",
|
|
17
|
+
};
|
|
18
|
+
const subscribers = new Set<() => void>();
|
|
19
|
+
const chats: Array<{ chat: Chat<UIMessage>; init: ChatInit<UIMessage> }> = [];
|
|
20
|
+
const runtime = {
|
|
21
|
+
thread: {
|
|
22
|
+
getState: () => ({ isLoading: state.isLoadingHistory }),
|
|
23
|
+
getModelContext: () => ({}),
|
|
24
|
+
subscribe: (callback: () => void) => {
|
|
25
|
+
subscribers.add(callback);
|
|
26
|
+
return () => {
|
|
27
|
+
subscribers.delete(callback);
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
threads: { mainItem: undefined },
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
state,
|
|
35
|
+
subscribers,
|
|
36
|
+
runtime,
|
|
37
|
+
chats,
|
|
38
|
+
snapshot:
|
|
39
|
+
vi.fn<(chat: Chat<UIMessage>, throttle: number | undefined) => void>(),
|
|
40
|
+
useAISDKRuntime: vi.fn<
|
|
41
|
+
(_chat: unknown, _options: unknown) => typeof runtime
|
|
42
|
+
>(() => runtime),
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Keep actual Chat requests, callbacks and native subscriptions. Observe only
|
|
47
|
+
// its public construction and resume entry point, without replacing results.
|
|
48
|
+
vi.mock("@ai-sdk/vue", async (importOriginal) => {
|
|
49
|
+
const actual = await importOriginal<typeof import("@ai-sdk/vue")>();
|
|
50
|
+
return {
|
|
51
|
+
...actual,
|
|
52
|
+
Chat: class extends actual.Chat<UIMessage> {
|
|
53
|
+
constructor(init: ChatInit<UIMessage>) {
|
|
54
|
+
super(init);
|
|
55
|
+
const chat: Chat<UIMessage> = this;
|
|
56
|
+
vi.spyOn(chat, "resumeStream");
|
|
57
|
+
mocks.chats.push({ chat, init });
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
vi.mock("./useChatSnapshot", async (importOriginal) => {
|
|
64
|
+
const actual = await importOriginal<typeof import("./useChatSnapshot")>();
|
|
65
|
+
return {
|
|
66
|
+
...actual,
|
|
67
|
+
useChatSnapshot(chat: Chat<UIMessage>, throttle: number | undefined) {
|
|
68
|
+
mocks.snapshot(chat, throttle);
|
|
69
|
+
return actual.useChatSnapshot(chat, throttle);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
vi.mock("@ulearning/ai-core/vue", async (importOriginal) => ({
|
|
75
|
+
...(await importOriginal<typeof import("@ulearning/ai-core/vue")>()),
|
|
76
|
+
useRemoteThreadListRuntime: ({
|
|
77
|
+
runtimeHook,
|
|
78
|
+
}: {
|
|
79
|
+
runtimeHook: () => unknown;
|
|
80
|
+
}) => runtimeHook(),
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
vi.mock("@ulearning/ai-store", async (importOriginal) => {
|
|
84
|
+
const actual = await importOriginal<typeof import("@ulearning/ai-store")>();
|
|
85
|
+
const { useMemo } = await import("@ulearning/ai-tap");
|
|
86
|
+
return {
|
|
87
|
+
...actual,
|
|
88
|
+
useAui: () =>
|
|
89
|
+
useMemo(() => {
|
|
90
|
+
const id = mocks.state.threadId;
|
|
91
|
+
const mainThreadId = mocks.state.mainThreadId;
|
|
92
|
+
return {
|
|
93
|
+
subscribe: () => () => {},
|
|
94
|
+
threadListItem: { source: undefined, getState: () => ({ id }) },
|
|
95
|
+
threads: { getState: () => ({ mainThreadId }) },
|
|
96
|
+
};
|
|
97
|
+
}, []),
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
vi.mock("./useAISDKRuntime", async (importOriginal) => ({
|
|
102
|
+
...(await importOriginal<typeof import("./useAISDKRuntime")>()),
|
|
103
|
+
useAISDKRuntime: mocks.useAISDKRuntime,
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
import { AssistantChatTransport } from "../transport/AssistantChatTransport";
|
|
107
|
+
import {
|
|
108
|
+
createResumableSessionStorage,
|
|
109
|
+
RESUMABLE_STREAM_ID_HEADER,
|
|
110
|
+
} from "../transport/resumable";
|
|
111
|
+
import { getResumedStreamIds } from "./DynamicChatTransport";
|
|
112
|
+
import { useChatRuntime } from "./useChatRuntime";
|
|
113
|
+
|
|
114
|
+
const sendMessagesOptions: Parameters<
|
|
115
|
+
ChatTransport<UIMessage>["sendMessages"]
|
|
116
|
+
>[0] = {
|
|
117
|
+
trigger: "submit-message",
|
|
118
|
+
chatId: "thread-id",
|
|
119
|
+
messageId: undefined,
|
|
120
|
+
messages: [{ id: "m1", role: "user", parts: [{ type: "text", text: "hi" }] }],
|
|
121
|
+
abortSignal: undefined,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
function mountRuntime(initial: UseChatRuntimeOptions = {}) {
|
|
125
|
+
const options = shallowRef(initial);
|
|
126
|
+
const view = render(
|
|
127
|
+
h(
|
|
128
|
+
defineComponent({
|
|
129
|
+
setup() {
|
|
130
|
+
const root = useTapRoot(function useRuntimeFixture() {
|
|
131
|
+
return useChatRuntime(options.value);
|
|
132
|
+
});
|
|
133
|
+
root.getValue();
|
|
134
|
+
return () => null;
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
return {
|
|
140
|
+
...view,
|
|
141
|
+
async rerender(next: UseChatRuntimeOptions = options.value) {
|
|
142
|
+
await act(() => {
|
|
143
|
+
options.value = { ...next };
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function observedChat(index = 0) {
|
|
150
|
+
const observed = mocks.chats[index];
|
|
151
|
+
if (!observed) throw new Error(`Missing actual Chat construction ${index}`);
|
|
152
|
+
return observed;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function resumableTransport(
|
|
156
|
+
storage: ResumableClientStorage,
|
|
157
|
+
reconnectToStream: ChatTransport<UIMessage>["reconnectToStream"] = async () =>
|
|
158
|
+
null,
|
|
159
|
+
): ChatTransport<UIMessage> & {
|
|
160
|
+
getResumableAdapter: () => {
|
|
161
|
+
storage: ResumableClientStorage;
|
|
162
|
+
resumeApi: string;
|
|
163
|
+
};
|
|
164
|
+
} {
|
|
165
|
+
return {
|
|
166
|
+
sendMessages: async () =>
|
|
167
|
+
new ReadableStream<UIMessageChunk>({
|
|
168
|
+
start: (controller) => controller.close(),
|
|
169
|
+
}),
|
|
170
|
+
reconnectToStream,
|
|
171
|
+
getResumableAdapter: () => ({ storage, resumeApi: "/api/chat/resume" }),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function observableStorage(initial: string | null = null) {
|
|
176
|
+
let streamId = initial;
|
|
177
|
+
const subscribers = new Set<() => void>();
|
|
178
|
+
const storage: ResumableClientStorage = {
|
|
179
|
+
getStreamId: () => streamId,
|
|
180
|
+
setStreamId: (id) => {
|
|
181
|
+
streamId = id;
|
|
182
|
+
subscribers.forEach((callback) => callback());
|
|
183
|
+
},
|
|
184
|
+
clear: vi.fn(() => {
|
|
185
|
+
streamId = null;
|
|
186
|
+
subscribers.forEach((callback) => callback());
|
|
187
|
+
}),
|
|
188
|
+
subscribe: (callback) => {
|
|
189
|
+
subscribers.add(callback);
|
|
190
|
+
return () => {
|
|
191
|
+
subscribers.delete(callback);
|
|
192
|
+
};
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
return storage;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
describe("useChatRuntime", () => {
|
|
199
|
+
afterEach(() => vi.restoreAllMocks());
|
|
200
|
+
beforeEach(() => {
|
|
201
|
+
mocks.snapshot.mockClear();
|
|
202
|
+
mocks.useAISDKRuntime.mockClear();
|
|
203
|
+
mocks.useAISDKRuntime.mockImplementation(() => mocks.runtime);
|
|
204
|
+
mocks.state.isLoadingHistory = false;
|
|
205
|
+
mocks.state.threadId = "thread-id";
|
|
206
|
+
mocks.state.mainThreadId = "thread-id";
|
|
207
|
+
mocks.subscribers.clear();
|
|
208
|
+
mocks.chats.length = 0;
|
|
209
|
+
window.sessionStorage.clear();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("refreshes AssistantChatTransport wiring when the runtime changes", async () => {
|
|
213
|
+
const bodies: Array<{ system: string }> = [];
|
|
214
|
+
const transport = new AssistantChatTransport({
|
|
215
|
+
fetch: vi.fn(async (_input, init) => {
|
|
216
|
+
bodies.push(JSON.parse(String(init?.body)));
|
|
217
|
+
return new Response(
|
|
218
|
+
new ReadableStream({ start: (controller) => controller.close() }),
|
|
219
|
+
{
|
|
220
|
+
headers: { "content-type": "text/event-stream" },
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
}),
|
|
224
|
+
});
|
|
225
|
+
const createRuntime = (system: string) => ({
|
|
226
|
+
thread: {
|
|
227
|
+
getState: () => ({ isLoading: false }),
|
|
228
|
+
getModelContext: () => ({ system }),
|
|
229
|
+
subscribe: () => () => {},
|
|
230
|
+
},
|
|
231
|
+
threads: { mainItem: undefined },
|
|
232
|
+
});
|
|
233
|
+
let currentRuntime = createRuntime("system-a");
|
|
234
|
+
mocks.useAISDKRuntime.mockImplementation(() => currentRuntime);
|
|
235
|
+
const view = mountRuntime({ transport });
|
|
236
|
+
const dynamicTransport = observedChat().init.transport;
|
|
237
|
+
if (!dynamicTransport) throw new Error("Missing real Chat transport");
|
|
238
|
+
await dynamicTransport.sendMessages(sendMessagesOptions);
|
|
239
|
+
currentRuntime = createRuntime("system-b");
|
|
240
|
+
await view.rerender();
|
|
241
|
+
await dynamicTransport.sendMessages(sendMessagesOptions);
|
|
242
|
+
expect(bodies).toEqual([
|
|
243
|
+
expect.objectContaining({ system: "system-a" }),
|
|
244
|
+
expect.objectContaining({ system: "system-b" }),
|
|
245
|
+
]);
|
|
246
|
+
expect(mocks.chats).toHaveLength(1);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("forwards a callback through a ref, so a later render's callback fires instead of the mounted one", async () => {
|
|
250
|
+
const onToolCallA = vi.fn();
|
|
251
|
+
const onToolCallB = vi.fn();
|
|
252
|
+
const view = mountRuntime({ onToolCall: onToolCallA });
|
|
253
|
+
const callback = observedChat().init.onToolCall;
|
|
254
|
+
if (!callback) throw new Error("Missing forwarded Chat callback");
|
|
255
|
+
const first: Parameters<typeof callback>[0] = {
|
|
256
|
+
toolCall: {
|
|
257
|
+
toolCallId: "first",
|
|
258
|
+
toolName: "test",
|
|
259
|
+
input: {},
|
|
260
|
+
dynamic: true,
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
const second: Parameters<typeof callback>[0] = {
|
|
264
|
+
toolCall: {
|
|
265
|
+
toolCallId: "second",
|
|
266
|
+
toolName: "test",
|
|
267
|
+
input: {},
|
|
268
|
+
dynamic: true,
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
await callback(first);
|
|
272
|
+
expect(onToolCallA).toHaveBeenCalledExactlyOnceWith(first);
|
|
273
|
+
await view.rerender({ onToolCall: onToolCallB });
|
|
274
|
+
await callback(second);
|
|
275
|
+
expect(onToolCallB).toHaveBeenCalledExactlyOnceWith(second);
|
|
276
|
+
expect(onToolCallA).toHaveBeenCalledOnce();
|
|
277
|
+
expect(mocks.chats).toHaveLength(1);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("coerces an unset sendAutomaticallyWhen to false, matching useChat's own default", async () => {
|
|
281
|
+
mountRuntime();
|
|
282
|
+
const callback = observedChat().init.sendAutomaticallyWhen;
|
|
283
|
+
if (!callback) throw new Error("Missing Chat automatic-send callback");
|
|
284
|
+
expect(await callback({ messages: [] })).toBe(false);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("forwards a defined chat update throttle to useChat", () => {
|
|
288
|
+
mountRuntime({ throttle: 50 });
|
|
289
|
+
mountRuntime({ throttle: undefined });
|
|
290
|
+
expect(mocks.snapshot).toHaveBeenNthCalledWith(1, observedChat(0).chat, 50);
|
|
291
|
+
expect(mocks.snapshot.mock.calls[1]).toEqual([
|
|
292
|
+
observedChat(1).chat,
|
|
293
|
+
undefined,
|
|
294
|
+
]);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("forwards a custom approval handler to the runtime only", () => {
|
|
298
|
+
const onRespondToToolApproval = vi.fn();
|
|
299
|
+
mountRuntime({ onRespondToToolApproval });
|
|
300
|
+
expect(mocks.useAISDKRuntime).toHaveBeenCalledWith(
|
|
301
|
+
expect.anything(),
|
|
302
|
+
expect.objectContaining({ onRespondToToolApproval }),
|
|
303
|
+
);
|
|
304
|
+
expect(observedChat().init).not.toHaveProperty("onRespondToToolApproval");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("forwards unstable_enableMessageQueue to the runtime, not the chat", () => {
|
|
308
|
+
mountRuntime({ unstable_enableMessageQueue: true });
|
|
309
|
+
expect(mocks.useAISDKRuntime).toHaveBeenCalledWith(
|
|
310
|
+
expect.anything(),
|
|
311
|
+
expect.objectContaining({ unstable_enableMessageQueue: true }),
|
|
312
|
+
);
|
|
313
|
+
expect(observedChat().init).not.toHaveProperty(
|
|
314
|
+
"unstable_enableMessageQueue",
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("waits for external history to load before resuming a stream", async () => {
|
|
319
|
+
mocks.state.isLoadingHistory = true;
|
|
320
|
+
const transport = resumableTransport(observableStorage("stream-1"));
|
|
321
|
+
mountRuntime({ transport });
|
|
322
|
+
const resumeStream = observedChat().chat.resumeStream;
|
|
323
|
+
expect(resumeStream).not.toHaveBeenCalled();
|
|
324
|
+
await act(() => {
|
|
325
|
+
mocks.state.isLoadingHistory = false;
|
|
326
|
+
mocks.subscribers.forEach((callback) => callback());
|
|
327
|
+
});
|
|
328
|
+
await waitFor(() => expect(resumeStream).toHaveBeenCalledTimes(1));
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("resumes a stream id discovered after mount", async () => {
|
|
332
|
+
const storage = observableStorage();
|
|
333
|
+
const reconnect = vi.fn<ChatTransport<UIMessage>["reconnectToStream"]>(
|
|
334
|
+
async () => null,
|
|
335
|
+
);
|
|
336
|
+
mountRuntime({ transport: resumableTransport(storage, reconnect) });
|
|
337
|
+
const resumeStream = observedChat().chat.resumeStream;
|
|
338
|
+
expect(resumeStream).not.toHaveBeenCalled();
|
|
339
|
+
await act(() => storage.setStreamId("stream-1"));
|
|
340
|
+
await waitFor(() => expect(resumeStream).toHaveBeenCalledTimes(1));
|
|
341
|
+
await act(() => storage.setStreamId("stream-1"));
|
|
342
|
+
expect(resumeStream).toHaveBeenCalledTimes(1);
|
|
343
|
+
await act(() => storage.setStreamId("stream-2"));
|
|
344
|
+
await waitFor(() => expect(resumeStream).toHaveBeenCalledTimes(2));
|
|
345
|
+
await act(() => storage.setStreamId("stream-1"));
|
|
346
|
+
expect(resumeStream).toHaveBeenCalledTimes(2);
|
|
347
|
+
expect(reconnect).toHaveBeenCalledTimes(2);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("does not resume the same stored id again after a remount", async () => {
|
|
351
|
+
const storage = createResumableSessionStorage({ key: "remount-stream-id" });
|
|
352
|
+
storage.setStreamId("stream-1", "thread-id");
|
|
353
|
+
const transport = resumableTransport(storage);
|
|
354
|
+
const first = mountRuntime({ transport });
|
|
355
|
+
const resumeStream = observedChat().chat.resumeStream;
|
|
356
|
+
await waitFor(() => expect(resumeStream).toHaveBeenCalledOnce());
|
|
357
|
+
first.unmount();
|
|
358
|
+
mountRuntime({ transport });
|
|
359
|
+
expect(resumeStream).toHaveBeenCalledOnce();
|
|
360
|
+
expect(observedChat(1).chat.resumeStream).not.toHaveBeenCalled();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("does not resume a stream id written by an active send", async () => {
|
|
364
|
+
const storage = createResumableSessionStorage({
|
|
365
|
+
key: "active-send-stream-id",
|
|
366
|
+
});
|
|
367
|
+
let streamController:
|
|
368
|
+
| ReadableStreamDefaultController<Uint8Array>
|
|
369
|
+
| undefined;
|
|
370
|
+
const fetchMock = vi.fn(
|
|
371
|
+
async () =>
|
|
372
|
+
new Response(
|
|
373
|
+
new ReadableStream<Uint8Array>({
|
|
374
|
+
start: (controller) => {
|
|
375
|
+
streamController = controller;
|
|
376
|
+
},
|
|
377
|
+
}),
|
|
378
|
+
{
|
|
379
|
+
status: 200,
|
|
380
|
+
headers: {
|
|
381
|
+
"content-type": "text/event-stream",
|
|
382
|
+
[RESUMABLE_STREAM_ID_HEADER]: "stream-1",
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
),
|
|
386
|
+
);
|
|
387
|
+
const transport = new AssistantChatTransport({
|
|
388
|
+
fetch: fetchMock,
|
|
389
|
+
resumable: { storage, resumeApi: "/api/chat/resume" },
|
|
390
|
+
});
|
|
391
|
+
mountRuntime({ transport });
|
|
392
|
+
const { chat } = observedChat();
|
|
393
|
+
const send = chat.sendMessage({ text: "hi" });
|
|
394
|
+
await waitFor(() =>
|
|
395
|
+
expect(storage.getStreamId("thread-id")).toBe("stream-1"),
|
|
396
|
+
);
|
|
397
|
+
if (!streamController) throw new Error("Missing real response stream");
|
|
398
|
+
streamController.enqueue(
|
|
399
|
+
new TextEncoder().encode(
|
|
400
|
+
[
|
|
401
|
+
'data: {"type":"start","messageId":"active-message"}',
|
|
402
|
+
"",
|
|
403
|
+
'data: {"type":"text-start","id":"text"}',
|
|
404
|
+
"",
|
|
405
|
+
'data: {"type":"text-delta","id":"text","delta":"hi"}',
|
|
406
|
+
"",
|
|
407
|
+
"",
|
|
408
|
+
].join("\n"),
|
|
409
|
+
),
|
|
410
|
+
);
|
|
411
|
+
await waitFor(() => expect(chat.status).toBe("streaming"));
|
|
412
|
+
streamController.enqueue(
|
|
413
|
+
new TextEncoder().encode(
|
|
414
|
+
'data: {"type":"text-end","id":"text"}\n\ndata: {"type":"finish"}\n\ndata: [DONE]\n\n',
|
|
415
|
+
),
|
|
416
|
+
);
|
|
417
|
+
streamController.close();
|
|
418
|
+
await send;
|
|
419
|
+
await waitFor(() => expect(chat.status).toBe("ready"));
|
|
420
|
+
expect(chat.resumeStream).not.toHaveBeenCalled();
|
|
421
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it("does not broadcast a stream id to another mounted thread", async () => {
|
|
425
|
+
const storage = createResumableSessionStorage({
|
|
426
|
+
key: "multi-thread-stream-id",
|
|
427
|
+
});
|
|
428
|
+
let streamController:
|
|
429
|
+
| ReadableStreamDefaultController<UIMessageChunk>
|
|
430
|
+
| undefined;
|
|
431
|
+
const transport = resumableTransport(storage);
|
|
432
|
+
transport.sendMessages = async () =>
|
|
433
|
+
new ReadableStream<UIMessageChunk>({
|
|
434
|
+
start(controller) {
|
|
435
|
+
streamController = controller;
|
|
436
|
+
controller.enqueue({ type: "start", messageId: "thread-b-message" });
|
|
437
|
+
controller.enqueue({ type: "text-start", id: "text" });
|
|
438
|
+
controller.enqueue({
|
|
439
|
+
type: "text-delta",
|
|
440
|
+
id: "text",
|
|
441
|
+
delta: "streaming",
|
|
442
|
+
});
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
mocks.state.threadId = "thread-a";
|
|
446
|
+
mocks.state.mainThreadId = "thread-a";
|
|
447
|
+
mountRuntime({ transport });
|
|
448
|
+
const threadA = observedChat().chat;
|
|
449
|
+
mocks.state.threadId = "thread-b";
|
|
450
|
+
mocks.state.mainThreadId = "thread-b";
|
|
451
|
+
mountRuntime({ transport });
|
|
452
|
+
const threadB = observedChat(1).chat;
|
|
453
|
+
const send = threadB.sendMessage({ text: "hi" });
|
|
454
|
+
await waitFor(() => expect(threadB.status).toBe("streaming"));
|
|
455
|
+
await act(() => storage.setStreamId("stream-b", "thread-b"));
|
|
456
|
+
if (!streamController) throw new Error("Missing real thread B stream");
|
|
457
|
+
streamController.enqueue({ type: "text-end", id: "text" });
|
|
458
|
+
streamController.enqueue({ type: "finish" });
|
|
459
|
+
streamController.close();
|
|
460
|
+
await send;
|
|
461
|
+
await waitFor(() => expect(threadB.status).toBe("ready"));
|
|
462
|
+
expect(threadA.resumeStream).not.toHaveBeenCalled();
|
|
463
|
+
expect(threadB.resumeStream).not.toHaveBeenCalled();
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it("resumes a persisted stream after reload changes the local thread id", async () => {
|
|
467
|
+
const storageKey = "reload-stream-id";
|
|
468
|
+
createResumableSessionStorage({ key: storageKey }).setStreamId(
|
|
469
|
+
"stream-a",
|
|
470
|
+
"__LOCALID_before_reload",
|
|
471
|
+
);
|
|
472
|
+
const storage = createResumableSessionStorage({ key: storageKey });
|
|
473
|
+
const transport = resumableTransport(storage);
|
|
474
|
+
mocks.state.threadId = "__LOCALID_background";
|
|
475
|
+
mocks.state.mainThreadId = "__LOCALID_after_reload";
|
|
476
|
+
mountRuntime({ transport });
|
|
477
|
+
const backgroundThread = observedChat().chat;
|
|
478
|
+
mocks.state.threadId = "__LOCALID_after_reload";
|
|
479
|
+
mountRuntime({ transport });
|
|
480
|
+
const mainThread = observedChat(1).chat;
|
|
481
|
+
expect(backgroundThread.resumeStream).not.toHaveBeenCalled();
|
|
482
|
+
await waitFor(() => expect(mainThread.resumeStream).toHaveBeenCalledOnce());
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("observes resumable storage from a replacement transport", async () => {
|
|
486
|
+
const storageA = observableStorage();
|
|
487
|
+
const storageB = observableStorage("stream-b");
|
|
488
|
+
const transportA = resumableTransport(storageA);
|
|
489
|
+
const transportB = resumableTransport(storageB);
|
|
490
|
+
const view = mountRuntime({ transport: transportA });
|
|
491
|
+
const resumeStream = observedChat().chat.resumeStream;
|
|
492
|
+
expect(resumeStream).not.toHaveBeenCalled();
|
|
493
|
+
await view.rerender({ transport: transportB });
|
|
494
|
+
await waitFor(() => expect(resumeStream).toHaveBeenCalledTimes(1));
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it("does not resume an unchanged stream id already handled by replacement storage", async () => {
|
|
498
|
+
mocks.state.isLoadingHistory = true;
|
|
499
|
+
const storageA = observableStorage("stream-1");
|
|
500
|
+
const storageB = observableStorage("stream-1");
|
|
501
|
+
getResumedStreamIds(storageB).add("stream-1");
|
|
502
|
+
const view = mountRuntime({ transport: resumableTransport(storageA) });
|
|
503
|
+
const resumeStream = observedChat().chat.resumeStream;
|
|
504
|
+
mocks.state.isLoadingHistory = false;
|
|
505
|
+
await view.rerender({ transport: resumableTransport(storageB) });
|
|
506
|
+
expect(resumeStream).not.toHaveBeenCalled();
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("does not clear a newer stream id when an older resume fails", async () => {
|
|
510
|
+
const storage = observableStorage("stream-1");
|
|
511
|
+
const error = new Error("resume failed");
|
|
512
|
+
let rejectFirstResume: ((error: Error) => void) | undefined;
|
|
513
|
+
const reconnect = vi
|
|
514
|
+
.fn<ChatTransport<UIMessage>["reconnectToStream"]>()
|
|
515
|
+
.mockImplementationOnce(
|
|
516
|
+
() =>
|
|
517
|
+
new Promise((_resolve, reject) => {
|
|
518
|
+
rejectFirstResume = reject;
|
|
519
|
+
}),
|
|
520
|
+
)
|
|
521
|
+
.mockResolvedValueOnce(null);
|
|
522
|
+
const onResumeError = vi.fn();
|
|
523
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
524
|
+
mountRuntime({
|
|
525
|
+
transport: resumableTransport(storage, reconnect),
|
|
526
|
+
onResumeError,
|
|
527
|
+
});
|
|
528
|
+
const resumeStream = observedChat().chat.resumeStream;
|
|
529
|
+
await waitFor(() => expect(resumeStream).toHaveBeenCalledTimes(1));
|
|
530
|
+
await act(() => storage.setStreamId("stream-2"));
|
|
531
|
+
await waitFor(() => expect(resumeStream).toHaveBeenCalledTimes(2));
|
|
532
|
+
const reject = rejectFirstResume;
|
|
533
|
+
if (!reject)
|
|
534
|
+
throw new Error("First real reconnect request was not pending");
|
|
535
|
+
await act(() => reject(error));
|
|
536
|
+
await waitFor(() => expect(onResumeError).toHaveBeenCalledWith(error));
|
|
537
|
+
expect(storage.getStreamId()).toBe("stream-2");
|
|
538
|
+
expect(storage.clear).not.toHaveBeenCalled();
|
|
539
|
+
warn.mockRestore();
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it.each([undefined, false, true])(
|
|
543
|
+
"keeps failed automatic checkpoints only when canResume is %s",
|
|
544
|
+
async (canResume) => {
|
|
545
|
+
const error = new Error("resume failed");
|
|
546
|
+
const clear = vi.fn();
|
|
547
|
+
const storage: ResumableClientStorage = {
|
|
548
|
+
getStreamId: () => "stream-1",
|
|
549
|
+
setStreamId: vi.fn(),
|
|
550
|
+
clear,
|
|
551
|
+
};
|
|
552
|
+
const onResumeError = vi.fn();
|
|
553
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
554
|
+
mountRuntime({
|
|
555
|
+
transport: resumableTransport(storage, async () => {
|
|
556
|
+
throw error;
|
|
557
|
+
}),
|
|
558
|
+
onResumeError,
|
|
559
|
+
canResume,
|
|
560
|
+
});
|
|
561
|
+
const resumeStream = observedChat().chat.resumeStream;
|
|
562
|
+
await waitFor(() => expect(onResumeError).toHaveBeenCalledWith(error));
|
|
563
|
+
expect(resumeStream).toHaveBeenCalledTimes(1);
|
|
564
|
+
if (canResume) {
|
|
565
|
+
expect(clear).not.toHaveBeenCalled();
|
|
566
|
+
} else {
|
|
567
|
+
expect(clear).toHaveBeenCalledOnce();
|
|
568
|
+
expect(onResumeError.mock.invocationCallOrder[0]).toBeLessThan(
|
|
569
|
+
clear.mock.invocationCallOrder[0]!,
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
expect(warn).toHaveBeenCalledWith(
|
|
573
|
+
"[assistant-ui] resumable: resume failed",
|
|
574
|
+
error,
|
|
575
|
+
);
|
|
576
|
+
warn.mockRestore();
|
|
577
|
+
},
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
it.each([undefined, false, true])(
|
|
581
|
+
"applies the canResume=%s checkpoint policy even when onResumeError throws",
|
|
582
|
+
async (canResume) => {
|
|
583
|
+
const error = new Error("resume failed");
|
|
584
|
+
const callbackError = new Error("callback failed");
|
|
585
|
+
const clear = vi.fn();
|
|
586
|
+
const storage: ResumableClientStorage = {
|
|
587
|
+
getStreamId: () => "stream-1",
|
|
588
|
+
setStreamId: vi.fn(),
|
|
589
|
+
clear,
|
|
590
|
+
};
|
|
591
|
+
const onResumeError = vi.fn(() => {
|
|
592
|
+
throw callbackError;
|
|
593
|
+
});
|
|
594
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
595
|
+
const consoleError = vi
|
|
596
|
+
.spyOn(console, "error")
|
|
597
|
+
.mockImplementation(() => {});
|
|
598
|
+
mountRuntime({
|
|
599
|
+
transport: resumableTransport(storage, async () => {
|
|
600
|
+
throw error;
|
|
601
|
+
}),
|
|
602
|
+
onResumeError,
|
|
603
|
+
canResume,
|
|
604
|
+
});
|
|
605
|
+
await waitFor(() => expect(onResumeError).toHaveBeenCalledWith(error));
|
|
606
|
+
if (canResume) {
|
|
607
|
+
expect(clear).not.toHaveBeenCalled();
|
|
608
|
+
} else {
|
|
609
|
+
expect(clear).toHaveBeenCalledOnce();
|
|
610
|
+
}
|
|
611
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
612
|
+
"[assistant-ui] resumable: onResumeError callback failed",
|
|
613
|
+
callbackError,
|
|
614
|
+
);
|
|
615
|
+
warn.mockRestore();
|
|
616
|
+
consoleError.mockRestore();
|
|
617
|
+
},
|
|
618
|
+
);
|
|
619
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { UIMessage } from "ai";
|
|
4
|
+
import type { AssistantRuntime } from "@ulearning/ai-core";
|
|
5
|
+
import { useRemoteThreadListRuntime } from "@ulearning/ai-core/vue";
|
|
6
|
+
import { useAui } from "@ulearning/ai-store";
|
|
7
|
+
import type { ChatTransport } from "ai";
|
|
8
|
+
import { useMemo, useState } from "@ulearning/ai-tap";
|
|
9
|
+
import { AssistantChatTransport } from "../transport/AssistantChatTransport";
|
|
10
|
+
import { useChatThread, type ChatThreadOptions } from "./useChatThread";
|
|
11
|
+
import { useDynamicChatTransport } from "./useDynamicChatTransport";
|
|
12
|
+
import { useHostDestroySignal } from "@ulearning/ai-store/internal";
|
|
13
|
+
import { isResourceExecution } from "@ulearning/ai-tap/internal";
|
|
14
|
+
import { toValue, type MaybeRefOrGetter } from "vue";
|
|
15
|
+
import { useRuntimeResource } from "@ulearning/ai-core/vue/internal";
|
|
16
|
+
import { useSyncExternalStore } from "@ulearning/ai-tap";
|
|
17
|
+
import { InMemoryThreadListAdapter } from "@ulearning/ai-core/internal";
|
|
18
|
+
|
|
19
|
+
export type UseChatRuntimeOptions<UI_MESSAGE extends UIMessage = UIMessage> =
|
|
20
|
+
Omit<ChatThreadOptions<UI_MESSAGE>, "transport"> & {
|
|
21
|
+
/**
|
|
22
|
+
* The transport threads send through. `AssistantChatTransport` instances
|
|
23
|
+
* are cloned per thread through `__internal_clone()` so their assistant-ui
|
|
24
|
+
* wiring remains isolated. Other transport instances are shared as-is.
|
|
25
|
+
*/
|
|
26
|
+
transport?: ChatTransport<UI_MESSAGE> | undefined;
|
|
27
|
+
onThreadIdChange?: ((threadId: string | undefined) => void) | undefined;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const useChatThreadRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
31
|
+
options: ChatThreadOptions<UI_MESSAGE> | undefined,
|
|
32
|
+
hostDestroySignal: AbortSignal,
|
|
33
|
+
): AssistantRuntime => {
|
|
34
|
+
const aui = useAui();
|
|
35
|
+
const id = useSyncExternalStore(
|
|
36
|
+
aui.subscribe,
|
|
37
|
+
() => aui.threadListItem.getState().id,
|
|
38
|
+
);
|
|
39
|
+
const isMainThread = useSyncExternalStore(
|
|
40
|
+
aui.subscribe,
|
|
41
|
+
() => aui.threads.getState().mainThreadId === id,
|
|
42
|
+
);
|
|
43
|
+
return useChatThread(options, {
|
|
44
|
+
id,
|
|
45
|
+
isMainThread,
|
|
46
|
+
getThreadListItem: () =>
|
|
47
|
+
aui.threadListItem.source ? aui.threadListItem : undefined,
|
|
48
|
+
stopOnClientDestroy: true,
|
|
49
|
+
hostDestroySignal,
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const useChatRuntimeResource = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
54
|
+
{ onThreadIdChange, ...options }: UseChatRuntimeOptions<UI_MESSAGE>,
|
|
55
|
+
hostDestroySignal: AbortSignal,
|
|
56
|
+
): AssistantRuntime => {
|
|
57
|
+
const [adapter] = useState(() => new InMemoryThreadListAdapter());
|
|
58
|
+
const fallback = useMemo(() => new AssistantChatTransport<UI_MESSAGE>(), []);
|
|
59
|
+
const transport = useDynamicChatTransport(options.transport ?? fallback);
|
|
60
|
+
return useRemoteThreadListRuntime({
|
|
61
|
+
runtimeHook: function RuntimeHook() {
|
|
62
|
+
return useChatThreadRuntime({ ...options, transport }, hostDestroySignal);
|
|
63
|
+
},
|
|
64
|
+
adapter,
|
|
65
|
+
allowNesting: true,
|
|
66
|
+
onThreadIdChange,
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** Live Vue options are evaluated inside the owned resource tree. */
|
|
71
|
+
export const useChatRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
72
|
+
options: MaybeRefOrGetter<UseChatRuntimeOptions<UI_MESSAGE>> = {},
|
|
73
|
+
): AssistantRuntime => {
|
|
74
|
+
const hostDestroySignal = useHostDestroySignal();
|
|
75
|
+
if (isResourceExecution())
|
|
76
|
+
return useChatRuntimeResource(toValue(options), hostDestroySignal);
|
|
77
|
+
return useRuntimeResource(() =>
|
|
78
|
+
useChatRuntimeResource(toValue(options), hostDestroySignal),
|
|
79
|
+
);
|
|
80
|
+
};
|