@librechat/agents 3.3.8 → 3.3.10

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 (156) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +4 -0
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +66 -13
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/graphs/MultiAgentGraph.cjs +35 -2
  6. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  7. package/dist/cjs/hitl/askUserQuestion.cjs +3 -2
  8. package/dist/cjs/hitl/askUserQuestion.cjs.map +1 -1
  9. package/dist/cjs/instrumentation.cjs +18 -48
  10. package/dist/cjs/instrumentation.cjs.map +1 -1
  11. package/dist/cjs/langfuse.cjs +174 -29
  12. package/dist/cjs/langfuse.cjs.map +1 -1
  13. package/dist/cjs/langfuseConfig.cjs +12 -0
  14. package/dist/cjs/langfuseConfig.cjs.map +1 -1
  15. package/dist/cjs/langfuseRuntimeContext.cjs +23 -2
  16. package/dist/cjs/langfuseRuntimeContext.cjs.map +1 -1
  17. package/dist/cjs/langfuseRuntimeScope.cjs +39 -8
  18. package/dist/cjs/langfuseRuntimeScope.cjs.map +1 -1
  19. package/dist/cjs/langfuseSpanRegistry.cjs +95 -0
  20. package/dist/cjs/langfuseSpanRegistry.cjs.map +1 -0
  21. package/dist/cjs/langfuseTraceShaping.cjs +121 -4
  22. package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
  23. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +39 -15
  24. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  25. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +25 -5
  26. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  27. package/dist/cjs/llm/init.cjs +3 -3
  28. package/dist/cjs/llm/invoke.cjs +5 -5
  29. package/dist/cjs/llm/openai/index.cjs +1 -1
  30. package/dist/cjs/main.cjs +10 -10
  31. package/dist/cjs/messages/prune.cjs +13 -1
  32. package/dist/cjs/messages/prune.cjs.map +1 -1
  33. package/dist/cjs/prompts/activityLabel.cjs +24 -12
  34. package/dist/cjs/prompts/activityLabel.cjs.map +1 -1
  35. package/dist/cjs/run.cjs +57 -22
  36. package/dist/cjs/run.cjs.map +1 -1
  37. package/dist/cjs/session/messageSerialization.cjs +6 -0
  38. package/dist/cjs/session/messageSerialization.cjs.map +1 -1
  39. package/dist/cjs/stream.cjs +21 -10
  40. package/dist/cjs/stream.cjs.map +1 -1
  41. package/dist/cjs/summarization/node.cjs +5 -0
  42. package/dist/cjs/summarization/node.cjs.map +1 -1
  43. package/dist/cjs/tools/ToolNode.cjs +253 -24
  44. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  45. package/dist/cjs/tools/handlers.cjs +1 -1
  46. package/dist/cjs/tools/search/tool.cjs +1 -1
  47. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -1
  48. package/dist/cjs/utils/index.cjs +2 -2
  49. package/dist/esm/agents/AgentContext.mjs +4 -0
  50. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  51. package/dist/esm/graphs/Graph.mjs +67 -14
  52. package/dist/esm/graphs/Graph.mjs.map +1 -1
  53. package/dist/esm/graphs/MultiAgentGraph.mjs +35 -2
  54. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  55. package/dist/esm/hitl/askUserQuestion.mjs +3 -2
  56. package/dist/esm/hitl/askUserQuestion.mjs.map +1 -1
  57. package/dist/esm/instrumentation.mjs +18 -48
  58. package/dist/esm/instrumentation.mjs.map +1 -1
  59. package/dist/esm/langfuse.mjs +176 -28
  60. package/dist/esm/langfuse.mjs.map +1 -1
  61. package/dist/esm/langfuseConfig.mjs +10 -1
  62. package/dist/esm/langfuseConfig.mjs.map +1 -1
  63. package/dist/esm/langfuseRuntimeContext.mjs +21 -3
  64. package/dist/esm/langfuseRuntimeContext.mjs.map +1 -1
  65. package/dist/esm/langfuseRuntimeScope.mjs +39 -10
  66. package/dist/esm/langfuseRuntimeScope.mjs.map +1 -1
  67. package/dist/esm/langfuseSpanRegistry.mjs +91 -0
  68. package/dist/esm/langfuseSpanRegistry.mjs.map +1 -0
  69. package/dist/esm/langfuseTraceShaping.mjs +121 -4
  70. package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
  71. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +39 -15
  72. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  73. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +25 -5
  74. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  75. package/dist/esm/llm/init.mjs +2 -2
  76. package/dist/esm/llm/invoke.mjs +5 -5
  77. package/dist/esm/llm/openai/index.mjs +1 -1
  78. package/dist/esm/main.mjs +8 -8
  79. package/dist/esm/messages/prune.mjs +13 -1
  80. package/dist/esm/messages/prune.mjs.map +1 -1
  81. package/dist/esm/prompts/activityLabel.mjs +24 -12
  82. package/dist/esm/prompts/activityLabel.mjs.map +1 -1
  83. package/dist/esm/run.mjs +57 -22
  84. package/dist/esm/run.mjs.map +1 -1
  85. package/dist/esm/session/messageSerialization.mjs +6 -0
  86. package/dist/esm/session/messageSerialization.mjs.map +1 -1
  87. package/dist/esm/stream.mjs +21 -10
  88. package/dist/esm/stream.mjs.map +1 -1
  89. package/dist/esm/summarization/node.mjs +5 -0
  90. package/dist/esm/summarization/node.mjs.map +1 -1
  91. package/dist/esm/tools/ToolNode.mjs +254 -25
  92. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  93. package/dist/esm/tools/handlers.mjs +1 -1
  94. package/dist/esm/tools/search/tool.mjs +1 -1
  95. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -1
  96. package/dist/esm/utils/index.mjs +2 -2
  97. package/dist/types/agents/AgentContext.d.ts +2 -0
  98. package/dist/types/graphs/Graph.d.ts +24 -0
  99. package/dist/types/hitl/askUserQuestion.d.ts +11 -1
  100. package/dist/types/langfuse.d.ts +16 -8
  101. package/dist/types/langfuseConfig.d.ts +6 -0
  102. package/dist/types/langfuseRuntimeContext.d.ts +27 -1
  103. package/dist/types/langfuseRuntimeScope.d.ts +17 -2
  104. package/dist/types/langfuseSpanRegistry.d.ts +17 -0
  105. package/dist/types/langfuseTraceShaping.d.ts +2 -1
  106. package/dist/types/llm/anthropic/utils/message_inputs.d.ts +1 -0
  107. package/dist/types/run.d.ts +7 -0
  108. package/dist/types/session/types.d.ts +1 -0
  109. package/dist/types/tools/ToolNode.d.ts +7 -1
  110. package/dist/types/types/hitl.d.ts +8 -0
  111. package/dist/types/types/tools.d.ts +30 -0
  112. package/package.json +7 -4
  113. package/src/__tests__/stream.eagerArgsDivergence.test.ts +753 -0
  114. package/src/agents/AgentContext.ts +5 -0
  115. package/src/graphs/Graph.ts +108 -20
  116. package/src/graphs/MultiAgentGraph.ts +56 -2
  117. package/src/graphs/__tests__/composition.smoke.test.ts +4 -0
  118. package/src/hitl/askUserQuestion.ts +14 -1
  119. package/src/instrumentation.ts +35 -77
  120. package/src/langfuse.ts +320 -43
  121. package/src/langfuseConfig.ts +24 -0
  122. package/src/langfuseRuntimeContext.ts +43 -1
  123. package/src/langfuseRuntimeScope.ts +94 -21
  124. package/src/langfuseSpanRegistry.ts +131 -0
  125. package/src/langfuseTraceShaping.ts +194 -7
  126. package/src/llm/anthropic/utils/message_inputs.ts +70 -19
  127. package/src/llm/anthropic/utils/streaming-tool-input.test.ts +186 -11
  128. package/src/llm/bedrock/utils/message_inputs.test.ts +120 -4
  129. package/src/llm/bedrock/utils/message_inputs.ts +32 -7
  130. package/src/messages/prune.ts +12 -1
  131. package/src/prompts/activityLabel.ts +23 -6
  132. package/src/run.ts +91 -45
  133. package/src/scripts/activity-labels/captured.json +56 -0
  134. package/src/scripts/activity-labels/checks.cjs +205 -0
  135. package/src/scripts/activity-labels/corpus.cjs +473 -0
  136. package/src/scripts/activity-labels/report.cjs +203 -0
  137. package/src/scripts/activity-labels/rescore.cjs +102 -0
  138. package/src/scripts/activity-labels/run.ts +705 -0
  139. package/src/scripts/activity-labels/variants.ts +71 -0
  140. package/src/session/messageSerialization.ts +12 -1
  141. package/src/session/types.ts +1 -0
  142. package/src/specs/activity-label-prompt.test.ts +26 -10
  143. package/src/specs/agent-handoffs.test.ts +306 -0
  144. package/src/specs/discovered-tools.test.ts +217 -0
  145. package/src/specs/langfuse-callbacks.test.ts +456 -0
  146. package/src/specs/langfuse-routing.integration.test.ts +138 -1
  147. package/src/specs/langfuse-span-registry.test.ts +70 -0
  148. package/src/specs/langfuse-trace-shaping.test.ts +294 -0
  149. package/src/specs/prune.test.ts +38 -1
  150. package/src/stream.ts +70 -6
  151. package/src/summarization/node.ts +5 -0
  152. package/src/tools/ToolNode.ts +400 -9
  153. package/src/tools/__tests__/ToolNode.invalidToolCalls.test.ts +757 -0
  154. package/src/tools/__tests__/hitl.test.ts +58 -0
  155. package/src/types/hitl.ts +8 -0
  156. package/src/types/tools.ts +35 -1
@@ -1708,6 +1708,11 @@ export class AgentContext {
1708
1708
  this.totalTokensFresh = false;
1709
1709
  }
1710
1710
 
1711
+ /** Returns a snapshot of the deferred tools discovered in this context. */
1712
+ getDiscoveredTools(): string[] {
1713
+ return Array.from(this.discoveredToolNames);
1714
+ }
1715
+
1711
1716
  /**
1712
1717
  * Marks tools as discovered via tool search.
1713
1718
  * Discovered tools will be included in the next model binding.
@@ -1,4 +1,5 @@
1
1
  /* eslint-disable no-console */
2
+ import { v4 } from 'uuid';
2
3
  import { nanoid } from 'nanoid';
3
4
  import { tool } from '@langchain/core/tools';
4
5
  import { ToolNode } from '@langchain/langgraph/prebuilt';
@@ -53,6 +54,14 @@ import {
53
54
  appendPredecessorHandoffCue,
54
55
  removePredecessorHandoffCue,
55
56
  } from '@/messages';
57
+ import {
58
+ attemptInvoke,
59
+ tryFallbackProviders,
60
+ getFallbackErrorContext,
61
+ getFallbackOverflowCandidates,
62
+ projectMessagesForProvider,
63
+ resolveServingModelId,
64
+ } from '@/llm/invoke';
56
65
  import {
57
66
  resetIfNotEmpty,
58
67
  isAnthropicLike,
@@ -64,14 +73,14 @@ import {
64
73
  sleep,
65
74
  } from '@/utils';
66
75
  import {
67
- attemptInvoke,
68
- tryFallbackProviders,
69
- getFallbackErrorContext,
70
- getFallbackOverflowCandidates,
71
- projectMessagesForProvider,
72
- resolveServingModelId,
73
- } from '@/llm/invoke';
74
- import { v4 } from 'uuid';
76
+ Constants,
77
+ GraphNodeKeys,
78
+ ContentTypes,
79
+ GraphEvents,
80
+ Providers,
81
+ StepTypes,
82
+ PREEMPT_BOUNDARY_HOOK_TIMEOUT_MS,
83
+ } from '@/common';
75
84
  import {
76
85
  createLangfuseHandler,
77
86
  createLangfuseTraceMetadata,
@@ -83,21 +92,16 @@ import {
83
92
  planContextOverflowRecovery,
84
93
  translateRecoveryBudget,
85
94
  } from '@/llm/contextOverflowRecovery';
95
+ import {
96
+ hasToolOutputTracingConfig,
97
+ resolveLangfuseConfig,
98
+ resolveToolOutputTracingConfig,
99
+ } from '@/langfuseConfig';
86
100
  import {
87
101
  compactToolContent,
88
102
  getToolContentCharLength,
89
103
  serializeToolContentBounded,
90
104
  } from '@/utils/toolContent';
91
- import { resolveMaxSeals } from '@/llm/preempt';
92
- import {
93
- Constants,
94
- GraphNodeKeys,
95
- ContentTypes,
96
- GraphEvents,
97
- Providers,
98
- StepTypes,
99
- PREEMPT_BOUNDARY_HOOK_TIMEOUT_MS,
100
- } from '@/common';
101
105
  import {
102
106
  annotateMessagesForLLM,
103
107
  ToolOutputReferenceRegistry,
@@ -125,12 +129,12 @@ import { shouldTriggerSummarization } from '@/summarization';
125
129
  import { resolveLocalToolsForBinding } from '@/tools/local';
126
130
  import { createSummarizeNode } from '@/summarization/node';
127
131
  import { messagesStateReducer } from '@/messages/reducer';
128
- import { resolveLangfuseConfig } from '@/langfuseConfig';
129
132
  import { createSchemaOnlyTools } from '@/tools/schema';
130
133
  import { AgentContext } from '@/agents/AgentContext';
131
134
  import { createFakeStreamingLLM } from '@/llm/fake';
132
135
  import { handleToolCalls } from '@/tools/handlers';
133
136
  import { isThinkingEnabled } from '@/llm/request';
137
+ import { resolveMaxSeals } from '@/llm/preempt';
134
138
  import { initializeModel } from '@/llm/init';
135
139
  import { HandlerRegistry } from '@/events';
136
140
  import { ChatOpenAI } from '@/llm/openai';
@@ -639,6 +643,10 @@ export abstract class Graph<
639
643
  currentToolMap?: t.ToolMap;
640
644
  }): CustomToolNode<T> | ToolNode<T>;
641
645
  abstract getRunMessages(): BaseMessage[] | undefined;
646
+ /** Returns a snapshot of deferred tools discovered by this graph. */
647
+ getDiscoveredTools(_agentId?: string): string[] {
648
+ return [];
649
+ }
642
650
  abstract getContentParts(): t.MessageContentComplex[] | undefined;
643
651
  abstract generateStepId(stepKey: string): [string, number];
644
652
  abstract getKeyList(
@@ -762,6 +770,15 @@ export abstract class Graph<
762
770
  new Map();
763
771
  eagerEventToolCallChunks: Map<string, t.EagerEventToolCallChunkState> =
764
772
  new Map();
773
+ /**
774
+ * Per-run eager prestart circuit breaker, shared by reference with every
775
+ * ToolNode this graph compiles. When a prestarted execution's args turn
776
+ * out to differ from the final request, ToolNode records the tool name
777
+ * here and the stream handler stops prestarting that tool for the rest of
778
+ * the run — the retry then executes normally instead of re-diverging in a
779
+ * loop (LibreChat#14371).
780
+ */
781
+ eagerEventToolSuppressions: Set<string> = new Set();
765
782
  /**
766
783
  * Run-scoped execution backend for built-in code tools. Defaults to the
767
784
  * remote Code API sandbox when unset.
@@ -811,6 +828,7 @@ export abstract class Graph<
811
828
  this.eagerEventToolExecutions.clear();
812
829
  this.clearEagerEventToolUsageCounts();
813
830
  this.eagerEventToolCallChunks.clear();
831
+ this.eagerEventToolSuppressions.clear();
814
832
  this.toolExecution = undefined;
815
833
  this.handlerDispatchedEventCounts.clear();
816
834
  /**
@@ -992,11 +1010,23 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
992
1010
  messages: BaseMessage[] = [];
993
1011
  /** Cached run messages preserved before clearHeavyState() so getRunMessages() works after cleanup. */
994
1012
  private cachedRunMessages?: BaseMessage[];
1013
+ /** Per-agent discovery snapshots preserved before contexts are reset on cleanup. */
1014
+ private cachedDiscoveredTools?: Map<string, string[]>;
995
1015
  /** Ids of AI turns the agent node returned THIS run; see isRunProducedMessage. */
996
1016
  protected runProducedAiMessageIds = new Set<string>();
997
1017
  /** Checkpoint scope whose messages match index-keyed tool snapshots. */
998
1018
  private originalToolContentCheckpointScope?: string;
999
1019
  runId: string | undefined;
1020
+ /**
1021
+ * Identity used to stamp Langfuse runtime scopes and handlers (see
1022
+ * `LangfuseRuntimeContext.runId`). Carries an opaque per-instance
1023
+ * component: public run ids are unrestricted and may repeat across
1024
+ * concurrently executing runs (retries, duplicate submissions,
1025
+ * tenant-local message ids), and equal stamps would let those runs adopt
1026
+ * each other's scopes. One graph instance = one execution's stamp, shared
1027
+ * by the stream handler and every graph-level scope of that execution.
1028
+ */
1029
+ readonly langfuseScopeRunId: string;
1000
1030
  /**
1001
1031
  * Boundary between historical messages (loaded from conversation state)
1002
1032
  * and messages produced during the current run. Set once in the state
@@ -1081,6 +1111,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1081
1111
  }: t.StandardGraphInput) {
1082
1112
  super();
1083
1113
  this.runId = runId;
1114
+ this.langfuseScopeRunId = `${runId ?? 'graph'}:${nanoid()}`;
1084
1115
  this.signal = signal;
1085
1116
  this.langfuse = langfuse;
1086
1117
  this.subagentUsageSink = subagentUsageSink;
@@ -1112,6 +1143,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1112
1143
  resetValues(keepContent?: boolean, checkpointScope?: string): void {
1113
1144
  this.messages = [];
1114
1145
  this.cachedRunMessages = undefined;
1146
+ this.cachedDiscoveredTools = undefined;
1115
1147
  this.config = resetIfNotEmpty(this.config, undefined);
1116
1148
  if (keepContent !== true) {
1117
1149
  this.contentData = resetIfNotEmpty(this.contentData, []);
@@ -1129,6 +1161,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1129
1161
  this.eagerEventToolExecutions.clear();
1130
1162
  this.clearEagerEventToolUsageCounts();
1131
1163
  this.eagerEventToolCallChunks.clear();
1164
+ this.eagerEventToolSuppressions.clear();
1132
1165
  this.handlerDispatchedStepIds = resetIfNotEmpty(
1133
1166
  this.handlerDispatchedStepIds,
1134
1167
  new Set()
@@ -1177,6 +1210,12 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1177
1210
 
1178
1211
  override clearHeavyState(): void {
1179
1212
  this.cachedRunMessages = this.messages.slice(this.startIndex);
1213
+ this.cachedDiscoveredTools = new Map(
1214
+ Array.from(this.agentContexts, ([agentId, context]) => [
1215
+ agentId,
1216
+ context.getDiscoveredTools(),
1217
+ ])
1218
+ );
1180
1219
  super.clearHeavyState();
1181
1220
  this.messages = [];
1182
1221
  this.overrideModel = undefined;
@@ -1471,6 +1510,32 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1471
1510
  return this.messages.slice(this.startIndex);
1472
1511
  }
1473
1512
 
1513
+ override getDiscoveredTools(agentId?: string): string[] {
1514
+ if (agentId != null) {
1515
+ const current =
1516
+ this.agentContexts.get(agentId)?.getDiscoveredTools() ?? [];
1517
+ if (current.length > 0 || this.cachedDiscoveredTools == null) {
1518
+ return current;
1519
+ }
1520
+ return [...(this.cachedDiscoveredTools.get(agentId) ?? [])];
1521
+ }
1522
+
1523
+ const discoveredTools = new Set<string>();
1524
+ for (const context of this.agentContexts.values()) {
1525
+ for (const toolName of context.getDiscoveredTools()) {
1526
+ discoveredTools.add(toolName);
1527
+ }
1528
+ }
1529
+ if (discoveredTools.size === 0 && this.cachedDiscoveredTools != null) {
1530
+ for (const snapshot of this.cachedDiscoveredTools.values()) {
1531
+ for (const toolName of snapshot) {
1532
+ discoveredTools.add(toolName);
1533
+ }
1534
+ }
1535
+ }
1536
+ return Array.from(discoveredTools);
1537
+ }
1538
+
1474
1539
  /**
1475
1540
  * True when THIS RUN produced `message` — the provenance the handoff cue
1476
1541
  * gate needs. Tracked as an id set rather than inferred from `startIndex`
@@ -1657,6 +1722,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1657
1722
  eagerEventToolUsageCount: this.getEagerEventToolUsageCount(
1658
1723
  agentContext?.agentId
1659
1724
  ),
1725
+ eagerEventToolSuppressions: this.eagerEventToolSuppressions,
1660
1726
  toolExecution: this.toolExecution,
1661
1727
  directToolNames: directToolNames.size > 0 ? directToolNames : undefined,
1662
1728
  interruptingToolNames:
@@ -3057,6 +3123,11 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3057
3123
  metadata: {
3058
3124
  ...(config.metadata ?? {}),
3059
3125
  ...traceMetadata,
3126
+ /** Canonical agent identity, stamped OUTSIDE trace-metadata
3127
+ * filtering: `createLangfuseTraceMetadata` drops values over its
3128
+ * length cap, but scope trust (`isForeignScope`) needs the id
3129
+ * verbatim regardless of length. */
3130
+ agentId,
3060
3131
  },
3061
3132
  };
3062
3133
  initializeLangfuseTracing(langfuse);
@@ -3069,6 +3140,16 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3069
3140
  tags: ['librechat', 'agent'],
3070
3141
  traceIdSeed:
3071
3142
  langfuse?.deterministicTraceId === true ? this.runId : undefined,
3143
+ runId: this.langfuseScopeRunId,
3144
+ toolOutputTracing: hasToolOutputTracingConfig(
3145
+ this.langfuse,
3146
+ agentContext.langfuse
3147
+ )
3148
+ ? resolveToolOutputTracingConfig(
3149
+ this.langfuse,
3150
+ agentContext.langfuse
3151
+ )
3152
+ : undefined,
3072
3153
  });
3073
3154
  if (langfuseHandler != null) {
3074
3155
  invokeConfig = {
@@ -3089,6 +3170,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3089
3170
  resolveLangfuseRuntimeScope({
3090
3171
  runLangfuse: this.langfuse,
3091
3172
  langfuseOverlay: agentContext.langfuse,
3173
+ runId: this.langfuseScopeRunId,
3174
+ agentId,
3092
3175
  }),
3093
3176
  () =>
3094
3177
  attemptInvoke(
@@ -3251,6 +3334,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3251
3334
  resolveLangfuseRuntimeScope({
3252
3335
  runLangfuse: this.langfuse,
3253
3336
  langfuseOverlay: agentContext.langfuse,
3337
+ runId: this.langfuseScopeRunId,
3338
+ agentId,
3254
3339
  }),
3255
3340
  () =>
3256
3341
  tryFallbackProviders({
@@ -3673,7 +3758,10 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3673
3758
  * the same run — `haltRun` is first-write-wins — is left alone.
3674
3759
  */
3675
3760
  const halt = this.hookRegistry.getHaltSignal(runId);
3676
- if (result.preventContinuation === true && halt?.source === 'PreemptBoundary') {
3761
+ if (
3762
+ result.preventContinuation === true &&
3763
+ halt?.source === 'PreemptBoundary'
3764
+ ) {
3677
3765
  this.preemptHaltReason = halt.reason;
3678
3766
  this.hookRegistry.clearHaltSignal(runId);
3679
3767
  }
@@ -80,6 +80,46 @@ function extractLegacyHandoffInstructions(
80
80
  return content.match(HANDOFF_INSTRUCTIONS_PATTERN)?.[1]?.trim() ?? null;
81
81
  }
82
82
 
83
+ /** Whether a tool name marks a handoff transfer (static or conditional). */
84
+ function isTransferToolName(name: unknown): boolean {
85
+ return (
86
+ typeof name === 'string' &&
87
+ (name.startsWith(Constants.LC_TRANSFER_TO_) ||
88
+ name === 'conditional_transfer')
89
+ );
90
+ }
91
+
92
+ /**
93
+ * Drop transfer `tool_use` content blocks from an AI message's array content.
94
+ * Companion to the reception's tool-call filtering: array-content providers
95
+ * (Anthropic) serialize retained blocks verbatim, so a transfer block whose
96
+ * call/result the reception stripped — or a parallel sibling's transfer block,
97
+ * whose result never reaches this recipient's state — would replay as an
98
+ * unmatched `tool_use`. Matched by the gathered ids AND by transfer name
99
+ * (sibling blocks have no collectable id here). String content passes through.
100
+ */
101
+ function filterTransferToolUseBlocks(
102
+ content: AIMessage['content'],
103
+ transferToolCallIds: ReadonlySet<string>
104
+ ): AIMessage['content'] {
105
+ if (!Array.isArray(content)) {
106
+ return content;
107
+ }
108
+ return content.filter((block) => {
109
+ if (
110
+ typeof block !== 'object' ||
111
+ (block as { type?: string } | null)?.type !== 'tool_use'
112
+ ) {
113
+ return true;
114
+ }
115
+ const toolUse = block as { id?: string; name?: string };
116
+ if (toolUse.id != null && transferToolCallIds.has(toolUse.id)) {
117
+ return false;
118
+ }
119
+ return !isTransferToolName(toolUse.name);
120
+ });
121
+ }
122
+
83
123
  function isValidHandoffGroupId(value: unknown): value is number {
84
124
  return typeof value === 'number' && Number.isSafeInteger(value) && value > 0;
85
125
  }
@@ -842,9 +882,23 @@ export class MultiAgentGraph extends StandardGraph {
842
882
  remainingToolCalls.length > 0 ||
843
883
  (typeof aiMsg.content === 'string' && aiMsg.content.trim())
844
884
  ) {
845
- /** Keep the message but without transfer tool calls */
885
+ /**
886
+ * Keep the message but without transfer tool calls — AND
887
+ * without their `tool_use` content blocks. Array-content
888
+ * providers (Anthropic) serialize the retained blocks
889
+ * verbatim, so a transfer block whose call/result this
890
+ * filter just stripped would reach the receiving agent as
891
+ * an unmatched `tool_use` and the provider rejects the
892
+ * request. Filtered by transfer NAME as well as the
893
+ * gathered ids: a parallel sibling's transfer block has no
894
+ * result in THIS recipient's state, so its id is never
895
+ * collected, but its name still marks it.
896
+ */
846
897
  const filteredAiMsg = new AIMessage({
847
- content: aiMsg.content,
898
+ content: filterTransferToolUseBlocks(
899
+ aiMsg.content,
900
+ transferToolCallIds
901
+ ),
848
902
  tool_calls: remainingToolCalls,
849
903
  id: aiMsg.id,
850
904
  });
@@ -135,6 +135,7 @@ describe('LangGraph composition smoke tests', () => {
135
135
  const usageCount = graph.eagerEventToolUsageCount;
136
136
  const scopedUsageCount = graph.getEagerEventToolUsageCount('agent');
137
137
  const chunks = graph.eagerEventToolCallChunks;
138
+ const suppressions = graph.eagerEventToolSuppressions;
138
139
 
139
140
  graph.eagerEventToolExecutions.set(
140
141
  'call_weather',
@@ -143,6 +144,7 @@ describe('LangGraph composition smoke tests', () => {
143
144
  graph.eagerEventToolUsageCount.set('weather', 1);
144
145
  scopedUsageCount.set('weather', 1);
145
146
  graph.eagerEventToolCallChunks.set('0', { argsText: '{"city":"NYC"}' });
147
+ graph.eagerEventToolSuppressions.add('weather');
146
148
 
147
149
  graph.resetValues();
148
150
 
@@ -150,10 +152,12 @@ describe('LangGraph composition smoke tests', () => {
150
152
  expect(graph.eagerEventToolUsageCount).toBe(usageCount);
151
153
  expect(graph.getEagerEventToolUsageCount('agent')).toBe(scopedUsageCount);
152
154
  expect(graph.eagerEventToolCallChunks).toBe(chunks);
155
+ expect(graph.eagerEventToolSuppressions).toBe(suppressions);
153
156
  expect(graph.eagerEventToolExecutions.size).toBe(0);
154
157
  expect(graph.eagerEventToolUsageCount.size).toBe(0);
155
158
  expect(scopedUsageCount.size).toBe(0);
156
159
  expect(graph.eagerEventToolCallChunks.size).toBe(0);
160
+ expect(graph.eagerEventToolSuppressions.size).toBe(0);
157
161
  });
158
162
 
159
163
  it('compiles and invokes the standard single-agent graph', async () => {
@@ -60,11 +60,24 @@ import type {
60
60
  * ```
61
61
  */
62
62
  export function askUserQuestion(
63
- question: AskUserQuestionRequest
63
+ question: AskUserQuestionRequest,
64
+ options?: {
65
+ /**
66
+ * The calling tool's `tool_call_id`, surfaced on the interrupt payload
67
+ * as `tool_call_id` so hosts can attribute the question (and answer) to
68
+ * the exact tool-call content part. Tool bodies created with
69
+ * `tool(fn, …)` receive it as `config.toolCall.id` — LangChain stamps
70
+ * the full ToolCall onto the config when a tool is invoked with one.
71
+ * Optional: positional hosts and custom nodes can omit it.
72
+ */
73
+ toolCallId?: string;
74
+ }
64
75
  ): AskUserQuestionResolution {
65
76
  const payload: AskUserQuestionInterruptPayload = {
66
77
  type: 'ask_user_question',
67
78
  question,
79
+ ...(options?.toolCallId != null &&
80
+ options.toolCallId !== '' && { tool_call_id: options.toolCallId }),
68
81
  };
69
82
  return interrupt<AskUserQuestionInterruptPayload, AskUserQuestionResolution>(
70
83
  payload
@@ -1,4 +1,4 @@
1
- import { createHash, randomBytes } from 'node:crypto';
1
+ import { randomBytes } from 'node:crypto';
2
2
  import { setLangfuseTracerProvider } from '@langfuse/tracing';
3
3
  import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';
4
4
  import { context, ROOT_CONTEXT, createContextKey } from '@opentelemetry/api';
@@ -13,16 +13,16 @@ import type { LangfuseSpanProcessorParams } from '@langfuse/otel';
13
13
  import type { Context } from '@opentelemetry/api';
14
14
  import type * as t from '@/types';
15
15
  import {
16
- createLibreChatTraceAttributes,
17
- hasLangfuseConfigCredentials,
18
- hasLangfuseEnvCredentials,
19
- hasLangfuseEnvConfig,
20
- } from '@/langfuse';
16
+ getLangfuseDestinationKey,
17
+ getLangfuseSpanProcessorParams,
18
+ registerLangfuseManagedSpan,
19
+ } from '@/langfuseSpanRegistry';
21
20
  import {
22
21
  resolveLangfuseConfigForSpan,
23
22
  resolveTraceIdSeedForSpan,
24
23
  } from '@/langfuseRuntimeScope';
25
24
  import { createLangfuseSpanProcessor } from '@/langfuseToolOutputTracing';
25
+ import { createLibreChatTraceAttributes } from '@/langfuse';
26
26
  import { traceIdFromSeed } from '@/langfuseRuntimeContext';
27
27
  import { isPresent } from '@/utils/misc';
28
28
 
@@ -75,75 +75,15 @@ export function ensureOpenTelemetryContextManager(): void {
75
75
  }
76
76
  }
77
77
 
78
- function resolveLangfuseEnvironment(
79
- langfuse?: t.LangfuseConfig
80
- ): string | undefined {
81
- const candidates = [
82
- langfuse?.environment,
83
- process.env.LANGFUSE_TRACING_ENVIRONMENT,
84
- process.env.NODE_ENV,
85
- ];
86
- for (const candidate of candidates) {
87
- if (candidate != null && candidate.trim() !== '') {
88
- return candidate.trim();
89
- }
90
- }
91
- return undefined;
92
- }
93
-
94
- function getLangfuseSpanProcessorParams(
95
- langfuse?: t.LangfuseConfig
96
- ): LangfuseSpanProcessorParams | undefined {
97
- if (langfuse?.enabled === false) {
98
- return undefined;
99
- }
100
- const environment = resolveLangfuseEnvironment(langfuse);
101
- if (hasLangfuseConfigCredentials(langfuse)) {
102
- return {
103
- publicKey: langfuse.publicKey,
104
- secretKey: langfuse.secretKey,
105
- ...(isPresent(langfuse.baseUrl) ? { baseUrl: langfuse.baseUrl } : {}),
106
- ...(isPresent(environment) ? { environment } : {}),
107
- };
108
- }
109
- if (hasLangfuseEnvConfig()) {
110
- const baseUrl =
111
- langfuse?.baseUrl ??
112
- process.env.LANGFUSE_BASE_URL ??
113
- process.env.LANGFUSE_BASEURL;
114
- return {
115
- publicKey: process.env.LANGFUSE_PUBLIC_KEY as string,
116
- secretKey: process.env.LANGFUSE_SECRET_KEY as string,
117
- ...(isPresent(baseUrl) ? { baseUrl } : {}),
118
- ...(isPresent(environment) ? { environment } : {}),
119
- };
120
- }
121
- if (isPresent(langfuse?.baseUrl) && hasLangfuseEnvCredentials()) {
122
- return {
123
- publicKey: process.env.LANGFUSE_PUBLIC_KEY as string,
124
- secretKey: process.env.LANGFUSE_SECRET_KEY as string,
125
- baseUrl: langfuse.baseUrl,
126
- ...(isPresent(environment) ? { environment } : {}),
127
- };
128
- }
129
- return undefined;
130
- }
131
-
132
- function hashCacheKeyValue(value: string | undefined): string | undefined {
133
- return isPresent(value)
134
- ? createHash('sha256').update(value, 'utf8').digest('hex')
135
- : undefined;
136
- }
137
-
138
- function getLangfuseTracerProviderKey(
139
- params: LangfuseSpanProcessorParams,
78
+ /** Cache key for processor instances: the destination plus processor-level
79
+ * policy (`toolOutputTracing` is baked into each processor's redaction
80
+ * behavior), unlike the pure destination identity used for span parenting. */
81
+ function getLangfuseProcessorCacheKey(
82
+ destinationKey: string,
140
83
  langfuse?: t.LangfuseConfig
141
84
  ): string {
142
85
  return JSON.stringify({
143
- publicKey: params.publicKey,
144
- secretKeyHash: hashCacheKeyValue(params.secretKey),
145
- baseUrl: params.baseUrl,
146
- environment: params.environment,
86
+ destinationKey,
147
87
  toolOutputTracing: langfuse?.toolOutputTracing,
148
88
  });
149
89
  }
@@ -160,25 +100,43 @@ class RoutingLangfuseSpanProcessor implements SpanProcessor {
160
100
  if (params == null) {
161
101
  return undefined;
162
102
  }
103
+ return this.ensureProcessorForKey(
104
+ getLangfuseProcessorCacheKey(getLangfuseDestinationKey(params), langfuse),
105
+ params,
106
+ langfuse
107
+ );
108
+ }
163
109
 
164
- const processorKey = getLangfuseTracerProviderKey(params, langfuse);
165
- const existing = this.processors.get(processorKey);
110
+ private ensureProcessorForKey(
111
+ processorCacheKey: string,
112
+ params: LangfuseSpanProcessorParams,
113
+ langfuse?: t.LangfuseConfig
114
+ ): SpanProcessor {
115
+ const existing = this.processors.get(processorCacheKey);
166
116
  if (existing != null) {
167
117
  return existing;
168
118
  }
169
119
 
170
120
  const processor = createLangfuseSpanProcessor(params, langfuse);
171
- this.processors.set(processorKey, processor);
121
+ this.processors.set(processorCacheKey, processor);
172
122
  return processor;
173
123
  }
174
124
 
175
125
  onStart(span: Span, parentContext: Context): void {
176
126
  const langfuse = resolveLangfuseConfigForSpan(parentContext);
177
- const processor = this.ensureProcessor(langfuse);
178
- if (processor == null) {
127
+ const params = getLangfuseSpanProcessorParams(langfuse);
128
+ if (params == null) {
179
129
  return;
180
130
  }
181
131
 
132
+ const destinationKey = getLangfuseDestinationKey(params);
133
+ const processor = this.ensureProcessorForKey(
134
+ getLangfuseProcessorCacheKey(destinationKey, langfuse),
135
+ params,
136
+ langfuse
137
+ );
138
+ registerLangfuseManagedSpan(span, destinationKey);
139
+
182
140
  const librechatTraceAttributes = createLibreChatTraceAttributes(
183
141
  langfuse?.librechatTraceAttributes ?? {}
184
142
  );