@librechat/agents 3.1.95 → 3.1.97

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 (67) hide show
  1. package/dist/cjs/graphs/Graph.cjs +54 -21
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/instrumentation.cjs +120 -9
  4. package/dist/cjs/instrumentation.cjs.map +1 -1
  5. package/dist/cjs/langfuse.cjs +30 -226
  6. package/dist/cjs/langfuse.cjs.map +1 -1
  7. package/dist/cjs/langfuseToolOutputTracing.cjs +465 -0
  8. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -0
  9. package/dist/cjs/main.cjs +1 -0
  10. package/dist/cjs/main.cjs.map +1 -1
  11. package/dist/cjs/run.cjs +142 -69
  12. package/dist/cjs/run.cjs.map +1 -1
  13. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +29 -2
  14. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  15. package/dist/cjs/tools/ToolNode.cjs +20 -8
  16. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  17. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +10 -6
  18. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  19. package/dist/esm/graphs/Graph.mjs +56 -23
  20. package/dist/esm/graphs/Graph.mjs.map +1 -1
  21. package/dist/esm/instrumentation.mjs +118 -9
  22. package/dist/esm/instrumentation.mjs.map +1 -1
  23. package/dist/esm/langfuse.mjs +28 -224
  24. package/dist/esm/langfuse.mjs.map +1 -1
  25. package/dist/esm/langfuseToolOutputTracing.mjs +457 -0
  26. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -0
  27. package/dist/esm/main.mjs +1 -1
  28. package/dist/esm/run.mjs +144 -71
  29. package/dist/esm/run.mjs.map +1 -1
  30. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +29 -3
  31. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  32. package/dist/esm/tools/ToolNode.mjs +20 -8
  33. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  34. package/dist/esm/tools/subagent/SubagentExecutor.mjs +10 -6
  35. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  36. package/dist/types/graphs/Graph.d.ts +5 -1
  37. package/dist/types/instrumentation.d.ts +5 -1
  38. package/dist/types/langfuse.d.ts +6 -28
  39. package/dist/types/langfuseToolOutputTracing.d.ts +20 -0
  40. package/dist/types/run.d.ts +5 -1
  41. package/dist/types/tools/BashProgrammaticToolCalling.d.ts +1 -0
  42. package/dist/types/tools/ToolNode.d.ts +4 -1
  43. package/dist/types/tools/subagent/SubagentExecutor.d.ts +2 -0
  44. package/dist/types/types/graph.d.ts +30 -0
  45. package/dist/types/types/run.d.ts +6 -0
  46. package/dist/types/types/tools.d.ts +7 -0
  47. package/package.json +2 -1
  48. package/src/graphs/Graph.ts +90 -34
  49. package/src/instrumentation.ts +172 -11
  50. package/src/langfuse.ts +59 -324
  51. package/src/langfuseToolOutputTracing.ts +683 -0
  52. package/src/run.ts +190 -87
  53. package/src/specs/langfuse-callbacks.test.ts +178 -1
  54. package/src/specs/langfuse-config.test.ts +112 -76
  55. package/src/specs/langfuse-instrumentation.test.ts +283 -0
  56. package/src/specs/langfuse-metadata.test.ts +54 -1
  57. package/src/specs/langfuse-tool-output-tracing.test.ts +588 -0
  58. package/src/tools/BashProgrammaticToolCalling.ts +39 -5
  59. package/src/tools/ToolNode.ts +28 -7
  60. package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +54 -0
  61. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +72 -4
  62. package/src/tools/__tests__/SubagentExecutor.test.ts +32 -0
  63. package/src/tools/__tests__/ToolNode.langfuse.test.ts +41 -0
  64. package/src/tools/subagent/SubagentExecutor.ts +11 -6
  65. package/src/types/graph.ts +32 -0
  66. package/src/types/run.ts +6 -0
  67. package/src/types/tools.ts +7 -0
@@ -0,0 +1,683 @@
1
+ import { context, createContextKey } from '@opentelemetry/api';
2
+ import { LangfuseSpanProcessor } from '@langfuse/otel';
3
+ import { LangfuseOtelSpanAttributes } from '@langfuse/tracing';
4
+ import { AsyncLocalStorage } from 'node:async_hooks';
5
+ import type {
6
+ ReadableSpan,
7
+ Span,
8
+ SpanProcessor,
9
+ } from '@opentelemetry/sdk-trace-base';
10
+ import type { LangfuseSpanProcessorParams } from '@langfuse/otel';
11
+ import type { Context } from '@opentelemetry/api';
12
+ import type * as t from '@/types';
13
+
14
+ export const LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT = '[tool output redacted]';
15
+
16
+ const langfuseToolOutputTracingConfigKey = createContextKey(
17
+ 'librechat.langfuse.tool-output-tracing'
18
+ );
19
+ const langfuseConfigKey = createContextKey('librechat.langfuse.config');
20
+ const toolOutputTracingStorage =
21
+ new AsyncLocalStorage<ResolvedLangfuseToolOutputTracingConfig>();
22
+ const langfuseConfigStorage = new AsyncLocalStorage<t.LangfuseConfig>();
23
+
24
+ const CHAT_ROLES = new Set([
25
+ 'assistant',
26
+ 'developer',
27
+ 'human',
28
+ 'system',
29
+ 'user',
30
+ ]);
31
+
32
+ export type ResolvedLangfuseToolOutputTracingConfig = {
33
+ enabled: boolean;
34
+ redactedToolNames: Set<string>;
35
+ redactedToolNameMatchMode: 'exact' | 'partial';
36
+ redactionText: string;
37
+ };
38
+
39
+ type SpanWithAttributes = ReadableSpan & {
40
+ attributes: Record<string, unknown>;
41
+ };
42
+
43
+ type RedactionResult = {
44
+ value: unknown;
45
+ changed: boolean;
46
+ };
47
+
48
+ type RedactionContext = {
49
+ toolNamesByCallId: Map<string, string>;
50
+ };
51
+
52
+ const TOOL_OUTPUT_FIELD_KEYS = ['content', 'artifact'];
53
+
54
+ function isRecord(value: unknown): value is Record<string, unknown> {
55
+ return value != null && typeof value === 'object' && !Array.isArray(value);
56
+ }
57
+
58
+ function isPresent(value: unknown): value is string {
59
+ return typeof value === 'string' && value.trim() !== '';
60
+ }
61
+
62
+ function parseBoolean(value: string | undefined): boolean | undefined {
63
+ if (value == null) {
64
+ return undefined;
65
+ }
66
+
67
+ const normalized = value.trim().toLowerCase();
68
+ if (['1', 'true', 'yes', 'on'].includes(normalized)) {
69
+ return true;
70
+ }
71
+ if (['0', 'false', 'no', 'off'].includes(normalized)) {
72
+ return false;
73
+ }
74
+
75
+ return undefined;
76
+ }
77
+
78
+ function normalizeToolName(name: string): string {
79
+ return name.trim().toLowerCase();
80
+ }
81
+
82
+ function normalizeToolNames(names: string[] | undefined): Set<string> {
83
+ const normalized = new Set<string>();
84
+ for (const name of names ?? []) {
85
+ if (isPresent(name)) {
86
+ normalized.add(normalizeToolName(name));
87
+ }
88
+ }
89
+ return normalized;
90
+ }
91
+
92
+ function parseToolNames(value: string | undefined): string[] | undefined {
93
+ if (!isPresent(value)) {
94
+ return undefined;
95
+ }
96
+
97
+ return value
98
+ .split(',')
99
+ .map((name) => name.trim())
100
+ .filter((name) => name !== '');
101
+ }
102
+
103
+ function getEnvToolOutputTracingEnabled(): boolean | undefined {
104
+ const traceToolOutputs = parseBoolean(
105
+ process.env.LANGFUSE_TRACE_TOOL_OUTPUTS
106
+ );
107
+ if (traceToolOutputs != null) {
108
+ return traceToolOutputs;
109
+ }
110
+
111
+ const redactToolOutputs = parseBoolean(
112
+ process.env.LANGFUSE_REDACT_TOOL_OUTPUTS
113
+ );
114
+ if (redactToolOutputs != null) {
115
+ return !redactToolOutputs;
116
+ }
117
+
118
+ return parseBoolean(process.env.LANGFUSE_TOOL_OUTPUT_TRACING_ENABLED);
119
+ }
120
+
121
+ function getEnvRedactedToolNames(): string[] | undefined {
122
+ return (
123
+ parseToolNames(process.env.LANGFUSE_REDACT_TOOL_OUTPUT_NAMES) ??
124
+ parseToolNames(process.env.LANGFUSE_REDACT_TOOL_NAMES)
125
+ );
126
+ }
127
+
128
+ function getEnvRedactionText(): string | undefined {
129
+ return isPresent(process.env.LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT)
130
+ ? process.env.LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT
131
+ : undefined;
132
+ }
133
+
134
+ function getEnvToolNameMatchMode(): 'exact' | 'partial' | undefined {
135
+ const mode = (
136
+ process.env.LANGFUSE_REDACT_TOOL_OUTPUT_NAME_MATCH_MODE ??
137
+ process.env.LANGFUSE_REDACT_TOOL_NAME_MATCH_MODE
138
+ )
139
+ ?.trim()
140
+ .toLowerCase();
141
+ if (mode === 'exact' || mode === 'partial') {
142
+ return mode;
143
+ }
144
+ return undefined;
145
+ }
146
+
147
+ function resolveToolOutputTracingConfig(
148
+ runLangfuse?: t.LangfuseConfig,
149
+ agentLangfuse?: t.LangfuseConfig
150
+ ): ResolvedLangfuseToolOutputTracingConfig {
151
+ const runConfig = runLangfuse?.toolOutputTracing;
152
+ const agentConfig = agentLangfuse?.toolOutputTracing;
153
+
154
+ return {
155
+ enabled:
156
+ agentConfig?.enabled ??
157
+ runConfig?.enabled ??
158
+ getEnvToolOutputTracingEnabled() ??
159
+ true,
160
+ redactedToolNames: normalizeToolNames(
161
+ agentConfig?.redactedToolNames ??
162
+ runConfig?.redactedToolNames ??
163
+ getEnvRedactedToolNames()
164
+ ),
165
+ redactedToolNameMatchMode:
166
+ agentConfig?.redactedToolNameMatchMode ??
167
+ runConfig?.redactedToolNameMatchMode ??
168
+ getEnvToolNameMatchMode() ??
169
+ 'exact',
170
+ redactionText:
171
+ agentConfig?.redactionText ??
172
+ runConfig?.redactionText ??
173
+ getEnvRedactionText() ??
174
+ LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT,
175
+ };
176
+ }
177
+
178
+ function shouldApplyToolOutputRedaction(
179
+ config: ResolvedLangfuseToolOutputTracingConfig
180
+ ): boolean {
181
+ return config.enabled === false || config.redactedToolNames.size > 0;
182
+ }
183
+
184
+ function toolNameMatches(
185
+ toolName: string | undefined,
186
+ config: ResolvedLangfuseToolOutputTracingConfig
187
+ ): boolean {
188
+ if (!isPresent(toolName)) {
189
+ return false;
190
+ }
191
+
192
+ const normalizedToolName = normalizeToolName(toolName);
193
+ if (config.redactedToolNameMatchMode === 'partial') {
194
+ for (const redactedToolName of config.redactedToolNames) {
195
+ if (normalizedToolName.includes(redactedToolName)) {
196
+ return true;
197
+ }
198
+ }
199
+ return false;
200
+ }
201
+
202
+ return config.redactedToolNames.has(normalizedToolName);
203
+ }
204
+
205
+ function shouldRedactTool(
206
+ toolName: string | undefined,
207
+ config: ResolvedLangfuseToolOutputTracingConfig
208
+ ): boolean {
209
+ return config.enabled === false || toolNameMatches(toolName, config);
210
+ }
211
+
212
+ function getStringField(
213
+ value: Record<string, unknown>,
214
+ key: string
215
+ ): string | undefined {
216
+ const field = value[key];
217
+ return typeof field === 'string' ? field : undefined;
218
+ }
219
+
220
+ function getNestedStringField(
221
+ value: Record<string, unknown>,
222
+ objectKey: string,
223
+ fieldKey: string
224
+ ): string | undefined {
225
+ const nested = value[objectKey];
226
+ if (!isRecord(nested)) {
227
+ return undefined;
228
+ }
229
+ return getStringField(nested, fieldKey);
230
+ }
231
+
232
+ function getSerializedToolCallId(
233
+ value: Record<string, unknown>
234
+ ): string | undefined {
235
+ return (
236
+ getStringField(value, 'tool_call_id') ??
237
+ getNestedStringField(value, 'kwargs', 'tool_call_id') ??
238
+ getNestedStringField(value, 'additional_kwargs', 'tool_call_id') ??
239
+ getNestedStringField(value, 'data', 'tool_call_id') ??
240
+ (typeof value.id === 'string' ? value.id : undefined)
241
+ );
242
+ }
243
+
244
+ function getSerializedToolName(
245
+ value: Record<string, unknown>,
246
+ redactionContext?: RedactionContext
247
+ ): string | undefined {
248
+ const role = getStringField(value, 'role');
249
+ const explicitName =
250
+ getStringField(value, 'name') ??
251
+ getStringField(value, 'tool_name') ??
252
+ getNestedStringField(value, 'function', 'name') ??
253
+ getNestedStringField(value, 'kwargs', 'name') ??
254
+ getNestedStringField(value, 'additional_kwargs', 'name') ??
255
+ getNestedStringField(value, 'data', 'name') ??
256
+ (role != null && role.toLowerCase() !== 'tool' ? role : undefined);
257
+
258
+ if (explicitName != null) {
259
+ return explicitName;
260
+ }
261
+
262
+ const toolCallId = getSerializedToolCallId(value);
263
+ return toolCallId != null
264
+ ? redactionContext?.toolNamesByCallId.get(toolCallId)
265
+ : undefined;
266
+ }
267
+
268
+ function hasToolMessageIdentity(value: Record<string, unknown>): boolean {
269
+ const type = getStringField(value, 'type') ?? getStringField(value, '_type');
270
+ if (type === 'tool' || type === 'tool_message') {
271
+ return true;
272
+ }
273
+
274
+ const id = value.id;
275
+ if (
276
+ Array.isArray(id) &&
277
+ id.some((part) => typeof part === 'string' && part.includes('ToolMessage'))
278
+ ) {
279
+ return true;
280
+ }
281
+
282
+ if (
283
+ 'tool_call_id' in value ||
284
+ getNestedStringField(value, 'kwargs', 'tool_call_id') != null ||
285
+ getNestedStringField(value, 'additional_kwargs', 'tool_call_id') != null
286
+ ) {
287
+ return true;
288
+ }
289
+
290
+ const role = getStringField(value, 'role');
291
+ return (
292
+ role != null &&
293
+ !CHAT_ROLES.has(role.toLowerCase()) &&
294
+ ('content' in value || isRecord(value.kwargs) || isRecord(value.data))
295
+ );
296
+ }
297
+
298
+ function redactToolContentFields(
299
+ value: Record<string, unknown>,
300
+ config: ResolvedLangfuseToolOutputTracingConfig
301
+ ): Record<string, unknown> {
302
+ const next = { ...value };
303
+
304
+ for (const outputKey of TOOL_OUTPUT_FIELD_KEYS) {
305
+ if (outputKey in next) {
306
+ next[outputKey] = config.redactionText;
307
+ }
308
+ }
309
+
310
+ for (const nestedKey of ['kwargs', 'data', 'additional_kwargs']) {
311
+ const nested = next[nestedKey];
312
+ if (!isRecord(nested)) {
313
+ continue;
314
+ }
315
+ const nextNested = { ...nested };
316
+ let changed = false;
317
+ for (const outputKey of TOOL_OUTPUT_FIELD_KEYS) {
318
+ if (outputKey in nextNested) {
319
+ nextNested[outputKey] = config.redactionText;
320
+ changed = true;
321
+ }
322
+ }
323
+ if (changed) {
324
+ next[nestedKey] = nextNested;
325
+ }
326
+ }
327
+
328
+ return next;
329
+ }
330
+
331
+ function collectToolCallNames(
332
+ value: unknown,
333
+ redactionContext: RedactionContext
334
+ ): void {
335
+ if (Array.isArray(value)) {
336
+ for (const item of value) {
337
+ collectToolCallNames(item, redactionContext);
338
+ }
339
+ return;
340
+ }
341
+
342
+ if (!isRecord(value)) {
343
+ return;
344
+ }
345
+
346
+ const toolCallId = getSerializedToolCallId(value);
347
+ const toolName = getSerializedToolName(value);
348
+ if (toolCallId != null && toolName != null) {
349
+ redactionContext.toolNamesByCallId.set(toolCallId, toolName);
350
+ }
351
+
352
+ for (const child of Object.values(value)) {
353
+ collectToolCallNames(child, redactionContext);
354
+ }
355
+ }
356
+
357
+ function redactValue(
358
+ value: unknown,
359
+ config: ResolvedLangfuseToolOutputTracingConfig,
360
+ redactionContext: RedactionContext
361
+ ): RedactionResult {
362
+ if (Array.isArray(value)) {
363
+ let changed = false;
364
+ const next: unknown[] = [];
365
+ for (const item of value) {
366
+ const result = redactValue(item, config, redactionContext);
367
+ if (result.changed) {
368
+ changed = true;
369
+ }
370
+ next.push(result.value);
371
+ }
372
+ return changed ? { value: next, changed } : { value, changed };
373
+ }
374
+
375
+ if (!isRecord(value)) {
376
+ return { value, changed: false };
377
+ }
378
+
379
+ const toolName = getSerializedToolName(value, redactionContext);
380
+ if (hasToolMessageIdentity(value) && shouldRedactTool(toolName, config)) {
381
+ return {
382
+ value: redactToolContentFields(value, config),
383
+ changed: true,
384
+ };
385
+ }
386
+
387
+ let changed = false;
388
+ const next: Record<string, unknown> = {};
389
+ for (const [key, child] of Object.entries(value)) {
390
+ const result = redactValue(child, config, redactionContext);
391
+ if (result.changed) {
392
+ changed = true;
393
+ }
394
+ next[key] = result.value;
395
+ }
396
+
397
+ return changed ? { value: next, changed } : { value, changed };
398
+ }
399
+
400
+ function redactSerializedValue(
401
+ value: unknown,
402
+ config: ResolvedLangfuseToolOutputTracingConfig
403
+ ): RedactionResult {
404
+ const redactionContext: RedactionContext = {
405
+ toolNamesByCallId: new Map(),
406
+ };
407
+ if (typeof value !== 'string') {
408
+ collectToolCallNames(value, redactionContext);
409
+ return redactValue(value, config, redactionContext);
410
+ }
411
+
412
+ const trimmed = value.trim();
413
+ if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
414
+ return { value, changed: false };
415
+ }
416
+
417
+ try {
418
+ const parsed = JSON.parse(value) as unknown;
419
+ collectToolCallNames(parsed, redactionContext);
420
+ const result = redactValue(parsed, config, redactionContext);
421
+ return result.changed
422
+ ? { value: JSON.stringify(result.value), changed: true }
423
+ : { value, changed: false };
424
+ } catch {
425
+ return { value, changed: false };
426
+ }
427
+ }
428
+
429
+ function redactAttribute(
430
+ attributes: Record<string, unknown>,
431
+ key: string,
432
+ config: ResolvedLangfuseToolOutputTracingConfig
433
+ ): void {
434
+ if (!(key in attributes)) {
435
+ return;
436
+ }
437
+
438
+ const result = redactSerializedValue(attributes[key], config);
439
+ if (result.changed) {
440
+ attributes[key] = result.value;
441
+ }
442
+ }
443
+
444
+ function isToolObservation(attributes: Record<string, unknown>): boolean {
445
+ const type = attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE];
446
+ return typeof type === 'string' && type.toLowerCase() === 'tool';
447
+ }
448
+
449
+ function redactToolObservationOutput(
450
+ span: ReadableSpan,
451
+ attributes: Record<string, unknown>,
452
+ config: ResolvedLangfuseToolOutputTracingConfig
453
+ ): void {
454
+ if (
455
+ !(
456
+ isToolObservation(attributes) &&
457
+ shouldRedactTool(span.name, config) &&
458
+ LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT in attributes
459
+ )
460
+ ) {
461
+ return;
462
+ }
463
+
464
+ attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT] =
465
+ config.redactionText;
466
+ }
467
+
468
+ export function redactLangfuseSpanToolOutputs(
469
+ span: ReadableSpan,
470
+ config: ResolvedLangfuseToolOutputTracingConfig
471
+ ): void {
472
+ if (!shouldApplyToolOutputRedaction(config)) {
473
+ return;
474
+ }
475
+
476
+ const attributes = (span as SpanWithAttributes).attributes;
477
+ redactToolObservationOutput(span, attributes, config);
478
+
479
+ for (const key of [
480
+ LangfuseOtelSpanAttributes.OBSERVATION_INPUT,
481
+ LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT,
482
+ LangfuseOtelSpanAttributes.TRACE_INPUT,
483
+ LangfuseOtelSpanAttributes.TRACE_OUTPUT,
484
+ ]) {
485
+ redactAttribute(attributes, key, config);
486
+ }
487
+ }
488
+
489
+ function getContextToolOutputTracingConfig(
490
+ activeContext: Context
491
+ ): ResolvedLangfuseToolOutputTracingConfig | undefined {
492
+ const asyncConfig = toolOutputTracingStorage.getStore();
493
+ if (asyncConfig != null) {
494
+ return asyncConfig;
495
+ }
496
+
497
+ const value = activeContext.getValue(langfuseToolOutputTracingConfigKey);
498
+ return isRecord(value)
499
+ ? (value as ResolvedLangfuseToolOutputTracingConfig)
500
+ : undefined;
501
+ }
502
+
503
+ export function getContextLangfuseConfig(
504
+ activeContext: Context
505
+ ): t.LangfuseConfig | undefined {
506
+ const asyncConfig = langfuseConfigStorage.getStore();
507
+ if (asyncConfig != null) {
508
+ return asyncConfig;
509
+ }
510
+
511
+ const value = activeContext.getValue(langfuseConfigKey);
512
+ return isRecord(value) ? (value as t.LangfuseConfig) : undefined;
513
+ }
514
+
515
+ class ToolOutputRedactingLangfuseSpanProcessor implements SpanProcessor {
516
+ private readonly processor: LangfuseSpanProcessor;
517
+ private readonly fallbackConfig?: ResolvedLangfuseToolOutputTracingConfig;
518
+ private readonly spanConfigs = new WeakMap<
519
+ object,
520
+ ResolvedLangfuseToolOutputTracingConfig
521
+ >();
522
+
523
+ constructor(
524
+ params?: LangfuseSpanProcessorParams,
525
+ fallbackConfig?: ResolvedLangfuseToolOutputTracingConfig
526
+ ) {
527
+ this.processor = new LangfuseSpanProcessor(params);
528
+ this.fallbackConfig = fallbackConfig;
529
+ }
530
+
531
+ onStart(span: Span, parentContext: Context): void {
532
+ const config =
533
+ getContextToolOutputTracingConfig(parentContext) ?? this.fallbackConfig;
534
+ if (config != null) {
535
+ this.spanConfigs.set(span, config);
536
+ }
537
+ this.processor.onStart(span, parentContext);
538
+ }
539
+
540
+ onEnd(span: ReadableSpan): void {
541
+ const config =
542
+ this.spanConfigs.get(span) ??
543
+ toolOutputTracingStorage.getStore() ??
544
+ this.fallbackConfig ??
545
+ resolveToolOutputTracingConfig();
546
+ redactLangfuseSpanToolOutputs(span, config);
547
+ this.processor.onEnd(span);
548
+ }
549
+
550
+ forceFlush(): Promise<void> {
551
+ return this.processor.forceFlush();
552
+ }
553
+
554
+ shutdown(): Promise<void> {
555
+ return this.processor.shutdown();
556
+ }
557
+ }
558
+
559
+ export function createLangfuseSpanProcessor(
560
+ params?: LangfuseSpanProcessorParams,
561
+ runLangfuse?: t.LangfuseConfig,
562
+ agentLangfuse?: t.LangfuseConfig
563
+ ): SpanProcessor {
564
+ const fallbackConfig =
565
+ runLangfuse != null || agentLangfuse != null
566
+ ? resolveToolOutputTracingConfig(runLangfuse, agentLangfuse)
567
+ : undefined;
568
+ return new ToolOutputRedactingLangfuseSpanProcessor(params, fallbackConfig);
569
+ }
570
+
571
+ export function withLangfuseToolOutputTracingConfig<T>(
572
+ runLangfuse: t.LangfuseConfig | undefined,
573
+ action: () => T,
574
+ agentLangfuse?: t.LangfuseConfig
575
+ ): T {
576
+ const langfuse = resolveLangfuseConfig(runLangfuse, agentLangfuse);
577
+ const hasNoToolOutputConfig =
578
+ runLangfuse?.toolOutputTracing == null &&
579
+ agentLangfuse?.toolOutputTracing == null;
580
+
581
+ if (langfuse == null && hasNoToolOutputConfig) {
582
+ return action();
583
+ }
584
+
585
+ const config = hasNoToolOutputConfig
586
+ ? undefined
587
+ : resolveToolOutputTracingConfig(runLangfuse, agentLangfuse);
588
+ let activeContext = context.active();
589
+ if (langfuse != null) {
590
+ activeContext = activeContext.setValue(langfuseConfigKey, langfuse);
591
+ }
592
+ if (config != null) {
593
+ activeContext = activeContext.setValue(
594
+ langfuseToolOutputTracingConfigKey,
595
+ config
596
+ );
597
+ }
598
+
599
+ const runWithContext = (): T => context.with(activeContext, action);
600
+ const runWithToolOutputConfig = (): T =>
601
+ config != null
602
+ ? toolOutputTracingStorage.run(config, runWithContext)
603
+ : runWithContext();
604
+
605
+ return langfuse != null
606
+ ? langfuseConfigStorage.run(langfuse, runWithToolOutputConfig)
607
+ : runWithToolOutputConfig();
608
+ }
609
+
610
+ function hasLangfuseEnvKeys(): boolean {
611
+ return (
612
+ isPresent(process.env.LANGFUSE_SECRET_KEY) &&
613
+ isPresent(process.env.LANGFUSE_PUBLIC_KEY)
614
+ );
615
+ }
616
+
617
+ function hasLangfuseConfigKeys(langfuse?: t.LangfuseConfig): boolean {
618
+ if (langfuse == null) {
619
+ return false;
620
+ }
621
+ return (
622
+ isPresent(langfuse.secretKey) &&
623
+ isPresent(langfuse.publicKey)
624
+ );
625
+ }
626
+
627
+ export function shouldTraceToolNodeForLangfuse({
628
+ runLangfuse,
629
+ agentLangfuse,
630
+ }: {
631
+ runLangfuse?: t.LangfuseConfig;
632
+ agentLangfuse?: t.LangfuseConfig;
633
+ }): boolean {
634
+ const langfuse = resolveLangfuseConfig(runLangfuse, agentLangfuse);
635
+ if (langfuse?.enabled === false) {
636
+ return false;
637
+ }
638
+
639
+ const explicit = langfuse?.toolNodeTracing?.enabled;
640
+ if (explicit != null) {
641
+ return (
642
+ explicit &&
643
+ (hasLangfuseConfigKeys(langfuse) || hasLangfuseEnvKeys())
644
+ );
645
+ }
646
+
647
+ return hasLangfuseConfigKeys(langfuse) || hasLangfuseEnvKeys();
648
+ }
649
+
650
+ export function resolveLangfuseConfig(
651
+ runLangfuse?: t.LangfuseConfig,
652
+ agentLangfuse?: t.LangfuseConfig
653
+ ): t.LangfuseConfig | undefined {
654
+ if (runLangfuse == null) {
655
+ return agentLangfuse;
656
+ }
657
+ if (agentLangfuse == null) {
658
+ return runLangfuse;
659
+ }
660
+
661
+ const toolNodeTracing =
662
+ runLangfuse.toolNodeTracing != null || agentLangfuse.toolNodeTracing != null
663
+ ? {
664
+ ...runLangfuse.toolNodeTracing,
665
+ ...agentLangfuse.toolNodeTracing,
666
+ }
667
+ : undefined;
668
+ const toolOutputTracing =
669
+ runLangfuse.toolOutputTracing != null ||
670
+ agentLangfuse.toolOutputTracing != null
671
+ ? {
672
+ ...runLangfuse.toolOutputTracing,
673
+ ...agentLangfuse.toolOutputTracing,
674
+ }
675
+ : undefined;
676
+
677
+ return {
678
+ ...runLangfuse,
679
+ ...agentLangfuse,
680
+ ...(toolNodeTracing != null ? { toolNodeTracing } : {}),
681
+ ...(toolOutputTracing != null ? { toolOutputTracing } : {}),
682
+ };
683
+ }