@assistant-ui/react-ai-sdk 1.3.6 → 1.3.8
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/ui/getVercelAIMessages.d.ts.map +1 -1
- package/dist/ui/getVercelAIMessages.js.map +1 -1
- package/dist/ui/use-chat/AssistantChatTransport.d.ts.map +1 -1
- package/dist/ui/use-chat/AssistantChatTransport.js.map +1 -1
- package/dist/ui/use-chat/useAISDKRuntime.d.ts.map +1 -1
- package/dist/ui/use-chat/useAISDKRuntime.js +22 -10
- package/dist/ui/use-chat/useAISDKRuntime.js.map +1 -1
- package/dist/ui/use-chat/useChatRuntime.d.ts.map +1 -1
- package/dist/ui/use-chat/useChatRuntime.js +1 -1
- package/dist/ui/use-chat/useChatRuntime.js.map +1 -1
- package/dist/ui/use-chat/useExternalHistory.d.ts.map +1 -1
- package/dist/ui/use-chat/useExternalHistory.js +137 -24
- package/dist/ui/use-chat/useExternalHistory.js.map +1 -1
- package/dist/ui/use-chat/useStreamingTiming.d.ts +11 -0
- package/dist/ui/use-chat/useStreamingTiming.d.ts.map +1 -0
- package/dist/ui/use-chat/useStreamingTiming.js +84 -0
- package/dist/ui/use-chat/useStreamingTiming.js.map +1 -0
- package/dist/ui/utils/convertMessage.d.ts +4 -4
- package/dist/ui/utils/convertMessage.d.ts.map +1 -1
- package/dist/ui/utils/convertMessage.js +94 -76
- package/dist/ui/utils/convertMessage.js.map +1 -1
- package/dist/ui/utils/sliceMessagesUntil.d.ts.map +1 -1
- package/dist/ui/utils/sliceMessagesUntil.js.map +1 -1
- package/package.json +11 -5
- package/src/ui/use-chat/useAISDKRuntime.test.ts +225 -0
- package/src/ui/use-chat/{useAISDKRuntime.tsx → useAISDKRuntime.ts} +34 -10
- package/src/ui/use-chat/{useChatRuntime.tsx → useChatRuntime.ts} +1 -1
- package/src/ui/use-chat/useExternalHistory.ts +283 -0
- package/src/ui/use-chat/useStreamingTiming.ts +102 -0
- package/src/ui/utils/convertMessage.test.ts +125 -0
- package/src/ui/utils/convertMessage.ts +107 -82
- package/src/ui/use-chat/useExternalHistory.tsx +0 -134
- /package/src/ui/{getVercelAIMessages.tsx → getVercelAIMessages.ts} +0 -0
- /package/src/ui/use-chat/{AssistantChatTransport.tsx → AssistantChatTransport.ts} +0 -0
- /package/src/ui/utils/{sliceMessagesUntil.tsx → sliceMessagesUntil.ts} +0 -0
|
@@ -13,69 +13,100 @@ import type { ReadonlyJSONObject } from "assistant-stream/utils";
|
|
|
13
13
|
|
|
14
14
|
type MessageMetadata = ThreadMessageLike["metadata"];
|
|
15
15
|
|
|
16
|
-
function stripClosingDelimiters(json: string) {
|
|
16
|
+
function stripClosingDelimiters(json: string): string {
|
|
17
17
|
return json.replace(/[}\]"]+$/, "");
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Resolves the interrupt fields for a tool call part.
|
|
22
|
+
*
|
|
23
|
+
* Two interrupt paths for tool approvals:
|
|
24
|
+
* 1. AI SDK server-side approval: approval-requested state with part.approval payload
|
|
25
|
+
* 2. Frontend tools: toolStatuses interrupt from context.human()
|
|
26
|
+
*/
|
|
27
|
+
function getToolInterrupt(
|
|
28
|
+
part: { state: string; approval?: unknown },
|
|
29
|
+
toolStatus: { type: string; payload?: unknown } | undefined,
|
|
30
|
+
): Record<string, unknown> {
|
|
31
|
+
if (part.state === "approval-requested" && "approval" in part) {
|
|
32
|
+
return {
|
|
33
|
+
interrupt: {
|
|
34
|
+
type: "human" as const,
|
|
35
|
+
payload: (part as { approval: unknown }).approval,
|
|
36
|
+
},
|
|
37
|
+
status: {
|
|
38
|
+
type: "requires-action" as const,
|
|
39
|
+
reason: "interrupt" as const,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (toolStatus?.type === "interrupt") {
|
|
45
|
+
return {
|
|
46
|
+
interrupt: toolStatus.payload,
|
|
47
|
+
status: {
|
|
48
|
+
type: "requires-action" as const,
|
|
49
|
+
reason: "interrupt" as const,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type MessageContent = Exclude<ThreadMessageLike["content"], string>;
|
|
58
|
+
|
|
59
|
+
function convertParts(
|
|
21
60
|
message: UIMessage,
|
|
22
61
|
metadata: useExternalMessageConverter.Metadata,
|
|
23
|
-
)
|
|
62
|
+
): MessageContent {
|
|
24
63
|
if (!message.parts || message.parts.length === 0) {
|
|
25
64
|
return [];
|
|
26
65
|
}
|
|
27
66
|
|
|
28
|
-
|
|
67
|
+
const converted = message.parts
|
|
29
68
|
.filter((p) => p.type !== "step-start" && p.type !== "file")
|
|
30
69
|
.map((part) => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
// Handle text parts
|
|
34
|
-
if (type === "text") {
|
|
70
|
+
if (part.type === "text") {
|
|
35
71
|
return {
|
|
36
72
|
type: "text",
|
|
37
73
|
text: part.text,
|
|
38
74
|
} satisfies TextMessagePart;
|
|
39
75
|
}
|
|
40
76
|
|
|
41
|
-
|
|
42
|
-
if (type === "reasoning") {
|
|
77
|
+
if (part.type === "reasoning") {
|
|
43
78
|
return {
|
|
44
79
|
type: "reasoning",
|
|
45
80
|
text: part.text,
|
|
46
81
|
} satisfies ReasoningMessagePart;
|
|
47
82
|
}
|
|
48
83
|
|
|
49
|
-
// Handle tool parts (both static tool-* and dynamic-tool)
|
|
50
|
-
// In AI SDK v6, isToolUIPart returns true for both static and dynamic tools
|
|
51
84
|
if (isToolUIPart(part)) {
|
|
52
|
-
// Use getToolName which works for both static and dynamic tools
|
|
53
85
|
const toolName = getToolName(part);
|
|
54
86
|
const toolCallId = part.toolCallId;
|
|
87
|
+
const args: ReadonlyJSONObject =
|
|
88
|
+
(part.input as ReadonlyJSONObject) || {};
|
|
55
89
|
|
|
56
|
-
// Extract args and result based on state
|
|
57
|
-
let args: ReadonlyJSONObject = {};
|
|
58
90
|
let result: unknown;
|
|
59
91
|
let isError = false;
|
|
60
92
|
|
|
61
|
-
if (
|
|
62
|
-
part.state === "input-streaming" ||
|
|
63
|
-
part.state === "input-available"
|
|
64
|
-
) {
|
|
65
|
-
args = (part.input as ReadonlyJSONObject) || {};
|
|
66
|
-
} else if (part.state === "output-available") {
|
|
67
|
-
args = (part.input as ReadonlyJSONObject) || {};
|
|
93
|
+
if (part.state === "output-available") {
|
|
68
94
|
result = part.output;
|
|
69
95
|
} else if (part.state === "output-error") {
|
|
70
|
-
args = (part.input as ReadonlyJSONObject) || {};
|
|
71
96
|
isError = true;
|
|
72
97
|
result = { error: part.errorText };
|
|
98
|
+
} else if (part.state === "output-denied") {
|
|
99
|
+
isError = true;
|
|
100
|
+
result = {
|
|
101
|
+
error:
|
|
102
|
+
(part as { approval: { reason?: string } }).approval.reason ||
|
|
103
|
+
"Tool approval denied",
|
|
104
|
+
};
|
|
73
105
|
}
|
|
74
106
|
|
|
75
107
|
let argsText = JSON.stringify(args);
|
|
76
108
|
if (part.state === "input-streaming") {
|
|
77
|
-
//
|
|
78
|
-
// these are added by the AI SDK in fix-json
|
|
109
|
+
// strip closing delimiters added by the AI SDK's fix-json
|
|
79
110
|
argsText = stripClosingDelimiters(argsText);
|
|
80
111
|
}
|
|
81
112
|
|
|
@@ -88,18 +119,11 @@ const convertParts = (
|
|
|
88
119
|
args,
|
|
89
120
|
result,
|
|
90
121
|
isError,
|
|
91
|
-
...(toolStatus
|
|
92
|
-
interrupt: toolStatus.payload,
|
|
93
|
-
status: {
|
|
94
|
-
type: "requires-action" as const,
|
|
95
|
-
reason: "interrupt",
|
|
96
|
-
},
|
|
97
|
-
}),
|
|
122
|
+
...getToolInterrupt(part, toolStatus),
|
|
98
123
|
} satisfies ToolCallMessagePart;
|
|
99
124
|
}
|
|
100
125
|
|
|
101
|
-
|
|
102
|
-
if (type === "source-url") {
|
|
126
|
+
if (part.type === "source-url") {
|
|
103
127
|
return {
|
|
104
128
|
type: "source",
|
|
105
129
|
sourceType: "url",
|
|
@@ -109,87 +133,88 @@ const convertParts = (
|
|
|
109
133
|
} satisfies SourceMessagePart;
|
|
110
134
|
}
|
|
111
135
|
|
|
112
|
-
|
|
113
|
-
if (type === "source-document") {
|
|
136
|
+
if (part.type === "source-document") {
|
|
114
137
|
console.warn(
|
|
115
|
-
|
|
138
|
+
"Source document parts are not yet supported in conversion",
|
|
116
139
|
);
|
|
117
140
|
return null;
|
|
118
141
|
}
|
|
119
142
|
|
|
120
|
-
|
|
121
|
-
if (type.startsWith("data-")) {
|
|
122
|
-
const name = type.substring(5);
|
|
143
|
+
if (part.type.startsWith("data-")) {
|
|
123
144
|
return {
|
|
124
145
|
type: "data",
|
|
125
|
-
name,
|
|
146
|
+
name: part.type.substring(5),
|
|
126
147
|
data: (part as any).data,
|
|
127
148
|
} satisfies DataMessagePart;
|
|
128
149
|
}
|
|
129
150
|
|
|
130
|
-
|
|
131
|
-
console.warn(`Unsupported message part type: ${type}`);
|
|
151
|
+
console.warn(`Unsupported message part type: ${part.type}`);
|
|
132
152
|
return null;
|
|
133
153
|
})
|
|
134
|
-
.filter(Boolean) as
|
|
135
|
-
|
|
154
|
+
.filter(Boolean) as MessageContent[number][];
|
|
155
|
+
|
|
156
|
+
const seenToolCallIds = new Set<string>();
|
|
157
|
+
return converted.filter((part) => {
|
|
158
|
+
if (part.type === "tool-call" && part.toolCallId != null) {
|
|
159
|
+
if (seenToolCallIds.has(part.toolCallId)) return false;
|
|
160
|
+
seenToolCallIds.add(part.toolCallId);
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
136
165
|
|
|
137
166
|
export const AISDKMessageConverter = unstable_createMessageConverter(
|
|
138
167
|
(message: UIMessage, metadata: useExternalMessageConverter.Metadata) => {
|
|
139
|
-
// UIMessage doesn't have createdAt, so we'll use current date or undefined
|
|
140
168
|
const createdAt = new Date();
|
|
169
|
+
const content = convertParts(message, metadata);
|
|
170
|
+
|
|
141
171
|
switch (message.role) {
|
|
142
172
|
case "user":
|
|
143
173
|
return {
|
|
144
174
|
role: "user",
|
|
145
175
|
id: message.id,
|
|
146
176
|
createdAt,
|
|
147
|
-
content
|
|
177
|
+
content,
|
|
148
178
|
attachments: message.parts
|
|
149
179
|
?.filter((p) => p.type === "file")
|
|
150
|
-
.map((part, idx) => {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
};
|
|
172
|
-
}),
|
|
180
|
+
.map((part, idx) => ({
|
|
181
|
+
id: idx.toString(),
|
|
182
|
+
type: part.mediaType.startsWith("image/") ? "image" : "file",
|
|
183
|
+
name: part.filename ?? "file",
|
|
184
|
+
content: [
|
|
185
|
+
part.mediaType.startsWith("image/")
|
|
186
|
+
? {
|
|
187
|
+
type: "image",
|
|
188
|
+
image: part.url,
|
|
189
|
+
filename: part.filename!,
|
|
190
|
+
}
|
|
191
|
+
: {
|
|
192
|
+
type: "file",
|
|
193
|
+
filename: part.filename!,
|
|
194
|
+
data: part.url,
|
|
195
|
+
mimeType: part.mediaType,
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
contentType: part.mediaType ?? "unknown/unknown",
|
|
199
|
+
status: { type: "complete" as const },
|
|
200
|
+
})),
|
|
173
201
|
metadata: message.metadata as MessageMetadata,
|
|
174
202
|
};
|
|
175
203
|
|
|
176
204
|
case "system":
|
|
205
|
+
case "assistant": {
|
|
206
|
+
const timing = metadata.messageTiming?.[message.id];
|
|
177
207
|
return {
|
|
178
|
-
role:
|
|
179
|
-
id: message.id,
|
|
180
|
-
createdAt,
|
|
181
|
-
content: convertParts(message, metadata),
|
|
182
|
-
metadata: message.metadata as MessageMetadata,
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
case "assistant":
|
|
186
|
-
return {
|
|
187
|
-
role: "assistant",
|
|
208
|
+
role: message.role,
|
|
188
209
|
id: message.id,
|
|
189
210
|
createdAt,
|
|
190
|
-
content
|
|
191
|
-
metadata:
|
|
211
|
+
content,
|
|
212
|
+
metadata: {
|
|
213
|
+
...(message.metadata as MessageMetadata),
|
|
214
|
+
...(timing && { timing }),
|
|
215
|
+
},
|
|
192
216
|
};
|
|
217
|
+
}
|
|
193
218
|
|
|
194
219
|
default:
|
|
195
220
|
console.warn(`Unsupported message role: ${message.role}`);
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
AssistantRuntime,
|
|
5
|
-
ThreadHistoryAdapter,
|
|
6
|
-
ThreadMessage,
|
|
7
|
-
MessageFormatAdapter,
|
|
8
|
-
getExternalStoreMessages,
|
|
9
|
-
MessageFormatRepository,
|
|
10
|
-
ExportedMessageRepository,
|
|
11
|
-
INTERNAL,
|
|
12
|
-
useAui,
|
|
13
|
-
} from "@assistant-ui/react";
|
|
14
|
-
import { useRef, useEffect, useState, RefObject, useCallback } from "react";
|
|
15
|
-
|
|
16
|
-
const { MessageRepository } = INTERNAL;
|
|
17
|
-
|
|
18
|
-
export const toExportedMessageRepository = <TMessage,>(
|
|
19
|
-
toThreadMessages: (messages: TMessage[]) => ThreadMessage[],
|
|
20
|
-
messages: MessageFormatRepository<TMessage>,
|
|
21
|
-
): ExportedMessageRepository => {
|
|
22
|
-
return {
|
|
23
|
-
headId: messages.headId!,
|
|
24
|
-
messages: messages.messages.map((m) => {
|
|
25
|
-
const message = toThreadMessages([m.message])[0]!;
|
|
26
|
-
return {
|
|
27
|
-
...m,
|
|
28
|
-
message,
|
|
29
|
-
};
|
|
30
|
-
}),
|
|
31
|
-
};
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export const useExternalHistory = <TMessage,>(
|
|
35
|
-
runtimeRef: RefObject<AssistantRuntime>,
|
|
36
|
-
historyAdapter: ThreadHistoryAdapter | undefined,
|
|
37
|
-
toThreadMessages: (messages: TMessage[]) => ThreadMessage[],
|
|
38
|
-
storageFormatAdapter: MessageFormatAdapter<TMessage, any>,
|
|
39
|
-
onSetMessages: (messages: TMessage[]) => void,
|
|
40
|
-
) => {
|
|
41
|
-
const loadedRef = useRef(false);
|
|
42
|
-
|
|
43
|
-
const aui = useAui();
|
|
44
|
-
const optionalThreadListItem = useCallback(
|
|
45
|
-
() => (aui.threadListItem.source ? aui.threadListItem() : null),
|
|
46
|
-
[aui],
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
50
|
-
|
|
51
|
-
const historyIds = useRef(new Set<string>());
|
|
52
|
-
|
|
53
|
-
const onSetMessagesRef = useRef<typeof onSetMessages>(() => onSetMessages);
|
|
54
|
-
useEffect(() => {
|
|
55
|
-
onSetMessagesRef.current = onSetMessages;
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// Load messages from history adapter on mount
|
|
59
|
-
useEffect(() => {
|
|
60
|
-
if (!historyAdapter || loadedRef.current) return;
|
|
61
|
-
|
|
62
|
-
const loadHistory = async () => {
|
|
63
|
-
setIsLoading(true);
|
|
64
|
-
try {
|
|
65
|
-
const repo = await historyAdapter
|
|
66
|
-
.withFormat?.(storageFormatAdapter)
|
|
67
|
-
.load();
|
|
68
|
-
if (repo && repo.messages.length > 0) {
|
|
69
|
-
const converted = toExportedMessageRepository(toThreadMessages, repo);
|
|
70
|
-
runtimeRef.current.thread.import(converted);
|
|
71
|
-
|
|
72
|
-
const tempRepo = new MessageRepository();
|
|
73
|
-
tempRepo.import(converted);
|
|
74
|
-
const messages = tempRepo.getMessages();
|
|
75
|
-
|
|
76
|
-
onSetMessagesRef.current(
|
|
77
|
-
messages.map(getExternalStoreMessages<TMessage>).flat(),
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
historyIds.current = new Set(
|
|
81
|
-
converted.messages.map((m) => m.message.id),
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
} catch (error) {
|
|
85
|
-
console.error("Failed to load message history:", error);
|
|
86
|
-
} finally {
|
|
87
|
-
setIsLoading(false);
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
if (!loadedRef.current) {
|
|
92
|
-
loadedRef.current = true;
|
|
93
|
-
if (!optionalThreadListItem()?.getState().remoteId) {
|
|
94
|
-
setIsLoading(false);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
loadHistory();
|
|
99
|
-
}
|
|
100
|
-
}, [
|
|
101
|
-
historyAdapter,
|
|
102
|
-
storageFormatAdapter,
|
|
103
|
-
toThreadMessages,
|
|
104
|
-
runtimeRef,
|
|
105
|
-
optionalThreadListItem,
|
|
106
|
-
]);
|
|
107
|
-
|
|
108
|
-
useEffect(() => {
|
|
109
|
-
return runtimeRef.current.thread.subscribe(async () => {
|
|
110
|
-
const { messages, isRunning } = runtimeRef.current.thread.getState();
|
|
111
|
-
if (isRunning) return;
|
|
112
|
-
|
|
113
|
-
for (let i = 0; i < messages.length; i++) {
|
|
114
|
-
const message = messages[i]!;
|
|
115
|
-
if (
|
|
116
|
-
message.status === undefined ||
|
|
117
|
-
message.status.type === "complete" ||
|
|
118
|
-
message.status.type === "incomplete"
|
|
119
|
-
) {
|
|
120
|
-
if (historyIds.current.has(message.id)) continue;
|
|
121
|
-
historyIds.current.add(message.id);
|
|
122
|
-
|
|
123
|
-
const parentId = i > 0 ? messages[i - 1]!.id : null;
|
|
124
|
-
await historyAdapter?.withFormat?.(storageFormatAdapter).append({
|
|
125
|
-
parentId,
|
|
126
|
-
message: getExternalStoreMessages<TMessage>(message)[0]!,
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
}, [historyAdapter, storageFormatAdapter, runtimeRef]);
|
|
132
|
-
|
|
133
|
-
return isLoading;
|
|
134
|
-
};
|
|
File without changes
|
|
File without changes
|
|
File without changes
|