@assistant-ui/react-langchain 0.0.28 → 0.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,7 +17,7 @@ npm install @assistant-ui/react @assistant-ui/react-langchain @langchain/react
17
17
  ```tsx
18
18
  import { useStreamRuntime } from "@assistant-ui/react-langchain";
19
19
  import { AssistantRuntimeProvider } from "@assistant-ui/react";
20
- import { Thread } from "@/components/assistant-ui/thread";
20
+ import { Thread } from "@/components/assistant-ui/elements/thread.aui";
21
21
 
22
22
  export function App() {
23
23
  const runtime = useStreamRuntime({
@@ -1 +1 @@
1
- {"version":3,"file":"convertMessages.d.ts","names":[],"sources":["../src/convertMessages.ts"],"mappings":";;;;;KAiBK,oCACH,4BAA4B;EAC1B,qBAAqB,YAAY;EACjC,gBAAgB,eAAe;;cAGtB,iBAAkB,SAAS;cA0B3B,8BACX,SAAS,sBACT,WAAU,sCACT,4BAA4B"}
1
+ {"version":3,"file":"convertMessages.d.ts","names":[],"sources":["../src/convertMessages.ts"],"mappings":";;;;;KAiBK,oCACH,4BAA4B;EAC1B,qBAAqB,YAAY;EACjC,gBAAgB,eAAe;;cAetB,iBAAkB,SAAS;cAuC3B,8BACX,SAAS,sBACT,WAAU,sCACT,4BAA4B"}
@@ -1,21 +1,35 @@
1
1
  "use client";
2
2
  import { convertLangChainContentBlock, getCustomMetadata, getMessageContent, uiMessageToDataPart, withAudioTranscript } from "./converter.js";
3
3
  //#region src/convertMessages.ts
4
+ const warnedMalformedMessages = /* @__PURE__ */ new Set();
5
+ const warnOnceInDevelopment = (message) => {
6
+ if (typeof process === "undefined" || process?.env?.NODE_ENV !== "development") return;
7
+ if (warnedMalformedMessages.has(message)) return;
8
+ warnedMalformedMessages.add(message);
9
+ console.warn(message);
10
+ };
4
11
  const getMessageType = (message) => {
5
12
  if (typeof message._getType === "function") return message._getType();
6
13
  if ("type" in message) return message.type;
7
- throw new Error("Cannot determine message type");
14
+ warnOnceInDevelopment("Cannot determine message type; rendering the message as system text");
15
+ return "unknown";
16
+ };
17
+ const contentBlocks = (content) => {
18
+ if (content == null) return [];
19
+ if (Array.isArray(content)) return content.filter((block) => typeof block === "object" && block !== null);
20
+ warnOnceInDevelopment(`Ignoring message content that is neither a string nor an array: ${typeof content}`);
21
+ return [];
8
22
  };
9
23
  const contentToParts = (content) => {
10
24
  if (typeof content === "string") return [{
11
25
  type: "text",
12
26
  text: content
13
27
  }];
14
- return content.map(convertLangChainContentBlock).filter((part) => part !== null && part !== void 0);
28
+ return contentBlocks(content).map(convertLangChainContentBlock).filter((part) => part !== null && part !== void 0);
15
29
  };
16
30
  const getStringContent = (content) => {
17
31
  if (typeof content === "string") return content;
18
- return content.filter((c) => c.type === "text").map((c) => c.text).join("");
32
+ return contentBlocks(content).filter((c) => c.type === "text").map((c) => c.text).join("");
19
33
  };
20
34
  const convertLangChainBaseMessage = (message, metadata = {}) => {
21
35
  switch (getMessageType(message)) {
@@ -73,7 +87,7 @@ const convertLangChainBaseMessage = (message, metadata = {}) => {
73
87
  id: message.id,
74
88
  content: [{
75
89
  type: "text",
76
- text: typeof message.content === "string" ? message.content : JSON.stringify(message.content)
90
+ text: typeof message.content === "string" ? message.content : JSON.stringify(message.content) ?? ""
77
91
  }]
78
92
  };
79
93
  }
@@ -1 +1 @@
1
- {"version":3,"file":"convertMessages.js","names":["parts"],"sources":["../src/convertMessages.ts"],"sourcesContent":["\"use client\";\n\nimport type { MessageTiming } from \"@assistant-ui/core\";\nimport type { useExternalMessageConverter } from \"@assistant-ui/core/react\";\nimport type { ReadonlyJSONObject } from \"assistant-stream/utils\";\nimport {\n convertLangChainContentBlock,\n getCustomMetadata,\n uiMessageToDataPart,\n withAudioTranscript,\n} from \"./converter\";\nimport type {\n LangChainBaseMessage,\n LangChainContentBlock,\n UIMessage,\n} from \"./types\";\n\ntype LangChainMessageConverterMetadata =\n useExternalMessageConverter.Metadata & {\n uiMessagesByParent?: Map<string, UIMessage[]>;\n messageTiming?: Record<string, MessageTiming>;\n };\n\nexport const getMessageType = (message: LangChainBaseMessage): string => {\n if (typeof message._getType === \"function\") return message._getType();\n if (\"type\" in message)\n return (message as Record<string, unknown>).type as string;\n throw new Error(\"Cannot determine message type\");\n};\n\nconst contentToParts = (content: unknown) => {\n if (typeof content === \"string\")\n return [{ type: \"text\" as const, text: content }];\n\n const parts = content as readonly LangChainContentBlock[];\n return parts\n .map(convertLangChainContentBlock)\n .filter((part) => part !== null && part !== undefined);\n};\n\nconst getStringContent = (content: unknown): string => {\n if (typeof content === \"string\") return content;\n const parts = content as readonly LangChainContentBlock[];\n return parts\n .filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n .map((c) => c.text)\n .join(\"\");\n};\n\nexport const convertLangChainBaseMessage = (\n message: LangChainBaseMessage,\n metadata: LangChainMessageConverterMetadata = {},\n): useExternalMessageConverter.Message => {\n const type = getMessageType(message);\n\n switch (type) {\n case \"system\":\n return {\n role: \"system\",\n id: message.id,\n content: [{ type: \"text\", text: getStringContent(message.content) }],\n metadata: {\n custom: getCustomMetadata(message.additional_kwargs),\n },\n };\n\n case \"human\":\n return {\n role: \"user\",\n id: message.id,\n content: contentToParts(message.content),\n metadata: {\n custom: getCustomMetadata(message.additional_kwargs),\n },\n };\n\n case \"ai\": {\n const toolCallParts =\n message.tool_calls?.map((tc) => ({\n type: \"tool-call\" as const,\n toolCallId: tc.id,\n toolName: tc.name,\n args: tc.args as ReadonlyJSONObject,\n argsText: JSON.stringify(tc.args),\n })) ?? [];\n\n const assistantStatus =\n typeof message.status === \"object\" ? message.status : undefined;\n\n const uiDataParts =\n (message.id\n ? metadata.uiMessagesByParent\n ?.get(message.id)\n ?.map(uiMessageToDataPart)\n : undefined) ?? [];\n\n const timing = metadata.messageTiming?.[message.id ?? \"\"];\n\n return {\n role: \"assistant\",\n id: message.id,\n content: [\n ...withAudioTranscript(\n contentToParts(message.content),\n message.additional_kwargs,\n ),\n ...toolCallParts,\n ...uiDataParts,\n ],\n metadata: {\n custom: getCustomMetadata(message.additional_kwargs),\n ...(timing && { timing }),\n },\n ...(assistantStatus && { status: assistantStatus }),\n };\n }\n\n case \"tool\":\n return {\n role: \"tool\",\n // `joinExternalMessages` only checks the name against the tool call\n // when it is non-null, so an empty name manufactures a mismatch.\n toolName: message.name || undefined,\n toolCallId: message.tool_call_id ?? \"\",\n result: message.content,\n artifact: message.artifact,\n isError: message.status === \"error\",\n };\n\n default:\n return {\n role: \"system\",\n id: message.id,\n content: [\n {\n type: \"text\",\n text:\n typeof message.content === \"string\"\n ? message.content\n : JSON.stringify(message.content),\n },\n ],\n };\n }\n};\n\nexport { getMessageContent } from \"./converter\";\n"],"mappings":";;;AAuBA,MAAa,kBAAkB,YAA0C;CACvE,IAAI,OAAO,QAAQ,aAAa,YAAY,OAAO,QAAQ,SAAS;CACpE,IAAI,UAAU,SACZ,OAAQ,QAAoC;CAC9C,MAAM,IAAI,MAAM,+BAA+B;AACjD;AAEA,MAAM,kBAAkB,YAAqB;CAC3C,IAAI,OAAO,YAAY,UACrB,OAAO,CAAC;EAAE,MAAM;EAAiB,MAAM;CAAQ,CAAC;CAGlD,OAAOA,QACJ,IAAI,4BAA4B,CAAC,CACjC,QAAQ,SAAS,SAAS,QAAQ,SAAS,KAAA,CAAS;AACzD;AAEA,MAAM,oBAAoB,YAA6B;CACrD,IAAI,OAAO,YAAY,UAAU,OAAO;CAExC,OAAOA,QACJ,QAAQ,MAA2C,EAAE,SAAS,MAAM,CAAC,CACrE,KAAK,MAAM,EAAE,IAAI,CAAC,CAClB,KAAK,EAAE;AACZ;AAEA,MAAa,+BACX,SACA,WAA8C,CAAC,MACP;CAGxC,QAFa,eAAe,OAEjB,GAAX;EACE,KAAK,UACH,OAAO;GACL,MAAM;GACN,IAAI,QAAQ;GACZ,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,iBAAiB,QAAQ,OAAO;GAAE,CAAC;GACnE,UAAU,EACR,QAAQ,kBAAkB,QAAQ,iBAAiB,EACrD;EACF;EAEF,KAAK,SACH,OAAO;GACL,MAAM;GACN,IAAI,QAAQ;GACZ,SAAS,eAAe,QAAQ,OAAO;GACvC,UAAU,EACR,QAAQ,kBAAkB,QAAQ,iBAAiB,EACrD;EACF;EAEF,KAAK,MAAM;GACT,MAAM,gBACJ,QAAQ,YAAY,KAAK,QAAQ;IAC/B,MAAM;IACN,YAAY,GAAG;IACf,UAAU,GAAG;IACb,MAAM,GAAG;IACT,UAAU,KAAK,UAAU,GAAG,IAAI;GAClC,EAAE,KAAK,CAAC;GAEV,MAAM,kBACJ,OAAO,QAAQ,WAAW,WAAW,QAAQ,SAAS,KAAA;GAExD,MAAM,eACH,QAAQ,KACL,SAAS,oBACL,IAAI,QAAQ,EAAE,CAAC,EACf,IAAI,mBAAmB,IAC3B,KAAA,MAAc,CAAC;GAErB,MAAM,SAAS,SAAS,gBAAgB,QAAQ,MAAM;GAEtD,OAAO;IACL,MAAM;IACN,IAAI,QAAQ;IACZ,SAAS;KACP,GAAG,oBACD,eAAe,QAAQ,OAAO,GAC9B,QAAQ,iBACV;KACA,GAAG;KACH,GAAG;IACL;IACA,UAAU;KACR,QAAQ,kBAAkB,QAAQ,iBAAiB;KACnD,GAAI,UAAU,EAAE,OAAO;IACzB;IACA,GAAI,mBAAmB,EAAE,QAAQ,gBAAgB;GACnD;EACF;EAEA,KAAK,QACH,OAAO;GACL,MAAM;GAGN,UAAU,QAAQ,QAAQ,KAAA;GAC1B,YAAY,QAAQ,gBAAgB;GACpC,QAAQ,QAAQ;GAChB,UAAU,QAAQ;GAClB,SAAS,QAAQ,WAAW;EAC9B;EAEF,SACE,OAAO;GACL,MAAM;GACN,IAAI,QAAQ;GACZ,SAAS,CACP;IACE,MAAM;IACN,MACE,OAAO,QAAQ,YAAY,WACvB,QAAQ,UACR,KAAK,UAAU,QAAQ,OAAO;GACtC,CACF;EACF;CACJ;AACF"}
1
+ {"version":3,"file":"convertMessages.js","names":[],"sources":["../src/convertMessages.ts"],"sourcesContent":["\"use client\";\n\nimport type { MessageTiming } from \"@assistant-ui/core\";\nimport type { useExternalMessageConverter } from \"@assistant-ui/core/react\";\nimport type { ReadonlyJSONObject } from \"assistant-stream/utils\";\nimport {\n convertLangChainContentBlock,\n getCustomMetadata,\n uiMessageToDataPart,\n withAudioTranscript,\n} from \"./converter\";\nimport type {\n LangChainBaseMessage,\n LangChainContentBlock,\n UIMessage,\n} from \"./types\";\n\ntype LangChainMessageConverterMetadata =\n useExternalMessageConverter.Metadata & {\n uiMessagesByParent?: Map<string, UIMessage[]>;\n messageTiming?: Record<string, MessageTiming>;\n };\n\nconst warnedMalformedMessages = new Set<string>();\nconst warnOnceInDevelopment = (message: string) => {\n if (\n typeof process === \"undefined\" ||\n process?.env?.NODE_ENV !== \"development\"\n )\n return;\n if (warnedMalformedMessages.has(message)) return;\n warnedMalformedMessages.add(message);\n console.warn(message);\n};\n\nexport const getMessageType = (message: LangChainBaseMessage): string => {\n if (typeof message._getType === \"function\") return message._getType();\n if (\"type\" in message)\n return (message as Record<string, unknown>).type as string;\n warnOnceInDevelopment(\n \"Cannot determine message type; rendering the message as system text\",\n );\n return \"unknown\";\n};\n\nconst contentBlocks = (content: unknown): readonly LangChainContentBlock[] => {\n if (content == null) return [];\n if (Array.isArray(content))\n return content.filter(\n (block) => typeof block === \"object\" && block !== null,\n );\n warnOnceInDevelopment(\n `Ignoring message content that is neither a string nor an array: ${typeof content}`,\n );\n return [];\n};\n\nconst contentToParts = (content: unknown) => {\n if (typeof content === \"string\")\n return [{ type: \"text\" as const, text: content }];\n\n return contentBlocks(content)\n .map(convertLangChainContentBlock)\n .filter((part) => part !== null && part !== undefined);\n};\n\nconst getStringContent = (content: unknown): string => {\n if (typeof content === \"string\") return content;\n return contentBlocks(content)\n .filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n .map((c) => c.text)\n .join(\"\");\n};\n\nexport const convertLangChainBaseMessage = (\n message: LangChainBaseMessage,\n metadata: LangChainMessageConverterMetadata = {},\n): useExternalMessageConverter.Message => {\n const type = getMessageType(message);\n\n switch (type) {\n case \"system\":\n return {\n role: \"system\",\n id: message.id,\n content: [{ type: \"text\", text: getStringContent(message.content) }],\n metadata: {\n custom: getCustomMetadata(message.additional_kwargs),\n },\n };\n\n case \"human\":\n return {\n role: \"user\",\n id: message.id,\n content: contentToParts(message.content),\n metadata: {\n custom: getCustomMetadata(message.additional_kwargs),\n },\n };\n\n case \"ai\": {\n const toolCallParts =\n message.tool_calls?.map((tc) => ({\n type: \"tool-call\" as const,\n toolCallId: tc.id,\n toolName: tc.name,\n args: tc.args as ReadonlyJSONObject,\n argsText: JSON.stringify(tc.args),\n })) ?? [];\n\n const assistantStatus =\n typeof message.status === \"object\" ? message.status : undefined;\n\n const uiDataParts =\n (message.id\n ? metadata.uiMessagesByParent\n ?.get(message.id)\n ?.map(uiMessageToDataPart)\n : undefined) ?? [];\n\n const timing = metadata.messageTiming?.[message.id ?? \"\"];\n\n return {\n role: \"assistant\",\n id: message.id,\n content: [\n ...withAudioTranscript(\n contentToParts(message.content),\n message.additional_kwargs,\n ),\n ...toolCallParts,\n ...uiDataParts,\n ],\n metadata: {\n custom: getCustomMetadata(message.additional_kwargs),\n ...(timing && { timing }),\n },\n ...(assistantStatus && { status: assistantStatus }),\n };\n }\n\n case \"tool\":\n return {\n role: \"tool\",\n // `joinExternalMessages` only checks the name against the tool call\n // when it is non-null, so an empty name manufactures a mismatch.\n toolName: message.name || undefined,\n toolCallId: message.tool_call_id ?? \"\",\n result: message.content,\n artifact: message.artifact,\n isError: message.status === \"error\",\n };\n\n default:\n return {\n role: \"system\",\n id: message.id,\n content: [\n {\n type: \"text\",\n text:\n typeof message.content === \"string\"\n ? message.content\n : (JSON.stringify(message.content) ?? \"\"),\n },\n ],\n };\n }\n};\n\nexport { getMessageContent } from \"./converter\";\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"converter.d.ts","names":[],"sources":["../src/converter.ts"],"mappings":";;;;KAaY;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;;KAED,uBACD,uCACA;cAES,+BACX,MAAM,0BACL;;;;;;;cAoEU,sBAAuB;EAAY;EAAc;GAC5D,gBAAgB,KAChB,kBAAkB,kDACP;EAAM;EAAc;;cAapB,oBACX,kBAAkB,wCACjB;cAGU,sBACX;EAAqB;EAAc,OAAO;GAE1C,IAAI,eACH;cAoBU,oBAAqB,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAsG1B,0CACX;EACE;EACA;EACA;GAGF,UAAU,SAAS,wBAClB,yBAAyB;;;;;cA8Df,uBAAwB;EACnC;IAAa;IAAqB;;;;;;;cAOvB,0BACX;EACE;IAAa;IAAqB;;GAGpC,mBACC,YAAY"}
1
+ {"version":3,"file":"converter.d.ts","names":[],"sources":["../src/converter.ts"],"mappings":";;;;KAaY;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;;KAED,uBACD,uCACA;cAES,+BACX,MAAM,0BACL;;;;;;;cAqEU,sBAAuB;EAAY;EAAc;GAC5D,gBAAgB,KAChB,kBAAkB,kDACP;EAAM;EAAc;;cAapB,oBACX,kBAAkB,wCACjB;cAGU,sBACX;EAAqB;EAAc,OAAO;GAE1C,IAAI,eACH;cAoBU,oBAAqB,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAuG1B,0CACX;EACE;EACA;EACA;GAGF,UAAU,SAAS,wBAClB,yBAAyB;;;;;cA+Df,uBAAwB;EACnC;IAAa;IAAqB;;;;;;;cAOvB,0BACX;EACE;IAAa;IAAqB;;GAGpC,mBACC,YAAY"}
package/dist/converter.js CHANGED
@@ -5,7 +5,7 @@ const convertLangChainContentBlock = (part) => {
5
5
  case "text":
6
6
  case "text_delta": return {
7
7
  type: "text",
8
- text: part.text
8
+ text: typeof part.text === "string" ? part.text : ""
9
9
  };
10
10
  case "image_url": {
11
11
  const image = typeof part.image_url === "string" ? part.image_url : part.image_url?.url;
@@ -32,14 +32,17 @@ const convertLangChainContentBlock = (part) => {
32
32
  mimeType
33
33
  };
34
34
  }
35
- case "thinking": return {
35
+ case "thinking": return hasVisibleText(part.thinking) ? {
36
36
  type: "reasoning",
37
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
- };
38
+ } : null;
39
+ case "reasoning": {
40
+ const text = getReasoningText(part);
41
+ return text ? {
42
+ type: "reasoning",
43
+ text
44
+ } : null;
45
+ }
43
46
  case "tool_use":
44
47
  case "input_json_delta": return null;
45
48
  default: return;
@@ -148,9 +151,11 @@ const getMessageContent = (msg) => {
148
151
  if (content.length === 1 && content[0]?.type === "text") return content[0].text ?? "";
149
152
  return content;
150
153
  };
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
+ const getReasoningText = (part) => {
155
+ const summary = part.summary?.map((s) => s?.text ?? "").join("\n\n\n") ?? "";
156
+ if (hasVisibleText(summary)) return summary;
157
+ const reasoning = part.reasoning ?? "";
158
+ return hasVisibleText(reasoning) ? reasoning : "";
154
159
  };
155
160
  const createLangChainStreamingTimingAccessors = (getType) => {
156
161
  const findAiMessage = (messages, messageId) => messages.find((message) => getType(message) === "ai" && message.id === messageId);
@@ -161,15 +166,18 @@ const createLangChainStreamingTimingAccessors = (getType) => {
161
166
  if (typeof content === "string") return content.length;
162
167
  if (!Array.isArray(content)) return 0;
163
168
  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);
169
+ for (const part of content) {
170
+ if (typeof part !== "object" || part === null) continue;
171
+ switch (part.type) {
172
+ case "text":
173
+ case "text_delta":
174
+ if (typeof part.text === "string") len += part.text.length;
175
+ break;
176
+ case "thinking":
177
+ if (hasVisibleText(part.thinking)) len += part.thinking.length;
178
+ break;
179
+ case "reasoning": len += getReasoningText(part).length;
180
+ }
173
181
  }
174
182
  return len;
175
183
  };
@@ -1 +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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"}
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 {\n type: \"text\" as const,\n text: typeof part.text === \"string\" ? part.text : \"\",\n };\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 hasVisibleText(part.thinking)\n ? { type: \"reasoning\" as const, text: part.thinking }\n : null;\n case \"reasoning\": {\n const text = getReasoningText(part);\n return text ? { type: \"reasoning\" as const, text } : null;\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 getReasoningText = (part: {\n readonly summary?: ReadonlyArray<{ readonly text?: string }>;\n readonly reasoning?: string;\n}): string => {\n const summary = part.summary?.map((s) => s?.text ?? \"\").join(\"\\n\\n\\n\") ?? \"\";\n if (hasVisibleText(summary)) return summary;\n const reasoning = part.reasoning ?? \"\";\n return hasVisibleText(reasoning) ? reasoning : \"\";\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 if (typeof part !== \"object\" || part === null) continue;\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 (hasVisibleText(part.thinking)) len += part.thinking.length;\n break;\n case \"reasoning\":\n len += getReasoningText(part).length;\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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"}
@@ -1 +1 @@
1
- {"version":3,"file":"useStreamRuntime.d.ts","names":[],"sources":["../src/useStreamRuntime.ts"],"mappings":";;;;cA6Ca,2BACX,WAAW;;kBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;cA+iBA,mBAAoB,YAAY,yDAAuB"}
1
+ {"version":3,"file":"useStreamRuntime.d.ts","names":[],"sources":["../src/useStreamRuntime.ts"],"mappings":";;;;cAoDa,2BACX,WAAW;;kBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;cAwjBA,mBAAoB,YAAY,yDAAuB"}
@@ -8,7 +8,7 @@ import { useLangChainStreamingTiming } from "./streamingTiming.js";
8
8
  import { createCloudThreadListAdapterCreateFallback, createToolCallCancellationStub, scanPendingToolCalls } from "@assistant-ui/core/internal";
9
9
  import { useAui, useAuiState } from "@assistant-ui/store";
10
10
  import { useCloudThreadListAdapter, useExternalMessageConverter, useExternalStoreRuntime, useRemoteThreadListRuntime } from "@assistant-ui/core/react";
11
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
11
+ import { useCallback, useEffect, useInsertionEffect, useMemo, useRef, useState } from "react";
12
12
  import { generateId, getExternalStoreMessages, pickExternalStoreSharedOptions } from "@assistant-ui/core";
13
13
  import { STREAM_CONTROLLER, useChannel, useStream } from "@langchain/react";
14
14
  //#region src/useStreamRuntime.ts
@@ -69,7 +69,9 @@ const useStreamThreadRuntime = (options) => {
69
69
  isRunning: effectiveIsRunning
70
70
  });
71
71
  const streamRef = useRef(stream);
72
- streamRef.current = stream;
72
+ useInsertionEffect(() => {
73
+ streamRef.current = stream;
74
+ }, [stream]);
73
75
  const activeRunConfigRef = useRef(void 0);
74
76
  const runConfigByMessageIdRef = useRef(/* @__PURE__ */ new Map());
75
77
  const activeThreadIdRef = useRef(externalId);
@@ -106,9 +108,13 @@ const useStreamThreadRuntime = (options) => {
106
108
  }
107
109
  }, [stream.messages]);
108
110
  const visibleMessagesRef = useRef(visibleMessages);
109
- visibleMessagesRef.current = visibleMessages;
111
+ useInsertionEffect(() => {
112
+ visibleMessagesRef.current = visibleMessages;
113
+ }, [visibleMessages]);
110
114
  const threadMessagesRef = useRef(threadMessages);
111
- threadMessagesRef.current = threadMessages;
115
+ useInsertionEffect(() => {
116
+ threadMessagesRef.current = threadMessages;
117
+ }, [threadMessages]);
112
118
  const stagedMessagesRef = useRef(/* @__PURE__ */ new Map());
113
119
  const stagedBaseMessagesRef = useRef(null);
114
120
  useEffect(() => {
@@ -338,8 +344,6 @@ const useStreamThreadRuntime = (options) => {
338
344
  */
339
345
  const useStreamRuntime = (rawOptions) => {
340
346
  const { cloud, unstable_threadListAdapter, create, delete: deleteFn, onThreadIdChange, ...options } = rawOptions;
341
- const optionsRef = useRef(options);
342
- optionsRef.current = options;
343
347
  const aui = useAui();
344
348
  const cloudAdapter = useCloudThreadListAdapter({
345
349
  cloud,
@@ -348,7 +352,7 @@ const useStreamRuntime = (rawOptions) => {
348
352
  });
349
353
  return useRemoteThreadListRuntime({
350
354
  runtimeHook: function RuntimeHook() {
351
- return useStreamThreadRuntime(optionsRef.current);
355
+ return useStreamThreadRuntime(options);
352
356
  },
353
357
  adapter: unstable_threadListAdapter ?? cloudAdapter,
354
358
  allowNesting: true,
@@ -1 +1 @@
1
- {"version":3,"file":"useStreamRuntime.js","names":[],"sources":["../src/useStreamRuntime.ts"],"sourcesContent":["/// <reference types=\"@assistant-ui/core/store\" />\n\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { AppendMessage, ToolExecutionStatus } from \"@assistant-ui/core\";\nimport {\n generateId,\n getExternalStoreMessages,\n pickExternalStoreSharedOptions,\n} from \"@assistant-ui/core\";\nimport type { ThreadMessage } from \"@assistant-ui/core\";\nimport {\n createCloudThreadListAdapterCreateFallback,\n createToolCallCancellationStub,\n scanPendingToolCalls,\n} from \"@assistant-ui/core/internal\";\nimport {\n useCloudThreadListAdapter,\n useExternalStoreRuntime,\n useExternalMessageConverter,\n useRemoteThreadListRuntime,\n} from \"@assistant-ui/core/react\";\nimport { useAui, useAuiState } from \"@assistant-ui/store\";\nimport { STREAM_CONTROLLER, useChannel, useStream } from \"@langchain/react\";\nimport type { Channel } from \"@langchain/react\";\nimport type {\n LangChainBaseMessage,\n LangChainToolCall,\n UIMessage,\n UseStreamRuntimeOptions,\n} from \"./types\";\nimport { groupUIMessagesByParent } from \"./converter\";\nexport { groupUIMessagesByParent } from \"./converter\";\nimport {\n convertLangChainBaseMessage,\n getMessageContent,\n getMessageType,\n} from \"./convertMessages\";\nimport { foldUIUpdates, mergeUIMessages } from \"./uiMessages\";\nimport { langChainExtras } from \"./runtimeExtras\";\nimport { resolveForkCheckpoint } from \"./resolveForkCheckpoint\";\nimport { useLangChainStreamingTiming } from \"./streamingTiming\";\n\nconst UI_CUSTOM_CHANNELS: readonly Channel[] = [\"custom\"];\n\nexport const runConfigToSubmitOptions = (\n runConfig: AppendMessage[\"runConfig\"],\n) =>\n runConfig?.custom\n ? { config: { configurable: runConfig.custom } }\n : undefined;\n\ntype NormalizedRunConfigOptions = NonNullable<\n ReturnType<typeof runConfigToSubmitOptions>\n>;\n\nconst getPendingToolCalls = (\n messages: readonly LangChainBaseMessage[],\n): LangChainToolCall[] =>\n scanPendingToolCalls(\n messages,\n (message) => {\n const type = getMessageType(message);\n if (type === \"ai\") return { toolCalls: message.tool_calls ?? [] };\n if (type === \"tool\" && message.tool_call_id) {\n return { toolCallId: message.tool_call_id };\n }\n return undefined;\n },\n (toolCall) => toolCall.id,\n );\n\nconst toStagedHumanMessage = (\n msg: AppendMessage,\n id = generateId(),\n): LangChainBaseMessage & { id: string } => ({\n id,\n _getType: () => \"human\",\n content: getMessageContent(msg),\n});\n\nconst humanContentText = (content: LangChainBaseMessage[\"content\"]) => {\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return \"\";\n return content\n .filter(\n (part): part is { type: \"text\"; text: string } =>\n typeof part === \"object\" &&\n part !== null &&\n part.type === \"text\" &&\n typeof part.text === \"string\",\n )\n .map((part) => part.text)\n .join(\"\");\n};\n\nconst hasSameMessageContent = (\n a: LangChainBaseMessage,\n b: LangChainBaseMessage,\n) => humanContentText(a.content) === humanContentText(b.content);\n\nconst truncateLangChainBaseMessages = (\n threadMessages: readonly ThreadMessage[],\n parentId: string | null,\n): LangChainBaseMessage[] => {\n if (parentId === null) return [];\n const parentIndex = threadMessages.findIndex((m) => m.id === parentId);\n if (parentIndex === -1) return [];\n const truncated: LangChainBaseMessage[] = [];\n for (let i = 0; i <= parentIndex && i < threadMessages.length; i++) {\n truncated.push(\n ...getExternalStoreMessages<LangChainBaseMessage>(threadMessages[i]!),\n );\n }\n return truncated;\n};\n\ntype DistributiveOmit<T, K extends keyof any> = T extends unknown\n ? Omit<T, K>\n : never;\n\nconst useStreamThreadRuntime = (\n options: DistributiveOmit<\n UseStreamRuntimeOptions,\n \"cloud\" | \"unstable_threadListAdapter\" | \"create\" | \"delete\"\n >,\n) => {\n const { adapters, autoCancelPendingToolCalls, unstable_allowCancellation } =\n options;\n const aui = useAui();\n const messagesKey = options.messagesKey ?? \"messages\";\n const uiStateKey = options.uiStateKey ?? \"ui\";\n\n const externalId = useAuiState((s) => s.threadListItem.externalId) as\n | string\n | null;\n // Object.assign preserves the discriminated transport union; object spread\n // collapses its arms and no longer satisfies UseStreamOptions.\n const stream = useStream(\n Object.assign({}, options, { threadId: externalId }),\n );\n const [stagedMessages, setStagedMessages] = useState<\n LangChainBaseMessage[] | null\n >(null);\n\n const [toolStatuses, setToolStatuses] = useState<\n Record<string, ToolExecutionStatus>\n >({});\n const hasExecutingTools = Object.values(toolStatuses).some(\n (s) => s?.type === \"executing\",\n );\n const effectiveIsRunning = stream.isLoading || hasExecutingTools;\n\n const uiStateValue = stream.values[uiStateKey];\n\n const customEvents = useChannel(stream, UI_CUSTOM_CHANNELS);\n const liveUiMessages = useMemo(\n () => foldUIUpdates(customEvents),\n [customEvents],\n );\n\n const mergedUiMessages = useMemo(\n () => mergeUIMessages(liveUiMessages, uiStateValue),\n [liveUiMessages, uiStateValue],\n );\n\n const visibleMessages =\n stagedMessages ?? (stream.messages as LangChainBaseMessage[]);\n\n const messageTiming = useLangChainStreamingTiming(\n visibleMessages,\n effectiveIsRunning,\n );\n\n const convertWithUI = useMemo<\n useExternalMessageConverter.Callback<LangChainBaseMessage>\n >(() => {\n const uiMessagesByParent =\n groupUIMessagesByParent<UIMessage>(mergedUiMessages);\n return (message, metadata) =>\n convertLangChainBaseMessage(message, {\n ...metadata,\n uiMessagesByParent,\n messageTiming,\n });\n }, [mergedUiMessages, messageTiming]);\n\n const threadMessages = useExternalMessageConverter({\n callback: convertWithUI,\n messages: visibleMessages,\n isRunning: effectiveIsRunning,\n });\n\n const streamRef = useRef(stream);\n streamRef.current = stream;\n\n const activeRunConfigRef = useRef<\n NormalizedRunConfigOptions[\"config\"] | undefined\n >(undefined);\n const runConfigByMessageIdRef = useRef(\n new Map<string, NormalizedRunConfigOptions[\"config\"] | undefined>(),\n );\n const activeThreadIdRef = useRef(externalId);\n const setActiveRunConfig = useCallback(\n (runConfig: AppendMessage[\"runConfig\"]) => {\n activeRunConfigRef.current = runConfigToSubmitOptions(runConfig)?.config;\n },\n [],\n );\n const withActiveRunConfig = useCallback(\n (submitOptions?: Record<string, unknown>) => {\n if (submitOptions && \"config\" in submitOptions) return submitOptions;\n if (activeRunConfigRef.current === undefined) return submitOptions;\n return { ...submitOptions, config: activeRunConfigRef.current };\n },\n [],\n );\n\n useEffect(() => {\n if (\n activeThreadIdRef.current !== null &&\n activeThreadIdRef.current !== externalId\n ) {\n activeRunConfigRef.current = undefined;\n runConfigByMessageIdRef.current.clear();\n }\n activeThreadIdRef.current = externalId;\n }, [externalId]);\n\n useEffect(() => {\n const messages = stream.messages as readonly LangChainBaseMessage[];\n const owned = runConfigByMessageIdRef.current;\n for (let i = messages.length - 1; i >= 0; i--) {\n const message = messages.at(i);\n if (\n !message?.id ||\n getMessageType(message) !== \"ai\" ||\n !message.tool_calls?.length\n ) {\n continue;\n }\n if (owned.has(message.id)) return;\n break;\n }\n for (const message of messages) {\n if (\n !message.id ||\n getMessageType(message) !== \"ai\" ||\n !message.tool_calls?.length ||\n owned.has(message.id)\n ) {\n continue;\n }\n owned.set(message.id, activeRunConfigRef.current);\n }\n }, [stream.messages]);\n\n const visibleMessagesRef = useRef(visibleMessages);\n visibleMessagesRef.current = visibleMessages;\n\n const threadMessagesRef = useRef(threadMessages);\n threadMessagesRef.current = threadMessages;\n\n const stagedMessagesRef = useRef(\n new Map<\n string,\n {\n message: LangChainBaseMessage & { id: string };\n runConfig: AppendMessage[\"runConfig\"];\n reconcileOnEcho: boolean;\n baseMessageCount: number;\n }\n >(),\n );\n const stagedBaseMessagesRef = useRef<LangChainBaseMessage[] | null>(null);\n useEffect(() => {\n if (stagedMessagesRef.current.size === 0) return;\n\n // Staged edits must keep their truncated base while stream updates arrive before promotion.\n const baseMessages =\n stagedBaseMessagesRef.current ??\n (stream.messages as LangChainBaseMessage[]);\n const remainingStagedMessages: LangChainBaseMessage[] = [];\n const matchedBaseMessageIndexes = new Set<number>();\n const visibleStagedIds = new Set(\n visibleMessagesRef.current.flatMap((m) => (m.id ? [m.id] : [])),\n );\n for (const [id, staged] of stagedMessagesRef.current) {\n if (!visibleStagedIds.has(id)) continue;\n const echoed = baseMessages.some((message, index) => {\n if (matchedBaseMessageIndexes.has(index)) return false;\n if (message.id === id) {\n matchedBaseMessageIndexes.add(index);\n return true;\n }\n if (\n !staged.reconcileOnEcho ||\n index < staged.baseMessageCount ||\n getMessageType(message) !== \"human\" ||\n !hasSameMessageContent(message, staged.message)\n ) {\n return false;\n }\n matchedBaseMessageIndexes.add(index);\n return true;\n });\n if (echoed) stagedMessagesRef.current.delete(id);\n else remainingStagedMessages.push(staged.message);\n }\n\n if (remainingStagedMessages.length === 0) {\n stagedBaseMessagesRef.current = null;\n visibleMessagesRef.current = baseMessages;\n setStagedMessages(null);\n return;\n }\n\n const nextMessages = [...baseMessages, ...remainingStagedMessages];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n }, [stream.messages]);\n\n const getStagedRun = (parentId: string | null) => {\n if (!parentId || !stagedMessagesRef.current.has(parentId)) return null;\n\n const staged: LangChainBaseMessage[] = [];\n for (const message of visibleMessagesRef.current) {\n if (message.id && stagedMessagesRef.current.has(message.id)) {\n staged.push(stagedMessagesRef.current.get(message.id)!.message);\n }\n if (message.id === parentId) break;\n }\n\n return {\n messages: staged,\n runConfig: stagedMessagesRef.current.get(parentId)!.runConfig,\n };\n };\n\n const stageUserMessage = (msg: AppendMessage, reconcileOnEcho = false) => {\n const stagedMessage = toStagedHumanMessage(msg);\n stagedMessagesRef.current.set(stagedMessage.id, {\n message: stagedMessage,\n runConfig: msg.runConfig,\n reconcileOnEcho,\n baseMessageCount: streamRef.current.messages.length,\n });\n const nextMessages = [...visibleMessagesRef.current, stagedMessage];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n return stagedMessage;\n };\n\n const removeStagedMessage = (id: string) => {\n if (!stagedMessagesRef.current.delete(id)) return;\n const nextMessages = visibleMessagesRef.current.filter(\n (message) => message.id !== id,\n );\n visibleMessagesRef.current = nextMessages;\n if (stagedMessagesRef.current.size === 0) {\n stagedBaseMessagesRef.current = null;\n setStagedMessages(null);\n } else {\n setStagedMessages(nextMessages);\n }\n };\n\n const extras = useMemo(\n () =>\n langChainExtras.provide({\n interrupt: stream.interrupt,\n interrupts: stream.interrupts,\n toolCalls: stream.toolCalls,\n subagents: stream.subagents,\n subgraphs: stream.subgraphs,\n stream,\n error: stream.error,\n submit: (values, submitOptions) => {\n const isResume = values == null || submitOptions?.command != null;\n return stream.submit(\n values,\n isResume ? withActiveRunConfig(submitOptions) : submitOptions,\n );\n },\n respond: (response, respondOptions) =>\n stream.respond(response, withActiveRunConfig(respondOptions)),\n respondAll: (responsesById, respondOptions) =>\n stream.respondAll(responsesById, withActiveRunConfig(respondOptions)),\n values: stream.values,\n messagesKey,\n }),\n [stream, messagesKey, withActiveRunConfig],\n );\n\n const runtime = useExternalStoreRuntime({\n ...pickExternalStoreSharedOptions(options),\n isRunning: stream.isLoading,\n isLoading: stream.isThreadLoading,\n messages: threadMessages,\n adapters,\n extras,\n unstable_enableToolInvocations: true,\n setToolStatuses,\n onNew: async (msg) => {\n if (!(msg.startRun ?? msg.role === \"user\")) {\n stageUserMessage(msg);\n return;\n }\n\n const stagedMessage = stageUserMessage(msg, true);\n const stagedMessageId = stagedMessage.id;\n setActiveRunConfig(msg.runConfig);\n const content = getMessageContent(msg);\n const cancellations =\n autoCancelPendingToolCalls !== false\n ? getPendingToolCalls(\n streamRef.current.messages as readonly LangChainBaseMessage[],\n ).map(createToolCallCancellationStub)\n : [];\n // A null threadId is not a no-op for the SDK: it rebinds the controller\n // away from its self-created thread and forces a fresh one, so the\n // submit waits for initialization to produce an identity; core no\n // longer holds appends on that barrier.\n try {\n const { externalId } = await aui.threadListItem.initialize();\n await streamRef.current.submit(\n {\n [messagesKey]: [\n ...cancellations,\n {\n id: stagedMessageId,\n type: \"human\",\n content,\n },\n ],\n },\n {\n ...runConfigToSubmitOptions(msg.runConfig),\n ...(externalId != null ? { threadId: externalId } : {}),\n },\n );\n } catch (error) {\n removeStagedMessage(stagedMessageId);\n throw error;\n }\n },\n onAddToolResult: async ({\n messageId,\n toolCallId,\n toolName,\n result,\n isError,\n artifact,\n }) => {\n const runConfig = runConfigByMessageIdRef.current.has(messageId)\n ? runConfigByMessageIdRef.current.get(messageId)\n : activeRunConfigRef.current;\n await stream.submit(\n {\n [messagesKey]: [\n {\n type: \"tool\",\n name: toolName,\n tool_call_id: toolCallId,\n content: JSON.stringify(result),\n ...(artifact !== undefined && { artifact }),\n status: isError ? \"error\" : \"success\",\n },\n ],\n },\n runConfig === undefined ? undefined : { config: runConfig },\n );\n },\n onReload: async (parentId, config) => {\n const stagedRun = getStagedRun(parentId);\n if (stagedRun) {\n const promotedIds = new Set<string>();\n for (const message of stagedRun.messages) {\n if (!message.id) continue;\n promotedIds.add(message.id);\n stagedMessagesRef.current.delete(message.id);\n }\n stagedBaseMessagesRef.current = null;\n if (stagedMessagesRef.current.size > 0) {\n const nextMessages = visibleMessagesRef.current.filter(\n (message) => !message.id || !promotedIds.has(message.id),\n );\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n } else {\n setStagedMessages(null);\n }\n const runConfig = config.runConfig ?? stagedRun.runConfig;\n setActiveRunConfig(runConfig);\n await stream.submit(\n {\n [messagesKey]: stagedRun.messages.map((message) => ({\n id: message.id,\n type: \"human\",\n content: message.content,\n })),\n },\n runConfigToSubmitOptions(runConfig),\n );\n return;\n }\n\n const threadId = externalId;\n if (!threadId || parentId == null) return;\n const s = streamRef.current;\n const checkpointId = await resolveForkCheckpoint(\n s.client,\n threadId,\n s.messages as readonly LangChainBaseMessage[],\n parentId,\n config.sourceId,\n s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),\n messagesKey,\n );\n if (!checkpointId) return;\n setActiveRunConfig(config.runConfig);\n await s.submit(null, {\n forkFrom: checkpointId,\n ...runConfigToSubmitOptions(config.runConfig),\n });\n },\n onEdit: async (message) => {\n if (!(message.startRun ?? message.role === \"user\")) {\n const truncated = truncateLangChainBaseMessages(\n threadMessagesRef.current,\n message.parentId,\n );\n const stagedMessage = toStagedHumanMessage(message);\n stagedMessagesRef.current.set(stagedMessage.id, {\n message: stagedMessage,\n runConfig: message.runConfig,\n reconcileOnEcho: false,\n baseMessageCount: 0,\n });\n stagedBaseMessagesRef.current = truncated;\n const nextMessages = [...truncated, stagedMessage];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n return;\n }\n\n const threadId = externalId;\n if (!threadId) return;\n const s = streamRef.current;\n const checkpointId = await resolveForkCheckpoint(\n s.client,\n threadId,\n s.messages as readonly LangChainBaseMessage[],\n message.parentId,\n message.sourceId,\n s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),\n messagesKey,\n );\n if (!checkpointId) return;\n const content = getMessageContent(message);\n setActiveRunConfig(message.runConfig);\n await s.submit(\n { [messagesKey]: [{ type: \"human\", content }] },\n {\n forkFrom: checkpointId,\n ...runConfigToSubmitOptions(message.runConfig),\n },\n );\n },\n onCancel:\n unstable_allowCancellation !== false\n ? async () => {\n activeRunConfigRef.current = undefined;\n await stream.stop();\n }\n : undefined,\n });\n\n return runtime;\n};\n\n/**\n * Creates an assistant-ui runtime backed by LangChain's `useStream` hook.\n * Accepts the same options as `useStream` from `@langchain/react`, plus\n * `cloud` and `adapters`.\n *\n * @example\n * ```tsx\n * import { useStreamRuntime } from \"@assistant-ui/react-langchain\";\n * import { AssistantRuntimeProvider, Thread } from \"@assistant-ui/react\";\n *\n * function App() {\n * const runtime = useStreamRuntime({\n * assistantId: \"agent\",\n * apiUrl: \"http://localhost:2024\",\n * });\n *\n * return (\n * <AssistantRuntimeProvider runtime={runtime}>\n * <Thread />\n * </AssistantRuntimeProvider>\n * );\n * }\n * ```\n */\nexport const useStreamRuntime = (rawOptions: UseStreamRuntimeOptions) => {\n const {\n cloud,\n unstable_threadListAdapter,\n create,\n delete: deleteFn,\n onThreadIdChange,\n ...options\n } = rawOptions;\n\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n const aui = useAui();\n const cloudAdapter = useCloudThreadListAdapter({\n cloud,\n create: createCloudThreadListAdapterCreateFallback(\n create,\n aui.threadListItem,\n ),\n delete: deleteFn,\n });\n const adapter = unstable_threadListAdapter ?? cloudAdapter;\n\n return useRemoteThreadListRuntime({\n runtimeHook: function RuntimeHook() {\n return useStreamThreadRuntime(optionsRef.current);\n },\n adapter,\n allowNesting: true,\n onThreadIdChange,\n });\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"useStreamRuntime.js","names":[],"sources":["../src/useStreamRuntime.ts"],"sourcesContent":["/// <reference types=\"@assistant-ui/core/store\" />\n\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useInsertionEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport type { AppendMessage, ToolExecutionStatus } from \"@assistant-ui/core\";\nimport {\n generateId,\n getExternalStoreMessages,\n pickExternalStoreSharedOptions,\n} from \"@assistant-ui/core\";\nimport type { ThreadMessage } from \"@assistant-ui/core\";\nimport {\n createCloudThreadListAdapterCreateFallback,\n createToolCallCancellationStub,\n scanPendingToolCalls,\n} from \"@assistant-ui/core/internal\";\nimport {\n useCloudThreadListAdapter,\n useExternalStoreRuntime,\n useExternalMessageConverter,\n useRemoteThreadListRuntime,\n} from \"@assistant-ui/core/react\";\nimport { useAui, useAuiState } from \"@assistant-ui/store\";\nimport { STREAM_CONTROLLER, useChannel, useStream } from \"@langchain/react\";\nimport type { Channel } from \"@langchain/react\";\nimport type {\n LangChainBaseMessage,\n LangChainToolCall,\n UIMessage,\n UseStreamRuntimeOptions,\n} from \"./types\";\nimport { groupUIMessagesByParent } from \"./converter\";\nexport { groupUIMessagesByParent } from \"./converter\";\nimport {\n convertLangChainBaseMessage,\n getMessageContent,\n getMessageType,\n} from \"./convertMessages\";\nimport { foldUIUpdates, mergeUIMessages } from \"./uiMessages\";\nimport { langChainExtras } from \"./runtimeExtras\";\nimport { resolveForkCheckpoint } from \"./resolveForkCheckpoint\";\nimport { useLangChainStreamingTiming } from \"./streamingTiming\";\n\nconst UI_CUSTOM_CHANNELS: readonly Channel[] = [\"custom\"];\n\nexport const runConfigToSubmitOptions = (\n runConfig: AppendMessage[\"runConfig\"],\n) =>\n runConfig?.custom\n ? { config: { configurable: runConfig.custom } }\n : undefined;\n\ntype NormalizedRunConfigOptions = NonNullable<\n ReturnType<typeof runConfigToSubmitOptions>\n>;\n\nconst getPendingToolCalls = (\n messages: readonly LangChainBaseMessage[],\n): LangChainToolCall[] =>\n scanPendingToolCalls(\n messages,\n (message) => {\n const type = getMessageType(message);\n if (type === \"ai\") return { toolCalls: message.tool_calls ?? [] };\n if (type === \"tool\" && message.tool_call_id) {\n return { toolCallId: message.tool_call_id };\n }\n return undefined;\n },\n (toolCall) => toolCall.id,\n );\n\nconst toStagedHumanMessage = (\n msg: AppendMessage,\n id = generateId(),\n): LangChainBaseMessage & { id: string } => ({\n id,\n _getType: () => \"human\",\n content: getMessageContent(msg),\n});\n\nconst humanContentText = (content: LangChainBaseMessage[\"content\"]) => {\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return \"\";\n return content\n .filter(\n (part): part is { type: \"text\"; text: string } =>\n typeof part === \"object\" &&\n part !== null &&\n part.type === \"text\" &&\n typeof part.text === \"string\",\n )\n .map((part) => part.text)\n .join(\"\");\n};\n\nconst hasSameMessageContent = (\n a: LangChainBaseMessage,\n b: LangChainBaseMessage,\n) => humanContentText(a.content) === humanContentText(b.content);\n\nconst truncateLangChainBaseMessages = (\n threadMessages: readonly ThreadMessage[],\n parentId: string | null,\n): LangChainBaseMessage[] => {\n if (parentId === null) return [];\n const parentIndex = threadMessages.findIndex((m) => m.id === parentId);\n if (parentIndex === -1) return [];\n const truncated: LangChainBaseMessage[] = [];\n for (let i = 0; i <= parentIndex && i < threadMessages.length; i++) {\n truncated.push(\n ...getExternalStoreMessages<LangChainBaseMessage>(threadMessages[i]!),\n );\n }\n return truncated;\n};\n\ntype DistributiveOmit<T, K extends keyof any> = T extends unknown\n ? Omit<T, K>\n : never;\n\nconst useStreamThreadRuntime = (\n options: DistributiveOmit<\n UseStreamRuntimeOptions,\n \"cloud\" | \"unstable_threadListAdapter\" | \"create\" | \"delete\"\n >,\n) => {\n const { adapters, autoCancelPendingToolCalls, unstable_allowCancellation } =\n options;\n const aui = useAui();\n const messagesKey = options.messagesKey ?? \"messages\";\n const uiStateKey = options.uiStateKey ?? \"ui\";\n\n const externalId = useAuiState((s) => s.threadListItem.externalId) as\n | string\n | null;\n // Object.assign preserves the discriminated transport union; object spread\n // collapses its arms and no longer satisfies UseStreamOptions.\n const stream = useStream(\n Object.assign({}, options, { threadId: externalId }),\n );\n const [stagedMessages, setStagedMessages] = useState<\n LangChainBaseMessage[] | null\n >(null);\n\n const [toolStatuses, setToolStatuses] = useState<\n Record<string, ToolExecutionStatus>\n >({});\n const hasExecutingTools = Object.values(toolStatuses).some(\n (s) => s?.type === \"executing\",\n );\n const effectiveIsRunning = stream.isLoading || hasExecutingTools;\n\n const uiStateValue = stream.values[uiStateKey];\n\n const customEvents = useChannel(stream, UI_CUSTOM_CHANNELS);\n const liveUiMessages = useMemo(\n () => foldUIUpdates(customEvents),\n [customEvents],\n );\n\n const mergedUiMessages = useMemo(\n () => mergeUIMessages(liveUiMessages, uiStateValue),\n [liveUiMessages, uiStateValue],\n );\n\n const visibleMessages =\n stagedMessages ?? (stream.messages as LangChainBaseMessage[]);\n\n const messageTiming = useLangChainStreamingTiming(\n visibleMessages,\n effectiveIsRunning,\n );\n\n const convertWithUI = useMemo<\n useExternalMessageConverter.Callback<LangChainBaseMessage>\n >(() => {\n const uiMessagesByParent =\n groupUIMessagesByParent<UIMessage>(mergedUiMessages);\n return (message, metadata) =>\n convertLangChainBaseMessage(message, {\n ...metadata,\n uiMessagesByParent,\n messageTiming,\n });\n }, [mergedUiMessages, messageTiming]);\n\n const threadMessages = useExternalMessageConverter({\n callback: convertWithUI,\n messages: visibleMessages,\n isRunning: effectiveIsRunning,\n });\n\n const streamRef = useRef(stream);\n useInsertionEffect(() => {\n streamRef.current = stream;\n }, [stream]);\n\n const activeRunConfigRef = useRef<\n NormalizedRunConfigOptions[\"config\"] | undefined\n >(undefined);\n const runConfigByMessageIdRef = useRef(\n new Map<string, NormalizedRunConfigOptions[\"config\"] | undefined>(),\n );\n const activeThreadIdRef = useRef(externalId);\n const setActiveRunConfig = useCallback(\n (runConfig: AppendMessage[\"runConfig\"]) => {\n activeRunConfigRef.current = runConfigToSubmitOptions(runConfig)?.config;\n },\n [],\n );\n const withActiveRunConfig = useCallback(\n (submitOptions?: Record<string, unknown>) => {\n if (submitOptions && \"config\" in submitOptions) return submitOptions;\n if (activeRunConfigRef.current === undefined) return submitOptions;\n return { ...submitOptions, config: activeRunConfigRef.current };\n },\n [],\n );\n\n useEffect(() => {\n if (\n activeThreadIdRef.current !== null &&\n activeThreadIdRef.current !== externalId\n ) {\n activeRunConfigRef.current = undefined;\n runConfigByMessageIdRef.current.clear();\n }\n activeThreadIdRef.current = externalId;\n }, [externalId]);\n\n useEffect(() => {\n const messages = stream.messages as readonly LangChainBaseMessage[];\n const owned = runConfigByMessageIdRef.current;\n for (let i = messages.length - 1; i >= 0; i--) {\n const message = messages.at(i);\n if (\n !message?.id ||\n getMessageType(message) !== \"ai\" ||\n !message.tool_calls?.length\n ) {\n continue;\n }\n if (owned.has(message.id)) return;\n break;\n }\n for (const message of messages) {\n if (\n !message.id ||\n getMessageType(message) !== \"ai\" ||\n !message.tool_calls?.length ||\n owned.has(message.id)\n ) {\n continue;\n }\n owned.set(message.id, activeRunConfigRef.current);\n }\n }, [stream.messages]);\n\n const visibleMessagesRef = useRef(visibleMessages);\n useInsertionEffect(() => {\n visibleMessagesRef.current = visibleMessages;\n }, [visibleMessages]);\n\n const threadMessagesRef = useRef(threadMessages);\n useInsertionEffect(() => {\n threadMessagesRef.current = threadMessages;\n }, [threadMessages]);\n\n const stagedMessagesRef = useRef(\n new Map<\n string,\n {\n message: LangChainBaseMessage & { id: string };\n runConfig: AppendMessage[\"runConfig\"];\n reconcileOnEcho: boolean;\n baseMessageCount: number;\n }\n >(),\n );\n const stagedBaseMessagesRef = useRef<LangChainBaseMessage[] | null>(null);\n useEffect(() => {\n if (stagedMessagesRef.current.size === 0) return;\n\n // Staged edits must keep their truncated base while stream updates arrive before promotion.\n const baseMessages =\n stagedBaseMessagesRef.current ??\n (stream.messages as LangChainBaseMessage[]);\n const remainingStagedMessages: LangChainBaseMessage[] = [];\n const matchedBaseMessageIndexes = new Set<number>();\n const visibleStagedIds = new Set(\n visibleMessagesRef.current.flatMap((m) => (m.id ? [m.id] : [])),\n );\n for (const [id, staged] of stagedMessagesRef.current) {\n if (!visibleStagedIds.has(id)) continue;\n const echoed = baseMessages.some((message, index) => {\n if (matchedBaseMessageIndexes.has(index)) return false;\n if (message.id === id) {\n matchedBaseMessageIndexes.add(index);\n return true;\n }\n if (\n !staged.reconcileOnEcho ||\n index < staged.baseMessageCount ||\n getMessageType(message) !== \"human\" ||\n !hasSameMessageContent(message, staged.message)\n ) {\n return false;\n }\n matchedBaseMessageIndexes.add(index);\n return true;\n });\n if (echoed) stagedMessagesRef.current.delete(id);\n else remainingStagedMessages.push(staged.message);\n }\n\n if (remainingStagedMessages.length === 0) {\n stagedBaseMessagesRef.current = null;\n visibleMessagesRef.current = baseMessages;\n // Reconciling against the upstream stream mutates the staged refs above,\n // which cannot happen during render.\n // eslint-disable-next-line react-hooks/set-state-in-effect\n setStagedMessages(null);\n return;\n }\n\n const nextMessages = [...baseMessages, ...remainingStagedMessages];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n }, [stream.messages]);\n\n const getStagedRun = (parentId: string | null) => {\n if (!parentId || !stagedMessagesRef.current.has(parentId)) return null;\n\n const staged: LangChainBaseMessage[] = [];\n for (const message of visibleMessagesRef.current) {\n if (message.id && stagedMessagesRef.current.has(message.id)) {\n staged.push(stagedMessagesRef.current.get(message.id)!.message);\n }\n if (message.id === parentId) break;\n }\n\n return {\n messages: staged,\n runConfig: stagedMessagesRef.current.get(parentId)!.runConfig,\n };\n };\n\n const stageUserMessage = (msg: AppendMessage, reconcileOnEcho = false) => {\n const stagedMessage = toStagedHumanMessage(msg);\n stagedMessagesRef.current.set(stagedMessage.id, {\n message: stagedMessage,\n runConfig: msg.runConfig,\n reconcileOnEcho,\n baseMessageCount: streamRef.current.messages.length,\n });\n const nextMessages = [...visibleMessagesRef.current, stagedMessage];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n return stagedMessage;\n };\n\n const removeStagedMessage = (id: string) => {\n if (!stagedMessagesRef.current.delete(id)) return;\n const nextMessages = visibleMessagesRef.current.filter(\n (message) => message.id !== id,\n );\n visibleMessagesRef.current = nextMessages;\n if (stagedMessagesRef.current.size === 0) {\n stagedBaseMessagesRef.current = null;\n setStagedMessages(null);\n } else {\n setStagedMessages(nextMessages);\n }\n };\n\n const extras = useMemo(\n () =>\n langChainExtras.provide({\n interrupt: stream.interrupt,\n interrupts: stream.interrupts,\n toolCalls: stream.toolCalls,\n subagents: stream.subagents,\n subgraphs: stream.subgraphs,\n stream,\n error: stream.error,\n submit: (values, submitOptions) => {\n const isResume = values == null || submitOptions?.command != null;\n return stream.submit(\n values,\n isResume ? withActiveRunConfig(submitOptions) : submitOptions,\n );\n },\n respond: (response, respondOptions) =>\n stream.respond(response, withActiveRunConfig(respondOptions)),\n respondAll: (responsesById, respondOptions) =>\n stream.respondAll(responsesById, withActiveRunConfig(respondOptions)),\n values: stream.values,\n messagesKey,\n }),\n [stream, messagesKey, withActiveRunConfig],\n );\n\n const runtime = useExternalStoreRuntime({\n ...pickExternalStoreSharedOptions(options),\n isRunning: stream.isLoading,\n isLoading: stream.isThreadLoading,\n messages: threadMessages,\n adapters,\n extras,\n unstable_enableToolInvocations: true,\n setToolStatuses,\n onNew: async (msg) => {\n if (!(msg.startRun ?? msg.role === \"user\")) {\n stageUserMessage(msg);\n return;\n }\n\n const stagedMessage = stageUserMessage(msg, true);\n const stagedMessageId = stagedMessage.id;\n setActiveRunConfig(msg.runConfig);\n const content = getMessageContent(msg);\n const cancellations =\n autoCancelPendingToolCalls !== false\n ? getPendingToolCalls(\n streamRef.current.messages as readonly LangChainBaseMessage[],\n ).map(createToolCallCancellationStub)\n : [];\n // A null threadId is not a no-op for the SDK: it rebinds the controller\n // away from its self-created thread and forces a fresh one, so the\n // submit waits for initialization to produce an identity; core no\n // longer holds appends on that barrier.\n try {\n const { externalId } = await aui.threadListItem.initialize();\n await streamRef.current.submit(\n {\n [messagesKey]: [\n ...cancellations,\n {\n id: stagedMessageId,\n type: \"human\",\n content,\n },\n ],\n },\n {\n ...runConfigToSubmitOptions(msg.runConfig),\n ...(externalId != null ? { threadId: externalId } : {}),\n },\n );\n } catch (error) {\n removeStagedMessage(stagedMessageId);\n throw error;\n }\n },\n onAddToolResult: async ({\n messageId,\n toolCallId,\n toolName,\n result,\n isError,\n artifact,\n }) => {\n const runConfig = runConfigByMessageIdRef.current.has(messageId)\n ? runConfigByMessageIdRef.current.get(messageId)\n : activeRunConfigRef.current;\n await stream.submit(\n {\n [messagesKey]: [\n {\n type: \"tool\",\n name: toolName,\n tool_call_id: toolCallId,\n content: JSON.stringify(result),\n ...(artifact !== undefined && { artifact }),\n status: isError ? \"error\" : \"success\",\n },\n ],\n },\n runConfig === undefined ? undefined : { config: runConfig },\n );\n },\n onReload: async (parentId, config) => {\n const stagedRun = getStagedRun(parentId);\n if (stagedRun) {\n const promotedIds = new Set<string>();\n for (const message of stagedRun.messages) {\n if (!message.id) continue;\n promotedIds.add(message.id);\n stagedMessagesRef.current.delete(message.id);\n }\n stagedBaseMessagesRef.current = null;\n if (stagedMessagesRef.current.size > 0) {\n const nextMessages = visibleMessagesRef.current.filter(\n (message) => !message.id || !promotedIds.has(message.id),\n );\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n } else {\n setStagedMessages(null);\n }\n const runConfig = config.runConfig ?? stagedRun.runConfig;\n setActiveRunConfig(runConfig);\n await stream.submit(\n {\n [messagesKey]: stagedRun.messages.map((message) => ({\n id: message.id,\n type: \"human\",\n content: message.content,\n })),\n },\n runConfigToSubmitOptions(runConfig),\n );\n return;\n }\n\n const threadId = externalId;\n if (!threadId || parentId == null) return;\n const s = streamRef.current;\n const checkpointId = await resolveForkCheckpoint(\n s.client,\n threadId,\n s.messages as readonly LangChainBaseMessage[],\n parentId,\n config.sourceId,\n s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),\n messagesKey,\n );\n if (!checkpointId) return;\n setActiveRunConfig(config.runConfig);\n await s.submit(null, {\n forkFrom: checkpointId,\n ...runConfigToSubmitOptions(config.runConfig),\n });\n },\n onEdit: async (message) => {\n if (!(message.startRun ?? message.role === \"user\")) {\n const truncated = truncateLangChainBaseMessages(\n threadMessagesRef.current,\n message.parentId,\n );\n const stagedMessage = toStagedHumanMessage(message);\n stagedMessagesRef.current.set(stagedMessage.id, {\n message: stagedMessage,\n runConfig: message.runConfig,\n reconcileOnEcho: false,\n baseMessageCount: 0,\n });\n stagedBaseMessagesRef.current = truncated;\n const nextMessages = [...truncated, stagedMessage];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n return;\n }\n\n const threadId = externalId;\n if (!threadId) return;\n const s = streamRef.current;\n const checkpointId = await resolveForkCheckpoint(\n s.client,\n threadId,\n s.messages as readonly LangChainBaseMessage[],\n message.parentId,\n message.sourceId,\n s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),\n messagesKey,\n );\n if (!checkpointId) return;\n const content = getMessageContent(message);\n setActiveRunConfig(message.runConfig);\n await s.submit(\n { [messagesKey]: [{ type: \"human\", content }] },\n {\n forkFrom: checkpointId,\n ...runConfigToSubmitOptions(message.runConfig),\n },\n );\n },\n onCancel:\n unstable_allowCancellation !== false\n ? async () => {\n activeRunConfigRef.current = undefined;\n await stream.stop();\n }\n : undefined,\n });\n\n return runtime;\n};\n\n/**\n * Creates an assistant-ui runtime backed by LangChain's `useStream` hook.\n * Accepts the same options as `useStream` from `@langchain/react`, plus\n * `cloud` and `adapters`.\n *\n * @example\n * ```tsx\n * import { useStreamRuntime } from \"@assistant-ui/react-langchain\";\n * import { AssistantRuntimeProvider, Thread } from \"@assistant-ui/react\";\n *\n * function App() {\n * const runtime = useStreamRuntime({\n * assistantId: \"agent\",\n * apiUrl: \"http://localhost:2024\",\n * });\n *\n * return (\n * <AssistantRuntimeProvider runtime={runtime}>\n * <Thread />\n * </AssistantRuntimeProvider>\n * );\n * }\n * ```\n */\nexport const useStreamRuntime = (rawOptions: UseStreamRuntimeOptions) => {\n const {\n cloud,\n unstable_threadListAdapter,\n create,\n delete: deleteFn,\n onThreadIdChange,\n ...options\n } = rawOptions;\n\n const aui = useAui();\n const cloudAdapter = useCloudThreadListAdapter({\n cloud,\n create: createCloudThreadListAdapterCreateFallback(\n create,\n aui.threadListItem,\n ),\n delete: deleteFn,\n });\n const adapter = unstable_threadListAdapter ?? cloudAdapter;\n\n return useRemoteThreadListRuntime({\n runtimeHook: function RuntimeHook() {\n return useStreamThreadRuntime(options);\n },\n adapter,\n allowNesting: true,\n onThreadIdChange,\n });\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/react-langchain",
3
- "version": "0.0.28",
3
+ "version": "0.0.30",
4
4
  "description": "LangChain useStream adapter for assistant-ui",
5
5
  "keywords": [
6
6
  "langchain",
@@ -40,10 +40,10 @@
40
40
  ],
41
41
  "sideEffects": false,
42
42
  "dependencies": {
43
- "@assistant-ui/core": "^0.3.16",
44
- "@assistant-ui/store": "^0.3.11",
43
+ "@assistant-ui/core": "^0.3.18",
44
+ "@assistant-ui/store": "^0.3.13",
45
45
  "assistant-cloud": "*",
46
- "assistant-stream": "^0.3.40"
46
+ "assistant-stream": "^0.3.42"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "@langchain/react": "^1.0.2",
@@ -59,11 +59,11 @@
59
59
  }
60
60
  },
61
61
  "devDependencies": {
62
- "@assistant-ui/x-buildutils": "0.0.24",
62
+ "@assistant-ui/x-buildutils": "0.0.26",
63
63
  "@langchain/core": "^1.2.9",
64
- "@langchain/langgraph-sdk": "^1.9.31",
65
- "@langchain/react": "^1.0.32",
66
- "@testing-library/react": "^16.3.2",
64
+ "@langchain/langgraph-sdk": "^1.10.2",
65
+ "@langchain/react": "^1.0.35",
66
+ "@testing-library/react": "^16.3.3",
67
67
  "@types/react": "^19.2.18",
68
68
  "jsdom": "^30.0.1",
69
69
  "react": "^19.2.8",
@@ -1,10 +1,12 @@
1
- import { describe, expect, it } from "vitest";
1
+ import { describe, expect, it, vi } from "vitest";
2
2
  import type { AppendMessage } from "@assistant-ui/core";
3
3
  import { convertExternalMessages } from "@assistant-ui/core/react";
4
4
  import {
5
5
  convertLangChainBaseMessage,
6
6
  getMessageContent,
7
+ getMessageType,
7
8
  } from "./convertMessages";
9
+ import { createLangChainStreamingTimingAccessors } from "./converter";
8
10
  import type { LangChainBaseMessage, UIMessage } from "./types";
9
11
 
10
12
  const humanMessage = (content: unknown): LangChainBaseMessage => ({
@@ -754,13 +756,38 @@ describe("convertLangChainBaseMessage reasoning content parts", () => {
754
756
  ]);
755
757
  });
756
758
 
757
- it("does not throw when a reasoning block omits both summary and reasoning", () => {
759
+ it("omits reasoning parts that contain only provider metadata", () => {
758
760
  const result = convertLangChainBaseMessage(
759
- aiMessage([{ type: "reasoning" }]),
761
+ aiMessage([
762
+ { type: "reasoning", signature: "signed", index: 0 },
763
+ { type: "reasoning", summary: [{ type: "summary_text" }], index: 1 },
764
+ { type: "text", text: "Answer." },
765
+ ]),
760
766
  {},
761
767
  );
762
768
 
763
- expect(contentOf(result)).toEqual([{ type: "reasoning", text: "" }]);
769
+ expect(contentOf(result)).toEqual([{ type: "text", text: "Answer." }]);
770
+ });
771
+
772
+ it("renders the summary, falling back to the reasoning string when it is blank", () => {
773
+ const convert = (summary: string) =>
774
+ contentOf(
775
+ convertLangChainBaseMessage(
776
+ aiMessage([
777
+ {
778
+ type: "reasoning",
779
+ reasoning: "Check net",
780
+ summary: [{ type: "summary_text", text: summary }],
781
+ },
782
+ ]),
783
+ {},
784
+ ),
785
+ );
786
+
787
+ expect(convert("temperature")).toEqual([
788
+ { type: "reasoning", text: "temperature" },
789
+ ]);
790
+ expect(convert(" ")).toEqual([{ type: "reasoning", text: "Check net" }]);
764
791
  });
765
792
 
766
793
  it("tolerates null entries inside the summary array", () => {
@@ -1047,3 +1074,162 @@ describe("convertLangChainBaseMessage tool messages", () => {
1047
1074
  ).toThrow(/does not match existing tool call/);
1048
1075
  });
1049
1076
  });
1077
+
1078
+ describe("convertLangChainBaseMessage malformed messages", () => {
1079
+ it("reports an unknown type for a message without _getType or type", () => {
1080
+ const message = { id: "msg-3", content: "hello" } as LangChainBaseMessage;
1081
+
1082
+ expect(getMessageType(message)).toBe("unknown");
1083
+ expect(convertLangChainBaseMessage(message, {})).toEqual({
1084
+ role: "system",
1085
+ id: "msg-3",
1086
+ content: [{ type: "text", text: "hello" }],
1087
+ });
1088
+ });
1089
+
1090
+ it("converts a human message with null content to empty content", () => {
1091
+ expect(
1092
+ contentOf(convertLangChainBaseMessage(humanMessage(null), {})),
1093
+ ).toEqual([]);
1094
+ });
1095
+
1096
+ it("keeps tool calls when an ai message carries object content", () => {
1097
+ const result = convertLangChainBaseMessage(
1098
+ {
1099
+ ...aiMessage({ text: "not an array" }),
1100
+ tool_calls: [{ id: "call-1", name: "lookup", args: { q: "x" } }],
1101
+ },
1102
+ {},
1103
+ );
1104
+
1105
+ expect(contentOf(result)).toEqual([
1106
+ {
1107
+ type: "tool-call",
1108
+ toolCallId: "call-1",
1109
+ toolName: "lookup",
1110
+ args: { q: "x" },
1111
+ argsText: '{"q":"x"}',
1112
+ },
1113
+ ]);
1114
+ });
1115
+
1116
+ it("converts a system message with null content to empty text", () => {
1117
+ const result = convertLangChainBaseMessage(
1118
+ { _getType: () => "system", id: "msg-4", content: null },
1119
+ {},
1120
+ );
1121
+
1122
+ expect(contentOf(result)).toEqual([{ type: "text", text: "" }]);
1123
+ });
1124
+
1125
+ it("renders a message without a type or content as empty system text", () => {
1126
+ const result = convertLangChainBaseMessage(
1127
+ { id: "msg-6" } as LangChainBaseMessage,
1128
+ {},
1129
+ );
1130
+
1131
+ expect(contentOf(result)).toEqual([{ type: "text", text: "" }]);
1132
+ });
1133
+
1134
+ it("skips null entries inside a content array", () => {
1135
+ const result = convertLangChainBaseMessage(
1136
+ humanMessage([null, { type: "text", text: "kept" }, undefined]),
1137
+ {},
1138
+ );
1139
+
1140
+ expect(contentOf(result)).toEqual([{ type: "text", text: "kept" }]);
1141
+ });
1142
+
1143
+ it("coalesces a text block without a text field to empty text", () => {
1144
+ const result = convertLangChainBaseMessage(
1145
+ humanMessage([{ type: "text" }]),
1146
+ {},
1147
+ );
1148
+
1149
+ expect(contentOf(result)).toEqual([{ type: "text", text: "" }]);
1150
+ });
1151
+
1152
+ it("skips null entries when measuring streamed text length", () => {
1153
+ const { getTextLength } = createLangChainStreamingTimingAccessors<{
1154
+ id?: string | undefined;
1155
+ content?: unknown;
1156
+ _getType: () => string;
1157
+ }>((message) => message._getType());
1158
+
1159
+ expect(
1160
+ getTextLength(
1161
+ [
1162
+ {
1163
+ _getType: () => "ai",
1164
+ id: "ai-1",
1165
+ content: [null, { type: "text", text: "abc" }, undefined],
1166
+ },
1167
+ ],
1168
+ "ai-1",
1169
+ ),
1170
+ ).toBe(3);
1171
+ });
1172
+
1173
+ it("renders a textless block as empty system text", () => {
1174
+ const result = convertLangChainBaseMessage(
1175
+ {
1176
+ _getType: () => "system",
1177
+ id: "msg-7",
1178
+ content: [{ type: "text" }, { type: "text", text: "kept" }],
1179
+ },
1180
+ {},
1181
+ );
1182
+
1183
+ expect(contentOf(result)).toEqual([{ type: "text", text: "kept" }]);
1184
+ });
1185
+
1186
+ it("skips null entries when collecting system text", () => {
1187
+ const result = convertLangChainBaseMessage(
1188
+ {
1189
+ _getType: () => "system",
1190
+ id: "msg-7",
1191
+ content: [null, { type: "text", text: "kept" }],
1192
+ },
1193
+ {},
1194
+ );
1195
+
1196
+ expect(contentOf(result)).toEqual([{ type: "text", text: "kept" }]);
1197
+ });
1198
+
1199
+ it("converts a system message with object content to empty text", () => {
1200
+ const result = convertLangChainBaseMessage(
1201
+ { _getType: () => "system", id: "msg-5", content: { text: "x" } },
1202
+ {},
1203
+ );
1204
+
1205
+ expect(contentOf(result)).toEqual([{ type: "text", text: "" }]);
1206
+ });
1207
+
1208
+ it("stays silent about non-array content outside development", () => {
1209
+ vi.stubEnv("NODE_ENV", "test");
1210
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
1211
+ try {
1212
+ convertLangChainBaseMessage(humanMessage(true), {});
1213
+ expect(warn).not.toHaveBeenCalled();
1214
+ } finally {
1215
+ warn.mockRestore();
1216
+ vi.unstubAllEnvs();
1217
+ }
1218
+ });
1219
+
1220
+ it("warns once in development about non-array content", () => {
1221
+ vi.stubEnv("NODE_ENV", "development");
1222
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
1223
+ try {
1224
+ convertLangChainBaseMessage(humanMessage(42), {});
1225
+ convertLangChainBaseMessage(humanMessage(42), {});
1226
+ expect(warn).toHaveBeenCalledTimes(1);
1227
+ expect(warn).toHaveBeenCalledWith(
1228
+ "Ignoring message content that is neither a string nor an array: number",
1229
+ );
1230
+ } finally {
1231
+ warn.mockRestore();
1232
+ vi.unstubAllEnvs();
1233
+ }
1234
+ });
1235
+ });
@@ -21,27 +21,52 @@ type LangChainMessageConverterMetadata =
21
21
  messageTiming?: Record<string, MessageTiming>;
22
22
  };
23
23
 
24
+ const warnedMalformedMessages = new Set<string>();
25
+ const warnOnceInDevelopment = (message: string) => {
26
+ if (
27
+ typeof process === "undefined" ||
28
+ process?.env?.NODE_ENV !== "development"
29
+ )
30
+ return;
31
+ if (warnedMalformedMessages.has(message)) return;
32
+ warnedMalformedMessages.add(message);
33
+ console.warn(message);
34
+ };
35
+
24
36
  export const getMessageType = (message: LangChainBaseMessage): string => {
25
37
  if (typeof message._getType === "function") return message._getType();
26
38
  if ("type" in message)
27
39
  return (message as Record<string, unknown>).type as string;
28
- throw new Error("Cannot determine message type");
40
+ warnOnceInDevelopment(
41
+ "Cannot determine message type; rendering the message as system text",
42
+ );
43
+ return "unknown";
44
+ };
45
+
46
+ const contentBlocks = (content: unknown): readonly LangChainContentBlock[] => {
47
+ if (content == null) return [];
48
+ if (Array.isArray(content))
49
+ return content.filter(
50
+ (block) => typeof block === "object" && block !== null,
51
+ );
52
+ warnOnceInDevelopment(
53
+ `Ignoring message content that is neither a string nor an array: ${typeof content}`,
54
+ );
55
+ return [];
29
56
  };
30
57
 
31
58
  const contentToParts = (content: unknown) => {
32
59
  if (typeof content === "string")
33
60
  return [{ type: "text" as const, text: content }];
34
61
 
35
- const parts = content as readonly LangChainContentBlock[];
36
- return parts
62
+ return contentBlocks(content)
37
63
  .map(convertLangChainContentBlock)
38
64
  .filter((part) => part !== null && part !== undefined);
39
65
  };
40
66
 
41
67
  const getStringContent = (content: unknown): string => {
42
68
  if (typeof content === "string") return content;
43
- const parts = content as readonly LangChainContentBlock[];
44
- return parts
69
+ return contentBlocks(content)
45
70
  .filter((c): c is { type: "text"; text: string } => c.type === "text")
46
71
  .map((c) => c.text)
47
72
  .join("");
@@ -137,7 +162,7 @@ export const convertLangChainBaseMessage = (
137
162
  text:
138
163
  typeof message.content === "string"
139
164
  ? message.content
140
- : JSON.stringify(message.content),
165
+ : (JSON.stringify(message.content) ?? ""),
141
166
  },
142
167
  ],
143
168
  };
package/src/converter.ts CHANGED
@@ -61,7 +61,10 @@ export const convertLangChainContentBlock = (
61
61
  switch (type) {
62
62
  case "text":
63
63
  case "text_delta":
64
- return { type: "text" as const, text: part.text };
64
+ return {
65
+ type: "text" as const,
66
+ text: typeof part.text === "string" ? part.text : "",
67
+ };
65
68
  case "image_url": {
66
69
  const image =
67
70
  typeof part.image_url === "string"
@@ -98,15 +101,13 @@ export const convertLangChainContentBlock = (
98
101
  };
99
102
  }
100
103
  case "thinking":
101
- return { type: "reasoning" as const, text: part.thinking };
102
- case "reasoning":
103
- return {
104
- type: "reasoning" as const,
105
- text:
106
- part.summary && part.summary.length > 0
107
- ? part.summary.map((s) => s?.text ?? "").join("\n\n\n")
108
- : (part.reasoning ?? ""),
109
- };
104
+ return hasVisibleText(part.thinking)
105
+ ? { type: "reasoning" as const, text: part.thinking }
106
+ : null;
107
+ case "reasoning": {
108
+ const text = getReasoningText(part);
109
+ return text ? { type: "reasoning" as const, text } : null;
110
+ }
110
111
  case "tool_use":
111
112
  case "input_json_delta":
112
113
  return null;
@@ -262,13 +263,14 @@ export const getMessageContent = (msg: AppendMessage) => {
262
263
  return content;
263
264
  };
264
265
 
265
- const reasoningTextLength = (part: {
266
+ const getReasoningText = (part: {
266
267
  readonly summary?: ReadonlyArray<{ readonly text?: string }>;
267
268
  readonly reasoning?: string;
268
- }): number => {
269
- if (part.summary && part.summary.length > 0)
270
- return part.summary.map((s) => s?.text ?? "").join("\n\n\n").length;
271
- return part.reasoning?.length ?? 0;
269
+ }): string => {
270
+ const summary = part.summary?.map((s) => s?.text ?? "").join("\n\n\n") ?? "";
271
+ if (hasVisibleText(summary)) return summary;
272
+ const reasoning = part.reasoning ?? "";
273
+ return hasVisibleText(reasoning) ? reasoning : "";
272
274
  };
273
275
 
274
276
  export const createLangChainStreamingTimingAccessors = <
@@ -299,16 +301,17 @@ export const createLangChainStreamingTimingAccessors = <
299
301
  if (!Array.isArray(content)) return 0;
300
302
  let len = 0;
301
303
  for (const part of content as readonly LangChainContentBlock[]) {
304
+ if (typeof part !== "object" || part === null) continue;
302
305
  switch (part.type) {
303
306
  case "text":
304
307
  case "text_delta":
305
308
  if (typeof part.text === "string") len += part.text.length;
306
309
  break;
307
310
  case "thinking":
308
- if (typeof part.thinking === "string") len += part.thinking.length;
311
+ if (hasVisibleText(part.thinking)) len += part.thinking.length;
309
312
  break;
310
313
  case "reasoning":
311
- len += reasoningTextLength(part);
314
+ len += getReasoningText(part).length;
312
315
  break;
313
316
  }
314
317
  }
@@ -34,4 +34,54 @@ describe("useLangChainStreamingTiming", () => {
34
34
  Math.ceil("deduced".length / 4),
35
35
  );
36
36
  });
37
+
38
+ it("counts a thinking block only when the converter renders it", () => {
39
+ const tokenCount = (thinking: string) => {
40
+ const messages: LangChainBaseMessage[] = [
41
+ {
42
+ id: "msg-1",
43
+ _getType: () => "ai",
44
+ content: [{ type: "thinking", thinking }],
45
+ },
46
+ ];
47
+ const { result, rerender } = renderHook(
48
+ ({ msgs, running }) => useLangChainStreamingTiming(msgs, running),
49
+ { initialProps: { msgs: messages, running: true } },
50
+ );
51
+ act(() => {
52
+ rerender({ msgs: messages, running: false });
53
+ });
54
+ return result.current["msg-1"]?.tokenCount;
55
+ };
56
+
57
+ expect(tokenCount("deduced")).toBe(Math.ceil("deduced".length / 4));
58
+ expect(tokenCount(" ")).toBeUndefined();
59
+ });
60
+
61
+ it("counts the summary text the converter renders, not the reasoning it shadows", () => {
62
+ const messages: LangChainBaseMessage[] = [
63
+ {
64
+ id: "msg-1",
65
+ _getType: () => "ai",
66
+ content: [
67
+ {
68
+ type: "reasoning",
69
+ reasoning: "partial thinking",
70
+ summary: [{ type: "summary_text", text: "first summary" }],
71
+ },
72
+ ],
73
+ },
74
+ ];
75
+ const { result, rerender } = renderHook(
76
+ ({ running }) => useLangChainStreamingTiming(messages, running),
77
+ { initialProps: { running: true } },
78
+ );
79
+ act(() => {
80
+ rerender({ running: false });
81
+ });
82
+
83
+ expect(result.current["msg-1"]?.tokenCount).toBe(
84
+ Math.ceil("first summary".length / 4),
85
+ );
86
+ });
37
87
  });
@@ -10,7 +10,7 @@ import type {
10
10
  } from "@assistant-ui/core";
11
11
  import { useAui } from "@assistant-ui/store";
12
12
  import type { LangChainBaseMessage } from "./types";
13
- import type { ReactNode } from "react";
13
+ import { startTransition, Suspense, type ReactNode } from "react";
14
14
  import {
15
15
  useLangChainRespond,
16
16
  useLangChainRespondAll,
@@ -898,3 +898,76 @@ describe("useStreamRuntime staged messages", () => {
898
898
  });
899
899
  });
900
900
  });
901
+
902
+ describe("useStreamRuntime committed refs", () => {
903
+ it("submits through the committed stream after an abandoned render", async () => {
904
+ const streamA = createMockStream();
905
+ const streamB = createMockStream();
906
+ const adapter = makeThreadListAdapter();
907
+ mockUseStream.mockImplementation((options: { apiUrl: string }) =>
908
+ options.apiUrl === "/api/b" ? streamB : streamA,
909
+ );
910
+ const host = renderHook(() =>
911
+ useStreamRuntime({
912
+ apiUrl: "/api/a",
913
+ unstable_threadListAdapter: adapter,
914
+ } as never),
915
+ );
916
+
917
+ const pending = new Promise<never>(() => {});
918
+ let blocked = false;
919
+ const interruptedRender = vi.fn();
920
+ const Blocker = () => {
921
+ if (blocked) {
922
+ interruptedRender();
923
+ throw pending;
924
+ }
925
+ return null;
926
+ };
927
+
928
+ const capture: { runtime: AssistantRuntime | null } = { runtime: null };
929
+ const Nested = ({ apiUrl }: { apiUrl: string }) => {
930
+ capture.runtime = useStreamRuntime({
931
+ apiUrl,
932
+ unstable_threadListAdapter: adapter,
933
+ } as never);
934
+ return null;
935
+ };
936
+ const Tree = ({ apiUrl }: { apiUrl: string }) => (
937
+ <AssistantRuntimeProvider runtime={host.result.current}>
938
+ <Suspense fallback={null}>
939
+ <Nested apiUrl={apiUrl} />
940
+ <Blocker />
941
+ </Suspense>
942
+ </AssistantRuntimeProvider>
943
+ );
944
+
945
+ const view = render(<Tree apiUrl="/api/a" />);
946
+ expect(capture.runtime).not.toBeNull();
947
+
948
+ act(() => {
949
+ blocked = true;
950
+ startTransition(() => view.rerender(<Tree apiUrl="/api/b" />));
951
+ });
952
+ expect(interruptedRender).toHaveBeenCalled();
953
+
954
+ await act(async () => {
955
+ await capture.runtime!.thread.append("hello");
956
+ });
957
+
958
+ expect(streamA.submit).toHaveBeenCalledOnce();
959
+ expect(streamB.submit).not.toHaveBeenCalled();
960
+
961
+ await act(async () => {
962
+ blocked = false;
963
+ view.rerender(<Tree apiUrl="/api/b" />);
964
+ });
965
+ await act(async () => {
966
+ await capture.runtime!.thread.append("second");
967
+ });
968
+
969
+ expect(streamB.submit).toHaveBeenCalledOnce();
970
+ view.unmount();
971
+ host.unmount();
972
+ });
973
+ });
@@ -1,7 +1,14 @@
1
1
  /// <reference types="@assistant-ui/core/store" />
2
2
  "use client";
3
3
 
4
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import {
5
+ useCallback,
6
+ useEffect,
7
+ useInsertionEffect,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ } from "react";
5
12
  import type { AppendMessage, ToolExecutionStatus } from "@assistant-ui/core";
6
13
  import {
7
14
  generateId,
@@ -192,7 +199,9 @@ const useStreamThreadRuntime = (
192
199
  });
193
200
 
194
201
  const streamRef = useRef(stream);
195
- streamRef.current = stream;
202
+ useInsertionEffect(() => {
203
+ streamRef.current = stream;
204
+ }, [stream]);
196
205
 
197
206
  const activeRunConfigRef = useRef<
198
207
  NormalizedRunConfigOptions["config"] | undefined
@@ -256,10 +265,14 @@ const useStreamThreadRuntime = (
256
265
  }, [stream.messages]);
257
266
 
258
267
  const visibleMessagesRef = useRef(visibleMessages);
259
- visibleMessagesRef.current = visibleMessages;
268
+ useInsertionEffect(() => {
269
+ visibleMessagesRef.current = visibleMessages;
270
+ }, [visibleMessages]);
260
271
 
261
272
  const threadMessagesRef = useRef(threadMessages);
262
- threadMessagesRef.current = threadMessages;
273
+ useInsertionEffect(() => {
274
+ threadMessagesRef.current = threadMessages;
275
+ }, [threadMessages]);
263
276
 
264
277
  const stagedMessagesRef = useRef(
265
278
  new Map<
@@ -311,6 +324,9 @@ const useStreamThreadRuntime = (
311
324
  if (remainingStagedMessages.length === 0) {
312
325
  stagedBaseMessagesRef.current = null;
313
326
  visibleMessagesRef.current = baseMessages;
327
+ // Reconciling against the upstream stream mutates the staged refs above,
328
+ // which cannot happen during render.
329
+ // eslint-disable-next-line react-hooks/set-state-in-effect
314
330
  setStagedMessages(null);
315
331
  return;
316
332
  }
@@ -613,9 +629,6 @@ export const useStreamRuntime = (rawOptions: UseStreamRuntimeOptions) => {
613
629
  ...options
614
630
  } = rawOptions;
615
631
 
616
- const optionsRef = useRef(options);
617
- optionsRef.current = options;
618
-
619
632
  const aui = useAui();
620
633
  const cloudAdapter = useCloudThreadListAdapter({
621
634
  cloud,
@@ -629,7 +642,7 @@ export const useStreamRuntime = (rawOptions: UseStreamRuntimeOptions) => {
629
642
 
630
643
  return useRemoteThreadListRuntime({
631
644
  runtimeHook: function RuntimeHook() {
632
- return useStreamThreadRuntime(optionsRef.current);
645
+ return useStreamThreadRuntime(options);
633
646
  },
634
647
  adapter,
635
648
  allowNesting: true,