@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.
Files changed (119) hide show
  1. package/dist/converters/convertMessage.d.ts +8 -6
  2. package/dist/converters/convertMessage.d.ts.map +1 -1
  3. package/dist/converters/convertMessage.js +48 -22
  4. package/dist/converters/convertMessage.js.map +1 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/index.native.d.ts +1 -1
  9. package/dist/index.native.d.ts.map +1 -1
  10. package/dist/index.native.js +1 -1
  11. package/dist/model-context/injectInteractableContext.d.ts +3 -0
  12. package/dist/model-context/injectInteractableContext.d.ts.map +1 -1
  13. package/dist/model-context/injectInteractableContext.js +3 -0
  14. package/dist/model-context/injectInteractableContext.js.map +1 -1
  15. package/dist/model-context/injectQuoteContext.d.ts +1 -0
  16. package/dist/model-context/injectQuoteContext.d.ts.map +1 -1
  17. package/dist/model-context/injectQuoteContext.js +1 -0
  18. package/dist/model-context/injectQuoteContext.js.map +1 -1
  19. package/dist/runtime/AISDKChat.js +1 -1
  20. package/dist/runtime/AISDKThreads.d.ts +5 -0
  21. package/dist/runtime/AISDKThreads.d.ts.map +1 -1
  22. package/dist/runtime/AISDKThreads.js +37 -4
  23. package/dist/runtime/AISDKThreads.js.map +1 -1
  24. package/dist/runtime/DynamicChatTransport.d.ts +49 -0
  25. package/dist/runtime/DynamicChatTransport.d.ts.map +1 -0
  26. package/dist/runtime/DynamicChatTransport.js +147 -0
  27. package/dist/runtime/DynamicChatTransport.js.map +1 -0
  28. package/dist/runtime/getResumableAdapter.d.ts +5 -0
  29. package/dist/runtime/getResumableAdapter.d.ts.map +1 -0
  30. package/dist/runtime/getResumableAdapter.js +12 -0
  31. package/dist/runtime/getResumableAdapter.js.map +1 -0
  32. package/dist/runtime/sdkIdentity.js +1 -1
  33. package/dist/runtime/toolHistoryCodec.d.ts +20 -0
  34. package/dist/runtime/toolHistoryCodec.d.ts.map +1 -0
  35. package/dist/runtime/toolHistoryCodec.js +107 -0
  36. package/dist/runtime/toolHistoryCodec.js.map +1 -0
  37. package/dist/runtime/useAISDKRuntime.d.ts +4 -1
  38. package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
  39. package/dist/runtime/useAISDKRuntime.js +154 -155
  40. package/dist/runtime/useAISDKRuntime.js.map +1 -1
  41. package/dist/runtime/useChatRuntime.d.ts +14 -2
  42. package/dist/runtime/useChatRuntime.d.ts.map +1 -1
  43. package/dist/runtime/useChatRuntime.js +12 -3
  44. package/dist/runtime/useChatRuntime.js.map +1 -1
  45. package/dist/runtime/useChatThread.d.ts +3 -2
  46. package/dist/runtime/useChatThread.d.ts.map +1 -1
  47. package/dist/runtime/useChatThread.js +56 -36
  48. package/dist/runtime/useChatThread.js.map +1 -1
  49. package/dist/runtime/useDynamicChatTransport.d.ts +4 -0
  50. package/dist/runtime/useDynamicChatTransport.d.ts.map +1 -0
  51. package/dist/runtime/useDynamicChatTransport.js +64 -0
  52. package/dist/runtime/useDynamicChatTransport.js.map +1 -0
  53. package/dist/runtime/useExternalHistory.d.ts.map +1 -1
  54. package/dist/runtime/useExternalHistory.js +11 -109
  55. package/dist/runtime/useExternalHistory.js.map +1 -1
  56. package/dist/runtime/useResourceCleanup.js +1 -1
  57. package/dist/runtime/useStreamingTiming.js +2 -2
  58. package/dist/runtime/useStreamingTiming.js.map +1 -1
  59. package/dist/tools/generativeTools.d.ts +2 -1
  60. package/dist/tools/generativeTools.d.ts.map +1 -1
  61. package/dist/tools/generativeTools.js +5 -5
  62. package/dist/tools/generativeTools.js.map +1 -1
  63. package/dist/usage.js +1 -1
  64. package/dist/utils/sliceMessagesUntil.d.ts.map +1 -1
  65. package/dist/utils/sliceMessagesUntil.js +1 -2
  66. package/dist/utils/sliceMessagesUntil.js.map +1 -1
  67. package/package.json +11 -10
  68. package/src/converters/convertMessage.test.ts +120 -2
  69. package/src/converters/convertMessage.tool-args-status.test.tsx +185 -0
  70. package/src/converters/convertMessage.ts +60 -19
  71. package/src/converters/toCreateMessage.test.ts +27 -0
  72. package/src/index.native.ts +1 -1
  73. package/src/index.ts +1 -1
  74. package/src/model-context/injectInteractableContext.ts +3 -0
  75. package/src/model-context/injectQuoteContext.ts +1 -0
  76. package/src/runtime/AISDKChat.integration.test.tsx +57 -2
  77. package/src/runtime/AISDKThreads.cloud.test.ts +3 -0
  78. package/src/runtime/AISDKThreads.test.ts +181 -0
  79. package/src/runtime/AISDKThreads.ts +31 -4
  80. package/src/runtime/DynamicChatTransport.test.ts +203 -0
  81. package/src/runtime/DynamicChatTransport.ts +273 -0
  82. package/src/runtime/__tests__/controlled-transport.ts +3 -0
  83. package/src/runtime/getResumableAdapter.ts +16 -0
  84. package/src/runtime/toolHistoryCodec.test.ts +161 -0
  85. package/src/runtime/toolHistoryCodec.ts +207 -0
  86. package/src/runtime/useAISDKRuntime.approval.test.tsx +12 -0
  87. package/src/runtime/useAISDKRuntime.reload.test.tsx +219 -0
  88. package/src/runtime/useAISDKRuntime.test.ts +538 -5
  89. package/src/runtime/useAISDKRuntime.ts +161 -51
  90. package/src/runtime/useChatRuntime.integration.test.tsx +319 -4
  91. package/src/runtime/useChatRuntime.local-storage.test.tsx +123 -0
  92. package/src/runtime/useChatRuntime.test.ts +107 -1
  93. package/src/runtime/useChatRuntime.ts +26 -4
  94. package/src/runtime/useChatThread.binding.test.tsx +143 -0
  95. package/src/runtime/useChatThread.ts +98 -81
  96. package/src/runtime/useDynamicChatTransport.ts +26 -0
  97. package/src/runtime/useExternalHistory.test.ts +111 -0
  98. package/src/runtime/useExternalHistory.ts +14 -210
  99. package/src/runtime/useStreamingTiming.ts +2 -2
  100. package/src/tools/generativeTools.test.ts +1 -1
  101. package/src/tools/generativeTools.ts +5 -6
  102. package/src/utils/sliceMessagesUntil.test.ts +2 -6
  103. package/src/utils/sliceMessagesUntil.ts +1 -5
  104. package/dist/converters/modelContentEnvelope.d.ts +0 -14
  105. package/dist/converters/modelContentEnvelope.d.ts.map +0 -1
  106. package/dist/converters/modelContentEnvelope.js +0 -22
  107. package/dist/converters/modelContentEnvelope.js.map +0 -1
  108. package/dist/converters/toolOutputConversion.d.ts +0 -26
  109. package/dist/converters/toolOutputConversion.d.ts.map +0 -1
  110. package/dist/converters/toolOutputConversion.js +0 -31
  111. package/dist/converters/toolOutputConversion.js.map +0 -1
  112. package/dist/tools/frontendTools.d.ts +0 -30
  113. package/dist/tools/frontendTools.d.ts.map +0 -1
  114. package/dist/tools/frontendTools.js +0 -33
  115. package/dist/tools/frontendTools.js.map +0 -1
  116. package/src/converters/modelContentEnvelope.ts +0 -41
  117. package/src/converters/toolOutputConversion.ts +0 -26
  118. package/src/tools/frontendTools.test.ts +0 -205
  119. 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 } = options ?? {};
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({ cloud, sdk: AI_SDK_SDK });
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
+ });