@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.
Files changed (148) hide show
  1. package/dist/cjs/graphs/Graph.cjs +47 -13
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/graphs/MultiAgentGraph.cjs +35 -2
  4. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  5. package/dist/cjs/hitl/askUserQuestion.cjs +3 -2
  6. package/dist/cjs/hitl/askUserQuestion.cjs.map +1 -1
  7. package/dist/cjs/instrumentation.cjs +18 -48
  8. package/dist/cjs/instrumentation.cjs.map +1 -1
  9. package/dist/cjs/langfuse.cjs +174 -29
  10. package/dist/cjs/langfuse.cjs.map +1 -1
  11. package/dist/cjs/langfuseConfig.cjs +12 -0
  12. package/dist/cjs/langfuseConfig.cjs.map +1 -1
  13. package/dist/cjs/langfuseRuntimeContext.cjs +23 -2
  14. package/dist/cjs/langfuseRuntimeContext.cjs.map +1 -1
  15. package/dist/cjs/langfuseRuntimeScope.cjs +39 -8
  16. package/dist/cjs/langfuseRuntimeScope.cjs.map +1 -1
  17. package/dist/cjs/langfuseSpanRegistry.cjs +95 -0
  18. package/dist/cjs/langfuseSpanRegistry.cjs.map +1 -0
  19. package/dist/cjs/langfuseTraceShaping.cjs +121 -4
  20. package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
  21. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +39 -15
  22. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  23. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +25 -5
  24. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  25. package/dist/cjs/llm/init.cjs +3 -3
  26. package/dist/cjs/llm/invoke.cjs +5 -5
  27. package/dist/cjs/llm/openai/index.cjs +1 -1
  28. package/dist/cjs/main.cjs +10 -10
  29. package/dist/cjs/messages/prune.cjs +13 -1
  30. package/dist/cjs/messages/prune.cjs.map +1 -1
  31. package/dist/cjs/prompts/activityLabel.cjs +24 -12
  32. package/dist/cjs/prompts/activityLabel.cjs.map +1 -1
  33. package/dist/cjs/run.cjs +47 -22
  34. package/dist/cjs/run.cjs.map +1 -1
  35. package/dist/cjs/session/messageSerialization.cjs +6 -0
  36. package/dist/cjs/session/messageSerialization.cjs.map +1 -1
  37. package/dist/cjs/stream.cjs +21 -10
  38. package/dist/cjs/stream.cjs.map +1 -1
  39. package/dist/cjs/summarization/node.cjs +5 -0
  40. package/dist/cjs/summarization/node.cjs.map +1 -1
  41. package/dist/cjs/tools/ToolNode.cjs +253 -24
  42. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  43. package/dist/cjs/tools/handlers.cjs +1 -1
  44. package/dist/cjs/tools/search/tool.cjs +1 -1
  45. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -1
  46. package/dist/cjs/utils/index.cjs +2 -2
  47. package/dist/esm/graphs/Graph.mjs +48 -14
  48. package/dist/esm/graphs/Graph.mjs.map +1 -1
  49. package/dist/esm/graphs/MultiAgentGraph.mjs +35 -2
  50. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  51. package/dist/esm/hitl/askUserQuestion.mjs +3 -2
  52. package/dist/esm/hitl/askUserQuestion.mjs.map +1 -1
  53. package/dist/esm/instrumentation.mjs +18 -48
  54. package/dist/esm/instrumentation.mjs.map +1 -1
  55. package/dist/esm/langfuse.mjs +176 -28
  56. package/dist/esm/langfuse.mjs.map +1 -1
  57. package/dist/esm/langfuseConfig.mjs +10 -1
  58. package/dist/esm/langfuseConfig.mjs.map +1 -1
  59. package/dist/esm/langfuseRuntimeContext.mjs +21 -3
  60. package/dist/esm/langfuseRuntimeContext.mjs.map +1 -1
  61. package/dist/esm/langfuseRuntimeScope.mjs +39 -10
  62. package/dist/esm/langfuseRuntimeScope.mjs.map +1 -1
  63. package/dist/esm/langfuseSpanRegistry.mjs +91 -0
  64. package/dist/esm/langfuseSpanRegistry.mjs.map +1 -0
  65. package/dist/esm/langfuseTraceShaping.mjs +121 -4
  66. package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
  67. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +39 -15
  68. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  69. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +25 -5
  70. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  71. package/dist/esm/llm/init.mjs +2 -2
  72. package/dist/esm/llm/invoke.mjs +5 -5
  73. package/dist/esm/llm/openai/index.mjs +1 -1
  74. package/dist/esm/main.mjs +8 -8
  75. package/dist/esm/messages/prune.mjs +13 -1
  76. package/dist/esm/messages/prune.mjs.map +1 -1
  77. package/dist/esm/prompts/activityLabel.mjs +24 -12
  78. package/dist/esm/prompts/activityLabel.mjs.map +1 -1
  79. package/dist/esm/run.mjs +47 -22
  80. package/dist/esm/run.mjs.map +1 -1
  81. package/dist/esm/session/messageSerialization.mjs +6 -0
  82. package/dist/esm/session/messageSerialization.mjs.map +1 -1
  83. package/dist/esm/stream.mjs +21 -10
  84. package/dist/esm/stream.mjs.map +1 -1
  85. package/dist/esm/summarization/node.mjs +5 -0
  86. package/dist/esm/summarization/node.mjs.map +1 -1
  87. package/dist/esm/tools/ToolNode.mjs +254 -25
  88. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  89. package/dist/esm/tools/handlers.mjs +1 -1
  90. package/dist/esm/tools/search/tool.mjs +1 -1
  91. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -1
  92. package/dist/esm/utils/index.mjs +2 -2
  93. package/dist/types/graphs/Graph.d.ts +19 -0
  94. package/dist/types/hitl/askUserQuestion.d.ts +11 -1
  95. package/dist/types/langfuse.d.ts +16 -8
  96. package/dist/types/langfuseConfig.d.ts +6 -0
  97. package/dist/types/langfuseRuntimeContext.d.ts +27 -1
  98. package/dist/types/langfuseRuntimeScope.d.ts +17 -2
  99. package/dist/types/langfuseSpanRegistry.d.ts +17 -0
  100. package/dist/types/langfuseTraceShaping.d.ts +2 -1
  101. package/dist/types/llm/anthropic/utils/message_inputs.d.ts +1 -0
  102. package/dist/types/session/types.d.ts +1 -0
  103. package/dist/types/tools/ToolNode.d.ts +7 -1
  104. package/dist/types/types/hitl.d.ts +8 -0
  105. package/dist/types/types/tools.d.ts +30 -0
  106. package/package.json +7 -4
  107. package/src/__tests__/stream.eagerArgsDivergence.test.ts +753 -0
  108. package/src/graphs/Graph.ts +69 -20
  109. package/src/graphs/MultiAgentGraph.ts +56 -2
  110. package/src/graphs/__tests__/composition.smoke.test.ts +4 -0
  111. package/src/hitl/askUserQuestion.ts +14 -1
  112. package/src/instrumentation.ts +35 -77
  113. package/src/langfuse.ts +320 -43
  114. package/src/langfuseConfig.ts +24 -0
  115. package/src/langfuseRuntimeContext.ts +43 -1
  116. package/src/langfuseRuntimeScope.ts +94 -21
  117. package/src/langfuseSpanRegistry.ts +131 -0
  118. package/src/langfuseTraceShaping.ts +194 -7
  119. package/src/llm/anthropic/utils/message_inputs.ts +70 -19
  120. package/src/llm/anthropic/utils/streaming-tool-input.test.ts +186 -11
  121. package/src/llm/bedrock/utils/message_inputs.test.ts +120 -4
  122. package/src/llm/bedrock/utils/message_inputs.ts +32 -7
  123. package/src/messages/prune.ts +12 -1
  124. package/src/prompts/activityLabel.ts +23 -6
  125. package/src/run.ts +76 -45
  126. package/src/scripts/activity-labels/captured.json +56 -0
  127. package/src/scripts/activity-labels/checks.cjs +205 -0
  128. package/src/scripts/activity-labels/corpus.cjs +473 -0
  129. package/src/scripts/activity-labels/report.cjs +203 -0
  130. package/src/scripts/activity-labels/rescore.cjs +102 -0
  131. package/src/scripts/activity-labels/run.ts +705 -0
  132. package/src/scripts/activity-labels/variants.ts +71 -0
  133. package/src/session/messageSerialization.ts +12 -1
  134. package/src/session/types.ts +1 -0
  135. package/src/specs/activity-label-prompt.test.ts +26 -10
  136. package/src/specs/agent-handoffs.test.ts +306 -0
  137. package/src/specs/langfuse-callbacks.test.ts +456 -0
  138. package/src/specs/langfuse-routing.integration.test.ts +138 -1
  139. package/src/specs/langfuse-span-registry.test.ts +70 -0
  140. package/src/specs/langfuse-trace-shaping.test.ts +294 -0
  141. package/src/specs/prune.test.ts +38 -1
  142. package/src/stream.ts +70 -6
  143. package/src/summarization/node.ts +5 -0
  144. package/src/tools/ToolNode.ts +400 -9
  145. package/src/tools/__tests__/ToolNode.invalidToolCalls.test.ts +757 -0
  146. package/src/tools/__tests__/hitl.test.ts +58 -0
  147. package/src/types/hitl.ts +8 -0
  148. 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 { context as otelContext } from '@opentelemetry/api';
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 { langfuse, traceIdSeed, ...handlerParams } = params ?? {};
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
- private withRuntimeContext<T>(action: () => T): T {
190
- const activeContext = otelContext.active();
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(activeContext) ?? this.langfuse;
193
- const seed = this.getDeterministicTraceSeed();
194
- return withLangfuseRuntimeScope(
195
- {
196
- langfuse,
197
- traceIdSeed: resolveTraceIdSeedForSpan(activeContext) ?? seed,
198
- },
199
- action
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(() => super.handleChainStart(...args));
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(() => super.handleAgentAction(...args));
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(() => super.handleGenerationStart(...args));
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(() => super.handleChatModelStart(...args));
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(() => super.handleLLMStart(...args));
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(() => super.handleToolStart(...args));
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(() => super.handleRetrieverStart(...args));
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
 
@@ -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