@librechat/agents 3.3.8 → 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.
- package/dist/cjs/graphs/Graph.cjs +47 -13
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +35 -2
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/hitl/askUserQuestion.cjs +3 -2
- package/dist/cjs/hitl/askUserQuestion.cjs.map +1 -1
- package/dist/cjs/instrumentation.cjs +18 -48
- package/dist/cjs/instrumentation.cjs.map +1 -1
- package/dist/cjs/langfuse.cjs +174 -29
- package/dist/cjs/langfuse.cjs.map +1 -1
- package/dist/cjs/langfuseConfig.cjs +12 -0
- package/dist/cjs/langfuseConfig.cjs.map +1 -1
- package/dist/cjs/langfuseRuntimeContext.cjs +23 -2
- package/dist/cjs/langfuseRuntimeContext.cjs.map +1 -1
- package/dist/cjs/langfuseRuntimeScope.cjs +39 -8
- package/dist/cjs/langfuseRuntimeScope.cjs.map +1 -1
- package/dist/cjs/langfuseSpanRegistry.cjs +95 -0
- package/dist/cjs/langfuseSpanRegistry.cjs.map +1 -0
- package/dist/cjs/langfuseTraceShaping.cjs +121 -4
- package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +39 -15
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +25 -5
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/init.cjs +3 -3
- package/dist/cjs/llm/invoke.cjs +5 -5
- package/dist/cjs/llm/openai/index.cjs +1 -1
- package/dist/cjs/main.cjs +10 -10
- package/dist/cjs/messages/prune.cjs +13 -1
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/prompts/activityLabel.cjs +24 -12
- package/dist/cjs/prompts/activityLabel.cjs.map +1 -1
- package/dist/cjs/run.cjs +47 -22
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/messageSerialization.cjs +6 -0
- package/dist/cjs/session/messageSerialization.cjs.map +1 -1
- package/dist/cjs/stream.cjs +21 -10
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +5 -0
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +253 -24
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/handlers.cjs +1 -1
- package/dist/cjs/tools/search/tool.cjs +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -1
- package/dist/cjs/utils/index.cjs +2 -2
- package/dist/esm/graphs/Graph.mjs +48 -14
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +35 -2
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/hitl/askUserQuestion.mjs +3 -2
- package/dist/esm/hitl/askUserQuestion.mjs.map +1 -1
- package/dist/esm/instrumentation.mjs +18 -48
- package/dist/esm/instrumentation.mjs.map +1 -1
- package/dist/esm/langfuse.mjs +176 -28
- package/dist/esm/langfuse.mjs.map +1 -1
- package/dist/esm/langfuseConfig.mjs +10 -1
- package/dist/esm/langfuseConfig.mjs.map +1 -1
- package/dist/esm/langfuseRuntimeContext.mjs +21 -3
- package/dist/esm/langfuseRuntimeContext.mjs.map +1 -1
- package/dist/esm/langfuseRuntimeScope.mjs +39 -10
- package/dist/esm/langfuseRuntimeScope.mjs.map +1 -1
- package/dist/esm/langfuseSpanRegistry.mjs +91 -0
- package/dist/esm/langfuseSpanRegistry.mjs.map +1 -0
- package/dist/esm/langfuseTraceShaping.mjs +121 -4
- package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +39 -15
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +25 -5
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/init.mjs +2 -2
- package/dist/esm/llm/invoke.mjs +5 -5
- package/dist/esm/llm/openai/index.mjs +1 -1
- package/dist/esm/main.mjs +8 -8
- package/dist/esm/messages/prune.mjs +13 -1
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/prompts/activityLabel.mjs +24 -12
- package/dist/esm/prompts/activityLabel.mjs.map +1 -1
- package/dist/esm/run.mjs +47 -22
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/messageSerialization.mjs +6 -0
- package/dist/esm/session/messageSerialization.mjs.map +1 -1
- package/dist/esm/stream.mjs +21 -10
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +5 -0
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +254 -25
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/handlers.mjs +1 -1
- package/dist/esm/tools/search/tool.mjs +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -1
- package/dist/esm/utils/index.mjs +2 -2
- package/dist/types/graphs/Graph.d.ts +19 -0
- package/dist/types/hitl/askUserQuestion.d.ts +11 -1
- package/dist/types/langfuse.d.ts +16 -8
- package/dist/types/langfuseConfig.d.ts +6 -0
- package/dist/types/langfuseRuntimeContext.d.ts +27 -1
- package/dist/types/langfuseRuntimeScope.d.ts +17 -2
- package/dist/types/langfuseSpanRegistry.d.ts +17 -0
- package/dist/types/langfuseTraceShaping.d.ts +2 -1
- package/dist/types/llm/anthropic/utils/message_inputs.d.ts +1 -0
- package/dist/types/session/types.d.ts +1 -0
- package/dist/types/tools/ToolNode.d.ts +7 -1
- package/dist/types/types/hitl.d.ts +8 -0
- package/dist/types/types/tools.d.ts +30 -0
- package/package.json +7 -4
- package/src/__tests__/stream.eagerArgsDivergence.test.ts +753 -0
- package/src/graphs/Graph.ts +69 -20
- package/src/graphs/MultiAgentGraph.ts +56 -2
- package/src/graphs/__tests__/composition.smoke.test.ts +4 -0
- package/src/hitl/askUserQuestion.ts +14 -1
- package/src/instrumentation.ts +35 -77
- package/src/langfuse.ts +320 -43
- package/src/langfuseConfig.ts +24 -0
- package/src/langfuseRuntimeContext.ts +43 -1
- package/src/langfuseRuntimeScope.ts +94 -21
- package/src/langfuseSpanRegistry.ts +131 -0
- package/src/langfuseTraceShaping.ts +194 -7
- package/src/llm/anthropic/utils/message_inputs.ts +70 -19
- package/src/llm/anthropic/utils/streaming-tool-input.test.ts +186 -11
- package/src/llm/bedrock/utils/message_inputs.test.ts +120 -4
- package/src/llm/bedrock/utils/message_inputs.ts +32 -7
- package/src/messages/prune.ts +12 -1
- package/src/prompts/activityLabel.ts +23 -6
- package/src/run.ts +76 -45
- package/src/scripts/activity-labels/captured.json +56 -0
- package/src/scripts/activity-labels/checks.cjs +205 -0
- package/src/scripts/activity-labels/corpus.cjs +473 -0
- package/src/scripts/activity-labels/report.cjs +203 -0
- package/src/scripts/activity-labels/rescore.cjs +102 -0
- package/src/scripts/activity-labels/run.ts +705 -0
- package/src/scripts/activity-labels/variants.ts +71 -0
- package/src/session/messageSerialization.ts +12 -1
- package/src/session/types.ts +1 -0
- package/src/specs/activity-label-prompt.test.ts +26 -10
- package/src/specs/agent-handoffs.test.ts +306 -0
- package/src/specs/langfuse-callbacks.test.ts +456 -0
- package/src/specs/langfuse-routing.integration.test.ts +138 -1
- package/src/specs/langfuse-span-registry.test.ts +70 -0
- package/src/specs/langfuse-trace-shaping.test.ts +294 -0
- package/src/specs/prune.test.ts +38 -1
- package/src/stream.ts +70 -6
- package/src/summarization/node.ts +5 -0
- package/src/tools/ToolNode.ts +400 -9
- package/src/tools/__tests__/ToolNode.invalidToolCalls.test.ts +757 -0
- package/src/tools/__tests__/hitl.test.ts +58 -0
- package/src/types/hitl.ts +8 -0
- package/src/types/tools.ts +35 -1
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { getLangfuseRuntimeConfig, getLangfuseRuntimeToolOutputTracingConfig, getTraceIdSeed, hasLangfuseRuntimeContextValue, runWithLangfuseRuntimeContext } from "./langfuseRuntimeContext.mjs";
|
|
1
|
+
import { getLangfuseRuntimeConfig, getLangfuseRuntimeToolOutputTracingConfig, getLangfuseScopeAgentId, getLangfuseScopeRunId, getTraceIdSeed, hasLangfuseRuntimeContextValue, replaceLangfuseRuntimeContext, runWithLangfuseRuntimeContext } from "./langfuseRuntimeContext.mjs";
|
|
2
2
|
import { hasToolOutputTracingConfig, resolveLangfuseConfig, resolveToolOutputTracingConfig } from "./langfuseConfig.mjs";
|
|
3
3
|
import { context, createContextKey } from "@opentelemetry/api";
|
|
4
4
|
//#region src/langfuseRuntimeScope.ts
|
|
5
5
|
const langfuseToolOutputTracingConfigKey = createContextKey("librechat.langfuse.tool-output-tracing");
|
|
6
6
|
const langfuseConfigKey = createContextKey("librechat.langfuse.config");
|
|
7
7
|
const langfuseTraceIdSeedKey = createContextKey("librechat.langfuse.trace-id-seed");
|
|
8
|
+
const langfuseScopeRunIdKey = createContextKey("librechat.langfuse.run-id");
|
|
9
|
+
const langfuseScopeAgentIdKey = createContextKey("librechat.langfuse.agent-id");
|
|
8
10
|
function isRecord(value) {
|
|
9
11
|
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
10
12
|
}
|
|
@@ -29,25 +31,52 @@ function resolveLangfuseConfigForSpan(activeContext) {
|
|
|
29
31
|
function resolveTraceIdSeedForSpan(activeContext) {
|
|
30
32
|
return 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 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 getLangfuseScopeAgentId() ?? getOtelScopeAgentId(activeContext);
|
|
51
|
+
}
|
|
32
52
|
function resolveToolOutputTracingConfigForSpan(activeContext) {
|
|
33
53
|
return getLangfuseRuntimeToolOutputTracingConfig() ?? getOtelToolOutputTracingConfig(activeContext);
|
|
34
54
|
}
|
|
35
|
-
function
|
|
36
|
-
if (
|
|
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 && !hasLangfuseRuntimeContextValue(scope)) return action();
|
|
37
62
|
let activeContext = context.active();
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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 ? replaceLangfuseRuntimeContext : runWithLangfuseRuntimeContext)(scope, () => 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: resolveLangfuseConfig(runLangfuse, langfuseOverlay),
|
|
46
73
|
traceIdSeed,
|
|
47
|
-
toolOutputTracing: !hasToolOutputTracingConfig(runLangfuse, langfuseOverlay) ? void 0 : resolveToolOutputTracingConfig(runLangfuse, langfuseOverlay)
|
|
74
|
+
toolOutputTracing: !hasToolOutputTracingConfig(runLangfuse, langfuseOverlay) ? void 0 : resolveToolOutputTracingConfig(runLangfuse, langfuseOverlay),
|
|
75
|
+
runId,
|
|
76
|
+
agentId
|
|
48
77
|
};
|
|
49
78
|
}
|
|
50
79
|
//#endregion
|
|
51
|
-
export { resolveLangfuseConfigForSpan, resolveLangfuseRuntimeScope, resolveToolOutputTracingConfigForSpan, resolveTraceIdSeedForSpan, withLangfuseRuntimeScope };
|
|
80
|
+
export { resolveLangfuseConfigForSpan, resolveLangfuseRuntimeScope, resolveLangfuseScopeAgentId, resolveLangfuseScopeRunId, resolveToolOutputTracingConfigForSpan, resolveTraceIdSeedForSpan, withLangfuseRuntimeScope };
|
|
52
81
|
|
|
53
82
|
//# sourceMappingURL=langfuseRuntimeScope.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"langfuseRuntimeScope.mjs","names":[],"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
|
|
1
|
+
{"version":3,"file":"langfuseRuntimeScope.mjs","names":[],"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,91 @@
|
|
|
1
|
+
import { isPresent } from "./utils/misc.mjs";
|
|
2
|
+
import { hasLangfuseConfigCredentials, hasLangfuseEnvConfig, hasLangfuseEnvCredentials } from "./langfuseConfig.mjs";
|
|
3
|
+
import { createHash } from "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 (hasLangfuseConfigCredentials(langfuse)) return {
|
|
43
|
+
publicKey: langfuse.publicKey,
|
|
44
|
+
secretKey: langfuse.secretKey,
|
|
45
|
+
...isPresent(langfuse.baseUrl) ? { baseUrl: langfuse.baseUrl } : {},
|
|
46
|
+
...isPresent(environment) ? { environment } : {}
|
|
47
|
+
};
|
|
48
|
+
if (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
|
+
...isPresent(baseUrl) ? { baseUrl } : {},
|
|
54
|
+
...isPresent(environment) ? { environment } : {}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (isPresent(langfuse?.baseUrl) && hasLangfuseEnvCredentials()) return {
|
|
58
|
+
publicKey: process.env.LANGFUSE_PUBLIC_KEY,
|
|
59
|
+
secretKey: process.env.LANGFUSE_SECRET_KEY,
|
|
60
|
+
baseUrl: langfuse.baseUrl,
|
|
61
|
+
...isPresent(environment) ? { environment } : {}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function hashCacheKeyValue(value) {
|
|
65
|
+
return isPresent(value) ? 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
|
+
export { getLangfuseDestinationKey, getLangfuseManagedSpanDestination, getLangfuseSpanProcessorParams, registerLangfuseManagedSpan, resolveLangfuseDestinationKey };
|
|
90
|
+
|
|
91
|
+
//# sourceMappingURL=langfuseSpanRegistry.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"langfuseSpanRegistry.mjs","names":[],"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,IAAI,6BAA6B,QAAQ,GACvC,OAAO;EACL,WAAW,SAAS;EACpB,WAAW,SAAS;EACpB,GAAI,UAAU,SAAS,OAAO,IAAI,EAAE,SAAS,SAAS,QAAQ,IAAI,CAAC;EACnE,GAAI,UAAU,WAAW,IAAI,EAAE,YAAY,IAAI,CAAC;CAClD;CAEF,IAAI,qBAAqB,GAAG;EAC1B,MAAM,UACJ,UAAU,WACV,QAAQ,IAAI,qBACZ,QAAQ,IAAI;EACd,OAAO;GACL,WAAW,QAAQ,IAAI;GACvB,WAAW,QAAQ,IAAI;GACvB,GAAI,UAAU,OAAO,IAAI,EAAE,QAAQ,IAAI,CAAC;GACxC,GAAI,UAAU,WAAW,IAAI,EAAE,YAAY,IAAI,CAAC;EAClD;CACF;CACA,IAAI,UAAU,UAAU,OAAO,KAAK,0BAA0B,GAC5D,OAAO;EACL,WAAW,QAAQ,IAAI;EACvB,WAAW,QAAQ,IAAI;EACvB,SAAS,SAAS;EAClB,GAAI,UAAU,WAAW,IAAI,EAAE,YAAY,IAAI,CAAC;CAClD;AAGJ;AAEA,SAAS,kBAAkB,OAA+C;CACxE,OAAO,UAAU,KAAK,IAClB,WAAW,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
|
+
import "./common/enum.mjs";
|
|
2
|
+
import "./common/index.mjs";
|
|
1
3
|
import { LangfuseOtelSpanAttributes } from "@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 = `${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 = LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT;
|
|
145
202
|
const question = findLastMessageText(parseAttributeValue(span.attributes[inputKey]), "user");
|
|
146
203
|
const answer = findLastMessageText(parseAttributeValue(span.attributes[outputKey]), "assistant");
|
|
147
|
-
|
|
148
|
-
|
|
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[LangfuseOtelSpanAttributes.TRACE_INPUT] = traceInput;
|
|
@@ -155,6 +221,55 @@ function isGenerationSpan(span) {
|
|
|
155
221
|
const type = span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE];
|
|
156
222
|
return typeof type === "string" && type.toLowerCase() === "generation";
|
|
157
223
|
}
|
|
224
|
+
function isToolSpan(span) {
|
|
225
|
+
const type = span.attributes[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[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] = ROOT_OBSERVATION_TYPE;
|
|
272
|
+
}
|
|
158
273
|
function hasTraceTag(span, expectedTag) {
|
|
159
274
|
const tags = parseAttributeValue(span.attributes[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.mjs","names":[],"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.mjs","names":[],"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 = 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 ("
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|