@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
package/src/langfuse.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { CallbackHandler } from '@langfuse/langchain';
|
|
2
|
-
import {
|
|
2
|
+
import { LangfuseOtelContextKeys } from '@langfuse/core';
|
|
3
3
|
import { AIMessage, AIMessageChunk } from '@langchain/core/messages';
|
|
4
4
|
import { isGraphInterrupt, isParentCommand } from '@langchain/langgraph';
|
|
5
|
+
import { context as otelContext, trace as otelTrace } from '@opentelemetry/api';
|
|
5
6
|
import {
|
|
6
7
|
getLangfuseTracerProvider,
|
|
7
8
|
propagateAttributes,
|
|
@@ -17,14 +18,34 @@ import type {
|
|
|
17
18
|
LLMResult,
|
|
18
19
|
} from '@langchain/core/outputs';
|
|
19
20
|
import type { PropagateAttributesParams } from '@langfuse/tracing';
|
|
21
|
+
import type { Context } from '@opentelemetry/api';
|
|
22
|
+
import type { ResolvedLangfuseToolOutputTracingConfig } from '@/langfuseRuntimeContext';
|
|
20
23
|
import type * as t from '@/types';
|
|
21
24
|
import {
|
|
22
25
|
resolveLangfuseConfigForSpan,
|
|
26
|
+
resolveLangfuseScopeAgentId,
|
|
27
|
+
resolveLangfuseScopeRunId,
|
|
23
28
|
resolveTraceIdSeedForSpan,
|
|
24
29
|
withLangfuseRuntimeScope,
|
|
25
30
|
} from '@/langfuseRuntimeScope';
|
|
31
|
+
import {
|
|
32
|
+
hasLangfuseConfigCredentials,
|
|
33
|
+
hasLangfuseEnvCredentials,
|
|
34
|
+
resolveToolOutputTracingConfig,
|
|
35
|
+
hasLangfuseEnvConfig,
|
|
36
|
+
} from '@/langfuseConfig';
|
|
37
|
+
import {
|
|
38
|
+
getLangfuseManagedSpanDestination,
|
|
39
|
+
resolveLangfuseDestinationKey,
|
|
40
|
+
} from '@/langfuseSpanRegistry';
|
|
26
41
|
import { isPresent, parseBooleanEnv } from '@/utils/misc';
|
|
27
42
|
|
|
43
|
+
export {
|
|
44
|
+
hasLangfuseConfigCredentials,
|
|
45
|
+
hasLangfuseEnvCredentials,
|
|
46
|
+
hasLangfuseEnvConfig,
|
|
47
|
+
};
|
|
48
|
+
|
|
28
49
|
const TRACE_METADATA_MAX_LENGTH = 200;
|
|
29
50
|
const LANGFUSE_FORCE_FLUSH_ON_DISPOSE = 'LANGFUSE_FORCE_FLUSH_ON_DISPOSE';
|
|
30
51
|
const GRAPH_INTERRUPT_CONTROL_FLOW = { controlFlow: 'GraphInterrupt' } as const;
|
|
@@ -45,12 +66,32 @@ type LangfuseHandlerParams = {
|
|
|
45
66
|
traceMetadata?: LangfuseTraceMetadata;
|
|
46
67
|
tags?: string[];
|
|
47
68
|
traceIdSeed?: string;
|
|
69
|
+
/** Identity of the run this handler traces; ambient runtime scopes are
|
|
70
|
+
* only adopted when stamped with the same run (see
|
|
71
|
+
* `LangfuseRuntimeContext.runId`). */
|
|
72
|
+
runId?: string;
|
|
73
|
+
/** The run's resolved tool-output policy — for multi-agent streams the
|
|
74
|
+
* conservative aggregate across agents, which `this.langfuse` (the
|
|
75
|
+
* primary agent's config) cannot reproduce. Applied when a foreign
|
|
76
|
+
* scope's policy is rejected. */
|
|
77
|
+
toolOutputTracing?: ResolvedLangfuseToolOutputTracingConfig;
|
|
78
|
+
/** The run's propagated trace name, re-propagated when a foreign scope's
|
|
79
|
+
* attributes are cleared. */
|
|
80
|
+
traceName?: string;
|
|
48
81
|
};
|
|
49
82
|
|
|
50
83
|
type AgentLangfuseHandlerParams = LangfuseHandlerParams & {
|
|
51
84
|
langfuse?: t.LangfuseConfig;
|
|
52
85
|
};
|
|
53
86
|
|
|
87
|
+
type HandlerIdentity = {
|
|
88
|
+
userId?: string;
|
|
89
|
+
sessionId?: string;
|
|
90
|
+
tags?: string[];
|
|
91
|
+
metadata?: LangfuseTraceMetadata;
|
|
92
|
+
traceName?: string;
|
|
93
|
+
};
|
|
94
|
+
|
|
54
95
|
type LangfuseAttributeParams = AgentLangfuseHandlerParams & {
|
|
55
96
|
traceName?: string;
|
|
56
97
|
};
|
|
@@ -169,15 +210,102 @@ function normalizeBedrockUsageForLangfuse(output: LLMResult): LLMResult {
|
|
|
169
210
|
return { ...output, generations };
|
|
170
211
|
}
|
|
171
212
|
|
|
213
|
+
const LANGGRAPH_NODE_METADATA_KEY = 'langgraph_node';
|
|
214
|
+
/** Explicit agent identity in invoke metadata. Every identity-stamping
|
|
215
|
+
* component (graph model path, ToolNode, summarization node) overwrites the
|
|
216
|
+
* canonical `agentId` at its own invoke, so spread order guarantees the
|
|
217
|
+
* closest stamper wins — key priority alone could not (an inherited key of
|
|
218
|
+
* either casing can name the wrong agent). `agent_id` remains a fallback
|
|
219
|
+
* for third-party graphs that only stamp the snake-case form. */
|
|
220
|
+
const AGENT_ID_METADATA_KEYS = ['agentId', 'agent_id'];
|
|
221
|
+
const LANGGRAPH_NODE_AGENT_PREFIXES = ['agent=', 'tools=', 'summarize='];
|
|
222
|
+
|
|
223
|
+
/** The LangGraph node a callback executes under, from its inherited
|
|
224
|
+
* `langgraph_node` run metadata. `undefined` when the callback carries no
|
|
225
|
+
* node identity. */
|
|
226
|
+
function getCallbackNode(
|
|
227
|
+
metadata?: Record<string, unknown>
|
|
228
|
+
): string | undefined {
|
|
229
|
+
const node = metadata?.[LANGGRAPH_NODE_METADATA_KEY];
|
|
230
|
+
return typeof node === 'string' && node !== '' ? node : undefined;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Whether a callback's node identifies the given agent. The outer workflow
|
|
234
|
+
* node carries the agent id VERBATIM — including ids that themselves begin
|
|
235
|
+
* with an internal prefix (an agent literally named `agent=research`) — so
|
|
236
|
+
* an exact match is checked before decoding the inner subgraph prefixes
|
|
237
|
+
* (`agent=` / `tools=` / `summarize=`). */
|
|
238
|
+
function callbackNodeMatchesAgent(node: string, agentId: string): boolean {
|
|
239
|
+
if (node === agentId) {
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
for (const prefix of LANGGRAPH_NODE_AGENT_PREFIXES) {
|
|
243
|
+
if (node.startsWith(prefix) && node.slice(prefix.length) === agentId) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Hosts often execute agent code inside their own OpenTelemetry spans (HTTP
|
|
252
|
+
* server auto-instrumentation on the global provider). Root observations must
|
|
253
|
+
* not inherit that ambient identity: the foreign parent is never exported to
|
|
254
|
+
* Langfuse, which orphans the trace root (root/trace input-output shaping is
|
|
255
|
+
* skipped because the span no longer looks like a root), collapses concurrent
|
|
256
|
+
* runs inside one request context — an agent run and the previous turn's
|
|
257
|
+
* title run — into a single merged trace with racing names and unioned tags,
|
|
258
|
+
* and bypasses the seeded deterministic trace id generator. Only a
|
|
259
|
+
* Langfuse-managed span bound to the same export destination as the starting
|
|
260
|
+
* run is a safe parent — that is the sanctioned way for hosts to group runs
|
|
261
|
+
* under their own Langfuse observations; a managed span from a different
|
|
262
|
+
* destination (another tenant's project) would leave this run's trace
|
|
263
|
+
* dangling in its own destination while inheriting the other trace's id.
|
|
264
|
+
*/
|
|
265
|
+
function detachForeignAmbientSpan(
|
|
266
|
+
activeContext: Context,
|
|
267
|
+
destinationKey?: string
|
|
268
|
+
): Context {
|
|
269
|
+
const activeSpan = otelTrace.getSpan(activeContext);
|
|
270
|
+
if (activeSpan == null) {
|
|
271
|
+
return activeContext;
|
|
272
|
+
}
|
|
273
|
+
const parentDestination = getLangfuseManagedSpanDestination(activeSpan);
|
|
274
|
+
if (parentDestination != null && parentDestination === destinationKey) {
|
|
275
|
+
return activeContext;
|
|
276
|
+
}
|
|
277
|
+
return otelTrace.deleteSpan(activeContext);
|
|
278
|
+
}
|
|
279
|
+
|
|
172
280
|
class ScopedLangfuseCallbackHandler extends CallbackHandler {
|
|
173
281
|
private readonly langfuse?: t.LangfuseConfig;
|
|
174
282
|
private readonly traceIdSeed?: string;
|
|
283
|
+
private readonly runId?: string;
|
|
284
|
+
private readonly identity: HandlerIdentity;
|
|
285
|
+
private readonly toolOutputTracing?: ResolvedLangfuseToolOutputTracingConfig;
|
|
286
|
+
private readonly trackedRunIds = new Set<string>();
|
|
175
287
|
|
|
176
288
|
constructor(params?: AgentLangfuseHandlerParams) {
|
|
177
|
-
const {
|
|
289
|
+
const {
|
|
290
|
+
langfuse,
|
|
291
|
+
traceIdSeed,
|
|
292
|
+
runId,
|
|
293
|
+
toolOutputTracing,
|
|
294
|
+
traceName,
|
|
295
|
+
...handlerParams
|
|
296
|
+
} = params ?? {};
|
|
178
297
|
super(handlerParams);
|
|
179
298
|
this.langfuse = langfuse;
|
|
180
299
|
this.traceIdSeed = traceIdSeed;
|
|
300
|
+
this.runId = runId;
|
|
301
|
+
this.toolOutputTracing = toolOutputTracing;
|
|
302
|
+
this.identity = {
|
|
303
|
+
userId: handlerParams.userId,
|
|
304
|
+
sessionId: handlerParams.sessionId,
|
|
305
|
+
tags: handlerParams.tags,
|
|
306
|
+
metadata: handlerParams.traceMetadata,
|
|
307
|
+
traceName,
|
|
308
|
+
};
|
|
181
309
|
}
|
|
182
310
|
|
|
183
311
|
private getDeterministicTraceSeed(): string | undefined {
|
|
@@ -186,17 +314,157 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
|
|
|
186
314
|
: undefined;
|
|
187
315
|
}
|
|
188
316
|
|
|
189
|
-
|
|
190
|
-
|
|
317
|
+
/**
|
|
318
|
+
* Mirrors the base handler's `runMap`: a start callback whose `parentRunId`
|
|
319
|
+
* this handler never observed gets no explicit parent span and falls back
|
|
320
|
+
* to the ambient OTEL context (`startAndRegisterOtelSpan`), so it needs the
|
|
321
|
+
* same foreign-span detachment as a true root. This happens whenever a
|
|
322
|
+
* handler is attached mid-graph — e.g. the per-agent handler created for a
|
|
323
|
+
* detached subagent's model invocations, whose surrounding graph runs were
|
|
324
|
+
* never traced.
|
|
325
|
+
*/
|
|
326
|
+
private startsDetachedRun(runId: string, parentRunId?: string): boolean {
|
|
327
|
+
const detached =
|
|
328
|
+
parentRunId == null || !this.trackedRunIds.has(parentRunId);
|
|
329
|
+
this.trackedRunIds.add(runId);
|
|
330
|
+
return detached;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Whether the ambient runtime scope belongs to a different run — or, for
|
|
335
|
+
* per-agent overlay scopes, to a different concurrently executing agent of
|
|
336
|
+
* the same run than the one this callback reports via its inherited
|
|
337
|
+
* `langgraph_node` metadata. Unstamped scopes on unstamped handlers are
|
|
338
|
+
* never foreign (host-managed handler semantics).
|
|
339
|
+
*/
|
|
340
|
+
private isForeignScope(
|
|
341
|
+
scopeRunId: string | undefined,
|
|
342
|
+
scopeAgentId: string | undefined,
|
|
343
|
+
callbackMetadata?: Record<string, unknown>
|
|
344
|
+
): boolean {
|
|
345
|
+
if (this.runId == null || scopeRunId == null) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
if (scopeRunId !== this.runId) {
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
if (scopeAgentId == null) {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
// Explicit agent identity (stamped into invoke metadata by the graph's
|
|
355
|
+
// model path and ToolNode) is unambiguous; node names are a fallback —
|
|
356
|
+
// an agent literally named `agent=research` makes its outer node
|
|
357
|
+
// indistinguishable from agent `research`'s inner model node.
|
|
358
|
+
for (const key of AGENT_ID_METADATA_KEYS) {
|
|
359
|
+
const explicitAgentId = callbackMetadata?.[key];
|
|
360
|
+
if (typeof explicitAgentId === 'string' && explicitAgentId !== '') {
|
|
361
|
+
return explicitAgentId !== scopeAgentId;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const callbackNode = getCallbackNode(callbackMetadata);
|
|
365
|
+
return (
|
|
366
|
+
callbackNode != null &&
|
|
367
|
+
!callbackNodeMatchesAgent(callbackNode, scopeAgentId)
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* LangChain executes non-awaited callbacks on a process-wide background
|
|
373
|
+
* queue (`consumeCallback`), so this callback may be running inside a
|
|
374
|
+
* DIFFERENT concurrent run's async context. The ambient runtime scope is
|
|
375
|
+
* therefore only adopted when it belongs to this handler's run (and, for
|
|
376
|
+
* agent sub-scopes, to this callback's agent): scopes are stamped at their
|
|
377
|
+
* call sites, and a foreign stamp means the scope's config would route
|
|
378
|
+
* spans to the wrong destination, its seed would collapse this run's spans
|
|
379
|
+
* into the foreign trace, and its tool-output policy could leak output the
|
|
380
|
+
* foreign run permits but this run redacts — so config, seed, AND redaction
|
|
381
|
+
* policy all fall back to this handler's own run. Unstamped scopes on
|
|
382
|
+
* unstamped handlers keep scope-first semantics (agent overlays and
|
|
383
|
+
* per-path seeds like the title/label scopes, host-managed handlers).
|
|
384
|
+
*
|
|
385
|
+
* Detached runs (roots, or starts whose parent this handler never tracked)
|
|
386
|
+
* take their span parent from the ambient OTEL context, so drop any
|
|
387
|
+
* foreign ambient span first — a run launched from a host's instrumented
|
|
388
|
+
* request context must start its own trace, not join an unexported
|
|
389
|
+
* foreign one.
|
|
390
|
+
*/
|
|
391
|
+
private withRuntimeContext<T>(
|
|
392
|
+
action: () => T,
|
|
393
|
+
isDetachedRun = false,
|
|
394
|
+
callbackMetadata?: Record<string, unknown>
|
|
395
|
+
): T {
|
|
396
|
+
const currentContext = otelContext.active();
|
|
397
|
+
const scopeRunId = resolveLangfuseScopeRunId(currentContext);
|
|
398
|
+
const scopeAgentId = resolveLangfuseScopeAgentId(currentContext);
|
|
399
|
+
if (this.isForeignScope(scopeRunId, scopeAgentId, callbackMetadata)) {
|
|
400
|
+
return this.withForeignScopeRejected(action);
|
|
401
|
+
}
|
|
191
402
|
const langfuse =
|
|
192
|
-
resolveLangfuseConfigForSpan(
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
langfuse
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
403
|
+
resolveLangfuseConfigForSpan(currentContext) ?? this.langfuse;
|
|
404
|
+
const activeContext = isDetachedRun
|
|
405
|
+
? detachForeignAmbientSpan(
|
|
406
|
+
currentContext,
|
|
407
|
+
resolveLangfuseDestinationKey(langfuse)
|
|
408
|
+
)
|
|
409
|
+
: currentContext;
|
|
410
|
+
const scoped = (): T =>
|
|
411
|
+
withLangfuseRuntimeScope(
|
|
412
|
+
{
|
|
413
|
+
langfuse,
|
|
414
|
+
traceIdSeed:
|
|
415
|
+
resolveTraceIdSeedForSpan(activeContext) ??
|
|
416
|
+
this.getDeterministicTraceSeed(),
|
|
417
|
+
runId: scopeRunId ?? this.runId,
|
|
418
|
+
},
|
|
419
|
+
action
|
|
420
|
+
);
|
|
421
|
+
return activeContext === currentContext
|
|
422
|
+
? scoped()
|
|
423
|
+
: otelContext.with(activeContext, scoped);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* A foreign concurrent run's context must be replaced wholesale, not
|
|
428
|
+
* merged: this library's scope keys (config, seed, tool-output policy,
|
|
429
|
+
* identity stamps) via the replace-mode runtime scope, `@langfuse/tracing`'s
|
|
430
|
+
* propagated trace attributes (userId, sessionId, tags, metadata, …) by
|
|
431
|
+
* deleting their context keys and re-propagating this handler's own
|
|
432
|
+
* identity, and the foreign active span — removed both so detached runs
|
|
433
|
+
* root their own trace and so `propagateAttributes` cannot stamp this
|
|
434
|
+
* run's identity onto the foreign run's still-recording span.
|
|
435
|
+
*/
|
|
436
|
+
private withForeignScopeRejected<T>(action: () => T): T {
|
|
437
|
+
let cleanContext = otelTrace.deleteSpan(otelContext.active());
|
|
438
|
+
for (const key of Object.values(LangfuseOtelContextKeys)) {
|
|
439
|
+
cleanContext = cleanContext.deleteValue(key);
|
|
440
|
+
}
|
|
441
|
+
const scoped = (): T =>
|
|
442
|
+
withLangfuseRuntimeScope(
|
|
443
|
+
{
|
|
444
|
+
langfuse: this.langfuse,
|
|
445
|
+
traceIdSeed: this.getDeterministicTraceSeed(),
|
|
446
|
+
runId: this.runId,
|
|
447
|
+
toolOutputTracing:
|
|
448
|
+
this.toolOutputTracing ??
|
|
449
|
+
resolveToolOutputTracingConfig(this.langfuse),
|
|
450
|
+
},
|
|
451
|
+
action,
|
|
452
|
+
{ replace: true }
|
|
453
|
+
);
|
|
454
|
+
const { userId, sessionId, tags, metadata, traceName } = this.identity;
|
|
455
|
+
const hasIdentity =
|
|
456
|
+
userId != null ||
|
|
457
|
+
sessionId != null ||
|
|
458
|
+
metadata != null ||
|
|
459
|
+
traceName != null ||
|
|
460
|
+
(tags?.length ?? 0) > 0;
|
|
461
|
+
return otelContext.with(cleanContext, () =>
|
|
462
|
+
hasIdentity
|
|
463
|
+
? propagateAttributes(
|
|
464
|
+
{ userId, sessionId, tags, metadata, traceName },
|
|
465
|
+
scoped
|
|
466
|
+
)
|
|
467
|
+
: scoped()
|
|
200
468
|
);
|
|
201
469
|
}
|
|
202
470
|
|
|
@@ -206,7 +474,11 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
|
|
|
206
474
|
override handleChainStart(
|
|
207
475
|
...args: Parameters<CallbackHandler['handleChainStart']>
|
|
208
476
|
): ReturnType<CallbackHandler['handleChainStart']> {
|
|
209
|
-
return this.withRuntimeContext(
|
|
477
|
+
return this.withRuntimeContext(
|
|
478
|
+
() => super.handleChainStart(...args),
|
|
479
|
+
this.startsDetachedRun(args[2], args[3]),
|
|
480
|
+
args[5]
|
|
481
|
+
);
|
|
210
482
|
}
|
|
211
483
|
|
|
212
484
|
override handleChainError(
|
|
@@ -233,25 +505,40 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
|
|
|
233
505
|
override handleAgentAction(
|
|
234
506
|
...args: Parameters<CallbackHandler['handleAgentAction']>
|
|
235
507
|
): ReturnType<CallbackHandler['handleAgentAction']> {
|
|
236
|
-
return this.withRuntimeContext(
|
|
508
|
+
return this.withRuntimeContext(
|
|
509
|
+
() => super.handleAgentAction(...args),
|
|
510
|
+
this.startsDetachedRun(args[1], args[2])
|
|
511
|
+
);
|
|
237
512
|
}
|
|
238
513
|
|
|
239
514
|
override handleGenerationStart(
|
|
240
515
|
...args: Parameters<CallbackHandler['handleGenerationStart']>
|
|
241
516
|
): ReturnType<CallbackHandler['handleGenerationStart']> {
|
|
242
|
-
return this.withRuntimeContext(
|
|
517
|
+
return this.withRuntimeContext(
|
|
518
|
+
() => super.handleGenerationStart(...args),
|
|
519
|
+
this.startsDetachedRun(args[2], args[3]),
|
|
520
|
+
args[6]
|
|
521
|
+
);
|
|
243
522
|
}
|
|
244
523
|
|
|
245
524
|
override handleChatModelStart(
|
|
246
525
|
...args: Parameters<CallbackHandler['handleChatModelStart']>
|
|
247
526
|
): ReturnType<CallbackHandler['handleChatModelStart']> {
|
|
248
|
-
return this.withRuntimeContext(
|
|
527
|
+
return this.withRuntimeContext(
|
|
528
|
+
() => super.handleChatModelStart(...args),
|
|
529
|
+
this.startsDetachedRun(args[2], args[3]),
|
|
530
|
+
args[6]
|
|
531
|
+
);
|
|
249
532
|
}
|
|
250
533
|
|
|
251
534
|
override handleLLMStart(
|
|
252
535
|
...args: Parameters<CallbackHandler['handleLLMStart']>
|
|
253
536
|
): ReturnType<CallbackHandler['handleLLMStart']> {
|
|
254
|
-
return this.withRuntimeContext(
|
|
537
|
+
return this.withRuntimeContext(
|
|
538
|
+
() => super.handleLLMStart(...args),
|
|
539
|
+
this.startsDetachedRun(args[2], args[3]),
|
|
540
|
+
args[6]
|
|
541
|
+
);
|
|
255
542
|
}
|
|
256
543
|
|
|
257
544
|
override handleLLMEnd(
|
|
@@ -269,7 +556,11 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
|
|
|
269
556
|
override handleToolStart(
|
|
270
557
|
...args: Parameters<CallbackHandler['handleToolStart']>
|
|
271
558
|
): ReturnType<CallbackHandler['handleToolStart']> {
|
|
272
|
-
return this.withRuntimeContext(
|
|
559
|
+
return this.withRuntimeContext(
|
|
560
|
+
() => super.handleToolStart(...args),
|
|
561
|
+
this.startsDetachedRun(args[2], args[3]),
|
|
562
|
+
args[5]
|
|
563
|
+
);
|
|
273
564
|
}
|
|
274
565
|
|
|
275
566
|
override handleToolError(
|
|
@@ -289,7 +580,11 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
|
|
|
289
580
|
override handleRetrieverStart(
|
|
290
581
|
...args: Parameters<CallbackHandler['handleRetrieverStart']>
|
|
291
582
|
): ReturnType<CallbackHandler['handleRetrieverStart']> {
|
|
292
|
-
return this.withRuntimeContext(
|
|
583
|
+
return this.withRuntimeContext(
|
|
584
|
+
() => super.handleRetrieverStart(...args),
|
|
585
|
+
this.startsDetachedRun(args[2], args[3]),
|
|
586
|
+
args[5]
|
|
587
|
+
);
|
|
293
588
|
}
|
|
294
589
|
}
|
|
295
590
|
|
|
@@ -309,19 +604,6 @@ function hasLangfuseTraceAttributes(langfuse?: t.LangfuseConfig): boolean {
|
|
|
309
604
|
);
|
|
310
605
|
}
|
|
311
606
|
|
|
312
|
-
export function hasLangfuseConfigCredentials(
|
|
313
|
-
langfuse?: t.LangfuseConfig
|
|
314
|
-
): langfuse is t.LangfuseConfig & {
|
|
315
|
-
publicKey: string;
|
|
316
|
-
secretKey: string;
|
|
317
|
-
} {
|
|
318
|
-
return (
|
|
319
|
-
langfuse != null &&
|
|
320
|
-
isPresent(langfuse.publicKey) &&
|
|
321
|
-
isPresent(langfuse.secretKey)
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
607
|
function hasLangfuseConfigBaseUrl(langfuse?: t.LangfuseConfig): boolean {
|
|
326
608
|
return isPresent(langfuse?.baseUrl);
|
|
327
609
|
}
|
|
@@ -425,17 +707,6 @@ export function getLangfuseTraceName(
|
|
|
425
707
|
return isPresent(agentName) ? `${fallback}: ${agentName}` : fallback;
|
|
426
708
|
}
|
|
427
709
|
|
|
428
|
-
export function hasLangfuseEnvConfig(): boolean {
|
|
429
|
-
return hasLangfuseEnvCredentials();
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
export function hasLangfuseEnvCredentials(): boolean {
|
|
433
|
-
return (
|
|
434
|
-
isPresent(process.env.LANGFUSE_SECRET_KEY) &&
|
|
435
|
-
isPresent(process.env.LANGFUSE_PUBLIC_KEY)
|
|
436
|
-
);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
710
|
export function shouldCreateLangfuseHandler(
|
|
440
711
|
langfuse?: t.LangfuseConfig
|
|
441
712
|
): boolean {
|
|
@@ -462,6 +733,9 @@ export function createLangfuseHandler({
|
|
|
462
733
|
traceMetadata,
|
|
463
734
|
tags,
|
|
464
735
|
traceIdSeed,
|
|
736
|
+
runId,
|
|
737
|
+
toolOutputTracing,
|
|
738
|
+
traceName,
|
|
465
739
|
}: AgentLangfuseHandlerParams): CallbackHandler | undefined {
|
|
466
740
|
if (!shouldCreateLangfuseHandler(langfuse)) {
|
|
467
741
|
return undefined;
|
|
@@ -476,6 +750,9 @@ export function createLangfuseHandler({
|
|
|
476
750
|
tags: mergeLangfuseTags(tags, langfuse?.tags),
|
|
477
751
|
langfuse,
|
|
478
752
|
traceIdSeed,
|
|
753
|
+
runId,
|
|
754
|
+
toolOutputTracing,
|
|
755
|
+
traceName,
|
|
479
756
|
});
|
|
480
757
|
}
|
|
481
758
|
|
package/src/langfuseConfig.ts
CHANGED
|
@@ -12,6 +12,30 @@ export function normalizeToolName(name: string): string {
|
|
|
12
12
|
return name.trim().toLowerCase();
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export function hasLangfuseConfigCredentials(
|
|
16
|
+
langfuse?: t.LangfuseConfig
|
|
17
|
+
): langfuse is t.LangfuseConfig & {
|
|
18
|
+
publicKey: string;
|
|
19
|
+
secretKey: string;
|
|
20
|
+
} {
|
|
21
|
+
return (
|
|
22
|
+
langfuse != null &&
|
|
23
|
+
isPresent(langfuse.publicKey) &&
|
|
24
|
+
isPresent(langfuse.secretKey)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function hasLangfuseEnvCredentials(): boolean {
|
|
29
|
+
return (
|
|
30
|
+
isPresent(process.env.LANGFUSE_SECRET_KEY) &&
|
|
31
|
+
isPresent(process.env.LANGFUSE_PUBLIC_KEY)
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function hasLangfuseEnvConfig(): boolean {
|
|
36
|
+
return hasLangfuseEnvCredentials();
|
|
37
|
+
}
|
|
38
|
+
|
|
15
39
|
function normalizeToolNames(names: string[] | undefined): Set<string> {
|
|
16
40
|
const normalized = new Set<string>();
|
|
17
41
|
for (const name of names ?? []) {
|
|
@@ -13,6 +13,22 @@ export type LangfuseRuntimeContext = {
|
|
|
13
13
|
langfuse?: t.LangfuseConfig;
|
|
14
14
|
traceIdSeed?: string;
|
|
15
15
|
toolOutputTracing?: ResolvedLangfuseToolOutputTracingConfig;
|
|
16
|
+
/**
|
|
17
|
+
* Identity of the run this scope belongs to. LangChain executes
|
|
18
|
+
* non-awaited callbacks on a shared background queue
|
|
19
|
+
* (`@langchain/core` `consumeCallback`, a process-wide `p-queue` with
|
|
20
|
+
* concurrency 1), so a callback can run inside a DIFFERENT concurrent
|
|
21
|
+
* run's async context. Handlers compare this id against their own run to
|
|
22
|
+
* decide whether an ambient scope is theirs to adopt.
|
|
23
|
+
*/
|
|
24
|
+
runId?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Identity of the agent whose overlay this scope carries, for per-agent
|
|
27
|
+
* scopes inside a run. Fan-out agents execute concurrently with distinct
|
|
28
|
+
* Langfuse overlays, so handlers additionally compare this against the
|
|
29
|
+
* agent a callback reports via its inherited `langgraph_node` metadata.
|
|
30
|
+
*/
|
|
31
|
+
agentId?: string;
|
|
16
32
|
};
|
|
17
33
|
|
|
18
34
|
const langfuseRuntimeContextStore =
|
|
@@ -28,6 +44,8 @@ export function hasLangfuseRuntimeContextValue(
|
|
|
28
44
|
return (
|
|
29
45
|
context.langfuse != null ||
|
|
30
46
|
hasText(context.traceIdSeed) ||
|
|
47
|
+
hasText(context.runId) ||
|
|
48
|
+
hasText(context.agentId) ||
|
|
31
49
|
context.toolOutputTracing != null
|
|
32
50
|
);
|
|
33
51
|
}
|
|
@@ -51,6 +69,14 @@ export function getTraceIdSeed(): string | undefined {
|
|
|
51
69
|
return getLangfuseRuntimeContext()?.traceIdSeed;
|
|
52
70
|
}
|
|
53
71
|
|
|
72
|
+
export function getLangfuseScopeRunId(): string | undefined {
|
|
73
|
+
return getLangfuseRuntimeContext()?.runId;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getLangfuseScopeAgentId(): string | undefined {
|
|
77
|
+
return getLangfuseRuntimeContext()?.agentId;
|
|
78
|
+
}
|
|
79
|
+
|
|
54
80
|
export function getLangfuseRuntimeToolOutputTracingConfig():
|
|
55
81
|
| ResolvedLangfuseToolOutputTracingConfig
|
|
56
82
|
| undefined {
|
|
@@ -60,7 +86,8 @@ export function getLangfuseRuntimeToolOutputTracingConfig():
|
|
|
60
86
|
/**
|
|
61
87
|
* Runs `fn` with a merged Langfuse runtime context. Undefined fields inherit
|
|
62
88
|
* from the parent scope; callers intentionally cannot clear parent values by
|
|
63
|
-
* passing `undefined
|
|
89
|
+
* passing `undefined` — use `replaceLangfuseRuntimeContext` when the parent
|
|
90
|
+
* scope must NOT leak through (a foreign concurrent run's scope).
|
|
64
91
|
*/
|
|
65
92
|
export function runWithLangfuseRuntimeContext<T>(
|
|
66
93
|
context: LangfuseRuntimeContext,
|
|
@@ -73,6 +100,8 @@ export function runWithLangfuseRuntimeContext<T>(
|
|
|
73
100
|
...(hasText(context.traceIdSeed)
|
|
74
101
|
? { traceIdSeed: context.traceIdSeed }
|
|
75
102
|
: {}),
|
|
103
|
+
...(hasText(context.runId) ? { runId: context.runId } : {}),
|
|
104
|
+
...(hasText(context.agentId) ? { agentId: context.agentId } : {}),
|
|
76
105
|
...(context.toolOutputTracing !== undefined
|
|
77
106
|
? { toolOutputTracing: context.toolOutputTracing }
|
|
78
107
|
: {}),
|
|
@@ -83,6 +112,19 @@ export function runWithLangfuseRuntimeContext<T>(
|
|
|
83
112
|
: fn();
|
|
84
113
|
}
|
|
85
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Runs `fn` with EXACTLY the provided context — the surrounding scope's
|
|
117
|
+
* fields do not leak through. A rejected foreign run's explicit destination
|
|
118
|
+
* or seed must not survive via merge inheritance when this run has none of
|
|
119
|
+
* its own (env-credential runs, non-deterministic runs).
|
|
120
|
+
*/
|
|
121
|
+
export function replaceLangfuseRuntimeContext<T>(
|
|
122
|
+
context: LangfuseRuntimeContext,
|
|
123
|
+
fn: () => T
|
|
124
|
+
): T {
|
|
125
|
+
return langfuseRuntimeContextStore.run({ ...context }, fn);
|
|
126
|
+
}
|
|
127
|
+
|
|
86
128
|
export function runWithTraceIdSeed<T>(
|
|
87
129
|
seed: string | undefined,
|
|
88
130
|
fn: () => T
|