@librechat/agents 3.3.7 → 3.3.9

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 (168) hide show
  1. package/dist/cjs/graphs/Graph.cjs +47 -13
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/graphs/MultiAgentGraph.cjs +56 -6
  4. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  5. package/dist/cjs/hitl/askUserQuestion.cjs +3 -2
  6. package/dist/cjs/hitl/askUserQuestion.cjs.map +1 -1
  7. package/dist/cjs/instrumentation.cjs +18 -48
  8. package/dist/cjs/instrumentation.cjs.map +1 -1
  9. package/dist/cjs/langfuse.cjs +174 -29
  10. package/dist/cjs/langfuse.cjs.map +1 -1
  11. package/dist/cjs/langfuseConfig.cjs +12 -0
  12. package/dist/cjs/langfuseConfig.cjs.map +1 -1
  13. package/dist/cjs/langfuseRuntimeContext.cjs +23 -2
  14. package/dist/cjs/langfuseRuntimeContext.cjs.map +1 -1
  15. package/dist/cjs/langfuseRuntimeScope.cjs +39 -8
  16. package/dist/cjs/langfuseRuntimeScope.cjs.map +1 -1
  17. package/dist/cjs/langfuseSpanRegistry.cjs +95 -0
  18. package/dist/cjs/langfuseSpanRegistry.cjs.map +1 -0
  19. package/dist/cjs/langfuseTraceShaping.cjs +121 -4
  20. package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
  21. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +39 -15
  22. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  23. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +25 -5
  24. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  25. package/dist/cjs/llm/init.cjs +3 -3
  26. package/dist/cjs/llm/invoke.cjs +5 -5
  27. package/dist/cjs/llm/openai/index.cjs +1 -1
  28. package/dist/cjs/main.cjs +10 -10
  29. package/dist/cjs/messages/format.cjs +124 -15
  30. package/dist/cjs/messages/format.cjs.map +1 -1
  31. package/dist/cjs/messages/injected.cjs +10 -1
  32. package/dist/cjs/messages/injected.cjs.map +1 -1
  33. package/dist/cjs/messages/prune.cjs +13 -1
  34. package/dist/cjs/messages/prune.cjs.map +1 -1
  35. package/dist/cjs/prompts/activityLabel.cjs +51 -11
  36. package/dist/cjs/prompts/activityLabel.cjs.map +1 -1
  37. package/dist/cjs/run.cjs +54 -24
  38. package/dist/cjs/run.cjs.map +1 -1
  39. package/dist/cjs/session/messageSerialization.cjs +6 -0
  40. package/dist/cjs/session/messageSerialization.cjs.map +1 -1
  41. package/dist/cjs/stream.cjs +21 -10
  42. package/dist/cjs/stream.cjs.map +1 -1
  43. package/dist/cjs/summarization/node.cjs +60 -0
  44. package/dist/cjs/summarization/node.cjs.map +1 -1
  45. package/dist/cjs/tools/ToolNode.cjs +253 -24
  46. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  47. package/dist/cjs/tools/handlers.cjs +1 -1
  48. package/dist/cjs/tools/search/tool.cjs +1 -1
  49. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -1
  50. package/dist/cjs/utils/index.cjs +2 -2
  51. package/dist/esm/graphs/Graph.mjs +48 -14
  52. package/dist/esm/graphs/Graph.mjs.map +1 -1
  53. package/dist/esm/graphs/MultiAgentGraph.mjs +56 -6
  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/format.mjs +124 -15
  80. package/dist/esm/messages/format.mjs.map +1 -1
  81. package/dist/esm/messages/injected.mjs +10 -1
  82. package/dist/esm/messages/injected.mjs.map +1 -1
  83. package/dist/esm/messages/prune.mjs +13 -1
  84. package/dist/esm/messages/prune.mjs.map +1 -1
  85. package/dist/esm/prompts/activityLabel.mjs +51 -11
  86. package/dist/esm/prompts/activityLabel.mjs.map +1 -1
  87. package/dist/esm/run.mjs +54 -24
  88. package/dist/esm/run.mjs.map +1 -1
  89. package/dist/esm/session/messageSerialization.mjs +6 -0
  90. package/dist/esm/session/messageSerialization.mjs.map +1 -1
  91. package/dist/esm/stream.mjs +21 -10
  92. package/dist/esm/stream.mjs.map +1 -1
  93. package/dist/esm/summarization/node.mjs +60 -0
  94. package/dist/esm/summarization/node.mjs.map +1 -1
  95. package/dist/esm/tools/ToolNode.mjs +254 -25
  96. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  97. package/dist/esm/tools/handlers.mjs +1 -1
  98. package/dist/esm/tools/search/tool.mjs +1 -1
  99. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -1
  100. package/dist/esm/utils/index.mjs +2 -2
  101. package/dist/types/graphs/Graph.d.ts +19 -0
  102. package/dist/types/hitl/askUserQuestion.d.ts +11 -1
  103. package/dist/types/langfuse.d.ts +16 -8
  104. package/dist/types/langfuseConfig.d.ts +6 -0
  105. package/dist/types/langfuseRuntimeContext.d.ts +27 -1
  106. package/dist/types/langfuseRuntimeScope.d.ts +17 -2
  107. package/dist/types/langfuseSpanRegistry.d.ts +17 -0
  108. package/dist/types/langfuseTraceShaping.d.ts +2 -1
  109. package/dist/types/llm/anthropic/utils/message_inputs.d.ts +1 -0
  110. package/dist/types/messages/format.d.ts +9 -8
  111. package/dist/types/prompts/activityLabel.d.ts +8 -1
  112. package/dist/types/run.d.ts +1 -1
  113. package/dist/types/session/types.d.ts +1 -0
  114. package/dist/types/tools/ToolNode.d.ts +7 -1
  115. package/dist/types/types/activityLabel.d.ts +8 -0
  116. package/dist/types/types/hitl.d.ts +8 -0
  117. package/dist/types/types/stream.d.ts +19 -0
  118. package/dist/types/types/tools.d.ts +30 -0
  119. package/package.json +7 -4
  120. package/src/__tests__/stream.eagerArgsDivergence.test.ts +753 -0
  121. package/src/graphs/Graph.ts +69 -20
  122. package/src/graphs/MultiAgentGraph.ts +74 -6
  123. package/src/graphs/__tests__/composition.smoke.test.ts +4 -0
  124. package/src/hitl/askUserQuestion.ts +14 -1
  125. package/src/instrumentation.ts +35 -77
  126. package/src/langfuse.ts +320 -43
  127. package/src/langfuseConfig.ts +24 -0
  128. package/src/langfuseRuntimeContext.ts +43 -1
  129. package/src/langfuseRuntimeScope.ts +94 -21
  130. package/src/langfuseSpanRegistry.ts +131 -0
  131. package/src/langfuseTraceShaping.ts +194 -7
  132. package/src/llm/anthropic/utils/message_inputs.ts +70 -19
  133. package/src/llm/anthropic/utils/streaming-tool-input.test.ts +186 -11
  134. package/src/llm/bedrock/utils/message_inputs.test.ts +120 -4
  135. package/src/llm/bedrock/utils/message_inputs.ts +32 -7
  136. package/src/messages/format.ts +222 -50
  137. package/src/messages/formatAgentMessages.test.ts +308 -6
  138. package/src/messages/injected.test.ts +18 -1
  139. package/src/messages/injected.ts +8 -1
  140. package/src/messages/prune.ts +12 -1
  141. package/src/prompts/activityLabel.ts +67 -2
  142. package/src/run.ts +86 -46
  143. package/src/scripts/activity-labels/captured.json +56 -0
  144. package/src/scripts/activity-labels/checks.cjs +205 -0
  145. package/src/scripts/activity-labels/corpus.cjs +473 -0
  146. package/src/scripts/activity-labels/report.cjs +203 -0
  147. package/src/scripts/activity-labels/rescore.cjs +102 -0
  148. package/src/scripts/activity-labels/run.ts +705 -0
  149. package/src/scripts/activity-labels/variants.ts +71 -0
  150. package/src/session/messageSerialization.ts +12 -1
  151. package/src/session/types.ts +1 -0
  152. package/src/specs/activity-label-prompt.test.ts +109 -0
  153. package/src/specs/agent-handoffs.test.ts +306 -0
  154. package/src/specs/langfuse-callbacks.test.ts +456 -0
  155. package/src/specs/langfuse-routing.integration.test.ts +138 -1
  156. package/src/specs/langfuse-span-registry.test.ts +70 -0
  157. package/src/specs/langfuse-trace-shaping.test.ts +294 -0
  158. package/src/specs/prune.test.ts +38 -1
  159. package/src/stream.ts +70 -6
  160. package/src/summarization/__tests__/node.test.ts +188 -0
  161. package/src/summarization/node.ts +72 -0
  162. package/src/tools/ToolNode.ts +400 -9
  163. package/src/tools/__tests__/ToolNode.invalidToolCalls.test.ts +757 -0
  164. package/src/tools/__tests__/hitl.test.ts +58 -0
  165. package/src/types/activityLabel.ts +8 -0
  166. package/src/types/hitl.ts +8 -0
  167. package/src/types/stream.ts +20 -0
  168. package/src/types/tools.ts +35 -1
@@ -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';
@@ -762,6 +766,15 @@ export abstract class Graph<
762
766
  new Map();
763
767
  eagerEventToolCallChunks: Map<string, t.EagerEventToolCallChunkState> =
764
768
  new Map();
769
+ /**
770
+ * Per-run eager prestart circuit breaker, shared by reference with every
771
+ * ToolNode this graph compiles. When a prestarted execution's args turn
772
+ * out to differ from the final request, ToolNode records the tool name
773
+ * here and the stream handler stops prestarting that tool for the rest of
774
+ * the run — the retry then executes normally instead of re-diverging in a
775
+ * loop (LibreChat#14371).
776
+ */
777
+ eagerEventToolSuppressions: Set<string> = new Set();
765
778
  /**
766
779
  * Run-scoped execution backend for built-in code tools. Defaults to the
767
780
  * remote Code API sandbox when unset.
@@ -811,6 +824,7 @@ export abstract class Graph<
811
824
  this.eagerEventToolExecutions.clear();
812
825
  this.clearEagerEventToolUsageCounts();
813
826
  this.eagerEventToolCallChunks.clear();
827
+ this.eagerEventToolSuppressions.clear();
814
828
  this.toolExecution = undefined;
815
829
  this.handlerDispatchedEventCounts.clear();
816
830
  /**
@@ -997,6 +1011,16 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
997
1011
  /** Checkpoint scope whose messages match index-keyed tool snapshots. */
998
1012
  private originalToolContentCheckpointScope?: string;
999
1013
  runId: string | undefined;
1014
+ /**
1015
+ * Identity used to stamp Langfuse runtime scopes and handlers (see
1016
+ * `LangfuseRuntimeContext.runId`). Carries an opaque per-instance
1017
+ * component: public run ids are unrestricted and may repeat across
1018
+ * concurrently executing runs (retries, duplicate submissions,
1019
+ * tenant-local message ids), and equal stamps would let those runs adopt
1020
+ * each other's scopes. One graph instance = one execution's stamp, shared
1021
+ * by the stream handler and every graph-level scope of that execution.
1022
+ */
1023
+ readonly langfuseScopeRunId: string;
1000
1024
  /**
1001
1025
  * Boundary between historical messages (loaded from conversation state)
1002
1026
  * and messages produced during the current run. Set once in the state
@@ -1081,6 +1105,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1081
1105
  }: t.StandardGraphInput) {
1082
1106
  super();
1083
1107
  this.runId = runId;
1108
+ this.langfuseScopeRunId = `${runId ?? 'graph'}:${nanoid()}`;
1084
1109
  this.signal = signal;
1085
1110
  this.langfuse = langfuse;
1086
1111
  this.subagentUsageSink = subagentUsageSink;
@@ -1129,6 +1154,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1129
1154
  this.eagerEventToolExecutions.clear();
1130
1155
  this.clearEagerEventToolUsageCounts();
1131
1156
  this.eagerEventToolCallChunks.clear();
1157
+ this.eagerEventToolSuppressions.clear();
1132
1158
  this.handlerDispatchedStepIds = resetIfNotEmpty(
1133
1159
  this.handlerDispatchedStepIds,
1134
1160
  new Set()
@@ -1657,6 +1683,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1657
1683
  eagerEventToolUsageCount: this.getEagerEventToolUsageCount(
1658
1684
  agentContext?.agentId
1659
1685
  ),
1686
+ eagerEventToolSuppressions: this.eagerEventToolSuppressions,
1660
1687
  toolExecution: this.toolExecution,
1661
1688
  directToolNames: directToolNames.size > 0 ? directToolNames : undefined,
1662
1689
  interruptingToolNames:
@@ -3057,6 +3084,11 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3057
3084
  metadata: {
3058
3085
  ...(config.metadata ?? {}),
3059
3086
  ...traceMetadata,
3087
+ /** Canonical agent identity, stamped OUTSIDE trace-metadata
3088
+ * filtering: `createLangfuseTraceMetadata` drops values over its
3089
+ * length cap, but scope trust (`isForeignScope`) needs the id
3090
+ * verbatim regardless of length. */
3091
+ agentId,
3060
3092
  },
3061
3093
  };
3062
3094
  initializeLangfuseTracing(langfuse);
@@ -3069,6 +3101,16 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3069
3101
  tags: ['librechat', 'agent'],
3070
3102
  traceIdSeed:
3071
3103
  langfuse?.deterministicTraceId === true ? this.runId : undefined,
3104
+ runId: this.langfuseScopeRunId,
3105
+ toolOutputTracing: hasToolOutputTracingConfig(
3106
+ this.langfuse,
3107
+ agentContext.langfuse
3108
+ )
3109
+ ? resolveToolOutputTracingConfig(
3110
+ this.langfuse,
3111
+ agentContext.langfuse
3112
+ )
3113
+ : undefined,
3072
3114
  });
3073
3115
  if (langfuseHandler != null) {
3074
3116
  invokeConfig = {
@@ -3089,6 +3131,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3089
3131
  resolveLangfuseRuntimeScope({
3090
3132
  runLangfuse: this.langfuse,
3091
3133
  langfuseOverlay: agentContext.langfuse,
3134
+ runId: this.langfuseScopeRunId,
3135
+ agentId,
3092
3136
  }),
3093
3137
  () =>
3094
3138
  attemptInvoke(
@@ -3251,6 +3295,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3251
3295
  resolveLangfuseRuntimeScope({
3252
3296
  runLangfuse: this.langfuse,
3253
3297
  langfuseOverlay: agentContext.langfuse,
3298
+ runId: this.langfuseScopeRunId,
3299
+ agentId,
3254
3300
  }),
3255
3301
  () =>
3256
3302
  tryFallbackProviders({
@@ -3673,7 +3719,10 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
3673
3719
  * the same run — `haltRun` is first-write-wins — is left alone.
3674
3720
  */
3675
3721
  const halt = this.hookRegistry.getHaltSignal(runId);
3676
- if (result.preventContinuation === true && halt?.source === 'PreemptBoundary') {
3722
+ if (
3723
+ result.preventContinuation === true &&
3724
+ halt?.source === 'PreemptBoundary'
3725
+ ) {
3677
3726
  this.preemptHaltReason = halt.reason;
3678
3727
  this.hookRegistry.clearHaltSignal(runId);
3679
3728
  }
@@ -27,6 +27,20 @@ import { Constants } from '@/common';
27
27
  const HANDOFF_INSTRUCTIONS_PATTERN = /(?:Instructions?|Context):\s*(.+)/is;
28
28
  const HANDOFF_INSTRUCTIONS_KEY = 'handoff_instructions';
29
29
 
30
+ /**
31
+ * Handoff and fan-in prompts that route work between agents. Built in-run and
32
+ * never persisted as standalone payload entries, so they are marked synthetic:
33
+ * `messagesStateReducer` would otherwise give them a plain UUID and downstream
34
+ * consumers — compaction coverage anchors — could not tell them apart from a
35
+ * message replayed out of the payload.
36
+ */
37
+ function buildRoutingPrompt(content: string): HumanMessage {
38
+ return new HumanMessage({
39
+ content,
40
+ additional_kwargs: { role: 'user', isMeta: true, source: 'routing' },
41
+ });
42
+ }
43
+
30
44
  function getHandoffInstructions(
31
45
  input: Record<string, unknown>,
32
46
  promptKey: string,
@@ -66,6 +80,46 @@ function extractLegacyHandoffInstructions(
66
80
  return content.match(HANDOFF_INSTRUCTIONS_PATTERN)?.[1]?.trim() ?? null;
67
81
  }
68
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
+
69
123
  function isValidHandoffGroupId(value: unknown): value is number {
70
124
  return typeof value === 'number' && Number.isSafeInteger(value) && value > 0;
71
125
  }
@@ -828,9 +882,23 @@ export class MultiAgentGraph extends StandardGraph {
828
882
  remainingToolCalls.length > 0 ||
829
883
  (typeof aiMsg.content === 'string' && aiMsg.content.trim())
830
884
  ) {
831
- /** 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
+ */
832
897
  const filteredAiMsg = new AIMessage({
833
- content: aiMsg.content,
898
+ content: filterTransferToolUseBlocks(
899
+ aiMsg.content,
900
+ transferToolCallIds
901
+ ),
834
902
  tool_calls: remainingToolCalls,
835
903
  id: aiMsg.id,
836
904
  });
@@ -986,12 +1054,12 @@ export class MultiAgentGraph extends StandardGraph {
986
1054
  new AIMessage(
987
1055
  `[Processed tool result and transferring to ${agentId}]`
988
1056
  ),
989
- new HumanMessage(instructions),
1057
+ buildRoutingPrompt(instructions),
990
1058
  ];
991
1059
  } else {
992
1060
  messagesForAgent = [
993
1061
  ...filteredMessages,
994
- new HumanMessage(instructions),
1062
+ buildRoutingPrompt(instructions),
995
1063
  ];
996
1064
  }
997
1065
  }
@@ -1204,7 +1272,7 @@ export class MultiAgentGraph extends StandardGraph {
1204
1272
  effectiveExcludeResults === false
1205
1273
  ) {
1206
1274
  return {
1207
- messages: [new HumanMessage(promptText)],
1275
+ messages: [buildRoutingPrompt(promptText)],
1208
1276
  };
1209
1277
  }
1210
1278
 
@@ -1212,7 +1280,7 @@ export class MultiAgentGraph extends StandardGraph {
1212
1280
  * to pass filtered messages + prompt to the destination agent
1213
1281
  */
1214
1282
  const filteredMessages = state.messages.slice(0, this.startIndex);
1215
- const promptMessage = new HumanMessage(promptText);
1283
+ const promptMessage = buildRoutingPrompt(promptText);
1216
1284
  return {
1217
1285
  messages: [promptMessage],
1218
1286
  agentMessages: messagesStateReducer(filteredMessages, [
@@ -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
  );