@librechat/agents 3.3.7 → 3.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/dist/cjs/graphs/Graph.cjs +47 -13
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/graphs/MultiAgentGraph.cjs +56 -6
  4. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  5. package/dist/cjs/hitl/askUserQuestion.cjs +3 -2
  6. package/dist/cjs/hitl/askUserQuestion.cjs.map +1 -1
  7. package/dist/cjs/instrumentation.cjs +18 -48
  8. package/dist/cjs/instrumentation.cjs.map +1 -1
  9. package/dist/cjs/langfuse.cjs +174 -29
  10. package/dist/cjs/langfuse.cjs.map +1 -1
  11. package/dist/cjs/langfuseConfig.cjs +12 -0
  12. package/dist/cjs/langfuseConfig.cjs.map +1 -1
  13. package/dist/cjs/langfuseRuntimeContext.cjs +23 -2
  14. package/dist/cjs/langfuseRuntimeContext.cjs.map +1 -1
  15. package/dist/cjs/langfuseRuntimeScope.cjs +39 -8
  16. package/dist/cjs/langfuseRuntimeScope.cjs.map +1 -1
  17. package/dist/cjs/langfuseSpanRegistry.cjs +95 -0
  18. package/dist/cjs/langfuseSpanRegistry.cjs.map +1 -0
  19. package/dist/cjs/langfuseTraceShaping.cjs +121 -4
  20. package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
  21. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +39 -15
  22. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  23. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +25 -5
  24. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  25. package/dist/cjs/llm/init.cjs +3 -3
  26. package/dist/cjs/llm/invoke.cjs +5 -5
  27. package/dist/cjs/llm/openai/index.cjs +1 -1
  28. package/dist/cjs/main.cjs +10 -10
  29. package/dist/cjs/messages/format.cjs +124 -15
  30. package/dist/cjs/messages/format.cjs.map +1 -1
  31. package/dist/cjs/messages/injected.cjs +10 -1
  32. package/dist/cjs/messages/injected.cjs.map +1 -1
  33. package/dist/cjs/messages/prune.cjs +13 -1
  34. package/dist/cjs/messages/prune.cjs.map +1 -1
  35. package/dist/cjs/prompts/activityLabel.cjs +51 -11
  36. package/dist/cjs/prompts/activityLabel.cjs.map +1 -1
  37. package/dist/cjs/run.cjs +54 -24
  38. package/dist/cjs/run.cjs.map +1 -1
  39. package/dist/cjs/session/messageSerialization.cjs +6 -0
  40. package/dist/cjs/session/messageSerialization.cjs.map +1 -1
  41. package/dist/cjs/stream.cjs +21 -10
  42. package/dist/cjs/stream.cjs.map +1 -1
  43. package/dist/cjs/summarization/node.cjs +60 -0
  44. package/dist/cjs/summarization/node.cjs.map +1 -1
  45. package/dist/cjs/tools/ToolNode.cjs +253 -24
  46. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  47. package/dist/cjs/tools/handlers.cjs +1 -1
  48. package/dist/cjs/tools/search/tool.cjs +1 -1
  49. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -1
  50. package/dist/cjs/utils/index.cjs +2 -2
  51. package/dist/esm/graphs/Graph.mjs +48 -14
  52. package/dist/esm/graphs/Graph.mjs.map +1 -1
  53. package/dist/esm/graphs/MultiAgentGraph.mjs +56 -6
  54. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  55. package/dist/esm/hitl/askUserQuestion.mjs +3 -2
  56. package/dist/esm/hitl/askUserQuestion.mjs.map +1 -1
  57. package/dist/esm/instrumentation.mjs +18 -48
  58. package/dist/esm/instrumentation.mjs.map +1 -1
  59. package/dist/esm/langfuse.mjs +176 -28
  60. package/dist/esm/langfuse.mjs.map +1 -1
  61. package/dist/esm/langfuseConfig.mjs +10 -1
  62. package/dist/esm/langfuseConfig.mjs.map +1 -1
  63. package/dist/esm/langfuseRuntimeContext.mjs +21 -3
  64. package/dist/esm/langfuseRuntimeContext.mjs.map +1 -1
  65. package/dist/esm/langfuseRuntimeScope.mjs +39 -10
  66. package/dist/esm/langfuseRuntimeScope.mjs.map +1 -1
  67. package/dist/esm/langfuseSpanRegistry.mjs +91 -0
  68. package/dist/esm/langfuseSpanRegistry.mjs.map +1 -0
  69. package/dist/esm/langfuseTraceShaping.mjs +121 -4
  70. package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
  71. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +39 -15
  72. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  73. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +25 -5
  74. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  75. package/dist/esm/llm/init.mjs +2 -2
  76. package/dist/esm/llm/invoke.mjs +5 -5
  77. package/dist/esm/llm/openai/index.mjs +1 -1
  78. package/dist/esm/main.mjs +8 -8
  79. package/dist/esm/messages/format.mjs +124 -15
  80. package/dist/esm/messages/format.mjs.map +1 -1
  81. package/dist/esm/messages/injected.mjs +10 -1
  82. package/dist/esm/messages/injected.mjs.map +1 -1
  83. package/dist/esm/messages/prune.mjs +13 -1
  84. package/dist/esm/messages/prune.mjs.map +1 -1
  85. package/dist/esm/prompts/activityLabel.mjs +51 -11
  86. package/dist/esm/prompts/activityLabel.mjs.map +1 -1
  87. package/dist/esm/run.mjs +54 -24
  88. package/dist/esm/run.mjs.map +1 -1
  89. package/dist/esm/session/messageSerialization.mjs +6 -0
  90. package/dist/esm/session/messageSerialization.mjs.map +1 -1
  91. package/dist/esm/stream.mjs +21 -10
  92. package/dist/esm/stream.mjs.map +1 -1
  93. package/dist/esm/summarization/node.mjs +60 -0
  94. package/dist/esm/summarization/node.mjs.map +1 -1
  95. package/dist/esm/tools/ToolNode.mjs +254 -25
  96. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  97. package/dist/esm/tools/handlers.mjs +1 -1
  98. package/dist/esm/tools/search/tool.mjs +1 -1
  99. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -1
  100. package/dist/esm/utils/index.mjs +2 -2
  101. package/dist/types/graphs/Graph.d.ts +19 -0
  102. package/dist/types/hitl/askUserQuestion.d.ts +11 -1
  103. package/dist/types/langfuse.d.ts +16 -8
  104. package/dist/types/langfuseConfig.d.ts +6 -0
  105. package/dist/types/langfuseRuntimeContext.d.ts +27 -1
  106. package/dist/types/langfuseRuntimeScope.d.ts +17 -2
  107. package/dist/types/langfuseSpanRegistry.d.ts +17 -0
  108. package/dist/types/langfuseTraceShaping.d.ts +2 -1
  109. package/dist/types/llm/anthropic/utils/message_inputs.d.ts +1 -0
  110. package/dist/types/messages/format.d.ts +9 -8
  111. package/dist/types/prompts/activityLabel.d.ts +8 -1
  112. package/dist/types/run.d.ts +1 -1
  113. package/dist/types/session/types.d.ts +1 -0
  114. package/dist/types/tools/ToolNode.d.ts +7 -1
  115. package/dist/types/types/activityLabel.d.ts +8 -0
  116. package/dist/types/types/hitl.d.ts +8 -0
  117. package/dist/types/types/stream.d.ts +19 -0
  118. package/dist/types/types/tools.d.ts +30 -0
  119. package/package.json +7 -4
  120. package/src/__tests__/stream.eagerArgsDivergence.test.ts +753 -0
  121. package/src/graphs/Graph.ts +69 -20
  122. package/src/graphs/MultiAgentGraph.ts +74 -6
  123. package/src/graphs/__tests__/composition.smoke.test.ts +4 -0
  124. package/src/hitl/askUserQuestion.ts +14 -1
  125. package/src/instrumentation.ts +35 -77
  126. package/src/langfuse.ts +320 -43
  127. package/src/langfuseConfig.ts +24 -0
  128. package/src/langfuseRuntimeContext.ts +43 -1
  129. package/src/langfuseRuntimeScope.ts +94 -21
  130. package/src/langfuseSpanRegistry.ts +131 -0
  131. package/src/langfuseTraceShaping.ts +194 -7
  132. package/src/llm/anthropic/utils/message_inputs.ts +70 -19
  133. package/src/llm/anthropic/utils/streaming-tool-input.test.ts +186 -11
  134. package/src/llm/bedrock/utils/message_inputs.test.ts +120 -4
  135. package/src/llm/bedrock/utils/message_inputs.ts +32 -7
  136. package/src/messages/format.ts +222 -50
  137. package/src/messages/formatAgentMessages.test.ts +308 -6
  138. package/src/messages/injected.test.ts +18 -1
  139. package/src/messages/injected.ts +8 -1
  140. package/src/messages/prune.ts +12 -1
  141. package/src/prompts/activityLabel.ts +67 -2
  142. package/src/run.ts +86 -46
  143. package/src/scripts/activity-labels/captured.json +56 -0
  144. package/src/scripts/activity-labels/checks.cjs +205 -0
  145. package/src/scripts/activity-labels/corpus.cjs +473 -0
  146. package/src/scripts/activity-labels/report.cjs +203 -0
  147. package/src/scripts/activity-labels/rescore.cjs +102 -0
  148. package/src/scripts/activity-labels/run.ts +705 -0
  149. package/src/scripts/activity-labels/variants.ts +71 -0
  150. package/src/session/messageSerialization.ts +12 -1
  151. package/src/session/types.ts +1 -0
  152. package/src/specs/activity-label-prompt.test.ts +109 -0
  153. package/src/specs/agent-handoffs.test.ts +306 -0
  154. package/src/specs/langfuse-callbacks.test.ts +456 -0
  155. package/src/specs/langfuse-routing.integration.test.ts +138 -1
  156. package/src/specs/langfuse-span-registry.test.ts +70 -0
  157. package/src/specs/langfuse-trace-shaping.test.ts +294 -0
  158. package/src/specs/prune.test.ts +38 -1
  159. package/src/stream.ts +70 -6
  160. package/src/summarization/__tests__/node.test.ts +188 -0
  161. package/src/summarization/node.ts +72 -0
  162. package/src/tools/ToolNode.ts +400 -9
  163. package/src/tools/__tests__/ToolNode.invalidToolCalls.test.ts +757 -0
  164. package/src/tools/__tests__/hitl.test.ts +58 -0
  165. package/src/types/activityLabel.ts +8 -0
  166. package/src/types/hitl.ts +8 -0
  167. package/src/types/stream.ts +20 -0
  168. package/src/types/tools.ts +35 -1
@@ -5,6 +5,8 @@ let _opentelemetry_api = require("@opentelemetry/api");
5
5
  const langfuseToolOutputTracingConfigKey = (0, _opentelemetry_api.createContextKey)("librechat.langfuse.tool-output-tracing");
6
6
  const langfuseConfigKey = (0, _opentelemetry_api.createContextKey)("librechat.langfuse.config");
7
7
  const langfuseTraceIdSeedKey = (0, _opentelemetry_api.createContextKey)("librechat.langfuse.trace-id-seed");
8
+ const langfuseScopeRunIdKey = (0, _opentelemetry_api.createContextKey)("librechat.langfuse.run-id");
9
+ const langfuseScopeAgentIdKey = (0, _opentelemetry_api.createContextKey)("librechat.langfuse.agent-id");
8
10
  function isRecord(value) {
9
11
  return value != null && typeof value === "object" && !Array.isArray(value);
10
12
  }
@@ -29,27 +31,56 @@ function resolveLangfuseConfigForSpan(activeContext) {
29
31
  function resolveTraceIdSeedForSpan(activeContext) {
30
32
  return require_langfuseRuntimeContext.getTraceIdSeed() ?? getOtelTraceIdSeed(activeContext);
31
33
  }
34
+ function getOtelScopeRunId(activeContext) {
35
+ const value = activeContext.getValue(langfuseScopeRunIdKey);
36
+ return typeof value === "string" && value.trim() !== "" ? value : void 0;
37
+ }
38
+ function getOtelScopeAgentId(activeContext) {
39
+ const value = activeContext.getValue(langfuseScopeAgentIdKey);
40
+ return typeof value === "string" && value.trim() !== "" ? value : void 0;
41
+ }
42
+ /** The run identity the active scope belongs to (see
43
+ * `LangfuseRuntimeContext.runId`), or `undefined` for unstamped scopes. */
44
+ function resolveLangfuseScopeRunId(activeContext) {
45
+ return require_langfuseRuntimeContext.getLangfuseScopeRunId() ?? getOtelScopeRunId(activeContext);
46
+ }
47
+ /** The agent identity of a per-agent overlay scope (see
48
+ * `LangfuseRuntimeContext.agentId`), or `undefined` for run-level scopes. */
49
+ function resolveLangfuseScopeAgentId(activeContext) {
50
+ return require_langfuseRuntimeContext.getLangfuseScopeAgentId() ?? getOtelScopeAgentId(activeContext);
51
+ }
32
52
  function resolveToolOutputTracingConfigForSpan(activeContext) {
33
53
  return require_langfuseRuntimeContext.getLangfuseRuntimeToolOutputTracingConfig() ?? getOtelToolOutputTracingConfig(activeContext);
34
54
  }
35
- function withLangfuseRuntimeScope(scope, action) {
36
- if (!require_langfuseRuntimeContext.hasLangfuseRuntimeContextValue(scope)) return action();
55
+ function setOrClearContextValue(activeContext, key, value, replace) {
56
+ if (value != null) return activeContext.setValue(key, value);
57
+ return replace ? activeContext.deleteValue(key) : activeContext;
58
+ }
59
+ function withLangfuseRuntimeScope(scope, action, options) {
60
+ const replace = options?.replace === true;
61
+ if (!replace && !require_langfuseRuntimeContext.hasLangfuseRuntimeContextValue(scope)) return action();
37
62
  let activeContext = _opentelemetry_api.context.active();
38
- if (scope.langfuse != null) activeContext = activeContext.setValue(langfuseConfigKey, scope.langfuse);
39
- if (scope.toolOutputTracing != null) activeContext = activeContext.setValue(langfuseToolOutputTracingConfigKey, scope.toolOutputTracing);
40
- if (hasText(scope.traceIdSeed)) activeContext = activeContext.setValue(langfuseTraceIdSeedKey, scope.traceIdSeed);
41
- return require_langfuseRuntimeContext.runWithLangfuseRuntimeContext(scope, () => _opentelemetry_api.context.with(activeContext, action));
63
+ activeContext = setOrClearContextValue(activeContext, langfuseConfigKey, scope.langfuse, replace);
64
+ activeContext = setOrClearContextValue(activeContext, langfuseToolOutputTracingConfigKey, scope.toolOutputTracing, replace);
65
+ activeContext = setOrClearContextValue(activeContext, langfuseTraceIdSeedKey, hasText(scope.traceIdSeed) ? scope.traceIdSeed : void 0, replace);
66
+ activeContext = setOrClearContextValue(activeContext, langfuseScopeRunIdKey, hasText(scope.runId) ? scope.runId : void 0, replace);
67
+ activeContext = setOrClearContextValue(activeContext, langfuseScopeAgentIdKey, hasText(scope.agentId) ? scope.agentId : void 0, replace);
68
+ return (replace ? require_langfuseRuntimeContext.replaceLangfuseRuntimeContext : require_langfuseRuntimeContext.runWithLangfuseRuntimeContext)(scope, () => _opentelemetry_api.context.with(activeContext, action));
42
69
  }
43
- function resolveLangfuseRuntimeScope({ runLangfuse, langfuseOverlay, traceIdSeed }) {
70
+ function resolveLangfuseRuntimeScope({ runLangfuse, langfuseOverlay, traceIdSeed, runId, agentId }) {
44
71
  return {
45
72
  langfuse: require_langfuseConfig.resolveLangfuseConfig(runLangfuse, langfuseOverlay),
46
73
  traceIdSeed,
47
- toolOutputTracing: !require_langfuseConfig.hasToolOutputTracingConfig(runLangfuse, langfuseOverlay) ? void 0 : require_langfuseConfig.resolveToolOutputTracingConfig(runLangfuse, langfuseOverlay)
74
+ toolOutputTracing: !require_langfuseConfig.hasToolOutputTracingConfig(runLangfuse, langfuseOverlay) ? void 0 : require_langfuseConfig.resolveToolOutputTracingConfig(runLangfuse, langfuseOverlay),
75
+ runId,
76
+ agentId
48
77
  };
49
78
  }
50
79
  //#endregion
51
80
  exports.resolveLangfuseConfigForSpan = resolveLangfuseConfigForSpan;
52
81
  exports.resolveLangfuseRuntimeScope = resolveLangfuseRuntimeScope;
82
+ exports.resolveLangfuseScopeAgentId = resolveLangfuseScopeAgentId;
83
+ exports.resolveLangfuseScopeRunId = resolveLangfuseScopeRunId;
53
84
  exports.resolveToolOutputTracingConfigForSpan = resolveToolOutputTracingConfigForSpan;
54
85
  exports.resolveTraceIdSeedForSpan = resolveTraceIdSeedForSpan;
55
86
  exports.withLangfuseRuntimeScope = withLangfuseRuntimeScope;
@@ -1 +1 @@
1
- {"version":3,"file":"langfuseRuntimeScope.cjs","names":["getLangfuseRuntimeConfig","getTraceIdSeed","getLangfuseRuntimeToolOutputTracingConfig","hasLangfuseRuntimeContextValue","context","runWithLangfuseRuntimeContext","resolveLangfuseConfig","hasToolOutputTracingConfig","resolveToolOutputTracingConfig"],"sources":["../../src/langfuseRuntimeScope.ts"],"sourcesContent":["import { context, createContextKey } from '@opentelemetry/api';\nimport type { Context } from '@opentelemetry/api';\nimport type {\n LangfuseRuntimeContext,\n ResolvedLangfuseToolOutputTracingConfig,\n} from '@/langfuseRuntimeContext';\nimport type * as t from '@/types';\nimport {\n getLangfuseRuntimeConfig,\n getLangfuseRuntimeToolOutputTracingConfig,\n getTraceIdSeed,\n hasLangfuseRuntimeContextValue,\n runWithLangfuseRuntimeContext,\n} from '@/langfuseRuntimeContext';\nimport {\n hasToolOutputTracingConfig,\n resolveLangfuseConfig,\n resolveToolOutputTracingConfig,\n} from '@/langfuseConfig';\n\nexport type LangfuseRuntimeScope = LangfuseRuntimeContext;\n\nexport type ResolveLangfuseRuntimeScopeParams = {\n runLangfuse?: t.LangfuseConfig;\n langfuseOverlay?: t.LangfuseConfig;\n traceIdSeed?: string;\n};\n\nconst langfuseToolOutputTracingConfigKey = createContextKey(\n 'librechat.langfuse.tool-output-tracing'\n);\nconst langfuseConfigKey = createContextKey('librechat.langfuse.config');\nconst langfuseTraceIdSeedKey = createContextKey(\n 'librechat.langfuse.trace-id-seed'\n);\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value != null && typeof value === 'object' && !Array.isArray(value);\n}\n\nfunction hasText(value: string | undefined): value is string {\n return value != null && value.trim() !== '';\n}\n\nexport function getOtelLangfuseConfig(\n activeContext: Context\n): t.LangfuseConfig | undefined {\n const value = activeContext.getValue(langfuseConfigKey);\n return isRecord(value) ? (value as t.LangfuseConfig) : undefined;\n}\n\nexport function getOtelTraceIdSeed(activeContext: Context): string | undefined {\n const value = activeContext.getValue(langfuseTraceIdSeedKey);\n return typeof value === 'string' && value.trim() !== '' ? value : undefined;\n}\n\nexport function getOtelToolOutputTracingConfig(\n activeContext: Context\n): ResolvedLangfuseToolOutputTracingConfig | undefined {\n const value = activeContext.getValue(langfuseToolOutputTracingConfigKey);\n return isRecord(value)\n ? (value as ResolvedLangfuseToolOutputTracingConfig)\n : undefined;\n}\n\nexport function resolveLangfuseConfigForSpan(\n activeContext: Context\n): t.LangfuseConfig | undefined {\n return getLangfuseRuntimeConfig() ?? getOtelLangfuseConfig(activeContext);\n}\n\nexport function resolveTraceIdSeedForSpan(\n activeContext: Context\n): string | undefined {\n return getTraceIdSeed() ?? getOtelTraceIdSeed(activeContext);\n}\n\nexport function resolveToolOutputTracingConfigForSpan(\n activeContext: Context\n): ResolvedLangfuseToolOutputTracingConfig | undefined {\n return (\n getLangfuseRuntimeToolOutputTracingConfig() ??\n getOtelToolOutputTracingConfig(activeContext)\n );\n}\n\nexport function withLangfuseRuntimeScope<T>(\n scope: LangfuseRuntimeScope,\n action: () => T\n): T {\n if (!hasLangfuseRuntimeContextValue(scope)) {\n return action();\n }\n\n let activeContext = context.active();\n if (scope.langfuse != null) {\n activeContext = activeContext.setValue(langfuseConfigKey, scope.langfuse);\n }\n if (scope.toolOutputTracing != null) {\n activeContext = activeContext.setValue(\n langfuseToolOutputTracingConfigKey,\n scope.toolOutputTracing\n );\n }\n if (hasText(scope.traceIdSeed)) {\n activeContext = activeContext.setValue(\n langfuseTraceIdSeedKey,\n scope.traceIdSeed\n );\n }\n\n // Span processors receive the OTel parent context in `onStart`, while\n // LangChain callback handlers may run outside that context and need ALS.\n // The trace id generator reads the seed from ALS or OTel context so SDK\n // callbacks that preserve only one of those contexts still keep trace/score\n // cohesion.\n return runWithLangfuseRuntimeContext(scope, () =>\n context.with(activeContext, action)\n );\n}\n\nexport function resolveLangfuseRuntimeScope({\n runLangfuse,\n langfuseOverlay,\n traceIdSeed,\n}: ResolveLangfuseRuntimeScopeParams): LangfuseRuntimeScope {\n const langfuse = resolveLangfuseConfig(runLangfuse, langfuseOverlay);\n const toolOutputTracing = !hasToolOutputTracingConfig(\n runLangfuse,\n langfuseOverlay\n )\n ? undefined\n : resolveToolOutputTracingConfig(runLangfuse, langfuseOverlay);\n return { langfuse, traceIdSeed, toolOutputTracing };\n}\n"],"mappings":";;;;AA4BA,MAAM,sCAAA,GAAA,mBAAA,iBAAA,CACJ,wCACF;AACA,MAAM,qBAAA,GAAA,mBAAA,iBAAA,CAAqC,2BAA2B;AACtE,MAAM,0BAAA,GAAA,mBAAA,iBAAA,CACJ,kCACF;AAEA,SAAS,SAAS,OAAkD;CAClE,OAAO,SAAS,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK;AAC3E;AAEA,SAAS,QAAQ,OAA4C;CAC3D,OAAO,SAAS,QAAQ,MAAM,KAAK,MAAM;AAC3C;AAEA,SAAgB,sBACd,eAC8B;CAC9B,MAAM,QAAQ,cAAc,SAAS,iBAAiB;CACtD,OAAO,SAAS,KAAK,IAAK,QAA6B,KAAA;AACzD;AAEA,SAAgB,mBAAmB,eAA4C;CAC7E,MAAM,QAAQ,cAAc,SAAS,sBAAsB;CAC3D,OAAO,OAAO,UAAU,YAAY,MAAM,KAAK,MAAM,KAAK,QAAQ,KAAA;AACpE;AAEA,SAAgB,+BACd,eACqD;CACrD,MAAM,QAAQ,cAAc,SAAS,kCAAkC;CACvE,OAAO,SAAS,KAAK,IAChB,QACD,KAAA;AACN;AAEA,SAAgB,6BACd,eAC8B;CAC9B,OAAOA,+BAAAA,yBAAyB,KAAK,sBAAsB,aAAa;AAC1E;AAEA,SAAgB,0BACd,eACoB;CACpB,OAAOC,+BAAAA,eAAe,KAAK,mBAAmB,aAAa;AAC7D;AAEA,SAAgB,sCACd,eACqD;CACrD,OACEC,+BAAAA,0CAA0C,KAC1C,+BAA+B,aAAa;AAEhD;AAEA,SAAgB,yBACd,OACA,QACG;CACH,IAAI,CAACC,+BAAAA,+BAA+B,KAAK,GACvC,OAAO,OAAO;CAGhB,IAAI,gBAAgBC,mBAAAA,QAAQ,OAAO;CACnC,IAAI,MAAM,YAAY,MACpB,gBAAgB,cAAc,SAAS,mBAAmB,MAAM,QAAQ;CAE1E,IAAI,MAAM,qBAAqB,MAC7B,gBAAgB,cAAc,SAC5B,oCACA,MAAM,iBACR;CAEF,IAAI,QAAQ,MAAM,WAAW,GAC3B,gBAAgB,cAAc,SAC5B,wBACA,MAAM,WACR;CAQF,OAAOC,+BAAAA,8BAA8B,aACnCD,mBAAAA,QAAQ,KAAK,eAAe,MAAM,CACpC;AACF;AAEA,SAAgB,4BAA4B,EAC1C,aACA,iBACA,eAC0D;CAQ1D,OAAO;EAAE,UAPQE,uBAAAA,sBAAsB,aAAa,eAOpC;EAAG;EAAa,mBANN,CAACC,uBAAAA,2BACzB,aACA,eACF,IACI,KAAA,IACAC,uBAAAA,+BAA+B,aAAa,eAAe;CACb;AACpD"}
1
+ {"version":3,"file":"langfuseRuntimeScope.cjs","names":["getLangfuseRuntimeConfig","getTraceIdSeed","getLangfuseScopeRunId","getLangfuseScopeAgentId","getLangfuseRuntimeToolOutputTracingConfig","hasLangfuseRuntimeContextValue","context","replaceLangfuseRuntimeContext","runWithLangfuseRuntimeContext","resolveLangfuseConfig","hasToolOutputTracingConfig","resolveToolOutputTracingConfig"],"sources":["../../src/langfuseRuntimeScope.ts"],"sourcesContent":["import { context, createContextKey } from '@opentelemetry/api';\nimport type { Context } from '@opentelemetry/api';\nimport type {\n LangfuseRuntimeContext,\n ResolvedLangfuseToolOutputTracingConfig,\n} from '@/langfuseRuntimeContext';\nimport type * as t from '@/types';\nimport {\n getLangfuseRuntimeConfig,\n getLangfuseRuntimeToolOutputTracingConfig,\n getLangfuseScopeAgentId,\n getLangfuseScopeRunId,\n getTraceIdSeed,\n hasLangfuseRuntimeContextValue,\n replaceLangfuseRuntimeContext,\n runWithLangfuseRuntimeContext,\n} from '@/langfuseRuntimeContext';\nimport {\n hasToolOutputTracingConfig,\n resolveLangfuseConfig,\n resolveToolOutputTracingConfig,\n} from '@/langfuseConfig';\n\nexport type LangfuseRuntimeScope = LangfuseRuntimeContext;\n\nexport type ResolveLangfuseRuntimeScopeParams = {\n runLangfuse?: t.LangfuseConfig;\n langfuseOverlay?: t.LangfuseConfig;\n traceIdSeed?: string;\n runId?: string;\n agentId?: string;\n};\n\nexport type LangfuseRuntimeScopeOptions = {\n /** Replace the surrounding scope entirely instead of merging with it:\n * fields absent from `scope` are CLEARED rather than inherited. Required\n * when rejecting a foreign concurrent run's scope, whose explicit values\n * must not survive merge inheritance. */\n replace?: boolean;\n};\n\nconst langfuseToolOutputTracingConfigKey = createContextKey(\n 'librechat.langfuse.tool-output-tracing'\n);\nconst langfuseConfigKey = createContextKey('librechat.langfuse.config');\nconst langfuseTraceIdSeedKey = createContextKey(\n 'librechat.langfuse.trace-id-seed'\n);\nconst langfuseScopeRunIdKey = createContextKey('librechat.langfuse.run-id');\nconst langfuseScopeAgentIdKey = createContextKey('librechat.langfuse.agent-id');\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value != null && typeof value === 'object' && !Array.isArray(value);\n}\n\nfunction hasText(value: string | undefined): value is string {\n return value != null && value.trim() !== '';\n}\n\nexport function getOtelLangfuseConfig(\n activeContext: Context\n): t.LangfuseConfig | undefined {\n const value = activeContext.getValue(langfuseConfigKey);\n return isRecord(value) ? (value as t.LangfuseConfig) : undefined;\n}\n\nexport function getOtelTraceIdSeed(activeContext: Context): string | undefined {\n const value = activeContext.getValue(langfuseTraceIdSeedKey);\n return typeof value === 'string' && value.trim() !== '' ? value : undefined;\n}\n\nexport function getOtelToolOutputTracingConfig(\n activeContext: Context\n): ResolvedLangfuseToolOutputTracingConfig | undefined {\n const value = activeContext.getValue(langfuseToolOutputTracingConfigKey);\n return isRecord(value)\n ? (value as ResolvedLangfuseToolOutputTracingConfig)\n : undefined;\n}\n\nexport function resolveLangfuseConfigForSpan(\n activeContext: Context\n): t.LangfuseConfig | undefined {\n return getLangfuseRuntimeConfig() ?? getOtelLangfuseConfig(activeContext);\n}\n\nexport function resolveTraceIdSeedForSpan(\n activeContext: Context\n): string | undefined {\n return getTraceIdSeed() ?? getOtelTraceIdSeed(activeContext);\n}\n\nfunction getOtelScopeRunId(activeContext: Context): string | undefined {\n const value = activeContext.getValue(langfuseScopeRunIdKey);\n return typeof value === 'string' && value.trim() !== '' ? value : undefined;\n}\n\nfunction getOtelScopeAgentId(activeContext: Context): string | undefined {\n const value = activeContext.getValue(langfuseScopeAgentIdKey);\n return typeof value === 'string' && value.trim() !== '' ? value : undefined;\n}\n\n/** The run identity the active scope belongs to (see\n * `LangfuseRuntimeContext.runId`), or `undefined` for unstamped scopes. */\nexport function resolveLangfuseScopeRunId(\n activeContext: Context\n): string | undefined {\n return getLangfuseScopeRunId() ?? getOtelScopeRunId(activeContext);\n}\n\n/** The agent identity of a per-agent overlay scope (see\n * `LangfuseRuntimeContext.agentId`), or `undefined` for run-level scopes. */\nexport function resolveLangfuseScopeAgentId(\n activeContext: Context\n): string | undefined {\n return getLangfuseScopeAgentId() ?? getOtelScopeAgentId(activeContext);\n}\n\nexport function resolveToolOutputTracingConfigForSpan(\n activeContext: Context\n): ResolvedLangfuseToolOutputTracingConfig | undefined {\n return (\n getLangfuseRuntimeToolOutputTracingConfig() ??\n getOtelToolOutputTracingConfig(activeContext)\n );\n}\n\nfunction setOrClearContextValue(\n activeContext: Context,\n key: Parameters<Context['setValue']>[0],\n value: unknown,\n replace: boolean\n): Context {\n if (value != null) {\n return activeContext.setValue(key, value);\n }\n return replace ? activeContext.deleteValue(key) : activeContext;\n}\n\nexport function withLangfuseRuntimeScope<T>(\n scope: LangfuseRuntimeScope,\n action: () => T,\n options?: LangfuseRuntimeScopeOptions\n): T {\n const replace = options?.replace === true;\n if (!replace && !hasLangfuseRuntimeContextValue(scope)) {\n return action();\n }\n\n let activeContext = context.active();\n activeContext = setOrClearContextValue(\n activeContext,\n langfuseConfigKey,\n scope.langfuse,\n replace\n );\n activeContext = setOrClearContextValue(\n activeContext,\n langfuseToolOutputTracingConfigKey,\n scope.toolOutputTracing,\n replace\n );\n activeContext = setOrClearContextValue(\n activeContext,\n langfuseTraceIdSeedKey,\n hasText(scope.traceIdSeed) ? scope.traceIdSeed : undefined,\n replace\n );\n activeContext = setOrClearContextValue(\n activeContext,\n langfuseScopeRunIdKey,\n hasText(scope.runId) ? scope.runId : undefined,\n replace\n );\n activeContext = setOrClearContextValue(\n activeContext,\n langfuseScopeAgentIdKey,\n hasText(scope.agentId) ? scope.agentId : undefined,\n replace\n );\n\n // Span processors receive the OTel parent context in `onStart`, while\n // LangChain callback handlers may run outside that context and need ALS.\n // The trace id generator reads the seed from ALS or OTel context so SDK\n // callbacks that preserve only one of those contexts still keep trace/score\n // cohesion.\n const runScoped = replace\n ? replaceLangfuseRuntimeContext\n : runWithLangfuseRuntimeContext;\n return runScoped(scope, () => context.with(activeContext, action));\n}\n\nexport function resolveLangfuseRuntimeScope({\n runLangfuse,\n langfuseOverlay,\n traceIdSeed,\n runId,\n agentId,\n}: ResolveLangfuseRuntimeScopeParams): LangfuseRuntimeScope {\n const langfuse = resolveLangfuseConfig(runLangfuse, langfuseOverlay);\n const toolOutputTracing = !hasToolOutputTracingConfig(\n runLangfuse,\n langfuseOverlay\n )\n ? undefined\n : resolveToolOutputTracingConfig(runLangfuse, langfuseOverlay);\n return { langfuse, traceIdSeed, toolOutputTracing, runId, agentId };\n}\n"],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ const require_misc = require("./utils/misc.cjs");
2
+ const require_langfuseConfig = require("./langfuseConfig.cjs");
3
+ let node_crypto = require("node:crypto");
4
+ //#region src/langfuseSpanRegistry.ts
5
+ /**
6
+ * Spans created through the Langfuse tracer provider, keyed by the export
7
+ * destination they were routed to. Callback handlers use this to distinguish
8
+ * safe ambient parents from spans a root observation must not inherit:
9
+ *
10
+ * - Foreign spans (a host's own OpenTelemetry instrumentation, e.g. HTTP
11
+ * server spans on the global provider) are never exported to Langfuse —
12
+ * inheriting one orphans the trace root (its root/trace input-output
13
+ * shaping is skipped), collapses concurrent runs inside one request
14
+ * context into a single merged trace, and bypasses the seeded
15
+ * deterministic trace id generator.
16
+ * - Langfuse-managed spans bound to a *different* destination (another
17
+ * tenant's project) would leave the new trace dangling in its own
18
+ * destination while inheriting the other tenant's trace id.
19
+ *
20
+ * Only a managed span whose destination matches the starting run's resolved
21
+ * destination is a safe parent — that is the sanctioned way for hosts to
22
+ * group runs under their own Langfuse observations.
23
+ */
24
+ const managedSpanDestinations = /* @__PURE__ */ new WeakMap();
25
+ function registerLangfuseManagedSpan(span, destinationKey) {
26
+ managedSpanDestinations.set(span, destinationKey);
27
+ }
28
+ function getLangfuseManagedSpanDestination(span) {
29
+ return managedSpanDestinations.get(span);
30
+ }
31
+ function resolveLangfuseEnvironment(langfuse) {
32
+ const candidates = [
33
+ langfuse?.environment,
34
+ process.env.LANGFUSE_TRACING_ENVIRONMENT,
35
+ process.env.NODE_ENV
36
+ ];
37
+ for (const candidate of candidates) if (candidate != null && candidate.trim() !== "") return candidate.trim();
38
+ }
39
+ function getLangfuseSpanProcessorParams(langfuse) {
40
+ if (langfuse?.enabled === false) return;
41
+ const environment = resolveLangfuseEnvironment(langfuse);
42
+ if (require_langfuseConfig.hasLangfuseConfigCredentials(langfuse)) return {
43
+ publicKey: langfuse.publicKey,
44
+ secretKey: langfuse.secretKey,
45
+ ...require_misc.isPresent(langfuse.baseUrl) ? { baseUrl: langfuse.baseUrl } : {},
46
+ ...require_misc.isPresent(environment) ? { environment } : {}
47
+ };
48
+ if (require_langfuseConfig.hasLangfuseEnvConfig()) {
49
+ const baseUrl = langfuse?.baseUrl ?? process.env.LANGFUSE_BASE_URL ?? process.env.LANGFUSE_BASEURL;
50
+ return {
51
+ publicKey: process.env.LANGFUSE_PUBLIC_KEY,
52
+ secretKey: process.env.LANGFUSE_SECRET_KEY,
53
+ ...require_misc.isPresent(baseUrl) ? { baseUrl } : {},
54
+ ...require_misc.isPresent(environment) ? { environment } : {}
55
+ };
56
+ }
57
+ if (require_misc.isPresent(langfuse?.baseUrl) && require_langfuseConfig.hasLangfuseEnvCredentials()) return {
58
+ publicKey: process.env.LANGFUSE_PUBLIC_KEY,
59
+ secretKey: process.env.LANGFUSE_SECRET_KEY,
60
+ baseUrl: langfuse.baseUrl,
61
+ ...require_misc.isPresent(environment) ? { environment } : {}
62
+ };
63
+ }
64
+ function hashCacheKeyValue(value) {
65
+ return require_misc.isPresent(value) ? (0, node_crypto.createHash)("sha256").update(value, "utf8").digest("hex") : void 0;
66
+ }
67
+ /**
68
+ * Identity of an export destination (project credentials + endpoint +
69
+ * environment) only. Processor-level policies like `toolOutputTracing` are
70
+ * deliberately excluded: two spans exporting to the same project under
71
+ * different redaction settings still share a destination and may parent one
72
+ * another.
73
+ */
74
+ function getLangfuseDestinationKey(params) {
75
+ return JSON.stringify({
76
+ publicKey: params.publicKey,
77
+ secretKeyHash: hashCacheKeyValue(params.secretKey),
78
+ baseUrl: params.baseUrl,
79
+ environment: params.environment
80
+ });
81
+ }
82
+ /** The export destination a run with this config resolves to, or `undefined`
83
+ * when no Langfuse destination is configured. */
84
+ function resolveLangfuseDestinationKey(langfuse) {
85
+ const params = getLangfuseSpanProcessorParams(langfuse);
86
+ return params == null ? void 0 : getLangfuseDestinationKey(params);
87
+ }
88
+ //#endregion
89
+ exports.getLangfuseDestinationKey = getLangfuseDestinationKey;
90
+ exports.getLangfuseManagedSpanDestination = getLangfuseManagedSpanDestination;
91
+ exports.getLangfuseSpanProcessorParams = getLangfuseSpanProcessorParams;
92
+ exports.registerLangfuseManagedSpan = registerLangfuseManagedSpan;
93
+ exports.resolveLangfuseDestinationKey = resolveLangfuseDestinationKey;
94
+
95
+ //# sourceMappingURL=langfuseSpanRegistry.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"langfuseSpanRegistry.cjs","names":["hasLangfuseConfigCredentials","isPresent","hasLangfuseEnvConfig","hasLangfuseEnvCredentials"],"sources":["../../src/langfuseSpanRegistry.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport type { LangfuseSpanProcessorParams } from '@langfuse/otel';\nimport type { Span } from '@opentelemetry/api';\nimport type * as t from '@/types';\nimport {\n hasLangfuseConfigCredentials,\n hasLangfuseEnvCredentials,\n hasLangfuseEnvConfig,\n} from '@/langfuseConfig';\nimport { isPresent } from '@/utils/misc';\n\n/**\n * Spans created through the Langfuse tracer provider, keyed by the export\n * destination they were routed to. Callback handlers use this to distinguish\n * safe ambient parents from spans a root observation must not inherit:\n *\n * - Foreign spans (a host's own OpenTelemetry instrumentation, e.g. HTTP\n * server spans on the global provider) are never exported to Langfuse —\n * inheriting one orphans the trace root (its root/trace input-output\n * shaping is skipped), collapses concurrent runs inside one request\n * context into a single merged trace, and bypasses the seeded\n * deterministic trace id generator.\n * - Langfuse-managed spans bound to a *different* destination (another\n * tenant's project) would leave the new trace dangling in its own\n * destination while inheriting the other tenant's trace id.\n *\n * Only a managed span whose destination matches the starting run's resolved\n * destination is a safe parent — that is the sanctioned way for hosts to\n * group runs under their own Langfuse observations.\n */\nconst managedSpanDestinations = new WeakMap<Span, string>();\n\nexport function registerLangfuseManagedSpan(\n span: Span,\n destinationKey: string\n): void {\n managedSpanDestinations.set(span, destinationKey);\n}\n\nexport function getLangfuseManagedSpanDestination(\n span: Span\n): string | undefined {\n return managedSpanDestinations.get(span);\n}\n\nfunction resolveLangfuseEnvironment(\n langfuse?: t.LangfuseConfig\n): string | undefined {\n const candidates = [\n langfuse?.environment,\n process.env.LANGFUSE_TRACING_ENVIRONMENT,\n process.env.NODE_ENV,\n ];\n for (const candidate of candidates) {\n if (candidate != null && candidate.trim() !== '') {\n return candidate.trim();\n }\n }\n return undefined;\n}\n\nexport function getLangfuseSpanProcessorParams(\n langfuse?: t.LangfuseConfig\n): LangfuseSpanProcessorParams | undefined {\n if (langfuse?.enabled === false) {\n return undefined;\n }\n const environment = resolveLangfuseEnvironment(langfuse);\n if (hasLangfuseConfigCredentials(langfuse)) {\n return {\n publicKey: langfuse.publicKey,\n secretKey: langfuse.secretKey,\n ...(isPresent(langfuse.baseUrl) ? { baseUrl: langfuse.baseUrl } : {}),\n ...(isPresent(environment) ? { environment } : {}),\n };\n }\n if (hasLangfuseEnvConfig()) {\n const baseUrl =\n langfuse?.baseUrl ??\n process.env.LANGFUSE_BASE_URL ??\n process.env.LANGFUSE_BASEURL;\n return {\n publicKey: process.env.LANGFUSE_PUBLIC_KEY as string,\n secretKey: process.env.LANGFUSE_SECRET_KEY as string,\n ...(isPresent(baseUrl) ? { baseUrl } : {}),\n ...(isPresent(environment) ? { environment } : {}),\n };\n }\n if (isPresent(langfuse?.baseUrl) && hasLangfuseEnvCredentials()) {\n return {\n publicKey: process.env.LANGFUSE_PUBLIC_KEY as string,\n secretKey: process.env.LANGFUSE_SECRET_KEY as string,\n baseUrl: langfuse.baseUrl,\n ...(isPresent(environment) ? { environment } : {}),\n };\n }\n return undefined;\n}\n\nfunction hashCacheKeyValue(value: string | undefined): string | undefined {\n return isPresent(value)\n ? createHash('sha256').update(value, 'utf8').digest('hex')\n : undefined;\n}\n\n/**\n * Identity of an export destination (project credentials + endpoint +\n * environment) only. Processor-level policies like `toolOutputTracing` are\n * deliberately excluded: two spans exporting to the same project under\n * different redaction settings still share a destination and may parent one\n * another.\n */\nexport function getLangfuseDestinationKey(\n params: LangfuseSpanProcessorParams\n): string {\n return JSON.stringify({\n publicKey: params.publicKey,\n secretKeyHash: hashCacheKeyValue(params.secretKey),\n baseUrl: params.baseUrl,\n environment: params.environment,\n });\n}\n\n/** The export destination a run with this config resolves to, or `undefined`\n * when no Langfuse destination is configured. */\nexport function resolveLangfuseDestinationKey(\n langfuse?: t.LangfuseConfig\n): string | undefined {\n const params = getLangfuseSpanProcessorParams(langfuse);\n return params == null ? undefined : getLangfuseDestinationKey(params);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AA8BA,MAAM,0CAA0B,IAAI,QAAsB;AAE1D,SAAgB,4BACd,MACA,gBACM;CACN,wBAAwB,IAAI,MAAM,cAAc;AAClD;AAEA,SAAgB,kCACd,MACoB;CACpB,OAAO,wBAAwB,IAAI,IAAI;AACzC;AAEA,SAAS,2BACP,UACoB;CACpB,MAAM,aAAa;EACjB,UAAU;EACV,QAAQ,IAAI;EACZ,QAAQ,IAAI;CACd;CACA,KAAK,MAAM,aAAa,YACtB,IAAI,aAAa,QAAQ,UAAU,KAAK,MAAM,IAC5C,OAAO,UAAU,KAAK;AAI5B;AAEA,SAAgB,+BACd,UACyC;CACzC,IAAI,UAAU,YAAY,OACxB;CAEF,MAAM,cAAc,2BAA2B,QAAQ;CACvD,IAAIA,uBAAAA,6BAA6B,QAAQ,GACvC,OAAO;EACL,WAAW,SAAS;EACpB,WAAW,SAAS;EACpB,GAAIC,aAAAA,UAAU,SAAS,OAAO,IAAI,EAAE,SAAS,SAAS,QAAQ,IAAI,CAAC;EACnE,GAAIA,aAAAA,UAAU,WAAW,IAAI,EAAE,YAAY,IAAI,CAAC;CAClD;CAEF,IAAIC,uBAAAA,qBAAqB,GAAG;EAC1B,MAAM,UACJ,UAAU,WACV,QAAQ,IAAI,qBACZ,QAAQ,IAAI;EACd,OAAO;GACL,WAAW,QAAQ,IAAI;GACvB,WAAW,QAAQ,IAAI;GACvB,GAAID,aAAAA,UAAU,OAAO,IAAI,EAAE,QAAQ,IAAI,CAAC;GACxC,GAAIA,aAAAA,UAAU,WAAW,IAAI,EAAE,YAAY,IAAI,CAAC;EAClD;CACF;CACA,IAAIA,aAAAA,UAAU,UAAU,OAAO,KAAKE,uBAAAA,0BAA0B,GAC5D,OAAO;EACL,WAAW,QAAQ,IAAI;EACvB,WAAW,QAAQ,IAAI;EACvB,SAAS,SAAS;EAClB,GAAIF,aAAAA,UAAU,WAAW,IAAI,EAAE,YAAY,IAAI,CAAC;CAClD;AAGJ;AAEA,SAAS,kBAAkB,OAA+C;CACxE,OAAOA,aAAAA,UAAU,KAAK,KAAA,GAAA,YAAA,WAAA,CACP,QAAQ,CAAC,CAAC,OAAO,OAAO,MAAM,CAAC,CAAC,OAAO,KAAK,IACvD,KAAA;AACN;;;;;;;;AASA,SAAgB,0BACd,QACQ;CACR,OAAO,KAAK,UAAU;EACpB,WAAW,OAAO;EAClB,eAAe,kBAAkB,OAAO,SAAS;EACjD,SAAS,OAAO;EAChB,aAAa,OAAO;CACtB,CAAC;AACH;;;AAIA,SAAgB,8BACd,UACoB;CACpB,MAAM,SAAS,+BAA+B,QAAQ;CACtD,OAAO,UAAU,OAAO,KAAA,IAAY,0BAA0B,MAAM;AACtE"}
@@ -1,3 +1,5 @@
1
+ require("./common/enum.cjs");
2
+ require("./common/index.cjs");
1
3
  let _langfuse_tracing = require("@langfuse/tracing");
2
4
  //#region src/langfuseTraceShaping.ts
3
5
  const LANGGRAPH_START_NODE = "__start__";
@@ -9,8 +11,12 @@ const TOOL_DISPATCH_SPAN_NAME = "tool-dispatch";
9
11
  const GENERATION_SPAN_NAME = "llm";
10
12
  const ROOT_OBSERVATION_TYPE = "agent";
11
13
  const CHAIN_OBSERVATION_TYPE = "chain";
14
+ const TOOL_OBSERVATION_TYPE = "tool";
12
15
  const AGENT_TRACE_TAG = "agent";
13
16
  const TITLE_TRACE_TAG = "title";
17
+ const EPHEMERAL_AGENT_SENDER_SEPARATOR = "___";
18
+ const EPHEMERAL_AGENT_INDEX_SEPARATOR = "____";
19
+ const OBSERVATION_METADATA_LANGGRAPH_NODE = `${_langfuse_tracing.LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.langgraph_node`;
14
20
  function isRecord(value) {
15
21
  return value != null && typeof value === "object" && !Array.isArray(value);
16
22
  }
@@ -94,13 +100,64 @@ function getMessageToolCalls(message) {
94
100
  }
95
101
  return calls;
96
102
  }
103
+ /**
104
+ * Id-bearing `invalid_tool_calls` on the assistant turn. ToolNode pairs these
105
+ * with synthesized error results (and an invalid-only turn is routed on them
106
+ * alone), so the tool-dispatch span must count them as part of the executing
107
+ * batch — otherwise an invalid-only dispatch finds zero calls and the span
108
+ * keeps the full serialized graph state as its input, and a mixed dispatch
109
+ * silently omits the malformed call. `args` stays the raw unparsed string.
110
+ */
111
+ /** Tool-result ids present in the serialized state — ToolNode's
112
+ * `!toolMessageIds.has(id)` execution filter, mirrored for the span. */
113
+ function getToolResultIds(messages) {
114
+ const ids = /* @__PURE__ */ new Set();
115
+ for (const message of messages) {
116
+ if (getMessageRole(message) !== "tool") continue;
117
+ const rawId = message.tool_call_id ?? (isRecord(message.kwargs) ? message.kwargs.tool_call_id : void 0) ?? (isRecord(message.data) ? message.data.tool_call_id : void 0);
118
+ if (typeof rawId === "string" && rawId !== "") ids.add(rawId);
119
+ }
120
+ return ids;
121
+ }
122
+ function getMessageInvalidToolCalls(message, answeredIds) {
123
+ const rawCalls = message.invalid_tool_calls ?? (isRecord(message.kwargs) ? message.kwargs.invalid_tool_calls : void 0) ?? (isRecord(message.data) ? message.data.invalid_tool_calls : void 0);
124
+ if (!Array.isArray(rawCalls)) return [];
125
+ const calls = [];
126
+ for (const rawCall of rawCalls) {
127
+ if (!isRecord(rawCall) || typeof rawCall.id !== "string" || rawCall.id === "" || answeredIds.has(rawCall.id) || rawCall.id.startsWith("srvtoolu_")) continue;
128
+ const call = normalizeToolCall(typeof rawCall.name === "string" && rawCall.name !== "" ? rawCall : {
129
+ ...rawCall,
130
+ name: "unknown"
131
+ });
132
+ if (call != null) calls.push(call);
133
+ }
134
+ return calls;
135
+ }
136
+ /** The serialized message's own id (uuid), NOT the LC-serialization type id
137
+ * array that `message.id` carries in constructor dumps. */
138
+ function getSerializedMessageId(message) {
139
+ const kwargsId = isRecord(message.kwargs) ? message.kwargs.id : void 0;
140
+ const dataId = isRecord(message.data) ? message.data.id : void 0;
141
+ const rawId = message.id;
142
+ const id = kwargsId ?? dataId ?? rawId;
143
+ return typeof id === "string" && id !== "" ? id : void 0;
144
+ }
97
145
  /** Latest assistant turn's tool calls — the calls this tool node is executing. */
98
146
  function findPendingToolCalls(value) {
99
147
  const messages = getMessageArray(value);
100
148
  if (messages == null) return [];
149
+ /**
150
+ * Invalid calls count only where ToolNode's own gate lets them execute:
151
+ * the messages-state form (a bare-array state means the node returns a
152
+ * plain output list and skips invalid handling) with an id-bearing
153
+ * assistant message (no id, no reducer upsert). Mirrors
154
+ * `canPromoteInvalidCalls` so the span never reports skipped calls.
155
+ */
156
+ const invalidCallsApply = !Array.isArray(value);
157
+ const answeredIds = invalidCallsApply ? getToolResultIds(messages) : void 0;
101
158
  for (let i = messages.length - 1; i >= 0; i--) {
102
159
  if (getMessageRole(messages[i]) !== "assistant") continue;
103
- const calls = getMessageToolCalls(messages[i]);
160
+ const calls = [...getMessageToolCalls(messages[i]), ...invalidCallsApply && getSerializedMessageId(messages[i]) != null ? getMessageInvalidToolCalls(messages[i], answeredIds) : []];
104
161
  if (calls.length > 0) return calls;
105
162
  }
106
163
  return [];
@@ -144,8 +201,17 @@ function shapeRootSpan(span) {
144
201
  const outputKey = _langfuse_tracing.LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT;
145
202
  const question = findLastMessageText(parseAttributeValue(span.attributes[inputKey]), "user");
146
203
  const answer = findLastMessageText(parseAttributeValue(span.attributes[outputKey]), "assistant");
147
- if (question != null) span.attributes[inputKey] = question;
148
- if (answer != null) span.attributes[outputKey] = answer;
204
+ /** A generation that IS the trace root — a bare `model.invoke` with no
205
+ * wrapping chain, i.e. the activity-label path — is also the only
206
+ * observation carrying its own prompt: reducing its observation input
207
+ * would discard the SystemMessage from the one place it is traced.
208
+ * Chain/agent roots keep the full reduction; their child generations
209
+ * still record the complete prompt. Trace-level input/output reduce
210
+ * either way, so the trace list keeps showing question and answer. */
211
+ if (!isGenerationSpan(span)) {
212
+ if (question != null) span.attributes[inputKey] = question;
213
+ if (answer != null) span.attributes[outputKey] = answer;
214
+ }
149
215
  const traceInput = question ?? span.attributes[inputKey];
150
216
  const traceOutput = answer ?? span.attributes[outputKey];
151
217
  if (traceInput != null) span.attributes[_langfuse_tracing.LangfuseOtelSpanAttributes.TRACE_INPUT] = traceInput;
@@ -155,6 +221,55 @@ function isGenerationSpan(span) {
155
221
  const type = span.attributes[_langfuse_tracing.LangfuseOtelSpanAttributes.OBSERVATION_TYPE];
156
222
  return typeof type === "string" && type.toLowerCase() === "generation";
157
223
  }
224
+ function isToolSpan(span) {
225
+ const type = span.attributes[_langfuse_tracing.LangfuseOtelSpanAttributes.OBSERVATION_TYPE];
226
+ return typeof type === "string" && type.toLowerCase() === TOOL_OBSERVATION_TYPE;
227
+ }
228
+ /**
229
+ * Whether the span is a LangGraph node execution whose node id is the span
230
+ * name — the shape of the outer workflow-agent node. `@langfuse/tracing`
231
+ * flattens object metadata to per-key attributes with string values stored
232
+ * raw, so LangGraph's `langgraph_node` arrives directly comparable.
233
+ */
234
+ function isWorkflowNodeSpan(span) {
235
+ return span.attributes[OBSERVATION_METADATA_LANGGRAPH_NODE] === span.name;
236
+ }
237
+ /**
238
+ * LibreChat ephemeral agents are identified as
239
+ * `endpoint__model___sender[____index]` (`__` encodes `:`; see LibreChat's
240
+ * `encodeEphemeralAgentId`), and the outer workflow node carries that id as
241
+ * its span name — so switching models renames the span (item 1). Returns the
242
+ * stable human `sender` segment only when the name matches that encoding:
243
+ * the `endpoint__model` prefix must contain both segments and — unlike
244
+ * display names such as `LibreChat Agent: Ops___EU` — never contains
245
+ * whitespace, so legitimate names that merely embed `___` are left alone.
246
+ */
247
+ function extractEphemeralAgentSender(name) {
248
+ let workingId = name;
249
+ const indexSeparator = workingId.lastIndexOf(EPHEMERAL_AGENT_INDEX_SEPARATOR);
250
+ if (indexSeparator !== -1 && /^\d+$/.test(workingId.slice(indexSeparator + 4))) workingId = workingId.slice(0, indexSeparator);
251
+ const senderSeparator = workingId.indexOf(EPHEMERAL_AGENT_SENDER_SEPARATOR);
252
+ if (senderSeparator <= 0) return;
253
+ const encodedPrefix = workingId.slice(0, senderSeparator);
254
+ if (/\s/.test(encodedPrefix)) return;
255
+ const prefixParts = encodedPrefix.split("__");
256
+ if (prefixParts.length < 2 || prefixParts.some((part) => part === "")) return;
257
+ const sender = workingId.slice(senderSeparator + 3).replace(/__/g, ":");
258
+ return sender === "" ? void 0 : sender;
259
+ }
260
+ /** Workflow-agent node spans are always children of the run's root chain and
261
+ * always carry `langgraph_node` metadata equal to their name, so host-named
262
+ * run roots (`LibreChat Agent: <name>`) and ordinary chains that merely look
263
+ * like encoded ids (`pipeline__stage___EU`) are never rename candidates.
264
+ * Successful decodes become `agent` observations, matching the inner
265
+ * `agent=<id>` node shaping. */
266
+ function shapeEphemeralAgentNodeSpan(span) {
267
+ if (isToolSpan(span) || isRootSpan(span) || !isWorkflowNodeSpan(span)) return;
268
+ const sender = extractEphemeralAgentSender(span.name);
269
+ if (sender == null) return;
270
+ span.name = sender;
271
+ span.attributes[_langfuse_tracing.LangfuseOtelSpanAttributes.OBSERVATION_TYPE] = ROOT_OBSERVATION_TYPE;
272
+ }
158
273
  function hasTraceTag(span, expectedTag) {
159
274
  const tags = parseAttributeValue(span.attributes[_langfuse_tracing.LangfuseOtelSpanAttributes.TRACE_TAGS]);
160
275
  return Array.isArray(tags) && tags.some((tag) => typeof tag === "string" && tag === expectedTag);
@@ -171,7 +286,8 @@ function shapeRootObservationType(span) {
171
286
  * Reshapes spans per Langfuse-team feedback before export:
172
287
  * - `agent=<id>` / `tools=<id>` node names carry the ephemeral agent id
173
288
  * (`provider__model`) — strip it so switching models doesn't break
174
- * name-based logic (item 1).
289
+ * name-based logic (item 1). The outer workflow node is named with the
290
+ * bare agent id; ephemeral ids reduce to their stable sender name.
175
291
  * - LLM generation spans keep the provider client class name (`ChatOpenAI`,
176
292
  * `AzureChatOpenAI`, …); rename them to a provider-agnostic `llm` so the
177
293
  * name reflects the operation, not the model (the model stays on the
@@ -188,6 +304,7 @@ function shapeLangfuseSpan(span) {
188
304
  if (mutable.name.startsWith(LANGGRAPH_AGENT_NODE_PREFIX)) shapeAgentNodeSpan(mutable);
189
305
  else if (mutable.name.startsWith(LANGGRAPH_TOOL_NODE_PREFIX)) shapeToolNodeSpan(mutable);
190
306
  else if (isGenerationSpan(mutable)) mutable.name = GENERATION_SPAN_NAME;
307
+ else shapeEphemeralAgentNodeSpan(mutable);
191
308
  if (!isRootSpan(span)) return;
192
309
  shapeRootObservationType(mutable);
193
310
  shapeRootSpan(mutable);
@@ -1 +1 @@
1
- {"version":3,"file":"langfuseTraceShaping.cjs","names":["LangfuseOtelSpanAttributes"],"sources":["../../src/langfuseTraceShaping.ts"],"sourcesContent":["import { LangfuseOtelSpanAttributes } from '@langfuse/tracing';\nimport type { ReadableSpan } from '@opentelemetry/sdk-trace-base';\n\nconst LANGGRAPH_START_NODE = '__start__';\nconst ANONYMOUS_LAMBDA_NAME = 'RunnableLambda';\nconst LANGGRAPH_AGENT_NODE_PREFIX = 'agent=';\nconst LANGGRAPH_TOOL_NODE_PREFIX = 'tools=';\nconst AGENT_NODE_SPAN_NAME = 'agent';\nconst TOOL_DISPATCH_SPAN_NAME = 'tool-dispatch';\nconst GENERATION_SPAN_NAME = 'llm';\nconst ROOT_OBSERVATION_TYPE = 'agent';\nconst CHAIN_OBSERVATION_TYPE = 'chain';\nconst AGENT_TRACE_TAG = 'agent';\nconst TITLE_TRACE_TAG = 'title';\n\ntype MutableSpan = ReadableSpan & {\n name: string;\n attributes: Record<string, unknown>;\n};\n\ntype SerializedToolCall = {\n name: string;\n args: unknown;\n id?: string;\n};\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value != null && typeof value === 'object' && !Array.isArray(value);\n}\n\nfunction parseAttributeValue(value: unknown): unknown {\n if (typeof value !== 'string') {\n return value;\n }\n const trimmed = value.trim();\n if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {\n return value;\n }\n try {\n return JSON.parse(value) as unknown;\n } catch {\n return value;\n }\n}\n\nfunction getMessageArray(\n value: unknown\n): Record<string, unknown>[] | undefined {\n if (Array.isArray(value)) {\n const records = value.filter(isRecord);\n return records.length > 0 ? records : undefined;\n }\n if (!isRecord(value)) {\n return undefined;\n }\n return (\n getMessageArray(value.messages) ??\n getMessageArray(value.input) ??\n getMessageArray(value.output)\n );\n}\n\nfunction getMessageRole(message: Record<string, unknown>): string | undefined {\n const id = message.id;\n if (Array.isArray(id)) {\n const className = id[id.length - 1];\n if (typeof className === 'string') {\n if (className.includes('Human')) {\n return 'user';\n }\n if (className.includes('AI')) {\n return 'assistant';\n }\n if (className.includes('System')) {\n return 'system';\n }\n if (className.includes('Tool')) {\n return 'tool';\n }\n }\n }\n const rawRole =\n message.type ?? message._type ?? message.role ?? message.sender;\n if (typeof rawRole !== 'string') {\n const kwargs = message.kwargs;\n return isRecord(kwargs) ? getMessageRole(kwargs) : undefined;\n }\n const normalized = rawRole.toLowerCase();\n if (normalized === 'human') {\n return 'user';\n }\n if (normalized === 'ai') {\n return 'assistant';\n }\n return normalized;\n}\n\nfunction getMessageText(message: Record<string, unknown>): string | undefined {\n const content =\n message.content ??\n (isRecord(message.kwargs) ? message.kwargs.content : undefined) ??\n (isRecord(message.data) ? message.data.content : undefined);\n if (typeof content === 'string') {\n return content;\n }\n if (!Array.isArray(content)) {\n return undefined;\n }\n const text = content\n .filter(isRecord)\n .map((part) => (typeof part.text === 'string' ? part.text : ''))\n .join('');\n return text === '' ? undefined : text;\n}\n\nfunction findLastMessageText(value: unknown, role: string): string | undefined {\n const messages = getMessageArray(value);\n if (messages == null) {\n return undefined;\n }\n for (let i = messages.length - 1; i >= 0; i--) {\n if (getMessageRole(messages[i]) !== role) {\n continue;\n }\n const text = getMessageText(messages[i]);\n if (text != null && text.trim() !== '') {\n return text;\n }\n }\n return undefined;\n}\n\nfunction normalizeToolCall(value: unknown): SerializedToolCall | undefined {\n if (!isRecord(value)) {\n return undefined;\n }\n const fn = value.function;\n if (isRecord(fn) && typeof fn.name === 'string') {\n return {\n name: fn.name,\n args: parseAttributeValue(fn.arguments),\n ...(typeof value.id === 'string' ? { id: value.id } : {}),\n };\n }\n if (typeof value.name !== 'string') {\n return undefined;\n }\n return {\n name: value.name,\n args: value.args ?? {},\n ...(typeof value.id === 'string' ? { id: value.id } : {}),\n };\n}\n\nfunction getMessageToolCalls(\n message: Record<string, unknown>\n): SerializedToolCall[] {\n const rawCalls =\n message.tool_calls ??\n (isRecord(message.kwargs) ? message.kwargs.tool_calls : undefined) ??\n (isRecord(message.additional_kwargs)\n ? message.additional_kwargs.tool_calls\n : undefined) ??\n (isRecord(message.data) ? message.data.tool_calls : undefined);\n if (!Array.isArray(rawCalls)) {\n return [];\n }\n const calls: SerializedToolCall[] = [];\n for (const rawCall of rawCalls) {\n const call = normalizeToolCall(rawCall);\n if (call != null) {\n calls.push(call);\n }\n }\n return calls;\n}\n\n/** Latest assistant turn's tool calls — the calls this tool node is executing. */\nfunction findPendingToolCalls(value: unknown): SerializedToolCall[] {\n const messages = getMessageArray(value);\n if (messages == null) {\n return [];\n }\n for (let i = messages.length - 1; i >= 0; i--) {\n if (getMessageRole(messages[i]) !== 'assistant') {\n continue;\n }\n const calls = getMessageToolCalls(messages[i]);\n if (calls.length > 0) {\n return calls;\n }\n }\n return [];\n}\n\nfunction getRootSpanParentId(span: ReadableSpan): string | undefined {\n const legacyParent = (span as { parentSpanId?: string }).parentSpanId;\n if (typeof legacyParent === 'string' && legacyParent !== '') {\n return legacyParent;\n }\n const parentContext = (span as { parentSpanContext?: { spanId?: string } })\n .parentSpanContext;\n const spanId = parentContext?.spanId;\n return typeof spanId === 'string' && spanId !== '' ? spanId : undefined;\n}\n\nfunction isRootSpan(span: ReadableSpan): boolean {\n return getRootSpanParentId(span) == null;\n}\n\n/**\n * LangGraph plumbing observations that add noise without information:\n * the duplicated `__start__` channel-seed nodes and anonymous\n * `RunnableLambda` pass-throughs (Langfuse team feedback items 4 & 5).\n * Internal ToolNode batch spans are disabled at their source so traced child\n * tools retain an exported parent. Explicitly traced ToolNodes are preserved.\n */\nexport function shouldDropLangfuseSpan(spanName: string): boolean {\n return (\n spanName === LANGGRAPH_START_NODE || spanName === ANONYMOUS_LAMBDA_NAME\n );\n}\n\nfunction shapeToolNodeSpan(span: MutableSpan): void {\n const inputKey = LangfuseOtelSpanAttributes.OBSERVATION_INPUT;\n span.name = TOOL_DISPATCH_SPAN_NAME;\n span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] =\n CHAIN_OBSERVATION_TYPE;\n const calls = findPendingToolCalls(\n parseAttributeValue(span.attributes[inputKey])\n );\n if (calls.length === 0) {\n return;\n }\n span.attributes[inputKey] = JSON.stringify(\n calls.map(({ name, args }) => ({ name, args }))\n );\n}\n\nfunction shapeAgentNodeSpan(span: MutableSpan): void {\n span.name = AGENT_NODE_SPAN_NAME;\n span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] =\n ROOT_OBSERVATION_TYPE;\n}\n\nfunction shapeRootSpan(span: MutableSpan): void {\n const inputKey = LangfuseOtelSpanAttributes.OBSERVATION_INPUT;\n const outputKey = LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT;\n const question = findLastMessageText(\n parseAttributeValue(span.attributes[inputKey]),\n 'user'\n );\n const answer = findLastMessageText(\n parseAttributeValue(span.attributes[outputKey]),\n 'assistant'\n );\n if (question != null) {\n span.attributes[inputKey] = question;\n }\n if (answer != null) {\n span.attributes[outputKey] = answer;\n }\n const traceInput = question ?? span.attributes[inputKey];\n const traceOutput = answer ?? span.attributes[outputKey];\n if (traceInput != null) {\n span.attributes[LangfuseOtelSpanAttributes.TRACE_INPUT] = traceInput;\n }\n if (traceOutput != null) {\n span.attributes[LangfuseOtelSpanAttributes.TRACE_OUTPUT] = traceOutput;\n }\n}\n\nfunction isGenerationSpan(span: MutableSpan): boolean {\n const type = span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE];\n return typeof type === 'string' && type.toLowerCase() === 'generation';\n}\n\nfunction hasTraceTag(span: MutableSpan, expectedTag: string): boolean {\n const tags = parseAttributeValue(\n span.attributes[LangfuseOtelSpanAttributes.TRACE_TAGS]\n );\n return (\n Array.isArray(tags) &&\n tags.some((tag) => typeof tag === 'string' && tag === expectedTag)\n );\n}\n\nfunction shapeRootObservationType(span: MutableSpan): void {\n if (isGenerationSpan(span)) {\n return;\n }\n if (hasTraceTag(span, AGENT_TRACE_TAG)) {\n span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] =\n ROOT_OBSERVATION_TYPE;\n return;\n }\n if (hasTraceTag(span, TITLE_TRACE_TAG)) {\n span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] =\n CHAIN_OBSERVATION_TYPE;\n }\n}\n\n/**\n * Reshapes spans per Langfuse-team feedback before export:\n * - `agent=<id>` / `tools=<id>` node names carry the ephemeral agent id\n * (`provider__model`) — strip it so switching models doesn't break\n * name-based logic (item 1).\n * - LLM generation spans keep the provider client class name (`ChatOpenAI`,\n * `AzureChatOpenAI`, …); rename them to a provider-agnostic `llm` so the\n * name reflects the operation, not the model (the model stays on the\n * generation's model attribute).\n * - Agent nodes become `agent` observations, while tool-dispatch nodes become\n * stable `chain` observations whose input is scoped to the pending calls.\n * Individual child calls remain `tool` observations (items 3 & 4).\n * - Agent trace roots become `agent` observations and title trace roots become\n * `chain` observations. Root and trace input/output are reduced to the user\n * question and assistant response when chat messages are available (item 2).\n */\nexport function shapeLangfuseSpan(span: ReadableSpan): void {\n const mutable = span as MutableSpan;\n if (mutable.name.startsWith(LANGGRAPH_AGENT_NODE_PREFIX)) {\n shapeAgentNodeSpan(mutable);\n } else if (mutable.name.startsWith(LANGGRAPH_TOOL_NODE_PREFIX)) {\n shapeToolNodeSpan(mutable);\n } else if (isGenerationSpan(mutable)) {\n mutable.name = GENERATION_SPAN_NAME;\n }\n if (!isRootSpan(span)) {\n return;\n }\n shapeRootObservationType(mutable);\n shapeRootSpan(mutable);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"langfuseTraceShaping.cjs","names":["LangfuseOtelSpanAttributes"],"sources":["../../src/langfuseTraceShaping.ts"],"sourcesContent":["import { LangfuseOtelSpanAttributes } from '@langfuse/tracing';\nimport type { ReadableSpan } from '@opentelemetry/sdk-trace-base';\nimport { Constants } from '@/common';\n\nconst LANGGRAPH_START_NODE = '__start__';\nconst ANONYMOUS_LAMBDA_NAME = 'RunnableLambda';\nconst LANGGRAPH_AGENT_NODE_PREFIX = 'agent=';\nconst LANGGRAPH_TOOL_NODE_PREFIX = 'tools=';\nconst AGENT_NODE_SPAN_NAME = 'agent';\nconst TOOL_DISPATCH_SPAN_NAME = 'tool-dispatch';\nconst GENERATION_SPAN_NAME = 'llm';\nconst ROOT_OBSERVATION_TYPE = 'agent';\nconst CHAIN_OBSERVATION_TYPE = 'chain';\nconst TOOL_OBSERVATION_TYPE = 'tool';\nconst AGENT_TRACE_TAG = 'agent';\nconst TITLE_TRACE_TAG = 'title';\nconst EPHEMERAL_AGENT_SENDER_SEPARATOR = '___';\nconst EPHEMERAL_AGENT_INDEX_SEPARATOR = '____';\nconst OBSERVATION_METADATA_LANGGRAPH_NODE = `${LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.langgraph_node`;\n\ntype MutableSpan = ReadableSpan & {\n name: string;\n attributes: Record<string, unknown>;\n};\n\ntype SerializedToolCall = {\n name: string;\n args: unknown;\n id?: string;\n};\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value != null && typeof value === 'object' && !Array.isArray(value);\n}\n\nfunction parseAttributeValue(value: unknown): unknown {\n if (typeof value !== 'string') {\n return value;\n }\n const trimmed = value.trim();\n if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {\n return value;\n }\n try {\n return JSON.parse(value) as unknown;\n } catch {\n return value;\n }\n}\n\nfunction getMessageArray(\n value: unknown\n): Record<string, unknown>[] | undefined {\n if (Array.isArray(value)) {\n const records = value.filter(isRecord);\n return records.length > 0 ? records : undefined;\n }\n if (!isRecord(value)) {\n return undefined;\n }\n return (\n getMessageArray(value.messages) ??\n getMessageArray(value.input) ??\n getMessageArray(value.output)\n );\n}\n\nfunction getMessageRole(message: Record<string, unknown>): string | undefined {\n const id = message.id;\n if (Array.isArray(id)) {\n const className = id[id.length - 1];\n if (typeof className === 'string') {\n if (className.includes('Human')) {\n return 'user';\n }\n if (className.includes('AI')) {\n return 'assistant';\n }\n if (className.includes('System')) {\n return 'system';\n }\n if (className.includes('Tool')) {\n return 'tool';\n }\n }\n }\n const rawRole =\n message.type ?? message._type ?? message.role ?? message.sender;\n if (typeof rawRole !== 'string') {\n const kwargs = message.kwargs;\n return isRecord(kwargs) ? getMessageRole(kwargs) : undefined;\n }\n const normalized = rawRole.toLowerCase();\n if (normalized === 'human') {\n return 'user';\n }\n if (normalized === 'ai') {\n return 'assistant';\n }\n return normalized;\n}\n\nfunction getMessageText(message: Record<string, unknown>): string | undefined {\n const content =\n message.content ??\n (isRecord(message.kwargs) ? message.kwargs.content : undefined) ??\n (isRecord(message.data) ? message.data.content : undefined);\n if (typeof content === 'string') {\n return content;\n }\n if (!Array.isArray(content)) {\n return undefined;\n }\n const text = content\n .filter(isRecord)\n .map((part) => (typeof part.text === 'string' ? part.text : ''))\n .join('');\n return text === '' ? undefined : text;\n}\n\nfunction findLastMessageText(value: unknown, role: string): string | undefined {\n const messages = getMessageArray(value);\n if (messages == null) {\n return undefined;\n }\n for (let i = messages.length - 1; i >= 0; i--) {\n if (getMessageRole(messages[i]) !== role) {\n continue;\n }\n const text = getMessageText(messages[i]);\n if (text != null && text.trim() !== '') {\n return text;\n }\n }\n return undefined;\n}\n\nfunction normalizeToolCall(value: unknown): SerializedToolCall | undefined {\n if (!isRecord(value)) {\n return undefined;\n }\n const fn = value.function;\n if (isRecord(fn) && typeof fn.name === 'string') {\n return {\n name: fn.name,\n args: parseAttributeValue(fn.arguments),\n ...(typeof value.id === 'string' ? { id: value.id } : {}),\n };\n }\n if (typeof value.name !== 'string') {\n return undefined;\n }\n return {\n name: value.name,\n args: value.args ?? {},\n ...(typeof value.id === 'string' ? { id: value.id } : {}),\n };\n}\n\nfunction getMessageToolCalls(\n message: Record<string, unknown>\n): SerializedToolCall[] {\n const rawCalls =\n message.tool_calls ??\n (isRecord(message.kwargs) ? message.kwargs.tool_calls : undefined) ??\n (isRecord(message.additional_kwargs)\n ? message.additional_kwargs.tool_calls\n : undefined) ??\n (isRecord(message.data) ? message.data.tool_calls : undefined);\n if (!Array.isArray(rawCalls)) {\n return [];\n }\n const calls: SerializedToolCall[] = [];\n for (const rawCall of rawCalls) {\n const call = normalizeToolCall(rawCall);\n if (call != null) {\n calls.push(call);\n }\n }\n return calls;\n}\n\n/**\n * Id-bearing `invalid_tool_calls` on the assistant turn. ToolNode pairs these\n * with synthesized error results (and an invalid-only turn is routed on them\n * alone), so the tool-dispatch span must count them as part of the executing\n * batch — otherwise an invalid-only dispatch finds zero calls and the span\n * keeps the full serialized graph state as its input, and a mixed dispatch\n * silently omits the malformed call. `args` stays the raw unparsed string.\n */\n/** Tool-result ids present in the serialized state — ToolNode's\n * `!toolMessageIds.has(id)` execution filter, mirrored for the span. */\nfunction getToolResultIds(\n messages: Record<string, unknown>[]\n): Set<string> {\n const ids = new Set<string>();\n for (const message of messages) {\n if (getMessageRole(message) !== 'tool') {\n continue;\n }\n const rawId =\n message.tool_call_id ??\n (isRecord(message.kwargs) ? message.kwargs.tool_call_id : undefined) ??\n (isRecord(message.data) ? message.data.tool_call_id : undefined);\n if (typeof rawId === 'string' && rawId !== '') {\n ids.add(rawId);\n }\n }\n return ids;\n}\n\nfunction getMessageInvalidToolCalls(\n message: Record<string, unknown>,\n answeredIds: ReadonlySet<string>\n): SerializedToolCall[] {\n const rawCalls =\n message.invalid_tool_calls ??\n (isRecord(message.kwargs) ? message.kwargs.invalid_tool_calls : undefined) ??\n (isRecord(message.data) ? message.data.invalid_tool_calls : undefined);\n if (!Array.isArray(rawCalls)) {\n return [];\n }\n const calls: SerializedToolCall[] = [];\n for (const rawCall of rawCalls) {\n // Same attribution predicate ToolNode executes with (id-bearing,\n // non-server) so the span never claims calls the node deliberately skips.\n if (\n !isRecord(rawCall) ||\n typeof rawCall.id !== 'string' ||\n rawCall.id === '' ||\n answeredIds.has(rawCall.id) ||\n rawCall.id.startsWith(Constants.ANTHROPIC_SERVER_TOOL_PREFIX)\n ) {\n continue;\n }\n // Same name fallback ToolNode synthesizes with — a nameless attributable\n // call still gets a result, so it must still count in the span input.\n const call = normalizeToolCall(\n typeof rawCall.name === 'string' && rawCall.name !== ''\n ? rawCall\n : { ...rawCall, name: 'unknown' }\n );\n if (call != null) {\n calls.push(call);\n }\n }\n return calls;\n}\n\n/** The serialized message's own id (uuid), NOT the LC-serialization type id\n * array that `message.id` carries in constructor dumps. */\nfunction getSerializedMessageId(\n message: Record<string, unknown>\n): string | undefined {\n const kwargsId = isRecord(message.kwargs) ? message.kwargs.id : undefined;\n const dataId = isRecord(message.data) ? message.data.id : undefined;\n const rawId = message.id;\n const id = kwargsId ?? dataId ?? rawId;\n return typeof id === 'string' && id !== '' ? id : undefined;\n}\n\n/** Latest assistant turn's tool calls — the calls this tool node is executing. */\nfunction findPendingToolCalls(value: unknown): SerializedToolCall[] {\n const messages = getMessageArray(value);\n if (messages == null) {\n return [];\n }\n /**\n * Invalid calls count only where ToolNode's own gate lets them execute:\n * the messages-state form (a bare-array state means the node returns a\n * plain output list and skips invalid handling) with an id-bearing\n * assistant message (no id, no reducer upsert). Mirrors\n * `canPromoteInvalidCalls` so the span never reports skipped calls.\n */\n const invalidCallsApply = !Array.isArray(value);\n const answeredIds = invalidCallsApply\n ? getToolResultIds(messages)\n : undefined;\n for (let i = messages.length - 1; i >= 0; i--) {\n if (getMessageRole(messages[i]) !== 'assistant') {\n continue;\n }\n const calls = [\n ...getMessageToolCalls(messages[i]),\n ...(invalidCallsApply && getSerializedMessageId(messages[i]) != null\n ? getMessageInvalidToolCalls(messages[i], answeredIds!)\n : []),\n ];\n if (calls.length > 0) {\n return calls;\n }\n }\n return [];\n}\n\nfunction getRootSpanParentId(span: ReadableSpan): string | undefined {\n const legacyParent = (span as { parentSpanId?: string }).parentSpanId;\n if (typeof legacyParent === 'string' && legacyParent !== '') {\n return legacyParent;\n }\n const parentContext = (span as { parentSpanContext?: { spanId?: string } })\n .parentSpanContext;\n const spanId = parentContext?.spanId;\n return typeof spanId === 'string' && spanId !== '' ? spanId : undefined;\n}\n\nfunction isRootSpan(span: ReadableSpan): boolean {\n return getRootSpanParentId(span) == null;\n}\n\n/**\n * LangGraph plumbing observations that add noise without information:\n * the duplicated `__start__` channel-seed nodes and anonymous\n * `RunnableLambda` pass-throughs (Langfuse team feedback items 4 & 5).\n * Internal ToolNode batch spans are disabled at their source so traced child\n * tools retain an exported parent. Explicitly traced ToolNodes are preserved.\n */\nexport function shouldDropLangfuseSpan(spanName: string): boolean {\n return (\n spanName === LANGGRAPH_START_NODE || spanName === ANONYMOUS_LAMBDA_NAME\n );\n}\n\nfunction shapeToolNodeSpan(span: MutableSpan): void {\n const inputKey = LangfuseOtelSpanAttributes.OBSERVATION_INPUT;\n span.name = TOOL_DISPATCH_SPAN_NAME;\n span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] =\n CHAIN_OBSERVATION_TYPE;\n const calls = findPendingToolCalls(\n parseAttributeValue(span.attributes[inputKey])\n );\n if (calls.length === 0) {\n return;\n }\n span.attributes[inputKey] = JSON.stringify(\n calls.map(({ name, args }) => ({ name, args }))\n );\n}\n\nfunction shapeAgentNodeSpan(span: MutableSpan): void {\n span.name = AGENT_NODE_SPAN_NAME;\n span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] =\n ROOT_OBSERVATION_TYPE;\n}\n\nfunction shapeRootSpan(span: MutableSpan): void {\n const inputKey = LangfuseOtelSpanAttributes.OBSERVATION_INPUT;\n const outputKey = LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT;\n const question = findLastMessageText(\n parseAttributeValue(span.attributes[inputKey]),\n 'user'\n );\n const answer = findLastMessageText(\n parseAttributeValue(span.attributes[outputKey]),\n 'assistant'\n );\n /** A generation that IS the trace root — a bare `model.invoke` with no\n * wrapping chain, i.e. the activity-label path — is also the only\n * observation carrying its own prompt: reducing its observation input\n * would discard the SystemMessage from the one place it is traced.\n * Chain/agent roots keep the full reduction; their child generations\n * still record the complete prompt. Trace-level input/output reduce\n * either way, so the trace list keeps showing question and answer. */\n if (!isGenerationSpan(span)) {\n if (question != null) {\n span.attributes[inputKey] = question;\n }\n if (answer != null) {\n span.attributes[outputKey] = answer;\n }\n }\n const traceInput = question ?? span.attributes[inputKey];\n const traceOutput = answer ?? span.attributes[outputKey];\n if (traceInput != null) {\n span.attributes[LangfuseOtelSpanAttributes.TRACE_INPUT] = traceInput;\n }\n if (traceOutput != null) {\n span.attributes[LangfuseOtelSpanAttributes.TRACE_OUTPUT] = traceOutput;\n }\n}\n\nfunction isGenerationSpan(span: MutableSpan): boolean {\n const type = span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE];\n return typeof type === 'string' && type.toLowerCase() === 'generation';\n}\n\nfunction isToolSpan(span: MutableSpan): boolean {\n const type = span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE];\n return (\n typeof type === 'string' && type.toLowerCase() === TOOL_OBSERVATION_TYPE\n );\n}\n\n/**\n * Whether the span is a LangGraph node execution whose node id is the span\n * name — the shape of the outer workflow-agent node. `@langfuse/tracing`\n * flattens object metadata to per-key attributes with string values stored\n * raw, so LangGraph's `langgraph_node` arrives directly comparable.\n */\nfunction isWorkflowNodeSpan(span: MutableSpan): boolean {\n return span.attributes[OBSERVATION_METADATA_LANGGRAPH_NODE] === span.name;\n}\n\n/**\n * LibreChat ephemeral agents are identified as\n * `endpoint__model___sender[____index]` (`__` encodes `:`; see LibreChat's\n * `encodeEphemeralAgentId`), and the outer workflow node carries that id as\n * its span name — so switching models renames the span (item 1). Returns the\n * stable human `sender` segment only when the name matches that encoding:\n * the `endpoint__model` prefix must contain both segments and — unlike\n * display names such as `LibreChat Agent: Ops___EU` — never contains\n * whitespace, so legitimate names that merely embed `___` are left alone.\n */\nfunction extractEphemeralAgentSender(name: string): string | undefined {\n let workingId = name;\n const indexSeparator = workingId.lastIndexOf(EPHEMERAL_AGENT_INDEX_SEPARATOR);\n if (\n indexSeparator !== -1 &&\n /^\\d+$/.test(\n workingId.slice(indexSeparator + EPHEMERAL_AGENT_INDEX_SEPARATOR.length)\n )\n ) {\n workingId = workingId.slice(0, indexSeparator);\n }\n const senderSeparator = workingId.indexOf(EPHEMERAL_AGENT_SENDER_SEPARATOR);\n if (senderSeparator <= 0) {\n return undefined;\n }\n const encodedPrefix = workingId.slice(0, senderSeparator);\n if (/\\s/.test(encodedPrefix)) {\n return undefined;\n }\n const prefixParts = encodedPrefix.split('__');\n if (prefixParts.length < 2 || prefixParts.some((part) => part === '')) {\n return undefined;\n }\n const sender = workingId\n .slice(senderSeparator + EPHEMERAL_AGENT_SENDER_SEPARATOR.length)\n .replace(/__/g, ':');\n return sender === '' ? undefined : sender;\n}\n\n/** Workflow-agent node spans are always children of the run's root chain and\n * always carry `langgraph_node` metadata equal to their name, so host-named\n * run roots (`LibreChat Agent: <name>`) and ordinary chains that merely look\n * like encoded ids (`pipeline__stage___EU`) are never rename candidates.\n * Successful decodes become `agent` observations, matching the inner\n * `agent=<id>` node shaping. */\nfunction shapeEphemeralAgentNodeSpan(span: MutableSpan): void {\n if (isToolSpan(span) || isRootSpan(span) || !isWorkflowNodeSpan(span)) {\n return;\n }\n const sender = extractEphemeralAgentSender(span.name);\n if (sender == null) {\n return;\n }\n span.name = sender;\n span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] =\n ROOT_OBSERVATION_TYPE;\n}\n\nfunction hasTraceTag(span: MutableSpan, expectedTag: string): boolean {\n const tags = parseAttributeValue(\n span.attributes[LangfuseOtelSpanAttributes.TRACE_TAGS]\n );\n return (\n Array.isArray(tags) &&\n tags.some((tag) => typeof tag === 'string' && tag === expectedTag)\n );\n}\n\nfunction shapeRootObservationType(span: MutableSpan): void {\n if (isGenerationSpan(span)) {\n return;\n }\n if (hasTraceTag(span, AGENT_TRACE_TAG)) {\n span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] =\n ROOT_OBSERVATION_TYPE;\n return;\n }\n if (hasTraceTag(span, TITLE_TRACE_TAG)) {\n span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] =\n CHAIN_OBSERVATION_TYPE;\n }\n}\n\n/**\n * Reshapes spans per Langfuse-team feedback before export:\n * - `agent=<id>` / `tools=<id>` node names carry the ephemeral agent id\n * (`provider__model`) — strip it so switching models doesn't break\n * name-based logic (item 1). The outer workflow node is named with the\n * bare agent id; ephemeral ids reduce to their stable sender name.\n * - LLM generation spans keep the provider client class name (`ChatOpenAI`,\n * `AzureChatOpenAI`, …); rename them to a provider-agnostic `llm` so the\n * name reflects the operation, not the model (the model stays on the\n * generation's model attribute).\n * - Agent nodes become `agent` observations, while tool-dispatch nodes become\n * stable `chain` observations whose input is scoped to the pending calls.\n * Individual child calls remain `tool` observations (items 3 & 4).\n * - Agent trace roots become `agent` observations and title trace roots become\n * `chain` observations. Root and trace input/output are reduced to the user\n * question and assistant response when chat messages are available (item 2).\n */\nexport function shapeLangfuseSpan(span: ReadableSpan): void {\n const mutable = span as MutableSpan;\n if (mutable.name.startsWith(LANGGRAPH_AGENT_NODE_PREFIX)) {\n shapeAgentNodeSpan(mutable);\n } else if (mutable.name.startsWith(LANGGRAPH_TOOL_NODE_PREFIX)) {\n shapeToolNodeSpan(mutable);\n } else if (isGenerationSpan(mutable)) {\n mutable.name = GENERATION_SPAN_NAME;\n } else {\n shapeEphemeralAgentNodeSpan(mutable);\n }\n if (!isRootSpan(span)) {\n return;\n }\n shapeRootObservationType(mutable);\n shapeRootSpan(mutable);\n}\n"],"mappings":"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"}
@@ -178,6 +178,29 @@ function _ensureMessageContents(messages) {
178
178
  else updatedMsgs.push(message);
179
179
  return updatedMsgs;
180
180
  }
181
+ /**
182
+ * Anthropic requires `tool_use.input` to be a JSON object; anything else is
183
+ * rejected with a 400 (`tool_use.input: Input should be an object`).
184
+ * History can carry non-object inputs: streaming leaves the raw partial-JSON
185
+ * string on the content block, and upstream context projections can leave
186
+ * `null` (observed live on a summarization-retained tool call replayed after
187
+ * compaction). A string is parsed when it forms complete JSON; every other
188
+ * shape degrades to `{}` — the call already executed, so the replayed input
189
+ * is informational and an empty object is the safe representation.
190
+ */
191
+ /** True for `{}` — no own enumerable keys on a non-array, non-null object. */
192
+ function isEmptyPlainObject(value) {
193
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0;
194
+ }
195
+ function coerceAnthropicToolUseInput(input) {
196
+ let candidate = input;
197
+ if (typeof candidate === "string") try {
198
+ candidate = JSON.parse(candidate);
199
+ } catch {
200
+ return {};
201
+ }
202
+ return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) ? candidate : {};
203
+ }
181
204
  function _convertLangChainToolCallToAnthropic(toolCall) {
182
205
  if (toolCall.id === void 0) throw new Error("Anthropic requires all tool calls to have an \"id\".");
183
206
  const isServerTool = toolCall.id.startsWith("srvtoolu_");
@@ -185,7 +208,7 @@ function _convertLangChainToolCallToAnthropic(toolCall) {
185
208
  type: isServerTool ? "server_tool_use" : "tool_use",
186
209
  id: isServerTool ? toolCall.id : normalizeAnthropicToolCallId(toolCall.id),
187
210
  name: toolCall.name,
188
- input: toolCall.args
211
+ input: coerceAnthropicToolUseInput(toolCall.args)
189
212
  };
190
213
  }
191
214
  const standardContentBlockConverter = {
@@ -344,17 +367,11 @@ function _formatContent(message) {
344
367
  */
345
368
  if ("id" in contentPart && typeof contentPart.id === "string" && contentPart.id.startsWith("srvtoolu_") && "name" in contentPart) {
346
369
  const rawPart = contentPart;
347
- let input = rawPart.input ?? rawPart.args;
348
- if (typeof input === "string") try {
349
- input = JSON.parse(input);
350
- } catch {
351
- input = {};
352
- }
353
370
  return {
354
371
  type: "server_tool_use",
355
372
  id: rawPart.id,
356
373
  name: rawPart.name ?? "web_search",
357
- input: input ?? {}
374
+ input: coerceAnthropicToolUseInput(rawPart.input ?? rawPart.args)
358
375
  };
359
376
  }
360
377
  /**
@@ -468,7 +485,7 @@ function _formatContent(message) {
468
485
  if ("index" in contentPartCopy) delete contentPartCopy.index;
469
486
  if (contentPartCopy.type === "input_json_delta") return null;
470
487
  if (contentPartCopy.type === "tool_use" && "id" in contentPartCopy && typeof contentPartCopy.id === "string" && contentPartCopy.id.startsWith("srvtoolu_")) contentPartCopy.type = "server_tool_use";
471
- if (contentPartCopy.type === "tool_use" && typeof contentPartCopy.id === "string" && (contentPartCopy.input === "" || contentPartCopy.input == null)) {
488
+ if (contentPartCopy.type === "tool_use" && typeof contentPartCopy.id === "string" && (contentPartCopy.input === "" || contentPartCopy.input == null || isEmptyPlainObject(contentPartCopy.input))) {
472
489
  const matchingToolCall = (0, _langchain_core_messages.isAIMessage)(message) ? message.tool_calls?.find((toolCall) => toolCall.id === contentPartCopy.id) : void 0;
473
490
  if (matchingToolCall) contentPartCopy.input = matchingToolCall.args;
474
491
  else {
@@ -480,12 +497,19 @@ function _formatContent(message) {
480
497
  if (merged !== "") contentPartCopy.input = merged;
481
498
  }
482
499
  }
483
- if ("input" in contentPartCopy) {
484
- if (typeof contentPartCopy.input === "string") try {
485
- contentPartCopy.input = JSON.parse(contentPartCopy.input);
486
- } catch {
487
- contentPartCopy.input = {};
488
- }
500
+ if (contentPartCopy.type === "tool_use" || contentPartCopy.type === "server_tool_use")
501
+ /**
502
+ * The API requires `input` to be a JSON object. Streaming leaves the
503
+ * raw partial-JSON string here, and context-pressure truncation can
504
+ * leave `null` on BOTH the block and its `tool_calls` mirror — so
505
+ * the restore above may itself restore a non-object. Coerce last,
506
+ * after every restore path has run.
507
+ */
508
+ contentPartCopy.input = coerceAnthropicToolUseInput("input" in contentPartCopy ? contentPartCopy.input : void 0);
509
+ else if ("input" in contentPartCopy && typeof contentPartCopy.input === "string") try {
510
+ contentPartCopy.input = JSON.parse(contentPartCopy.input);
511
+ } catch {
512
+ contentPartCopy.input = {};
489
513
  }
490
514
  /**
491
515
  * For multi-turn conversations with citations, we must preserve ALL blocks