@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
|
@@ -55,7 +55,188 @@ const chatCallbacks = () => ({
|
|
|
55
55
|
sendAutomaticallyWhen: vi.fn(() => false),
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
const thread = (id: string) => ({
|
|
59
|
+
id,
|
|
60
|
+
title: id,
|
|
61
|
+
is_archived: false,
|
|
62
|
+
last_message_at: null,
|
|
63
|
+
external_id: null,
|
|
64
|
+
metadata: null,
|
|
65
|
+
});
|
|
66
|
+
const makeCloud = (id: string) =>
|
|
67
|
+
({
|
|
68
|
+
registerSdk: vi.fn(),
|
|
69
|
+
threads: {
|
|
70
|
+
list: vi.fn(async ({ is_archived }: { is_archived?: boolean } = {}) => ({
|
|
71
|
+
threads: is_archived ? [] : [thread(id)],
|
|
72
|
+
})),
|
|
73
|
+
get: vi.fn(async () => thread(id)),
|
|
74
|
+
messages: {
|
|
75
|
+
list: vi.fn(async () => ({ messages: [] })),
|
|
76
|
+
create: vi.fn(async () => ({ message_id: "stored" })),
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
runs: { report: vi.fn() },
|
|
80
|
+
telemetry: { enabled: false },
|
|
81
|
+
}) as unknown as AssistantCloud;
|
|
82
|
+
|
|
58
83
|
describe("AISDKThreads", () => {
|
|
84
|
+
it.each(
|
|
85
|
+
(["client", "scope", "both", "rapid"] as const).flatMap((change) =>
|
|
86
|
+
(["uncontrolled", "changed", "unchanged"] as const).map((selection) => ({
|
|
87
|
+
change,
|
|
88
|
+
selection,
|
|
89
|
+
})),
|
|
90
|
+
),
|
|
91
|
+
)(
|
|
92
|
+
"reloads Cloud threads when $change changes with selection=$selection",
|
|
93
|
+
async ({ change, selection }) => {
|
|
94
|
+
const cloudA = makeCloud("a-thread");
|
|
95
|
+
const cloudB = makeCloud("b-thread");
|
|
96
|
+
const intermediate = makeCloud("intermediate-thread");
|
|
97
|
+
const stream = createCancellableTransport();
|
|
98
|
+
let cloud = cloudA;
|
|
99
|
+
let scopeId = "workspace-a";
|
|
100
|
+
let threadId = selection === "uncontrolled" ? undefined : "a-thread";
|
|
101
|
+
const onThreadIdChange = vi.fn();
|
|
102
|
+
const { handle, rerender } = createLiveHandle(() => ({
|
|
103
|
+
cloud,
|
|
104
|
+
scopeId,
|
|
105
|
+
threadId,
|
|
106
|
+
onThreadIdChange,
|
|
107
|
+
transport: () => stream.transport,
|
|
108
|
+
}));
|
|
109
|
+
try {
|
|
110
|
+
await handle.getClient().threads.getLoadThreadsPromise();
|
|
111
|
+
await vi.waitFor(() =>
|
|
112
|
+
expect(handle.getClient().threads.getState().threadIds).toContain(
|
|
113
|
+
"a-thread",
|
|
114
|
+
),
|
|
115
|
+
);
|
|
116
|
+
if (selection === "uncontrolled") {
|
|
117
|
+
flushTapSync(() =>
|
|
118
|
+
handle.getClient().threads.switchToThread("a-thread"),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
await vi.waitFor(() =>
|
|
122
|
+
expect(handle.getClient().threads.getState().mainThreadId).toBe(
|
|
123
|
+
"a-thread",
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
await vi.waitFor(() =>
|
|
127
|
+
expect(handle.getClient().thread.getState().isLoading).toBe(false),
|
|
128
|
+
);
|
|
129
|
+
flushTapSync(() => handle.getClient().composer.setText("A only"));
|
|
130
|
+
flushTapSync(() => handle.getClient().composer.send());
|
|
131
|
+
await vi.waitFor(() =>
|
|
132
|
+
expect(handle.getClient().thread.getState().isRunning).toBe(true),
|
|
133
|
+
);
|
|
134
|
+
stream.emit(
|
|
135
|
+
{ type: "start" },
|
|
136
|
+
{ type: "text-start", id: "partial" },
|
|
137
|
+
{ type: "text-delta", id: "partial", delta: "A response" },
|
|
138
|
+
);
|
|
139
|
+
await vi.waitFor(() =>
|
|
140
|
+
expect(threadText(handle)).toContain("A response"),
|
|
141
|
+
);
|
|
142
|
+
const callsBeforeScopeChange = vi.mocked(cloudA.threads.list).mock.calls
|
|
143
|
+
.length;
|
|
144
|
+
rerender();
|
|
145
|
+
expect(cloudA.threads.list).toHaveBeenCalledTimes(
|
|
146
|
+
callsBeforeScopeChange,
|
|
147
|
+
);
|
|
148
|
+
const writesBeforeScopeChange = vi.mocked(
|
|
149
|
+
cloudA.threads.messages.create,
|
|
150
|
+
).mock.calls.length;
|
|
151
|
+
const fetchesBeforeScopeChange = vi.mocked(cloudA.threads.get).mock
|
|
152
|
+
.calls.length;
|
|
153
|
+
if (change === "rapid") {
|
|
154
|
+
cloud = intermediate;
|
|
155
|
+
scopeId = "workspace-intermediate";
|
|
156
|
+
rerender();
|
|
157
|
+
}
|
|
158
|
+
if (change === "scope") {
|
|
159
|
+
vi.mocked(cloudA.threads.list).mockImplementation(
|
|
160
|
+
makeCloud("b-thread").threads.list,
|
|
161
|
+
);
|
|
162
|
+
} else {
|
|
163
|
+
cloud = cloudB;
|
|
164
|
+
}
|
|
165
|
+
if (change !== "client") scopeId = "workspace-b";
|
|
166
|
+
if (selection === "changed") threadId = "b-thread";
|
|
167
|
+
rerender();
|
|
168
|
+
rerender();
|
|
169
|
+
await vi.waitFor(() =>
|
|
170
|
+
expect(handle.getClient().threads.getState().threadIds).toContain(
|
|
171
|
+
"b-thread",
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
expect(handle.getClient().threads.getState().threadIds).not.toContain(
|
|
175
|
+
"a-thread",
|
|
176
|
+
);
|
|
177
|
+
await vi.waitFor(() => expect(stream.getCancelCount()).toBe(1));
|
|
178
|
+
expect(cloudB.threads.messages.create).not.toHaveBeenCalled();
|
|
179
|
+
expect(cloudA.threads.messages.create).toHaveBeenCalledTimes(
|
|
180
|
+
writesBeforeScopeChange,
|
|
181
|
+
);
|
|
182
|
+
if (change === "rapid") {
|
|
183
|
+
expect(intermediate.threads.list).not.toHaveBeenCalled();
|
|
184
|
+
expect(handle.getClient().threads.getState().threadIds).not.toContain(
|
|
185
|
+
"intermediate-thread",
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
if (selection === "changed") {
|
|
189
|
+
await vi.waitFor(() =>
|
|
190
|
+
expect(handle.getClient().threads.getState().mainThreadId).toBe(
|
|
191
|
+
"b-thread",
|
|
192
|
+
),
|
|
193
|
+
);
|
|
194
|
+
expect(onThreadIdChange).not.toHaveBeenCalled();
|
|
195
|
+
} else if (selection === "unchanged") {
|
|
196
|
+
expect(
|
|
197
|
+
handle.getClient().threads.item("main").getState().status,
|
|
198
|
+
).toBe("new");
|
|
199
|
+
expect(onThreadIdChange).toHaveBeenCalledExactlyOnceWith(undefined);
|
|
200
|
+
const fetches = vi
|
|
201
|
+
.mocked(cloud.threads.get)
|
|
202
|
+
.mock.calls.slice(
|
|
203
|
+
change === "scope" ? fetchesBeforeScopeChange : 0,
|
|
204
|
+
);
|
|
205
|
+
expect(fetches).not.toContainEqual(["a-thread"]);
|
|
206
|
+
}
|
|
207
|
+
} finally {
|
|
208
|
+
handle.destroy();
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
it("loads once when entering Cloud mode", async () => {
|
|
214
|
+
let cloud: AssistantCloud | undefined;
|
|
215
|
+
const { handle, rerender } = createLiveHandle(() => ({ cloud }));
|
|
216
|
+
try {
|
|
217
|
+
cloud = makeCloud("a-thread");
|
|
218
|
+
rerender();
|
|
219
|
+
await vi.waitFor(() =>
|
|
220
|
+
expect(handle.getClient().threads.getState().threadIds).toContain(
|
|
221
|
+
"a-thread",
|
|
222
|
+
),
|
|
223
|
+
);
|
|
224
|
+
expect(cloud.threads.list).toHaveBeenCalledTimes(2);
|
|
225
|
+
cloud = undefined;
|
|
226
|
+
rerender();
|
|
227
|
+
cloud = makeCloud("b-thread");
|
|
228
|
+
rerender();
|
|
229
|
+
await vi.waitFor(() =>
|
|
230
|
+
expect(handle.getClient().threads.getState().threadIds).toContain(
|
|
231
|
+
"b-thread",
|
|
232
|
+
),
|
|
233
|
+
);
|
|
234
|
+
expect(cloud.threads.list).toHaveBeenCalledTimes(2);
|
|
235
|
+
} finally {
|
|
236
|
+
handle.destroy();
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
59
240
|
it("emits the selected thread id when its composer sends", async () => {
|
|
60
241
|
const { transport } = createControlledTransport();
|
|
61
242
|
const handle = createAssistantClient(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { resource, useResource, withKey } from "@assistant-ui/tap";
|
|
4
|
-
import { useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
5
5
|
import type { Chat, UIMessage } from "@ai-sdk/react";
|
|
6
6
|
import type { ChatTransport } from "ai";
|
|
7
7
|
import type { AssistantCloud } from "assistant-cloud";
|
|
@@ -49,6 +49,11 @@ export type AISDKThreadsOptions<UI_MESSAGE extends UIMessage = UIMessage> =
|
|
|
49
49
|
* switch and stops on delete; per-thread history loads once per thread.
|
|
50
50
|
*/
|
|
51
51
|
cloud?: AssistantCloud | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* Stable identity for the account or workspace owning Cloud runtime state.
|
|
54
|
+
* Provide it from the first render and change it when that scope changes.
|
|
55
|
+
*/
|
|
56
|
+
scopeId?: string | undefined;
|
|
52
57
|
/**
|
|
53
58
|
* Controlled thread id for the cloud list. Ignored without `cloud`.
|
|
54
59
|
*/
|
|
@@ -61,7 +66,7 @@ export type AISDKThreadsOptions<UI_MESSAGE extends UIMessage = UIMessage> =
|
|
|
61
66
|
|
|
62
67
|
type AISDKThreadChatOptions<UI_MESSAGE extends UIMessage = UIMessage> = Omit<
|
|
63
68
|
AISDKThreadsOptions<UI_MESSAGE>,
|
|
64
|
-
"cloud" | "threadId" | "onThreadIdChange"
|
|
69
|
+
"cloud" | "scopeId" | "threadId" | "onThreadIdChange"
|
|
65
70
|
>;
|
|
66
71
|
|
|
67
72
|
type ChatOptionsRef<UI_MESSAGE extends UIMessage> = {
|
|
@@ -182,7 +187,8 @@ const AISDKChatThread = resource(useAISDKChatThread);
|
|
|
182
187
|
const useAISDKThreads = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
183
188
|
options?: AISDKThreadsOptions<UI_MESSAGE>,
|
|
184
189
|
) => {
|
|
185
|
-
const { cloud, threadId, onThreadIdChange, ...threadOptions } =
|
|
190
|
+
const { cloud, scopeId, threadId, onThreadIdChange, ...threadOptions } =
|
|
191
|
+
options ?? {};
|
|
186
192
|
const [chats] = useState(() => new Map<string, ChatEntry<UI_MESSAGE>>());
|
|
187
193
|
const bindCloud = cloud !== undefined;
|
|
188
194
|
|
|
@@ -198,7 +204,28 @@ const useAISDKThreads = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
198
204
|
}
|
|
199
205
|
});
|
|
200
206
|
|
|
201
|
-
const cloudAdapter = useCloudThreadListAdapter({
|
|
207
|
+
const cloudAdapter = useCloudThreadListAdapter({
|
|
208
|
+
cloud,
|
|
209
|
+
scopeId,
|
|
210
|
+
sdk: AI_SDK_SDK,
|
|
211
|
+
});
|
|
212
|
+
const clientRef = useAssistantClientRef();
|
|
213
|
+
const previousCloudAdapter = useRef(bindCloud ? cloudAdapter : undefined);
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
const previous = previousCloudAdapter.current;
|
|
216
|
+
previousCloudAdapter.current = bindCloud ? cloudAdapter : undefined;
|
|
217
|
+
if (!bindCloud || previous === undefined || previous === cloudAdapter)
|
|
218
|
+
return;
|
|
219
|
+
// Wait until RemoteThreadList commits the replacement adapter before
|
|
220
|
+
// requesting its explicit reload.
|
|
221
|
+
let cancelled = false;
|
|
222
|
+
queueMicrotask(() => {
|
|
223
|
+
if (!cancelled) void clientRef.current!.threads.reload();
|
|
224
|
+
});
|
|
225
|
+
return () => {
|
|
226
|
+
cancelled = true;
|
|
227
|
+
};
|
|
228
|
+
}, [bindCloud, clientRef, cloudAdapter]);
|
|
202
229
|
const thread = (id: string) => {
|
|
203
230
|
const element = AISDKChatThread({
|
|
204
231
|
threadId: id,
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { AssistantRuntime } from "@assistant-ui/core";
|
|
2
|
+
import type { UIMessage } from "@ai-sdk/react";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { AssistantChatTransport } from "../transport/AssistantChatTransport";
|
|
5
|
+
import { DynamicChatTransport } from "./DynamicChatTransport";
|
|
6
|
+
|
|
7
|
+
const emptyStreamResponse = () =>
|
|
8
|
+
new Response(
|
|
9
|
+
new ReadableStream({ start: (controller) => controller.close() }),
|
|
10
|
+
{
|
|
11
|
+
headers: { "content-type": "text/event-stream" },
|
|
12
|
+
},
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const sendMessagesOptions = (chatId: string) => ({
|
|
16
|
+
trigger: "submit-message" as const,
|
|
17
|
+
chatId,
|
|
18
|
+
messageId: undefined,
|
|
19
|
+
messages: [
|
|
20
|
+
{
|
|
21
|
+
id: `message-${chatId}`,
|
|
22
|
+
role: "user" as const,
|
|
23
|
+
parts: [{ type: "text" as const, text: "hello" }],
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
abortSignal: undefined,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const createRuntime = (system: string) =>
|
|
30
|
+
({
|
|
31
|
+
thread: {
|
|
32
|
+
getModelContext: () => ({ system }),
|
|
33
|
+
},
|
|
34
|
+
}) as AssistantRuntime;
|
|
35
|
+
|
|
36
|
+
describe("DynamicChatTransport", () => {
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
vi.restoreAllMocks();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("rejects sends without a registered thread context", () => {
|
|
42
|
+
const dynamicTransport = new DynamicChatTransport(
|
|
43
|
+
new AssistantChatTransport<UIMessage>(),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
expect(() =>
|
|
47
|
+
dynamicTransport.sendMessages(sendMessagesOptions("missing-thread")),
|
|
48
|
+
).toThrow(
|
|
49
|
+
'DynamicChatTransport has no registered context for chat "missing-thread"',
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("keeps AssistantChatTransport wiring scoped per thread", async () => {
|
|
54
|
+
const createTransport = () => {
|
|
55
|
+
const bodies: Array<{ id: string; system: string }> = [];
|
|
56
|
+
const fetch = vi.fn(async (_input, init) => {
|
|
57
|
+
bodies.push(JSON.parse(String(init?.body)));
|
|
58
|
+
return emptyStreamResponse();
|
|
59
|
+
});
|
|
60
|
+
return {
|
|
61
|
+
bodies,
|
|
62
|
+
fetch,
|
|
63
|
+
transport: new AssistantChatTransport<UIMessage>({
|
|
64
|
+
fetch,
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
const initial = createTransport();
|
|
69
|
+
const dynamicTransport = new DynamicChatTransport(initial.transport);
|
|
70
|
+
const ownerA = {};
|
|
71
|
+
const ownerB = {};
|
|
72
|
+
|
|
73
|
+
dynamicTransport.setThreadContext(
|
|
74
|
+
"thread-a",
|
|
75
|
+
ownerA,
|
|
76
|
+
createRuntime("system-a"),
|
|
77
|
+
() => ({
|
|
78
|
+
initialize: async () => ({
|
|
79
|
+
remoteId: "remote-a",
|
|
80
|
+
externalId: undefined,
|
|
81
|
+
}),
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
dynamicTransport.setThreadContext(
|
|
85
|
+
"thread-b",
|
|
86
|
+
ownerB,
|
|
87
|
+
createRuntime("system-b"),
|
|
88
|
+
() => ({
|
|
89
|
+
initialize: async () => ({
|
|
90
|
+
remoteId: "remote-b",
|
|
91
|
+
externalId: undefined,
|
|
92
|
+
}),
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
await dynamicTransport.sendMessages(sendMessagesOptions("thread-a"));
|
|
97
|
+
await dynamicTransport.sendMessages(sendMessagesOptions("thread-b"));
|
|
98
|
+
|
|
99
|
+
const replacement = createTransport();
|
|
100
|
+
dynamicTransport.setTransport(replacement.transport);
|
|
101
|
+
await dynamicTransport.sendMessages(sendMessagesOptions("thread-a"));
|
|
102
|
+
await dynamicTransport.sendMessages(sendMessagesOptions("thread-b"));
|
|
103
|
+
|
|
104
|
+
expect(initial.fetch).toHaveBeenCalledTimes(2);
|
|
105
|
+
expect(replacement.fetch).toHaveBeenCalledTimes(2);
|
|
106
|
+
expect(initial.bodies).toEqual([
|
|
107
|
+
expect.objectContaining({ id: "remote-a", system: "system-a" }),
|
|
108
|
+
expect.objectContaining({ id: "remote-b", system: "system-b" }),
|
|
109
|
+
]);
|
|
110
|
+
expect(replacement.bodies).toEqual([
|
|
111
|
+
expect.objectContaining({ id: "remote-a", system: "system-a" }),
|
|
112
|
+
expect.objectContaining({ id: "remote-b", system: "system-b" }),
|
|
113
|
+
]);
|
|
114
|
+
expect(dynamicTransport.getCurrentTransport("thread-a")).not.toBe(
|
|
115
|
+
dynamicTransport.getCurrentTransport("thread-b"),
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("defers replacement clones until a thread uses the transport", async () => {
|
|
120
|
+
const clone = vi.spyOn(
|
|
121
|
+
AssistantChatTransport.prototype,
|
|
122
|
+
"__internal_clone",
|
|
123
|
+
);
|
|
124
|
+
const dynamicTransport = new DynamicChatTransport(
|
|
125
|
+
new AssistantChatTransport<UIMessage>(),
|
|
126
|
+
);
|
|
127
|
+
const owner = {};
|
|
128
|
+
dynamicTransport.setThreadContext(
|
|
129
|
+
"thread-a",
|
|
130
|
+
owner,
|
|
131
|
+
createRuntime("system-a"),
|
|
132
|
+
() => ({
|
|
133
|
+
initialize: async () => ({
|
|
134
|
+
remoteId: "remote-a",
|
|
135
|
+
externalId: undefined,
|
|
136
|
+
}),
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
clone.mockClear();
|
|
140
|
+
|
|
141
|
+
for (let index = 0; index < 10; index++) {
|
|
142
|
+
dynamicTransport.setTransport(
|
|
143
|
+
new AssistantChatTransport<UIMessage>({
|
|
144
|
+
fetch: async () => emptyStreamResponse(),
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
dynamicTransport.setThreadContext(
|
|
149
|
+
"thread-a",
|
|
150
|
+
owner,
|
|
151
|
+
createRuntime("updated-system-a"),
|
|
152
|
+
() => ({
|
|
153
|
+
initialize: async () => ({
|
|
154
|
+
remoteId: "updated-remote-a",
|
|
155
|
+
externalId: undefined,
|
|
156
|
+
}),
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(clone).not.toHaveBeenCalled();
|
|
161
|
+
|
|
162
|
+
await dynamicTransport.sendMessages(sendMessagesOptions("thread-a"));
|
|
163
|
+
|
|
164
|
+
expect(clone).toHaveBeenCalledOnce();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("retries a replacement clone after cloning throws", async () => {
|
|
168
|
+
const initialFetch = vi.fn(async () => emptyStreamResponse());
|
|
169
|
+
const dynamicTransport = new DynamicChatTransport(
|
|
170
|
+
new AssistantChatTransport<UIMessage>({ fetch: initialFetch }),
|
|
171
|
+
);
|
|
172
|
+
dynamicTransport.setThreadContext(
|
|
173
|
+
"thread-a",
|
|
174
|
+
{},
|
|
175
|
+
createRuntime("system-a"),
|
|
176
|
+
() => ({
|
|
177
|
+
initialize: async () => ({
|
|
178
|
+
remoteId: "remote-a",
|
|
179
|
+
externalId: undefined,
|
|
180
|
+
}),
|
|
181
|
+
}),
|
|
182
|
+
);
|
|
183
|
+
await dynamicTransport.sendMessages(sendMessagesOptions("thread-a"));
|
|
184
|
+
|
|
185
|
+
const replacement = new AssistantChatTransport<UIMessage>();
|
|
186
|
+
const cloneError = new Error("clone failed");
|
|
187
|
+
const clone = vi
|
|
188
|
+
.spyOn(replacement, "__internal_clone")
|
|
189
|
+
.mockImplementation(() => {
|
|
190
|
+
throw cloneError;
|
|
191
|
+
});
|
|
192
|
+
dynamicTransport.setTransport(replacement);
|
|
193
|
+
|
|
194
|
+
expect(() =>
|
|
195
|
+
dynamicTransport.sendMessages(sendMessagesOptions("thread-a")),
|
|
196
|
+
).toThrow(cloneError);
|
|
197
|
+
expect(() =>
|
|
198
|
+
dynamicTransport.sendMessages(sendMessagesOptions("thread-a")),
|
|
199
|
+
).toThrow(cloneError);
|
|
200
|
+
expect(clone).toHaveBeenCalledTimes(2);
|
|
201
|
+
expect(initialFetch).toHaveBeenCalledOnce();
|
|
202
|
+
});
|
|
203
|
+
});
|