@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.
@@ -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 { LangChainBaseMessage, LangChainContentBlock, LangChainToolCall, RemoveUIMessage, UIMessage, UseStreamRuntimeOptions } from "./types.js";
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":";;;;cAmEa,mCAAmC,yBAAyB;;;;;;;cAa5D,8BACX,mBAAmB,wBACnB,uBACC,eAAe"}
1
+ {"version":3,"file":"streamingTiming.d.ts","names":[],"sources":["../src/streamingTiming.ts"],"mappings":";;;cAQa,gEAAiC,yBAAA;;;;;;;cASjC,8BACX,mBAAmB,wBACnB,uBACC,eAAe"}
@@ -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 findAiMessage = (messages, messageId) => messages.find((m) => getMessageType(m) === "ai" && m.id === messageId);
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 {\n useStreamingTiming,\n type StreamingTimingAccessors,\n} from \"@assistant-ui/core/react\";\nimport type { LangChainBaseMessage, LangChainContentBlock } from \"./types\";\nimport { getMessageType } from \"./convertMessages\";\n\nconst findAiMessage = (\n messages: readonly LangChainBaseMessage[],\n messageId: string,\n): LangChainBaseMessage | undefined =>\n messages.find((m) => getMessageType(m) === \"ai\" && m.id === messageId);\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\nconst getTextLength = (\n messages: readonly LangChainBaseMessage[],\n messageId: string,\n): number => {\n const m = findAiMessage(messages, messageId);\n if (!m) return 0;\n const content = m.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\nconst getToolCallCount = (\n messages: readonly LangChainBaseMessage[],\n messageId: string,\n): number => findAiMessage(messages, messageId)?.tool_calls?.length ?? 0;\n\nconst getAssistantMessageId = (\n messages: readonly LangChainBaseMessage[],\n): string | undefined => {\n for (let i = messages.length - 1; i >= 0; i--) {\n const m = messages[i];\n if (m && getMessageType(m) === \"ai\" && m.id) return m.id;\n }\n return undefined;\n};\n\nexport const langChainStreamingTimingAccessors: StreamingTimingAccessors<LangChainBaseMessage> =\n {\n getAssistantMessageId,\n getTextLength,\n getToolCallCount,\n };\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":";;;;AAUA,MAAM,iBACJ,UACA,cAEA,SAAS,MAAM,MAAM,eAAe,CAAC,MAAM,QAAQ,EAAE,OAAO,SAAS;AAEvE,MAAM,uBAAuB,SAGf;CACZ,IAAI,KAAK,WAAW,KAAK,QAAQ,SAAS,GACxC,OAAO,KAAK,QAAQ,KAAK,MAAM,GAAG,QAAQ,EAAE,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC;CAC/D,OAAO,KAAK,WAAW,UAAU;AACnC;AAEA,MAAM,iBACJ,UACA,cACW;CACX,MAAM,IAAI,cAAc,UAAU,SAAS;CAC3C,IAAI,CAAC,GAAG,OAAO;CACf,MAAM,UAAU,EAAE;CAClB,IAAI,OAAO,YAAY,UAAU,OAAO,QAAQ;CAChD,IAAI,CAAC,MAAM,QAAQ,OAAO,GAAG,OAAO;CACpC,IAAI,MAAM;CACV,KAAK,MAAM,QAAQ,SACjB,QAAQ,KAAK,MAAb;EACE,KAAK;EACL,KAAK;GACH,IAAI,OAAO,KAAK,SAAS,UAAU,OAAO,KAAK,KAAK;GACpD;EACF,KAAK;GACH,IAAI,OAAO,KAAK,aAAa,UAAU,OAAO,KAAK,SAAS;GAC5D;EACF,KAAK,aACH,OAAO,oBAAoB,IAAI;CAEnC;CAEF,OAAO;AACT;AAEA,MAAM,oBACJ,UACA,cACW,cAAc,UAAU,SAAS,CAAC,EAAE,YAAY,UAAU;AAEvE,MAAM,yBACJ,aACuB;CACvB,KAAK,IAAI,IAAI,SAAS,SAAS,GAAG,KAAK,GAAG,KAAK;EAC7C,MAAM,IAAI,SAAS;EACnB,IAAI,KAAK,eAAe,CAAC,MAAM,QAAQ,EAAE,IAAI,OAAO,EAAE;CACxD;AAEF;AAEA,MAAa,oCACX;CACE;CACA;CACA;AACF;;;;;;;AAQF,MAAa,+BACX,UACA,cAEA,mBAAmB,UAAU,WAAW,iCAAiC"}
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
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;KAoBY;EACN;EAAc;;EACd;EAAoB;;EACpB;EAAmB;IAAsB;;;EACzC;EAAkB;;EAElB;EACA,UAAU;IAAQ;IAAsB;;EACxC;;EAGA;EACA;EACA;EACA;EACA;IAAa;;;EAGb;EACA;EACA;EACA;EACA;IAAa;;;EAGb;EACA;EACA;EACA;EACA;IAAa;;;EAGb;EACA;EACA;EACA;;EAEA;;KAEM;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
+ {"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 { UIMessage, UseStreamRuntimeOptions } from "./types.js";
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":";;;cAsCa,2BACX,WAAW;;kBAAA;;;;;;;;;cAgBA,0BACX,mBACC,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;cAmfF,mBAAoB,YAAY,yDAAuB"}
1
+ {"version":3,"file":"useStreamRuntime.d.ts","names":[],"sources":["../src/useStreamRuntime.ts"],"mappings":";;;;cA6Ca,2BACX,WAAW;;kBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;cA+iBA,mBAAoB,YAAY,yDAAuB"}
@@ -1,9 +1,11 @@
1
1
  "use client";
2
- import { convertLangChainBaseMessage, getMessageContent, getMessageType } from "./convertMessages.js";
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
- * Group the graph's accumulated `UIMessage`s by the assistant message they
17
- * belong to. Non-array state and entries without a parent link are dropped.
18
- * The parent id comes from `metadata.message_id` (Python SDK) or
19
- * `metadata.id` (JS SDK).
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 seenStagedIds = /* @__PURE__ */ new Set();
134
- for (const message of visibleMessagesRef.current) {
135
- if (!message.id || seenStagedIds.has(message.id)) continue;
136
- if (baseMessageIds.has(message.id)) continue;
137
- const staged = stagedMessagesRef.current.get(message.id);
138
- if (!staged) continue;
139
- remainingStagedMessages.push(staged.message);
140
- seenStagedIds.add(message.id);
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: effectiveIsRunning,
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((t) => ({
212
- type: "tool",
213
- name: t.name,
214
- tool_call_id: t.id,
215
- content: JSON.stringify({ cancelled: true }),
216
- status: "error"
217
- })) : [];
218
- const { externalId } = await aui.threadListItem.initialize();
219
- await streamRef.current.submit({ [messagesKey]: [...cancellations, {
220
- type: "human",
221
- content
222
- }] }, {
223
- ...runConfigToSubmitOptions(msg.runConfig),
224
- ...externalId != null ? { threadId: externalId } : {}
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({