@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.
- package/README.md +1 -1
- package/dist/converters/convertMessage.d.ts.map +1 -1
- package/dist/converters/convertMessage.js +23 -2
- package/dist/converters/convertMessage.js.map +1 -1
- package/dist/converters/toCreateMessage.d.ts.map +1 -1
- package/dist/converters/toCreateMessage.js +6 -2
- package/dist/converters/toCreateMessage.js.map +1 -1
- package/dist/model-context/injectInteractableContext.d.ts +1 -1
- package/dist/model-context/injectInteractableContext.js +1 -1
- package/dist/model-context/injectInteractableContext.js.map +1 -1
- package/dist/model-context/injectQuoteContext.d.ts +1 -1
- package/dist/model-context/injectQuoteContext.js +1 -1
- package/dist/model-context/injectQuoteContext.js.map +1 -1
- package/dist/runtime/AISDKChat.js +1 -1
- package/dist/runtime/AISDKThreads.d.ts +10 -8
- package/dist/runtime/AISDKThreads.d.ts.map +1 -1
- package/dist/runtime/AISDKThreads.js +34 -27
- package/dist/runtime/AISDKThreads.js.map +1 -1
- package/dist/runtime/useAISDKRuntime.d.ts +18 -3
- package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
- package/dist/runtime/useAISDKRuntime.js +199 -73
- package/dist/runtime/useAISDKRuntime.js.map +1 -1
- package/dist/runtime/useChatRuntime.js +1 -1
- package/dist/runtime/useChatThread.d.ts +14 -1
- package/dist/runtime/useChatThread.d.ts.map +1 -1
- package/dist/runtime/useChatThread.js +11 -4
- package/dist/runtime/useChatThread.js.map +1 -1
- package/dist/runtime/useExternalHistory.d.ts.map +1 -1
- package/dist/runtime/useExternalHistory.js +52 -47
- package/dist/runtime/useExternalHistory.js.map +1 -1
- package/dist/runtime/useResourceCleanup.js +1 -1
- package/dist/usage.d.ts +1 -2
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +5 -7
- package/dist/usage.js.map +1 -1
- package/package.json +12 -12
- package/src/converters/convertMessage.test.ts +22 -0
- package/src/converters/convertMessage.ts +26 -2
- package/src/converters/toCreateMessage.ts +6 -5
- package/src/model-context/injectInteractableContext.ts +1 -1
- package/src/model-context/injectQuoteContext.ts +1 -1
- package/src/runtime/AISDKThreads.cloud.test.ts +11 -1
- package/src/runtime/AISDKThreads.test.ts +102 -0
- package/src/runtime/AISDKThreads.ts +16 -9
- package/src/runtime/useAISDKRuntime.denied-tool.test.tsx +122 -0
- package/src/runtime/useAISDKRuntime.test.ts +479 -10
- package/src/runtime/useAISDKRuntime.ts +199 -55
- package/src/runtime/useChatRuntime.integration.test.tsx +46 -0
- package/src/runtime/useChatRuntime.test.ts +16 -0
- package/src/runtime/useChatThread.ts +26 -0
- package/src/runtime/useExternalHistory.test.ts +143 -1
- package/src/runtime/useExternalHistory.ts +53 -56
- package/src/usage.test.ts +26 -8
- package/src/usage.ts +4 -9
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
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 {
|
|
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 =
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
153
|
+
useInsertionEffect(() => {
|
|
154
|
+
messagesRef.current = messages;
|
|
155
|
+
}, [messages]);
|
|
129
156
|
const adapterRef = useRef(suggestionAdapter);
|
|
130
|
-
|
|
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
|
|
221
|
-
chatHelpers.status === "submitted" ||
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
[
|
|
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 (
|
|
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
|
-
|
|
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) {
|