@assistant-ui/react-ai-sdk 1.3.7 → 1.3.9
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.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 +84 -75
- 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/dist/ui/utils/vercelAttachmentAdapter.js +1 -1
- package/dist/ui/utils/vercelAttachmentAdapter.js.map +1 -1
- package/package.json +14 -8
- 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/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 +98 -82
- package/src/ui/utils/vercelAttachmentAdapter.ts +1 -1
- 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/use-chat/{useChatRuntime.tsx → useChatRuntime.ts} +0 -0
- /package/src/ui/utils/{sliceMessagesUntil.tsx → sliceMessagesUntil.ts} +0 -0
|
@@ -13,14 +13,53 @@ 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
|
}
|
|
@@ -28,54 +67,46 @@ const convertParts = (
|
|
|
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,96 +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
|
|
154
|
+
.filter(Boolean) as MessageContent[number][];
|
|
135
155
|
|
|
136
156
|
const seenToolCallIds = new Set<string>();
|
|
137
|
-
return converted.filter((part
|
|
157
|
+
return converted.filter((part) => {
|
|
138
158
|
if (part.type === "tool-call" && part.toolCallId != null) {
|
|
139
159
|
if (seenToolCallIds.has(part.toolCallId)) return false;
|
|
140
160
|
seenToolCallIds.add(part.toolCallId);
|
|
141
161
|
}
|
|
142
162
|
return true;
|
|
143
163
|
});
|
|
144
|
-
}
|
|
164
|
+
}
|
|
145
165
|
|
|
146
166
|
export const AISDKMessageConverter = unstable_createMessageConverter(
|
|
147
167
|
(message: UIMessage, metadata: useExternalMessageConverter.Metadata) => {
|
|
148
|
-
// UIMessage doesn't have createdAt, so we'll use current date or undefined
|
|
149
168
|
const createdAt = new Date();
|
|
169
|
+
const content = convertParts(message, metadata);
|
|
170
|
+
|
|
150
171
|
switch (message.role) {
|
|
151
172
|
case "user":
|
|
152
173
|
return {
|
|
153
174
|
role: "user",
|
|
154
175
|
id: message.id,
|
|
155
176
|
createdAt,
|
|
156
|
-
content
|
|
177
|
+
content,
|
|
157
178
|
attachments: message.parts
|
|
158
179
|
?.filter((p) => p.type === "file")
|
|
159
|
-
.map((part, idx) => {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
};
|
|
181
|
-
}),
|
|
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
|
+
})),
|
|
182
201
|
metadata: message.metadata as MessageMetadata,
|
|
183
202
|
};
|
|
184
203
|
|
|
185
204
|
case "system":
|
|
205
|
+
case "assistant": {
|
|
206
|
+
const timing = metadata.messageTiming?.[message.id];
|
|
186
207
|
return {
|
|
187
|
-
role:
|
|
208
|
+
role: message.role,
|
|
188
209
|
id: message.id,
|
|
189
210
|
createdAt,
|
|
190
|
-
content
|
|
191
|
-
metadata:
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
return {
|
|
196
|
-
role: "assistant",
|
|
197
|
-
id: message.id,
|
|
198
|
-
createdAt,
|
|
199
|
-
content: convertParts(message, metadata),
|
|
200
|
-
metadata: message.metadata as MessageMetadata,
|
|
211
|
+
content,
|
|
212
|
+
metadata: {
|
|
213
|
+
...(message.metadata as MessageMetadata),
|
|
214
|
+
...(timing && { timing }),
|
|
215
|
+
},
|
|
201
216
|
};
|
|
217
|
+
}
|
|
202
218
|
|
|
203
219
|
default:
|
|
204
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
|
|
File without changes
|