@assistant-ui/react-ai-sdk 1.3.21 → 1.3.25
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 +14 -40
- package/dist/frontendTools.d.ts +2 -6
- package/dist/frontendTools.d.ts.map +1 -1
- package/dist/frontendTools.js +35 -3
- package/dist/frontendTools.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/modelContentEnvelope.d.ts +14 -0
- package/dist/modelContentEnvelope.d.ts.map +1 -0
- package/dist/modelContentEnvelope.js +20 -0
- package/dist/modelContentEnvelope.js.map +1 -0
- package/dist/ui/resumable.d.ts +20 -0
- package/dist/ui/resumable.d.ts.map +1 -0
- package/dist/ui/resumable.js +25 -0
- package/dist/ui/resumable.js.map +1 -0
- package/dist/ui/use-chat/AssistantChatTransport.d.ts +7 -1
- package/dist/ui/use-chat/AssistantChatTransport.d.ts.map +1 -1
- package/dist/ui/use-chat/AssistantChatTransport.js +73 -2
- package/dist/ui/use-chat/AssistantChatTransport.js.map +1 -1
- package/dist/ui/use-chat/useAISDKRuntime.d.ts +17 -2
- package/dist/ui/use-chat/useAISDKRuntime.d.ts.map +1 -1
- package/dist/ui/use-chat/useAISDKRuntime.js +35 -7
- package/dist/ui/use-chat/useAISDKRuntime.js.map +1 -1
- package/dist/ui/use-chat/useChatRuntime.d.ts +2 -0
- package/dist/ui/use-chat/useChatRuntime.d.ts.map +1 -1
- package/dist/ui/use-chat/useChatRuntime.js +31 -1
- package/dist/ui/use-chat/useChatRuntime.js.map +1 -1
- package/dist/ui/utils/convertMessage.d.ts +4 -0
- package/dist/ui/utils/convertMessage.d.ts.map +1 -1
- package/dist/ui/utils/convertMessage.js +87 -5
- package/dist/ui/utils/convertMessage.js.map +1 -1
- package/package.json +9 -10
- package/src/frontendTools.test.ts +128 -0
- package/src/frontendTools.ts +41 -6
- package/src/index.ts +8 -0
- package/src/modelContentEnvelope.ts +39 -0
- package/src/ui/resumable.ts +42 -0
- package/src/ui/use-chat/AssistantChatTransport.ts +104 -3
- package/src/ui/use-chat/useAISDKRuntime.test.ts +132 -0
- package/src/ui/use-chat/useAISDKRuntime.ts +69 -12
- package/src/ui/use-chat/useChatRuntime.ts +38 -0
- package/src/ui/utils/convertMessage.test.ts +331 -0
- package/src/ui/utils/convertMessage.ts +107 -16
|
@@ -8,9 +8,22 @@ import {
|
|
|
8
8
|
type UIMessage,
|
|
9
9
|
} from "ai";
|
|
10
10
|
import { toToolsJSONSchema } from "assistant-stream";
|
|
11
|
+
import {
|
|
12
|
+
RESUMABLE_STREAM_ID_HEADER,
|
|
13
|
+
type AssistantChatResumableOptions,
|
|
14
|
+
} from "../resumable";
|
|
11
15
|
|
|
12
16
|
type InitializableThreadListItem = Pick<ThreadListItemRuntime, "initialize">;
|
|
13
17
|
|
|
18
|
+
const FINISH_MARKER = '"type":"finish"';
|
|
19
|
+
const FINISH_BUFFER_LIMIT = 4096;
|
|
20
|
+
const FINISH_BUFFER_TAIL = 1024;
|
|
21
|
+
|
|
22
|
+
export type AssistantChatTransportInitOptions<UI_MESSAGE extends UIMessage> =
|
|
23
|
+
HttpChatTransportInitOptions<UI_MESSAGE> & {
|
|
24
|
+
resumable?: AssistantChatResumableOptions;
|
|
25
|
+
};
|
|
26
|
+
|
|
14
27
|
export class AssistantChatTransport<
|
|
15
28
|
UI_MESSAGE extends UIMessage,
|
|
16
29
|
> extends DefaultChatTransport<UI_MESSAGE> {
|
|
@@ -18,9 +31,22 @@ export class AssistantChatTransport<
|
|
|
18
31
|
private getThreadListItem:
|
|
19
32
|
| (() => InitializableThreadListItem | undefined)
|
|
20
33
|
| undefined;
|
|
21
|
-
|
|
34
|
+
private readonly resumable: AssistantChatResumableOptions | undefined;
|
|
35
|
+
|
|
36
|
+
constructor(initOptions?: AssistantChatTransportInitOptions<UI_MESSAGE>) {
|
|
37
|
+
const { resumable, ...rest } = initOptions ?? {};
|
|
38
|
+
const userFetch = rest.fetch;
|
|
39
|
+
const userPrepareReconnect = rest.prepareReconnectToStreamRequest;
|
|
40
|
+
|
|
22
41
|
super({
|
|
23
|
-
...
|
|
42
|
+
...rest,
|
|
43
|
+
...(resumable && {
|
|
44
|
+
fetch: wrapFetchWithResumable(resumable, userFetch),
|
|
45
|
+
prepareReconnectToStreamRequest: wrapPrepareReconnect(
|
|
46
|
+
resumable,
|
|
47
|
+
userPrepareReconnect,
|
|
48
|
+
),
|
|
49
|
+
}),
|
|
24
50
|
prepareSendMessagesRequest: async (options) => {
|
|
25
51
|
const context = this.runtime?.thread.getModelContext();
|
|
26
52
|
const threadListItem =
|
|
@@ -38,7 +64,7 @@ export class AssistantChatTransport<
|
|
|
38
64
|
},
|
|
39
65
|
};
|
|
40
66
|
const preparedRequest =
|
|
41
|
-
await
|
|
67
|
+
await rest.prepareSendMessagesRequest?.(optionsEx);
|
|
42
68
|
|
|
43
69
|
return {
|
|
44
70
|
...preparedRequest,
|
|
@@ -53,15 +79,90 @@ export class AssistantChatTransport<
|
|
|
53
79
|
};
|
|
54
80
|
},
|
|
55
81
|
});
|
|
82
|
+
|
|
83
|
+
this.resumable = resumable;
|
|
56
84
|
}
|
|
57
85
|
|
|
58
86
|
setRuntime(runtime: AssistantRuntime) {
|
|
59
87
|
this.runtime = runtime;
|
|
60
88
|
}
|
|
61
89
|
|
|
90
|
+
getResumableAdapter(): AssistantChatResumableOptions | undefined {
|
|
91
|
+
return this.resumable;
|
|
92
|
+
}
|
|
93
|
+
|
|
62
94
|
__internal_setGetThreadListItem(
|
|
63
95
|
getter: () => InitializableThreadListItem | undefined,
|
|
64
96
|
) {
|
|
65
97
|
this.getThreadListItem = getter;
|
|
66
98
|
}
|
|
67
99
|
}
|
|
100
|
+
|
|
101
|
+
function wrapFetchWithResumable(
|
|
102
|
+
resumable: AssistantChatResumableOptions,
|
|
103
|
+
userFetch: HttpChatTransportInitOptions<UIMessage>["fetch"],
|
|
104
|
+
): NonNullable<HttpChatTransportInitOptions<UIMessage>["fetch"]> {
|
|
105
|
+
const baseFetch: typeof globalThis.fetch = userFetch
|
|
106
|
+
? (input, init) => userFetch(input as RequestInfo | URL, init)
|
|
107
|
+
: globalThis.fetch.bind(globalThis);
|
|
108
|
+
|
|
109
|
+
return async (input, init) => {
|
|
110
|
+
const res = await baseFetch(input, init);
|
|
111
|
+
const id = res.headers.get(RESUMABLE_STREAM_ID_HEADER);
|
|
112
|
+
if (id) resumable.storage.setStreamId(id);
|
|
113
|
+
if (!res.body) return res;
|
|
114
|
+
|
|
115
|
+
const detectFinish = resumable.isFinishEvent ?? defaultIsFinishEvent;
|
|
116
|
+
// a single decoder is required so multi-byte sequences split across
|
|
117
|
+
// chunks buffer via stream: true rather than getting dropped.
|
|
118
|
+
const decoder = new TextDecoder();
|
|
119
|
+
let accumulator = "";
|
|
120
|
+
const tap = new TransformStream<Uint8Array, Uint8Array>({
|
|
121
|
+
transform(chunk, controller) {
|
|
122
|
+
controller.enqueue(chunk);
|
|
123
|
+
accumulator += decoder.decode(chunk, { stream: true });
|
|
124
|
+
if (detectFinish(chunk, accumulator)) {
|
|
125
|
+
resumable.storage.clear();
|
|
126
|
+
accumulator = "";
|
|
127
|
+
} else if (accumulator.length > FINISH_BUFFER_LIMIT) {
|
|
128
|
+
accumulator = accumulator.slice(-FINISH_BUFFER_TAIL);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return new Response(res.body.pipeThrough(tap), {
|
|
134
|
+
status: res.status,
|
|
135
|
+
statusText: res.statusText,
|
|
136
|
+
headers: res.headers,
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function defaultIsFinishEvent(_chunk: Uint8Array, accumulator: string) {
|
|
142
|
+
return accumulator.includes(FINISH_MARKER);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function wrapPrepareReconnect(
|
|
146
|
+
resumable: AssistantChatResumableOptions,
|
|
147
|
+
userPrepareReconnect: HttpChatTransportInitOptions<UIMessage>["prepareReconnectToStreamRequest"],
|
|
148
|
+
): NonNullable<
|
|
149
|
+
HttpChatTransportInitOptions<UIMessage>["prepareReconnectToStreamRequest"]
|
|
150
|
+
> {
|
|
151
|
+
return async (options) => {
|
|
152
|
+
const streamId = resumable.storage.getStreamId();
|
|
153
|
+
if (!streamId) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
"AssistantChatTransport: no resumable stream id available; nothing to resume",
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
const api =
|
|
159
|
+
typeof resumable.resumeApi === "function"
|
|
160
|
+
? resumable.resumeApi(streamId)
|
|
161
|
+
: resumable.resumeApi;
|
|
162
|
+
const userPrepared = await userPrepareReconnect?.({ ...options, api });
|
|
163
|
+
return {
|
|
164
|
+
...userPrepared,
|
|
165
|
+
api: userPrepared?.api ?? api,
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -140,6 +140,88 @@ describe("useAISDKRuntime", () => {
|
|
|
140
140
|
expect(chat.messages[0].parts[1].state).toBe("output-available");
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
+
it("appends a new user message without sending when startRun is false", async () => {
|
|
144
|
+
const chat = createChatHelpers([
|
|
145
|
+
{ id: "u1", role: "user", parts: [{ type: "text", text: "earlier" }] },
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
const { result } = renderHook(() => useAISDKRuntime(chat));
|
|
149
|
+
|
|
150
|
+
await waitFor(() => {
|
|
151
|
+
expect(result.current.thread.getState().messages.length).toBe(1);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
act(() => {
|
|
155
|
+
result.current.thread.append({
|
|
156
|
+
role: "user",
|
|
157
|
+
content: [{ type: "text", text: "hold this" }],
|
|
158
|
+
startRun: false,
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
await waitFor(() => {
|
|
163
|
+
expect(chat.setMessages).toHaveBeenCalled();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(chat.sendMessage).not.toHaveBeenCalled();
|
|
167
|
+
expect(chat.messages).toHaveLength(2);
|
|
168
|
+
expect(chat.messages[1]).toEqual(
|
|
169
|
+
expect.objectContaining({
|
|
170
|
+
role: "user",
|
|
171
|
+
id: expect.any(String),
|
|
172
|
+
parts: expect.arrayContaining([
|
|
173
|
+
expect.objectContaining({ type: "text", text: "hold this" }),
|
|
174
|
+
]),
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("edits without sending when startRun is false", async () => {
|
|
180
|
+
const chat = createChatHelpers([
|
|
181
|
+
{ id: "u1", role: "user", parts: [{ type: "text", text: "first" }] },
|
|
182
|
+
{
|
|
183
|
+
id: "a1",
|
|
184
|
+
role: "assistant",
|
|
185
|
+
parts: [{ type: "text", text: "first-answer" }],
|
|
186
|
+
},
|
|
187
|
+
{ id: "u2", role: "user", parts: [{ type: "text", text: "second" }] },
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
const { result } = renderHook(() => useAISDKRuntime(chat));
|
|
191
|
+
|
|
192
|
+
await waitFor(() => {
|
|
193
|
+
expect(result.current.thread.getState().messages.length).toBe(3);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
act(() => {
|
|
197
|
+
result.current.thread.append({
|
|
198
|
+
role: "user",
|
|
199
|
+
parentId: "u1",
|
|
200
|
+
content: [{ type: "text", text: "rewrite, no run" }],
|
|
201
|
+
startRun: false,
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
await waitFor(() => {
|
|
206
|
+
expect(chat.setMessages).toHaveBeenCalled();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
expect(chat.sendMessage).not.toHaveBeenCalled();
|
|
210
|
+
expect(chat.messages.map((m: any) => m.id)).toEqual([
|
|
211
|
+
"u1",
|
|
212
|
+
"a1",
|
|
213
|
+
expect.any(String),
|
|
214
|
+
]);
|
|
215
|
+
expect(chat.messages[2]).toEqual(
|
|
216
|
+
expect.objectContaining({
|
|
217
|
+
role: "user",
|
|
218
|
+
parts: expect.arrayContaining([
|
|
219
|
+
expect.objectContaining({ type: "text", text: "rewrite, no run" }),
|
|
220
|
+
]),
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
143
225
|
it("edit slices history to parentId and sends the edited message", async () => {
|
|
144
226
|
const chat = createChatHelpers([
|
|
145
227
|
{ id: "u1", role: "user", parts: [{ type: "text", text: "first" }] },
|
|
@@ -184,6 +266,56 @@ describe("useAISDKRuntime", () => {
|
|
|
184
266
|
);
|
|
185
267
|
});
|
|
186
268
|
|
|
269
|
+
it("forwards onResume so runtime.thread.resumeRun is delivered to the adapter", async () => {
|
|
270
|
+
const chat = createChatHelpers([
|
|
271
|
+
{ id: "u1", role: "user", parts: [{ type: "text", text: "first" }] },
|
|
272
|
+
]);
|
|
273
|
+
const onResume = vi.fn().mockResolvedValue(undefined);
|
|
274
|
+
|
|
275
|
+
const { result } = renderHook(() => useAISDKRuntime(chat, { onResume }));
|
|
276
|
+
|
|
277
|
+
await waitFor(() => {
|
|
278
|
+
expect(result.current.thread.getState().messages.length).toBe(1);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
act(() => {
|
|
282
|
+
result.current.thread.resumeRun({
|
|
283
|
+
parentId: "u1",
|
|
284
|
+
runConfig: { custom: { turnId: "t-42" } },
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
await waitFor(() => {
|
|
289
|
+
expect(onResume).toHaveBeenCalledTimes(1);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
expect(onResume).toHaveBeenCalledWith(
|
|
293
|
+
expect.objectContaining({
|
|
294
|
+
parentId: "u1",
|
|
295
|
+
sourceId: null,
|
|
296
|
+
runConfig: { custom: { turnId: "t-42" } },
|
|
297
|
+
}),
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("rejects when resumeRun is called without an onResume adapter", async () => {
|
|
302
|
+
const chat = createChatHelpers([
|
|
303
|
+
{ id: "u1", role: "user", parts: [{ type: "text", text: "first" }] },
|
|
304
|
+
]);
|
|
305
|
+
|
|
306
|
+
const { result } = renderHook(() => useAISDKRuntime(chat));
|
|
307
|
+
|
|
308
|
+
await waitFor(() => {
|
|
309
|
+
expect(result.current.thread.getState().messages.length).toBe(1);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
await expect(
|
|
313
|
+
result.current.thread.resumeRun({
|
|
314
|
+
parentId: "u1",
|
|
315
|
+
}) as unknown as Promise<void>,
|
|
316
|
+
).rejects.toThrow("Runtime does not support resuming runs.");
|
|
317
|
+
});
|
|
318
|
+
|
|
187
319
|
it("reload slices history and regenerates with metadata", async () => {
|
|
188
320
|
const chat = createChatHelpers([
|
|
189
321
|
{ id: "u1", role: "user", parts: [{ type: "text", text: "first" }] },
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useMemo, useRef } from "react";
|
|
4
4
|
import type { UIMessage, useChat, CreateUIMessage } from "@ai-sdk/react";
|
|
5
|
-
import { isToolUIPart } from "ai";
|
|
5
|
+
import { isToolUIPart, generateId } from "ai";
|
|
6
6
|
import {
|
|
7
7
|
useExternalStoreRuntime,
|
|
8
8
|
useRuntimeAdapters,
|
|
@@ -14,18 +14,22 @@ import type {
|
|
|
14
14
|
ThreadHistoryAdapter,
|
|
15
15
|
AssistantRuntime,
|
|
16
16
|
ThreadMessage,
|
|
17
|
+
ThreadSuggestion,
|
|
17
18
|
MessageFormatAdapter,
|
|
18
19
|
MessageFormatItem,
|
|
19
20
|
MessageFormatRepository,
|
|
20
21
|
AppendMessage,
|
|
21
22
|
RunConfig,
|
|
23
|
+
McpAppMetadata,
|
|
22
24
|
} from "@assistant-ui/core";
|
|
23
25
|
import { getExternalStoreMessages } from "@assistant-ui/core";
|
|
26
|
+
import type { ReadonlyJSONObject } from "assistant-stream/utils";
|
|
24
27
|
import { sliceMessagesUntil } from "../utils/sliceMessagesUntil";
|
|
25
28
|
import { toCreateMessage } from "../utils/toCreateMessage";
|
|
26
29
|
import { vercelAttachmentAdapter } from "../utils/vercelAttachmentAdapter";
|
|
27
30
|
import { getVercelAIMessages } from "../getVercelAIMessages";
|
|
28
31
|
import { AISDKMessageConverter } from "../utils/convertMessage";
|
|
32
|
+
import { wrapModelContentEnvelope } from "../../modelContentEnvelope";
|
|
29
33
|
import {
|
|
30
34
|
type AISDKStorageFormat,
|
|
31
35
|
aiSDKV6FormatAdapter,
|
|
@@ -42,6 +46,16 @@ export type CustomToCreateMessageFunction = <
|
|
|
42
46
|
message: AppendMessage,
|
|
43
47
|
) => CreateUIMessage<UI_MESSAGE>;
|
|
44
48
|
|
|
49
|
+
const toUIMessage = <UI_MESSAGE extends UIMessage>(
|
|
50
|
+
createMessage: CreateUIMessage<UI_MESSAGE>,
|
|
51
|
+
fallbackRole: UI_MESSAGE["role"],
|
|
52
|
+
): UI_MESSAGE =>
|
|
53
|
+
({
|
|
54
|
+
...createMessage,
|
|
55
|
+
id: createMessage.id ?? generateId(),
|
|
56
|
+
role: createMessage.role ?? fallbackRole,
|
|
57
|
+
}) as UI_MESSAGE;
|
|
58
|
+
|
|
45
59
|
export type AISDKRuntimeAdapter = {
|
|
46
60
|
adapters?:
|
|
47
61
|
| (NonNullable<ExternalStoreAdapter["adapters"]> & {
|
|
@@ -58,6 +72,21 @@ export type AISDKRuntimeAdapter = {
|
|
|
58
72
|
* @default true
|
|
59
73
|
*/
|
|
60
74
|
cancelPendingToolCallsOnSend?: boolean | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* Called when `runtime.thread.resumeRun(config)` is invoked.
|
|
77
|
+
*
|
|
78
|
+
* When omitted, `resumeRun` throws `"Runtime does not support resuming runs."`.
|
|
79
|
+
* Provide this to bridge resume invocations into a custom replay channel
|
|
80
|
+
* (for example, an SSE reconnect endpoint keyed by turn id).
|
|
81
|
+
*/
|
|
82
|
+
onResume?: ExternalStoreAdapter["onResume"];
|
|
83
|
+
/**
|
|
84
|
+
* Follow up suggestions to surface on the thread. Use this to drive
|
|
85
|
+
* dynamic suggestions from application state, tool results, or backend
|
|
86
|
+
* responses; flows into `thread.suggestions` and is rendered by
|
|
87
|
+
* components that read it (such as the shadcn `ThreadFollowupSuggestions`).
|
|
88
|
+
*/
|
|
89
|
+
suggestions?: readonly ThreadSuggestion[] | undefined;
|
|
61
90
|
};
|
|
62
91
|
|
|
63
92
|
export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
@@ -66,6 +95,8 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
66
95
|
adapters,
|
|
67
96
|
toCreateMessage: customToCreateMessage,
|
|
68
97
|
cancelPendingToolCallsOnSend = true,
|
|
98
|
+
onResume,
|
|
99
|
+
suggestions,
|
|
69
100
|
}: AISDKRuntimeAdapter = {},
|
|
70
101
|
) => {
|
|
71
102
|
const contextAdapters = useRuntimeAdapters();
|
|
@@ -75,6 +106,10 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
75
106
|
const toolArgsKeyOrderCacheRef = useRef<Map<string, Map<string, string[]>>>(
|
|
76
107
|
new Map(),
|
|
77
108
|
);
|
|
109
|
+
const toolLastInputCacheRef = useRef<Map<string, ReadonlyJSONObject>>(
|
|
110
|
+
new Map(),
|
|
111
|
+
);
|
|
112
|
+
const mcpAppMetadataCacheRef = useRef<Map<string, McpAppMetadata>>(new Map());
|
|
78
113
|
const lastRunConfigRef = useRef<RunConfig | undefined>(undefined);
|
|
79
114
|
|
|
80
115
|
const hasExecutingTools = Object.values(toolStatuses).some(
|
|
@@ -95,6 +130,8 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
95
130
|
toolStatuses,
|
|
96
131
|
messageTiming,
|
|
97
132
|
toolArgsKeyOrderCache: toolArgsKeyOrderCacheRef.current,
|
|
133
|
+
toolLastInputCache: toolLastInputCacheRef.current,
|
|
134
|
+
mcpAppMetadataCache: mcpAppMetadataCacheRef.current,
|
|
98
135
|
...(chatHelpers.error && { error: chatHelpers.error.message }),
|
|
99
136
|
}),
|
|
100
137
|
[toolStatuses, messageTiming, chatHelpers.error],
|
|
@@ -115,10 +152,14 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
115
152
|
getTools: () => runtimeRef.current.thread.getModelContext().tools,
|
|
116
153
|
onResult: (command) => {
|
|
117
154
|
if (command.type === "add-tool-result") {
|
|
155
|
+
const output =
|
|
156
|
+
command.modelContent !== undefined
|
|
157
|
+
? wrapModelContentEnvelope(command.result, command.modelContent)
|
|
158
|
+
: command.result;
|
|
118
159
|
chatHelpers.addToolResult({
|
|
119
160
|
tool: command.toolName,
|
|
120
161
|
toolCallId: command.toolCallId,
|
|
121
|
-
output
|
|
162
|
+
output,
|
|
122
163
|
options: { metadata: lastRunConfigRef.current },
|
|
123
164
|
});
|
|
124
165
|
}
|
|
@@ -240,27 +281,41 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
240
281
|
await toolInvocations.abort();
|
|
241
282
|
},
|
|
242
283
|
onNew: async (message) => {
|
|
243
|
-
lastRunConfigRef.current = message.runConfig;
|
|
244
|
-
await completePendingToolCalls();
|
|
245
|
-
|
|
246
284
|
const createMessage = (
|
|
247
285
|
customToCreateMessage ?? toCreateMessage
|
|
248
286
|
)<UI_MESSAGE>(message);
|
|
287
|
+
|
|
288
|
+
if (!(message.startRun ?? message.role === "user")) {
|
|
289
|
+
chatHelpers.setMessages((current) => [
|
|
290
|
+
...current,
|
|
291
|
+
toUIMessage<UI_MESSAGE>(createMessage, message.role),
|
|
292
|
+
]);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
lastRunConfigRef.current = message.runConfig;
|
|
297
|
+
await completePendingToolCalls();
|
|
249
298
|
await chatHelpers.sendMessage(createMessage, {
|
|
250
299
|
metadata: message.runConfig,
|
|
251
300
|
});
|
|
252
301
|
},
|
|
253
302
|
onEdit: async (message) => {
|
|
254
|
-
lastRunConfigRef.current = message.runConfig;
|
|
255
|
-
const newMessages = sliceMessagesUntil(
|
|
256
|
-
chatHelpers.messages,
|
|
257
|
-
message.parentId,
|
|
258
|
-
);
|
|
259
|
-
chatHelpers.setMessages(newMessages);
|
|
260
|
-
|
|
261
303
|
const createMessage = (
|
|
262
304
|
customToCreateMessage ?? toCreateMessage
|
|
263
305
|
)<UI_MESSAGE>(message);
|
|
306
|
+
|
|
307
|
+
if (!(message.startRun ?? message.role === "user")) {
|
|
308
|
+
chatHelpers.setMessages((current) => [
|
|
309
|
+
...sliceMessagesUntil(current, message.parentId),
|
|
310
|
+
toUIMessage<UI_MESSAGE>(createMessage, message.role),
|
|
311
|
+
]);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
lastRunConfigRef.current = message.runConfig;
|
|
316
|
+
chatHelpers.setMessages((current) =>
|
|
317
|
+
sliceMessagesUntil(current, message.parentId),
|
|
318
|
+
);
|
|
264
319
|
await chatHelpers.sendMessage(createMessage, {
|
|
265
320
|
metadata: message.runConfig,
|
|
266
321
|
});
|
|
@@ -295,6 +350,8 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
295
350
|
},
|
|
296
351
|
onResumeToolCall: (options) =>
|
|
297
352
|
toolInvocations.resume(options.toolCallId, options.payload),
|
|
353
|
+
...(onResume && { onResume }),
|
|
354
|
+
...(suggestions && { suggestions }),
|
|
298
355
|
adapters: {
|
|
299
356
|
attachments: vercelAttachmentAdapter,
|
|
300
357
|
...contextAdapters,
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "./useAISDKRuntime";
|
|
16
16
|
import type { ChatInit, ChatTransport } from "ai";
|
|
17
17
|
import { AssistantChatTransport } from "./AssistantChatTransport";
|
|
18
|
+
import type { AssistantChatResumableOptions } from "../resumable";
|
|
18
19
|
import { useEffect, useMemo, useRef } from "react";
|
|
19
20
|
|
|
20
21
|
export type UseChatRuntimeOptions<UI_MESSAGE extends UIMessage = UIMessage> =
|
|
@@ -22,6 +23,8 @@ export type UseChatRuntimeOptions<UI_MESSAGE extends UIMessage = UIMessage> =
|
|
|
22
23
|
cloud?: AssistantCloud | undefined;
|
|
23
24
|
adapters?: AISDKRuntimeAdapter["adapters"] | undefined;
|
|
24
25
|
toCreateMessage?: CustomToCreateMessageFunction;
|
|
26
|
+
onResume?: AISDKRuntimeAdapter["onResume"];
|
|
27
|
+
suggestions?: AISDKRuntimeAdapter["suggestions"];
|
|
25
28
|
};
|
|
26
29
|
|
|
27
30
|
const useDynamicChatTransport = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
@@ -50,6 +53,18 @@ const useDynamicChatTransport = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
50
53
|
return dynamicTransport;
|
|
51
54
|
};
|
|
52
55
|
|
|
56
|
+
const getResumableAdapter = <UI_MESSAGE extends UIMessage>(
|
|
57
|
+
transport: ChatTransport<UI_MESSAGE>,
|
|
58
|
+
): AssistantChatResumableOptions | undefined => {
|
|
59
|
+
if (transport instanceof AssistantChatTransport) {
|
|
60
|
+
return transport.getResumableAdapter();
|
|
61
|
+
}
|
|
62
|
+
const candidate = (transport as { getResumableAdapter?: () => unknown })
|
|
63
|
+
.getResumableAdapter;
|
|
64
|
+
if (typeof candidate !== "function") return undefined;
|
|
65
|
+
return candidate.call(transport) as AssistantChatResumableOptions | undefined;
|
|
66
|
+
};
|
|
67
|
+
|
|
53
68
|
const useChatThreadRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
54
69
|
options?: UseChatRuntimeOptions<UI_MESSAGE>,
|
|
55
70
|
): AssistantRuntime => {
|
|
@@ -57,6 +72,8 @@ const useChatThreadRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
57
72
|
adapters,
|
|
58
73
|
transport: transportOptions,
|
|
59
74
|
toCreateMessage,
|
|
75
|
+
onResume,
|
|
76
|
+
suggestions,
|
|
60
77
|
...chatOptions
|
|
61
78
|
} = options ?? {};
|
|
62
79
|
|
|
@@ -80,6 +97,8 @@ const useChatThreadRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
80
97
|
const runtime = useAISDKRuntime(chat, {
|
|
81
98
|
adapters,
|
|
82
99
|
...(toCreateMessage && { toCreateMessage }),
|
|
100
|
+
...(onResume && { onResume }),
|
|
101
|
+
...(suggestions && { suggestions }),
|
|
83
102
|
});
|
|
84
103
|
|
|
85
104
|
if (transport instanceof AssistantChatTransport) {
|
|
@@ -89,6 +108,25 @@ const useChatThreadRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
89
108
|
);
|
|
90
109
|
}
|
|
91
110
|
|
|
111
|
+
// biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
|
|
112
|
+
const resumeFiredRef = useRef(false);
|
|
113
|
+
// biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (resumeFiredRef.current) return;
|
|
116
|
+
const adapter = getResumableAdapter(transport);
|
|
117
|
+
if (!adapter) return;
|
|
118
|
+
const pending = adapter.storage.getStreamId();
|
|
119
|
+
if (!pending) return;
|
|
120
|
+
resumeFiredRef.current = true;
|
|
121
|
+
chat.resumeStream().catch((err: unknown) => {
|
|
122
|
+
console.warn(
|
|
123
|
+
"[assistant-ui] resumable: resume failed; clearing stored stream id",
|
|
124
|
+
err,
|
|
125
|
+
);
|
|
126
|
+
adapter.storage.clear();
|
|
127
|
+
});
|
|
128
|
+
}, [transport, chat]);
|
|
129
|
+
|
|
92
130
|
return runtime;
|
|
93
131
|
};
|
|
94
132
|
|