@librechat/agents 3.6.7 → 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 (88) 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/esm/common/constants.mjs +8 -1
  32. package/dist/esm/common/constants.mjs.map +1 -1
  33. package/dist/esm/graphs/Graph.mjs +18 -6
  34. package/dist/esm/graphs/Graph.mjs.map +1 -1
  35. package/dist/esm/graphs/MultiAgentGraph.mjs +6 -1
  36. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  37. package/dist/esm/instrumentation.mjs +4 -2
  38. package/dist/esm/instrumentation.mjs.map +1 -1
  39. package/dist/esm/langfuse.mjs +47 -16
  40. package/dist/esm/langfuse.mjs.map +1 -1
  41. package/dist/esm/langfuseOperation.mjs +6 -0
  42. package/dist/esm/langfuseOperation.mjs.map +1 -0
  43. package/dist/esm/langfuseRuntimeContext.mjs +3 -2
  44. package/dist/esm/langfuseRuntimeContext.mjs.map +1 -1
  45. package/dist/esm/langfuseRuntimeScope.mjs +12 -3
  46. package/dist/esm/langfuseRuntimeScope.mjs.map +1 -1
  47. package/dist/esm/langfuseSpanRegistry.mjs +34 -1
  48. package/dist/esm/langfuseSpanRegistry.mjs.map +1 -1
  49. package/dist/esm/langfuseToolOutputTracing.mjs +1 -6
  50. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  51. package/dist/esm/langfuseTraceShaping.mjs +58 -10
  52. package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
  53. package/dist/esm/llm/openai/index.mjs +8 -0
  54. package/dist/esm/llm/openai/index.mjs.map +1 -1
  55. package/dist/esm/main.mjs +3 -3
  56. package/dist/esm/run.mjs +59 -25
  57. package/dist/esm/run.mjs.map +1 -1
  58. package/dist/esm/stream.mjs +1 -1
  59. package/dist/esm/tools/ToolNode.mjs +2 -2
  60. package/dist/esm/tools/subagent/SubagentExecutor.mjs +2 -2
  61. package/dist/types/common/constants.d.ts +8 -0
  62. package/dist/types/graphs/Graph.d.ts +8 -3
  63. package/dist/types/graphs/MultiAgentGraph.d.ts +1 -0
  64. package/dist/types/langfuse.d.ts +10 -1
  65. package/dist/types/langfuseOperation.d.ts +2 -0
  66. package/dist/types/langfuseRuntimeContext.d.ts +2 -0
  67. package/dist/types/langfuseRuntimeScope.d.ts +3 -1
  68. package/dist/types/langfuseSpanRegistry.d.ts +11 -1
  69. package/dist/types/langfuseTraceShaping.d.ts +8 -6
  70. package/dist/types/run.d.ts +4 -3
  71. package/dist/types/types/activityLabel.d.ts +4 -3
  72. package/dist/types/types/reasoningLabel.d.ts +1 -1
  73. package/package.json +5 -5
  74. package/src/common/constants.ts +9 -0
  75. package/src/graphs/Graph.ts +125 -103
  76. package/src/graphs/MultiAgentGraph.ts +6 -2
  77. package/src/instrumentation.ts +12 -0
  78. package/src/langfuse.ts +83 -17
  79. package/src/langfuseOperation.ts +2 -0
  80. package/src/langfuseRuntimeContext.ts +6 -0
  81. package/src/langfuseRuntimeScope.ts +31 -1
  82. package/src/langfuseSpanRegistry.ts +78 -2
  83. package/src/langfuseToolOutputTracing.ts +0 -2
  84. package/src/langfuseTraceShaping.ts +120 -26
  85. package/src/llm/openai/index.ts +34 -3
  86. package/src/run.ts +118 -49
  87. package/src/types/activityLabel.ts +4 -3
  88. package/src/types/reasoningLabel.ts +1 -1
@@ -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
@@ -7,6 +7,7 @@ import type {
7
7
  import type * as t from '@/types';
8
8
  import {
9
9
  getLangfuseRuntimeConfig,
10
+ getLangfuseRuntimeContext,
10
11
  getLangfuseRuntimeToolOutputTracingConfig,
11
12
  getLangfuseScopeAgentId,
12
13
  getLangfuseScopeRunId,
@@ -27,6 +28,7 @@ export type ResolveLangfuseRuntimeScopeParams = {
27
28
  runLangfuse?: t.LangfuseConfig;
28
29
  langfuseOverlay?: t.LangfuseConfig;
29
30
  traceIdSeed?: string;
31
+ traceAnchor?: object;
30
32
  runId?: string;
31
33
  agentId?: string;
32
34
  };
@@ -46,6 +48,9 @@ const langfuseConfigKey = createContextKey('librechat.langfuse.config');
46
48
  const langfuseTraceIdSeedKey = createContextKey(
47
49
  'librechat.langfuse.trace-id-seed'
48
50
  );
51
+ const langfuseTraceAnchorKey = createContextKey(
52
+ 'librechat.langfuse.trace-anchor'
53
+ );
49
54
  const langfuseScopeRunIdKey = createContextKey('librechat.langfuse.run-id');
50
55
  const langfuseScopeAgentIdKey = createContextKey('librechat.langfuse.agent-id');
51
56
 
@@ -69,6 +74,17 @@ export function getOtelTraceIdSeed(activeContext: Context): string | undefined {
69
74
  return typeof value === 'string' && value.trim() !== '' ? value : undefined;
70
75
  }
71
76
 
77
+ export function resolveLangfuseTraceAnchor(
78
+ activeContext: Context
79
+ ): object | undefined {
80
+ const runtimeAnchor = getLangfuseRuntimeContext()?.traceAnchor;
81
+ if (runtimeAnchor != null) {
82
+ return runtimeAnchor;
83
+ }
84
+ const value = activeContext.getValue(langfuseTraceAnchorKey);
85
+ return typeof value === 'object' && value != null ? value : undefined;
86
+ }
87
+
72
88
  export function getOtelToolOutputTracingConfig(
73
89
  activeContext: Context
74
90
  ): ResolvedLangfuseToolOutputTracingConfig | undefined {
@@ -166,6 +182,12 @@ export function withLangfuseRuntimeScope<T>(
166
182
  hasText(scope.traceIdSeed) ? scope.traceIdSeed : undefined,
167
183
  replace
168
184
  );
185
+ activeContext = setOrClearContextValue(
186
+ activeContext,
187
+ langfuseTraceAnchorKey,
188
+ scope.traceAnchor,
189
+ replace
190
+ );
169
191
  activeContext = setOrClearContextValue(
170
192
  activeContext,
171
193
  langfuseScopeRunIdKey,
@@ -194,6 +216,7 @@ export function resolveLangfuseRuntimeScope({
194
216
  runLangfuse,
195
217
  langfuseOverlay,
196
218
  traceIdSeed,
219
+ traceAnchor,
197
220
  runId,
198
221
  agentId,
199
222
  }: ResolveLangfuseRuntimeScopeParams): LangfuseRuntimeScope {
@@ -204,5 +227,12 @@ export function resolveLangfuseRuntimeScope({
204
227
  )
205
228
  ? undefined
206
229
  : resolveToolOutputTracingConfig(runLangfuse, langfuseOverlay);
207
- return { langfuse, traceIdSeed, toolOutputTracing, runId, agentId };
230
+ return {
231
+ langfuse,
232
+ traceIdSeed,
233
+ traceAnchor,
234
+ toolOutputTracing,
235
+ runId,
236
+ agentId,
237
+ };
208
238
  }
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { context, trace } from '@opentelemetry/api';
2
3
  import type { LangfuseSpanProcessorParams } from '@langfuse/otel';
3
- import type { Span } from '@opentelemetry/api';
4
+ import type { Span, SpanContext } from '@opentelemetry/api';
4
5
  import type * as t from '@/types';
5
6
  import {
6
7
  hasLangfuseConfigCredentials,
@@ -16,7 +17,7 @@ import { isPresent } from '@/utils/misc';
16
17
  *
17
18
  * - Foreign spans (a host's own OpenTelemetry instrumentation, e.g. HTTP
18
19
  * server spans on the global provider) are never exported to Langfuse —
19
- * inheriting one orphans the trace root (its root/trace input-output
20
+ * inheriting one orphans the trace root (its root-observation input/output
20
21
  * shaping is skipped), collapses concurrent runs inside one request
21
22
  * context into a single merged trace, and bypasses the seeded
22
23
  * deterministic trace id generator.
@@ -30,6 +31,19 @@ import { isPresent } from '@/utils/misc';
30
31
  */
31
32
  const managedSpanDestinations = new WeakMap<Span, string>();
32
33
 
34
+ type AnchoredSpan = {
35
+ destinationKey: string;
36
+ spanContext: SpanContext;
37
+ };
38
+
39
+ type TraceAnchorState = {
40
+ spans: Map<string, AnchoredSpan>;
41
+ spanIds: Set<string>;
42
+ };
43
+
44
+ const ROOT_TRACE_ANCHOR = '';
45
+ const traceAnchorStates = new WeakMap<object, TraceAnchorState>();
46
+
33
47
  export function registerLangfuseManagedSpan(
34
48
  span: Span,
35
49
  destinationKey: string
@@ -43,6 +57,68 @@ export function getLangfuseManagedSpanDestination(
43
57
  return managedSpanDestinations.get(span);
44
58
  }
45
59
 
60
+ /** Captures the first exported observation for a run and for each agent lane. */
61
+ export function registerLangfuseTraceAnchorSpan(
62
+ anchor: object,
63
+ span: Span,
64
+ destinationKey: string,
65
+ agentId?: string
66
+ ): void {
67
+ let state = traceAnchorStates.get(anchor);
68
+ if (state == null) {
69
+ state = { spans: new Map<string, AnchoredSpan>(), spanIds: new Set() };
70
+ traceAnchorStates.set(anchor, state);
71
+ }
72
+ const spanContext = span.spanContext();
73
+ state.spanIds.add(spanContext.spanId);
74
+ const key = agentId ?? ROOT_TRACE_ANCHOR;
75
+ if (state.spans.has(key)) {
76
+ return;
77
+ }
78
+ state.spans.set(key, { destinationKey, spanContext });
79
+ }
80
+
81
+ /**
82
+ * Resolves the most specific safe parent available for auxiliary work. An
83
+ * active observation wins so labels emitted inside a tool or reasoning step
84
+ * stay beside their source; otherwise the run root anchors the label in the
85
+ * same trace. An agent lane is the fallback when its overlay exports to a
86
+ * different destination. Cross-destination parents are never returned.
87
+ */
88
+ export function resolveLangfuseTraceAnchorParent(
89
+ anchor: object | undefined,
90
+ destinationKey: string | undefined,
91
+ agentId?: string
92
+ ): SpanContext | undefined {
93
+ if (anchor == null || destinationKey == null) {
94
+ return undefined;
95
+ }
96
+ const state = traceAnchorStates.get(anchor);
97
+ if (state == null) {
98
+ return undefined;
99
+ }
100
+ const agentSpan = agentId == null ? undefined : state.spans.get(agentId);
101
+ const rootSpan = state.spans.get(ROOT_TRACE_ANCHOR);
102
+ let anchoredSpan = rootSpan;
103
+ if (anchoredSpan?.destinationKey !== destinationKey) {
104
+ anchoredSpan = agentSpan;
105
+ }
106
+ if (anchoredSpan?.destinationKey !== destinationKey) {
107
+ return undefined;
108
+ }
109
+
110
+ const activeSpan = trace.getSpan(context.active());
111
+ if (
112
+ activeSpan != null &&
113
+ getLangfuseManagedSpanDestination(activeSpan) === destinationKey &&
114
+ activeSpan.spanContext().traceId === anchoredSpan.spanContext.traceId &&
115
+ state.spanIds.has(activeSpan.spanContext().spanId)
116
+ ) {
117
+ return activeSpan.spanContext();
118
+ }
119
+ return anchoredSpan.spanContext;
120
+ }
121
+
46
122
  function resolveLangfuseEnvironment(
47
123
  langfuse?: t.LangfuseConfig
48
124
  ): string | undefined {
@@ -779,8 +779,6 @@ export function redactLangfuseSpanToolOutputs(
779
779
  for (const key of [
780
780
  LangfuseOtelSpanAttributes.OBSERVATION_INPUT,
781
781
  LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT,
782
- LangfuseOtelSpanAttributes.TRACE_INPUT,
783
- LangfuseOtelSpanAttributes.TRACE_OUTPUT,
784
782
  ]) {
785
783
  redactAttribute(attributes, key, config);
786
784
  }