@librechat/agents 3.4.0 → 3.4.2

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.
Files changed (204) hide show
  1. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  2. package/dist/cjs/graphs/Graph.cjs +165 -57
  3. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  4. package/dist/cjs/graphs/MultiAgentGraph.cjs +81 -33
  5. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  6. package/dist/cjs/graphs/applyGraphRuntimeConfig.cjs +14 -0
  7. package/dist/cjs/graphs/applyGraphRuntimeConfig.cjs.map +1 -0
  8. package/dist/cjs/graphs/createGraph.cjs +15 -0
  9. package/dist/cjs/graphs/createGraph.cjs.map +1 -0
  10. package/dist/cjs/graphs/index.cjs +1 -0
  11. package/dist/cjs/hooks/HookRegistry.cjs +83 -0
  12. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  13. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +1 -1
  14. package/dist/cjs/hooks/executeHooks.cjs +60 -14
  15. package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
  16. package/dist/cjs/hooks/index.cjs.map +1 -1
  17. package/dist/cjs/hooks/types.cjs +2 -0
  18. package/dist/cjs/hooks/types.cjs.map +1 -1
  19. package/dist/cjs/llm/init.cjs +3 -3
  20. package/dist/cjs/llm/invoke.cjs +2 -2
  21. package/dist/cjs/main.cjs +34 -24
  22. package/dist/cjs/messages/format.cjs +40 -23
  23. package/dist/cjs/messages/format.cjs.map +1 -1
  24. package/dist/cjs/run.cjs +130 -53
  25. package/dist/cjs/run.cjs.map +1 -1
  26. package/dist/cjs/session/AgentSession.cjs +37 -2
  27. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  28. package/dist/cjs/session/JsonlSessionStore.cjs +2 -2
  29. package/dist/cjs/stream.cjs +1 -1
  30. package/dist/cjs/summarization/node.cjs +6 -3
  31. package/dist/cjs/summarization/node.cjs.map +1 -1
  32. package/dist/cjs/tools/BashExecutor.cjs +1 -1
  33. package/dist/cjs/tools/CodeExecutor.cjs +1 -1
  34. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +1 -1
  35. package/dist/cjs/tools/SubagentTool.cjs +4 -4
  36. package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
  37. package/dist/cjs/tools/ToolNode.cjs +321 -95
  38. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  39. package/dist/cjs/tools/ToolSearch.cjs +1 -1
  40. package/dist/cjs/tools/eagerEventExecution.cjs +1 -0
  41. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +3 -3
  42. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +2 -2
  43. package/dist/cjs/tools/search/rerankers.cjs +154 -1
  44. package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
  45. package/dist/cjs/tools/search/tool.cjs +4 -1
  46. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  47. package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs +601 -0
  48. package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs.map +1 -0
  49. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1114 -233
  50. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  51. package/dist/cjs/tools/subagent/SubagentReplay.cjs +177 -0
  52. package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -0
  53. package/dist/cjs/tools/subagent/childGraphConfig.cjs +278 -0
  54. package/dist/cjs/tools/subagent/childGraphConfig.cjs.map +1 -0
  55. package/dist/cjs/tools/subagent/index.cjs +1 -0
  56. package/dist/cjs/tools/subagent/runtimeLimits.cjs +5 -0
  57. package/dist/cjs/tools/subagent/runtimeLimits.cjs.map +1 -0
  58. package/dist/cjs/tools/toolOutputReferences.cjs +20 -0
  59. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  60. package/dist/cjs/utils/index.cjs +2 -2
  61. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  62. package/dist/esm/graphs/Graph.mjs +165 -57
  63. package/dist/esm/graphs/Graph.mjs.map +1 -1
  64. package/dist/esm/graphs/MultiAgentGraph.mjs +81 -33
  65. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  66. package/dist/esm/graphs/applyGraphRuntimeConfig.mjs +14 -0
  67. package/dist/esm/graphs/applyGraphRuntimeConfig.mjs.map +1 -0
  68. package/dist/esm/graphs/createGraph.mjs +15 -0
  69. package/dist/esm/graphs/createGraph.mjs.map +1 -0
  70. package/dist/esm/graphs/index.mjs +1 -0
  71. package/dist/esm/hooks/HookRegistry.mjs +83 -0
  72. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  73. package/dist/esm/hooks/createWorkspacePolicyHook.mjs +1 -1
  74. package/dist/esm/hooks/executeHooks.mjs +60 -14
  75. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  76. package/dist/esm/hooks/index.mjs.map +1 -1
  77. package/dist/esm/hooks/types.mjs +2 -1
  78. package/dist/esm/hooks/types.mjs.map +1 -1
  79. package/dist/esm/llm/init.mjs +1 -1
  80. package/dist/esm/llm/invoke.mjs +2 -2
  81. package/dist/esm/main.mjs +21 -19
  82. package/dist/esm/messages/format.mjs +40 -23
  83. package/dist/esm/messages/format.mjs.map +1 -1
  84. package/dist/esm/run.mjs +131 -54
  85. package/dist/esm/run.mjs.map +1 -1
  86. package/dist/esm/session/AgentSession.mjs +37 -2
  87. package/dist/esm/session/AgentSession.mjs.map +1 -1
  88. package/dist/esm/session/JsonlSessionStore.mjs +2 -2
  89. package/dist/esm/stream.mjs +1 -1
  90. package/dist/esm/summarization/node.mjs +6 -3
  91. package/dist/esm/summarization/node.mjs.map +1 -1
  92. package/dist/esm/tools/BashExecutor.mjs +1 -1
  93. package/dist/esm/tools/CodeExecutor.mjs +1 -1
  94. package/dist/esm/tools/ProgrammaticToolCalling.mjs +1 -1
  95. package/dist/esm/tools/SubagentTool.mjs +4 -4
  96. package/dist/esm/tools/SubagentTool.mjs.map +1 -1
  97. package/dist/esm/tools/ToolNode.mjs +322 -96
  98. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  99. package/dist/esm/tools/ToolSearch.mjs +1 -1
  100. package/dist/esm/tools/eagerEventExecution.mjs +1 -1
  101. package/dist/esm/tools/local/LocalExecutionEngine.mjs +3 -3
  102. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +2 -2
  103. package/dist/esm/tools/search/rerankers.mjs +154 -1
  104. package/dist/esm/tools/search/rerankers.mjs.map +1 -1
  105. package/dist/esm/tools/search/tool.mjs +4 -1
  106. package/dist/esm/tools/search/tool.mjs.map +1 -1
  107. package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs +597 -0
  108. package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs.map +1 -0
  109. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1114 -233
  110. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  111. package/dist/esm/tools/subagent/SubagentReplay.mjs +170 -0
  112. package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -0
  113. package/dist/esm/tools/subagent/childGraphConfig.mjs +272 -0
  114. package/dist/esm/tools/subagent/childGraphConfig.mjs.map +1 -0
  115. package/dist/esm/tools/subagent/index.mjs +1 -0
  116. package/dist/esm/tools/subagent/runtimeLimits.mjs +5 -0
  117. package/dist/esm/tools/subagent/runtimeLimits.mjs.map +1 -0
  118. package/dist/esm/tools/toolOutputReferences.mjs +20 -0
  119. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  120. package/dist/esm/utils/index.mjs +2 -2
  121. package/dist/types/agents/AgentContext.d.ts +1 -1
  122. package/dist/types/graphs/Graph.d.ts +22 -2
  123. package/dist/types/graphs/MultiAgentGraph.d.ts +7 -1
  124. package/dist/types/graphs/applyGraphRuntimeConfig.d.ts +3 -0
  125. package/dist/types/graphs/createGraph.d.ts +13 -0
  126. package/dist/types/graphs/graphFactory.d.ts +13 -0
  127. package/dist/types/graphs/index.d.ts +2 -0
  128. package/dist/types/hooks/HookRegistry.d.ts +10 -1
  129. package/dist/types/hooks/executeHooks.d.ts +5 -1
  130. package/dist/types/hooks/index.d.ts +2 -2
  131. package/dist/types/hooks/types.d.ts +12 -0
  132. package/dist/types/run.d.ts +6 -3
  133. package/dist/types/session/AgentSession.d.ts +1 -0
  134. package/dist/types/tools/SubagentTool.d.ts +1 -1
  135. package/dist/types/tools/ToolNode.d.ts +23 -15
  136. package/dist/types/tools/search/rerankers.d.ts +28 -0
  137. package/dist/types/tools/search/types.d.ts +38 -1
  138. package/dist/types/tools/subagent/SubagentExecutionRegistry.d.ts +171 -0
  139. package/dist/types/tools/subagent/SubagentExecutor.d.ts +70 -36
  140. package/dist/types/tools/subagent/SubagentReplay.d.ts +85 -0
  141. package/dist/types/tools/subagent/childGraphConfig.d.ts +37 -0
  142. package/dist/types/tools/subagent/index.d.ts +1 -1
  143. package/dist/types/tools/subagent/runtimeLimits.d.ts +4 -0
  144. package/dist/types/tools/toolOutputReferences.d.ts +12 -0
  145. package/dist/types/types/graph.d.ts +189 -24
  146. package/dist/types/types/hitl.d.ts +15 -0
  147. package/package.json +8 -2
  148. package/src/agents/AgentContext.ts +1 -1
  149. package/src/graphs/Graph.ts +349 -104
  150. package/src/graphs/MultiAgentGraph.ts +149 -47
  151. package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +48 -9
  152. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1 -1
  153. package/src/graphs/__tests__/Graph.subagentResumeState.test.ts +80 -0
  154. package/src/graphs/__tests__/MultiAgentGraph.test.ts +86 -0
  155. package/src/graphs/__tests__/composition.smoke.test.ts +152 -0
  156. package/src/graphs/__tests__/createGraph.test.ts +206 -0
  157. package/src/graphs/applyGraphRuntimeConfig.ts +25 -0
  158. package/src/graphs/createGraph.ts +32 -0
  159. package/src/graphs/graphFactory.ts +12 -0
  160. package/src/graphs/index.ts +2 -0
  161. package/src/hooks/HookRegistry.ts +151 -1
  162. package/src/hooks/__tests__/HookRegistry.test.ts +96 -0
  163. package/src/hooks/__tests__/executeHooks.test.ts +160 -0
  164. package/src/hooks/executeHooks.ts +133 -15
  165. package/src/hooks/index.ts +3 -1
  166. package/src/hooks/types.ts +16 -0
  167. package/src/messages/format.ts +54 -26
  168. package/src/messages/formatAgentMessages.reducer.test.ts +162 -0
  169. package/src/messages/formatAgentMessages.steer.test.ts +16 -11
  170. package/src/messages/formatAgentMessages.test.ts +7 -2
  171. package/src/run.ts +214 -56
  172. package/src/session/AgentSession.ts +52 -2
  173. package/src/session/__tests__/JsonlSessionStore.test.ts +53 -0
  174. package/src/specs/graph-subagent.live.test.ts +242 -0
  175. package/src/specs/graph-subagent.test.ts +322 -0
  176. package/src/specs/langfuse-routing.integration.test.ts +120 -1
  177. package/src/specs/subagent.test.ts +130 -4
  178. package/src/summarization/__tests__/node.test.ts +32 -14
  179. package/src/summarization/node.ts +15 -11
  180. package/src/tools/SubagentTool.ts +4 -4
  181. package/src/tools/ToolNode.ts +643 -157
  182. package/src/tools/__tests__/SubagentExecutor.test.ts +740 -15
  183. package/src/tools/__tests__/SubagentReplay.test.ts +329 -0
  184. package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +208 -2
  185. package/src/tools/__tests__/directToolHITLResumeScope.test.ts +512 -2
  186. package/src/tools/__tests__/hitl.test.ts +234 -5
  187. package/src/tools/__tests__/subagentHooks.test.ts +1126 -61
  188. package/src/tools/__tests__/toolOutputReferences.test.ts +19 -2
  189. package/src/tools/search/rag-api-reranker.test.ts +891 -0
  190. package/src/tools/search/rerankers.ts +260 -0
  191. package/src/tools/search/tool.ts +6 -0
  192. package/src/tools/search/types.ts +45 -1
  193. package/src/tools/subagent/SubagentExecutionRegistry.ts +1159 -0
  194. package/src/tools/subagent/SubagentExecutor.ts +2232 -312
  195. package/src/tools/subagent/SubagentReplay.ts +584 -0
  196. package/src/tools/subagent/__tests__/SubagentExecutionRegistry.test.ts +637 -0
  197. package/src/tools/subagent/__tests__/SubagentExecutor.lazy.test.ts +3054 -0
  198. package/src/tools/subagent/__tests__/childGraphConfig.test.ts +521 -0
  199. package/src/tools/subagent/childGraphConfig.ts +694 -0
  200. package/src/tools/subagent/index.ts +6 -0
  201. package/src/tools/subagent/runtimeLimits.ts +7 -0
  202. package/src/tools/toolOutputReferences.ts +40 -1
  203. package/src/types/graph.ts +222 -24
  204. package/src/types/hitl.ts +16 -0
@@ -339,6 +339,7 @@ interface FormatAssistantMessageOptions {
339
339
  preserveUnpairedServerToolUses?: boolean;
340
340
  preserveReasoningContent?: boolean;
341
341
  provider?: Providers;
342
+ sourceMessageId?: string;
342
343
  }
343
344
 
344
345
  interface FormatAgentMessagesOptions {
@@ -514,6 +515,16 @@ function formatAssistantMessage(
514
515
  | RoleBearingMessage<ToolMessage>
515
516
  | RoleBearingMessage<HumanMessage>
516
517
  > = [];
518
+ const appendFormattedMessage = (
519
+ formattedMessage: (typeof formattedMessages)[number]
520
+ ): void => {
521
+ stampSourceMessageIdentity(
522
+ formattedMessage,
523
+ options?.sourceMessageId,
524
+ formattedMessages.length
525
+ );
526
+ formattedMessages.push(formattedMessage);
527
+ };
517
528
  let currentContent: MessageContentComplex[] = [];
518
529
  let lastAIMessage: RoleBearingMessage<AIMessage> | null = null;
519
530
  let hasReasoning = false;
@@ -634,7 +645,7 @@ function formatAssistantMessage(
634
645
  ) {
635
646
  currentContent.push(part);
636
647
  lastAIMessage = createAIMessage(toLangChainContent(currentContent));
637
- formattedMessages.push(lastAIMessage);
648
+ appendFormattedMessage(lastAIMessage);
638
649
  currentContent = [];
639
650
  continue;
640
651
  }
@@ -646,13 +657,13 @@ function formatAssistantMessage(
646
657
  }, '');
647
658
  content = `${content}\n${getTextContent(part)}`.trim();
648
659
  lastAIMessage = createAIMessage(content);
649
- formattedMessages.push(lastAIMessage);
660
+ appendFormattedMessage(lastAIMessage);
650
661
  currentContent = [];
651
662
  continue;
652
663
  }
653
664
  // Create a new AIMessage with this text and prepare for tool calls
654
665
  lastAIMessage = createAIMessage(getTextContent(part));
655
- formattedMessages.push(lastAIMessage);
666
+ appendFormattedMessage(lastAIMessage);
656
667
  } else if (part.type === ContentTypes.TOOL_CALL) {
657
668
  // Skip malformed tool call entries without tool_call property
658
669
  if (part.tool_call == null) {
@@ -711,7 +722,7 @@ function formatAssistantMessage(
711
722
  if (!lastAIMessage) {
712
723
  // "Heal" the payload by creating an AIMessage to precede the tool call
713
724
  lastAIMessage = createAIMessage('');
714
- formattedMessages.push(lastAIMessage);
725
+ appendFormattedMessage(lastAIMessage);
715
726
  } else {
716
727
  attachPendingReasoningContent(lastAIMessage);
717
728
  }
@@ -749,7 +760,7 @@ function formatAssistantMessage(
749
760
  lastAIMessage.tool_calls.push(tool_call as ToolCall);
750
761
  }
751
762
 
752
- formattedMessages.push(
763
+ appendFormattedMessage(
753
764
  withMessageRole(
754
765
  new ToolMessage({
755
766
  tool_call_id: tool_call.id ?? '',
@@ -788,14 +799,14 @@ function formatAssistantMessage(
788
799
  currentContent.some((content) => content.type !== ContentTypes.TEXT)
789
800
  ) {
790
801
  lastAIMessage = createAIMessage(toLangChainContent(currentContent));
791
- formattedMessages.push(lastAIMessage);
802
+ appendFormattedMessage(lastAIMessage);
792
803
  } else {
793
804
  const flushed = currentContent
794
805
  .reduce((acc, curr) => `${acc}${getTextContent(curr)}\n`, '')
795
806
  .trim();
796
807
  if (flushed.length > 0) {
797
808
  lastAIMessage = createAIMessage(flushed);
798
- formattedMessages.push(lastAIMessage);
809
+ appendFormattedMessage(lastAIMessage);
799
810
  }
800
811
  }
801
812
  currentContent = [];
@@ -808,7 +819,7 @@ function formatAssistantMessage(
808
819
  * assistant reasoning silently vanishes on replay.
809
820
  */
810
821
  lastAIMessage = createAIMessage('');
811
- formattedMessages.push(lastAIMessage);
822
+ appendFormattedMessage(lastAIMessage);
812
823
  }
813
824
  const steerPart = part as {
814
825
  steer?: string;
@@ -818,7 +829,7 @@ function formatAssistantMessage(
818
829
  Array.isArray(steerPart.media) && steerPart.media.length > 0
819
830
  ? toLangChainContent(steerPart.media)
820
831
  : (steerPart.steer ?? '');
821
- formattedMessages.push(
832
+ appendFormattedMessage(
822
833
  withMessageRole(
823
834
  new HumanMessage({
824
835
  content: steerContent as MessageContent,
@@ -856,7 +867,7 @@ function formatAssistantMessage(
856
867
  let content = '';
857
868
  for (const part of currentContent) {
858
869
  if (part.type !== ContentTypes.TEXT) {
859
- formattedMessages.push(
870
+ appendFormattedMessage(
860
871
  createAIMessage(toLangChainContent(currentContent))
861
872
  );
862
873
  return formattedMessages;
@@ -866,10 +877,10 @@ function formatAssistantMessage(
866
877
  content = content.trim();
867
878
 
868
879
  if (content) {
869
- formattedMessages.push(createAIMessage(content));
880
+ appendFormattedMessage(createAIMessage(content));
870
881
  }
871
882
  } else if (currentContent.length > 0) {
872
- formattedMessages.push(createAIMessage(toLangChainContent(currentContent)));
883
+ appendFormattedMessage(createAIMessage(toLangChainContent(currentContent)));
873
884
  }
874
885
 
875
886
  return formattedMessages;
@@ -886,6 +897,29 @@ function getSourceMessageId(message: Partial<TMessage>): string | undefined {
886
897
  return normalized.length > 0 ? normalized : undefined;
887
898
  }
888
899
 
900
+ /**
901
+ * Keeps the first formatted message backward-compatible with its persisted
902
+ * source id and preserves source correlation on every derived message.
903
+ * Derived ids remain unset so the reducer can assign collision-free identities
904
+ * during its existing pass. This also prevents invented provider-shaped ids
905
+ * from leaking into provider request payloads.
906
+ */
907
+ function stampSourceMessageIdentity(
908
+ message: RoleBearingMessage<BaseMessage>,
909
+ sourceMessageId: string | undefined,
910
+ derivedIndex = 0
911
+ ): void {
912
+ if (sourceMessageId == null) {
913
+ return;
914
+ }
915
+ message.additional_kwargs.sourceMessageId = sourceMessageId;
916
+ if (derivedIndex !== 0) {
917
+ return;
918
+ }
919
+ message.id = sourceMessageId;
920
+ message.lc_kwargs.id = sourceMessageId;
921
+ }
922
+
889
923
  /**
890
924
  * Labels all agent content for parallel patterns (fan-out/fan-in)
891
925
  * Groups consecutive content by agent and wraps with clear labels
@@ -1565,9 +1599,7 @@ export const formatAgentMessages = (
1565
1599
  | RoleBearingMessage<HumanMessage>
1566
1600
  | RoleBearingMessage<AIMessage>
1567
1601
  | RoleBearingMessage<SystemMessage>;
1568
- if (sourceMessageId != null && sourceMessageId !== '') {
1569
- formattedMessage.id = sourceMessageId;
1570
- }
1602
+ stampSourceMessageIdentity(formattedMessage, sourceMessageId);
1571
1603
  flushSteerAnchor(formattedMessage);
1572
1604
  messages.push(formattedMessage);
1573
1605
 
@@ -1746,12 +1778,8 @@ export const formatAgentMessages = (
1746
1778
  options?.preserveReasoningContent ??
1747
1779
  options?.provider === Providers.DEEPSEEK,
1748
1780
  provider: options?.provider,
1781
+ sourceMessageId,
1749
1782
  });
1750
- if (sourceMessageId != null && sourceMessageId !== '') {
1751
- for (const formattedMessage of formattedMessages) {
1752
- formattedMessage.id = sourceMessageId;
1753
- }
1754
- }
1755
1783
  /**
1756
1784
  * A steer that ends an assistant message leaves the replay on a
1757
1785
  * `HumanMessage`. The next payload message is itself a user turn, so the
@@ -1775,12 +1803,12 @@ export const formatAgentMessages = (
1775
1803
  * the model may simply never answer. So the intent is recorded and flushed
1776
1804
  * only when a message actually follows.
1777
1805
  *
1778
- * Pushed AFTER the id stamping above, deliberately. `messagesStateReducer`
1779
- * treats a repeated id as replace-in-place, so an anchor carrying the
1780
- * shared `sourceMessageId` would overwrite the steer it exists to protect.
1781
- * Left unstamped, it reaches the reducer with a null id and is assigned a
1782
- * fresh one. `endsWithSteerMessage` reads only `additional_kwargs.source`,
1783
- * so the deferral cannot change which messages get anchored.
1806
+ * Pushed AFTER source identity stamping above, deliberately. The anchor is
1807
+ * synthetic rather than derived from the persisted assistant entry, so it
1808
+ * must not claim that entry's source metadata. Left unstamped, it reaches
1809
+ * the reducer with a null id and is assigned a fresh one.
1810
+ * `endsWithSteerMessage` reads only `additional_kwargs.source`, so the
1811
+ * deferral cannot change which messages get anchored.
1784
1812
  */
1785
1813
  /**
1786
1814
  * Guarded on emission: an assistant entry whose blocks all filtered away
@@ -0,0 +1,162 @@
1
+ import { convertMessagesToResponsesInput } from '@langchain/openai';
2
+ import { Annotation, END, START, StateGraph } from '@langchain/langgraph';
3
+ import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
4
+ import type { BaseMessage } from '@langchain/core/messages';
5
+ import { messagesStateReducer } from './reducer';
6
+ import { formatAgentMessages } from './format';
7
+ import { ContentTypes } from '@/common';
8
+
9
+ const formatToolTurn = () =>
10
+ formatAgentMessages([
11
+ {
12
+ role: 'assistant',
13
+ messageId: 'msg_assistant_1',
14
+ content: [
15
+ {
16
+ type: ContentTypes.TEXT,
17
+ [ContentTypes.TEXT]: 'Running tool',
18
+ tool_call_ids: ['tool_1'],
19
+ },
20
+ {
21
+ type: ContentTypes.TOOL_CALL,
22
+ tool_call: {
23
+ id: 'tool_1',
24
+ name: 'search',
25
+ args: '{"query":"hello"}',
26
+ output: 'world',
27
+ },
28
+ },
29
+ ],
30
+ },
31
+ ]).messages;
32
+
33
+ describe('formatAgentMessages reducer compatibility', () => {
34
+ it('does not expose derived message ids as OpenAI Responses item ids', () => {
35
+ const messages = formatAgentMessages([
36
+ {
37
+ role: 'assistant',
38
+ messageId: 'msg_provider_1',
39
+ content: [
40
+ {
41
+ type: ContentTypes.TEXT,
42
+ [ContentTypes.TEXT]: 'Running tool',
43
+ tool_call_ids: ['tool_1'],
44
+ },
45
+ {
46
+ type: ContentTypes.TOOL_CALL,
47
+ tool_call: {
48
+ id: 'tool_1',
49
+ name: 'search',
50
+ args: '{"query":"hello"}',
51
+ output: 'world',
52
+ },
53
+ },
54
+ {
55
+ type: ContentTypes.TEXT,
56
+ [ContentTypes.TEXT]: 'Finished',
57
+ },
58
+ ],
59
+ },
60
+ ]).messages;
61
+ const input = convertMessagesToResponsesInput({
62
+ messages,
63
+ zdrEnabled: false,
64
+ model: 'gpt-5.6',
65
+ });
66
+
67
+ expect(
68
+ input
69
+ .filter((item) => item.type === 'message')
70
+ .map((item) => ('id' in item ? item.id : undefined))
71
+ ).toEqual(['msg_provider_1', undefined]);
72
+ });
73
+
74
+ it('does not let a later source id replace a derived message', () => {
75
+ const messages = formatAgentMessages([
76
+ {
77
+ role: 'assistant',
78
+ messageId: 'a',
79
+ content: [
80
+ {
81
+ type: ContentTypes.TEXT,
82
+ [ContentTypes.TEXT]: 'Running tool',
83
+ tool_call_ids: ['tool_1'],
84
+ },
85
+ {
86
+ type: ContentTypes.TOOL_CALL,
87
+ tool_call: {
88
+ id: 'tool_1',
89
+ name: 'search',
90
+ args: '{"query":"hello"}',
91
+ output: 'world',
92
+ },
93
+ },
94
+ ],
95
+ },
96
+ {
97
+ role: 'user',
98
+ messageId: 'a:derived:1',
99
+ content: 'Continue',
100
+ },
101
+ ]).messages;
102
+
103
+ const merged = messagesStateReducer([], messages);
104
+
105
+ expect(merged).toHaveLength(3);
106
+ expect(merged[0]).toBeInstanceOf(AIMessage);
107
+ expect(merged[1]).toBeInstanceOf(ToolMessage);
108
+ expect(merged[2]).toBeInstanceOf(HumanMessage);
109
+ });
110
+
111
+ it('lets the reducer identify derived messages while retaining source correlation', () => {
112
+ const messages = formatToolTurn();
113
+
114
+ expect(messages).toHaveLength(2);
115
+ expect(messages[0]).toBeInstanceOf(AIMessage);
116
+ expect(messages[1]).toBeInstanceOf(ToolMessage);
117
+ expect(messages.map((message) => message.id)).toEqual([
118
+ 'msg_assistant_1',
119
+ undefined,
120
+ ]);
121
+ expect(messages.map((message) => message.lc_kwargs.id)).toEqual([
122
+ 'msg_assistant_1',
123
+ undefined,
124
+ ]);
125
+ expect(
126
+ messages.map((message) => message.additional_kwargs.sourceMessageId)
127
+ ).toEqual(['msg_assistant_1', 'msg_assistant_1']);
128
+
129
+ const merged = messagesStateReducer([], messages);
130
+ expect(merged).toHaveLength(2);
131
+ expect(merged[0]).toBeInstanceOf(AIMessage);
132
+ expect(merged[1]).toBeInstanceOf(ToolMessage);
133
+ expect(merged[0].id).toBe('msg_assistant_1');
134
+ expect(merged[1].id).toEqual(expect.any(String));
135
+ expect(merged[1].id).not.toBe(merged[0].id);
136
+ });
137
+
138
+ it('preserves the tool-call pair through a StateGraph input write', async () => {
139
+ const State = Annotation.Root({
140
+ messages: Annotation<BaseMessage[]>({
141
+ reducer: messagesStateReducer,
142
+ default: () => [],
143
+ }),
144
+ });
145
+ let observedMessages: BaseMessage[] = [];
146
+ const graph = new StateGraph(State)
147
+ .addNode('capture', (state) => {
148
+ observedMessages = state.messages;
149
+ return {};
150
+ })
151
+ .addEdge(START, 'capture')
152
+ .addEdge('capture', END)
153
+ .compile();
154
+
155
+ await graph.invoke({ messages: formatToolTurn() });
156
+
157
+ expect(observedMessages).toHaveLength(2);
158
+ expect(observedMessages[0]).toBeInstanceOf(AIMessage);
159
+ expect((observedMessages[0] as AIMessage).tool_calls).toHaveLength(1);
160
+ expect(observedMessages[1]).toBeInstanceOf(ToolMessage);
161
+ });
162
+ });
@@ -370,12 +370,9 @@ describe('formatAgentMessages trailing-steer anchor', () => {
370
370
  expect(following.content).toEqual([{ type: 'text', text: 'Next turn' }]);
371
371
  });
372
372
 
373
- /**
374
- * `messagesStateReducer` treats a repeated id as replace-in-place, so an
375
- * anchor stamped with the shared `sourceMessageId` would overwrite the very
376
- * steer it exists to protect. It must reach the reducer unstamped.
377
- */
378
- it('does not stamp the anchor with the shared source message id', () => {
373
+ /** The synthetic anchor is not derived from the persisted assistant entry,
374
+ * so it must not inherit that entry's source identity metadata. */
375
+ it('does not stamp the anchor with the source message identity', () => {
379
376
  const payload: TPayload = [
380
377
  { role: 'user', content: 'Original request' },
381
378
  {
@@ -395,13 +392,20 @@ describe('formatAgentMessages trailing-steer anchor', () => {
395
392
  (message) => message.additional_kwargs.source === 'steer'
396
393
  );
397
394
  expect(steerIndex).toBeGreaterThan(0);
398
- expect(messages[steerIndex].id).toBe('assistant_1');
395
+ expect(messages[steerIndex].id).toBeUndefined();
399
396
  expect(messages[steerIndex - 1].id).toBe('assistant_1');
397
+ expect(messages[steerIndex].additional_kwargs.sourceMessageId).toBe(
398
+ 'assistant_1'
399
+ );
400
+ expect(messages[steerIndex - 1].additional_kwargs.sourceMessageId).toBe(
401
+ 'assistant_1'
402
+ );
400
403
 
401
404
  const anchor = messages[steerIndex + 1];
402
405
  expect(anchor).toBeInstanceOf(AIMessage);
403
406
  expect(anchor.content).not.toBe('');
404
407
  expect(anchor.id).not.toBe('assistant_1');
408
+ expect(anchor.additional_kwargs.sourceMessageId).toBeUndefined();
405
409
  });
406
410
 
407
411
  /**
@@ -469,9 +473,7 @@ describe('formatAgentMessages trailing-steer anchor', () => {
469
473
  const last = messages[messages.length - 1];
470
474
  expect(last.additional_kwargs.source).toBe('steer');
471
475
  expect(
472
- messages.some(
473
- (m) => m instanceof AIMessage && m.content === '_'
474
- )
476
+ messages.some((m) => m instanceof AIMessage && m.content === '_')
475
477
  ).toBe(false);
476
478
  });
477
479
 
@@ -536,7 +538,10 @@ describe('formatAgentMessages trailing-steer anchor', () => {
536
538
  {
537
539
  role: 'assistant',
538
540
  content: [
539
- { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Doing the other thing.' },
541
+ {
542
+ type: ContentTypes.TEXT,
543
+ [ContentTypes.TEXT]: 'Doing the other thing.',
544
+ },
540
545
  ],
541
546
  },
542
547
  ];
@@ -76,7 +76,7 @@ describe('formatAgentMessages', () => {
76
76
  expect(Object.keys(result.messages[1])).not.toContain('role');
77
77
  });
78
78
 
79
- it('preserves source messageId on formatted messages', () => {
79
+ it('preserves source messageId correlation with unique formatted ids', () => {
80
80
  const payload: TPayload = [
81
81
  {
82
82
  role: 'assistant',
@@ -112,8 +112,13 @@ describe('formatAgentMessages', () => {
112
112
  'user',
113
113
  ]);
114
114
  expect(result.messages[0].id).toBe('msg_assistant_1');
115
- expect(result.messages[1].id).toBe('msg_assistant_1');
115
+ expect(result.messages[1].id).toBeUndefined();
116
116
  expect(result.messages[2].id).toBe('msg_user_1');
117
+ expect(
118
+ result.messages.map(
119
+ (message) => message.additional_kwargs.sourceMessageId
120
+ )
121
+ ).toEqual(['msg_assistant_1', 'msg_assistant_1', 'msg_user_1']);
117
122
  });
118
123
 
119
124
  it('should handle system messages', () => {