@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.
- package/dist/cjs/graphs/Graph.cjs +60 -21
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/instrumentation.cjs +120 -9
- package/dist/cjs/instrumentation.cjs.map +1 -1
- package/dist/cjs/langfuse.cjs +30 -226
- package/dist/cjs/langfuse.cjs.map +1 -1
- package/dist/cjs/langfuseToolOutputTracing.cjs +476 -0
- package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -0
- package/dist/cjs/llm/bedrock/index.cjs +10 -0
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/toolCache.cjs +125 -0
- package/dist/cjs/llm/bedrock/toolCache.cjs.map +1 -0
- package/dist/cjs/messages/cache.cjs +17 -9
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/run.cjs +142 -69
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +26 -9
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +10 -6
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +62 -23
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/instrumentation.mjs +118 -9
- package/dist/esm/instrumentation.mjs.map +1 -1
- package/dist/esm/langfuse.mjs +28 -224
- package/dist/esm/langfuse.mjs.map +1 -1
- package/dist/esm/langfuseToolOutputTracing.mjs +468 -0
- package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -0
- package/dist/esm/llm/bedrock/index.mjs +10 -0
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/bedrock/toolCache.mjs +122 -0
- package/dist/esm/llm/bedrock/toolCache.mjs.map +1 -0
- package/dist/esm/messages/cache.mjs +17 -9
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/run.mjs +144 -71
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +26 -9
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +10 -6
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +5 -1
- package/dist/types/instrumentation.d.ts +5 -1
- package/dist/types/langfuse.d.ts +6 -28
- package/dist/types/langfuseToolOutputTracing.d.ts +20 -0
- package/dist/types/llm/bedrock/index.d.ts +16 -0
- package/dist/types/llm/bedrock/toolCache.d.ts +4 -0
- package/dist/types/messages/cache.d.ts +2 -2
- package/dist/types/run.d.ts +5 -1
- package/dist/types/tools/ToolNode.d.ts +4 -1
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +2 -0
- package/dist/types/types/graph.d.ts +30 -0
- package/dist/types/types/llm.d.ts +2 -2
- package/dist/types/types/run.d.ts +6 -0
- package/dist/types/types/tools.d.ts +7 -0
- package/package.json +2 -1
- package/src/agents/__tests__/AgentContext.anthropic.live.test.ts +332 -0
- package/src/agents/__tests__/AgentContext.bedrock.live.test.ts +504 -0
- package/src/graphs/Graph.ts +104 -34
- package/src/instrumentation.ts +172 -11
- package/src/langfuse.ts +59 -324
- package/src/langfuseToolOutputTracing.ts +702 -0
- package/src/llm/bedrock/index.ts +32 -1
- package/src/llm/bedrock/llm.spec.ts +154 -1
- package/src/llm/bedrock/toolCache.test.ts +131 -0
- package/src/llm/bedrock/toolCache.ts +191 -0
- package/src/messages/cache.test.ts +97 -38
- package/src/messages/cache.ts +18 -10
- package/src/run.ts +190 -87
- package/src/specs/langfuse-callbacks.test.ts +178 -1
- package/src/specs/langfuse-config.test.ts +112 -76
- package/src/specs/langfuse-instrumentation.test.ts +283 -0
- package/src/specs/langfuse-metadata.test.ts +54 -1
- package/src/specs/langfuse-tool-output-tracing.test.ts +616 -0
- package/src/tools/ToolNode.ts +35 -8
- package/src/tools/__tests__/SubagentExecutor.test.ts +32 -0
- package/src/tools/__tests__/ToolNode.langfuse.test.ts +47 -0
- package/src/tools/subagent/SubagentExecutor.ts +11 -6
- package/src/types/graph.ts +32 -0
- package/src/types/llm.ts +2 -2
- package/src/types/run.ts +6 -0
- package/src/types/tools.ts +7 -0
|
@@ -1,105 +1,104 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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/
|
|
18
|
-
|
|
19
|
-
isDefaultExportSpan: jest.fn(() => false),
|
|
8
|
+
jest.mock('@langfuse/langchain', () => ({
|
|
9
|
+
CallbackHandler: jest.fn().mockImplementation((params) => ({ params })),
|
|
20
10
|
}));
|
|
21
11
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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(
|
|
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('
|
|
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:
|
|
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
|
-
|
|
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
|
|
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(
|
|
112
|
-
|
|
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
|
});
|