@assistant-ui/ai-sdk 0.0.2 → 0.0.4

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 (54) hide show
  1. package/README.md +1 -1
  2. package/dist/converters/convertMessage.d.ts.map +1 -1
  3. package/dist/converters/convertMessage.js +23 -2
  4. package/dist/converters/convertMessage.js.map +1 -1
  5. package/dist/converters/toCreateMessage.d.ts.map +1 -1
  6. package/dist/converters/toCreateMessage.js +6 -2
  7. package/dist/converters/toCreateMessage.js.map +1 -1
  8. package/dist/model-context/injectInteractableContext.d.ts +1 -1
  9. package/dist/model-context/injectInteractableContext.js +1 -1
  10. package/dist/model-context/injectInteractableContext.js.map +1 -1
  11. package/dist/model-context/injectQuoteContext.d.ts +1 -1
  12. package/dist/model-context/injectQuoteContext.js +1 -1
  13. package/dist/model-context/injectQuoteContext.js.map +1 -1
  14. package/dist/runtime/AISDKChat.js +1 -1
  15. package/dist/runtime/AISDKThreads.d.ts +10 -8
  16. package/dist/runtime/AISDKThreads.d.ts.map +1 -1
  17. package/dist/runtime/AISDKThreads.js +34 -27
  18. package/dist/runtime/AISDKThreads.js.map +1 -1
  19. package/dist/runtime/useAISDKRuntime.d.ts +18 -3
  20. package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
  21. package/dist/runtime/useAISDKRuntime.js +199 -73
  22. package/dist/runtime/useAISDKRuntime.js.map +1 -1
  23. package/dist/runtime/useChatRuntime.js +1 -1
  24. package/dist/runtime/useChatThread.d.ts +14 -1
  25. package/dist/runtime/useChatThread.d.ts.map +1 -1
  26. package/dist/runtime/useChatThread.js +11 -4
  27. package/dist/runtime/useChatThread.js.map +1 -1
  28. package/dist/runtime/useExternalHistory.d.ts.map +1 -1
  29. package/dist/runtime/useExternalHistory.js +52 -47
  30. package/dist/runtime/useExternalHistory.js.map +1 -1
  31. package/dist/runtime/useResourceCleanup.js +1 -1
  32. package/dist/usage.d.ts +1 -2
  33. package/dist/usage.d.ts.map +1 -1
  34. package/dist/usage.js +5 -7
  35. package/dist/usage.js.map +1 -1
  36. package/package.json +12 -12
  37. package/src/converters/convertMessage.test.ts +22 -0
  38. package/src/converters/convertMessage.ts +26 -2
  39. package/src/converters/toCreateMessage.ts +6 -5
  40. package/src/model-context/injectInteractableContext.ts +1 -1
  41. package/src/model-context/injectQuoteContext.ts +1 -1
  42. package/src/runtime/AISDKThreads.cloud.test.ts +11 -1
  43. package/src/runtime/AISDKThreads.test.ts +102 -0
  44. package/src/runtime/AISDKThreads.ts +16 -9
  45. package/src/runtime/useAISDKRuntime.denied-tool.test.tsx +122 -0
  46. package/src/runtime/useAISDKRuntime.test.ts +479 -10
  47. package/src/runtime/useAISDKRuntime.ts +199 -55
  48. package/src/runtime/useChatRuntime.integration.test.tsx +46 -0
  49. package/src/runtime/useChatRuntime.test.ts +16 -0
  50. package/src/runtime/useChatThread.ts +26 -0
  51. package/src/runtime/useExternalHistory.test.ts +143 -1
  52. package/src/runtime/useExternalHistory.ts +53 -56
  53. package/src/usage.test.ts +26 -8
  54. package/src/usage.ts +4 -9
@@ -1,6 +1,13 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useMemo, useRef, useState } from "react";
3
+ import {
4
+ useCallback,
5
+ useEffect,
6
+ useInsertionEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ } from "react";
4
11
  import type {
5
12
  UIMessage,
6
13
  useChat,
@@ -35,7 +42,10 @@ import {
35
42
  getExternalStoreMessages,
36
43
  pickExternalStoreSharedOptions,
37
44
  } from "@assistant-ui/core";
38
- import { consumeSuggestionResult } from "@assistant-ui/core/internal";
45
+ import {
46
+ consumeSuggestionResult,
47
+ MessageRepository,
48
+ } from "@assistant-ui/core/internal";
39
49
  import type { ReadonlyJSONObject } from "assistant-stream/utils";
40
50
  import { sliceMessagesUntil } from "../utils/sliceMessagesUntil";
41
51
  import { toCreateMessage } from "../converters/toCreateMessage";
@@ -70,48 +80,63 @@ const toUIMessage = <UI_MESSAGE extends UIMessage>(
70
80
  role: createMessage.role ?? fallbackRole,
71
81
  }) as UI_MESSAGE;
72
82
 
73
- export type AISDKRuntimeAdapter = ExternalStoreSharedOptions & {
74
- adapters?:
75
- | (NonNullable<ExternalStoreAdapter["adapters"]> & {
76
- history?: ThreadHistoryAdapter | undefined;
77
- suggestion?: SuggestionAdapter | undefined;
78
- })
79
- | undefined;
80
- toCreateMessage?: CustomToCreateMessageFunction;
81
- /**
82
- * Whether to automatically cancel pending interactive tool calls when the user sends a new message.
83
- *
84
- * When enabled (default), the pending tool calls will be marked as failed with an error message
85
- * indicating the user cancelled the tool call by sending a new message.
86
- *
87
- * @default true
88
- */
89
- cancelPendingToolCallsOnSend?: boolean | undefined;
90
- /**
91
- * Called when `runtime.thread.resumeRun(config)` is invoked.
92
- *
93
- * When omitted, `resumeRun` throws `"Runtime does not support resuming runs."`.
94
- * Provide this to bridge resume invocations into a custom replay channel
95
- * (for example, an SSE reconnect endpoint keyed by turn id).
96
- */
97
- onResume?: ExternalStoreAdapter["onResume"];
98
- /**
99
- * Called when `runtime.thread.resumeToolCall(options)` is invoked for a tool call the in-process tracker does not own.
100
- *
101
- * When omitted, `resumeToolCall` throws `"Tool call ${toolCallId} is not waiting for resume."`.
102
- * Provide this to bridge resume-tool-call invocations into a custom handler.
103
- */
104
- onResumeToolCall?: ExternalStoreAdapter["onResumeToolCall"];
105
- /**
106
- * How consecutive assistant messages are rendered.
107
- *
108
- * `"concat-content"` (the default) merges them into a single thread message.
109
- * `"none"` keeps each assistant message as its own thread message, which is
110
- * useful when a backend persists proactive or consecutive assistant messages
111
- * as separate entries.
112
- */
113
- joinStrategy?: JoinStrategy | undefined;
114
- };
83
+ export type AISDKRuntimeAdapter<UI_MESSAGE extends UIMessage = UIMessage> =
84
+ ExternalStoreSharedOptions & {
85
+ adapters?:
86
+ | (NonNullable<ExternalStoreAdapter["adapters"]> & {
87
+ history?: ThreadHistoryAdapter | undefined;
88
+ suggestion?: SuggestionAdapter | undefined;
89
+ })
90
+ | undefined;
91
+ toCreateMessage?: CustomToCreateMessageFunction;
92
+ unstable_messageRepositoryInstance?: MessageRepository | undefined;
93
+ /**
94
+ * Whether to automatically cancel pending interactive tool calls when the user sends a new message.
95
+ *
96
+ * When enabled (default), the pending tool calls will be marked as failed with an error message
97
+ * indicating the user cancelled the tool call by sending a new message.
98
+ *
99
+ * @default true
100
+ */
101
+ cancelPendingToolCallsOnSend?: boolean | undefined;
102
+ /**
103
+ * Called when `runtime.thread.resumeRun(config)` is invoked.
104
+ *
105
+ * When omitted, `resumeRun` throws `"Runtime does not support resuming runs."`.
106
+ * Provide this to bridge resume invocations into a custom replay channel
107
+ * (for example, an SSE reconnect endpoint keyed by turn id).
108
+ */
109
+ onResume?: ExternalStoreAdapter["onResume"];
110
+ /**
111
+ * Called when `runtime.thread.resumeToolCall(options)` is invoked for a tool call the in-process tracker does not own.
112
+ *
113
+ * When omitted, `resumeToolCall` throws `"Tool call ${toolCallId} is not waiting for resume."`.
114
+ * Provide this to bridge resume-tool-call invocations into a custom handler.
115
+ */
116
+ onResumeToolCall?: ExternalStoreAdapter["onResumeToolCall"];
117
+ /**
118
+ * How consecutive assistant messages are rendered.
119
+ *
120
+ * `"concat-content"` (the default) merges them into a single thread message.
121
+ * `"none"` keeps each assistant message as its own thread message, which is
122
+ * useful when a backend persists proactive or consecutive assistant messages
123
+ * as separate entries.
124
+ */
125
+ joinStrategy?: JoinStrategy | undefined;
126
+ /**
127
+ * A branch-aware AI SDK message tree seeded once when `useChat` is empty.
128
+ * After that seed, live updates come only from `useChat`. A later empty
129
+ * chat or a new object identity does not reload the tree.
130
+ */
131
+ messageRepository?: MessageFormatRepository<UI_MESSAGE>;
132
+ /**
133
+ * Called after an explicit `switchToBranch` (for example a BranchPicker
134
+ * click). Complements `setMessages` and does not enable switching by itself.
135
+ *
136
+ * @deprecated This API is still under active development and might change without notice.
137
+ */
138
+ unstable_onBranchChange?: ExternalStoreAdapter["unstable_onBranchChange"];
139
+ };
115
140
 
116
141
  const EMPTY_SUGGESTIONS: readonly ThreadSuggestion[] = [];
117
142
 
@@ -125,9 +150,13 @@ const useGeneratedSuggestions = (
125
150
  const controllerRef = useRef<AbortController | null>(null);
126
151
  const wasRunningRef = useRef(false);
127
152
  const messagesRef = useRef(messages);
128
- messagesRef.current = messages;
153
+ useInsertionEffect(() => {
154
+ messagesRef.current = messages;
155
+ }, [messages]);
129
156
  const adapterRef = useRef(suggestionAdapter);
130
- adapterRef.current = suggestionAdapter;
157
+ useInsertionEffect(() => {
158
+ adapterRef.current = suggestionAdapter;
159
+ }, [suggestionAdapter]);
131
160
  const hasAdapter = suggestionAdapter != null;
132
161
 
133
162
  useEffect(() => {
@@ -188,9 +217,11 @@ const useGeneratedSuggestions = (
188
217
  return suggestions;
189
218
  };
190
219
 
220
+ const NO_CANCELLED_MESSAGE_IDS: ReadonlySet<string> = new Set();
221
+
191
222
  export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
192
223
  chatHelpers: ReturnType<typeof useChat<UI_MESSAGE>>,
193
- adapter: AISDKRuntimeAdapter = {},
224
+ adapter: AISDKRuntimeAdapter<UI_MESSAGE> = {},
194
225
  ) => {
195
226
  const {
196
227
  adapters,
@@ -199,12 +230,18 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
199
230
  onResume,
200
231
  onResumeToolCall,
201
232
  joinStrategy,
233
+ messageRepository,
234
+ unstable_onBranchChange,
202
235
  } = adapter;
203
236
  const suggestionAdapter = adapters?.suggestion;
204
237
  const contextAdapters = useRuntimeAdapters();
205
238
  const [toolStatuses, setToolStatuses] = useState<
206
239
  Record<string, ToolExecutionStatus>
207
240
  >({});
241
+ const [cancelledMessages, setCancelledMessages] = useState<{
242
+ chatId: string;
243
+ ids: ReadonlySet<string>;
244
+ } | null>(null);
208
245
  const toolArgsKeyOrderCacheRef = useRef<Map<string, Map<string, string[]>>>(
209
246
  new Map(),
210
247
  );
@@ -217,10 +254,10 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
217
254
  const hasExecutingTools = Object.values(toolStatuses).some(
218
255
  (s) => s?.type === "executing",
219
256
  );
220
- const isRunning =
221
- chatHelpers.status === "submitted" ||
222
- chatHelpers.status === "streaming" ||
223
- hasExecutingTools;
257
+ const providerIsRunning =
258
+ chatHelpers.status === "submitted" || chatHelpers.status === "streaming";
259
+ const isRunning = providerIsRunning || hasExecutingTools;
260
+ const wasProviderRunningRef = useRef(providerIsRunning);
224
261
 
225
262
  const messageTiming = useStreamingTiming(chatHelpers.messages, isRunning);
226
263
 
@@ -230,6 +267,42 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
230
267
  const optimisticMessageId =
231
268
  isRunning && lastMessage?.role === "assistant" ? lastMessage.id : undefined;
232
269
 
270
+ const cancelledMessageIds =
271
+ cancelledMessages?.chatId === chatHelpers.id
272
+ ? cancelledMessages.ids
273
+ : NO_CANCELLED_MESSAGE_IDS;
274
+
275
+ const retractCancellation = useCallback(
276
+ (chatId: string, messageId: string) => {
277
+ setCancelledMessages((prev) => {
278
+ if (prev?.chatId !== chatId || !prev.ids.has(messageId)) return prev;
279
+ const ids = new Set(prev.ids);
280
+ ids.delete(messageId);
281
+ return { chatId, ids };
282
+ });
283
+ },
284
+ [],
285
+ );
286
+
287
+ // A provider run that resumes the stopped response retracts its cancellation;
288
+ // a run that starts a new response leaves the stopped one marked.
289
+ const resumedMessageId =
290
+ providerIsRunning && lastMessage?.role === "assistant"
291
+ ? lastMessage.id
292
+ : undefined;
293
+
294
+ useEffect(() => {
295
+ const wasProviderRunning = wasProviderRunningRef.current;
296
+ wasProviderRunningRef.current = providerIsRunning;
297
+ if (wasProviderRunning || !resumedMessageId) return;
298
+ retractCancellation(chatHelpers.id, resumedMessageId);
299
+ }, [
300
+ providerIsRunning,
301
+ resumedMessageId,
302
+ chatHelpers.id,
303
+ retractCancellation,
304
+ ]);
305
+
233
306
  const messages = AISDKMessageConverter.useThreadMessages({
234
307
  isRunning,
235
308
  messages: chatHelpers.messages,
@@ -243,11 +316,29 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
243
316
  mcpAppMetadataCache: mcpAppMetadataCacheRef.current,
244
317
  ...(optimisticMessageId && { optimisticMessageId }),
245
318
  ...(chatHelpers.error && { error: chatHelpers.error.message }),
319
+ ...(cancelledMessageIds.size > 0 && { cancelledMessageIds }),
246
320
  }),
247
- [toolStatuses, messageTiming, optimisticMessageId, chatHelpers.error],
321
+ [
322
+ toolStatuses,
323
+ messageTiming,
324
+ optimisticMessageId,
325
+ chatHelpers.error,
326
+ cancelledMessageIds,
327
+ ],
248
328
  ),
249
329
  });
250
330
 
331
+ const exportedMessageRepository = useMemo(() => {
332
+ if (!messageRepository) return undefined;
333
+ const converted = toExportedMessageRepository(
334
+ AISDKMessageConverter.toThreadMessages as (
335
+ messages: UI_MESSAGE[],
336
+ ) => ThreadMessage[],
337
+ messageRepository,
338
+ );
339
+ return converted.messages.length > 0 ? converted : undefined;
340
+ }, [messageRepository]);
341
+
251
342
  const generatedSuggestions = useGeneratedSuggestions(
252
343
  suggestionAdapter,
253
344
  messages,
@@ -306,7 +397,11 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
306
397
  let hasChanges = false;
307
398
  const parts = lastMessage.parts?.map((part) => {
308
399
  if (!isToolUIPart(part)) return part;
309
- if (part.state === "output-available" || part.state === "output-error")
400
+ if (
401
+ part.state === "output-available" ||
402
+ part.state === "output-error" ||
403
+ part.state === "output-denied"
404
+ )
310
405
  return part;
311
406
 
312
407
  hasChanges = true;
@@ -323,9 +418,17 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
323
418
  });
324
419
  };
325
420
 
421
+ const hasSeededRepositoryRef = useRef(false);
422
+ const shouldFeedRepository =
423
+ exportedMessageRepository != null &&
424
+ !hasSeededRepositoryRef.current &&
425
+ messages.length === 0;
426
+
326
427
  const runtime = useExternalStoreRuntime({
327
- isRunning,
328
- messages,
428
+ isRunning: providerIsRunning,
429
+ ...(shouldFeedRepository
430
+ ? { messageRepository: exportedMessageRepository }
431
+ : { messages }),
329
432
  unstable_enableToolInvocations: true,
330
433
  setToolStatuses,
331
434
  setMessages: (messages) =>
@@ -391,10 +494,27 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
391
494
  runtimeRef.current.thread.import(exportedRepo);
392
495
  },
393
496
  onCancel: async () => {
497
+ const message = chatHelpers.messages.at(-1);
498
+ const cancelledId =
499
+ isRunning && message?.role === "assistant" ? message.id : undefined;
500
+ if (cancelledId) {
501
+ const liveIds = new Set(chatHelpers.messages.map((m) => m.id));
502
+ setCancelledMessages((prev) => {
503
+ const kept =
504
+ prev?.chatId === chatHelpers.id
505
+ ? [...prev.ids].filter((id) => liveIds.has(id))
506
+ : [];
507
+ return {
508
+ chatId: chatHelpers.id,
509
+ ids: new Set([...kept, cancelledId]),
510
+ };
511
+ });
512
+ }
394
513
  try {
395
514
  await chatHelpers.stop();
396
515
  } catch (error) {
397
516
  if (!(error instanceof Error && error.name === "AbortError")) {
517
+ if (cancelledId) retractCancellation(chatHelpers.id, cancelledId);
398
518
  throw error;
399
519
  }
400
520
  }
@@ -508,9 +628,14 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
508
628
  }),
509
629
  ),
510
630
  ...pickExternalStoreSharedOptions(adapter),
631
+ ...(adapter.unstable_messageRepositoryInstance && {
632
+ unstable_messageRepositoryInstance:
633
+ adapter.unstable_messageRepositoryInstance,
634
+ }),
511
635
  ...(suggestionAdapter ? { suggestions: generatedSuggestions } : {}),
512
636
  ...(onResume && { onResume }),
513
637
  ...(onResumeToolCall && { onResumeToolCall }),
638
+ ...(unstable_onBranchChange && { unstable_onBranchChange }),
514
639
  adapters: {
515
640
  attachments: vercelAttachmentAdapter,
516
641
  ...contextAdapters,
@@ -520,5 +645,24 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
520
645
  isLoading,
521
646
  });
522
647
 
648
+ const setMessagesRef = useRef(chatHelpers.setMessages);
649
+ useInsertionEffect(() => {
650
+ setMessagesRef.current = chatHelpers.setMessages;
651
+ }, [chatHelpers.setMessages]);
652
+
653
+ useEffect(() => {
654
+ if (hasSeededRepositoryRef.current) return;
655
+ if (!exportedMessageRepository) return;
656
+ if (chatHelpers.messages.length > 0) {
657
+ hasSeededRepositoryRef.current = true;
658
+ return;
659
+ }
660
+ const tempRepo = new MessageRepository();
661
+ tempRepo.import(exportedMessageRepository);
662
+ setMessagesRef.current(
663
+ tempRepo.getMessages().flatMap(getExternalStoreMessages<UI_MESSAGE>),
664
+ );
665
+ hasSeededRepositoryRef.current = true;
666
+ }, [exportedMessageRepository, chatHelpers.messages.length]);
523
667
  return runtime;
524
668
  };
@@ -8,6 +8,7 @@ import { StrictMode, useState } from "react";
8
8
  import { describe, expect, it } from "vitest";
9
9
  import { AssistantChatTransport } from "../transport/AssistantChatTransport";
10
10
  import { useChatRuntime } from "./useChatRuntime";
11
+ import { useThreadTokenUsage } from "../usage";
11
12
 
12
13
  const messages: UIMessage[] = [
13
14
  {
@@ -65,3 +66,48 @@ describe("useChatRuntime integration", () => {
65
66
  });
66
67
  });
67
68
  });
69
+
70
+ const UsageProbe = () => {
71
+ const usage = useThreadTokenUsage();
72
+ return (
73
+ <output data-testid="total-tokens">{usage?.totalTokens ?? "none"}</output>
74
+ );
75
+ };
76
+
77
+ const UsageApp = () => {
78
+ const [transport] = useState(
79
+ () => new AssistantChatTransport({ api: "/api/chat" }),
80
+ );
81
+ const runtime = useChatRuntime({
82
+ messages: [
83
+ ...messages,
84
+ {
85
+ id: "assistant-with-usage",
86
+ role: "assistant",
87
+ parts: [{ type: "text", text: "Hi" }],
88
+ metadata: { usage: { inputTokens: 40, outputTokens: 2 } },
89
+ },
90
+ ],
91
+ transport,
92
+ });
93
+
94
+ return (
95
+ <AssistantRuntimeProvider runtime={runtime}>
96
+ <UsageProbe />
97
+ </AssistantRuntimeProvider>
98
+ );
99
+ };
100
+
101
+ describe("useThreadTokenUsage through useChatRuntime", () => {
102
+ it("reads usage from the message metadata a server attached", async () => {
103
+ render(
104
+ <StrictMode>
105
+ <UsageApp />
106
+ </StrictMode>,
107
+ );
108
+
109
+ await waitFor(() => {
110
+ expect(screen.getByTestId("total-tokens").textContent).toBe("42");
111
+ });
112
+ });
113
+ });
@@ -96,6 +96,22 @@ describe("useChatRuntime", () => {
96
96
  window.sessionStorage.clear();
97
97
  });
98
98
 
99
+ it("forwards a defined chat update throttle to useChat", () => {
100
+ mocks.useChat.mockReturnValue({
101
+ resumeStream: vi.fn(),
102
+ status: "ready",
103
+ });
104
+
105
+ renderHook(() => useChatRuntime({ throttle: 50 }));
106
+ renderHook(() => useChatRuntime({ throttle: undefined }));
107
+
108
+ expect(mocks.useChat).toHaveBeenNthCalledWith(
109
+ 1,
110
+ expect.objectContaining({ throttle: 50 }),
111
+ );
112
+ expect(mocks.useChat.mock.calls[1]?.[0]).not.toHaveProperty("throttle");
113
+ });
114
+
99
115
  it("waits for external history to load before resuming a stream", async () => {
100
116
  mocks.state.isLoadingHistory = true;
101
117
  const resumeStream = vi.fn().mockResolvedValue(undefined);
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useChat, type Chat, type UIMessage } from "@ai-sdk/react";
4
+ import type { MessageRepository } from "@assistant-ui/core/internal";
4
5
  import {
5
6
  pickExternalStoreSharedOptions,
6
7
  type AssistantRuntime,
@@ -32,6 +33,7 @@ import { useResourceCleanup } from "./useResourceCleanup";
32
33
  export type ChatThreadOptions<UI_MESSAGE extends UIMessage = UIMessage> =
33
34
  ChatInit<UI_MESSAGE> &
34
35
  ExternalStoreSharedOptions & {
36
+ throttle?: number | undefined;
35
37
  adapters?: AISDKRuntimeAdapter["adapters"] | undefined;
36
38
  toCreateMessage?: CustomToCreateMessageFunction;
37
39
  onResume?: AISDKRuntimeAdapter["onResume"];
@@ -44,6 +46,8 @@ export type ChatThreadOptions<UI_MESSAGE extends UIMessage = UIMessage> =
44
46
  */
45
47
  onResumeError?: ((error: unknown) => void) | undefined;
46
48
  joinStrategy?: AISDKRuntimeAdapter["joinStrategy"];
49
+ messageRepository?: AISDKRuntimeAdapter<UI_MESSAGE>["messageRepository"];
50
+ unstable_onBranchChange?: AISDKRuntimeAdapter["unstable_onBranchChange"];
47
51
  };
48
52
 
49
53
  export type ChatThreadEnvironment<UI_MESSAGE extends UIMessage = UIMessage> = {
@@ -57,6 +61,12 @@ export type ChatThreadEnvironment<UI_MESSAGE extends UIMessage = UIMessage> = {
57
61
  * from the instance.
58
62
  */
59
63
  chat?: Chat<UI_MESSAGE> | undefined;
64
+ /**
65
+ * An externally owned per-thread message repository. Hosts that route
66
+ * multiple threads through one mounting pass a distinct instance per
67
+ * thread so histories and branches stay isolated.
68
+ */
69
+ messageRepositoryInstance?: MessageRepository | undefined;
60
70
  };
61
71
 
62
72
  const useDynamicChatTransport = <UI_MESSAGE extends UIMessage = UIMessage>(
@@ -122,6 +132,7 @@ export const splitChatThreadOptions = <UI_MESSAGE extends UIMessage>(
122
132
  const {
123
133
  adapters,
124
134
  transport,
135
+ throttle,
125
136
  toCreateMessage,
126
137
  isDisabled: _isDisabled,
127
138
  isSendDisabled: _isSendDisabled,
@@ -131,6 +142,8 @@ export const splitChatThreadOptions = <UI_MESSAGE extends UIMessage>(
131
142
  onResumeToolCall,
132
143
  onResumeError,
133
144
  joinStrategy,
145
+ messageRepository,
146
+ unstable_onBranchChange,
134
147
  ...chatInit
135
148
  } = options ?? {};
136
149
  // peel guard: any shared key left in `chatInit` collapses this to `never`
@@ -141,11 +154,14 @@ export const splitChatThreadOptions = <UI_MESSAGE extends UIMessage>(
141
154
  return {
142
155
  adapters,
143
156
  transport,
157
+ throttle,
144
158
  toCreateMessage,
145
159
  onResume,
146
160
  onResumeToolCall,
147
161
  onResumeError,
148
162
  joinStrategy,
163
+ messageRepository,
164
+ unstable_onBranchChange,
149
165
  chatInit,
150
166
  };
151
167
  };
@@ -157,11 +173,14 @@ export const useChatThread = <UI_MESSAGE extends UIMessage = UIMessage>(
157
173
  const {
158
174
  adapters,
159
175
  transport: transportOptions,
176
+ throttle,
160
177
  toCreateMessage,
161
178
  onResume,
162
179
  onResumeToolCall,
163
180
  onResumeError,
164
181
  joinStrategy,
182
+ messageRepository,
183
+ unstable_onBranchChange,
165
184
  chatInit: chatOptions,
166
185
  } = splitChatThreadOptions(options);
167
186
 
@@ -171,6 +190,7 @@ export const useChatThread = <UI_MESSAGE extends UIMessage = UIMessage>(
171
190
  getThreadListItem,
172
191
  stopOnClientDestroy = false,
173
192
  chat: externalChat,
193
+ messageRepositoryInstance,
174
194
  } = env;
175
195
 
176
196
  const defaultTransport = useMemo(() => new AssistantChatTransport(), []);
@@ -181,6 +201,7 @@ export const useChatThread = <UI_MESSAGE extends UIMessage = UIMessage>(
181
201
  ...chatOptions,
182
202
  id,
183
203
  transport,
204
+ ...(throttle !== undefined && { throttle }),
184
205
  ...(externalChat !== undefined && { chat: externalChat }),
185
206
  });
186
207
 
@@ -195,6 +216,11 @@ export const useChatThread = <UI_MESSAGE extends UIMessage = UIMessage>(
195
216
  ...(onResume && { onResume }),
196
217
  ...(onResumeToolCall && { onResumeToolCall }),
197
218
  ...(joinStrategy && { joinStrategy }),
219
+ ...(messageRepository && { messageRepository }),
220
+ ...(messageRepositoryInstance && {
221
+ unstable_messageRepositoryInstance: messageRepositoryInstance,
222
+ }),
223
+ ...(unstable_onBranchChange && { unstable_onBranchChange }),
198
224
  });
199
225
 
200
226
  if (sourceTransport instanceof AssistantChatTransport) {