@librechat/agents 3.1.96 → 3.1.98

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