@librechat/agents 3.1.90 → 3.1.92

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 (94) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +9 -5
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +48 -14
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/instrumentation.cjs +2 -7
  6. package/dist/cjs/instrumentation.cjs.map +1 -1
  7. package/dist/cjs/langfuse.cjs +285 -0
  8. package/dist/cjs/langfuse.cjs.map +1 -0
  9. package/dist/cjs/main.cjs +25 -0
  10. package/dist/cjs/main.cjs.map +1 -1
  11. package/dist/cjs/run.cjs +75 -44
  12. package/dist/cjs/run.cjs.map +1 -1
  13. package/dist/cjs/stream.cjs +10 -3
  14. package/dist/cjs/stream.cjs.map +1 -1
  15. package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs +380 -0
  16. package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs.map +1 -0
  17. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs +997 -0
  18. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs.map +1 -0
  19. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs +575 -0
  20. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs.map +1 -0
  21. package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs +165 -0
  22. package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs.map +1 -0
  23. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +17 -5
  24. package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -1
  25. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs +110 -6
  26. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs.map +1 -1
  27. package/dist/cjs/utils/callbacks.cjs +27 -0
  28. package/dist/cjs/utils/callbacks.cjs.map +1 -0
  29. package/dist/esm/agents/AgentContext.mjs +9 -5
  30. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  31. package/dist/esm/graphs/Graph.mjs +48 -14
  32. package/dist/esm/graphs/Graph.mjs.map +1 -1
  33. package/dist/esm/instrumentation.mjs +2 -7
  34. package/dist/esm/instrumentation.mjs.map +1 -1
  35. package/dist/esm/langfuse.mjs +275 -0
  36. package/dist/esm/langfuse.mjs.map +1 -0
  37. package/dist/esm/main.mjs +5 -1
  38. package/dist/esm/main.mjs.map +1 -1
  39. package/dist/esm/run.mjs +75 -44
  40. package/dist/esm/run.mjs.map +1 -1
  41. package/dist/esm/stream.mjs +10 -3
  42. package/dist/esm/stream.mjs.map +1 -1
  43. package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs +378 -0
  44. package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs.map +1 -0
  45. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs +994 -0
  46. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs.map +1 -0
  47. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs +566 -0
  48. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs.map +1 -0
  49. package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs +155 -0
  50. package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs.map +1 -0
  51. package/dist/esm/tools/local/LocalExecutionEngine.mjs +17 -6
  52. package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -1
  53. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs +111 -7
  54. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs.map +1 -1
  55. package/dist/esm/utils/callbacks.mjs +24 -0
  56. package/dist/esm/utils/callbacks.mjs.map +1 -0
  57. package/dist/types/agents/AgentContext.d.ts +4 -1
  58. package/dist/types/graphs/Graph.d.ts +6 -5
  59. package/dist/types/index.d.ts +1 -0
  60. package/dist/types/langfuse.d.ts +57 -0
  61. package/dist/types/tools/cloudflare/CloudflareBridgeRuntime.d.ts +23 -0
  62. package/dist/types/tools/cloudflare/CloudflareProgrammaticToolCalling.d.ts +4 -0
  63. package/dist/types/tools/cloudflare/CloudflareSandboxExecutionEngine.d.ts +21 -0
  64. package/dist/types/tools/cloudflare/CloudflareSandboxTools.d.ts +22 -0
  65. package/dist/types/tools/cloudflare/index.d.ts +4 -0
  66. package/dist/types/tools/local/LocalExecutionEngine.d.ts +1 -0
  67. package/dist/types/types/graph.d.ts +8 -0
  68. package/dist/types/types/run.d.ts +2 -2
  69. package/dist/types/types/tools.d.ts +118 -2
  70. package/dist/types/utils/callbacks.d.ts +5 -0
  71. package/package.json +4 -4
  72. package/src/__tests__/stream.eagerEventExecution.test.ts +66 -0
  73. package/src/agents/AgentContext.ts +13 -3
  74. package/src/graphs/Graph.ts +57 -16
  75. package/src/index.ts +1 -0
  76. package/src/instrumentation.ts +2 -7
  77. package/src/langfuse.ts +441 -0
  78. package/src/run.ts +105 -59
  79. package/src/specs/langfuse-callbacks.test.ts +75 -0
  80. package/src/specs/langfuse-config.test.ts +114 -0
  81. package/src/specs/langfuse-metadata.test.ts +19 -1
  82. package/src/stream.ts +13 -3
  83. package/src/tools/__tests__/CloudflareSandboxExecution.test.ts +537 -0
  84. package/src/tools/cloudflare/CloudflareBridgeRuntime.ts +480 -0
  85. package/src/tools/cloudflare/CloudflareProgrammaticToolCalling.ts +1162 -0
  86. package/src/tools/cloudflare/CloudflareSandboxExecutionEngine.ts +744 -0
  87. package/src/tools/cloudflare/CloudflareSandboxTools.ts +225 -0
  88. package/src/tools/cloudflare/index.ts +4 -0
  89. package/src/tools/local/LocalExecutionEngine.ts +20 -4
  90. package/src/tools/local/resolveLocalExecutionTools.ts +169 -7
  91. package/src/types/graph.ts +9 -0
  92. package/src/types/run.ts +2 -7
  93. package/src/types/tools.ts +141 -2
  94. package/src/utils/callbacks.ts +39 -0
@@ -0,0 +1,441 @@
1
+ import { CallbackHandler } from '@langfuse/langchain';
2
+ import { isDefaultExportSpan, LangfuseSpanProcessor } from '@langfuse/otel';
3
+ import {
4
+ LangfuseOtelSpanAttributes,
5
+ createObservationAttributes,
6
+ } from '@langfuse/tracing';
7
+ import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
8
+ import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';
9
+ import { SpanStatusCode } from '@opentelemetry/api';
10
+ import type { Serialized } from '@langchain/core/load/serializable';
11
+ import type { BaseMessage } from '@langchain/core/messages';
12
+ import type { LLMResult } from '@langchain/core/outputs';
13
+ import type { Attributes, Span } from '@opentelemetry/api';
14
+ import type * as t from '@/types';
15
+ import { isPresent } from '@/utils/misc';
16
+
17
+ const TRACE_METADATA_MAX_LENGTH = 200;
18
+ const LANGFUSE_TRACER_NAME = 'langfuse-sdk';
19
+
20
+ export type LangfuseTraceMetadata = Record<string, string>;
21
+
22
+ type LangfuseHandlerParams = {
23
+ userId?: string;
24
+ sessionId?: string;
25
+ traceMetadata?: LangfuseTraceMetadata;
26
+ tags?: string[];
27
+ };
28
+
29
+ type AgentLangfuseHandlerParams = LangfuseHandlerParams & {
30
+ langfuse?: t.LangfuseConfig;
31
+ };
32
+
33
+ type ResolvedLangfuseConfig = t.LangfuseConfig & {
34
+ enabled: true;
35
+ publicKey: string;
36
+ secretKey: string;
37
+ };
38
+
39
+ function getEnvLangfuseBaseUrl(): string | undefined {
40
+ return process.env.LANGFUSE_BASE_URL ?? process.env.LANGFUSE_BASEURL;
41
+ }
42
+
43
+ function createTraceMetadata(
44
+ metadata: Record<string, unknown>
45
+ ): LangfuseTraceMetadata {
46
+ const traceMetadata: LangfuseTraceMetadata = {};
47
+ for (const [key, value] of Object.entries(metadata)) {
48
+ if (value == null) {
49
+ continue;
50
+ }
51
+ const stringValue = typeof value === 'string' ? value : String(value);
52
+ if (
53
+ stringValue.trim() === '' ||
54
+ stringValue.length > TRACE_METADATA_MAX_LENGTH
55
+ ) {
56
+ continue;
57
+ }
58
+ traceMetadata[key] = stringValue;
59
+ }
60
+ return traceMetadata;
61
+ }
62
+
63
+ export function createLangfuseTraceMetadata({
64
+ messageId,
65
+ parentMessageId,
66
+ agentId,
67
+ agentName,
68
+ }: {
69
+ messageId?: unknown;
70
+ parentMessageId?: unknown;
71
+ agentId?: unknown;
72
+ agentName?: unknown;
73
+ }): LangfuseTraceMetadata {
74
+ return createTraceMetadata({
75
+ messageId,
76
+ parentMessageId,
77
+ agentId,
78
+ agentName,
79
+ });
80
+ }
81
+
82
+ function getModelName(serialized: Serialized): string {
83
+ const serializedRecord = serialized as unknown as Record<string, unknown>;
84
+ const kwargs = serializedRecord.kwargs as Record<string, unknown> | undefined;
85
+ const modelName =
86
+ kwargs?.model ??
87
+ kwargs?.model_name ??
88
+ kwargs?.modelName ??
89
+ kwargs?.model_id ??
90
+ kwargs?.modelId ??
91
+ serializedRecord.name;
92
+
93
+ if (typeof modelName === 'string' && modelName.trim() !== '') {
94
+ return modelName;
95
+ }
96
+
97
+ if (Array.isArray(serializedRecord.id) && serializedRecord.id.length > 0) {
98
+ return String(serializedRecord.id[serializedRecord.id.length - 1]);
99
+ }
100
+
101
+ return 'ChatModel';
102
+ }
103
+
104
+ function getModelParameters(
105
+ extraParams?: Record<string, unknown>
106
+ ): Record<string, string | number> {
107
+ const invocationParams = extraParams?.invocation_params;
108
+ const params =
109
+ invocationParams != null && typeof invocationParams === 'object'
110
+ ? (invocationParams as Record<string, unknown>)
111
+ : (extraParams ?? {});
112
+
113
+ return Object.fromEntries(
114
+ Object.entries(params).filter(([, value]) => {
115
+ return typeof value === 'string' || typeof value === 'number';
116
+ })
117
+ ) as Record<string, string | number>;
118
+ }
119
+
120
+ function getOutput(output: LLMResult): unknown {
121
+ return output.generations.map((generation) =>
122
+ generation.map((item) => {
123
+ if ('message' in item && item.message != null) {
124
+ return (item.message as { content?: unknown }).content;
125
+ }
126
+ return item.text;
127
+ })
128
+ );
129
+ }
130
+
131
+ function getUsageDetails(
132
+ output: LLMResult
133
+ ): Record<string, number> | undefined {
134
+ const llmOutput = output.llmOutput as Record<string, unknown> | undefined;
135
+ const usage = llmOutput?.tokenUsage ?? llmOutput?.usage;
136
+ if (usage == null || typeof usage !== 'object') {
137
+ return undefined;
138
+ }
139
+
140
+ const usageEntries = Object.entries(usage as Record<string, unknown>).filter(
141
+ ([, value]) => typeof value === 'number'
142
+ );
143
+
144
+ return usageEntries.length > 0
145
+ ? (Object.fromEntries(usageEntries) as Record<string, number>)
146
+ : undefined;
147
+ }
148
+
149
+ export function getLangfuseTraceName(
150
+ traceMetadata?: LangfuseTraceMetadata,
151
+ fallback: string = 'LibreChat Agent'
152
+ ): string {
153
+ const agentName = traceMetadata?.agentName;
154
+ return isPresent(agentName) ? `${fallback}: ${agentName}` : fallback;
155
+ }
156
+
157
+ function getTraceAttributes({
158
+ userId,
159
+ sessionId,
160
+ traceMetadata,
161
+ tags,
162
+ }: LangfuseHandlerParams): Attributes {
163
+ const attributes: Attributes = {
164
+ [LangfuseOtelSpanAttributes.TRACE_NAME]:
165
+ getLangfuseTraceName(traceMetadata),
166
+ };
167
+
168
+ if (isPresent(userId)) {
169
+ attributes[LangfuseOtelSpanAttributes.TRACE_USER_ID] = userId;
170
+ }
171
+ if (isPresent(sessionId)) {
172
+ attributes[LangfuseOtelSpanAttributes.TRACE_SESSION_ID] = sessionId;
173
+ }
174
+ if (tags != null && tags.length > 0) {
175
+ attributes[LangfuseOtelSpanAttributes.TRACE_TAGS] = tags;
176
+ }
177
+ for (const [key, value] of Object.entries(traceMetadata ?? {})) {
178
+ attributes[`${LangfuseOtelSpanAttributes.TRACE_METADATA}.${key}`] = value;
179
+ }
180
+
181
+ return attributes;
182
+ }
183
+
184
+ export class LangfuseAgentCallbackHandler extends BaseCallbackHandler {
185
+ name = 'librechat_langfuse_agent_handler';
186
+
187
+ private readonly provider: BasicTracerProvider;
188
+ private readonly processor: LangfuseSpanProcessor;
189
+ private readonly userId?: string;
190
+ private readonly sessionId?: string;
191
+ private readonly traceMetadata?: LangfuseTraceMetadata;
192
+ private readonly tags?: string[];
193
+ private readonly spans = new Map<string, Span>();
194
+
195
+ constructor({
196
+ langfuse,
197
+ userId,
198
+ sessionId,
199
+ traceMetadata,
200
+ tags,
201
+ }: LangfuseHandlerParams & { langfuse: ResolvedLangfuseConfig }) {
202
+ super();
203
+ this.userId = userId;
204
+ this.sessionId = sessionId;
205
+ this.traceMetadata = traceMetadata;
206
+ this.tags = tags;
207
+ this.processor = new LangfuseSpanProcessor({
208
+ publicKey: langfuse.publicKey,
209
+ secretKey: langfuse.secretKey,
210
+ ...(isPresent(langfuse.baseUrl) ? { baseUrl: langfuse.baseUrl } : {}),
211
+ environment:
212
+ process.env.LANGFUSE_TRACING_ENVIRONMENT ??
213
+ process.env.NODE_ENV ??
214
+ 'development',
215
+ exportMode: 'immediate',
216
+ shouldExportSpan: ({ otelSpan }): boolean =>
217
+ isDefaultExportSpan(otelSpan) ||
218
+ otelSpan.instrumentationScope.name === LANGFUSE_TRACER_NAME,
219
+ });
220
+ this.provider = new BasicTracerProvider({
221
+ spanProcessors: [this.processor],
222
+ });
223
+ }
224
+
225
+ private startGenerationSpan({
226
+ llm,
227
+ input,
228
+ runId,
229
+ extraParams,
230
+ metadata,
231
+ name,
232
+ }: {
233
+ llm: Serialized;
234
+ input: unknown;
235
+ runId: string;
236
+ extraParams?: Record<string, unknown>;
237
+ metadata?: Record<string, unknown>;
238
+ name?: string;
239
+ }): void {
240
+ if (this.spans.has(runId)) {
241
+ return;
242
+ }
243
+
244
+ const tracer = this.provider.getTracer(LANGFUSE_TRACER_NAME);
245
+ const spanName =
246
+ typeof name === 'string' && name.trim() !== '' ? name : getModelName(llm);
247
+ const span = tracer.startSpan(spanName, {
248
+ attributes: {
249
+ ...getTraceAttributes({
250
+ userId: this.userId,
251
+ sessionId: this.sessionId,
252
+ traceMetadata: this.traceMetadata,
253
+ tags: this.tags,
254
+ }),
255
+ ...createObservationAttributes('generation', {
256
+ input,
257
+ model: getModelName(llm),
258
+ modelParameters: getModelParameters(extraParams),
259
+ metadata: {
260
+ ...metadata,
261
+ ...this.traceMetadata,
262
+ },
263
+ }),
264
+ },
265
+ });
266
+ this.spans.set(runId, span);
267
+ }
268
+
269
+ async handleChatModelStart(
270
+ llm: Serialized,
271
+ messages: BaseMessage[][],
272
+ runId: string,
273
+ _parentRunId?: string,
274
+ extraParams?: Record<string, unknown>,
275
+ _tags?: string[],
276
+ metadata?: Record<string, unknown>,
277
+ name?: string
278
+ ): Promise<void> {
279
+ this.startGenerationSpan({
280
+ llm,
281
+ input: messages,
282
+ runId,
283
+ extraParams,
284
+ metadata,
285
+ name,
286
+ });
287
+ }
288
+
289
+ async handleLLMStart(
290
+ llm: Serialized,
291
+ prompts: string[],
292
+ runId: string,
293
+ _parentRunId?: string,
294
+ extraParams?: Record<string, unknown>,
295
+ _tags?: string[],
296
+ metadata?: Record<string, unknown>,
297
+ name?: string
298
+ ): Promise<void> {
299
+ this.startGenerationSpan({
300
+ llm,
301
+ input: prompts,
302
+ runId,
303
+ extraParams,
304
+ metadata,
305
+ name,
306
+ });
307
+ }
308
+
309
+ async handleLLMEnd(output: LLMResult, runId: string): Promise<void> {
310
+ const span = this.spans.get(runId);
311
+ if (!span) {
312
+ return;
313
+ }
314
+
315
+ span.setAttributes(
316
+ createObservationAttributes('generation', {
317
+ output: getOutput(output),
318
+ usageDetails: getUsageDetails(output),
319
+ })
320
+ );
321
+ span.end();
322
+ this.spans.delete(runId);
323
+ await this.flush();
324
+ }
325
+
326
+ async handleLLMError(err: unknown, runId: string): Promise<void> {
327
+ const span = this.spans.get(runId);
328
+ if (!span) {
329
+ return;
330
+ }
331
+
332
+ const message = err instanceof Error ? err.message : String(err);
333
+ span.setStatus({ code: SpanStatusCode.ERROR, message });
334
+ span.setAttributes(
335
+ createObservationAttributes('generation', {
336
+ level: 'ERROR',
337
+ statusMessage: message,
338
+ })
339
+ );
340
+ span.end();
341
+ this.spans.delete(runId);
342
+ await this.flush();
343
+ }
344
+
345
+ private async flush(): Promise<void> {
346
+ try {
347
+ await this.provider.forceFlush();
348
+ } catch (error) {
349
+ process.emitWarning(
350
+ `[LangfuseAgentCallbackHandler] Failed to flush Langfuse spans: ${
351
+ error instanceof Error ? error.message : String(error)
352
+ }`
353
+ );
354
+ }
355
+ }
356
+
357
+ async dispose(): Promise<void> {
358
+ for (const span of this.spans.values()) {
359
+ span.end();
360
+ }
361
+ this.spans.clear();
362
+ await this.flush();
363
+ try {
364
+ await this.provider.shutdown();
365
+ } catch (error) {
366
+ process.emitWarning(
367
+ `[LangfuseAgentCallbackHandler] Failed to shut down Langfuse provider: ${
368
+ error instanceof Error ? error.message : String(error)
369
+ }`
370
+ );
371
+ }
372
+ }
373
+ }
374
+
375
+ function hasRequiredLangfuseConfig(
376
+ langfuse?: t.LangfuseConfig
377
+ ): langfuse is ResolvedLangfuseConfig {
378
+ return (
379
+ langfuse?.enabled === true &&
380
+ isPresent(langfuse.publicKey) &&
381
+ isPresent(langfuse.secretKey)
382
+ );
383
+ }
384
+
385
+ export function createLegacyLangfuseHandler(
386
+ params: LangfuseHandlerParams
387
+ ): CallbackHandler {
388
+ return new CallbackHandler(params);
389
+ }
390
+
391
+ export function createLangfuseHandler({
392
+ langfuse,
393
+ userId,
394
+ sessionId,
395
+ traceMetadata,
396
+ tags,
397
+ }: AgentLangfuseHandlerParams): LangfuseAgentCallbackHandler | undefined {
398
+ if (!hasRequiredLangfuseConfig(langfuse)) {
399
+ return undefined;
400
+ }
401
+
402
+ return new LangfuseAgentCallbackHandler({
403
+ langfuse,
404
+ userId,
405
+ sessionId,
406
+ traceMetadata,
407
+ tags,
408
+ });
409
+ }
410
+
411
+ export function hasExplicitLangfuseConfig(
412
+ contexts: Iterable<{ langfuse?: t.LangfuseConfig }>
413
+ ): boolean {
414
+ for (const context of contexts) {
415
+ if (context.langfuse != null) {
416
+ return true;
417
+ }
418
+ }
419
+ return false;
420
+ }
421
+
422
+ export function hasLangfuseEnvConfig(): boolean {
423
+ return (
424
+ isPresent(process.env.LANGFUSE_SECRET_KEY) &&
425
+ isPresent(process.env.LANGFUSE_PUBLIC_KEY) &&
426
+ isPresent(getEnvLangfuseBaseUrl())
427
+ );
428
+ }
429
+
430
+ export function isLangfuseCallbackHandler(value: unknown): boolean {
431
+ return (
432
+ value instanceof CallbackHandler ||
433
+ value instanceof LangfuseAgentCallbackHandler
434
+ );
435
+ }
436
+
437
+ export async function disposeLangfuseHandler(value: unknown): Promise<void> {
438
+ if (value instanceof LangfuseAgentCallbackHandler) {
439
+ await value.dispose();
440
+ }
441
+ }
package/src/run.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  // src/run.ts
2
2
  import './instrumentation';
3
- import { CallbackHandler } from '@langfuse/langchain';
4
3
  import { PromptTemplate } from '@langchain/core/prompts';
5
4
  import { RunnableLambda } from '@langchain/core/runnables';
6
5
  import { AzureChatOpenAI, ChatOpenAI } from '@langchain/openai';
@@ -31,7 +30,21 @@ import { initializeModel } from '@/llm/init';
31
30
  import { HandlerRegistry } from '@/events';
32
31
  import { executeHooks } from '@/hooks';
33
32
  import { isOpenAILike } from '@/utils/llm';
34
- import { isPresent } from '@/utils/misc';
33
+ import {
34
+ appendCallbacks,
35
+ findCallback,
36
+ type CallbackEntry,
37
+ } from '@/utils/callbacks';
38
+ import {
39
+ createLegacyLangfuseHandler,
40
+ createLangfuseTraceMetadata,
41
+ createLangfuseHandler,
42
+ disposeLangfuseHandler,
43
+ getLangfuseTraceName,
44
+ hasExplicitLangfuseConfig,
45
+ hasLangfuseEnvConfig,
46
+ isLangfuseCallbackHandler,
47
+ } from '@/langfuse';
35
48
  import type { HookRegistry } from '@/hooks';
36
49
 
37
50
  export const defaultOmitOptions = new Set([
@@ -592,43 +605,48 @@ export class Run<_T extends t.BaseGraphState> {
592
605
  /** Custom event callback to intercept and handle custom events */
593
606
  const customEventCallback = this.createCustomEventCallback();
594
607
 
595
- const baseCallbacks = (config.callbacks as t.ProvidedCallbacks) ?? [];
596
608
  const streamCallbacks = streamOptions?.callbacks
597
609
  ? this.getCallbacks(streamOptions.callbacks)
598
- : [];
610
+ : undefined;
599
611
 
600
612
  const customHandler = BaseCallbackHandler.fromMethods({
601
613
  [Callback.CUSTOM_EVENT]: customEventCallback,
602
614
  });
603
615
  customHandler.awaitHandlers = true;
604
616
 
605
- config.callbacks = baseCallbacks
606
- .concat(streamCallbacks)
607
- .concat(customHandler);
617
+ config.callbacks = appendCallbacks(
618
+ config.callbacks,
619
+ streamCallbacks ? [streamCallbacks, customHandler] : [customHandler]
620
+ );
608
621
 
609
622
  if (
610
- isPresent(process.env.LANGFUSE_SECRET_KEY) &&
611
- isPresent(process.env.LANGFUSE_PUBLIC_KEY) &&
612
- isPresent(process.env.LANGFUSE_BASE_URL)
623
+ hasLangfuseEnvConfig() &&
624
+ !hasExplicitLangfuseConfig(this.Graph.agentContexts.values())
613
625
  ) {
614
- const userId = config.configurable?.user_id;
615
- const sessionId = config.configurable?.thread_id;
626
+ const userId =
627
+ typeof config.configurable?.user_id === 'string'
628
+ ? config.configurable.user_id
629
+ : undefined;
630
+ const sessionId =
631
+ typeof config.configurable?.thread_id === 'string'
632
+ ? config.configurable.thread_id
633
+ : undefined;
616
634
  const primaryContext = this.Graph.agentContexts.get(
617
635
  this.Graph.defaultAgentId
618
636
  );
619
- const traceMetadata = {
637
+ const traceMetadata = createLangfuseTraceMetadata({
620
638
  messageId: this.id,
621
639
  parentMessageId: config.configurable?.requestBody?.parentMessageId,
622
640
  agentName: primaryContext?.name,
623
- };
624
- const handler = new CallbackHandler({
641
+ });
642
+ const handler = createLegacyLangfuseHandler({
625
643
  userId,
626
644
  sessionId,
627
645
  traceMetadata,
646
+ tags: ['librechat', 'agent'],
628
647
  });
629
- config.callbacks = (
630
- (config.callbacks as t.ProvidedCallbacks) ?? []
631
- ).concat([handler]);
648
+ config.runName = config.runName ?? getLangfuseTraceName(traceMetadata);
649
+ config.callbacks = appendCallbacks(config.callbacks, [handler]);
632
650
  }
633
651
 
634
652
  if (!this.id) {
@@ -1134,29 +1152,51 @@ export class Run<_T extends t.BaseGraphState> {
1134
1152
  titleMethod = TitleMethod.COMPLETION,
1135
1153
  titlePromptTemplate,
1136
1154
  }: t.RunTitleOptions): Promise<{ language?: string; title?: string }> {
1137
- if (
1138
- chainOptions != null &&
1139
- isPresent(process.env.LANGFUSE_SECRET_KEY) &&
1140
- isPresent(process.env.LANGFUSE_PUBLIC_KEY) &&
1141
- isPresent(process.env.LANGFUSE_BASE_URL)
1142
- ) {
1143
- const userId = chainOptions.configurable?.user_id;
1144
- const sessionId = chainOptions.configurable?.thread_id;
1145
- const titleContext = this.Graph?.agentContexts.get(
1146
- this.Graph.defaultAgentId
1147
- );
1148
- const traceMetadata = {
1149
- messageId: 'title-' + this.id,
1150
- agentName: titleContext?.name,
1151
- };
1152
- const handler = new CallbackHandler({
1153
- userId,
1154
- sessionId,
1155
- traceMetadata,
1156
- });
1157
- chainOptions.callbacks = (
1158
- (chainOptions.callbacks as t.ProvidedCallbacks) ?? []
1159
- ).concat([handler]);
1155
+ let titleLangfuseHandler: CallbackEntry | undefined;
1156
+ const titleContext =
1157
+ this.Graph == null
1158
+ ? undefined
1159
+ : this.Graph.agentContexts.get(this.Graph.defaultAgentId);
1160
+ const traceMetadata = createLangfuseTraceMetadata({
1161
+ messageId: 'title-' + this.id,
1162
+ agentName: titleContext?.name,
1163
+ });
1164
+ const titleRunName = getLangfuseTraceName(traceMetadata, 'LibreChat Title');
1165
+
1166
+ if (chainOptions != null) {
1167
+ const userId =
1168
+ typeof chainOptions.configurable?.user_id === 'string'
1169
+ ? chainOptions.configurable.user_id
1170
+ : undefined;
1171
+ const sessionId =
1172
+ typeof chainOptions.configurable?.thread_id === 'string'
1173
+ ? chainOptions.configurable.thread_id
1174
+ : undefined;
1175
+ const hasExplicitLangfuse =
1176
+ this.Graph != null &&
1177
+ hasExplicitLangfuseConfig(this.Graph.agentContexts.values());
1178
+ if (titleContext?.langfuse != null) {
1179
+ titleLangfuseHandler = createLangfuseHandler({
1180
+ langfuse: titleContext.langfuse,
1181
+ userId,
1182
+ sessionId,
1183
+ traceMetadata,
1184
+ tags: ['librechat', 'title'],
1185
+ });
1186
+ } else if (hasLangfuseEnvConfig() && !hasExplicitLangfuse) {
1187
+ titleLangfuseHandler = createLegacyLangfuseHandler({
1188
+ userId,
1189
+ sessionId,
1190
+ traceMetadata,
1191
+ tags: ['librechat', 'title'],
1192
+ });
1193
+ }
1194
+
1195
+ if (titleLangfuseHandler != null) {
1196
+ chainOptions.callbacks = appendCallbacks(chainOptions.callbacks, [
1197
+ titleLangfuseHandler,
1198
+ ]);
1199
+ }
1160
1200
  }
1161
1201
 
1162
1202
  const convoTemplate = PromptTemplate.fromTemplate(
@@ -1218,27 +1258,33 @@ export class Run<_T extends t.BaseGraphState> {
1218
1258
  const invokeConfig = Object.assign({}, chainOptions, {
1219
1259
  run_id: this.id,
1220
1260
  runId: this.id,
1261
+ runName: chainOptions?.runName ?? titleRunName,
1221
1262
  });
1222
1263
 
1223
1264
  try {
1224
- return await fullChain.invoke(
1225
- { input: inputText, output: response },
1226
- invokeConfig
1227
- );
1228
- } catch (_e) {
1229
- // Fallback: strip callbacks to avoid EventStream tracer errors in certain environments
1230
- // But preserve langfuse handler if it exists
1231
- const langfuseHandler = (
1232
- invokeConfig.callbacks as t.ProvidedCallbacks
1233
- )?.find((cb) => cb instanceof CallbackHandler);
1234
- const { callbacks: _cb, ...rest } = invokeConfig;
1235
- const safeConfig = Object.assign({}, rest, {
1236
- callbacks: langfuseHandler ? [langfuseHandler] : [],
1237
- });
1238
- return await fullChain.invoke(
1239
- { input: inputText, output: response },
1240
- safeConfig as Partial<RunnableConfig>
1241
- );
1265
+ try {
1266
+ return await fullChain.invoke(
1267
+ { input: inputText, output: response },
1268
+ invokeConfig
1269
+ );
1270
+ } catch (_e) {
1271
+ // Fallback: strip callbacks to avoid EventStream tracer errors in certain environments
1272
+ // but preserve Langfuse tracing if it exists.
1273
+ const langfuseHandler = findCallback(
1274
+ invokeConfig.callbacks,
1275
+ isLangfuseCallbackHandler
1276
+ );
1277
+ const { callbacks: _cb, ...rest } = invokeConfig;
1278
+ const safeConfig = Object.assign({}, rest, {
1279
+ callbacks: langfuseHandler ? [langfuseHandler] : [],
1280
+ });
1281
+ return await fullChain.invoke(
1282
+ { input: inputText, output: response },
1283
+ safeConfig as Partial<RunnableConfig>
1284
+ );
1285
+ }
1286
+ } finally {
1287
+ await disposeLangfuseHandler(titleLangfuseHandler);
1242
1288
  }
1243
1289
  }
1244
1290
  }