@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
@@ -1,9 +1,21 @@
1
1
  import { LangfuseOtelSpanAttributes } from '@langfuse/tracing';
2
2
  import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
3
- import { Constants } from '@/common';
3
+ import {
4
+ Constants,
5
+ STANDARD_GRAPH_RUN_NAME,
6
+ MULTI_AGENT_GRAPH_RUN_NAME,
7
+ AGENT_MODEL_CALL_RUN_NAME,
8
+ ACTIVITY_LABEL_RUN_NAME,
9
+ REASONING_LABEL_RUN_NAME,
10
+ ACTIVITY_PHASE_RUN_NAME,
11
+ ACTIVITY_PHASE_LABEL_RUN_NAME,
12
+ } from '@/common';
13
+ import { LANGFUSE_OPERATION_METADATA_KEY } from '@/langfuseOperation';
4
14
 
5
15
  const LANGGRAPH_START_NODE = '__start__';
16
+ const LANGGRAPH_RUN_NAME = 'LangGraph';
6
17
  const ANONYMOUS_LAMBDA_NAME = 'RunnableLambda';
18
+ const RUNNABLE_SEQUENCE_NAME = 'RunnableSequence';
7
19
  const LANGGRAPH_AGENT_NODE_PREFIX = 'agent=';
8
20
  const LANGGRAPH_TOOL_NODE_PREFIX = 'tools=';
9
21
  const AGENT_NODE_SPAN_NAME = 'agent';
@@ -14,11 +26,15 @@ const CHAIN_OBSERVATION_TYPE = 'chain';
14
26
  const TOOL_OBSERVATION_TYPE = 'tool';
15
27
  const AGENT_TRACE_TAG = 'agent';
16
28
  const TITLE_TRACE_TAG = 'title';
29
+ const ACTIVITY_LABEL_TRACE_TAG = 'activity-label';
30
+ const REASONING_LABEL_TRACE_TAG = 'reasoning-label';
17
31
  const ACTIVITY_PHASE_TRACE_TAG = 'activity-phase';
18
- const ACTIVITY_PHASE_ROOT_NAME = 'summarize-activity-phase';
19
32
  const EPHEMERAL_AGENT_SENDER_SEPARATOR = '___';
20
33
  const EPHEMERAL_AGENT_INDEX_SEPARATOR = '____';
34
+ const DEPRECATED_TRACE_INPUT_ATTRIBUTE = 'langfuse.trace.input';
35
+ const DEPRECATED_TRACE_OUTPUT_ATTRIBUTE = 'langfuse.trace.output';
21
36
  const OBSERVATION_METADATA_LANGGRAPH_NODE = `${LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.langgraph_node`;
37
+ const OBSERVATION_METADATA_OPERATION = `${LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.${LANGFUSE_OPERATION_METADATA_KEY}`;
22
38
 
23
39
  type MutableSpan = ReadableSpan & {
24
40
  name: string;
@@ -192,9 +208,7 @@ function getMessageToolCalls(
192
208
  */
193
209
  /** Tool-result ids present in the serialized state — ToolNode's
194
210
  * `!toolMessageIds.has(id)` execution filter, mirrored for the span. */
195
- function getToolResultIds(
196
- messages: Record<string, unknown>[]
197
- ): Set<string> {
211
+ function getToolResultIds(messages: Record<string, unknown>[]): Set<string> {
198
212
  const ids = new Set<string>();
199
213
  for (const message of messages) {
200
214
  if (getMessageRole(message) !== 'tool') {
@@ -217,7 +231,9 @@ function getMessageInvalidToolCalls(
217
231
  ): SerializedToolCall[] {
218
232
  const rawCalls =
219
233
  message.invalid_tool_calls ??
220
- (isRecord(message.kwargs) ? message.kwargs.invalid_tool_calls : undefined) ??
234
+ (isRecord(message.kwargs)
235
+ ? message.kwargs.invalid_tool_calls
236
+ : undefined) ??
221
237
  (isRecord(message.data) ? message.data.invalid_tool_calls : undefined);
222
238
  if (!Array.isArray(rawCalls)) {
223
239
  return [];
@@ -345,7 +361,80 @@ function shapeAgentNodeSpan(span: MutableSpan): void {
345
361
  ROOT_OBSERVATION_TYPE;
346
362
  }
347
363
 
348
- function shapeRootSpan(span: MutableSpan): void {
364
+ function shapeGraphSpan(span: MutableSpan): void {
365
+ if (span.name === LANGGRAPH_RUN_NAME) {
366
+ span.name = STANDARD_GRAPH_RUN_NAME;
367
+ }
368
+ span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] =
369
+ ROOT_OBSERVATION_TYPE;
370
+ }
371
+
372
+ function isAgentModelCallSpan(span: MutableSpan): boolean {
373
+ if (
374
+ span.name !== RUNNABLE_SEQUENCE_NAME &&
375
+ span.name !== AGENT_MODEL_CALL_RUN_NAME
376
+ ) {
377
+ return false;
378
+ }
379
+ const node = span.attributes[OBSERVATION_METADATA_LANGGRAPH_NODE];
380
+ return (
381
+ typeof node === 'string' && node.startsWith(LANGGRAPH_AGENT_NODE_PREFIX)
382
+ );
383
+ }
384
+
385
+ function shapeAgentModelCallSpan(span: MutableSpan): void {
386
+ span.name = AGENT_MODEL_CALL_RUN_NAME;
387
+ span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] =
388
+ CHAIN_OBSERVATION_TYPE;
389
+ }
390
+
391
+ function shapeActivityPhaseSpan(span: MutableSpan): void {
392
+ span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] =
393
+ CHAIN_OBSERVATION_TYPE;
394
+ }
395
+
396
+ function shapeGenerationSpan(span: MutableSpan): void {
397
+ if (
398
+ hasTraceTag(span, ACTIVITY_LABEL_TRACE_TAG) &&
399
+ span.attributes[OBSERVATION_METADATA_OPERATION] === ACTIVITY_LABEL_RUN_NAME
400
+ ) {
401
+ span.name = ACTIVITY_LABEL_RUN_NAME;
402
+ return;
403
+ }
404
+ if (
405
+ hasTraceTag(span, REASONING_LABEL_TRACE_TAG) &&
406
+ span.attributes[OBSERVATION_METADATA_OPERATION] === REASONING_LABEL_RUN_NAME
407
+ ) {
408
+ span.name = REASONING_LABEL_RUN_NAME;
409
+ return;
410
+ }
411
+ if (
412
+ hasTraceTag(span, ACTIVITY_PHASE_TRACE_TAG) &&
413
+ span.attributes[OBSERVATION_METADATA_OPERATION] ===
414
+ ACTIVITY_PHASE_LABEL_RUN_NAME
415
+ ) {
416
+ span.name = ACTIVITY_PHASE_LABEL_RUN_NAME;
417
+ return;
418
+ }
419
+ span.name = GENERATION_SPAN_NAME;
420
+ }
421
+
422
+ function isGraphSpan(span: MutableSpan): boolean {
423
+ if (
424
+ span.name !== LANGGRAPH_RUN_NAME &&
425
+ span.name !== STANDARD_GRAPH_RUN_NAME &&
426
+ span.name !== MULTI_AGENT_GRAPH_RUN_NAME
427
+ ) {
428
+ return false;
429
+ }
430
+ return (
431
+ isRootSpan(span) ||
432
+ typeof span.attributes[OBSERVATION_METADATA_LANGGRAPH_NODE] === 'string' ||
433
+ hasTraceTag(span, AGENT_TRACE_TAG)
434
+ );
435
+ }
436
+
437
+ function shapeConversationPayload(span: MutableSpan): void {
349
438
  const inputKey = LangfuseOtelSpanAttributes.OBSERVATION_INPUT;
350
439
  const outputKey = LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT;
351
440
  const question = findLastMessageText(
@@ -361,8 +450,7 @@ function shapeRootSpan(span: MutableSpan): void {
361
450
  * observation carrying its own prompt: reducing its observation input
362
451
  * would discard the SystemMessage from the one place it is traced.
363
452
  * Chain/agent roots keep the full reduction; their child generations
364
- * still record the complete prompt. Trace-level input/output reduce
365
- * either way, so the trace list keeps showing question and answer. */
453
+ * still record the complete prompt. */
366
454
  if (!isGenerationSpan(span)) {
367
455
  if (question != null) {
368
456
  span.attributes[inputKey] = question;
@@ -371,14 +459,6 @@ function shapeRootSpan(span: MutableSpan): void {
371
459
  span.attributes[outputKey] = answer;
372
460
  }
373
461
  }
374
- const traceInput = question ?? span.attributes[inputKey];
375
- const traceOutput = answer ?? span.attributes[outputKey];
376
- if (traceInput != null) {
377
- span.attributes[LangfuseOtelSpanAttributes.TRACE_INPUT] = traceInput;
378
- }
379
- if (traceOutput != null) {
380
- span.attributes[LangfuseOtelSpanAttributes.TRACE_OUTPUT] = traceOutput;
381
- }
382
462
  }
383
463
 
384
464
  function isGenerationSpan(span: MutableSpan): boolean {
@@ -477,7 +557,7 @@ function shapeRootObservationType(span: MutableSpan): void {
477
557
  }
478
558
  if (
479
559
  hasTraceTag(span, TITLE_TRACE_TAG) ||
480
- (span.name === ACTIVITY_PHASE_ROOT_NAME &&
560
+ (span.name === ACTIVITY_PHASE_RUN_NAME &&
481
561
  hasTraceTag(span, ACTIVITY_PHASE_TRACE_TAG))
482
562
  ) {
483
563
  span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] =
@@ -496,32 +576,46 @@ function shapeRootObservationType(span: MutableSpan): void {
496
576
  * (`provider__model`) — strip it so switching models doesn't break
497
577
  * name-based logic (item 1). The outer workflow node is named with the
498
578
  * bare agent id; ephemeral ids reduce to their stable sender name.
579
+ * - Graph and prompt-to-model framework names become stable SDK operations:
580
+ * `AgentGraph` / `MultiAgentGraph` and `AgentModelCall`.
499
581
  * - LLM generation spans keep the provider client class name (`ChatOpenAI`,
500
- * `AzureChatOpenAI`, …); rename them to a provider-agnostic `llm` so the
501
- * name reflects the operation, not the model (the model stays on the
502
- * generation's model attribute).
582
+ * `AzureChatOpenAI`, …); rename ordinary calls to a provider-agnostic `llm`
583
+ * and label calls by their activity/reasoning role. The model stays on the
584
+ * generation's model attribute.
503
585
  * - Agent nodes become `agent` observations, while tool-dispatch nodes become
504
586
  * stable `chain` observations whose input is scoped to the pending calls.
505
587
  * Individual child calls remain `tool` observations (items 3 & 4).
506
588
  * - Agent trace roots become `agent` observations, while title and activity
507
- * summary roots become `chain` observations. Root and trace input/output are
508
- * reduced to the user question and assistant response when chat messages are
509
- * available (item 2).
589
+ * summary operations become `chain` observations. Root-observation
590
+ * input/output are reduced to the user question and assistant response when
591
+ * chat messages are available (item 2).
510
592
  */
511
593
  export function shapeLangfuseSpan(span: ReadableSpan): void {
512
594
  const mutable = span as MutableSpan;
595
+ delete mutable.attributes[DEPRECATED_TRACE_INPUT_ATTRIBUTE];
596
+ delete mutable.attributes[DEPRECATED_TRACE_OUTPUT_ATTRIBUTE];
597
+ const isGraphObservation = isGraphSpan(mutable);
513
598
  if (mutable.name.startsWith(LANGGRAPH_AGENT_NODE_PREFIX)) {
514
599
  shapeAgentNodeSpan(mutable);
515
600
  } else if (mutable.name.startsWith(LANGGRAPH_TOOL_NODE_PREFIX)) {
516
601
  shapeToolNodeSpan(mutable);
602
+ } else if (isGraphObservation) {
603
+ shapeGraphSpan(mutable);
604
+ } else if (isAgentModelCallSpan(mutable)) {
605
+ shapeAgentModelCallSpan(mutable);
606
+ } else if (mutable.name === ACTIVITY_PHASE_RUN_NAME) {
607
+ shapeActivityPhaseSpan(mutable);
517
608
  } else if (isGenerationSpan(mutable)) {
518
- mutable.name = GENERATION_SPAN_NAME;
609
+ shapeGenerationSpan(mutable);
519
610
  } else {
520
611
  shapeEphemeralAgentNodeSpan(mutable);
521
612
  }
522
613
  if (!isRootSpan(span)) {
614
+ if (isGraphObservation) {
615
+ shapeConversationPayload(mutable);
616
+ }
523
617
  return;
524
618
  }
525
619
  shapeRootObservationType(mutable);
526
- shapeRootSpan(mutable);
620
+ shapeConversationPayload(mutable);
527
621
  }
@@ -58,14 +58,14 @@ import {
58
58
  STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY,
59
59
  OPENAI_CHAT_SEQUENTIAL_STREAMED_TOOL_CALL_ADAPTER,
60
60
  } from '@/tools/streamedToolCallSeals';
61
- import { isReasoningModel, _convertMessagesToOpenAIParams } from './utils';
62
- import { INTENT_ARG, isIntentLabelProperty } from '@/tools/intentArg';
63
- import { smoothStream, resolveStreamDelay } from '@/llm/stream/smoother';
64
61
  import {
65
62
  hasReasoningKwargs,
66
63
  hasToolCallChunks,
67
64
  getReasoningKwargsText,
68
65
  } from '@/llm/stream/chunkAdapters';
66
+ import { isReasoningModel, _convertMessagesToOpenAIParams } from './utils';
67
+ import { smoothStream, resolveStreamDelay } from '@/llm/stream/smoother';
68
+ import { INTENT_ARG, isIntentLabelProperty } from '@/tools/intentArg';
69
69
  import { dropRepeatedScalarMetadata } from './streamMetadata';
70
70
 
71
71
  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
@@ -919,6 +919,33 @@ function attachResponsesReplayPosition(
919
919
  chunk.message.lc_kwargs.additional_kwargs = additionalKwargs;
920
920
  }
921
921
 
922
+ function getResponsesStreamError(
923
+ event: OpenAIClient.Responses.ResponseStreamEvent
924
+ ): Error | undefined {
925
+ if (event.type === 'error') {
926
+ return new OpenAIClient.APIError(
927
+ undefined,
928
+ event,
929
+ event.message,
930
+ undefined
931
+ );
932
+ }
933
+ if (event.type !== 'response.failed') {
934
+ return;
935
+ }
936
+ if (event.response.error != null) {
937
+ return new OpenAIClient.APIError(
938
+ undefined,
939
+ event.response.error,
940
+ event.response.error.message,
941
+ undefined
942
+ );
943
+ }
944
+ return new OpenAIClient.OpenAIError(
945
+ `Response ${event.response.id} failed without error details.`
946
+ );
947
+ }
948
+
922
949
  async function* convertLibreChatResponsesStream(
923
950
  stream: AsyncIterable<OpenAIClient.Responses.ResponseStreamEvent>,
924
951
  options: ResponsesStreamChunkOptions,
@@ -929,6 +956,10 @@ async function* convertLibreChatResponsesStream(
929
956
  try {
930
957
  for await (const event of stream) {
931
958
  options.signal?.throwIfAborted();
959
+ const streamError = getResponsesStreamError(event);
960
+ if (streamError != null) {
961
+ throw streamError;
962
+ }
932
963
  const convertedChunk =
933
964
  convertResponsesDeltaToChatGenerationChunk(event) ??
934
965
  convertDroppedResponsesReplayOutput(event);
package/src/run.ts CHANGED
@@ -26,6 +26,16 @@ import type { MultiAgentGraph } from '@/graphs/MultiAgentGraph';
26
26
  import type { StandardGraph } from '@/graphs/Graph';
27
27
  import type { HookRegistry } from '@/hooks';
28
28
  import type * as t from '@/types';
29
+ import {
30
+ Callback,
31
+ GraphEvents,
32
+ TitleMethod,
33
+ ACTIVITY_LABEL_RUN_NAME,
34
+ REASONING_LABEL_RUN_NAME,
35
+ ACTIVITY_PHASE_RUN_NAME,
36
+ ACTIVITY_PHASE_LABEL_RUN_NAME,
37
+ DEFAULT_RECURSION_LIMIT,
38
+ } from '@/common';
29
39
  import {
30
40
  requireValidSubagentResumeManifest,
31
41
  stripSubagentResumeManifest,
@@ -58,6 +68,10 @@ import {
58
68
  resolveLangfuseConfig,
59
69
  resolveToolOutputTracingConfig,
60
70
  } from '@/langfuseConfig';
71
+ import {
72
+ resolveLangfuseDestinationKey,
73
+ resolveLangfuseTraceAnchorParent,
74
+ } from '@/langfuseSpanRegistry';
61
75
  import {
62
76
  appendCallbacks,
63
77
  filterCallbacks,
@@ -72,17 +86,12 @@ import {
72
86
  getRunStepResumeState,
73
87
  stripRunStepResumeState,
74
88
  } from '@/tools/runStepResume';
75
- import {
76
- Callback,
77
- GraphEvents,
78
- TitleMethod,
79
- DEFAULT_RECURSION_LIMIT,
80
- } from '@/common';
81
89
  import {
82
90
  createCompletionTitleRunnable,
83
91
  createTitleRunnable,
84
92
  } from '@/utils/title';
85
93
  import { applyGraphRuntimeConfig } from '@/graphs/applyGraphRuntimeConfig';
94
+ import { LANGFUSE_OPERATION_METADATA_KEY } from '@/langfuseOperation';
86
95
  import { createTokenCounter, encodingForModel } from '@/utils/tokens';
87
96
  import { initializeLangfuseTracing } from './instrumentation';
88
97
  import { seedRunInitialSessions } from '@/utils/toolSessions';
@@ -986,6 +995,9 @@ export class Run<_T extends t.BaseGraphState> {
986
995
  * instead of `inputs.messages`.
987
996
  */
988
997
  const isResume = inputs instanceof Command;
998
+ if (!isResume) {
999
+ graph.startFreshLangfuseExecution();
1000
+ }
989
1001
  const stateInputs = isResume ? undefined : (inputs as t.IState);
990
1002
  if (stateInputs != null) {
991
1003
  this.activityPhaseTraceInput = findActivityPhaseTraceInput(
@@ -1116,6 +1128,7 @@ export class Run<_T extends t.BaseGraphState> {
1116
1128
  streamLangfuseConfig?.deterministicTraceId === true
1117
1129
  ? this.id
1118
1130
  : undefined,
1131
+ traceAnchor: graph.langfuseTraceAnchor,
1119
1132
  // The graph's per-execution stamp, NOT the public run id: public ids
1120
1133
  // may repeat across concurrent executions (retries, tenant-local
1121
1134
  // message ids), and equal stamps defeat foreign-scope rejection.
@@ -1131,6 +1144,7 @@ export class Run<_T extends t.BaseGraphState> {
1131
1144
  streamLangfuseConfig?.deterministicTraceId === true
1132
1145
  ? this.id
1133
1146
  : undefined,
1147
+ traceAnchor: graph.langfuseTraceAnchor,
1134
1148
  runId: graph.langfuseScopeRunId,
1135
1149
  // The aggregate multi-agent policy from the runtime scope — the
1136
1150
  // handler must restore THIS (not the primary agent's config-derived
@@ -1139,7 +1153,6 @@ export class Run<_T extends t.BaseGraphState> {
1139
1153
  traceName,
1140
1154
  });
1141
1155
  if (langfuseHandler != null) {
1142
- config.runName = traceName;
1143
1156
  config.callbacks = appendCallbacks(config.callbacks, [langfuseHandler]);
1144
1157
  }
1145
1158
 
@@ -1201,18 +1214,22 @@ export class Run<_T extends t.BaseGraphState> {
1201
1214
  * arg to `BaseGraphState`. Cast on the call so the resume path
1202
1215
  * type-checks without widening the wrapper for every caller.
1203
1216
  */
1204
- const stream = graphRunnable.streamEvents(inputs as t.IState, config, {
1205
- raiseError: true,
1206
- /**
1207
- * Prevent EventStreamCallbackHandler from processing custom events.
1208
- * Custom events are already handled via our createCustomEventCallback()
1209
- * which routes them through the handlerRegistry.
1210
- * Without this flag, EventStreamCallbackHandler throws errors when
1211
- * custom events are dispatched for run IDs not in its internal map
1212
- * (due to timing issues in parallel execution or after run cleanup).
1213
- */
1214
- ignoreCustomEvent: true,
1215
- });
1217
+ const stream = graphRunnable.streamEvents(
1218
+ inputs as t.IState,
1219
+ { ...config, runName: graph.runName },
1220
+ {
1221
+ raiseError: true,
1222
+ /**
1223
+ * Prevent EventStreamCallbackHandler from processing custom events.
1224
+ * Custom events are already handled via our createCustomEventCallback()
1225
+ * which routes them through the handlerRegistry.
1226
+ * Without this flag, EventStreamCallbackHandler throws errors when
1227
+ * custom events are dispatched for run IDs not in its internal map
1228
+ * (due to timing issues in parallel execution or after run cleanup).
1229
+ */
1230
+ ignoreCustomEvent: true,
1231
+ }
1232
+ );
1216
1233
 
1217
1234
  for await (const event of stream) {
1218
1235
  const { data, metadata, ...info } = event;
@@ -2123,6 +2140,7 @@ export class Run<_T extends t.BaseGraphState> {
2123
2140
  const labelAgentName =
2124
2141
  labelAgentId == null ? undefined : labelContext?.name;
2125
2142
  const labelMetadata: Record<string, unknown> = {
2143
+ [LANGFUSE_OPERATION_METADATA_KEY]: ACTIVITY_LABEL_RUN_NAME,
2126
2144
  sourceRunId: this.id,
2127
2145
  responseId: this.id,
2128
2146
  activityIndex: labelIndex,
@@ -2132,7 +2150,7 @@ export class Run<_T extends t.BaseGraphState> {
2132
2150
  ...(labelAgentId == null ? {} : { agentId: labelAgentId }),
2133
2151
  ...(labelAgentName == null ? {} : { agentName: labelAgentName }),
2134
2152
  };
2135
- const traceMetadata = {
2153
+ const traceMetadata: Record<string, string> = {
2136
2154
  ...createLangfuseTraceMetadata({
2137
2155
  messageId: 'activity-label-' + this.id,
2138
2156
  parentMessageId: labelParentMessageId,
@@ -2150,16 +2168,9 @@ export class Run<_T extends t.BaseGraphState> {
2150
2168
  labelContext?.langfuse
2151
2169
  );
2152
2170
  initializeLangfuseTracing(labelLangfuseConfig);
2153
- /** Seed policy, threading two constraints:
2154
- * 1. `runWithLangfuseRuntimeContext` SPREADS the surrounding context, so
2155
- * an absent seed INHERITS the parent run's and collapses every label
2156
- * into that trace. When a parent seed is active we must override it
2157
- * with a per-label one.
2158
- * 2. Without deterministic tracing there is no parent seed, and forcing
2159
- * one here would make label trace ids deterministic when neither
2160
- * `processStream` nor `generateTitle` are — so leave it unset.
2161
- * Seeded when determinism is opted into OR a parent seed is live;
2162
- * otherwise unseeded, matching the other generation paths. */
2171
+ /** A captured source observation parents the label in the agent trace.
2172
+ * The distinct seed remains the standalone fallback when labeling runs
2173
+ * before tracing starts or against another Langfuse destination. */
2163
2174
  const inheritedTraceSeed = getTraceIdSeed();
2164
2175
  const labelTraceSeed =
2165
2176
  labelLangfuseConfig?.deterministicTraceId === true ||
@@ -2174,6 +2185,15 @@ export class Run<_T extends t.BaseGraphState> {
2174
2185
  traceIdSeed: labelTraceSeed,
2175
2186
  runId: labelScopeRunId,
2176
2187
  });
2188
+ const labelParentSpanContext = resolveLangfuseTraceAnchorParent(
2189
+ this.Graph?.langfuseTraceAnchor,
2190
+ resolveLangfuseDestinationKey(labelLangfuseConfig),
2191
+ labelAgentId
2192
+ );
2193
+ if (labelParentSpanContext != null) {
2194
+ labelMetadata.sourceTraceId = labelParentSpanContext.traceId;
2195
+ traceMetadata.sourceTraceId = labelParentSpanContext.traceId;
2196
+ }
2177
2197
  /** Handler only when a session id resolved from
2178
2198
  * `chainOptions.configurable.thread_id`: without it the label call has
2179
2199
  * no conversation identity, and tracing it would create an orphan
@@ -2187,15 +2207,18 @@ export class Run<_T extends t.BaseGraphState> {
2187
2207
  langfuse: labelLangfuseConfig,
2188
2208
  userId: labelUserId,
2189
2209
  sessionId: labelSessionId,
2190
- traceMetadata,
2210
+ traceMetadata:
2211
+ labelParentSpanContext == null ? traceMetadata : undefined,
2191
2212
  tags: labelTags,
2192
2213
  traceIdSeed:
2193
2214
  labelLangfuseConfig?.deterministicTraceId === true
2194
2215
  ? labelTraceSeed
2195
2216
  : undefined,
2217
+ parentSpanContext: labelParentSpanContext,
2218
+ inheritTraceIdentity: labelParentSpanContext != null,
2196
2219
  runId: labelScopeRunId,
2197
2220
  toolOutputTracing: labelRuntimeScope.toolOutputTracing,
2198
- traceName: labelRunName,
2221
+ traceName: labelParentSpanContext == null ? labelRunName : undefined,
2199
2222
  });
2200
2223
  }
2201
2224
  if (labelLangfuseHandler != null) {
@@ -2297,9 +2320,11 @@ export class Run<_T extends t.BaseGraphState> {
2297
2320
  langfuse: labelLangfuseConfig,
2298
2321
  userId: labelUserId,
2299
2322
  sessionId: labelSessionId,
2300
- traceName: labelRunName,
2301
- traceMetadata,
2323
+ traceName: labelParentSpanContext == null ? labelRunName : undefined,
2324
+ traceMetadata:
2325
+ labelParentSpanContext == null ? traceMetadata : undefined,
2302
2326
  tags: labelTags,
2327
+ inheritTraceIdentity: labelParentSpanContext != null,
2303
2328
  },
2304
2329
  () =>
2305
2330
  model.invoke(
@@ -2457,6 +2482,7 @@ export class Run<_T extends t.BaseGraphState> {
2457
2482
  const resolvedSourceRunId = sourceRunId ?? this.id;
2458
2483
  const reasoningResponseId = responseId ?? this.id;
2459
2484
  const reasoningMetadata: Record<string, unknown> = {
2485
+ [LANGFUSE_OPERATION_METADATA_KEY]: REASONING_LABEL_RUN_NAME,
2460
2486
  sourceRunId: resolvedSourceRunId,
2461
2487
  ...(sourceTraceId == null ? {} : { sourceTraceId }),
2462
2488
  responseId: reasoningResponseId,
@@ -2470,7 +2496,7 @@ export class Run<_T extends t.BaseGraphState> {
2470
2496
  ...(reasoningAgentId == null ? {} : { agentId: reasoningAgentId }),
2471
2497
  ...(reasoningAgentName == null ? {} : { agentName: reasoningAgentName }),
2472
2498
  };
2473
- const traceMetadata = {
2499
+ const traceMetadata: Record<string, string> = {
2474
2500
  ...createLangfuseTraceMetadata({
2475
2501
  messageId: `reasoning-label-${reasoningResponseId}`,
2476
2502
  parentMessageId: reasoningParentMessageId,
@@ -2512,21 +2538,40 @@ export class Run<_T extends t.BaseGraphState> {
2512
2538
  traceIdSeed: reasoningTraceSeed,
2513
2539
  runId: reasoningScopeRunId,
2514
2540
  });
2541
+ const candidateReasoningParent = resolveLangfuseTraceAnchorParent(
2542
+ this.Graph?.langfuseTraceAnchor,
2543
+ resolveLangfuseDestinationKey(reasoningLangfuseConfig),
2544
+ reasoningAgentId
2545
+ );
2546
+ const reasoningParentSpanContext =
2547
+ resolvedSourceRunId === this.id &&
2548
+ (sourceTraceId == null ||
2549
+ sourceTraceId === candidateReasoningParent?.traceId)
2550
+ ? candidateReasoningParent
2551
+ : undefined;
2552
+ if (reasoningParentSpanContext != null) {
2553
+ reasoningMetadata.sourceTraceId = reasoningParentSpanContext.traceId;
2554
+ traceMetadata.sourceTraceId = reasoningParentSpanContext.traceId;
2555
+ }
2515
2556
  let reasoningLangfuseHandler: CallbackEntry | undefined;
2516
2557
  if (reasoningSessionId != null) {
2517
2558
  reasoningLangfuseHandler = createLangfuseHandler({
2518
2559
  langfuse: reasoningLangfuseConfig,
2519
2560
  userId: reasoningUserId,
2520
2561
  sessionId: reasoningSessionId,
2521
- traceMetadata,
2562
+ traceMetadata:
2563
+ reasoningParentSpanContext == null ? traceMetadata : undefined,
2522
2564
  tags: reasoningTags,
2523
2565
  traceIdSeed:
2524
2566
  reasoningLangfuseConfig?.deterministicTraceId === true
2525
2567
  ? reasoningTraceSeed
2526
2568
  : undefined,
2569
+ parentSpanContext: reasoningParentSpanContext,
2570
+ inheritTraceIdentity: reasoningParentSpanContext != null,
2527
2571
  runId: reasoningScopeRunId,
2528
2572
  toolOutputTracing: reasoningRuntimeScope.toolOutputTracing,
2529
- traceName: reasoningRunName,
2573
+ traceName:
2574
+ reasoningParentSpanContext == null ? reasoningRunName : undefined,
2530
2575
  });
2531
2576
  }
2532
2577
  if (reasoningLangfuseHandler != null) {
@@ -2585,9 +2630,12 @@ export class Run<_T extends t.BaseGraphState> {
2585
2630
  langfuse: reasoningLangfuseConfig,
2586
2631
  userId: reasoningUserId,
2587
2632
  sessionId: reasoningSessionId,
2588
- traceName: reasoningRunName,
2589
- traceMetadata,
2633
+ traceName:
2634
+ reasoningParentSpanContext == null ? reasoningRunName : undefined,
2635
+ traceMetadata:
2636
+ reasoningParentSpanContext == null ? traceMetadata : undefined,
2590
2637
  tags: reasoningTags,
2638
+ inheritTraceIdentity: reasoningParentSpanContext != null,
2591
2639
  },
2592
2640
  () =>
2593
2641
  model.invoke(
@@ -2638,9 +2686,10 @@ export class Run<_T extends t.BaseGraphState> {
2638
2686
 
2639
2687
  /**
2640
2688
  * Generates one parent summary for two or more logical activities. The
2641
- * summary model is traced as a dedicated activity-phase chain root in the
2642
- * conversation session, with the model callback recorded as its generation
2643
- * child. No session id means no phase trace, avoiding orphan observations.
2689
+ * summary model is traced as an activity-phase chain beneath the source
2690
+ * agent run, with the model callback recorded as its generation child. It
2691
+ * falls back to a dedicated trace when no source observation is available.
2692
+ * No session id means no phase trace, avoiding orphan observations.
2644
2693
  */
2645
2694
  async generateActivityPhaseLabel({
2646
2695
  provider,
@@ -2771,6 +2820,7 @@ export class Run<_T extends t.BaseGraphState> {
2771
2820
  const phaseParentMessageId =
2772
2821
  phaseChainOptions.configurable?.requestBody?.parentMessageId;
2773
2822
  const phaseMetadata: Record<string, unknown> = {
2823
+ [LANGFUSE_OPERATION_METADATA_KEY]: ACTIVITY_PHASE_LABEL_RUN_NAME,
2774
2824
  sourceRunId: sourceRunId ?? this.id,
2775
2825
  ...(sourceTraceId == null ? {} : { sourceTraceId }),
2776
2826
  responseId: phaseMessageId,
@@ -2785,7 +2835,7 @@ export class Run<_T extends t.BaseGraphState> {
2785
2835
  ...(phaseAgentName == null ? {} : { agentName: phaseAgentName }),
2786
2836
  ...(closingTextPhase == null ? {} : { closingTextPhase }),
2787
2837
  };
2788
- const traceMetadata = {
2838
+ const traceMetadata: Record<string, string> = {
2789
2839
  ...createLangfuseTraceMetadata({
2790
2840
  messageId: phaseMessageId,
2791
2841
  parentMessageId: phaseParentMessageId,
@@ -2826,6 +2876,19 @@ export class Run<_T extends t.BaseGraphState> {
2826
2876
  traceIdSeed: phaseTraceSeed,
2827
2877
  runId: phaseScopeRunId,
2828
2878
  });
2879
+ const candidatePhaseParent = resolveLangfuseTraceAnchorParent(
2880
+ this.Graph?.langfuseTraceAnchor,
2881
+ resolveLangfuseDestinationKey(phaseLangfuseConfig)
2882
+ );
2883
+ const phaseParentSpanContext =
2884
+ (sourceRunId == null || sourceRunId === this.id) &&
2885
+ (sourceTraceId == null || sourceTraceId === candidatePhaseParent?.traceId)
2886
+ ? candidatePhaseParent
2887
+ : undefined;
2888
+ if (phaseParentSpanContext != null) {
2889
+ phaseMetadata.sourceTraceId = phaseParentSpanContext.traceId;
2890
+ traceMetadata.sourceTraceId = phaseParentSpanContext.traceId;
2891
+ }
2829
2892
  let phaseLangfuseHandler: CallbackEntry | undefined;
2830
2893
  const sourceUserText =
2831
2894
  this.activityPhaseTraceInput ??
@@ -2835,15 +2898,18 @@ export class Run<_T extends t.BaseGraphState> {
2835
2898
  langfuse: phaseLangfuseConfig,
2836
2899
  userId: phaseUserId,
2837
2900
  sessionId: phaseSessionId,
2838
- traceMetadata,
2901
+ traceMetadata:
2902
+ phaseParentSpanContext == null ? traceMetadata : undefined,
2839
2903
  tags: phaseTags,
2840
2904
  traceIdSeed:
2841
2905
  phaseLangfuseConfig?.deterministicTraceId === true
2842
2906
  ? phaseTraceSeed
2843
2907
  : undefined,
2908
+ parentSpanContext: phaseParentSpanContext,
2909
+ inheritTraceIdentity: phaseParentSpanContext != null,
2844
2910
  runId: phaseScopeRunId,
2845
2911
  toolOutputTracing: phaseRuntimeScope.toolOutputTracing,
2846
- traceName: phaseTraceName,
2912
+ traceName: phaseParentSpanContext == null ? phaseTraceName : undefined,
2847
2913
  });
2848
2914
  }
2849
2915
  if (phaseLangfuseHandler != null) {
@@ -2864,7 +2930,7 @@ export class Run<_T extends t.BaseGraphState> {
2864
2930
  const invokeConfig = Object.assign({}, phaseChainOptions, {
2865
2931
  run_id: phaseRunId,
2866
2932
  runId: phaseRunId,
2867
- runName: 'summarize-activity-phase',
2933
+ runName: ACTIVITY_PHASE_RUN_NAME,
2868
2934
  tags: [...new Set([...(phaseChainOptions.tags ?? []), ...phaseTags])],
2869
2935
  metadata: {
2870
2936
  ...(phaseChainOptions.metadata ?? {}),
@@ -2943,7 +3009,7 @@ export class Run<_T extends t.BaseGraphState> {
2943
3009
  ? { label, messages: [new AIMessage(label)] }
2944
3010
  : { messages: [] };
2945
3011
  },
2946
- }).withConfig({ runName: 'summarize-activity-phase' });
3012
+ }).withConfig({ runName: ACTIVITY_PHASE_RUN_NAME });
2947
3013
 
2948
3014
  try {
2949
3015
  const result = await withLangfuseRuntimeScope(phaseRuntimeScope, () =>
@@ -2952,9 +3018,12 @@ export class Run<_T extends t.BaseGraphState> {
2952
3018
  langfuse: phaseLangfuseConfig,
2953
3019
  userId: phaseUserId,
2954
3020
  sessionId: phaseSessionId,
2955
- traceName: phaseTraceName,
2956
- traceMetadata,
3021
+ traceName:
3022
+ phaseParentSpanContext == null ? phaseTraceName : undefined,
3023
+ traceMetadata:
3024
+ phaseParentSpanContext == null ? traceMetadata : undefined,
2957
3025
  tags: phaseTags,
3026
+ inheritTraceIdentity: phaseParentSpanContext != null,
2958
3027
  },
2959
3028
  () =>
2960
3029
  phaseRunnable.invoke(