@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
@@ -1,105 +1,104 @@
1
- import { LangfuseSpanProcessor } from '@langfuse/otel';
2
- import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';
3
- import { HumanMessage } from '@langchain/core/messages';
4
- import type { Serialized } from '@langchain/core/load/serializable';
5
- import { createLangfuseHandler } from '@/langfuse';
6
-
7
- const mockSpan = {
8
- end: jest.fn(),
9
- setAttributes: jest.fn(),
10
- setStatus: jest.fn(),
11
- };
12
- const mockStartSpan = jest.fn(() => mockSpan);
13
- const mockGetTracer = jest.fn(() => ({
14
- startSpan: mockStartSpan,
15
- }));
1
+ import { CallbackHandler } from '@langfuse/langchain';
2
+ import {
3
+ createLangfuseHandler,
4
+ hasLangfuseConfigCredentials,
5
+ shouldCreateLangfuseHandler,
6
+ } from '@/langfuse';
16
7
 
17
- jest.mock('@langfuse/otel', () => ({
18
- LangfuseSpanProcessor: jest.fn().mockImplementation(() => ({})),
19
- isDefaultExportSpan: jest.fn(() => false),
8
+ jest.mock('@langfuse/langchain', () => ({
9
+ CallbackHandler: jest.fn().mockImplementation((params) => ({ params })),
20
10
  }));
21
11
 
22
- jest.mock('@opentelemetry/sdk-trace-base', () => ({
23
- BasicTracerProvider: jest.fn().mockImplementation(() => ({
24
- forceFlush: jest.fn(),
25
- getTracer: mockGetTracer,
26
- shutdown: jest.fn(),
27
- })),
28
- }));
12
+ const MockedCallbackHandler = CallbackHandler as jest.MockedClass<
13
+ typeof CallbackHandler
14
+ >;
29
15
 
30
16
  describe('createLangfuseHandler', () => {
17
+ const originalEnv = process.env;
18
+
31
19
  beforeEach(() => {
32
20
  jest.clearAllMocks();
21
+ process.env = { ...originalEnv };
22
+ delete process.env.LANGFUSE_PUBLIC_KEY;
23
+ delete process.env.LANGFUSE_SECRET_KEY;
24
+ delete process.env.LANGFUSE_BASE_URL;
25
+ delete process.env.LANGFUSE_BASEURL;
26
+ });
27
+
28
+ afterEach(() => {
29
+ process.env = originalEnv;
33
30
  });
34
31
 
35
- it('creates a handler when keys are provided and baseUrl is omitted', () => {
32
+ it('creates the official Langfuse callback handler when env keys are present', () => {
33
+ process.env.LANGFUSE_PUBLIC_KEY = 'pk-env';
34
+ process.env.LANGFUSE_SECRET_KEY = 'sk-env';
35
+ process.env.LANGFUSE_BASE_URL = 'https://langfuse.env';
36
+
37
+ const handler = createLangfuseHandler({
38
+ userId: 'user-1',
39
+ sessionId: 'thread-1',
40
+ traceMetadata: {
41
+ messageId: 'message-1',
42
+ agentId: 'agent-1',
43
+ agentName: 'DWAINE',
44
+ },
45
+ tags: ['librechat', 'agent'],
46
+ });
47
+
48
+ expect(handler).toBeDefined();
49
+ expect(MockedCallbackHandler).toHaveBeenCalledWith({
50
+ userId: 'user-1',
51
+ sessionId: 'thread-1',
52
+ traceMetadata: {
53
+ messageId: 'message-1',
54
+ agentId: 'agent-1',
55
+ agentName: 'DWAINE',
56
+ },
57
+ tags: ['librechat', 'agent'],
58
+ });
59
+ });
60
+
61
+ it('creates a handler for explicit credentials supplied in config', () => {
36
62
  const handler = createLangfuseHandler({
37
63
  langfuse: {
38
- enabled: true,
39
64
  publicKey: 'pk-test',
40
65
  secretKey: 'sk-test',
41
66
  },
42
67
  });
43
68
 
44
69
  expect(handler).toBeDefined();
45
- expect(LangfuseSpanProcessor).toHaveBeenCalledWith(
46
- expect.objectContaining({
47
- publicKey: 'pk-test',
48
- secretKey: 'sk-test',
49
- exportMode: 'immediate',
50
- })
51
- );
52
- expect(
53
- (LangfuseSpanProcessor as jest.Mock).mock.calls[0][0].baseUrl
54
- ).toBeUndefined();
55
- expect(BasicTracerProvider).toHaveBeenCalledTimes(1);
70
+ expect(MockedCallbackHandler).toHaveBeenCalledTimes(1);
56
71
  });
57
72
 
58
- it('starts per-agent spans with v5 trace attributes', async () => {
73
+ it('hydrates redaction-only config from env keys', () => {
74
+ process.env.LANGFUSE_PUBLIC_KEY = 'pk-env';
75
+ process.env.LANGFUSE_SECRET_KEY = 'sk-env';
76
+ process.env.LANGFUSE_BASE_URL = 'https://langfuse.env';
77
+
59
78
  const handler = createLangfuseHandler({
60
79
  langfuse: {
61
- enabled: true,
80
+ toolOutputTracing: { enabled: false },
81
+ },
82
+ });
83
+
84
+ expect(handler).toBeDefined();
85
+ expect(MockedCallbackHandler).toHaveBeenCalledTimes(1);
86
+ });
87
+
88
+ it('does not create a handler when Langfuse is disabled', () => {
89
+ const handler = createLangfuseHandler({
90
+ langfuse: {
91
+ enabled: false,
62
92
  publicKey: 'pk-test',
63
93
  secretKey: 'sk-test',
64
94
  },
65
- userId: 'user-1',
66
- sessionId: 'thread-1',
67
- traceMetadata: {
68
- messageId: 'message-1',
69
- agentId: 'agent-1',
70
- agentName: 'DWAINE',
71
- },
72
- tags: ['librechat', 'agent'],
73
95
  });
74
96
 
75
- await handler?.handleChatModelStart(
76
- {
77
- id: ['langchain', 'chat_models', 'ChatOpenAI'],
78
- kwargs: { model: 'gpt-4o' },
79
- } as unknown as Serialized,
80
- [[new HumanMessage('hello')]],
81
- 'run-1'
82
- );
83
-
84
- expect(mockGetTracer).toHaveBeenCalledWith('langfuse-sdk');
85
- expect(mockStartSpan).toHaveBeenCalledWith(
86
- 'gpt-4o',
87
- expect.objectContaining({
88
- attributes: expect.objectContaining({
89
- 'langfuse.trace.name': 'LibreChat Agent: DWAINE',
90
- 'langfuse.trace.metadata.agentId': 'agent-1',
91
- 'langfuse.trace.metadata.messageId': 'message-1',
92
- 'langfuse.observation.model.name': 'gpt-4o',
93
- 'langfuse.observation.type': 'generation',
94
- 'user.id': 'user-1',
95
- 'session.id': 'thread-1',
96
- 'langfuse.trace.tags': ['librechat', 'agent'],
97
- }),
98
- })
99
- );
97
+ expect(handler).toBeUndefined();
98
+ expect(MockedCallbackHandler).not.toHaveBeenCalled();
100
99
  });
101
100
 
102
- it('does not create a handler when a required key is missing', () => {
101
+ it('does not create a handler when credentials are unavailable', () => {
103
102
  const handler = createLangfuseHandler({
104
103
  langfuse: {
105
104
  enabled: true,
@@ -108,7 +107,44 @@ describe('createLangfuseHandler', () => {
108
107
  });
109
108
 
110
109
  expect(handler).toBeUndefined();
111
- expect(LangfuseSpanProcessor).not.toHaveBeenCalled();
112
- expect(BasicTracerProvider).not.toHaveBeenCalled();
110
+ expect(MockedCallbackHandler).not.toHaveBeenCalled();
111
+ });
112
+
113
+ it('detects complete config credentials', () => {
114
+ expect(
115
+ hasLangfuseConfigCredentials({
116
+ publicKey: 'pk-test',
117
+ secretKey: 'sk-test',
118
+ })
119
+ ).toBe(true);
120
+ expect(
121
+ hasLangfuseConfigCredentials({
122
+ publicKey: 'pk-test',
123
+ })
124
+ ).toBe(false);
125
+ });
126
+
127
+ it('uses env credentials for redaction-only configs', () => {
128
+ process.env.LANGFUSE_PUBLIC_KEY = 'pk-env';
129
+ process.env.LANGFUSE_SECRET_KEY = 'sk-env';
130
+ process.env.LANGFUSE_BASE_URL = 'https://langfuse.env';
131
+
132
+ expect(
133
+ shouldCreateLangfuseHandler({
134
+ toolOutputTracing: { enabled: false },
135
+ })
136
+ ).toBe(true);
137
+ });
138
+
139
+ it('uses env credentials with a config-provided baseUrl', () => {
140
+ process.env.LANGFUSE_PUBLIC_KEY = 'pk-env';
141
+ process.env.LANGFUSE_SECRET_KEY = 'sk-env';
142
+
143
+ expect(
144
+ shouldCreateLangfuseHandler({
145
+ baseUrl: 'https://langfuse.config',
146
+ toolOutputTracing: { enabled: false },
147
+ })
148
+ ).toBe(true);
113
149
  });
114
150
  });
@@ -0,0 +1,283 @@
1
+ const mockLangfuseSpanProcessorInstance = {};
2
+ const mockLangfuseSpanProcessor = jest.fn(
3
+ () => mockLangfuseSpanProcessorInstance
4
+ );
5
+ const mockSetLangfuseTracerProvider = jest.fn();
6
+ let mockContextHasActiveValue = false;
7
+ const mockContextKey = Symbol('mock-context-key');
8
+ const mockContextActive = jest.fn(() => ({
9
+ getValue: jest.fn(() => mockContextHasActiveValue),
10
+ }));
11
+ const mockContextWith = jest.fn(
12
+ (_contextValue: unknown, callback: () => boolean) => callback()
13
+ );
14
+ const mockSetGlobalContextManager = jest.fn(() => true);
15
+ const mockRootContext = {
16
+ setValue: jest.fn(() => ({})),
17
+ };
18
+ const mockContextManager = {
19
+ disable: jest.fn(),
20
+ enable: jest.fn(),
21
+ };
22
+ const mockAsyncLocalStorageContextManager = jest.fn(() => mockContextManager);
23
+ const mockTracerProvider = {
24
+ forceFlush: jest.fn(),
25
+ getTracer: jest.fn(),
26
+ shutdown: jest.fn(),
27
+ };
28
+ type BasicTracerProviderInput = {
29
+ spanProcessors: Array<{
30
+ forceFlush?: unknown;
31
+ onEnd?: unknown;
32
+ onStart?: unknown;
33
+ shutdown?: unknown;
34
+ }>;
35
+ };
36
+ type RoutingSpanProcessorForTest = BasicTracerProviderInput['spanProcessors'][0] & {
37
+ processors: Map<
38
+ string,
39
+ {
40
+ fallbackConfig?: {
41
+ enabled?: boolean;
42
+ };
43
+ }
44
+ >;
45
+ };
46
+ const mockBasicTracerProvider = jest.fn(
47
+ (_input?: BasicTracerProviderInput) => mockTracerProvider
48
+ );
49
+
50
+ jest.mock('@langfuse/otel', () => ({
51
+ LangfuseSpanProcessor: mockLangfuseSpanProcessor,
52
+ isDefaultExportSpan: jest.fn(() => false),
53
+ }));
54
+
55
+ jest.mock('@langfuse/tracing', () => ({
56
+ ...jest.requireActual('@langfuse/tracing'),
57
+ setLangfuseTracerProvider: mockSetLangfuseTracerProvider,
58
+ }));
59
+
60
+ jest.mock('@opentelemetry/api', () => ({
61
+ ...jest.requireActual('@opentelemetry/api'),
62
+ context: {
63
+ ...jest.requireActual('@opentelemetry/api').context,
64
+ active: mockContextActive,
65
+ setGlobalContextManager: mockSetGlobalContextManager,
66
+ with: mockContextWith,
67
+ },
68
+ createContextKey: jest.fn(() => mockContextKey),
69
+ ROOT_CONTEXT: mockRootContext,
70
+ }));
71
+
72
+ jest.mock('@opentelemetry/context-async-hooks', () => ({
73
+ AsyncLocalStorageContextManager: mockAsyncLocalStorageContextManager,
74
+ }));
75
+
76
+ jest.mock('@opentelemetry/sdk-trace-base', () => ({
77
+ BasicTracerProvider: mockBasicTracerProvider,
78
+ SpanStatusCode: jest.requireActual('@opentelemetry/api').SpanStatusCode,
79
+ }));
80
+
81
+ describe('Langfuse instrumentation', () => {
82
+ const originalEnv = process.env;
83
+
84
+ beforeEach(() => {
85
+ jest.resetModules();
86
+ jest.clearAllMocks();
87
+ mockContextHasActiveValue = false;
88
+ mockSetGlobalContextManager.mockReturnValue(true);
89
+ process.env = { ...originalEnv };
90
+ delete process.env.LANGFUSE_SECRET_KEY;
91
+ delete process.env.LANGFUSE_PUBLIC_KEY;
92
+ delete process.env.LANGFUSE_BASE_URL;
93
+ delete process.env.LANGFUSE_BASEURL;
94
+ });
95
+
96
+ afterEach(() => {
97
+ process.env = originalEnv;
98
+ });
99
+
100
+ it('does not initialize tracing when Langfuse env vars are missing', async () => {
101
+ const { initializeLangfuseTracingFromEnv } = await import(
102
+ '@/instrumentation'
103
+ );
104
+
105
+ expect(initializeLangfuseTracingFromEnv()).toBeUndefined();
106
+ expect(mockLangfuseSpanProcessor).not.toHaveBeenCalled();
107
+ expect(mockBasicTracerProvider).not.toHaveBeenCalled();
108
+ expect(mockAsyncLocalStorageContextManager).not.toHaveBeenCalled();
109
+ expect(mockSetLangfuseTracerProvider).not.toHaveBeenCalled();
110
+ });
111
+
112
+ it('registers an isolated Langfuse tracer provider from env config', async () => {
113
+ process.env.LANGFUSE_SECRET_KEY = 'sk-test';
114
+ process.env.LANGFUSE_PUBLIC_KEY = 'pk-test';
115
+ process.env.LANGFUSE_BASE_URL = 'https://langfuse.test';
116
+
117
+ const { initializeLangfuseTracingFromEnv } = await import(
118
+ '@/instrumentation'
119
+ );
120
+ const provider = initializeLangfuseTracingFromEnv();
121
+
122
+ expect(provider).toBe(mockTracerProvider);
123
+ expect(mockLangfuseSpanProcessor).toHaveBeenCalledTimes(1);
124
+ const providerInput = mockBasicTracerProvider.mock
125
+ .calls[0][0] as BasicTracerProviderInput;
126
+ expect(providerInput.spanProcessors).toHaveLength(1);
127
+ expect(providerInput.spanProcessors[0]).not.toBe(
128
+ mockLangfuseSpanProcessorInstance
129
+ );
130
+ expect(providerInput.spanProcessors[0]).toMatchObject({
131
+ forceFlush: expect.any(Function),
132
+ onEnd: expect.any(Function),
133
+ onStart: expect.any(Function),
134
+ shutdown: expect.any(Function),
135
+ });
136
+ expect(mockSetLangfuseTracerProvider).toHaveBeenCalledWith(
137
+ mockTracerProvider
138
+ );
139
+ expect(mockAsyncLocalStorageContextManager).toHaveBeenCalledTimes(1);
140
+ expect(mockContextManager.enable).toHaveBeenCalledTimes(1);
141
+ expect(mockSetGlobalContextManager).toHaveBeenCalledWith(
142
+ mockContextManager
143
+ );
144
+ });
145
+
146
+ it('registers tracing from explicit Langfuse config credentials', async () => {
147
+ const { initializeLangfuseTracing } = await import('@/instrumentation');
148
+ const provider = initializeLangfuseTracing({
149
+ publicKey: 'pk-config',
150
+ secretKey: 'sk-config',
151
+ baseUrl: 'https://langfuse.config',
152
+ });
153
+
154
+ expect(provider).toBe(mockTracerProvider);
155
+ expect(mockLangfuseSpanProcessor).toHaveBeenCalledWith(
156
+ expect.objectContaining({
157
+ publicKey: 'pk-config',
158
+ secretKey: 'sk-config',
159
+ baseUrl: 'https://langfuse.config',
160
+ })
161
+ );
162
+ expect(mockBasicTracerProvider).toHaveBeenCalledTimes(1);
163
+ });
164
+
165
+ it('uses env credentials with a config-provided Langfuse baseUrl', async () => {
166
+ process.env.LANGFUSE_SECRET_KEY = 'sk-env';
167
+ process.env.LANGFUSE_PUBLIC_KEY = 'pk-env';
168
+
169
+ const { initializeLangfuseTracing } = await import('@/instrumentation');
170
+ const provider = initializeLangfuseTracing({
171
+ baseUrl: 'https://langfuse.config',
172
+ toolOutputTracing: { enabled: false },
173
+ });
174
+
175
+ expect(provider).toBe(mockTracerProvider);
176
+ expect(mockLangfuseSpanProcessor).toHaveBeenCalledWith(
177
+ expect.objectContaining({
178
+ publicKey: 'pk-env',
179
+ secretKey: 'sk-env',
180
+ baseUrl: 'https://langfuse.config',
181
+ })
182
+ );
183
+ expect(mockBasicTracerProvider).toHaveBeenCalledTimes(1);
184
+ });
185
+
186
+ it('does not replace the global provider when explicit credentials change', async () => {
187
+ const { initializeLangfuseTracing } = await import('@/instrumentation');
188
+ initializeLangfuseTracing({
189
+ publicKey: 'pk-first',
190
+ secretKey: 'sk-first',
191
+ baseUrl: 'https://langfuse.first',
192
+ });
193
+ initializeLangfuseTracing({
194
+ publicKey: 'pk-second',
195
+ secretKey: 'sk-second',
196
+ baseUrl: 'https://langfuse.second',
197
+ });
198
+
199
+ expect(mockLangfuseSpanProcessor).toHaveBeenCalledTimes(2);
200
+ expect(mockLangfuseSpanProcessor).toHaveBeenNthCalledWith(
201
+ 1,
202
+ expect.objectContaining({
203
+ publicKey: 'pk-first',
204
+ secretKey: 'sk-first',
205
+ baseUrl: 'https://langfuse.first',
206
+ })
207
+ );
208
+ expect(mockLangfuseSpanProcessor).toHaveBeenNthCalledWith(
209
+ 2,
210
+ expect.objectContaining({
211
+ publicKey: 'pk-second',
212
+ secretKey: 'sk-second',
213
+ baseUrl: 'https://langfuse.second',
214
+ })
215
+ );
216
+ expect(mockBasicTracerProvider).toHaveBeenCalledTimes(1);
217
+ expect(mockSetLangfuseTracerProvider).toHaveBeenCalledTimes(1);
218
+ });
219
+
220
+ it('passes explicit redaction config into the redacting processor fallback', async () => {
221
+ const { initializeLangfuseTracing } = await import('@/instrumentation');
222
+ initializeLangfuseTracing({
223
+ publicKey: 'pk-config',
224
+ secretKey: 'sk-config',
225
+ baseUrl: 'https://langfuse.config',
226
+ toolOutputTracing: { enabled: false },
227
+ });
228
+
229
+ const providerInput = mockBasicTracerProvider.mock
230
+ .calls[0][0] as BasicTracerProviderInput;
231
+ const routingProcessor =
232
+ providerInput.spanProcessors[0] as RoutingSpanProcessorForTest;
233
+ const childProcessors = Array.from(routingProcessor.processors.values());
234
+ expect(childProcessors[0]?.fallbackConfig).toMatchObject({
235
+ enabled: false,
236
+ });
237
+ });
238
+
239
+ it('reuses the isolated provider after initialization', async () => {
240
+ process.env.LANGFUSE_SECRET_KEY = 'sk-test';
241
+ process.env.LANGFUSE_PUBLIC_KEY = 'pk-test';
242
+ process.env.LANGFUSE_BASE_URL = 'https://langfuse.test';
243
+
244
+ const { initializeLangfuseTracingFromEnv } = await import(
245
+ '@/instrumentation'
246
+ );
247
+ const firstProvider = initializeLangfuseTracingFromEnv();
248
+ const secondProvider = initializeLangfuseTracingFromEnv();
249
+
250
+ expect(firstProvider).toBe(mockTracerProvider);
251
+ expect(secondProvider).toBe(mockTracerProvider);
252
+ expect(mockLangfuseSpanProcessor).toHaveBeenCalledTimes(1);
253
+ expect(mockBasicTracerProvider).toHaveBeenCalledTimes(1);
254
+ expect(mockSetLangfuseTracerProvider).toHaveBeenCalledTimes(1);
255
+ });
256
+
257
+ it('does not replace an existing active context manager', async () => {
258
+ mockContextHasActiveValue = true;
259
+ process.env.LANGFUSE_SECRET_KEY = 'sk-test';
260
+ process.env.LANGFUSE_PUBLIC_KEY = 'pk-test';
261
+ process.env.LANGFUSE_BASE_URL = 'https://langfuse.test';
262
+
263
+ await import('@/instrumentation');
264
+
265
+ expect(mockAsyncLocalStorageContextManager).not.toHaveBeenCalled();
266
+ expect(mockSetGlobalContextManager).not.toHaveBeenCalled();
267
+ });
268
+
269
+ it('disables the local context manager when registration is rejected', async () => {
270
+ mockSetGlobalContextManager.mockReturnValue(false);
271
+ process.env.LANGFUSE_SECRET_KEY = 'sk-test';
272
+ process.env.LANGFUSE_PUBLIC_KEY = 'pk-test';
273
+ process.env.LANGFUSE_BASE_URL = 'https://langfuse.test';
274
+
275
+ await import('@/instrumentation');
276
+
277
+ expect(mockContextManager.enable).toHaveBeenCalledTimes(1);
278
+ expect(mockSetGlobalContextManager).toHaveBeenCalledWith(
279
+ mockContextManager
280
+ );
281
+ expect(mockContextManager.disable).toHaveBeenCalledTimes(1);
282
+ });
283
+ });
@@ -12,7 +12,8 @@ const MockedCallbackHandler = CallbackHandler as jest.MockedClass<
12
12
 
13
13
  async function createTestRun(
14
14
  agentName?: string,
15
- agentOverrides: Record<string, unknown> = {}
15
+ agentOverrides: Record<string, unknown> = {},
16
+ runOverrides: Record<string, unknown> = {}
16
17
  ): Promise<Run<never>> {
17
18
  const run = await Run.create({
18
19
  runId: 'test-run-id',
@@ -29,6 +30,7 @@ async function createTestRun(
29
30
  },
30
31
  ],
31
32
  },
33
+ ...runOverrides,
32
34
  });
33
35
 
34
36
  const emptyStream = (async function* (): AsyncGenerator {
@@ -106,4 +108,55 @@ describe('Langfuse trace metadata includes agentName', () => {
106
108
 
107
109
  expect(MockedCallbackHandler).not.toHaveBeenCalled();
108
110
  });
111
+
112
+ it('uses the nested Langfuse CallbackHandler for redaction-only agent config', async () => {
113
+ const run = await createTestRun('DWAINE', {
114
+ langfuse: {
115
+ toolOutputTracing: { enabled: false },
116
+ },
117
+ });
118
+ await run.processStream(
119
+ { messages: [] },
120
+ { configurable: { thread_id: 't1', user_id: 'u1' }, version: 'v2' }
121
+ );
122
+
123
+ expect(MockedCallbackHandler).toHaveBeenCalledTimes(1);
124
+ });
125
+
126
+ it('uses the nested Langfuse CallbackHandler for redaction-only run config', async () => {
127
+ const run = await createTestRun(
128
+ 'DWAINE',
129
+ {},
130
+ {
131
+ langfuse: {
132
+ toolOutputTracing: { enabled: false },
133
+ },
134
+ }
135
+ );
136
+ await run.processStream(
137
+ { messages: [] },
138
+ { configurable: { thread_id: 't1', user_id: 'u1' }, version: 'v2' }
139
+ );
140
+
141
+ expect(MockedCallbackHandler).toHaveBeenCalledTimes(1);
142
+ });
143
+
144
+ it('preserves run-level Langfuse config after graph cleanup for later turns', async () => {
145
+ const langfuse = {
146
+ toolOutputTracing: { enabled: false },
147
+ };
148
+ const run = await createTestRun(
149
+ 'DWAINE',
150
+ {},
151
+ {
152
+ langfuse,
153
+ }
154
+ );
155
+ await run.processStream(
156
+ { messages: [] },
157
+ { configurable: { thread_id: 't1', user_id: 'u1' }, version: 'v2' }
158
+ );
159
+
160
+ expect(run.Graph?.langfuse).toBe(langfuse);
161
+ });
109
162
  });