@vegintech/langchain-react-agent 0.0.41 → 0.0.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -318,8 +318,8 @@ interface MessageContentRendererProps {
318
318
  components?: Components;
319
319
  securityConfig?: MessageListProps["securityConfig"];
320
320
  }
321
- /** 消息内容渲染组件 - 支持字符串和多模态内容块 */
322
- declare const MessageContentRenderer: React$1.FC<MessageContentRendererProps>;
321
+ /** memo 化:content/components/securityConfig 未变化时跳过重渲染 */
322
+ declare const MessageContentRenderer: React$1.NamedExoticComponent<MessageContentRendererProps>;
323
323
  //#endregion
324
324
  //#region src/components/DebugPanel.d.ts
325
325
  interface DebugPanelProps {
@@ -351,4 +351,4 @@ type AgentStream = ReturnType<typeof useStream>;
351
351
  */
352
352
  declare function useAgentStream(options: AgentStreamOptions): AgentStream;
353
353
  //#endregion
354
- export { AgentChat, type AgentChatInputRef, type AgentChatProps, type AgentChatRef, type AgentChatStreamProps, type AgentStream, type AgentStreamOptions, type BackendTool, type ChatMessage, type ContextItem, DebugPanel, type DebugPanelProps, type EmptyStateConfig, type FrontendTool, type InputConfig, type InterruptConfig, type InterruptEvent, type InterruptManagerProps, type InterruptRenderProps, type MessageConfig, type MessageContent, type MessageContentBlock, MessageContentRenderer, type MessageContentRendererProps, type MessageType, type SenderCustomizationProps, type SenderSlotConfig, type SenderSubmitParams, type ToolCallInput, ToolCard, type ToolCardProps, type ToolDefinition, type ToolExecutionRecord, type ToolExecutionStatus, type ToolParameterSchema, type ToolRenderProps, useAgentStream };
354
+ export { AgentChat, type AgentChatInputRef, type AgentChatProps, type AgentChatRef, type AgentChatStreamProps, type AgentStream, type AgentStreamOptions, type BackendTool, type ChatMessage, type ContextItem, DebugPanel, type DebugPanelProps, type EmptyStateConfig, type FrontendTool, type InputConfig, type InterruptConfig, type InterruptEvent, type InterruptManagerProps, type InterruptRenderProps, type MessageConfig, type MessageContent, type MessageContentBlock, MessageContentRenderer, type MessageContentRendererProps, type MessageType, type PreSendResult, type SenderCustomizationProps, type SenderSlotConfig, type SenderSubmitParams, type ToolCallInput, ToolCard, type ToolCardProps, type ToolDefinition, type ToolExecutionRecord, type ToolExecutionStatus, type ToolParameterSchema, type ToolRenderProps, useAgentStream };
package/dist/index.mjs CHANGED
@@ -1,11 +1,11 @@
1
- import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
1
+ import React, { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
2
2
  import { Actions, Bubble, Sender, Think, ThoughtChain } from "@ant-design/x";
3
3
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
4
  import { Streamdown } from "streamdown";
5
5
  import { useStream } from "@langchain/langgraph-sdk/react";
6
6
  //#region src/components/ChatInput.tsx
7
7
  const slotConfig = [];
8
- const ChatInput = forwardRef(({ onSend, onStop, isLoading = false, disabled = false, placeholder = "输入消息...", className = "", onPasteFile, footer, skill: externalSkill, header, prefix }, ref) => {
8
+ const ChatInputInner = forwardRef(({ onSend, onStop, isLoading = false, disabled = false, placeholder = "输入消息...", className = "", onPasteFile, footer, skill: externalSkill, header, prefix }, ref) => {
9
9
  const senderRef = useRef(null);
10
10
  const [internalSkill, setInternalSkill] = useState(externalSkill);
11
11
  useImperativeHandle(ref, () => ({
@@ -16,6 +16,7 @@ const ChatInput = forwardRef(({ onSend, onStop, isLoading = false, disabled = fa
16
16
  }));
17
17
  const handleSubmit = useCallback((message, slotConfig, skillData) => {
18
18
  if (message.trim() && !disabled && !isLoading) {
19
+ console.log("handle submit", slotConfig);
19
20
  onSend({
20
21
  message,
21
22
  slotConfig,
@@ -50,7 +51,9 @@ const ChatInput = forwardRef(({ onSend, onStop, isLoading = false, disabled = fa
50
51
  })
51
52
  });
52
53
  });
53
- ChatInput.displayName = "ChatInput";
54
+ ChatInputInner.displayName = "ChatInput";
55
+ /** 输入框组件:memo 化,避免父组件每次渲染时重建 Sender 内部状态 */
56
+ const ChatInput = memo(ChatInputInner);
54
57
  //#endregion
55
58
  //#region src/components/ToolCallRenderer.tsx
56
59
  /**
@@ -62,9 +65,10 @@ ChatInput.displayName = "ChatInput";
62
65
  * 3. 默认渲染:简单的工具卡片样式
63
66
  */
64
67
  const ToolCallRenderer = ({ tool, record, isLoading }) => {
65
- if (tool?.render) return /* @__PURE__ */ jsx("div", {
68
+ const customRender = tool?.render;
69
+ if (customRender) return /* @__PURE__ */ jsx("div", {
66
70
  className: "tool-call-wrapper",
67
- children: tool.render({
71
+ children: /* @__PURE__ */ jsx(customRender, {
68
72
  name: record.name,
69
73
  args: record.args,
70
74
  result: record.result,
@@ -119,25 +123,25 @@ function isFrontendTool(tool) {
119
123
  //#endregion
120
124
  //#region src/components/ReasoningContent.tsx
121
125
  const REASONING_CONTENT_MAX_HEIGHT = 58;
122
- const ReasoningContent = ({ content }) => {
126
+ const ReasoningContent = memo(function ReasoningContent({ content }) {
123
127
  const [isExpanded, setIsExpanded] = useState(false);
124
128
  const containerRef = useRef(null);
125
129
  const [isOverflowing, setIsOverflowing] = useState(false);
130
+ const checkOverflow = useCallback(() => {
131
+ const node = containerRef.current;
132
+ if (!node) return;
133
+ setIsOverflowing(node.scrollHeight > REASONING_CONTENT_MAX_HEIGHT);
134
+ }, []);
126
135
  useEffect(() => {
127
- const checkOverflow = () => {
128
- if (containerRef.current) {
129
- const originalMaxHeight = containerRef.current.style.maxHeight;
130
- containerRef.current.style.maxHeight = "none";
131
- const height = containerRef.current.scrollHeight;
132
- containerRef.current.style.maxHeight = originalMaxHeight;
133
- setIsOverflowing(height > REASONING_CONTENT_MAX_HEIGHT);
134
- }
135
- };
136
136
  checkOverflow();
137
+ }, [content, checkOverflow]);
138
+ useEffect(() => {
139
+ const node = containerRef.current;
140
+ if (!node) return;
137
141
  const resizeObserver = new ResizeObserver(checkOverflow);
138
- if (containerRef.current) resizeObserver.observe(containerRef.current);
142
+ resizeObserver.observe(node);
139
143
  return () => resizeObserver.disconnect();
140
- }, [content]);
144
+ }, [checkOverflow]);
141
145
  return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
142
146
  ref: containerRef,
143
147
  style: {
@@ -164,49 +168,46 @@ const ReasoningContent = ({ content }) => {
164
168
  }],
165
169
  onClick: () => setIsExpanded(true)
166
170
  })] });
167
- };
171
+ });
172
+ ReasoningContent.displayName = "ReasoningContent";
168
173
  //#endregion
169
174
  //#region src/components/MessageContentRenderer.tsx
170
175
  const CustomParagraph = (props) => {
171
176
  const { node: _node, ...rest } = props;
172
177
  return /* @__PURE__ */ jsx("span", { ...rest });
173
178
  };
174
- /** 渲染单个内容块 */
175
- const ContentBlock = ({ block, index, components, securityConfig }) => {
176
- if (block.type === "text" && "text" in block) return /* @__PURE__ */ jsx(Streamdown, {
177
- components: {
178
- p: CustomParagraph,
179
- ...components
180
- },
181
- allowedTags: securityConfig?.allowedTags,
182
- literalTagContent: securityConfig?.literalTagContent,
183
- controls: {
184
- table: {
185
- copy: false,
186
- download: false,
187
- fullscreen: false
188
- },
189
- code: {
190
- copy: false,
191
- download: false
192
- }
193
- },
194
- children: String(block.text)
195
- }, index);
196
- if (block.type === "image_url" && "image_url" in block) {
197
- const imageUrl = block.image_url.url;
198
- const [imageSize, setImageSize] = React.useState(null);
199
- React.useEffect(() => {
200
- const img = new Image();
201
- img.onload = () => {
202
- setImageSize({
203
- width: img.naturalWidth,
204
- height: img.naturalHeight
205
- });
206
- };
207
- img.src = imageUrl;
208
- }, [imageUrl]);
209
- const getImageStyle = () => {
179
+ const STREAMDOWN_CONTROLS = {
180
+ table: {
181
+ copy: false,
182
+ download: false,
183
+ fullscreen: false
184
+ },
185
+ code: {
186
+ copy: false,
187
+ download: false
188
+ }
189
+ };
190
+ /**
191
+ * 图片内容块
192
+ *
193
+ * 独立组件承载图片加载状态(修复在条件分支中调用 hooks 的问题)
194
+ */
195
+ const ImageBlock = memo(function ImageBlock({ imageUrl }) {
196
+ const [imageSize, setImageSize] = React.useState(null);
197
+ React.useEffect(() => {
198
+ const img = new Image();
199
+ img.onload = () => {
200
+ setImageSize({
201
+ width: img.naturalWidth,
202
+ height: img.naturalHeight
203
+ });
204
+ };
205
+ img.src = imageUrl;
206
+ }, [imageUrl]);
207
+ return /* @__PURE__ */ jsx("img", {
208
+ src: imageUrl,
209
+ alt: "Message content",
210
+ style: useMemo(() => {
210
211
  const baseStyle = {
211
212
  borderRadius: "8px",
212
213
  marginTop: "8px",
@@ -223,50 +224,55 @@ const ContentBlock = ({ block, index, components, securityConfig }) => {
223
224
  width: "auto",
224
225
  height: "auto"
225
226
  };
226
- else return {
227
+ return {
227
228
  ...baseStyle,
228
229
  maxWidth: "100%",
229
230
  width: "auto",
230
231
  height: "auto"
231
232
  };
232
- };
233
- return /* @__PURE__ */ jsx("img", {
234
- src: imageUrl,
235
- alt: "Message content",
236
- style: getImageStyle()
237
- }, index);
233
+ }, [imageSize])
234
+ });
235
+ });
236
+ /** 渲染单个内容块(memo 化,内容未变化时跳过重渲染) */
237
+ const ContentBlock = memo(function ContentBlock({ block, components, securityConfig }) {
238
+ const mergedComponents = useMemo(() => ({
239
+ p: CustomParagraph,
240
+ ...components
241
+ }), [components]);
242
+ if (block.type === "text" && "text" in block) return /* @__PURE__ */ jsx(Streamdown, {
243
+ components: mergedComponents,
244
+ allowedTags: securityConfig?.allowedTags,
245
+ literalTagContent: securityConfig?.literalTagContent,
246
+ controls: STREAMDOWN_CONTROLS,
247
+ children: String(block.text)
248
+ });
249
+ if (block.type === "image_url" && "image_url" in block) {
250
+ const imageUrl = block.image_url.url;
251
+ return /* @__PURE__ */ jsx(ImageBlock, { imageUrl });
238
252
  }
239
253
  return null;
240
- };
254
+ });
241
255
  /** 消息内容渲染组件 - 支持字符串和多模态内容块 */
242
- const MessageContentRenderer = ({ content, components, securityConfig }) => {
256
+ const MessageContentRendererInner = ({ content, components, securityConfig }) => {
257
+ const mergedComponents = useMemo(() => ({
258
+ p: CustomParagraph,
259
+ ...components
260
+ }), [components]);
243
261
  if (typeof content === "string") return /* @__PURE__ */ jsx(Streamdown, {
244
- components: {
245
- p: CustomParagraph,
246
- ...components
247
- },
262
+ components: mergedComponents,
248
263
  allowedTags: securityConfig?.allowedTags,
249
264
  literalTagContent: securityConfig?.literalTagContent,
250
- controls: {
251
- table: {
252
- copy: false,
253
- download: false,
254
- fullscreen: false
255
- },
256
- code: {
257
- copy: false,
258
- download: false
259
- }
260
- },
265
+ controls: STREAMDOWN_CONTROLS,
261
266
  children: content
262
267
  });
263
268
  return /* @__PURE__ */ jsx(Fragment, { children: content.map((block, index) => /* @__PURE__ */ jsx(ContentBlock, {
264
269
  block,
265
- index,
266
270
  components,
267
271
  securityConfig
268
272
  }, index)) });
269
273
  };
274
+ /** memo 化:content/components/securityConfig 未变化时跳过重渲染 */
275
+ const MessageContentRenderer = memo(MessageContentRendererInner);
270
276
  //#endregion
271
277
  //#region src/components/WaveLoading.tsx
272
278
  /**
@@ -308,6 +314,32 @@ const WaveLoading = ({ color = "#1890ff" }) => {
308
314
  };
309
315
  //#endregion
310
316
  //#region src/components/MessageList.tsx
317
+ /** 浅比较依赖数组(用于判断渲染结果是否可以复用) */
318
+ const depsEqual = (a, b) => {
319
+ if (a.length !== b.length) return false;
320
+ for (let i = 0; i < a.length; i++) if (!Object.is(a[i], b[i])) return false;
321
+ return true;
322
+ };
323
+ /**
324
+ * 依赖不变时复用缓存值
325
+ *
326
+ * 保证 React 元素/渲染项的引用稳定,让 React 的 bailout 与 antd Bubble 的 memo 生效,
327
+ * 流式更新时只重建真正变化的消息
328
+ */
329
+ const getCachedValue = (cache, key, deps, build) => {
330
+ const cached = cache.get(key);
331
+ if (cached && depsEqual(cached.deps, deps)) return cached.value;
332
+ const value = build();
333
+ cache.set(key, {
334
+ deps,
335
+ value
336
+ });
337
+ return value;
338
+ };
339
+ /** 清理已不存在的缓存项(如切换会话后) */
340
+ const sweepCache = (cache, aliveKeys) => {
341
+ for (const key of cache.keys()) if (!aliveKeys.has(key)) cache.delete(key);
342
+ };
311
343
  const renderMessageContent = (message, isLastMessage, isLoading, tools, toolExecutions, components, securityConfig) => {
312
344
  const hasToolCalls = message.toolCalls && message.toolCalls.length > 0;
313
345
  const isContentEmpty = !message.content || (typeof message.content === "string" ? message.content === "" : message.content.length === 0);
@@ -342,14 +374,6 @@ const renderMessageContent = (message, isLastMessage, isLoading, tools, toolExec
342
374
  ]
343
375
  }, message.id);
344
376
  };
345
- const toBubbleItem = (message, isLastMessage, isLoading, tools, toolExecutions, components, securityConfig) => {
346
- return {
347
- key: message.id,
348
- role: message.type === "human" ? "user" : "ai",
349
- content: renderMessageContent(message, isLastMessage, isLoading, tools, toolExecutions, components, securityConfig),
350
- placement: message.type === "human" ? "end" : "start"
351
- };
352
- };
353
377
  const renderToolCalls = (toolCalls, tools, toolExecutions, isLoading) => {
354
378
  return toolCalls.map((call) => {
355
379
  const tool = findTool(tools, call.name);
@@ -395,18 +419,32 @@ const roleConfig = {
395
419
  };
396
420
  const MessageList = ({ messages, isLoading = false, className = "", tools, toolExecutions, components, securityConfig, loadingColor, interruptRender }) => {
397
421
  const reasoningCacheRef = useRef(/* @__PURE__ */ new Map());
422
+ const fixedMessageCacheRef = useRef(/* @__PURE__ */ new Map());
423
+ const messageElementCacheRef = useRef(/* @__PURE__ */ new Map());
424
+ const bubbleItemCacheRef = useRef(/* @__PURE__ */ new Map());
398
425
  const processedMessages = useMemo(() => {
399
426
  const cache = reasoningCacheRef.current;
427
+ const fixedCache = fixedMessageCacheRef.current;
400
428
  return messages.map((message) => {
401
- if (message.type === "ai" && message.reasoningContent) cache.set(message.id, message.reasoningContent);
402
- if (message.type === "ai" && !message.reasoningContent) {
403
- const cachedReasoning = cache.get(message.id);
404
- if (cachedReasoning) return {
405
- ...message,
406
- reasoningContent: cachedReasoning
407
- };
429
+ if (message.type !== "ai") return message;
430
+ if (message.reasoningContent) {
431
+ cache.set(message.id, message.reasoningContent);
432
+ return message;
408
433
  }
409
- return message;
434
+ const cachedReasoning = cache.get(message.id);
435
+ if (!cachedReasoning) return message;
436
+ const existing = fixedCache.get(message.id);
437
+ if (existing && existing.raw === message && existing.reasoning === cachedReasoning) return existing.fixed;
438
+ const fixed = {
439
+ ...message,
440
+ reasoningContent: cachedReasoning
441
+ };
442
+ fixedCache.set(message.id, {
443
+ raw: message,
444
+ reasoning: cachedReasoning,
445
+ fixed
446
+ });
447
+ return fixed;
410
448
  });
411
449
  }, [messages]);
412
450
  const groupedItems = useMemo(() => {
@@ -421,28 +459,48 @@ const MessageList = ({ messages, isLoading = false, className = "", tools, toolE
421
459
  }
422
460
  return groups;
423
461
  }, [processedMessages]);
462
+ const getMessageElement = (message, isLastMessage) => {
463
+ const hasToolCalls = !!(message.toolCalls && message.toolCalls.length > 0);
464
+ const deps = [
465
+ message,
466
+ isLastMessage,
467
+ components,
468
+ securityConfig
469
+ ];
470
+ if (isLastMessage || hasToolCalls) deps.push(isLoading);
471
+ if (hasToolCalls) {
472
+ deps.push(tools);
473
+ for (const call of message.toolCalls) {
474
+ const execution = toolExecutions.get(call.id);
475
+ deps.push(call.id, execution?.status, execution?.result, execution?.error);
476
+ }
477
+ }
478
+ return getCachedValue(messageElementCacheRef.current, message.id, deps, () => renderMessageContent(message, isLastMessage, isLoading, tools, toolExecutions, components, securityConfig));
479
+ };
424
480
  const items = groupedItems.map((group, groupIndex) => {
425
481
  const isLastGroup = groupIndex === groupedItems.length - 1;
426
482
  const isLastAiGroup = isLastGroup && group.type === "ai";
427
- if (group.type === "user" || group.messages.length === 1) {
428
- const message = group.messages[0];
429
- const bubbleItem = toBubbleItem(message, isLastGroup && group.messages.length === 1, isLoading, tools, toolExecutions, components, securityConfig);
430
- if (isLastAiGroup && interruptRender) return {
431
- ...bubbleItem,
432
- content: /* @__PURE__ */ jsxs(Fragment, { children: [bubbleItem.content, interruptRender()] })
483
+ const cacheKey = group.messages[0].id;
484
+ const elements = group.messages.map((message, msgIndex) => getMessageElement(message, isLastGroup && msgIndex === group.messages.length - 1));
485
+ const includeInterrupt = !!(isLastAiGroup && interruptRender);
486
+ const deps = [
487
+ isLastGroup,
488
+ isLastAiGroup,
489
+ ...elements
490
+ ];
491
+ if (includeInterrupt) deps.push(interruptRender);
492
+ return getCachedValue(bubbleItemCacheRef.current, cacheKey, deps, () => {
493
+ const content = includeInterrupt ? /* @__PURE__ */ jsxs(Fragment, { children: [elements, interruptRender?.()] }) : elements.length === 1 ? elements[0] : /* @__PURE__ */ jsx(Fragment, { children: elements });
494
+ return {
495
+ key: cacheKey,
496
+ role: group.type === "user" ? "user" : "ai",
497
+ content,
498
+ placement: group.type === "user" ? "end" : "start"
433
499
  };
434
- return bubbleItem;
435
- }
436
- const mergedContent = /* @__PURE__ */ jsxs(Fragment, { children: [group.messages.map((message, msgIndex) => {
437
- return renderMessageContent(message, isLastGroup && msgIndex === group.messages.length - 1, isLoading, tools, toolExecutions, components, securityConfig);
438
- }), isLastAiGroup && interruptRender?.()] });
439
- return {
440
- key: group.messages.map((m) => m.id).join("-"),
441
- role: "ai",
442
- content: mergedContent,
443
- placement: "start"
444
- };
500
+ });
445
501
  });
502
+ if (messageElementCacheRef.current.size > processedMessages.length) sweepCache(messageElementCacheRef.current, new Set(processedMessages.map((m) => m.id)));
503
+ if (bubbleItemCacheRef.current.size > groupedItems.length) sweepCache(bubbleItemCacheRef.current, new Set(groupedItems.map((group) => group.messages[0].id)));
446
504
  const hasVisibleContent = (msg) => {
447
505
  if (!msg) return false;
448
506
  const hasContent = msg.content && (typeof msg.content === "string" ? msg.content !== "" : msg.content.length > 0);
@@ -577,9 +635,13 @@ function useToolExecution({ tools, toolCalls, isLoading = false, onExecutionChan
577
635
  const batchSubmittedRef = useRef(false);
578
636
  const cancelledCallIdsRef = useRef(/* @__PURE__ */ new Set());
579
637
  const cancelledExecutionsRef = useRef(/* @__PURE__ */ new Set());
638
+ const toolCallsRef = useRef(toolCalls);
639
+ useEffect(() => {
640
+ toolCallsRef.current = toolCalls;
641
+ }, [toolCalls]);
580
642
  useEffect(() => {
581
643
  if (!completedToolResults || completedToolResults.size === 0) return;
582
- completedToolResults.forEach(({ result, status }, callId) => {
644
+ completedToolResults.forEach(({ result }, callId) => {
583
645
  if (notifiedCompletedRef.current.has(callId)) return;
584
646
  if (cancelledExecutionsRef.current.has(callId)) {
585
647
  notifiedCompletedRef.current.add(callId);
@@ -733,7 +795,7 @@ function useToolExecution({ tools, toolCalls, isLoading = false, onExecutionChan
733
795
  cancelledExecutionsRef.current.add(callId);
734
796
  batchCallIdsRef.current.delete(callId);
735
797
  }
736
- const call = toolCalls.find((c) => c.id === callId);
798
+ const call = toolCallsRef.current.find((c) => c.id === callId);
737
799
  if (call) onExecutionChange?.({
738
800
  callId,
739
801
  name: call.name,
@@ -741,7 +803,7 @@ function useToolExecution({ tools, toolCalls, isLoading = false, onExecutionChan
741
803
  status: "cancelled"
742
804
  });
743
805
  }
744
- }, [toolCalls, onExecutionChange]);
806
+ }, [onExecutionChange]);
745
807
  useEffect(() => {
746
808
  processToolCalls();
747
809
  }, [useMemo(() => {
@@ -826,37 +888,72 @@ function toChatMessage(message, toolResults) {
826
888
  };
827
889
  }
828
890
  /**
829
- * 预处理消息列表:建立 tool_call_id -> { result, status } 映射,并过滤 ToolMessage
891
+ * 创建增量消息处理器
892
+ *
893
+ * 与每次全量重建不同,处理器按消息对象引用做缓存:
894
+ * - 未变化的原始消息会复用同一个 ChatMessage 引用(流式场景下大部分消息不变,下游 memo 才能生效)
895
+ * - ToolMessage 的 JSON 解析结果逐条缓存,避免每个流式分片重复解析
896
+ * - 工具结果集合未变化时返回同一个 Map 引用
830
897
  */
831
- function processMessages(rawMessages) {
832
- const toolResults = /* @__PURE__ */ new Map();
833
- for (const message of rawMessages) if (message.type === "tool") {
834
- const toolCallId = message.tool_call_id || message.additional_kwargs?.tool_call_id;
835
- if (toolCallId) {
836
- const textContent = extractTextFromContent(extractContent(message));
837
- const status = message.status;
838
- try {
839
- toolResults.set(toolCallId, {
840
- result: JSON.parse(textContent),
841
- status
842
- });
843
- } catch {
844
- toolResults.set(toolCallId, {
845
- result: textContent,
846
- status
847
- });
898
+ function createMessageProcessor() {
899
+ const toolParseCache = /* @__PURE__ */ new WeakMap();
900
+ const chatCache = /* @__PURE__ */ new WeakMap();
901
+ let toolResults = /* @__PURE__ */ new Map();
902
+ return { process(rawMessages) {
903
+ const nextToolResults = /* @__PURE__ */ new Map();
904
+ for (const message of rawMessages) {
905
+ if (message.type !== "tool") continue;
906
+ let parsed = toolParseCache.get(message);
907
+ if (!parsed) {
908
+ const callId = message.tool_call_id || message.additional_kwargs?.tool_call_id;
909
+ if (!callId) continue;
910
+ const textContent = extractTextFromContent(extractContent(message));
911
+ const status = message.status;
912
+ let result;
913
+ try {
914
+ result = JSON.parse(textContent);
915
+ } catch {
916
+ result = textContent;
917
+ }
918
+ parsed = {
919
+ callId,
920
+ entry: {
921
+ result,
922
+ status
923
+ }
924
+ };
925
+ toolParseCache.set(message, parsed);
848
926
  }
927
+ nextToolResults.set(parsed.callId, parsed.entry);
849
928
  }
850
- }
851
- const messages = [];
852
- for (const message of rawMessages) {
853
- const chatMessage = toChatMessage(message, toolResults);
854
- if (chatMessage) messages.push(chatMessage);
855
- }
856
- return {
857
- messages,
858
- toolResults
859
- };
929
+ let toolResultsChanged = nextToolResults.size !== toolResults.size;
930
+ if (!toolResultsChanged) {
931
+ for (const [callId, entry] of nextToolResults) if (toolResults.get(callId) !== entry) {
932
+ toolResultsChanged = true;
933
+ break;
934
+ }
935
+ }
936
+ if (toolResultsChanged) toolResults = nextToolResults;
937
+ const messages = [];
938
+ for (const message of rawMessages) {
939
+ let chat;
940
+ if (chatCache.has(message)) {
941
+ chat = chatCache.get(message) ?? null;
942
+ if (chat && chat.toolCalls && toolResultsChanged) {
943
+ chat = toChatMessage(message, toolResults);
944
+ chatCache.set(message, chat);
945
+ }
946
+ } else {
947
+ chat = toChatMessage(message, toolResults);
948
+ chatCache.set(message, chat);
949
+ }
950
+ if (chat) messages.push(chat);
951
+ }
952
+ return {
953
+ messages,
954
+ toolResults
955
+ };
956
+ } };
860
957
  }
861
958
  //#endregion
862
959
  //#region src/utils/injectStyles.ts
@@ -1045,6 +1142,11 @@ injectStyles();
1045
1142
  const AgentChat = forwardRef(({ stream, className = "", tools, contexts, messageConfig, inputConfig, interruptConfig, agentState, welcome }, ref) => {
1046
1143
  const { onPreSend, ...chatInputConfig } = inputConfig || {};
1047
1144
  const chatInputRef = useRef(null);
1145
+ const [messageProcessor] = useState(() => createMessageProcessor());
1146
+ const streamRef = useRef(stream);
1147
+ useEffect(() => {
1148
+ streamRef.current = stream;
1149
+ });
1048
1150
  useImperativeHandle(ref, () => ({
1049
1151
  input: chatInputRef.current,
1050
1152
  setSkill: (skill) => chatInputRef.current?.setSkill?.(skill),
@@ -1052,18 +1154,19 @@ const AgentChat = forwardRef(({ stream, className = "", tools, contexts, message
1052
1154
  focusInput: () => chatInputRef.current?.focus?.()
1053
1155
  }));
1054
1156
  const interruptEvent = stream.interrupt;
1157
+ const submitLatest = useCallback((values, options) => streamRef.current.submit(values, options), []);
1055
1158
  const { renderInterrupt: interruptRender } = useInterrupt({
1056
1159
  interrupt: interruptEvent ? {
1057
1160
  value: interruptEvent.value,
1058
1161
  id: interruptEvent.id
1059
1162
  } : null,
1060
1163
  config: interruptConfig,
1061
- onSubmit: stream.submit
1164
+ onSubmit: submitLatest
1062
1165
  });
1063
1166
  const [toolExecutions, setToolExecutions] = useState(/* @__PURE__ */ new Map());
1064
1167
  const { messages, toolResults } = useMemo(() => {
1065
- return processMessages(stream.messages);
1066
- }, [stream.messages]);
1168
+ return messageProcessor.process(stream.messages);
1169
+ }, [messageProcessor, stream.messages]);
1067
1170
  const allToolCalls = useMemo(() => {
1068
1171
  return messages.flatMap((msg) => msg.toolCalls || []);
1069
1172
  }, [messages]);
@@ -1083,8 +1186,12 @@ const AgentChat = forwardRef(({ stream, className = "", tools, contexts, message
1083
1186
  return false;
1084
1187
  }, [toolExecutions, frontendToolNames]);
1085
1188
  const isLoading = stream.isLoading || hasToolExecuting;
1189
+ const securityConfig = useMemo(() => ({
1190
+ allowedTags: messageConfig?.allowedTags,
1191
+ literalTagContent: messageConfig?.literalTagContent
1192
+ }), [messageConfig?.allowedTags, messageConfig?.literalTagContent]);
1086
1193
  const submitToStream = useCallback(async (submitMessages) => {
1087
- await stream.submit({
1194
+ await streamRef.current.submit({
1088
1195
  ...agentState,
1089
1196
  messages: submitMessages,
1090
1197
  agentkit: {
@@ -1104,7 +1211,6 @@ const AgentChat = forwardRef(({ stream, className = "", tools, contexts, message
1104
1211
  }
1105
1212
  });
1106
1213
  }, [
1107
- stream,
1108
1214
  frontendTools,
1109
1215
  contexts,
1110
1216
  agentState
@@ -1137,8 +1243,8 @@ const AgentChat = forwardRef(({ stream, className = "", tools, contexts, message
1137
1243
  });
1138
1244
  const handleStop = useCallback(async () => {
1139
1245
  cancelToolExecution();
1140
- await stream.stop();
1141
- }, [stream, cancelToolExecution]);
1246
+ await streamRef.current.stop();
1247
+ }, [cancelToolExecution]);
1142
1248
  const shouldRenderWelcome = messages.length === 0 && !isLoading && welcome;
1143
1249
  return /* @__PURE__ */ jsxs("div", {
1144
1250
  className: `agent-chat-container ${className}`,
@@ -1151,10 +1257,7 @@ const AgentChat = forwardRef(({ stream, className = "", tools, contexts, message
1151
1257
  tools,
1152
1258
  toolExecutions,
1153
1259
  components: messageConfig?.components,
1154
- securityConfig: {
1155
- allowedTags: messageConfig?.allowedTags,
1156
- literalTagContent: messageConfig?.literalTagContent
1157
- },
1260
+ securityConfig,
1158
1261
  loadingColor: messageConfig?.loadingColor,
1159
1262
  interruptRender
1160
1263
  }), /* @__PURE__ */ jsx(ChatInput, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vegintech/langchain-react-agent",
3
- "version": "0.0.41",
3
+ "version": "0.0.43",
4
4
  "description": "LangChain Agent UI component library for React",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -23,9 +23,9 @@
23
23
  "prepublishOnly": "vp run build"
24
24
  },
25
25
  "dependencies": {
26
- "@ant-design/x": "^2.4.0",
26
+ "@ant-design/x": "^2.8.0",
27
27
  "@langchain/langgraph-sdk": "^1.8.7",
28
- "streamdown": "^2.5.0"
28
+ "streamdown": "^2.6.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/node": "^25.5.0",