@assistant-ui/react-langchain 0.0.26 → 0.0.28
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/convertMessages.d.ts +2 -76
- package/dist/convertMessages.d.ts.map +1 -1
- package/dist/convertMessages.js +3 -150
- package/dist/convertMessages.js.map +1 -1
- package/dist/converter.d.ts +180 -0
- package/dist/converter.d.ts.map +1 -0
- package/dist/converter.js +213 -0
- package/dist/converter.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/streamingTiming.d.ts +1 -2
- package/dist/streamingTiming.d.ts.map +1 -1
- package/dist/streamingTiming.js +2 -36
- package/dist/streamingTiming.js.map +1 -1
- package/dist/types.d.ts +2 -55
- package/dist/types.d.ts.map +1 -1
- package/dist/useStreamRuntime.d.ts +2 -8
- package/dist/useStreamRuntime.d.ts.map +1 -1
- package/dist/useStreamRuntime.js +65 -57
- package/dist/useStreamRuntime.js.map +1 -1
- package/package.json +23 -9
- package/src/convertMessages.test.ts +99 -0
- package/src/convertMessages.ts +13 -208
- package/src/converter.ts +372 -0
- package/src/streamingTiming.test.tsx +37 -0
- package/src/streamingTiming.ts +5 -68
- package/src/types.ts +1 -39
- package/src/useStreamRuntime.test.tsx +183 -8
- package/src/useStreamRuntime.ts +120 -67
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { parseDataUrl, resolveFilePartSource } from "@assistant-ui/core/internal";
|
|
2
|
+
//#region src/converter.ts
|
|
3
|
+
const convertLangChainContentBlock = (part) => {
|
|
4
|
+
switch (part.type) {
|
|
5
|
+
case "text":
|
|
6
|
+
case "text_delta": return {
|
|
7
|
+
type: "text",
|
|
8
|
+
text: part.text
|
|
9
|
+
};
|
|
10
|
+
case "image_url": {
|
|
11
|
+
const image = typeof part.image_url === "string" ? part.image_url : part.image_url?.url;
|
|
12
|
+
if (!image) return null;
|
|
13
|
+
return {
|
|
14
|
+
type: "image",
|
|
15
|
+
image
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
case "file": return {
|
|
19
|
+
type: "file",
|
|
20
|
+
filename: part.metadata?.filename ?? "file",
|
|
21
|
+
data: part.source_type === "url" ? part.url : part.source_type === "id" ? part.id : part.data,
|
|
22
|
+
mimeType: part.mime_type ?? "application/octet-stream",
|
|
23
|
+
...(part.source_type === "url" || part.source_type === "id") && { sourceType: part.source_type }
|
|
24
|
+
};
|
|
25
|
+
case "audio": {
|
|
26
|
+
const mimeType = part.mime_type ?? "application/octet-stream";
|
|
27
|
+
const subtype = mimeType.startsWith("audio/") ? mimeType.slice(6) : void 0;
|
|
28
|
+
return {
|
|
29
|
+
type: "file",
|
|
30
|
+
filename: subtype ? `audio.${subtype}` : "audio",
|
|
31
|
+
data: part.data,
|
|
32
|
+
mimeType
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
case "thinking": return {
|
|
36
|
+
type: "reasoning",
|
|
37
|
+
text: part.thinking
|
|
38
|
+
};
|
|
39
|
+
case "reasoning": return {
|
|
40
|
+
type: "reasoning",
|
|
41
|
+
text: part.summary && part.summary.length > 0 ? part.summary.map((s) => s?.text ?? "").join("\n\n\n") : part.reasoning ?? ""
|
|
42
|
+
};
|
|
43
|
+
case "tool_use":
|
|
44
|
+
case "input_json_delta": return null;
|
|
45
|
+
default: return;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const hasVisibleText = (text) => typeof text === "string" && text.trim() !== "";
|
|
49
|
+
/**
|
|
50
|
+
* Audio output arrives outside the content array: providers leave `content`
|
|
51
|
+
* empty and put the spoken text in `additional_kwargs.audio.transcript`. The
|
|
52
|
+
* audio bytes stay behind because no provider reports their media type, and a
|
|
53
|
+
* streamed response carries raw PCM rather than a playable file.
|
|
54
|
+
*/
|
|
55
|
+
const withAudioTranscript = (parts, additionalKwargs) => {
|
|
56
|
+
const transcript = (additionalKwargs?.audio)?.transcript;
|
|
57
|
+
if (typeof transcript !== "string" || !hasVisibleText(transcript)) return parts;
|
|
58
|
+
if (parts.some((part) => part.type === "text" && hasVisibleText(part.text))) return parts;
|
|
59
|
+
return [...parts.filter((part) => part.type !== "text"), {
|
|
60
|
+
type: "text",
|
|
61
|
+
text: transcript
|
|
62
|
+
}];
|
|
63
|
+
};
|
|
64
|
+
const getCustomMetadata = (additionalKwargs) => additionalKwargs?.metadata ?? {};
|
|
65
|
+
const uiMessageToDataPart = (ui) => ({
|
|
66
|
+
type: "data",
|
|
67
|
+
name: ui.name,
|
|
68
|
+
data: ui.props
|
|
69
|
+
});
|
|
70
|
+
/**
|
|
71
|
+
* Audio media types that reach a provider's audio input through the LangChain
|
|
72
|
+
* `audio` block. langchain-core derives OpenAI's `input_audio.format` by
|
|
73
|
+
* splitting `mime_type` on `/`, and that format is a wav-or-mp3 enum, so
|
|
74
|
+
* `audio/mpeg` passes the converter and is rejected at the provider.
|
|
75
|
+
*/
|
|
76
|
+
const audioBlockMimeTypes = /* @__PURE__ */ new Map([
|
|
77
|
+
["audio/mp3", "audio/mp3"],
|
|
78
|
+
["audio/mpeg", "audio/mp3"],
|
|
79
|
+
["audio/wav", "audio/wav"],
|
|
80
|
+
["audio/wave", "audio/wav"],
|
|
81
|
+
["audio/x-wav", "audio/wav"]
|
|
82
|
+
]);
|
|
83
|
+
const getMessageContent = (msg) => {
|
|
84
|
+
const allContent = [...msg.content, ...msg.attachments?.flatMap((a) => a.content) ?? []];
|
|
85
|
+
const hasNonText = allContent.some((part) => part.type === "file" || part.type === "image" || part.type === "audio");
|
|
86
|
+
const hasText = allContent.some((part) => part.type === "text");
|
|
87
|
+
if (hasNonText && !hasText) allContent.unshift({
|
|
88
|
+
type: "text",
|
|
89
|
+
text: " "
|
|
90
|
+
});
|
|
91
|
+
const content = allContent.flatMap((part) => {
|
|
92
|
+
const type = part.type;
|
|
93
|
+
switch (type) {
|
|
94
|
+
case "text": return {
|
|
95
|
+
type: "text",
|
|
96
|
+
text: part.text
|
|
97
|
+
};
|
|
98
|
+
case "image": return {
|
|
99
|
+
type: "image_url",
|
|
100
|
+
image_url: { url: part.image }
|
|
101
|
+
};
|
|
102
|
+
case "file": {
|
|
103
|
+
const metadata = { filename: part.filename ?? "file" };
|
|
104
|
+
if (part.sourceType === "id") return {
|
|
105
|
+
type: "file",
|
|
106
|
+
id: part.data,
|
|
107
|
+
mime_type: part.mimeType,
|
|
108
|
+
filename: metadata.filename,
|
|
109
|
+
metadata,
|
|
110
|
+
source_type: "id"
|
|
111
|
+
};
|
|
112
|
+
const source = resolveFilePartSource(part);
|
|
113
|
+
if (source.kind === "url") return {
|
|
114
|
+
type: "file",
|
|
115
|
+
url: source.url,
|
|
116
|
+
mime_type: part.mimeType,
|
|
117
|
+
filename: metadata.filename,
|
|
118
|
+
metadata,
|
|
119
|
+
source_type: "url"
|
|
120
|
+
};
|
|
121
|
+
const audioMimeType = audioBlockMimeTypes.get(source.mimeType.toLowerCase());
|
|
122
|
+
if (audioMimeType) return {
|
|
123
|
+
type: "audio",
|
|
124
|
+
data: source.data,
|
|
125
|
+
mime_type: audioMimeType,
|
|
126
|
+
source_type: "base64"
|
|
127
|
+
};
|
|
128
|
+
return {
|
|
129
|
+
type: "file",
|
|
130
|
+
data: source.data,
|
|
131
|
+
mime_type: source.mimeType,
|
|
132
|
+
filename: metadata.filename,
|
|
133
|
+
metadata,
|
|
134
|
+
source_type: "base64"
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
case "audio": return {
|
|
138
|
+
type: "audio",
|
|
139
|
+
data: parseDataUrl(part.audio.data)?.data ?? part.audio.data,
|
|
140
|
+
mime_type: `audio/${part.audio.format}`,
|
|
141
|
+
source_type: "base64"
|
|
142
|
+
};
|
|
143
|
+
case "data": return [];
|
|
144
|
+
case "tool-call": throw new Error("Tool call appends are not supported.");
|
|
145
|
+
default: throw new Error(`Unsupported append message part type: ${type}`);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
if (content.length === 1 && content[0]?.type === "text") return content[0].text ?? "";
|
|
149
|
+
return content;
|
|
150
|
+
};
|
|
151
|
+
const reasoningTextLength = (part) => {
|
|
152
|
+
if (part.summary && part.summary.length > 0) return part.summary.map((s) => s?.text ?? "").join("\n\n\n").length;
|
|
153
|
+
return part.reasoning?.length ?? 0;
|
|
154
|
+
};
|
|
155
|
+
const createLangChainStreamingTimingAccessors = (getType) => {
|
|
156
|
+
const findAiMessage = (messages, messageId) => messages.find((message) => getType(message) === "ai" && message.id === messageId);
|
|
157
|
+
const getTextLength = (messages, messageId) => {
|
|
158
|
+
const message = findAiMessage(messages, messageId);
|
|
159
|
+
if (!message) return 0;
|
|
160
|
+
const content = message.content;
|
|
161
|
+
if (typeof content === "string") return content.length;
|
|
162
|
+
if (!Array.isArray(content)) return 0;
|
|
163
|
+
let len = 0;
|
|
164
|
+
for (const part of content) switch (part.type) {
|
|
165
|
+
case "text":
|
|
166
|
+
case "text_delta":
|
|
167
|
+
if (typeof part.text === "string") len += part.text.length;
|
|
168
|
+
break;
|
|
169
|
+
case "thinking":
|
|
170
|
+
if (typeof part.thinking === "string") len += part.thinking.length;
|
|
171
|
+
break;
|
|
172
|
+
case "reasoning": len += reasoningTextLength(part);
|
|
173
|
+
}
|
|
174
|
+
return len;
|
|
175
|
+
};
|
|
176
|
+
const getToolCallCount = (messages, messageId) => findAiMessage(messages, messageId)?.tool_calls?.length ?? 0;
|
|
177
|
+
const getAssistantMessageId = (messages) => {
|
|
178
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
179
|
+
const message = messages[i];
|
|
180
|
+
if (message && getType(message) === "ai" && message.id) return message.id;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
return {
|
|
184
|
+
getAssistantMessageId,
|
|
185
|
+
getTextLength,
|
|
186
|
+
getToolCallCount
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Resolve the assistant message a `UIMessage` belongs to: the parent id comes
|
|
191
|
+
* from `metadata.message_id` (Python SDK) or `metadata.id` (JS SDK).
|
|
192
|
+
*/
|
|
193
|
+
const getUIMessageParentId = (ui) => ui.metadata?.message_id ?? ui.metadata?.id;
|
|
194
|
+
/**
|
|
195
|
+
* Group the graph's accumulated `UIMessage`s by the assistant message they
|
|
196
|
+
* belong to. Non-array state and entries without a parent link are dropped.
|
|
197
|
+
*/
|
|
198
|
+
const groupUIMessagesByParent = (value) => {
|
|
199
|
+
const map = /* @__PURE__ */ new Map();
|
|
200
|
+
if (!Array.isArray(value)) return map;
|
|
201
|
+
for (const ui of value) {
|
|
202
|
+
const parentId = getUIMessageParentId(ui);
|
|
203
|
+
if (!parentId) continue;
|
|
204
|
+
const existing = map.get(parentId);
|
|
205
|
+
if (existing) existing.push(ui);
|
|
206
|
+
else map.set(parentId, [ui]);
|
|
207
|
+
}
|
|
208
|
+
return map;
|
|
209
|
+
};
|
|
210
|
+
//#endregion
|
|
211
|
+
export { convertLangChainContentBlock, createLangChainStreamingTimingAccessors, getCustomMetadata, getMessageContent, getUIMessageParentId, groupUIMessagesByParent, uiMessageToDataPart, withAudioTranscript };
|
|
212
|
+
|
|
213
|
+
//# sourceMappingURL=converter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"converter.js","names":["_exhaustiveCheck"],"sources":["../src/converter.ts"],"sourcesContent":["import type {\n AppendMessage,\n DataMessagePart,\n ThreadAssistantMessage,\n ThreadUserMessage,\n} from \"@assistant-ui/core\";\nimport {\n parseDataUrl,\n resolveFilePartSource,\n} from \"@assistant-ui/core/internal\";\nimport type { StreamingTimingAccessors } from \"@assistant-ui/core/react\";\n\n/** Known content block types from @langchain/core messages. */\nexport type LangChainContentBlock =\n | { type: \"text\"; text: string }\n | { type: \"text_delta\"; text: string }\n | { type: \"image_url\"; image_url: string | { url?: string } }\n | { type: \"thinking\"; thinking: string }\n | {\n type: \"reasoning\";\n summary?: Array<{ type: \"summary_text\"; text?: string }>;\n reasoning?: string;\n }\n | {\n type: \"file\";\n data: string;\n mime_type: string;\n source_type?: \"base64\";\n metadata?: { filename?: string };\n }\n | {\n type: \"file\";\n url: string;\n mime_type?: string;\n source_type: \"url\";\n metadata?: { filename?: string };\n }\n | {\n type: \"file\";\n id: string;\n mime_type?: string;\n source_type: \"id\";\n metadata?: { filename?: string };\n }\n | {\n type: \"audio\";\n data: string;\n mime_type: string;\n source_type: \"base64\";\n }\n | { type: \"tool_use\" | \"input_json_delta\" };\n\ntype ConvertedContentPart =\n | ThreadUserMessage[\"content\"][number]\n | ThreadAssistantMessage[\"content\"][number];\n\nexport const convertLangChainContentBlock = (\n part: LangChainContentBlock,\n): ConvertedContentPart | null | undefined => {\n const type = part.type;\n switch (type) {\n case \"text\":\n case \"text_delta\":\n return { type: \"text\" as const, text: part.text };\n case \"image_url\": {\n const image =\n typeof part.image_url === \"string\"\n ? part.image_url\n : part.image_url?.url;\n if (!image) return null;\n return { type: \"image\" as const, image };\n }\n case \"file\":\n return {\n type: \"file\" as const,\n filename: part.metadata?.filename ?? \"file\",\n data:\n part.source_type === \"url\"\n ? part.url\n : part.source_type === \"id\"\n ? part.id\n : part.data,\n mimeType: part.mime_type ?? \"application/octet-stream\",\n ...((part.source_type === \"url\" || part.source_type === \"id\") && {\n sourceType: part.source_type,\n }),\n };\n case \"audio\": {\n const mimeType = part.mime_type ?? \"application/octet-stream\";\n const subtype = mimeType.startsWith(\"audio/\")\n ? mimeType.slice(\"audio/\".length)\n : undefined;\n return {\n type: \"file\" as const,\n filename: subtype ? `audio.${subtype}` : \"audio\",\n data: part.data,\n mimeType,\n };\n }\n case \"thinking\":\n return { type: \"reasoning\" as const, text: part.thinking };\n case \"reasoning\":\n return {\n type: \"reasoning\" as const,\n text:\n part.summary && part.summary.length > 0\n ? part.summary.map((s) => s?.text ?? \"\").join(\"\\n\\n\\n\")\n : (part.reasoning ?? \"\"),\n };\n case \"tool_use\":\n case \"input_json_delta\":\n return null;\n default:\n return undefined;\n }\n};\n\nconst hasVisibleText = (text: unknown): boolean =>\n typeof text === \"string\" && text.trim() !== \"\";\n\n/**\n * Audio output arrives outside the content array: providers leave `content`\n * empty and put the spoken text in `additional_kwargs.audio.transcript`. The\n * audio bytes stay behind because no provider reports their media type, and a\n * streamed response carries raw PCM rather than a playable file.\n */\nexport const withAudioTranscript = <T extends { type: string; text?: unknown }>(\n parts: readonly T[],\n additionalKwargs: Record<string, unknown> | undefined,\n): readonly (T | { type: \"text\"; text: string })[] => {\n const audio = additionalKwargs?.audio as { transcript?: unknown } | undefined;\n const transcript = audio?.transcript;\n if (typeof transcript !== \"string\" || !hasVisibleText(transcript))\n return parts;\n if (parts.some((part) => part.type === \"text\" && hasVisibleText(part.text)))\n return parts;\n return [\n ...parts.filter((part) => part.type !== \"text\"),\n { type: \"text\" as const, text: transcript },\n ];\n};\n\nexport const getCustomMetadata = (\n additionalKwargs: Record<string, unknown> | undefined,\n): Record<string, unknown> =>\n (additionalKwargs?.metadata as Record<string, unknown>) ?? {};\n\nexport const uiMessageToDataPart = <\n TUIMessage extends { name: string; props: Record<string, unknown> },\n>(\n ui: TUIMessage,\n): DataMessagePart => ({\n type: \"data\",\n name: ui.name,\n data: ui.props,\n});\n\n/**\n * Audio media types that reach a provider's audio input through the LangChain\n * `audio` block. langchain-core derives OpenAI's `input_audio.format` by\n * splitting `mime_type` on `/`, and that format is a wav-or-mp3 enum, so\n * `audio/mpeg` passes the converter and is rejected at the provider.\n */\nconst audioBlockMimeTypes = new Map<string, \"audio/mp3\" | \"audio/wav\">([\n [\"audio/mp3\", \"audio/mp3\"],\n [\"audio/mpeg\", \"audio/mp3\"],\n [\"audio/wav\", \"audio/wav\"],\n [\"audio/wave\", \"audio/wav\"],\n [\"audio/x-wav\", \"audio/wav\"],\n]);\n\nexport const getMessageContent = (msg: AppendMessage) => {\n const allContent = [\n ...msg.content,\n ...(msg.attachments?.flatMap((a) => a.content) ?? []),\n ];\n\n const hasNonText = allContent.some(\n (part) =>\n part.type === \"file\" || part.type === \"image\" || part.type === \"audio\",\n );\n const hasText = allContent.some((part) => part.type === \"text\");\n if (hasNonText && !hasText) {\n allContent.unshift({ type: \"text\", text: \" \" });\n }\n\n const content = allContent.flatMap((part) => {\n const type = part.type;\n switch (type) {\n case \"text\":\n return { type: \"text\" as const, text: part.text };\n case \"image\":\n return { type: \"image_url\" as const, image_url: { url: part.image } };\n case \"file\": {\n const metadata = { filename: part.filename ?? \"file\" };\n if (part.sourceType === \"id\") {\n return {\n type: \"file\" as const,\n id: part.data,\n mime_type: part.mimeType,\n filename: metadata.filename,\n metadata,\n source_type: \"id\" as const,\n };\n }\n const source = resolveFilePartSource(part);\n if (source.kind === \"url\") {\n return {\n type: \"file\" as const,\n url: source.url,\n mime_type: part.mimeType,\n filename: metadata.filename,\n metadata,\n source_type: \"url\" as const,\n };\n }\n const audioMimeType = audioBlockMimeTypes.get(\n source.mimeType.toLowerCase(),\n );\n if (audioMimeType) {\n return {\n type: \"audio\" as const,\n data: source.data,\n mime_type: audioMimeType,\n source_type: \"base64\" as const,\n };\n }\n return {\n type: \"file\" as const,\n data: source.data,\n mime_type: source.mimeType,\n filename: metadata.filename,\n metadata,\n source_type: \"base64\" as const,\n };\n }\n case \"audio\": {\n const parsed = parseDataUrl(part.audio.data);\n return {\n type: \"audio\" as const,\n data: parsed?.data ?? part.audio.data,\n mime_type: `audio/${part.audio.format}`,\n source_type: \"base64\" as const,\n };\n }\n case \"data\":\n return [];\n case \"tool-call\":\n throw new Error(\"Tool call appends are not supported.\");\n default: {\n const _exhaustiveCheck: \"reasoning\" | \"source\" | \"generative-ui\" = type;\n throw new Error(\n `Unsupported append message part type: ${_exhaustiveCheck}`,\n );\n }\n }\n });\n\n if (content.length === 1 && content[0]?.type === \"text\") {\n return content[0].text ?? \"\";\n }\n return content;\n};\n\nconst reasoningTextLength = (part: {\n readonly summary?: ReadonlyArray<{ readonly text?: string }>;\n readonly reasoning?: string;\n}): number => {\n if (part.summary && part.summary.length > 0)\n return part.summary.map((s) => s?.text ?? \"\").join(\"\\n\\n\\n\").length;\n return part.reasoning?.length ?? 0;\n};\n\nexport const createLangChainStreamingTimingAccessors = <\n TMessage extends {\n id?: string | undefined;\n content?: unknown;\n tool_calls?: readonly unknown[] | undefined;\n },\n>(\n getType: (message: TMessage) => string,\n): StreamingTimingAccessors<TMessage> => {\n const findAiMessage = (\n messages: readonly TMessage[],\n messageId: string,\n ): TMessage | undefined =>\n messages.find(\n (message) => getType(message) === \"ai\" && message.id === messageId,\n );\n\n const getTextLength = (\n messages: readonly TMessage[],\n messageId: string,\n ): number => {\n const message = findAiMessage(messages, messageId);\n if (!message) return 0;\n const content = message.content;\n if (typeof content === \"string\") return content.length;\n if (!Array.isArray(content)) return 0;\n let len = 0;\n for (const part of content as readonly LangChainContentBlock[]) {\n switch (part.type) {\n case \"text\":\n case \"text_delta\":\n if (typeof part.text === \"string\") len += part.text.length;\n break;\n case \"thinking\":\n if (typeof part.thinking === \"string\") len += part.thinking.length;\n break;\n case \"reasoning\":\n len += reasoningTextLength(part);\n break;\n }\n }\n return len;\n };\n\n const getToolCallCount = (\n messages: readonly TMessage[],\n messageId: string,\n ): number => findAiMessage(messages, messageId)?.tool_calls?.length ?? 0;\n\n const getAssistantMessageId = (\n messages: readonly TMessage[],\n ): string | undefined => {\n for (let i = messages.length - 1; i >= 0; i--) {\n const message = messages[i];\n if (message && getType(message) === \"ai\" && message.id) return message.id;\n }\n return undefined;\n };\n\n return {\n getAssistantMessageId,\n getTextLength,\n getToolCallCount,\n };\n};\n\n/**\n * Resolve the assistant message a `UIMessage` belongs to: the parent id comes\n * from `metadata.message_id` (Python SDK) or `metadata.id` (JS SDK).\n */\nexport const getUIMessageParentId = (ui: {\n metadata?: { message_id?: string; id?: string } | undefined;\n}): string | undefined => ui.metadata?.message_id ?? ui.metadata?.id;\n\n/**\n * Group the graph's accumulated `UIMessage`s by the assistant message they\n * belong to. Non-array state and entries without a parent link are dropped.\n */\nexport const groupUIMessagesByParent = <\n T extends {\n metadata?: { message_id?: string; id?: string } | undefined;\n },\n>(\n value: unknown,\n): Map<string, T[]> => {\n const map = new Map<string, T[]>();\n if (!Array.isArray(value)) return map;\n for (const ui of value as T[]) {\n const parentId = getUIMessageParentId(ui);\n if (!parentId) continue;\n const existing = map.get(parentId);\n if (existing) {\n existing.push(ui);\n } else {\n map.set(parentId, [ui]);\n }\n }\n return map;\n};\n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LangChainContentBlock } from "./converter.js";
|
|
2
|
+
import { LangChainBaseMessage, LangChainToolCall, RemoveUIMessage, UIMessage, UseStreamRuntimeOptions } from "./types.js";
|
|
2
3
|
import { convertLangChainBaseMessage } from "./convertMessages.js";
|
|
3
4
|
import { useLangChainError, useLangChainInterruptState, useLangChainInterrupts, useLangChainRespond, useLangChainRespondAll, useLangChainSend, useLangChainSendCommand, useLangChainState, useLangChainStream, useLangChainSubagents, useLangChainSubgraphs, useLangChainSubmit, useLangChainToolCalls } from "./hooks.js";
|
|
4
5
|
import { useStreamRuntime } from "./useStreamRuntime.js";
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { LangChainBaseMessage } from "./types.js";
|
|
2
|
-
import { StreamingTimingAccessors } from "@assistant-ui/core/react";
|
|
3
2
|
import { MessageTiming } from "@assistant-ui/core";
|
|
4
3
|
//#region src/streamingTiming.d.ts
|
|
5
|
-
declare const langChainStreamingTimingAccessors: StreamingTimingAccessors<LangChainBaseMessage>;
|
|
4
|
+
declare const langChainStreamingTimingAccessors: import("@assistant-ui/core").StreamingTimingAccessors<LangChainBaseMessage>;
|
|
6
5
|
/**
|
|
7
6
|
* Tracks per-message streaming timing for LangChain messages. Delegates to
|
|
8
7
|
* the shared `useStreamingTiming` primitive in `@assistant-ui/core/react`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"streamingTiming.d.ts","names":[],"sources":["../src/streamingTiming.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"streamingTiming.d.ts","names":[],"sources":["../src/streamingTiming.ts"],"mappings":";;;cAQa,gEAAiC,yBAAA;;;;;;;cASjC,8BACX,mBAAmB,wBACnB,uBACC,eAAe"}
|
package/dist/streamingTiming.js
CHANGED
|
@@ -1,43 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { createLangChainStreamingTimingAccessors } from "./converter.js";
|
|
2
3
|
import { getMessageType } from "./convertMessages.js";
|
|
3
4
|
import { useStreamingTiming } from "@assistant-ui/core/react";
|
|
4
5
|
//#region src/streamingTiming.ts
|
|
5
|
-
const
|
|
6
|
-
const reasoningTextLength = (part) => {
|
|
7
|
-
if (part.summary && part.summary.length > 0) return part.summary.map((s) => s?.text ?? "").join("\n\n\n").length;
|
|
8
|
-
return part.reasoning?.length ?? 0;
|
|
9
|
-
};
|
|
10
|
-
const getTextLength = (messages, messageId) => {
|
|
11
|
-
const m = findAiMessage(messages, messageId);
|
|
12
|
-
if (!m) return 0;
|
|
13
|
-
const content = m.content;
|
|
14
|
-
if (typeof content === "string") return content.length;
|
|
15
|
-
if (!Array.isArray(content)) return 0;
|
|
16
|
-
let len = 0;
|
|
17
|
-
for (const part of content) switch (part.type) {
|
|
18
|
-
case "text":
|
|
19
|
-
case "text_delta":
|
|
20
|
-
if (typeof part.text === "string") len += part.text.length;
|
|
21
|
-
break;
|
|
22
|
-
case "thinking":
|
|
23
|
-
if (typeof part.thinking === "string") len += part.thinking.length;
|
|
24
|
-
break;
|
|
25
|
-
case "reasoning": len += reasoningTextLength(part);
|
|
26
|
-
}
|
|
27
|
-
return len;
|
|
28
|
-
};
|
|
29
|
-
const getToolCallCount = (messages, messageId) => findAiMessage(messages, messageId)?.tool_calls?.length ?? 0;
|
|
30
|
-
const getAssistantMessageId = (messages) => {
|
|
31
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
32
|
-
const m = messages[i];
|
|
33
|
-
if (m && getMessageType(m) === "ai" && m.id) return m.id;
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
const langChainStreamingTimingAccessors = {
|
|
37
|
-
getAssistantMessageId,
|
|
38
|
-
getTextLength,
|
|
39
|
-
getToolCallCount
|
|
40
|
-
};
|
|
6
|
+
const langChainStreamingTimingAccessors = createLangChainStreamingTimingAccessors(getMessageType);
|
|
41
7
|
/**
|
|
42
8
|
* Tracks per-message streaming timing for LangChain messages. Delegates to
|
|
43
9
|
* the shared `useStreamingTiming` primitive in `@assistant-ui/core/react`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"streamingTiming.js","names":[],"sources":["../src/streamingTiming.ts"],"sourcesContent":["\"use client\";\n\nimport type { MessageTiming } from \"@assistant-ui/core\";\nimport {
|
|
1
|
+
{"version":3,"file":"streamingTiming.js","names":[],"sources":["../src/streamingTiming.ts"],"sourcesContent":["\"use client\";\n\nimport type { MessageTiming } from \"@assistant-ui/core\";\nimport { useStreamingTiming } from \"@assistant-ui/core/react\";\nimport { createLangChainStreamingTimingAccessors } from \"./converter\";\nimport { getMessageType } from \"./convertMessages\";\nimport type { LangChainBaseMessage } from \"./types\";\n\nexport const langChainStreamingTimingAccessors =\n createLangChainStreamingTimingAccessors<LangChainBaseMessage>(getMessageType);\n\n/**\n * Tracks per-message streaming timing for LangChain messages. Delegates to\n * the shared `useStreamingTiming` primitive in `@assistant-ui/core/react`,\n * adapted to the `LangChainBaseMessage` shape (`_getType() -> \"ai\"`, content\n * blocks including text/thinking/reasoning, `tool_calls`).\n */\nexport const useLangChainStreamingTiming = (\n messages: readonly LangChainBaseMessage[],\n isRunning: boolean,\n): Record<string, MessageTiming> =>\n useStreamingTiming(messages, isRunning, langChainStreamingTimingAccessors);\n"],"mappings":";;;;;AAQA,MAAa,oCACX,wCAA8D,cAAc;;;;;;;AAQ9E,MAAa,+BACX,UACA,cAEA,mBAAmB,UAAU,WAAW,iCAAiC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,61 +1,8 @@
|
|
|
1
|
+
import { LangChainContentBlock } from "./converter.js";
|
|
1
2
|
import { AttachmentAdapter, DictationAdapter, ExternalStoreSharedOptions, FeedbackAdapter, MessageStatus, RealtimeVoiceAdapter, RemoteThreadListAdapter, SpeechSynthesisAdapter } from "@assistant-ui/core";
|
|
2
3
|
import { AnyStream, AssembledToolCall, SubagentDiscoverySnapshot, SubgraphDiscoverySnapshot, UseStreamOptions } from "@langchain/react";
|
|
3
4
|
import { AssistantCloud } from "assistant-cloud";
|
|
4
5
|
//#region src/types.d.ts
|
|
5
|
-
/** Known content block types from @langchain/core messages. */
|
|
6
|
-
type LangChainContentBlock = {
|
|
7
|
-
type: "text";
|
|
8
|
-
text: string;
|
|
9
|
-
} | {
|
|
10
|
-
type: "text_delta";
|
|
11
|
-
text: string;
|
|
12
|
-
} | {
|
|
13
|
-
type: "image_url";
|
|
14
|
-
image_url: string | {
|
|
15
|
-
url?: string;
|
|
16
|
-
};
|
|
17
|
-
} | {
|
|
18
|
-
type: "thinking";
|
|
19
|
-
thinking: string;
|
|
20
|
-
} | {
|
|
21
|
-
type: "reasoning";
|
|
22
|
-
summary?: Array<{
|
|
23
|
-
type: "summary_text";
|
|
24
|
-
text?: string;
|
|
25
|
-
}>;
|
|
26
|
-
reasoning?: string;
|
|
27
|
-
} | {
|
|
28
|
-
type: "file";
|
|
29
|
-
data: string;
|
|
30
|
-
mime_type: string;
|
|
31
|
-
source_type?: "base64";
|
|
32
|
-
metadata?: {
|
|
33
|
-
filename?: string;
|
|
34
|
-
};
|
|
35
|
-
} | {
|
|
36
|
-
type: "file";
|
|
37
|
-
url: string;
|
|
38
|
-
mime_type?: string;
|
|
39
|
-
source_type: "url";
|
|
40
|
-
metadata?: {
|
|
41
|
-
filename?: string;
|
|
42
|
-
};
|
|
43
|
-
} | {
|
|
44
|
-
type: "file";
|
|
45
|
-
id: string;
|
|
46
|
-
mime_type?: string;
|
|
47
|
-
source_type: "id";
|
|
48
|
-
metadata?: {
|
|
49
|
-
filename?: string;
|
|
50
|
-
};
|
|
51
|
-
} | {
|
|
52
|
-
type: "audio";
|
|
53
|
-
data: string;
|
|
54
|
-
mime_type: string;
|
|
55
|
-
source_type: "base64";
|
|
56
|
-
} | {
|
|
57
|
-
type: "tool_use" | "input_json_delta";
|
|
58
|
-
};
|
|
59
6
|
type LangChainToolCall = {
|
|
60
7
|
id: string;
|
|
61
8
|
name: string;
|
|
@@ -183,5 +130,5 @@ type LangChainRuntimeExtraOptions = ExternalStoreSharedOptions & {
|
|
|
183
130
|
};
|
|
184
131
|
type UseStreamRuntimeOptions = UseStreamOptions extends (infer O) ? O extends UseStreamOptions ? O & LangChainRuntimeExtraOptions : never : never;
|
|
185
132
|
//#endregion
|
|
186
|
-
export { LangChainBaseMessage, LangChainContentBlock, LangChainRuntimeExtraOptions, LangChainRuntimeExtras, LangChainToolCall, RemoveUIMessage, UIMessage, UseStreamRuntimeOptions };
|
|
133
|
+
export { LangChainBaseMessage, type LangChainContentBlock, LangChainRuntimeExtraOptions, LangChainRuntimeExtras, LangChainToolCall, RemoveUIMessage, UIMessage, UseStreamRuntimeOptions };
|
|
187
134
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;KAqBY;EACV;EACA;EACA,MAAM;;;;;;;;;KAUI,UACV,+BACA,eAAe,0BAA0B;EAEzC;EACA;EACA,MAAM;EACN,OAAO;EACP;IACE;IACA;IACA;IACA;IACA;IACA;KACC;;;;;;;KAQO;EACV;EACA;;;;;;;;;KAUU;EACV;EACA;EACA;EACA;EACA,oBAAoB;;EAEpB,sBAAsB;;EAEtB;;EAEA,SAAS;;EAET;;KAGU;EACV;IAAa;;EACb;IAAuB;IAAa;;EACpC,oBAAoB;EACpB,WAAW,oBAAoB;EAC/B,WAAW,oBAAoB;EAC/B,QAAQ;EACR;EACA,SACE,QAAQ,4CACR,UAAU,4BACP;EACL,UACE,mBACA,UAAU,4BACP;EACL,aACE,eAAe,yBACf,UAAU,4BACP;EACL,QAAQ;EACR;;KAGU,+BAA+B;;;;;;;;;EASzC,qBAAqB;EACrB,QAAQ;EACR;IAEM,cAAc;IACd,SAAS;IACT,YAAY;IACZ,QAAQ;IACR,WAAW;;;;;;;;EASjB;;;;;EAKA;;;;;EAKA,6BAA6B;;EAE7B,gBAAgB;IAAU;;;EAE1B,WAAW,qBAAqB;;;;;;;EAOhC;;KAOU,0BAA0B,gCAA+B,KACjE,UAAU,mBACR,IAAI"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/// <reference types="@assistant-ui/core/store" />
|
|
2
|
-
import {
|
|
2
|
+
import { groupUIMessagesByParent } from "./converter.js";
|
|
3
|
+
import { UseStreamRuntimeOptions } from "./types.js";
|
|
3
4
|
import { AppendMessage } from "@assistant-ui/core";
|
|
4
5
|
//#region src/useStreamRuntime.d.ts
|
|
5
6
|
declare const runConfigToSubmitOptions: (runConfig: AppendMessage["runConfig"]) => {
|
|
@@ -7,13 +8,6 @@ declare const runConfigToSubmitOptions: (runConfig: AppendMessage["runConfig"])
|
|
|
7
8
|
configurable: Record<string, unknown>;
|
|
8
9
|
};
|
|
9
10
|
} | undefined;
|
|
10
|
-
/**
|
|
11
|
-
* Group the graph's accumulated `UIMessage`s by the assistant message they
|
|
12
|
-
* belong to. Non-array state and entries without a parent link are dropped.
|
|
13
|
-
* The parent id comes from `metadata.message_id` (Python SDK) or
|
|
14
|
-
* `metadata.id` (JS SDK).
|
|
15
|
-
*/
|
|
16
|
-
declare const groupUIMessagesByParent: (value: unknown) => Map<string, UIMessage[]>;
|
|
17
11
|
/**
|
|
18
12
|
* Creates an assistant-ui runtime backed by LangChain's `useStream` hook.
|
|
19
13
|
* Accepts the same options as `useStream` from `@langchain/react`, plus
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useStreamRuntime.d.ts","names":[],"sources":["../src/useStreamRuntime.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"useStreamRuntime.d.ts","names":[],"sources":["../src/useStreamRuntime.ts"],"mappings":";;;;cA6Ca,2BACX,WAAW;;kBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;cA+iBA,mBAAoB,YAAY,yDAAuB"}
|
package/dist/useStreamRuntime.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { getMessageContent, groupUIMessagesByParent } from "./converter.js";
|
|
3
|
+
import { convertLangChainBaseMessage, getMessageType } from "./convertMessages.js";
|
|
3
4
|
import { langChainExtras } from "./runtimeExtras.js";
|
|
4
5
|
import { foldUIUpdates, mergeUIMessages } from "./uiMessages.js";
|
|
5
6
|
import { resolveForkCheckpoint } from "./resolveForkCheckpoint.js";
|
|
6
7
|
import { useLangChainStreamingTiming } from "./streamingTiming.js";
|
|
8
|
+
import { createCloudThreadListAdapterCreateFallback, createToolCallCancellationStub, scanPendingToolCalls } from "@assistant-ui/core/internal";
|
|
7
9
|
import { useAui, useAuiState } from "@assistant-ui/store";
|
|
8
10
|
import { useCloudThreadListAdapter, useExternalMessageConverter, useExternalStoreRuntime, useRemoteThreadListRuntime } from "@assistant-ui/core/react";
|
|
9
11
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
@@ -12,38 +14,22 @@ import { STREAM_CONTROLLER, useChannel, useStream } from "@langchain/react";
|
|
|
12
14
|
//#region src/useStreamRuntime.ts
|
|
13
15
|
const UI_CUSTOM_CHANNELS = ["custom"];
|
|
14
16
|
const runConfigToSubmitOptions = (runConfig) => runConfig?.custom ? { config: { configurable: runConfig.custom } } : void 0;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
*/
|
|
21
|
-
const groupUIMessagesByParent = (value) => {
|
|
22
|
-
const map = /* @__PURE__ */ new Map();
|
|
23
|
-
if (!Array.isArray(value)) return map;
|
|
24
|
-
for (const ui of value) {
|
|
25
|
-
const parentId = ui.metadata?.message_id ?? ui.metadata?.id;
|
|
26
|
-
if (!parentId) continue;
|
|
27
|
-
const existing = map.get(parentId);
|
|
28
|
-
if (existing) existing.push(ui);
|
|
29
|
-
else map.set(parentId, [ui]);
|
|
30
|
-
}
|
|
31
|
-
return map;
|
|
32
|
-
};
|
|
33
|
-
const getPendingToolCalls = (messages) => {
|
|
34
|
-
const pending = /* @__PURE__ */ new Map();
|
|
35
|
-
for (const m of messages) {
|
|
36
|
-
const type = getMessageType(m);
|
|
37
|
-
if (type === "ai") for (const tc of m.tool_calls ?? []) pending.set(tc.id, tc);
|
|
38
|
-
else if (type === "tool" && m.tool_call_id) pending.delete(m.tool_call_id);
|
|
39
|
-
}
|
|
40
|
-
return [...pending.values()];
|
|
41
|
-
};
|
|
17
|
+
const getPendingToolCalls = (messages) => scanPendingToolCalls(messages, (message) => {
|
|
18
|
+
const type = getMessageType(message);
|
|
19
|
+
if (type === "ai") return { toolCalls: message.tool_calls ?? [] };
|
|
20
|
+
if (type === "tool" && message.tool_call_id) return { toolCallId: message.tool_call_id };
|
|
21
|
+
}, (toolCall) => toolCall.id);
|
|
42
22
|
const toStagedHumanMessage = (msg, id = generateId()) => ({
|
|
43
23
|
id,
|
|
44
24
|
_getType: () => "human",
|
|
45
25
|
content: getMessageContent(msg)
|
|
46
26
|
});
|
|
27
|
+
const humanContentText = (content) => {
|
|
28
|
+
if (typeof content === "string") return content;
|
|
29
|
+
if (!Array.isArray(content)) return "";
|
|
30
|
+
return content.filter((part) => typeof part === "object" && part !== null && part.type === "text" && typeof part.text === "string").map((part) => part.text).join("");
|
|
31
|
+
};
|
|
32
|
+
const hasSameMessageContent = (a, b) => humanContentText(a.content) === humanContentText(b.content);
|
|
47
33
|
const truncateLangChainBaseMessages = (threadMessages, parentId) => {
|
|
48
34
|
if (parentId === null) return [];
|
|
49
35
|
const parentIndex = threadMessages.findIndex((m) => m.id === parentId);
|
|
@@ -128,16 +114,22 @@ const useStreamThreadRuntime = (options) => {
|
|
|
128
114
|
useEffect(() => {
|
|
129
115
|
if (stagedMessagesRef.current.size === 0) return;
|
|
130
116
|
const baseMessages = stagedBaseMessagesRef.current ?? stream.messages;
|
|
131
|
-
const baseMessageIds = new Set(baseMessages.flatMap((message) => message.id ? [message.id] : []));
|
|
132
117
|
const remainingStagedMessages = [];
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
118
|
+
const matchedBaseMessageIndexes = /* @__PURE__ */ new Set();
|
|
119
|
+
const visibleStagedIds = new Set(visibleMessagesRef.current.flatMap((m) => m.id ? [m.id] : []));
|
|
120
|
+
for (const [id, staged] of stagedMessagesRef.current) {
|
|
121
|
+
if (!visibleStagedIds.has(id)) continue;
|
|
122
|
+
if (baseMessages.some((message, index) => {
|
|
123
|
+
if (matchedBaseMessageIndexes.has(index)) return false;
|
|
124
|
+
if (message.id === id) {
|
|
125
|
+
matchedBaseMessageIndexes.add(index);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
if (!staged.reconcileOnEcho || index < staged.baseMessageCount || getMessageType(message) !== "human" || !hasSameMessageContent(message, staged.message)) return false;
|
|
129
|
+
matchedBaseMessageIndexes.add(index);
|
|
130
|
+
return true;
|
|
131
|
+
})) stagedMessagesRef.current.delete(id);
|
|
132
|
+
else remainingStagedMessages.push(staged.message);
|
|
141
133
|
}
|
|
142
134
|
if (remainingStagedMessages.length === 0) {
|
|
143
135
|
stagedBaseMessagesRef.current = null;
|
|
@@ -161,15 +153,27 @@ const useStreamThreadRuntime = (options) => {
|
|
|
161
153
|
runConfig: stagedMessagesRef.current.get(parentId).runConfig
|
|
162
154
|
};
|
|
163
155
|
};
|
|
164
|
-
const stageUserMessage = (msg) => {
|
|
156
|
+
const stageUserMessage = (msg, reconcileOnEcho = false) => {
|
|
165
157
|
const stagedMessage = toStagedHumanMessage(msg);
|
|
166
158
|
stagedMessagesRef.current.set(stagedMessage.id, {
|
|
167
159
|
message: stagedMessage,
|
|
168
|
-
runConfig: msg.runConfig
|
|
160
|
+
runConfig: msg.runConfig,
|
|
161
|
+
reconcileOnEcho,
|
|
162
|
+
baseMessageCount: streamRef.current.messages.length
|
|
169
163
|
});
|
|
170
164
|
const nextMessages = [...visibleMessagesRef.current, stagedMessage];
|
|
171
165
|
visibleMessagesRef.current = nextMessages;
|
|
172
166
|
setStagedMessages(nextMessages);
|
|
167
|
+
return stagedMessage;
|
|
168
|
+
};
|
|
169
|
+
const removeStagedMessage = (id) => {
|
|
170
|
+
if (!stagedMessagesRef.current.delete(id)) return;
|
|
171
|
+
const nextMessages = visibleMessagesRef.current.filter((message) => message.id !== id);
|
|
172
|
+
visibleMessagesRef.current = nextMessages;
|
|
173
|
+
if (stagedMessagesRef.current.size === 0) {
|
|
174
|
+
stagedBaseMessagesRef.current = null;
|
|
175
|
+
setStagedMessages(null);
|
|
176
|
+
} else setStagedMessages(nextMessages);
|
|
173
177
|
};
|
|
174
178
|
const extras = useMemo(() => langChainExtras.provide({
|
|
175
179
|
interrupt: stream.interrupt,
|
|
@@ -194,7 +198,7 @@ const useStreamThreadRuntime = (options) => {
|
|
|
194
198
|
]);
|
|
195
199
|
return useExternalStoreRuntime({
|
|
196
200
|
...pickExternalStoreSharedOptions(options),
|
|
197
|
-
isRunning:
|
|
201
|
+
isRunning: stream.isLoading,
|
|
198
202
|
isLoading: stream.isThreadLoading,
|
|
199
203
|
messages: threadMessages,
|
|
200
204
|
adapters,
|
|
@@ -206,23 +210,24 @@ const useStreamThreadRuntime = (options) => {
|
|
|
206
210
|
stageUserMessage(msg);
|
|
207
211
|
return;
|
|
208
212
|
}
|
|
213
|
+
const stagedMessageId = stageUserMessage(msg, true).id;
|
|
209
214
|
setActiveRunConfig(msg.runConfig);
|
|
210
215
|
const content = getMessageContent(msg);
|
|
211
|
-
const cancellations = autoCancelPendingToolCalls !== false ? getPendingToolCalls(streamRef.current.messages).map(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
216
|
+
const cancellations = autoCancelPendingToolCalls !== false ? getPendingToolCalls(streamRef.current.messages).map(createToolCallCancellationStub) : [];
|
|
217
|
+
try {
|
|
218
|
+
const { externalId } = await aui.threadListItem.initialize();
|
|
219
|
+
await streamRef.current.submit({ [messagesKey]: [...cancellations, {
|
|
220
|
+
id: stagedMessageId,
|
|
221
|
+
type: "human",
|
|
222
|
+
content
|
|
223
|
+
}] }, {
|
|
224
|
+
...runConfigToSubmitOptions(msg.runConfig),
|
|
225
|
+
...externalId != null ? { threadId: externalId } : {}
|
|
226
|
+
});
|
|
227
|
+
} catch (error) {
|
|
228
|
+
removeStagedMessage(stagedMessageId);
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
226
231
|
},
|
|
227
232
|
onAddToolResult: async ({ messageId, toolCallId, toolName, result, isError, artifact }) => {
|
|
228
233
|
const runConfig = runConfigByMessageIdRef.current.has(messageId) ? runConfigByMessageIdRef.current.get(messageId) : activeRunConfigRef.current;
|
|
@@ -276,7 +281,9 @@ const useStreamThreadRuntime = (options) => {
|
|
|
276
281
|
const stagedMessage = toStagedHumanMessage(message);
|
|
277
282
|
stagedMessagesRef.current.set(stagedMessage.id, {
|
|
278
283
|
message: stagedMessage,
|
|
279
|
-
runConfig: message.runConfig
|
|
284
|
+
runConfig: message.runConfig,
|
|
285
|
+
reconcileOnEcho: false,
|
|
286
|
+
baseMessageCount: 0
|
|
280
287
|
});
|
|
281
288
|
stagedBaseMessagesRef.current = truncated;
|
|
282
289
|
const nextMessages = [...truncated, stagedMessage];
|
|
@@ -333,9 +340,10 @@ const useStreamRuntime = (rawOptions) => {
|
|
|
333
340
|
const { cloud, unstable_threadListAdapter, create, delete: deleteFn, onThreadIdChange, ...options } = rawOptions;
|
|
334
341
|
const optionsRef = useRef(options);
|
|
335
342
|
optionsRef.current = options;
|
|
343
|
+
const aui = useAui();
|
|
336
344
|
const cloudAdapter = useCloudThreadListAdapter({
|
|
337
345
|
cloud,
|
|
338
|
-
create,
|
|
346
|
+
create: createCloudThreadListAdapterCreateFallback(create, aui.threadListItem),
|
|
339
347
|
delete: deleteFn
|
|
340
348
|
});
|
|
341
349
|
return useRemoteThreadListRuntime({
|