@ulearning/ai-sdk-adapter 0.0.0-stage → 1.0.0-next.0
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/LICENSE +21 -0
- package/README.md +29 -2
- package/dist/adapters/aiSDKFormatAdapter.d.ts +6 -0
- package/dist/adapters/aiSDKFormatAdapter.d.ts.map +1 -0
- package/dist/adapters/aiSDKFormatAdapter.js +17 -0
- package/dist/adapters/aiSDKFormatAdapter.js.map +1 -0
- package/dist/adapters/vercelAttachmentAdapter.d.ts +3 -0
- package/dist/adapters/vercelAttachmentAdapter.d.ts.map +1 -0
- package/dist/adapters/vercelAttachmentAdapter.js +36 -0
- package/dist/adapters/vercelAttachmentAdapter.js.map +1 -0
- package/dist/aiSDKExtras.d.ts +13 -0
- package/dist/aiSDKExtras.d.ts.map +1 -0
- package/dist/aiSDKExtras.js +6 -0
- package/dist/aiSDKExtras.js.map +1 -0
- package/dist/converters/convertMessage.d.ts +44 -0
- package/dist/converters/convertMessage.d.ts.map +1 -0
- package/dist/converters/convertMessage.js +465 -0
- package/dist/converters/convertMessage.js.map +1 -0
- package/dist/converters/toCreateMessage.d.ts +4 -0
- package/dist/converters/toCreateMessage.d.ts.map +1 -0
- package/dist/converters/toCreateMessage.js +65 -0
- package/dist/converters/toCreateMessage.js.map +1 -0
- package/dist/converters/toolApprovalAnswers.d.ts +3 -0
- package/dist/converters/toolApprovalAnswers.d.ts.map +1 -0
- package/dist/converters/toolApprovalAnswers.js +16 -0
- package/dist/converters/toolApprovalAnswers.js.map +1 -0
- package/dist/hooks.d.ts +17 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +19 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/model-context/injectInteractableContext.d.ts +37 -0
- package/dist/model-context/injectInteractableContext.d.ts.map +1 -0
- package/dist/model-context/injectInteractableContext.js +54 -0
- package/dist/model-context/injectInteractableContext.js.map +1 -0
- package/dist/model-context/injectQuoteContext.d.ts +25 -0
- package/dist/model-context/injectQuoteContext.d.ts.map +1 -0
- package/dist/model-context/injectQuoteContext.js +51 -0
- package/dist/model-context/injectQuoteContext.js.map +1 -0
- package/dist/runtime/AISDKChat.d.ts +13 -0
- package/dist/runtime/AISDKChat.d.ts.map +1 -0
- package/dist/runtime/AISDKChat.js +34 -0
- package/dist/runtime/AISDKChat.js.map +1 -0
- package/dist/runtime/AISDKThreads.d.ts +24 -0
- package/dist/runtime/AISDKThreads.d.ts.map +1 -0
- package/dist/runtime/AISDKThreads.js +92 -0
- package/dist/runtime/AISDKThreads.js.map +1 -0
- package/dist/runtime/DynamicChatTransport.d.ts +50 -0
- package/dist/runtime/DynamicChatTransport.d.ts.map +1 -0
- package/dist/runtime/DynamicChatTransport.js +153 -0
- package/dist/runtime/DynamicChatTransport.js.map +1 -0
- package/dist/runtime/getResumableAdapter.d.ts +5 -0
- package/dist/runtime/getResumableAdapter.d.ts.map +1 -0
- package/dist/runtime/getResumableAdapter.js +11 -0
- package/dist/runtime/getResumableAdapter.js.map +1 -0
- package/dist/runtime/toolHistoryCodec.d.ts +20 -0
- package/dist/runtime/toolHistoryCodec.d.ts.map +1 -0
- package/dist/runtime/toolHistoryCodec.js +122 -0
- package/dist/runtime/toolHistoryCodec.js.map +1 -0
- package/dist/runtime/useAISDKRuntime.d.ts +95 -0
- package/dist/runtime/useAISDKRuntime.d.ts.map +1 -0
- package/dist/runtime/useAISDKRuntime.js +754 -0
- package/dist/runtime/useAISDKRuntime.js.map +1 -0
- package/dist/runtime/useChatRuntime.d.ts +17 -0
- package/dist/runtime/useChatRuntime.d.ts.map +1 -0
- package/dist/runtime/useChatRuntime.js +50 -0
- package/dist/runtime/useChatRuntime.js.map +1 -0
- package/dist/runtime/useChatSnapshot.d.ts +6 -0
- package/dist/runtime/useChatSnapshot.d.ts.map +1 -0
- package/dist/runtime/useChatSnapshot.js +62 -0
- package/dist/runtime/useChatSnapshot.js.map +1 -0
- package/dist/runtime/useChatThread.d.ts +121 -0
- package/dist/runtime/useChatThread.d.ts.map +1 -0
- package/dist/runtime/useChatThread.js +255 -0
- package/dist/runtime/useChatThread.js.map +1 -0
- package/dist/runtime/useDynamicChatTransport.d.ts +4 -0
- package/dist/runtime/useDynamicChatTransport.d.ts.map +1 -0
- package/dist/runtime/useDynamicChatTransport.js +26 -0
- package/dist/runtime/useDynamicChatTransport.js.map +1 -0
- package/dist/runtime/useExternalHistory.d.ts +12 -0
- package/dist/runtime/useExternalHistory.d.ts.map +1 -0
- package/dist/runtime/useExternalHistory.js +556 -0
- package/dist/runtime/useExternalHistory.js.map +1 -0
- package/dist/runtime/useMessageQueue.d.ts +14 -0
- package/dist/runtime/useMessageQueue.d.ts.map +1 -0
- package/dist/runtime/useMessageQueue.js +142 -0
- package/dist/runtime/useMessageQueue.js.map +1 -0
- package/dist/runtime/useResourceCleanup.d.ts +2 -0
- package/dist/runtime/useResourceCleanup.d.ts.map +1 -0
- package/dist/runtime/useResourceCleanup.js +47 -0
- package/dist/runtime/useResourceCleanup.js.map +1 -0
- package/dist/runtime/useStreamingTiming.d.ts +12 -0
- package/dist/runtime/useStreamingTiming.d.ts.map +1 -0
- package/dist/runtime/useStreamingTiming.js +36 -0
- package/dist/runtime/useStreamingTiming.js.map +1 -0
- package/dist/tools/generativeTools.d.ts +71 -0
- package/dist/tools/generativeTools.d.ts.map +1 -0
- package/dist/tools/generativeTools.js +258 -0
- package/dist/tools/generativeTools.js.map +1 -0
- package/dist/tools/mcp-stdio.node.d.ts +2 -0
- package/dist/tools/mcp-stdio.node.d.ts.map +1 -0
- package/dist/tools/mcp-stdio.node.js +1 -0
- package/dist/tools/mcp-stdio.unsupported.d.ts +3 -0
- package/dist/tools/mcp-stdio.unsupported.d.ts.map +1 -0
- package/dist/tools/mcp-stdio.unsupported.js +10 -0
- package/dist/tools/mcp-stdio.unsupported.js.map +1 -0
- package/dist/transport/AssistantChatTransport.d.ts +20 -0
- package/dist/transport/AssistantChatTransport.d.ts.map +1 -0
- package/dist/transport/AssistantChatTransport.js +151 -0
- package/dist/transport/AssistantChatTransport.js.map +1 -0
- package/dist/transport/resumable.d.ts +36 -0
- package/dist/transport/resumable.d.ts.map +1 -0
- package/dist/transport/resumable.js +112 -0
- package/dist/transport/resumable.js.map +1 -0
- package/dist/usage.d.ts +24 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +121 -0
- package/dist/usage.js.map +1 -0
- package/dist/utils/getVercelAIMessages.d.ts +4 -0
- package/dist/utils/getVercelAIMessages.d.ts.map +1 -0
- package/dist/utils/getVercelAIMessages.js +8 -0
- package/dist/utils/getVercelAIMessages.js.map +1 -0
- package/dist/utils/sliceMessagesUntil.d.ts +3 -0
- package/dist/utils/sliceMessagesUntil.d.ts.map +1 -0
- package/dist/utils/sliceMessagesUntil.js +10 -0
- package/dist/utils/sliceMessagesUntil.js.map +1 -0
- package/package.json +96 -3
- package/src/__tests__/standalone.e2e.ts +118 -0
- package/src/adapters/aiSDKFormatAdapter.test.ts +36 -0
- package/src/adapters/aiSDKFormatAdapter.ts +18 -0
- package/src/adapters/vercelAttachmentAdapter.test.ts +52 -0
- package/src/adapters/vercelAttachmentAdapter.ts +36 -0
- package/src/aiSDKExtras.ts +31 -0
- package/src/converters/convertMessage.metadata-key.test.tsx +182 -0
- package/src/converters/convertMessage.test.ts +2428 -0
- package/src/converters/convertMessage.tool-args-status.test.tsx +185 -0
- package/src/converters/convertMessage.ts +919 -0
- package/src/converters/toCreateMessage.test.ts +716 -0
- package/src/converters/toCreateMessage.ts +110 -0
- package/src/converters/toolApprovalAnswers.ts +27 -0
- package/src/hooks.test.ts +168 -0
- package/src/hooks.ts +26 -0
- package/src/index.ts +29 -0
- package/src/model-context/injectInteractableContext.test.ts +167 -0
- package/src/model-context/injectInteractableContext.ts +64 -0
- package/src/model-context/injectQuoteContext.test.ts +33 -0
- package/src/model-context/injectQuoteContext.ts +63 -0
- package/src/runtime/AISDKChat.artifacts.test.ts +48 -0
- package/src/runtime/AISDKChat.integration.test.tsx +148 -0
- package/src/runtime/AISDKChat.test.ts +241 -0
- package/src/runtime/AISDKChat.ts +43 -0
- package/src/runtime/AISDKThreads.test.ts +811 -0
- package/src/runtime/AISDKThreads.ts +194 -0
- package/src/runtime/DynamicChatTransport.test.ts +228 -0
- package/src/runtime/DynamicChatTransport.ts +281 -0
- package/src/runtime/__tests__/controlled-transport.ts +70 -0
- package/src/runtime/__tests__/tap.ts +80 -0
- package/src/runtime/__tests__/vue.ts +46 -0
- package/src/runtime/getResumableAdapter.ts +16 -0
- package/src/runtime/toolHistoryCodec.test.ts +187 -0
- package/src/runtime/toolHistoryCodec.ts +245 -0
- package/src/runtime/useAISDKRuntime.approval.integration.test.tsx +1956 -0
- package/src/runtime/useAISDKRuntime.approval.test.tsx +580 -0
- package/src/runtime/useAISDKRuntime.denied-tool.test.tsx +122 -0
- package/src/runtime/useAISDKRuntime.fast-refresh.test.tsx +302 -0
- package/src/runtime/useAISDKRuntime.metadata-cache.test.tsx +157 -0
- package/src/runtime/useAISDKRuntime.queue.test.tsx +707 -0
- package/src/runtime/useAISDKRuntime.reload.test.tsx +217 -0
- package/src/runtime/useAISDKRuntime.test.ts +2345 -0
- package/src/runtime/useAISDKRuntime.tool-output.test.tsx +113 -0
- package/src/runtime/useAISDKRuntime.ts +1503 -0
- package/src/runtime/useAISDKRuntime.voice.test.tsx +319 -0
- package/src/runtime/useChatOnTap.test.ts +60 -0
- package/src/runtime/useChatRuntime.fast-refresh.test.tsx +83 -0
- package/src/runtime/useChatRuntime.integration.test.tsx +661 -0
- package/src/runtime/useChatRuntime.local-storage.test.tsx +114 -0
- package/src/runtime/useChatRuntime.native.test.ts +147 -0
- package/src/runtime/useChatRuntime.run-telemetry.test.tsx +226 -0
- package/src/runtime/useChatRuntime.test.ts +619 -0
- package/src/runtime/useChatRuntime.ts +80 -0
- package/src/runtime/useChatSnapshot.test.ts +60 -0
- package/src/runtime/useChatSnapshot.ts +86 -0
- package/src/runtime/useChatThread.binding.test.tsx +140 -0
- package/src/runtime/useChatThread.test.ts +993 -0
- package/src/runtime/useChatThread.transport.test.tsx +149 -0
- package/src/runtime/useChatThread.ts +533 -0
- package/src/runtime/useDynamicChatTransport.ts +26 -0
- package/src/runtime/useExternalHistory.test.ts +4187 -0
- package/src/runtime/useExternalHistory.ts +902 -0
- package/src/runtime/useMessageQueue.effects.test.tsx +88 -0
- package/src/runtime/useMessageQueue.test.tsx +256 -0
- package/src/runtime/useMessageQueue.ts +189 -0
- package/src/runtime/useResourceCleanup.ts +58 -0
- package/src/runtime/useStreamingTiming.test.tsx +96 -0
- package/src/runtime/useStreamingTiming.ts +69 -0
- package/src/tools/generativeTools.test.ts +1058 -0
- package/src/tools/generativeTools.ts +491 -0
- package/src/tools/mcp-stdio.node.ts +1 -0
- package/src/tools/mcp-stdio.unsupported.ts +12 -0
- package/src/transport/AssistantChatTransport.test.ts +631 -0
- package/src/transport/AssistantChatTransport.ts +236 -0
- package/src/transport/resumable.test.ts +262 -0
- package/src/transport/resumable.ts +162 -0
- package/src/usage.test.ts +204 -0
- package/src/usage.ts +179 -0
- package/src/utils/getVercelAIMessages.ts +11 -0
- package/src/utils/sliceMessagesUntil.test.ts +51 -0
- package/src/utils/sliceMessagesUntil.ts +16 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { act, renderHook, waitFor } from "./__tests__/tap";
|
|
4
|
+
import { useEffect } from "@ulearning/ai-tap";
|
|
5
|
+
import { describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { Chat } from "@ai-sdk/vue";
|
|
7
|
+
import {
|
|
8
|
+
AssistantChatTransport,
|
|
9
|
+
type InitializableThreadListItem,
|
|
10
|
+
} from "../transport/AssistantChatTransport";
|
|
11
|
+
import { useChatThread } from "./useChatThread";
|
|
12
|
+
|
|
13
|
+
const itemFor = (remoteId: string) => ({
|
|
14
|
+
initialize: async () => ({ remoteId, externalId: undefined }),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const finishedStream = () =>
|
|
18
|
+
new Response(
|
|
19
|
+
new ReadableStream<Uint8Array>({
|
|
20
|
+
start(controller) {
|
|
21
|
+
controller.enqueue(
|
|
22
|
+
new TextEncoder().encode(
|
|
23
|
+
'data: {"type":"start"}\n\ndata: {"type":"finish"}\n\ndata: [DONE]\n\n',
|
|
24
|
+
),
|
|
25
|
+
);
|
|
26
|
+
controller.close();
|
|
27
|
+
},
|
|
28
|
+
}),
|
|
29
|
+
{ headers: { "content-type": "text/event-stream" } },
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
describe("useChatThread shared transport isolation", () => {
|
|
33
|
+
it("gives each thread its own clone wired to its own thread-list item", async () => {
|
|
34
|
+
const transport = new AssistantChatTransport({ api: "/api/chat" });
|
|
35
|
+
const setRuntime = vi.spyOn(transport, "setRuntime");
|
|
36
|
+
const setGetItem = vi.spyOn(transport, "__internal_setGetThreadListItem");
|
|
37
|
+
const clones: AssistantChatTransport<never>[] = [];
|
|
38
|
+
const getters: (() => InitializableThreadListItem | undefined)[] = [];
|
|
39
|
+
const realClone = transport.__internal_clone.bind(transport);
|
|
40
|
+
vi.spyOn(transport, "__internal_clone").mockImplementation(() => {
|
|
41
|
+
const clone = realClone();
|
|
42
|
+
const index = clones.push(clone as AssistantChatTransport<never>) - 1;
|
|
43
|
+
vi.spyOn(clone, "__internal_setGetThreadListItem").mockImplementation(
|
|
44
|
+
(getter) => {
|
|
45
|
+
getters[index] = getter;
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
return clone;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
renderHook(() =>
|
|
52
|
+
useChatThread(
|
|
53
|
+
{ transport },
|
|
54
|
+
{
|
|
55
|
+
id: "thread-a",
|
|
56
|
+
isMainThread: true,
|
|
57
|
+
getThreadListItem: () => itemFor("remote-a"),
|
|
58
|
+
},
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
renderHook(() =>
|
|
62
|
+
useChatThread(
|
|
63
|
+
{ transport },
|
|
64
|
+
{
|
|
65
|
+
id: "thread-b",
|
|
66
|
+
isMainThread: false,
|
|
67
|
+
getThreadListItem: () => itemFor("remote-b"),
|
|
68
|
+
},
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
expect(setRuntime).not.toHaveBeenCalled();
|
|
73
|
+
expect(setGetItem).not.toHaveBeenCalled();
|
|
74
|
+
expect(clones).toHaveLength(2);
|
|
75
|
+
expect(clones[0]).not.toBe(clones[1]);
|
|
76
|
+
|
|
77
|
+
const remoteIdOf = async (
|
|
78
|
+
getter?: () => InitializableThreadListItem | undefined,
|
|
79
|
+
) => (await getter?.()?.initialize())?.remoteId;
|
|
80
|
+
expect(await remoteIdOf(getters[0])).toBe("remote-a");
|
|
81
|
+
expect(await remoteIdOf(getters[1])).toBe("remote-b");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("sends each thread's request with that thread's remoteId and model context", async () => {
|
|
85
|
+
const sent: { id: unknown; system: unknown }[] = [];
|
|
86
|
+
const transport = new AssistantChatTransport({
|
|
87
|
+
api: "/api/chat",
|
|
88
|
+
fetch: async (_input, init) => {
|
|
89
|
+
const body = JSON.parse(String(init?.body)) as Record<string, unknown>;
|
|
90
|
+
sent.push({ id: body["id"], system: body["system"] });
|
|
91
|
+
return finishedStream();
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const mount = (id: string, remoteId: string, system: string) =>
|
|
96
|
+
renderHook(() => {
|
|
97
|
+
const runtime = useChatThread(
|
|
98
|
+
{ transport },
|
|
99
|
+
{
|
|
100
|
+
id,
|
|
101
|
+
isMainThread: id === "thread-a",
|
|
102
|
+
getThreadListItem: () => itemFor(remoteId),
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
useEffect(
|
|
106
|
+
() =>
|
|
107
|
+
runtime.registerModelContextProvider({
|
|
108
|
+
getModelContext: () => ({ system }),
|
|
109
|
+
}),
|
|
110
|
+
[runtime],
|
|
111
|
+
);
|
|
112
|
+
return runtime;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const threadA = mount("thread-a", "remote-a", "system-a");
|
|
116
|
+
const threadB = mount("thread-b", "remote-b", "system-b");
|
|
117
|
+
|
|
118
|
+
threadA.rerender();
|
|
119
|
+
|
|
120
|
+
await act(async () => {
|
|
121
|
+
threadB.result.current.thread.append("hello");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await waitFor(() => expect(sent).toHaveLength(1));
|
|
125
|
+
expect(sent[0]).toEqual({ id: "remote-b", system: "system-b" });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("uses the supplied instance when the caller owns the chat", () => {
|
|
129
|
+
const transport = new AssistantChatTransport({ api: "/api/chat" });
|
|
130
|
+
const clone = vi.spyOn(transport, "__internal_clone");
|
|
131
|
+
const chat = new Chat({ id: "thread-a", transport });
|
|
132
|
+
|
|
133
|
+
renderHook(() =>
|
|
134
|
+
useChatThread(
|
|
135
|
+
{ transport },
|
|
136
|
+
{
|
|
137
|
+
id: "thread-a",
|
|
138
|
+
isMainThread: true,
|
|
139
|
+
getThreadListItem: () => itemFor("remote-a"),
|
|
140
|
+
chat,
|
|
141
|
+
},
|
|
142
|
+
),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// A caller-owned chat is already bound to its transport, so cloning again
|
|
146
|
+
// would wire an instance the chat never sends through.
|
|
147
|
+
expect(clone).not.toHaveBeenCalled();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Chat } from "@ai-sdk/vue";
|
|
4
|
+
import type { UIMessage } from "ai";
|
|
5
|
+
import { useChatSnapshot } from "./useChatSnapshot";
|
|
6
|
+
import type { MessageRepository } from "@ulearning/ai-core/internal";
|
|
7
|
+
import {
|
|
8
|
+
pickExternalStoreSharedOptions,
|
|
9
|
+
type AssistantRuntime,
|
|
10
|
+
type ExternalStoreSharedOptions,
|
|
11
|
+
} from "@ulearning/ai-core";
|
|
12
|
+
import {
|
|
13
|
+
useAISDKRuntime,
|
|
14
|
+
type AISDKRuntimeAdapter,
|
|
15
|
+
type CustomToCreateMessageFunction,
|
|
16
|
+
} from "./useAISDKRuntime";
|
|
17
|
+
import type { ChatInit } from "ai";
|
|
18
|
+
import {
|
|
19
|
+
AssistantChatTransport,
|
|
20
|
+
type InitializableThreadListItem,
|
|
21
|
+
} from "../transport/AssistantChatTransport";
|
|
22
|
+
import {
|
|
23
|
+
useCallback,
|
|
24
|
+
useEffect,
|
|
25
|
+
useEffectEvent,
|
|
26
|
+
useInsertionEffect,
|
|
27
|
+
useMemo,
|
|
28
|
+
useRef,
|
|
29
|
+
useState,
|
|
30
|
+
useSyncExternalStore,
|
|
31
|
+
} from "@ulearning/ai-tap";
|
|
32
|
+
import { useResourceCleanup } from "./useResourceCleanup";
|
|
33
|
+
import {
|
|
34
|
+
DynamicChatTransport,
|
|
35
|
+
getResumedStreamIds,
|
|
36
|
+
} from "./DynamicChatTransport";
|
|
37
|
+
import type { ResumableClientStorage } from "../transport/resumable";
|
|
38
|
+
import { getResumableAdapter } from "./getResumableAdapter";
|
|
39
|
+
import { useDynamicChatTransport } from "./useDynamicChatTransport";
|
|
40
|
+
|
|
41
|
+
export type ChatThreadOptions<UI_MESSAGE extends UIMessage = UIMessage> =
|
|
42
|
+
ChatInit<UI_MESSAGE> &
|
|
43
|
+
ExternalStoreSharedOptions & {
|
|
44
|
+
throttle?: number | undefined;
|
|
45
|
+
adapters?: AISDKRuntimeAdapter["adapters"] | undefined;
|
|
46
|
+
toCreateMessage?: CustomToCreateMessageFunction;
|
|
47
|
+
onResume?: AISDKRuntimeAdapter["onResume"];
|
|
48
|
+
canResume?: AISDKRuntimeAdapter["canResume"];
|
|
49
|
+
onResumeToolCall?: AISDKRuntimeAdapter["onResumeToolCall"];
|
|
50
|
+
onRespondToToolApproval?: AISDKRuntimeAdapter["onRespondToToolApproval"];
|
|
51
|
+
/**
|
|
52
|
+
* Called when a resumable stream reconnect fails. Use this to
|
|
53
|
+
* surface a toast, report telemetry, or mark the thread as needing a
|
|
54
|
+
* retry. With `canResume: true`, transient errors keep the stream id for
|
|
55
|
+
* manual retry. Otherwise, a failed reconnect clears the matching id.
|
|
56
|
+
* A 204 or 404 from the resume endpoint always clears the matching id.
|
|
57
|
+
*/
|
|
58
|
+
onResumeError?: ((error: unknown) => void) | undefined;
|
|
59
|
+
joinStrategy?: AISDKRuntimeAdapter["joinStrategy"];
|
|
60
|
+
messageRepository?: AISDKRuntimeAdapter<UI_MESSAGE>["messageRepository"];
|
|
61
|
+
/** @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release. */
|
|
62
|
+
unstable_onBranchChange?: AISDKRuntimeAdapter["unstable_onBranchChange"];
|
|
63
|
+
/** @deprecated Experimental since 2026-10-15. Not scheduled for removal; the API may change in any release. */
|
|
64
|
+
unstable_enableMessageQueue?: AISDKRuntimeAdapter["unstable_enableMessageQueue"];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type ChatThreadEnvironment<UI_MESSAGE extends UIMessage = UIMessage> = {
|
|
68
|
+
id: string;
|
|
69
|
+
isMainThread: boolean;
|
|
70
|
+
getThreadListItem: () => InitializableThreadListItem | undefined;
|
|
71
|
+
stopOnClientDestroy?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Aborts when the Vue scope hosting the runtime is deleted. A nested
|
|
74
|
+
* runtime resolves the destroy signal of the provider above it, which
|
|
75
|
+
* outlives the nested component, so this stops the chat on its own unmount.
|
|
76
|
+
*/
|
|
77
|
+
hostDestroySignal?: AbortSignal | undefined;
|
|
78
|
+
/**
|
|
79
|
+
* An externally owned chat instance. State lives on the instance, so it
|
|
80
|
+
* survives the hosting resource unmounting; construction options are read
|
|
81
|
+
* from the instance.
|
|
82
|
+
*/
|
|
83
|
+
chat?: Chat<UI_MESSAGE> | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* An externally owned per-thread message repository. Hosts that route
|
|
86
|
+
* multiple threads through one mounting pass a distinct instance per
|
|
87
|
+
* thread so histories and branches stay isolated.
|
|
88
|
+
*/
|
|
89
|
+
messageRepositoryInstance?: MessageRepository | undefined;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
type ChatThreadTransportBinding = {
|
|
93
|
+
runtime: AssistantRuntime;
|
|
94
|
+
getThreadListItem: () => InitializableThreadListItem | undefined;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const getNoPendingStreamId = () => null;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Splits the combined options into the assistant-ui side and the `ChatInit`
|
|
101
|
+
* remainder the AI SDK consumes, so external `Chat` construction forwards the
|
|
102
|
+
* same fields `useChat` would.
|
|
103
|
+
*/
|
|
104
|
+
export const splitChatThreadOptions = <UI_MESSAGE extends UIMessage>(
|
|
105
|
+
options: ChatThreadOptions<UI_MESSAGE> | undefined,
|
|
106
|
+
) => {
|
|
107
|
+
const {
|
|
108
|
+
adapters,
|
|
109
|
+
transport,
|
|
110
|
+
throttle,
|
|
111
|
+
toCreateMessage,
|
|
112
|
+
isDisabled: _isDisabled,
|
|
113
|
+
isSendDisabled: _isSendDisabled,
|
|
114
|
+
unstable_capabilities: _unstable_capabilities,
|
|
115
|
+
suggestions: _suggestions,
|
|
116
|
+
onResume,
|
|
117
|
+
canResume,
|
|
118
|
+
onResumeToolCall,
|
|
119
|
+
onRespondToToolApproval,
|
|
120
|
+
onResumeError,
|
|
121
|
+
joinStrategy,
|
|
122
|
+
messageRepository,
|
|
123
|
+
unstable_onBranchChange,
|
|
124
|
+
unstable_enableMessageQueue,
|
|
125
|
+
...chatInit
|
|
126
|
+
} = options ?? {};
|
|
127
|
+
// peel guard: any shared key left in `chatInit` collapses this to `never`
|
|
128
|
+
true satisfies keyof typeof chatInit &
|
|
129
|
+
keyof ExternalStoreSharedOptions extends never
|
|
130
|
+
? true
|
|
131
|
+
: never;
|
|
132
|
+
return {
|
|
133
|
+
adapters,
|
|
134
|
+
transport,
|
|
135
|
+
throttle,
|
|
136
|
+
toCreateMessage,
|
|
137
|
+
onResume,
|
|
138
|
+
canResume,
|
|
139
|
+
onResumeToolCall,
|
|
140
|
+
onRespondToToolApproval,
|
|
141
|
+
onResumeError,
|
|
142
|
+
joinStrategy,
|
|
143
|
+
messageRepository,
|
|
144
|
+
unstable_onBranchChange,
|
|
145
|
+
unstable_enableMessageQueue,
|
|
146
|
+
chatInit,
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
type ChatCallbacks<UI_MESSAGE extends UIMessage> = Pick<
|
|
151
|
+
ChatInit<UI_MESSAGE>,
|
|
152
|
+
"onToolCall" | "onData" | "onFinish" | "onError" | "sendAutomaticallyWhen"
|
|
153
|
+
> & {
|
|
154
|
+
canResume?: boolean | undefined;
|
|
155
|
+
resumableStorage?: ResumableClientStorage | undefined;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
type ChatRequest = {
|
|
159
|
+
reconnectFailure?: { error: unknown };
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const requestsByChat = new WeakMap<object, ChatRequest>();
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Constructs a `Chat` whose callbacks read the latest options through
|
|
166
|
+
* `callbacksRef`, the forwarding `useChat` applies only to a chat it
|
|
167
|
+
* constructs itself.
|
|
168
|
+
*/
|
|
169
|
+
export const createChat = <UI_MESSAGE extends UIMessage>(
|
|
170
|
+
init: ChatInit<UI_MESSAGE>,
|
|
171
|
+
callbacksRef: { readonly current: ChatCallbacks<UI_MESSAGE> | undefined },
|
|
172
|
+
): Chat<UI_MESSAGE> => {
|
|
173
|
+
const transport = init.transport;
|
|
174
|
+
const chat = new Chat<UI_MESSAGE>({
|
|
175
|
+
...init,
|
|
176
|
+
...(transport && {
|
|
177
|
+
transport: {
|
|
178
|
+
get resumeStreamIsReplay() {
|
|
179
|
+
return transport.resumeStreamIsReplay ?? false;
|
|
180
|
+
},
|
|
181
|
+
sendMessages: (options) => {
|
|
182
|
+
requestsByChat.set(chat, {});
|
|
183
|
+
// A new send or regeneration changes the replay target even when
|
|
184
|
+
// the request fails before returning a replacement checkpoint.
|
|
185
|
+
if (callbacksRef.current?.canResume) {
|
|
186
|
+
const storage =
|
|
187
|
+
callbacksRef.current.resumableStorage ??
|
|
188
|
+
getResumableAdapter(transport)?.storage;
|
|
189
|
+
storage?.clear(options.chatId);
|
|
190
|
+
}
|
|
191
|
+
return transport.sendMessages(options);
|
|
192
|
+
},
|
|
193
|
+
reconnectToStream: async (options) => {
|
|
194
|
+
const request: ChatRequest = {};
|
|
195
|
+
requestsByChat.set(chat, request);
|
|
196
|
+
try {
|
|
197
|
+
return await transport.reconnectToStream(options);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
// SDK 7 suppresses a superseded reconnect's rejection after its
|
|
200
|
+
// abort signal fires. Preserve the actual transport failure for
|
|
201
|
+
// the resume invocation that owns it, without borrowing Chat.error
|
|
202
|
+
// from a newer send or resume. Genuine aborts remain cancellation.
|
|
203
|
+
if (
|
|
204
|
+
typeof error !== "object" ||
|
|
205
|
+
error === null ||
|
|
206
|
+
!("name" in error) ||
|
|
207
|
+
error.name !== "AbortError"
|
|
208
|
+
) {
|
|
209
|
+
request.reconnectFailure = { error };
|
|
210
|
+
}
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
}),
|
|
216
|
+
onToolCall: (arg) => callbacksRef.current?.onToolCall?.(arg),
|
|
217
|
+
onData: (arg) => callbacksRef.current?.onData?.(arg),
|
|
218
|
+
onFinish: (arg) => callbacksRef.current?.onFinish?.(arg),
|
|
219
|
+
onError: (arg) => callbacksRef.current?.onError?.(arg),
|
|
220
|
+
sendAutomaticallyWhen: (arg) =>
|
|
221
|
+
callbacksRef.current?.sendAutomaticallyWhen?.(arg) ?? false,
|
|
222
|
+
});
|
|
223
|
+
return chat;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
export const useChatThread = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
227
|
+
options: ChatThreadOptions<UI_MESSAGE> | undefined,
|
|
228
|
+
env: ChatThreadEnvironment<UI_MESSAGE>,
|
|
229
|
+
): AssistantRuntime => {
|
|
230
|
+
const {
|
|
231
|
+
adapters,
|
|
232
|
+
transport: transportOptions,
|
|
233
|
+
throttle,
|
|
234
|
+
toCreateMessage,
|
|
235
|
+
onResume,
|
|
236
|
+
canResume,
|
|
237
|
+
onResumeToolCall,
|
|
238
|
+
onRespondToToolApproval,
|
|
239
|
+
onResumeError,
|
|
240
|
+
joinStrategy,
|
|
241
|
+
messageRepository,
|
|
242
|
+
unstable_onBranchChange,
|
|
243
|
+
unstable_enableMessageQueue,
|
|
244
|
+
chatInit: chatOptions,
|
|
245
|
+
} = splitChatThreadOptions(options);
|
|
246
|
+
|
|
247
|
+
const {
|
|
248
|
+
id,
|
|
249
|
+
isMainThread,
|
|
250
|
+
getThreadListItem,
|
|
251
|
+
stopOnClientDestroy = true,
|
|
252
|
+
hostDestroySignal,
|
|
253
|
+
chat: externalChat,
|
|
254
|
+
messageRepositoryInstance,
|
|
255
|
+
} = env;
|
|
256
|
+
|
|
257
|
+
const defaultTransport = useMemo(() => new AssistantChatTransport(), []);
|
|
258
|
+
const configuredTransport = transportOptions ?? defaultTransport;
|
|
259
|
+
const transport = useDynamicChatTransport(
|
|
260
|
+
configuredTransport,
|
|
261
|
+
externalChat === undefined,
|
|
262
|
+
);
|
|
263
|
+
const transportContextOwner = useMemo(() => ({}), []);
|
|
264
|
+
const getThreadListItemRef = useRef(getThreadListItem);
|
|
265
|
+
useInsertionEffect(() => {
|
|
266
|
+
getThreadListItemRef.current = getThreadListItem;
|
|
267
|
+
}, [getThreadListItem]);
|
|
268
|
+
const getCurrentThreadListItem = useCallback(
|
|
269
|
+
() => getThreadListItemRef.current(),
|
|
270
|
+
[],
|
|
271
|
+
);
|
|
272
|
+
const resumableStorage = useMemo(
|
|
273
|
+
() =>
|
|
274
|
+
transport instanceof DynamicChatTransport
|
|
275
|
+
? transport.getCurrentResumableStorage()
|
|
276
|
+
: getResumableAdapter(transport)?.storage,
|
|
277
|
+
[transport],
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
const transportBindingRef = useRef<ChatThreadTransportBinding | null>(null);
|
|
281
|
+
// This is null when useMemo runs, so the proxy pins its mount render's binding. The fallback
|
|
282
|
+
// serves only pre-commit sends; after insertion effects, transportBindingRef is authoritative.
|
|
283
|
+
let initialTransportBinding: ChatThreadTransportBinding | null = null;
|
|
284
|
+
const chatTransport = useMemo(
|
|
285
|
+
() =>
|
|
286
|
+
transport instanceof DynamicChatTransport
|
|
287
|
+
? transport.createThreadProxy(transportContextOwner, () => {
|
|
288
|
+
const binding =
|
|
289
|
+
transportBindingRef.current ?? initialTransportBinding;
|
|
290
|
+
if (!binding) {
|
|
291
|
+
throw new Error("Chat transport used before runtime setup");
|
|
292
|
+
}
|
|
293
|
+
return binding;
|
|
294
|
+
})
|
|
295
|
+
: transport,
|
|
296
|
+
[initialTransportBinding, transport, transportContextOwner],
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const latestChatOptionsRef = useRef({
|
|
300
|
+
...chatOptions,
|
|
301
|
+
canResume,
|
|
302
|
+
resumableStorage,
|
|
303
|
+
});
|
|
304
|
+
useEffect(() => {
|
|
305
|
+
latestChatOptionsRef.current = {
|
|
306
|
+
...chatOptions,
|
|
307
|
+
canResume,
|
|
308
|
+
resumableStorage,
|
|
309
|
+
};
|
|
310
|
+
});
|
|
311
|
+
// `useChat` stops a chat it constructs whenever it unmounts, and a
|
|
312
|
+
// resource's soft unmount runs that cleanup, so the thread owns its chat.
|
|
313
|
+
const [ownedChat] = useState(
|
|
314
|
+
() =>
|
|
315
|
+
externalChat ??
|
|
316
|
+
createChat(
|
|
317
|
+
{ ...chatOptions, id, transport: chatTransport },
|
|
318
|
+
latestChatOptionsRef,
|
|
319
|
+
),
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
const chat = useChatSnapshot(externalChat ?? ownedChat, throttle);
|
|
323
|
+
|
|
324
|
+
useResourceCleanup(
|
|
325
|
+
stopOnClientDestroy,
|
|
326
|
+
() => {
|
|
327
|
+
void chat.stop().catch(() => {});
|
|
328
|
+
},
|
|
329
|
+
hostDestroySignal,
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const subscribeToResumableStorage = useCallback(
|
|
333
|
+
(callback: () => void) =>
|
|
334
|
+
isMainThread
|
|
335
|
+
? (resumableStorage?.subscribe?.(callback, id) ?? (() => {}))
|
|
336
|
+
: () => {},
|
|
337
|
+
[id, isMainThread, resumableStorage],
|
|
338
|
+
);
|
|
339
|
+
const getPendingStreamId = useCallback(
|
|
340
|
+
() => (isMainThread ? (resumableStorage?.getStreamId(id) ?? null) : null),
|
|
341
|
+
[id, isMainThread, resumableStorage],
|
|
342
|
+
);
|
|
343
|
+
const pendingStreamId = useSyncExternalStore(
|
|
344
|
+
subscribeToResumableStorage,
|
|
345
|
+
getPendingStreamId,
|
|
346
|
+
getNoPendingStreamId,
|
|
347
|
+
);
|
|
348
|
+
const isChatRunning =
|
|
349
|
+
chat.status === "submitted" || chat.status === "streaming";
|
|
350
|
+
|
|
351
|
+
const staticResumedStreamIds = useMemo(
|
|
352
|
+
() => getResumedStreamIds(resumableStorage),
|
|
353
|
+
[resumableStorage],
|
|
354
|
+
);
|
|
355
|
+
const onResumeErrorRef = useRef(onResumeError);
|
|
356
|
+
useEffect(() => {
|
|
357
|
+
onResumeErrorRef.current = onResumeError;
|
|
358
|
+
});
|
|
359
|
+
const resumeChatStream = chat.resumeStream;
|
|
360
|
+
const resumeStream = useCallback(async () => {
|
|
361
|
+
const resumedStreamIds =
|
|
362
|
+
transport instanceof DynamicChatTransport
|
|
363
|
+
? transport.getResumedStreamIds()
|
|
364
|
+
: staticResumedStreamIds;
|
|
365
|
+
const streamId = resumableStorage?.getStreamId(id);
|
|
366
|
+
if (!streamId) return;
|
|
367
|
+
resumedStreamIds.add(streamId);
|
|
368
|
+
try {
|
|
369
|
+
const activeChat = externalChat ?? ownedChat;
|
|
370
|
+
activeChat.clearError();
|
|
371
|
+
const pending = resumeChatStream();
|
|
372
|
+
const request = requestsByChat.get(activeChat);
|
|
373
|
+
await pending;
|
|
374
|
+
if (request?.reconnectFailure) {
|
|
375
|
+
throw request.reconnectFailure.error;
|
|
376
|
+
}
|
|
377
|
+
// Chat.error is shared with sends and resumes that can start before
|
|
378
|
+
// this promise settles, including inside the caller's onFinish.
|
|
379
|
+
if (requestsByChat.get(activeChat) === request && activeChat.error) {
|
|
380
|
+
throw activeChat.error;
|
|
381
|
+
}
|
|
382
|
+
} catch (error) {
|
|
383
|
+
try {
|
|
384
|
+
onResumeErrorRef.current?.(error);
|
|
385
|
+
} catch (callbackError) {
|
|
386
|
+
console.error(
|
|
387
|
+
"[assistant-ui] resumable: onResumeError callback failed",
|
|
388
|
+
callbackError,
|
|
389
|
+
);
|
|
390
|
+
} finally {
|
|
391
|
+
if (!canResume && resumableStorage?.getStreamId(id) === streamId) {
|
|
392
|
+
resumableStorage.clear(id);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
throw error;
|
|
396
|
+
}
|
|
397
|
+
}, [
|
|
398
|
+
canResume,
|
|
399
|
+
externalChat,
|
|
400
|
+
id,
|
|
401
|
+
ownedChat,
|
|
402
|
+
resumeChatStream,
|
|
403
|
+
resumableStorage,
|
|
404
|
+
staticResumedStreamIds,
|
|
405
|
+
transport,
|
|
406
|
+
]);
|
|
407
|
+
|
|
408
|
+
const handleBranchChange = useCallback<
|
|
409
|
+
NonNullable<AISDKRuntimeAdapter["unstable_onBranchChange"]>
|
|
410
|
+
>(
|
|
411
|
+
(event) => {
|
|
412
|
+
if (canResume) resumableStorage?.clear(id);
|
|
413
|
+
unstable_onBranchChange?.(event);
|
|
414
|
+
},
|
|
415
|
+
[canResume, id, resumableStorage, unstable_onBranchChange],
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
const runtime = useAISDKRuntime(chat, {
|
|
419
|
+
adapters: {
|
|
420
|
+
...adapters,
|
|
421
|
+
threadList: { threadId: id, ...adapters?.threadList },
|
|
422
|
+
},
|
|
423
|
+
...pickExternalStoreSharedOptions(options ?? {}),
|
|
424
|
+
...(toCreateMessage && { toCreateMessage }),
|
|
425
|
+
...(onResume
|
|
426
|
+
? { onResume }
|
|
427
|
+
: canResume && resumableStorage
|
|
428
|
+
? { onResume: resumeStream }
|
|
429
|
+
: {}),
|
|
430
|
+
// AI SDK 7 starts resume-stream with fresh message state (chat.ts), so the
|
|
431
|
+
// host must replay the full message with its original start.messageId.
|
|
432
|
+
// Without that ID the replay appends a second message. A stored stream ID
|
|
433
|
+
// alone cannot establish this contract; only the host can opt in.
|
|
434
|
+
canResume: onResume
|
|
435
|
+
? !!canResume
|
|
436
|
+
: !!canResume && !!pendingStreamId && !isChatRunning,
|
|
437
|
+
...(onResumeToolCall && { onResumeToolCall }),
|
|
438
|
+
...(onRespondToToolApproval && { onRespondToToolApproval }),
|
|
439
|
+
...(joinStrategy && { joinStrategy }),
|
|
440
|
+
...(messageRepository && { messageRepository }),
|
|
441
|
+
...(messageRepositoryInstance && {
|
|
442
|
+
unstable_messageRepositoryInstance: messageRepositoryInstance,
|
|
443
|
+
}),
|
|
444
|
+
// The chat outlives this runtime when a host mounts only the visible
|
|
445
|
+
// thread, so a host approval answer is kept with it. This is the Chat
|
|
446
|
+
// instance, not the useChat helpers, which are re-minted every render and
|
|
447
|
+
// would be a dead WeakMap key by the next one.
|
|
448
|
+
unstable_hostApprovalOwner: externalChat ?? ownedChat,
|
|
449
|
+
...(((canResume && resumableStorage) || unstable_onBranchChange) && {
|
|
450
|
+
unstable_onBranchChange: handleBranchChange,
|
|
451
|
+
}),
|
|
452
|
+
...(unstable_enableMessageQueue && { unstable_enableMessageQueue }),
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
initialTransportBinding = {
|
|
456
|
+
runtime,
|
|
457
|
+
getThreadListItem: getCurrentThreadListItem,
|
|
458
|
+
};
|
|
459
|
+
useInsertionEffect(() => {
|
|
460
|
+
transportBindingRef.current = {
|
|
461
|
+
runtime,
|
|
462
|
+
getThreadListItem: getCurrentThreadListItem,
|
|
463
|
+
};
|
|
464
|
+
}, [runtime, getCurrentThreadListItem]);
|
|
465
|
+
|
|
466
|
+
const registerTransportContext = useEffectEvent(
|
|
467
|
+
(dynamicTransport: DynamicChatTransport<UI_MESSAGE>, chatId: string) => {
|
|
468
|
+
dynamicTransport.setThreadContext(
|
|
469
|
+
chatId,
|
|
470
|
+
transportContextOwner,
|
|
471
|
+
runtime,
|
|
472
|
+
getCurrentThreadListItem,
|
|
473
|
+
);
|
|
474
|
+
return () =>
|
|
475
|
+
dynamicTransport.unregisterThread(chatId, transportContextOwner);
|
|
476
|
+
},
|
|
477
|
+
);
|
|
478
|
+
useInsertionEffect(() => {
|
|
479
|
+
if (!(transport instanceof DynamicChatTransport)) return undefined;
|
|
480
|
+
return registerTransportContext(transport, id);
|
|
481
|
+
}, [id, runtime, transport, transportContextOwner]);
|
|
482
|
+
|
|
483
|
+
if (
|
|
484
|
+
!(transport instanceof DynamicChatTransport) &&
|
|
485
|
+
configuredTransport instanceof AssistantChatTransport
|
|
486
|
+
) {
|
|
487
|
+
configuredTransport.setRuntime(runtime);
|
|
488
|
+
configuredTransport.__internal_setGetThreadListItem(
|
|
489
|
+
getCurrentThreadListItem,
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const subscribeToRuntime = useCallback(
|
|
494
|
+
(callback: () => void) => runtime.thread.subscribe(callback),
|
|
495
|
+
[runtime],
|
|
496
|
+
);
|
|
497
|
+
const getHistoryLoadingSnapshot = useCallback(
|
|
498
|
+
() => runtime.thread.getState().isLoading,
|
|
499
|
+
[runtime],
|
|
500
|
+
);
|
|
501
|
+
const isLoadingHistory = useSyncExternalStore(
|
|
502
|
+
subscribeToRuntime,
|
|
503
|
+
getHistoryLoadingSnapshot,
|
|
504
|
+
getHistoryLoadingSnapshot,
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
useEffect(() => {
|
|
508
|
+
const resumedStreamIds =
|
|
509
|
+
transport instanceof DynamicChatTransport
|
|
510
|
+
? transport.getResumedStreamIds()
|
|
511
|
+
: staticResumedStreamIds;
|
|
512
|
+
if (!pendingStreamId || resumedStreamIds.has(pendingStreamId)) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (isChatRunning) {
|
|
516
|
+
resumedStreamIds.add(pendingStreamId);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (isLoadingHistory) return;
|
|
520
|
+
resumeStream().catch((err: unknown) => {
|
|
521
|
+
console.warn("[assistant-ui] resumable: resume failed", err);
|
|
522
|
+
});
|
|
523
|
+
}, [
|
|
524
|
+
isChatRunning,
|
|
525
|
+
isLoadingHistory,
|
|
526
|
+
pendingStreamId,
|
|
527
|
+
resumeStream,
|
|
528
|
+
staticResumedStreamIds,
|
|
529
|
+
transport,
|
|
530
|
+
]);
|
|
531
|
+
|
|
532
|
+
return runtime;
|
|
533
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { UIMessage } from "ai";
|
|
2
|
+
import type { ChatTransport } from "ai";
|
|
3
|
+
import { useEffect, useInsertionEffect, useState } from "@ulearning/ai-tap";
|
|
4
|
+
import { DynamicChatTransport } from "./DynamicChatTransport";
|
|
5
|
+
|
|
6
|
+
export const useDynamicChatTransport = <UI_MESSAGE extends UIMessage>(
|
|
7
|
+
transport: ChatTransport<UI_MESSAGE>,
|
|
8
|
+
enabled = true,
|
|
9
|
+
): ChatTransport<UI_MESSAGE> => {
|
|
10
|
+
const [dynamicTransport] = useState(() =>
|
|
11
|
+
transport instanceof DynamicChatTransport
|
|
12
|
+
? transport
|
|
13
|
+
: new DynamicChatTransport(transport),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
useInsertionEffect(() => {
|
|
17
|
+
if (!enabled || dynamicTransport === transport) return;
|
|
18
|
+
dynamicTransport.setTransport(transport);
|
|
19
|
+
}, [dynamicTransport, enabled, transport]);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!enabled || dynamicTransport === transport) return;
|
|
22
|
+
dynamicTransport.flushTransportChange();
|
|
23
|
+
}, [dynamicTransport, enabled, transport]);
|
|
24
|
+
|
|
25
|
+
return enabled ? dynamicTransport : transport;
|
|
26
|
+
};
|