@librechat/agents 3.6.7 → 3.6.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 (103) 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/search/search.cjs +19 -9
  31. package/dist/cjs/tools/search/search.cjs.map +1 -1
  32. package/dist/cjs/tools/search/tool.cjs +2 -1
  33. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  34. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +3 -3
  35. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  36. package/dist/esm/common/constants.mjs +8 -1
  37. package/dist/esm/common/constants.mjs.map +1 -1
  38. package/dist/esm/graphs/Graph.mjs +18 -6
  39. package/dist/esm/graphs/Graph.mjs.map +1 -1
  40. package/dist/esm/graphs/MultiAgentGraph.mjs +6 -1
  41. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  42. package/dist/esm/instrumentation.mjs +4 -2
  43. package/dist/esm/instrumentation.mjs.map +1 -1
  44. package/dist/esm/langfuse.mjs +47 -16
  45. package/dist/esm/langfuse.mjs.map +1 -1
  46. package/dist/esm/langfuseOperation.mjs +6 -0
  47. package/dist/esm/langfuseOperation.mjs.map +1 -0
  48. package/dist/esm/langfuseRuntimeContext.mjs +3 -2
  49. package/dist/esm/langfuseRuntimeContext.mjs.map +1 -1
  50. package/dist/esm/langfuseRuntimeScope.mjs +12 -3
  51. package/dist/esm/langfuseRuntimeScope.mjs.map +1 -1
  52. package/dist/esm/langfuseSpanRegistry.mjs +34 -1
  53. package/dist/esm/langfuseSpanRegistry.mjs.map +1 -1
  54. package/dist/esm/langfuseToolOutputTracing.mjs +1 -6
  55. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  56. package/dist/esm/langfuseTraceShaping.mjs +58 -10
  57. package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
  58. package/dist/esm/llm/openai/index.mjs +8 -0
  59. package/dist/esm/llm/openai/index.mjs.map +1 -1
  60. package/dist/esm/main.mjs +3 -3
  61. package/dist/esm/run.mjs +59 -25
  62. package/dist/esm/run.mjs.map +1 -1
  63. package/dist/esm/stream.mjs +1 -1
  64. package/dist/esm/tools/ToolNode.mjs +2 -2
  65. package/dist/esm/tools/search/search.mjs +19 -9
  66. package/dist/esm/tools/search/search.mjs.map +1 -1
  67. package/dist/esm/tools/search/tool.mjs +2 -1
  68. package/dist/esm/tools/search/tool.mjs.map +1 -1
  69. package/dist/esm/tools/subagent/SubagentExecutor.mjs +3 -3
  70. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  71. package/dist/types/common/constants.d.ts +8 -0
  72. package/dist/types/graphs/Graph.d.ts +8 -3
  73. package/dist/types/graphs/MultiAgentGraph.d.ts +1 -0
  74. package/dist/types/langfuse.d.ts +10 -1
  75. package/dist/types/langfuseOperation.d.ts +2 -0
  76. package/dist/types/langfuseRuntimeContext.d.ts +2 -0
  77. package/dist/types/langfuseRuntimeScope.d.ts +3 -1
  78. package/dist/types/langfuseSpanRegistry.d.ts +11 -1
  79. package/dist/types/langfuseTraceShaping.d.ts +8 -6
  80. package/dist/types/run.d.ts +4 -3
  81. package/dist/types/tools/search/types.d.ts +15 -0
  82. package/dist/types/types/activityLabel.d.ts +4 -3
  83. package/dist/types/types/reasoningLabel.d.ts +1 -1
  84. package/package.json +5 -5
  85. package/src/common/constants.ts +9 -0
  86. package/src/graphs/Graph.ts +125 -103
  87. package/src/graphs/MultiAgentGraph.ts +6 -2
  88. package/src/instrumentation.ts +12 -0
  89. package/src/langfuse.ts +83 -17
  90. package/src/langfuseOperation.ts +2 -0
  91. package/src/langfuseRuntimeContext.ts +6 -0
  92. package/src/langfuseRuntimeScope.ts +31 -1
  93. package/src/langfuseSpanRegistry.ts +78 -2
  94. package/src/langfuseToolOutputTracing.ts +0 -2
  95. package/src/langfuseTraceShaping.ts +120 -26
  96. package/src/llm/openai/index.ts +34 -3
  97. package/src/run.ts +118 -49
  98. package/src/tools/search/search.ts +25 -8
  99. package/src/tools/search/tool.ts +2 -0
  100. package/src/tools/search/types.ts +16 -0
  101. package/src/tools/subagent/SubagentExecutor.ts +8 -4
  102. package/src/types/activityLabel.ts +4 -3
  103. package/src/types/reasoningLabel.ts +1 -1
@@ -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
  }
@@ -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);