@librechat/agents 3.6.6 → 3.6.8

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 (93) hide show
  1. package/dist/cjs/common/constants.cjs +14 -0
  2. package/dist/cjs/common/constants.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +18 -6
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/graphs/MultiAgentGraph.cjs +6 -1
  6. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  7. package/dist/cjs/instrumentation.cjs +2 -0
  8. package/dist/cjs/instrumentation.cjs.map +1 -1
  9. package/dist/cjs/langfuse.cjs +46 -15
  10. package/dist/cjs/langfuse.cjs.map +1 -1
  11. package/dist/cjs/langfuseOperation.cjs +6 -0
  12. package/dist/cjs/langfuseOperation.cjs.map +1 -0
  13. package/dist/cjs/langfuseRuntimeContext.cjs +3 -1
  14. package/dist/cjs/langfuseRuntimeContext.cjs.map +1 -1
  15. package/dist/cjs/langfuseRuntimeScope.cjs +11 -1
  16. package/dist/cjs/langfuseRuntimeScope.cjs.map +1 -1
  17. package/dist/cjs/langfuseSpanRegistry.cjs +35 -0
  18. package/dist/cjs/langfuseSpanRegistry.cjs.map +1 -1
  19. package/dist/cjs/langfuseToolOutputTracing.cjs +1 -6
  20. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
  21. package/dist/cjs/langfuseTraceShaping.cjs +58 -10
  22. package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
  23. package/dist/cjs/llm/openai/index.cjs +8 -0
  24. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  25. package/dist/cjs/main.cjs +8 -1
  26. package/dist/cjs/run.cjs +59 -25
  27. package/dist/cjs/run.cjs.map +1 -1
  28. package/dist/cjs/stream.cjs +1 -1
  29. package/dist/cjs/tools/ToolNode.cjs +2 -2
  30. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +2 -2
  31. package/dist/cjs/utils/tokens.cjs +9 -5
  32. package/dist/cjs/utils/tokens.cjs.map +1 -1
  33. package/dist/esm/common/constants.mjs +8 -1
  34. package/dist/esm/common/constants.mjs.map +1 -1
  35. package/dist/esm/graphs/Graph.mjs +18 -6
  36. package/dist/esm/graphs/Graph.mjs.map +1 -1
  37. package/dist/esm/graphs/MultiAgentGraph.mjs +6 -1
  38. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  39. package/dist/esm/instrumentation.mjs +4 -2
  40. package/dist/esm/instrumentation.mjs.map +1 -1
  41. package/dist/esm/langfuse.mjs +47 -16
  42. package/dist/esm/langfuse.mjs.map +1 -1
  43. package/dist/esm/langfuseOperation.mjs +6 -0
  44. package/dist/esm/langfuseOperation.mjs.map +1 -0
  45. package/dist/esm/langfuseRuntimeContext.mjs +3 -2
  46. package/dist/esm/langfuseRuntimeContext.mjs.map +1 -1
  47. package/dist/esm/langfuseRuntimeScope.mjs +12 -3
  48. package/dist/esm/langfuseRuntimeScope.mjs.map +1 -1
  49. package/dist/esm/langfuseSpanRegistry.mjs +34 -1
  50. package/dist/esm/langfuseSpanRegistry.mjs.map +1 -1
  51. package/dist/esm/langfuseToolOutputTracing.mjs +1 -6
  52. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  53. package/dist/esm/langfuseTraceShaping.mjs +58 -10
  54. package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
  55. package/dist/esm/llm/openai/index.mjs +8 -0
  56. package/dist/esm/llm/openai/index.mjs.map +1 -1
  57. package/dist/esm/main.mjs +3 -3
  58. package/dist/esm/run.mjs +59 -25
  59. package/dist/esm/run.mjs.map +1 -1
  60. package/dist/esm/stream.mjs +1 -1
  61. package/dist/esm/tools/ToolNode.mjs +2 -2
  62. package/dist/esm/tools/subagent/SubagentExecutor.mjs +2 -2
  63. package/dist/esm/utils/tokens.mjs +9 -5
  64. package/dist/esm/utils/tokens.mjs.map +1 -1
  65. package/dist/types/common/constants.d.ts +8 -0
  66. package/dist/types/graphs/Graph.d.ts +8 -3
  67. package/dist/types/graphs/MultiAgentGraph.d.ts +1 -0
  68. package/dist/types/langfuse.d.ts +10 -1
  69. package/dist/types/langfuseOperation.d.ts +2 -0
  70. package/dist/types/langfuseRuntimeContext.d.ts +2 -0
  71. package/dist/types/langfuseRuntimeScope.d.ts +3 -1
  72. package/dist/types/langfuseSpanRegistry.d.ts +11 -1
  73. package/dist/types/langfuseTraceShaping.d.ts +8 -6
  74. package/dist/types/run.d.ts +4 -3
  75. package/dist/types/types/activityLabel.d.ts +4 -3
  76. package/dist/types/types/reasoningLabel.d.ts +1 -1
  77. package/package.json +5 -5
  78. package/src/common/constants.ts +9 -0
  79. package/src/graphs/Graph.ts +125 -103
  80. package/src/graphs/MultiAgentGraph.ts +6 -2
  81. package/src/instrumentation.ts +12 -0
  82. package/src/langfuse.ts +83 -17
  83. package/src/langfuseOperation.ts +2 -0
  84. package/src/langfuseRuntimeContext.ts +6 -0
  85. package/src/langfuseRuntimeScope.ts +31 -1
  86. package/src/langfuseSpanRegistry.ts +78 -2
  87. package/src/langfuseToolOutputTracing.ts +0 -2
  88. package/src/langfuseTraceShaping.ts +120 -26
  89. package/src/llm/openai/index.ts +34 -3
  90. package/src/run.ts +118 -49
  91. package/src/types/activityLabel.ts +4 -3
  92. package/src/types/reasoningLabel.ts +1 -1
  93. package/src/utils/tokens.ts +21 -15
@@ -74,6 +74,17 @@ import {
74
74
  appendPredecessorHandoffCue,
75
75
  removePredecessorHandoffCue,
76
76
  } from '@/messages';
77
+ import {
78
+ Constants,
79
+ GraphNodeKeys,
80
+ ContentTypes,
81
+ GraphEvents,
82
+ Providers,
83
+ StepTypes,
84
+ STANDARD_GRAPH_RUN_NAME,
85
+ AGENT_MODEL_CALL_RUN_NAME,
86
+ PREEMPT_BOUNDARY_HOOK_TIMEOUT_MS,
87
+ } from '@/common';
77
88
  import {
78
89
  resetIfNotEmpty,
79
90
  isAnthropicLike,
@@ -106,15 +117,6 @@ import {
106
117
  isGraphSubagentConfig,
107
118
  normalizeSubagentConfigEntries,
108
119
  } from '@/tools/subagent';
109
- import {
110
- Constants,
111
- GraphNodeKeys,
112
- ContentTypes,
113
- GraphEvents,
114
- Providers,
115
- StepTypes,
116
- PREEMPT_BOUNDARY_HOOK_TIMEOUT_MS,
117
- } from '@/common';
118
120
  import {
119
121
  createLangfuseHandler,
120
122
  createLangfuseTraceMetadata,
@@ -1260,6 +1262,7 @@ export abstract class Graph<
1260
1262
  }
1261
1263
 
1262
1264
  export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1265
+ readonly runName: string = STANDARD_GRAPH_RUN_NAME;
1263
1266
  overrideModel?: t.ChatModel;
1264
1267
  private subagentModelOverride?: t.ChatModel;
1265
1268
  private readonly graphFactory: GraphFactory;
@@ -1286,10 +1289,12 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1286
1289
  * component: public run ids are unrestricted and may repeat across
1287
1290
  * concurrently executing runs (retries, duplicate submissions,
1288
1291
  * tenant-local message ids), and equal stamps would let those runs adopt
1289
- * each other's scopes. One graph instance = one execution's stamp, shared
1290
- * by the stream handler and every graph-level scope of that execution.
1292
+ * each other's scopes. Fresh stream executions rotate this stamp; resumes
1293
+ * retain it so the continuation stays in the interrupted execution.
1291
1294
  */
1292
- readonly langfuseScopeRunId: string;
1295
+ langfuseScopeRunId: string;
1296
+ /** Opaque key for parenting post-processing observations to this run. */
1297
+ langfuseTraceAnchor: object = {};
1293
1298
  /**
1294
1299
  * Boundary between historical messages (loaded from conversation state)
1295
1300
  * and messages produced during the current run. Set once in the state
@@ -1498,6 +1503,12 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1498
1503
  this.defaultAgentId = agents[0].agentId;
1499
1504
  }
1500
1505
 
1506
+ /** Rotates the Langfuse identities that must never cross fresh executions. */
1507
+ startFreshLangfuseExecution(): void {
1508
+ this.langfuseTraceAnchor = {};
1509
+ this.langfuseScopeRunId = `${this.runId ?? 'graph'}:${nanoid()}`;
1510
+ }
1511
+
1501
1512
  /* Init */
1502
1513
 
1503
1514
  resetValues(keepContent?: boolean, checkpointScope?: string): void {
@@ -2913,7 +2924,9 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2913
2924
  });
2914
2925
 
2915
2926
  if (agentContext.systemRunnable) {
2916
- model = agentContext.systemRunnable.pipe(model as Runnable);
2927
+ model = agentContext.systemRunnable
2928
+ .pipe(model as Runnable)
2929
+ .withConfig({ runName: AGENT_MODEL_CALL_RUN_NAME });
2917
2930
  }
2918
2931
 
2919
2932
  if (agentContext.tokenCalculationPromise) {
@@ -4010,6 +4023,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4010
4023
  tags: ['librechat', 'agent'],
4011
4024
  traceIdSeed:
4012
4025
  langfuse?.deterministicTraceId === true ? this.runId : undefined,
4026
+ traceAnchor: this.langfuseTraceAnchor,
4027
+ agentId,
4013
4028
  runId: this.langfuseScopeRunId,
4014
4029
  toolOutputTracing: hasToolOutputTracingConfig(
4015
4030
  this.langfuse,
@@ -4052,6 +4067,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4052
4067
  resolveLangfuseRuntimeScope({
4053
4068
  runLangfuse: this.langfuse,
4054
4069
  langfuseOverlay: agentContext.langfuse,
4070
+ traceAnchor: this.langfuseTraceAnchor,
4055
4071
  runId: this.langfuseScopeRunId,
4056
4072
  agentId,
4057
4073
  }),
@@ -4845,94 +4861,98 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
4845
4861
  createChildGraphByKind: createConfiguredChildGraph,
4846
4862
  createDetachedChildGraphFactory: (
4847
4863
  parentHandlerRegistry
4848
- ): GraphFactory =>
4849
- snapshotChildGraphFactory(parentHandlerRegistry),
4864
+ ): GraphFactory => snapshotChildGraphFactory(parentHandlerRegistry),
4850
4865
  });
4851
4866
  this.registerSubagentExecutor(executor);
4852
4867
 
4853
- const subagentTool = tool(async (rawInput, config) => {
4854
- const input = rawInput as {
4855
- description?: string;
4856
- subagent_type?: string;
4857
- subagent_thread_id?: string;
4858
- run_in_background?: boolean;
4859
- };
4860
- const description =
4861
- typeof input.description === 'string' &&
4862
- input.description.trim().length > 0
4863
- ? input.description
4864
- : DEFAULT_SUBAGENT_DESCRIPTION;
4865
- const subagentType =
4866
- typeof input.subagent_type === 'string' ? input.subagent_type : '';
4867
- const subagentThreadId =
4868
- typeof input.subagent_thread_id === 'string' &&
4869
- input.subagent_thread_id.trim() !== ''
4870
- ? input.subagent_thread_id.trim()
4871
- : undefined;
4872
- const threadId = config.configurable?.thread_id as string | undefined;
4873
- /** Surface the parent call id so child checkpoints, interrupts, and
4874
- * update events remain correlated across replay and resume. */
4875
- const toolRuntime = config as {
4876
- toolCallId?: string;
4877
- toolCall?: { id?: string };
4878
- };
4879
- const toolCall = toolRuntime.toolCall;
4880
- let parentToolCallId: string | undefined;
4881
- if (
4882
- typeof toolRuntime.toolCallId === 'string' &&
4883
- toolRuntime.toolCallId !== ''
4884
- ) {
4885
- parentToolCallId = toolRuntime.toolCallId;
4886
- } else if (typeof toolCall?.id === 'string' && toolCall.id !== '') {
4887
- parentToolCallId = toolCall.id;
4888
- }
4889
- /** The parent tool batch's entry-captured scope (stamped by
4890
- * ToolNode before PreToolUse hooks) — binds this child to the
4891
- * run that dispatched it, not to whatever controller a reset
4892
- * installed while the hooks were awaited. */
4893
- const batchScope = config.configurable?.[
4894
- RUN_BREAKER_SCOPE_CONFIG_KEY
4895
- ] as RunBreakerScope | undefined;
4896
- const executeParams = {
4897
- description,
4898
- subagentType,
4899
- threadId,
4900
- signal: config.signal,
4901
- parentToolCallId,
4902
- breaker: batchScope?.controller,
4903
- /**
4904
- * Forward the parent's `configurable` so host-set fields
4905
- * (`requestBody`, `user`, etc.) propagate into the child
4906
- * workflow. The executor scrubs run-identity fields before
4907
- * forwarding — see `SubagentExecuteParams.parentConfigurable`.
4908
- */
4909
- parentConfigurable: config.configurable as
4910
- | Record<string, unknown>
4911
- | undefined,
4912
- };
4913
- if (input.run_in_background === true) {
4914
- return executor.executeInBackground({
4915
- ...executeParams,
4916
- ...(subagentThreadId == null
4917
- ? {}
4918
- : { subagentThreadId }),
4919
- });
4920
- }
4921
- if (subagentThreadId != null) {
4922
- return JSON.stringify({
4923
- status: 'rejected',
4924
- tool: Constants.SUBAGENT,
4925
- message:
4926
- 'Child-thread continuation requires run_in_background.',
4927
- });
4928
- }
4929
- const result = await executor.execute(executeParams);
4930
- return result.content;
4931
- }, buildSubagentToolParams(executableConfigs, {
4932
- background: this.subagentTasks != null,
4933
- threadContinuation:
4934
- this.subagentTasks?.store.supportsThreadContinuation === true,
4935
- }));
4868
+ const subagentTool = tool(
4869
+ async (rawInput, config) => {
4870
+ const input = rawInput as {
4871
+ description?: string;
4872
+ subagent_type?: string;
4873
+ subagent_thread_id?: string;
4874
+ run_in_background?: boolean;
4875
+ };
4876
+ const description =
4877
+ typeof input.description === 'string' &&
4878
+ input.description.trim().length > 0
4879
+ ? input.description
4880
+ : DEFAULT_SUBAGENT_DESCRIPTION;
4881
+ const subagentType =
4882
+ typeof input.subagent_type === 'string'
4883
+ ? input.subagent_type
4884
+ : '';
4885
+ const subagentThreadId =
4886
+ typeof input.subagent_thread_id === 'string' &&
4887
+ input.subagent_thread_id.trim() !== ''
4888
+ ? input.subagent_thread_id.trim()
4889
+ : undefined;
4890
+ const threadId = config.configurable?.thread_id as
4891
+ | string
4892
+ | undefined;
4893
+ /** Surface the parent call id so child checkpoints, interrupts, and
4894
+ * update events remain correlated across replay and resume. */
4895
+ const toolRuntime = config as {
4896
+ toolCallId?: string;
4897
+ toolCall?: { id?: string };
4898
+ };
4899
+ const toolCall = toolRuntime.toolCall;
4900
+ let parentToolCallId: string | undefined;
4901
+ if (
4902
+ typeof toolRuntime.toolCallId === 'string' &&
4903
+ toolRuntime.toolCallId !== ''
4904
+ ) {
4905
+ parentToolCallId = toolRuntime.toolCallId;
4906
+ } else if (typeof toolCall?.id === 'string' && toolCall.id !== '') {
4907
+ parentToolCallId = toolCall.id;
4908
+ }
4909
+ /** The parent tool batch's entry-captured scope (stamped by
4910
+ * ToolNode before PreToolUse hooks) — binds this child to the
4911
+ * run that dispatched it, not to whatever controller a reset
4912
+ * installed while the hooks were awaited. */
4913
+ const batchScope = config.configurable?.[
4914
+ RUN_BREAKER_SCOPE_CONFIG_KEY
4915
+ ] as RunBreakerScope | undefined;
4916
+ const executeParams = {
4917
+ description,
4918
+ subagentType,
4919
+ threadId,
4920
+ signal: config.signal,
4921
+ parentToolCallId,
4922
+ breaker: batchScope?.controller,
4923
+ /**
4924
+ * Forward the parent's `configurable` so host-set fields
4925
+ * (`requestBody`, `user`, etc.) propagate into the child
4926
+ * workflow. The executor scrubs run-identity fields before
4927
+ * forwarding — see `SubagentExecuteParams.parentConfigurable`.
4928
+ */
4929
+ parentConfigurable: config.configurable as
4930
+ | Record<string, unknown>
4931
+ | undefined,
4932
+ };
4933
+ if (input.run_in_background === true) {
4934
+ return executor.executeInBackground({
4935
+ ...executeParams,
4936
+ ...(subagentThreadId == null ? {} : { subagentThreadId }),
4937
+ });
4938
+ }
4939
+ if (subagentThreadId != null) {
4940
+ return JSON.stringify({
4941
+ status: 'rejected',
4942
+ tool: Constants.SUBAGENT,
4943
+ message:
4944
+ 'Child-thread continuation requires run_in_background.',
4945
+ });
4946
+ }
4947
+ const result = await executor.execute(executeParams);
4948
+ return result.content;
4949
+ },
4950
+ buildSubagentToolParams(executableConfigs, {
4951
+ background: this.subagentTasks != null,
4952
+ threadContinuation:
4953
+ this.subagentTasks?.store.supportsThreadContinuation === true,
4954
+ })
4955
+ );
4936
4956
  const replayableSubagentTool = subagentTool as typeof subagentTool &
4937
4957
  ReplayableSubagentTool;
4938
4958
  replayableSubagentTool[SUBAGENT_REPLAY_CONTROLLER] = {
@@ -5218,7 +5238,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
5218
5238
  .addEdge(summarizeNode, agentNode)
5219
5239
  .addEdge(toolNode, agentContext.toolEnd ? END : agentNode);
5220
5240
 
5221
- return workflow.compile();
5241
+ return workflow.compile({ name: STANDARD_GRAPH_RUN_NAME });
5222
5242
  }
5223
5243
 
5224
5244
  createWorkflow(): t.CompiledStateWorkflow {
@@ -5247,11 +5267,13 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
5247
5267
  >,
5248
5268
  { ends: [END] }
5249
5269
  )
5250
- .addEdge(START, this.defaultAgentId)
5251
- // LangGraph compile() types are overly strict for opt-in options
5252
- .compile(this.compileOptions as unknown as never);
5270
+ .addEdge(START, this.defaultAgentId);
5253
5271
 
5254
- return workflow;
5272
+ // LangGraph compile() types are overly strict for opt-in options
5273
+ return workflow.compile({
5274
+ ...this.compileOptions,
5275
+ name: STANDARD_GRAPH_RUN_NAME,
5276
+ } as unknown as never);
5255
5277
  }
5256
5278
 
5257
5279
  /**
@@ -20,9 +20,9 @@ import type { ToolRuntime } from '@langchain/core/tools';
20
20
  import type { GraphFactoryDependencies } from '@/graphs/graphFactory';
21
21
  import type * as t from '@/types';
22
22
  import { serializeToolContentBounded } from '@/utils/toolContent';
23
+ import { Constants, MULTI_AGENT_GRAPH_RUN_NAME } from '@/common';
23
24
  import { HARD_MAX_TOOL_RESULT_CHARS } from '@/utils/truncation';
24
25
  import { StandardGraph } from './Graph';
25
- import { Constants } from '@/common';
26
26
 
27
27
  /** Pattern to extract instructions from transfer ToolMessage content */
28
28
  const HANDOFF_INSTRUCTIONS_PATTERN = /(?:Instructions?|Context):\s*(.+)/is;
@@ -177,6 +177,7 @@ function withHandoffGroupMetadata(
177
177
  * OR continues its workflow (direct edges), but not both simultaneously.
178
178
  */
179
179
  export class MultiAgentGraph extends StandardGraph {
180
+ override readonly runName = MULTI_AGENT_GRAPH_RUN_NAME;
180
181
  private edges: t.GraphEdge[];
181
182
  private startingNodes: Set<string> = new Set();
182
183
  private directEdges: t.GraphEdge[] = [];
@@ -1438,6 +1439,9 @@ export class MultiAgentGraph extends StandardGraph {
1438
1439
  }
1439
1440
  }
1440
1441
 
1441
- return builder.compile(this.compileOptions as unknown as never);
1442
+ return builder.compile({
1443
+ ...this.compileOptions,
1444
+ name: MULTI_AGENT_GRAPH_RUN_NAME,
1445
+ } as unknown as never);
1442
1446
  }
1443
1447
  }
@@ -16,9 +16,12 @@ import {
16
16
  getLangfuseDestinationKey,
17
17
  getLangfuseSpanProcessorParams,
18
18
  registerLangfuseManagedSpan,
19
+ registerLangfuseTraceAnchorSpan,
19
20
  } from '@/langfuseSpanRegistry';
20
21
  import {
21
22
  resolveLangfuseConfigForSpan,
23
+ resolveLangfuseScopeAgentId,
24
+ resolveLangfuseTraceAnchor,
22
25
  resolveTraceIdSeedForSpan,
23
26
  } from '@/langfuseRuntimeScope';
24
27
  import { createLangfuseSpanProcessor } from '@/langfuseToolOutputTracing';
@@ -137,6 +140,15 @@ class RoutingLangfuseSpanProcessor implements SpanProcessor {
137
140
  langfuse
138
141
  );
139
142
  registerLangfuseManagedSpan(span, destinationKey);
143
+ const traceAnchor = resolveLangfuseTraceAnchor(parentContext);
144
+ if (traceAnchor != null) {
145
+ registerLangfuseTraceAnchorSpan(
146
+ traceAnchor,
147
+ span,
148
+ destinationKey,
149
+ resolveLangfuseScopeAgentId(parentContext)
150
+ );
151
+ }
140
152
 
141
153
  const librechatTraceAttributes = createLibreChatTraceAttributes(
142
154
  langfuse?.librechatTraceAttributes ?? {}
package/src/langfuse.ts CHANGED
@@ -18,7 +18,7 @@ import type {
18
18
  LLMResult,
19
19
  } from '@langchain/core/outputs';
20
20
  import type { PropagateAttributesParams } from '@langfuse/tracing';
21
- import type { Context } from '@opentelemetry/api';
21
+ import type { Context, SpanContext } from '@opentelemetry/api';
22
22
  import type { ResolvedLangfuseToolOutputTracingConfig } from '@/langfuseRuntimeContext';
23
23
  import type * as t from '@/types';
24
24
  import {
@@ -36,6 +36,7 @@ import {
36
36
  } from '@/langfuseConfig';
37
37
  import {
38
38
  getLangfuseManagedSpanDestination,
39
+ registerLangfuseManagedSpan,
39
40
  resolveLangfuseDestinationKey,
40
41
  } from '@/langfuseSpanRegistry';
41
42
  import { isPresent, parseBooleanEnv } from '@/utils/misc';
@@ -66,6 +67,14 @@ type LangfuseHandlerParams = {
66
67
  traceMetadata?: LangfuseTraceMetadata;
67
68
  tags?: string[];
68
69
  traceIdSeed?: string;
70
+ /** Opaque key used by the span processor to capture this run's parents. */
71
+ traceAnchor?: object;
72
+ /** Agent lane this handler owns when ambient callback scope is unavailable. */
73
+ agentId?: string;
74
+ /** Exported observation that auxiliary work should be nested beneath. */
75
+ parentSpanContext?: SpanContext;
76
+ /** Keep an existing parent trace's user, session, name, and metadata. */
77
+ inheritTraceIdentity?: boolean;
69
78
  /** Identity of the run this handler traces; ambient runtime scopes are
70
79
  * only adopted when stamped with the same run (see
71
80
  * `LangfuseRuntimeContext.runId`). */
@@ -251,7 +260,7 @@ function callbackNodeMatchesAgent(node: string, agentId: string): boolean {
251
260
  * Hosts often execute agent code inside their own OpenTelemetry spans (HTTP
252
261
  * server auto-instrumentation on the global provider). Root observations must
253
262
  * not inherit that ambient identity: the foreign parent is never exported to
254
- * Langfuse, which orphans the trace root (root/trace input-output shaping is
263
+ * Langfuse, which orphans the trace root (root-observation input/output shaping is
255
264
  * skipped because the span no longer looks like a root), collapses concurrent
256
265
  * runs inside one request context — an agent run and the previous turn's
257
266
  * title run — into a single merged trace with racing names and unioned tags,
@@ -280,6 +289,9 @@ function detachForeignAmbientSpan(
280
289
  class ScopedLangfuseCallbackHandler extends CallbackHandler {
281
290
  private readonly langfuse?: t.LangfuseConfig;
282
291
  private readonly traceIdSeed?: string;
292
+ private readonly traceAnchor?: object;
293
+ private readonly agentId?: string;
294
+ private readonly parentSpanContext?: SpanContext;
283
295
  private readonly runId?: string;
284
296
  private readonly identity: HandlerIdentity;
285
297
  private readonly toolOutputTracing?: ResolvedLangfuseToolOutputTracingConfig;
@@ -289,22 +301,40 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
289
301
  const {
290
302
  langfuse,
291
303
  traceIdSeed,
304
+ traceAnchor,
305
+ agentId,
306
+ parentSpanContext,
307
+ inheritTraceIdentity,
292
308
  runId,
293
309
  toolOutputTracing,
294
310
  traceName,
295
311
  ...handlerParams
296
312
  } = params ?? {};
297
- super(handlerParams);
313
+ super({
314
+ ...handlerParams,
315
+ ...(inheritTraceIdentity === true
316
+ ? {
317
+ userId: undefined,
318
+ sessionId: undefined,
319
+ traceMetadata: undefined,
320
+ }
321
+ : {}),
322
+ });
298
323
  this.langfuse = langfuse;
299
324
  this.traceIdSeed = traceIdSeed;
325
+ this.traceAnchor = traceAnchor;
326
+ this.agentId = agentId;
327
+ this.parentSpanContext = parentSpanContext;
300
328
  this.runId = runId;
301
329
  this.toolOutputTracing = toolOutputTracing;
302
330
  this.identity = {
303
- userId: handlerParams.userId,
304
- sessionId: handlerParams.sessionId,
331
+ userId: inheritTraceIdentity === true ? undefined : handlerParams.userId,
332
+ sessionId:
333
+ inheritTraceIdentity === true ? undefined : handlerParams.sessionId,
305
334
  tags: handlerParams.tags,
306
- metadata: handlerParams.traceMetadata,
307
- traceName,
335
+ metadata:
336
+ inheritTraceIdentity === true ? undefined : handlerParams.traceMetadata,
337
+ traceName: inheritTraceIdentity === true ? undefined : traceName,
308
338
  };
309
339
  }
310
340
 
@@ -314,6 +344,22 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
314
344
  : undefined;
315
345
  }
316
346
 
347
+ private applyExplicitParent(
348
+ activeContext: Context,
349
+ langfuse?: t.LangfuseConfig
350
+ ): Context {
351
+ if (this.parentSpanContext == null) {
352
+ return activeContext;
353
+ }
354
+ const destinationKey = resolveLangfuseDestinationKey(langfuse);
355
+ if (destinationKey == null) {
356
+ return activeContext;
357
+ }
358
+ const parentSpan = otelTrace.wrapSpanContext(this.parentSpanContext);
359
+ registerLangfuseManagedSpan(parentSpan, destinationKey);
360
+ return otelTrace.setSpan(activeContext, parentSpan);
361
+ }
362
+
317
363
  /**
318
364
  * Mirrors the base handler's `runMap`: a start callback whose `parentRunId`
319
365
  * this handler never observed gets no explicit parent span and falls back
@@ -401,12 +447,15 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
401
447
  }
402
448
  const langfuse =
403
449
  resolveLangfuseConfigForSpan(currentContext) ?? this.langfuse;
450
+ const parentedContext = isDetachedRun
451
+ ? this.applyExplicitParent(currentContext, langfuse)
452
+ : currentContext;
404
453
  const activeContext = isDetachedRun
405
454
  ? detachForeignAmbientSpan(
406
- currentContext,
455
+ parentedContext,
407
456
  resolveLangfuseDestinationKey(langfuse)
408
457
  )
409
- : currentContext;
458
+ : parentedContext;
410
459
  const scoped = (): T =>
411
460
  withLangfuseRuntimeScope(
412
461
  {
@@ -414,7 +463,9 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
414
463
  traceIdSeed:
415
464
  resolveTraceIdSeedForSpan(activeContext) ??
416
465
  this.getDeterministicTraceSeed(),
466
+ traceAnchor: this.traceAnchor,
417
467
  runId: scopeRunId ?? this.runId,
468
+ agentId: scopeAgentId ?? this.agentId,
418
469
  },
419
470
  action
420
471
  );
@@ -438,12 +489,15 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
438
489
  for (const key of Object.values(LangfuseOtelContextKeys)) {
439
490
  cleanContext = cleanContext.deleteValue(key);
440
491
  }
492
+ cleanContext = this.applyExplicitParent(cleanContext, this.langfuse);
441
493
  const scoped = (): T =>
442
494
  withLangfuseRuntimeScope(
443
495
  {
444
496
  langfuse: this.langfuse,
445
497
  traceIdSeed: this.getDeterministicTraceSeed(),
498
+ traceAnchor: this.traceAnchor,
446
499
  runId: this.runId,
500
+ agentId: this.agentId,
447
501
  toolOutputTracing:
448
502
  this.toolOutputTracing ??
449
503
  resolveToolOutputTracingConfig(this.langfuse),
@@ -733,6 +787,10 @@ export function createLangfuseHandler({
733
787
  traceMetadata,
734
788
  tags,
735
789
  traceIdSeed,
790
+ traceAnchor,
791
+ agentId,
792
+ parentSpanContext,
793
+ inheritTraceIdentity,
736
794
  runId,
737
795
  toolOutputTracing,
738
796
  traceName,
@@ -743,13 +801,17 @@ export function createLangfuseHandler({
743
801
  return new ScopedLangfuseCallbackHandler({
744
802
  userId,
745
803
  sessionId,
746
- traceMetadata: mergeLangfuseTraceMetadata(
747
- traceMetadata,
748
- langfuse?.metadata
749
- ),
804
+ traceMetadata:
805
+ inheritTraceIdentity === true
806
+ ? undefined
807
+ : mergeLangfuseTraceMetadata(traceMetadata, langfuse?.metadata),
750
808
  tags: mergeLangfuseTags(tags, langfuse?.tags),
751
809
  langfuse,
752
810
  traceIdSeed,
811
+ traceAnchor,
812
+ agentId,
813
+ parentSpanContext,
814
+ inheritTraceIdentity,
753
815
  runId,
754
816
  toolOutputTracing,
755
817
  traceName,
@@ -763,13 +825,17 @@ function createPropagateAttributeParams({
763
825
  traceMetadata,
764
826
  traceName,
765
827
  tags,
828
+ inheritTraceIdentity,
766
829
  }: LangfuseAttributeParams): PropagateAttributesParams {
767
830
  return {
768
- userId,
769
- sessionId,
770
- traceName,
831
+ userId: inheritTraceIdentity === true ? undefined : userId,
832
+ sessionId: inheritTraceIdentity === true ? undefined : sessionId,
833
+ traceName: inheritTraceIdentity === true ? undefined : traceName,
771
834
  tags: mergeLangfuseTags(tags, langfuse?.tags),
772
- metadata: mergeLangfuseTraceMetadata(traceMetadata, langfuse?.metadata),
835
+ metadata:
836
+ inheritTraceIdentity === true
837
+ ? undefined
838
+ : mergeLangfuseTraceMetadata(traceMetadata, langfuse?.metadata),
773
839
  };
774
840
  }
775
841
 
@@ -0,0 +1,2 @@
1
+ /** Internal observation metadata key used to distinguish SDK operations from public tags. */
2
+ export const LANGFUSE_OPERATION_METADATA_KEY = 'librechat.sdk.operation';
@@ -12,6 +12,8 @@ export type ResolvedLangfuseToolOutputTracingConfig = {
12
12
  export type LangfuseRuntimeContext = {
13
13
  langfuse?: t.LangfuseConfig;
14
14
  traceIdSeed?: string;
15
+ /** Opaque run-owned key used to recover the exported trace parent. */
16
+ traceAnchor?: object;
15
17
  toolOutputTracing?: ResolvedLangfuseToolOutputTracingConfig;
16
18
  /**
17
19
  * Identity of the run this scope belongs to. LangChain executes
@@ -44,6 +46,7 @@ export function hasLangfuseRuntimeContextValue(
44
46
  return (
45
47
  context.langfuse != null ||
46
48
  hasText(context.traceIdSeed) ||
49
+ context.traceAnchor != null ||
47
50
  hasText(context.runId) ||
48
51
  hasText(context.agentId) ||
49
52
  context.toolOutputTracing != null
@@ -100,6 +103,9 @@ export function runWithLangfuseRuntimeContext<T>(
100
103
  ...(hasText(context.traceIdSeed)
101
104
  ? { traceIdSeed: context.traceIdSeed }
102
105
  : {}),
106
+ ...(context.traceAnchor != null
107
+ ? { traceAnchor: context.traceAnchor }
108
+ : {}),
103
109
  ...(hasText(context.runId) ? { runId: context.runId } : {}),
104
110
  ...(hasText(context.agentId) ? { agentId: context.agentId } : {}),
105
111
  ...(context.toolOutputTracing !== undefined