@assistant-ui/ai-sdk 0.0.5 → 0.0.7
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/adapters/aiSDKFormatAdapter.d.ts +2 -8
- package/dist/adapters/aiSDKFormatAdapter.js +1 -25
- package/dist/adapters/vercelAttachmentAdapter.d.ts +1 -2
- package/dist/adapters/vercelAttachmentAdapter.d.ts.map +1 -1
- package/dist/aiSDKExtras.d.ts +2 -3
- package/dist/aiSDKExtras.d.ts.map +1 -1
- package/dist/converters/convertMessage.d.ts +5 -4
- package/dist/converters/convertMessage.d.ts.map +1 -1
- package/dist/converters/convertMessage.js +40 -3
- package/dist/converters/convertMessage.js.map +1 -1
- package/dist/converters/modelContentEnvelope.d.ts +4 -5
- package/dist/converters/modelContentEnvelope.d.ts.map +1 -1
- package/dist/converters/toCreateMessage.d.ts +1 -2
- package/dist/converters/toCreateMessage.d.ts.map +1 -1
- package/dist/converters/toolOutputConversion.d.ts +2 -3
- package/dist/converters/toolOutputConversion.d.ts.map +1 -1
- package/dist/hooks.d.ts +2 -3
- package/dist/hooks.d.ts.map +1 -1
- package/dist/model-context/injectInteractableContext.d.ts +1 -2
- package/dist/model-context/injectInteractableContext.d.ts.map +1 -1
- package/dist/model-context/injectQuoteContext.d.ts +1 -2
- package/dist/model-context/injectQuoteContext.d.ts.map +1 -1
- package/dist/runtime/AISDKChat.d.ts +2 -3
- package/dist/runtime/AISDKChat.d.ts.map +1 -1
- package/dist/runtime/AISDKThreads.d.ts +2 -3
- package/dist/runtime/AISDKThreads.d.ts.map +1 -1
- package/dist/runtime/AISDKThreads.js +87 -68
- package/dist/runtime/AISDKThreads.js.map +1 -1
- package/dist/runtime/sdkIdentity.d.ts +5 -0
- package/dist/runtime/sdkIdentity.d.ts.map +1 -0
- package/dist/runtime/sdkIdentity.js +9 -0
- package/dist/runtime/sdkIdentity.js.map +1 -0
- package/dist/runtime/useAISDKRuntime.d.ts +17 -5
- package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
- package/dist/runtime/useAISDKRuntime.js +103 -38
- package/dist/runtime/useAISDKRuntime.js.map +1 -1
- package/dist/runtime/useChatRuntime.d.ts +2 -3
- package/dist/runtime/useChatRuntime.d.ts.map +1 -1
- package/dist/runtime/useChatRuntime.js +5 -1
- package/dist/runtime/useChatRuntime.js.map +1 -1
- package/dist/runtime/useChatThread.d.ts +19 -5
- package/dist/runtime/useChatThread.d.ts.map +1 -1
- package/dist/runtime/useChatThread.js +31 -10
- package/dist/runtime/useChatThread.js.map +1 -1
- package/dist/runtime/useExternalHistory.d.ts +2 -3
- package/dist/runtime/useExternalHistory.d.ts.map +1 -1
- package/dist/runtime/useExternalHistory.js +31 -20
- package/dist/runtime/useExternalHistory.js.map +1 -1
- package/dist/runtime/useResourceCleanup.d.ts +1 -2
- package/dist/runtime/useResourceCleanup.d.ts.map +1 -1
- package/dist/runtime/useStreamingTiming.d.ts +2 -3
- package/dist/runtime/useStreamingTiming.d.ts.map +1 -1
- package/dist/tools/frontendTools.d.ts +3 -4
- package/dist/tools/frontendTools.d.ts.map +1 -1
- package/dist/tools/generativeTools.d.ts +5 -6
- package/dist/tools/generativeTools.d.ts.map +1 -1
- package/dist/tools/mcp-stdio.unsupported.d.ts +1 -2
- package/dist/tools/mcp-stdio.unsupported.d.ts.map +1 -1
- package/dist/transport/AssistantChatTransport.d.ts +3 -4
- package/dist/transport/AssistantChatTransport.d.ts.map +1 -1
- package/dist/transport/resumable.d.ts +4 -5
- package/dist/transport/resumable.d.ts.map +1 -1
- package/dist/usage.d.ts +4 -5
- package/dist/usage.d.ts.map +1 -1
- package/dist/utils/getVercelAIMessages.d.ts +1 -2
- package/dist/utils/getVercelAIMessages.d.ts.map +1 -1
- package/dist/utils/sliceMessagesUntil.d.ts +1 -2
- package/dist/utils/sliceMessagesUntil.d.ts.map +1 -1
- package/package.json +15 -14
- package/src/adapters/aiSDKFormatAdapter.ts +4 -41
- package/src/converters/convertMessage.test.ts +152 -0
- package/src/converters/convertMessage.ts +95 -5
- package/src/runtime/AISDKChat.test.ts +4 -5
- package/src/runtime/AISDKThreads.cloud.test.ts +12 -3
- package/src/runtime/AISDKThreads.test.ts +140 -13
- package/src/runtime/AISDKThreads.ts +23 -4
- package/src/runtime/sdkIdentity.ts +9 -0
- package/src/runtime/useAISDKRuntime.approval.integration.test.tsx +287 -0
- package/src/runtime/useAISDKRuntime.approval.test.tsx +257 -1
- package/src/runtime/useAISDKRuntime.test.ts +46 -4
- package/src/runtime/useAISDKRuntime.ts +164 -24
- package/src/runtime/useAISDKRuntime.voice.test.tsx +270 -0
- package/src/runtime/useChatRuntime.test.ts +72 -5
- package/src/runtime/useChatRuntime.ts +2 -1
- package/src/runtime/useChatThread.test.ts +74 -0
- package/src/runtime/useChatThread.ts +45 -6
- package/src/runtime/useExternalHistory.test.ts +75 -0
- package/src/runtime/useExternalHistory.ts +28 -11
- package/src/tools/generativeTools.test.ts +7 -1
- package/src/transport/AssistantChatTransport.test.ts +1 -9
- package/dist/adapters/aiSDKFormatAdapter.d.ts.map +0 -1
- package/dist/adapters/aiSDKFormatAdapter.js.map +0 -1
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "@assistant-ui/core/react";
|
|
10
10
|
import { useAui, useAuiState } from "@assistant-ui/store";
|
|
11
11
|
import { useChatThread, type ChatThreadOptions } from "./useChatThread";
|
|
12
|
+
import { AI_SDK_SDK } from "./sdkIdentity";
|
|
12
13
|
|
|
13
14
|
export type UseChatRuntimeOptions<UI_MESSAGE extends UIMessage = UIMessage> =
|
|
14
15
|
ChatThreadOptions<UI_MESSAGE> & {
|
|
@@ -38,7 +39,7 @@ export const useChatRuntime = <UI_MESSAGE extends UIMessage = UIMessage>({
|
|
|
38
39
|
onThreadIdChange,
|
|
39
40
|
...options
|
|
40
41
|
}: UseChatRuntimeOptions<UI_MESSAGE> = {}): AssistantRuntime => {
|
|
41
|
-
const cloudAdapter = useCloudThreadListAdapter({ cloud });
|
|
42
|
+
const cloudAdapter = useCloudThreadListAdapter({ cloud, sdk: AI_SDK_SDK });
|
|
42
43
|
return useRemoteThreadListRuntime({
|
|
43
44
|
runtimeHook: function RuntimeHook() {
|
|
44
45
|
return useChatThreadRuntime(options);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { resource, useResource, flushTapSync } from "@assistant-ui/tap";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import {
|
|
7
|
+
RuntimeAdapter,
|
|
8
|
+
runtimeAdapterTransformScopes,
|
|
9
|
+
} from "@assistant-ui/core/store";
|
|
10
|
+
import {
|
|
11
|
+
attachTransformScopes,
|
|
12
|
+
AuiConfig,
|
|
13
|
+
createAssistantClient,
|
|
14
|
+
} from "@assistant-ui/store/client";
|
|
15
|
+
import { useChatThread, type ChatThreadEnvironment } from "./useChatThread";
|
|
16
|
+
import {
|
|
17
|
+
createCancellableTransport,
|
|
18
|
+
nextTask,
|
|
19
|
+
} from "./__tests__/controlled-transport";
|
|
20
|
+
|
|
21
|
+
const createHost = (
|
|
22
|
+
env: Pick<ChatThreadEnvironment, "stopOnClientDestroy">,
|
|
23
|
+
) => {
|
|
24
|
+
const useHost = (options: Parameters<typeof useChatThread>[0]) => {
|
|
25
|
+
const [threadListItem] = useState(() => ({
|
|
26
|
+
initialize: async () => ({ remoteId: "main", externalId: undefined }),
|
|
27
|
+
}));
|
|
28
|
+
const runtime = useChatThread(options, {
|
|
29
|
+
id: "main",
|
|
30
|
+
isMainThread: true,
|
|
31
|
+
getThreadListItem: () => threadListItem,
|
|
32
|
+
...env,
|
|
33
|
+
});
|
|
34
|
+
return useResource(RuntimeAdapter(runtime));
|
|
35
|
+
};
|
|
36
|
+
attachTransformScopes(useHost, runtimeAdapterTransformScopes);
|
|
37
|
+
return resource(useHost);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const streamThenDestroy = async (
|
|
41
|
+
env: Pick<ChatThreadEnvironment, "stopOnClientDestroy">,
|
|
42
|
+
) => {
|
|
43
|
+
const { transport, getCancelCount, close } = createCancellableTransport();
|
|
44
|
+
const Host = createHost(env);
|
|
45
|
+
const handle = createAssistantClient(
|
|
46
|
+
AuiConfig({ threads: Host({ transport }) }),
|
|
47
|
+
);
|
|
48
|
+
handle.subscribe(() => {});
|
|
49
|
+
const aui = handle.getClient();
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
flushTapSync(() => aui.composer.setText("stop me"));
|
|
53
|
+
flushTapSync(() => aui.composer.send());
|
|
54
|
+
await vi.waitFor(() => {
|
|
55
|
+
expect(aui.thread.getState().isRunning).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
} finally {
|
|
58
|
+
handle.destroy();
|
|
59
|
+
}
|
|
60
|
+
await nextTask();
|
|
61
|
+
const cancelCount = getCancelCount();
|
|
62
|
+
if (cancelCount === 0) close();
|
|
63
|
+
return cancelCount;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
describe("useChatThread", () => {
|
|
67
|
+
it("stops an in-flight chat on client destroy when stopOnClientDestroy is omitted", async () => {
|
|
68
|
+
expect(await streamThenDestroy({})).toBe(1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("leaves an in-flight chat running on client destroy when stopOnClientDestroy is false", async () => {
|
|
72
|
+
expect(await streamThenDestroy({ stopOnClientDestroy: false })).toBe(0);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { Chat, useChat, type UIMessage } from "@ai-sdk/react";
|
|
4
4
|
import type { MessageRepository } from "@assistant-ui/core/internal";
|
|
5
5
|
import {
|
|
6
6
|
pickExternalStoreSharedOptions,
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
useEffect,
|
|
27
27
|
useMemo,
|
|
28
28
|
useRef,
|
|
29
|
+
useState,
|
|
29
30
|
useSyncExternalStore,
|
|
30
31
|
} from "react";
|
|
31
32
|
import { useResourceCleanup } from "./useResourceCleanup";
|
|
@@ -38,6 +39,7 @@ export type ChatThreadOptions<UI_MESSAGE extends UIMessage = UIMessage> =
|
|
|
38
39
|
toCreateMessage?: CustomToCreateMessageFunction;
|
|
39
40
|
onResume?: AISDKRuntimeAdapter["onResume"];
|
|
40
41
|
onResumeToolCall?: AISDKRuntimeAdapter["onResumeToolCall"];
|
|
42
|
+
onRespondToToolApproval?: AISDKRuntimeAdapter["onRespondToToolApproval"];
|
|
41
43
|
/**
|
|
42
44
|
* Called when an automatic resumable stream reconnect fails. Use this to
|
|
43
45
|
* surface a toast, report telemetry, or mark the thread as needing a
|
|
@@ -140,6 +142,7 @@ export const splitChatThreadOptions = <UI_MESSAGE extends UIMessage>(
|
|
|
140
142
|
suggestions: _suggestions,
|
|
141
143
|
onResume,
|
|
142
144
|
onResumeToolCall,
|
|
145
|
+
onRespondToToolApproval,
|
|
143
146
|
onResumeError,
|
|
144
147
|
joinStrategy,
|
|
145
148
|
messageRepository,
|
|
@@ -158,6 +161,7 @@ export const splitChatThreadOptions = <UI_MESSAGE extends UIMessage>(
|
|
|
158
161
|
toCreateMessage,
|
|
159
162
|
onResume,
|
|
160
163
|
onResumeToolCall,
|
|
164
|
+
onRespondToToolApproval,
|
|
161
165
|
onResumeError,
|
|
162
166
|
joinStrategy,
|
|
163
167
|
messageRepository,
|
|
@@ -166,6 +170,30 @@ export const splitChatThreadOptions = <UI_MESSAGE extends UIMessage>(
|
|
|
166
170
|
};
|
|
167
171
|
};
|
|
168
172
|
|
|
173
|
+
type ChatCallbacks<UI_MESSAGE extends UIMessage> = Pick<
|
|
174
|
+
ChatInit<UI_MESSAGE>,
|
|
175
|
+
"onToolCall" | "onData" | "onFinish" | "onError" | "sendAutomaticallyWhen"
|
|
176
|
+
>;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Constructs a `Chat` whose callbacks read the latest options through
|
|
180
|
+
* `callbacksRef`, the forwarding `useChat` applies only to a chat it
|
|
181
|
+
* constructs itself.
|
|
182
|
+
*/
|
|
183
|
+
export const createChat = <UI_MESSAGE extends UIMessage>(
|
|
184
|
+
init: ChatInit<UI_MESSAGE>,
|
|
185
|
+
callbacksRef: { readonly current: ChatCallbacks<UI_MESSAGE> | undefined },
|
|
186
|
+
): Chat<UI_MESSAGE> =>
|
|
187
|
+
new Chat<UI_MESSAGE>({
|
|
188
|
+
...init,
|
|
189
|
+
onToolCall: (arg) => callbacksRef.current?.onToolCall?.(arg),
|
|
190
|
+
onData: (arg) => callbacksRef.current?.onData?.(arg),
|
|
191
|
+
onFinish: (arg) => callbacksRef.current?.onFinish?.(arg),
|
|
192
|
+
onError: (arg) => callbacksRef.current?.onError?.(arg),
|
|
193
|
+
sendAutomaticallyWhen: (arg) =>
|
|
194
|
+
callbacksRef.current?.sendAutomaticallyWhen?.(arg) ?? false,
|
|
195
|
+
});
|
|
196
|
+
|
|
169
197
|
export const useChatThread = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
170
198
|
options: ChatThreadOptions<UI_MESSAGE> | undefined,
|
|
171
199
|
env: ChatThreadEnvironment<UI_MESSAGE>,
|
|
@@ -177,6 +205,7 @@ export const useChatThread = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
177
205
|
toCreateMessage,
|
|
178
206
|
onResume,
|
|
179
207
|
onResumeToolCall,
|
|
208
|
+
onRespondToToolApproval,
|
|
180
209
|
onResumeError,
|
|
181
210
|
joinStrategy,
|
|
182
211
|
messageRepository,
|
|
@@ -188,7 +217,7 @@ export const useChatThread = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
188
217
|
id,
|
|
189
218
|
isMainThread,
|
|
190
219
|
getThreadListItem,
|
|
191
|
-
stopOnClientDestroy =
|
|
220
|
+
stopOnClientDestroy = true,
|
|
192
221
|
chat: externalChat,
|
|
193
222
|
messageRepositoryInstance,
|
|
194
223
|
} = env;
|
|
@@ -197,12 +226,21 @@ export const useChatThread = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
197
226
|
const sourceTransport = transportOptions ?? defaultTransport;
|
|
198
227
|
const transport = useDynamicChatTransport(sourceTransport);
|
|
199
228
|
|
|
229
|
+
const latestChatOptionsRef = useRef(chatOptions);
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
latestChatOptionsRef.current = chatOptions;
|
|
232
|
+
});
|
|
233
|
+
// `useChat` stops a chat it constructs whenever it unmounts, and a
|
|
234
|
+
// resource's soft unmount runs that cleanup, so the thread owns its chat.
|
|
235
|
+
const [ownedChat] = useState(
|
|
236
|
+
() =>
|
|
237
|
+
externalChat ??
|
|
238
|
+
createChat({ ...chatOptions, id, transport }, latestChatOptionsRef),
|
|
239
|
+
);
|
|
240
|
+
|
|
200
241
|
const chat = useChat({
|
|
201
|
-
|
|
202
|
-
id,
|
|
203
|
-
transport,
|
|
242
|
+
chat: externalChat ?? ownedChat,
|
|
204
243
|
...(throttle !== undefined && { throttle }),
|
|
205
|
-
...(externalChat !== undefined && { chat: externalChat }),
|
|
206
244
|
});
|
|
207
245
|
|
|
208
246
|
useResourceCleanup(stopOnClientDestroy, () => {
|
|
@@ -215,6 +253,7 @@ export const useChatThread = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
215
253
|
...(toCreateMessage && { toCreateMessage }),
|
|
216
254
|
...(onResume && { onResume }),
|
|
217
255
|
...(onResumeToolCall && { onResumeToolCall }),
|
|
256
|
+
...(onRespondToToolApproval && { onRespondToToolApproval }),
|
|
218
257
|
...(joinStrategy && { joinStrategy }),
|
|
219
258
|
...(messageRepository && { messageRepository }),
|
|
220
259
|
...(messageRepositoryInstance && {
|
|
@@ -498,6 +498,52 @@ describe("useExternalHistory persistence", () => {
|
|
|
498
498
|
};
|
|
499
499
|
};
|
|
500
500
|
|
|
501
|
+
it("persists a message that lands while the thread is idle", async () => {
|
|
502
|
+
const { append, step } = createPersistenceHarness(false);
|
|
503
|
+
const message = createAssistantMessage(
|
|
504
|
+
{ type: "complete", reason: "stop" },
|
|
505
|
+
[{ id: "spoken-1", parts: ["spoken"] }],
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
await step({ messages: [message] });
|
|
509
|
+
|
|
510
|
+
await waitFor(() => expect(append).toHaveBeenCalledTimes(1));
|
|
511
|
+
expect(append).toHaveBeenCalledWith({
|
|
512
|
+
parentId: null,
|
|
513
|
+
message: { id: "spoken-1", parts: ["spoken"] },
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it("schedules one idle persistence attempt per landed message when append fails", async () => {
|
|
518
|
+
const { append, step, flush } = createPersistenceHarness(false);
|
|
519
|
+
append.mockRejectedValueOnce(new Error("offline"));
|
|
520
|
+
const first = createAssistantMessage(
|
|
521
|
+
{ type: "complete", reason: "stop" },
|
|
522
|
+
[{ id: "spoken-1", parts: ["first"] }],
|
|
523
|
+
"assistant-a",
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
await step({ messages: [first] });
|
|
527
|
+
await waitFor(() => expect(append).toHaveBeenCalledTimes(1));
|
|
528
|
+
await step({ messages: [first] });
|
|
529
|
+
await step({ messages: [first] });
|
|
530
|
+
await flush();
|
|
531
|
+
expect(append).toHaveBeenCalledTimes(1);
|
|
532
|
+
|
|
533
|
+
const second = createAssistantMessage(
|
|
534
|
+
{ type: "complete", reason: "stop" },
|
|
535
|
+
[{ id: "spoken-2", parts: ["second"] }],
|
|
536
|
+
"assistant-b",
|
|
537
|
+
);
|
|
538
|
+
await step({ messages: [first, second] });
|
|
539
|
+
await waitFor(() => expect(append).toHaveBeenCalledTimes(3));
|
|
540
|
+
expect(append.mock.calls.map(([item]) => item)).toEqual([
|
|
541
|
+
{ parentId: null, message: { id: "spoken-1", parts: ["first"] } },
|
|
542
|
+
{ parentId: null, message: { id: "spoken-1", parts: ["first"] } },
|
|
543
|
+
{ parentId: "spoken-1", message: { id: "spoken-2", parts: ["second"] } },
|
|
544
|
+
]);
|
|
545
|
+
});
|
|
546
|
+
|
|
501
547
|
it("persists a settled turn when the history adapter becomes active after it", async () => {
|
|
502
548
|
let listener: (() => void) | undefined;
|
|
503
549
|
let isRunning = false;
|
|
@@ -780,6 +826,35 @@ describe("useExternalHistory persistence", () => {
|
|
|
780
826
|
],
|
|
781
827
|
expect.any(Object),
|
|
782
828
|
);
|
|
829
|
+
expect(reportTelemetry.mock.calls[0]![1]).toMatchObject({
|
|
830
|
+
message: expect.objectContaining({
|
|
831
|
+
id: "assistant-a",
|
|
832
|
+
status: { type: "complete", reason: "stop" },
|
|
833
|
+
}),
|
|
834
|
+
});
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
it("reports a run that failed before any assistant message", async () => {
|
|
838
|
+
const { append, reportTelemetry, runCycle, flush } =
|
|
839
|
+
createPersistenceHarness(true);
|
|
840
|
+
const failed = createAssistantMessage(
|
|
841
|
+
{
|
|
842
|
+
type: "incomplete",
|
|
843
|
+
reason: "error",
|
|
844
|
+
error: { code: "AI_APICallError", message: "upstream failed" },
|
|
845
|
+
},
|
|
846
|
+
[],
|
|
847
|
+
);
|
|
848
|
+
|
|
849
|
+
await runCycle([failed]);
|
|
850
|
+
await flush();
|
|
851
|
+
|
|
852
|
+
expect(append).not.toHaveBeenCalled();
|
|
853
|
+
expect(reportTelemetry).toHaveBeenCalledTimes(1);
|
|
854
|
+
expect(reportTelemetry).toHaveBeenCalledWith(
|
|
855
|
+
[],
|
|
856
|
+
expect.objectContaining({ message: failed }),
|
|
857
|
+
);
|
|
783
858
|
});
|
|
784
859
|
|
|
785
860
|
it("restores deferred telemetry for reloaded paused messages", async () => {
|
|
@@ -50,6 +50,11 @@ const isAwaitingToolApproval = (message: ThreadMessage) =>
|
|
|
50
50
|
message.status?.type === "requires-action" &&
|
|
51
51
|
message.status.reason === "tool-calls";
|
|
52
52
|
|
|
53
|
+
const isTerminalMessage = (message: ThreadMessage) =>
|
|
54
|
+
message.status === undefined ||
|
|
55
|
+
message.status.type === "complete" ||
|
|
56
|
+
message.status.type === "incomplete";
|
|
57
|
+
|
|
53
58
|
const encodeContent = <TMessage>(
|
|
54
59
|
storageFormatAdapter: MessageFormatAdapter<TMessage, any>,
|
|
55
60
|
item: MessageFormatItem<TMessage>,
|
|
@@ -185,6 +190,14 @@ export const useExternalHistory = <TMessage>(
|
|
|
185
190
|
if (!formatAdapter) return;
|
|
186
191
|
const adapter = formatAdapter;
|
|
187
192
|
|
|
193
|
+
const idleScheduledInnerIds = new Set<string>();
|
|
194
|
+
const unpersistedInnerIds = (message: ThreadMessage) =>
|
|
195
|
+
isTerminalMessage(message)
|
|
196
|
+
? getExternalStoreMessages<TMessage>(message)
|
|
197
|
+
.map((innerMessage) => storageFormatAdapter.getId(innerMessage))
|
|
198
|
+
.filter((innerId) => !persistedInnerMessages.current.has(innerId))
|
|
199
|
+
: [];
|
|
200
|
+
|
|
188
201
|
const unsubscribe = runtimeRef.current.thread.subscribe(() => {
|
|
189
202
|
const threadState = runtimeRef.current.thread.getState();
|
|
190
203
|
const { isRunning } = threadState;
|
|
@@ -220,12 +233,16 @@ export const useExternalHistory = <TMessage>(
|
|
|
220
233
|
return;
|
|
221
234
|
}
|
|
222
235
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
236
|
+
if (wasRunning) {
|
|
237
|
+
// Record step boundary offset (synchronous for accuracy)
|
|
238
|
+
if (runStartRef.current != null) {
|
|
239
|
+
stepBoundariesRef.current.push(Date.now() - runStartRef.current);
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
const pending = threadState.messages.flatMap(unpersistedInnerIds);
|
|
243
|
+
if (pending.every((innerId) => idleScheduledInnerIds.has(innerId)))
|
|
244
|
+
return;
|
|
245
|
+
for (const innerId of pending) idleScheduledInnerIds.add(innerId);
|
|
229
246
|
}
|
|
230
247
|
|
|
231
248
|
// Debounce: wait one macrotask so agentic step flickers are absorbed
|
|
@@ -313,10 +330,7 @@ export const useExternalHistory = <TMessage>(
|
|
|
313
330
|
for (const message of messages) {
|
|
314
331
|
const innerMessages = getExternalStoreMessages<TMessage>(message);
|
|
315
332
|
|
|
316
|
-
const isTerminal =
|
|
317
|
-
message.status === undefined ||
|
|
318
|
-
message.status.type === "complete" ||
|
|
319
|
-
message.status.type === "incomplete";
|
|
333
|
+
const isTerminal = isTerminalMessage(message);
|
|
320
334
|
const isAwaitingToolCalls = isAwaitingToolApproval(message);
|
|
321
335
|
// A paused message's later content can only reach storage via update, so it is persisted early only when the adapter supports update.
|
|
322
336
|
const isReady =
|
|
@@ -371,7 +385,10 @@ export const useExternalHistory = <TMessage>(
|
|
|
371
385
|
|
|
372
386
|
if (deferredTelemetryIds.current.has(message.id) && isTerminal) {
|
|
373
387
|
deferredTelemetryIds.current.delete(message.id);
|
|
374
|
-
adapter.reportTelemetry?.(batchItems,
|
|
388
|
+
adapter.reportTelemetry?.(batchItems, {
|
|
389
|
+
...telemetryOptions,
|
|
390
|
+
message,
|
|
391
|
+
});
|
|
375
392
|
}
|
|
376
393
|
}
|
|
377
394
|
})
|
|
@@ -623,8 +623,14 @@ describe("AISDKToolkit", () => {
|
|
|
623
623
|
toolCallId: "call-docs-search",
|
|
624
624
|
messages: [],
|
|
625
625
|
};
|
|
626
|
+
const execute = toolSet.docs_search?.execute as
|
|
627
|
+
| ((
|
|
628
|
+
args: { query: string },
|
|
629
|
+
options: typeof executeOptions,
|
|
630
|
+
) => Promise<string>)
|
|
631
|
+
| undefined;
|
|
626
632
|
await expect(
|
|
627
|
-
|
|
633
|
+
execute?.({ query: "assistant-ui" }, executeOptions),
|
|
628
634
|
).resolves.toBe("docs result");
|
|
629
635
|
expect(docsExecute).toHaveBeenCalledWith(
|
|
630
636
|
{ query: "assistant-ui" },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { UIMessage } from "ai";
|
|
2
|
-
import {
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
3
|
|
|
4
4
|
import { RESUMABLE_STREAM_ID_HEADER } from "./resumable";
|
|
5
5
|
import { AssistantChatTransport } from "./AssistantChatTransport";
|
|
@@ -26,10 +26,6 @@ const sendMessagesOptions = {
|
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
describe("AssistantChatTransport.prepareSendMessagesRequest", () => {
|
|
29
|
-
beforeEach(() => {
|
|
30
|
-
vi.clearAllMocks();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
29
|
it("passes the initialized remote thread id to prepareSendMessagesRequest as options.id", async () => {
|
|
34
30
|
const threadListItem = createThreadListItem("remote-thread-id");
|
|
35
31
|
const captured: { id: unknown } = { id: undefined };
|
|
@@ -209,10 +205,6 @@ const wrappedFetchOf = (
|
|
|
209
205
|
).fetch;
|
|
210
206
|
|
|
211
207
|
describe("AssistantChatTransport resumable fetch wrapper", () => {
|
|
212
|
-
beforeEach(() => {
|
|
213
|
-
vi.clearAllMocks();
|
|
214
|
-
});
|
|
215
|
-
|
|
216
208
|
it("passes a 204 with a non-null empty body through untouched (WebKit)", async () => {
|
|
217
209
|
const response = nullBodyStatusWithBody(204);
|
|
218
210
|
const fetchMock = vi.fn(async () => response);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"aiSDKFormatAdapter.d.ts","names":[],"sources":["../../src/adapters/aiSDKFormatAdapter.ts"],"mappings":";;;KAQY,qBAAqB,KAAK;cAEzB,sBAAsB,qBACjC,WACA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"aiSDKFormatAdapter.js","names":["UIMessage","MessageFormatAdapter","MessageFormatItem","MessageStorageEntry","AISDKStorageFormat","Omit","aiSDKV6FormatAdapter","format","encode","message","id","parts","decode","stored","parentId","parent_id","content","getId"],"sources":["../../src/adapters/aiSDKFormatAdapter.ts"],"sourcesContent":["import type { UIMessage } from \"ai\";\nimport type {\n MessageFormatAdapter,\n MessageFormatItem,\n MessageStorageEntry,\n} from \"@assistant-ui/core\";\n\n// Storage format for AI SDK messages - just the UIMessage\nexport type AISDKStorageFormat = Omit<UIMessage, \"id\">;\n\nexport const aiSDKV6FormatAdapter: MessageFormatAdapter<\n UIMessage,\n AISDKStorageFormat\n> = {\n format: \"ai-sdk/v6\",\n\n encode({\n message: { id, parts, ...message },\n }: MessageFormatItem<UIMessage>): AISDKStorageFormat {\n return {\n ...message,\n parts,\n };\n },\n\n decode(\n stored: MessageStorageEntry<AISDKStorageFormat>,\n ): MessageFormatItem<UIMessage> {\n return {\n parentId: stored.parent_id,\n message: {\n id: stored.id,\n ...stored.content,\n },\n };\n },\n\n getId(message: UIMessage): string {\n return message.id;\n },\n};\n"],"mappings":";AAUA,MAAaM,uBAGT;CACFC,QAAQ;CAERC,OAAO,EACLC,SAAS,EAAEC,IAAIC,OAAO,GAAGF,aAC0B;EACnD,OAAO;GACL,GAAGA;GACHE;EACF;CACF;CAEAC,OACEC,QAC8B;EAC9B,OAAO;GACLC,UAAUD,OAAOE;GACjBN,SAAS;IACPC,IAAIG,OAAOH;IACX,GAAGG,OAAOG;GACZ;EACF;CACF;CAEAC,MAAMR,SAA4B;EAChC,OAAOA,QAAQC;CACjB;AACF"}
|