@librechat/agents 3.1.90 → 3.1.91

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 (80) 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 +46 -14
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/langfuse.cjs +234 -0
  6. package/dist/cjs/langfuse.cjs.map +1 -0
  7. package/dist/cjs/main.cjs +25 -0
  8. package/dist/cjs/main.cjs.map +1 -1
  9. package/dist/cjs/run.cjs +44 -27
  10. package/dist/cjs/run.cjs.map +1 -1
  11. package/dist/cjs/stream.cjs +10 -3
  12. package/dist/cjs/stream.cjs.map +1 -1
  13. package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs +380 -0
  14. package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs.map +1 -0
  15. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs +997 -0
  16. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs.map +1 -0
  17. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs +575 -0
  18. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs.map +1 -0
  19. package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs +165 -0
  20. package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs.map +1 -0
  21. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +17 -5
  22. package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -1
  23. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs +110 -6
  24. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs.map +1 -1
  25. package/dist/esm/agents/AgentContext.mjs +9 -5
  26. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  27. package/dist/esm/graphs/Graph.mjs +46 -14
  28. package/dist/esm/graphs/Graph.mjs.map +1 -1
  29. package/dist/esm/langfuse.mjs +226 -0
  30. package/dist/esm/langfuse.mjs.map +1 -0
  31. package/dist/esm/main.mjs +5 -1
  32. package/dist/esm/main.mjs.map +1 -1
  33. package/dist/esm/run.mjs +44 -27
  34. package/dist/esm/run.mjs.map +1 -1
  35. package/dist/esm/stream.mjs +10 -3
  36. package/dist/esm/stream.mjs.map +1 -1
  37. package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs +378 -0
  38. package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs.map +1 -0
  39. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs +994 -0
  40. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs.map +1 -0
  41. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs +566 -0
  42. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs.map +1 -0
  43. package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs +155 -0
  44. package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs.map +1 -0
  45. package/dist/esm/tools/local/LocalExecutionEngine.mjs +17 -6
  46. package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -1
  47. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs +111 -7
  48. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs.map +1 -1
  49. package/dist/types/agents/AgentContext.d.ts +4 -1
  50. package/dist/types/graphs/Graph.d.ts +6 -5
  51. package/dist/types/index.d.ts +1 -0
  52. package/dist/types/langfuse.d.ts +48 -0
  53. package/dist/types/tools/cloudflare/CloudflareBridgeRuntime.d.ts +23 -0
  54. package/dist/types/tools/cloudflare/CloudflareProgrammaticToolCalling.d.ts +4 -0
  55. package/dist/types/tools/cloudflare/CloudflareSandboxExecutionEngine.d.ts +21 -0
  56. package/dist/types/tools/cloudflare/CloudflareSandboxTools.d.ts +22 -0
  57. package/dist/types/tools/cloudflare/index.d.ts +4 -0
  58. package/dist/types/tools/local/LocalExecutionEngine.d.ts +1 -0
  59. package/dist/types/types/graph.d.ts +8 -0
  60. package/dist/types/types/tools.d.ts +118 -2
  61. package/package.json +4 -4
  62. package/src/__tests__/stream.eagerEventExecution.test.ts +66 -0
  63. package/src/agents/AgentContext.ts +13 -3
  64. package/src/graphs/Graph.ts +53 -16
  65. package/src/index.ts +1 -0
  66. package/src/langfuse.ts +358 -0
  67. package/src/run.ts +60 -38
  68. package/src/specs/langfuse-config.test.ts +57 -0
  69. package/src/specs/langfuse-metadata.test.ts +19 -1
  70. package/src/stream.ts +13 -3
  71. package/src/tools/__tests__/CloudflareSandboxExecution.test.ts +537 -0
  72. package/src/tools/cloudflare/CloudflareBridgeRuntime.ts +480 -0
  73. package/src/tools/cloudflare/CloudflareProgrammaticToolCalling.ts +1162 -0
  74. package/src/tools/cloudflare/CloudflareSandboxExecutionEngine.ts +744 -0
  75. package/src/tools/cloudflare/CloudflareSandboxTools.ts +225 -0
  76. package/src/tools/cloudflare/index.ts +4 -0
  77. package/src/tools/local/LocalExecutionEngine.ts +20 -4
  78. package/src/tools/local/resolveLocalExecutionTools.ts +169 -7
  79. package/src/types/graph.ts +9 -0
  80. package/src/types/tools.ts +141 -2
@@ -2847,6 +2847,72 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
2847
2847
  expect(graph.eagerEventToolCallChunks.size).toBe(0);
2848
2848
  });
2849
2849
 
2850
+ it('does not prestart streamed Cloudflare sandbox direct coding tools', async () => {
2851
+ const graph = createGraph({
2852
+ toolExecution: {
2853
+ engine: 'cloudflare-sandbox',
2854
+ cloudflare: { sandbox: {} },
2855
+ } as StandardGraph['toolExecution'],
2856
+ getAgentContext: jest.fn(
2857
+ (): Partial<AgentContext> => ({
2858
+ provider: Providers.OPENAI,
2859
+ reasoningKey: 'reasoning_content',
2860
+ toolDefinitions: [{ name: Constants.BASH_TOOL }, { name: 'weather' }],
2861
+ graphTools: [],
2862
+ agentId: 'agent_1',
2863
+ })
2864
+ ) as unknown as StandardGraph['getAgentContext'],
2865
+ });
2866
+ const dispatchSpy = jest.spyOn(events, 'safeDispatchCustomEvent');
2867
+ const handler = new ChatModelStreamHandler();
2868
+ const metadata = { langgraph_node: 'agent' };
2869
+
2870
+ await handler.handle(
2871
+ GraphEvents.CHAT_MODEL_STREAM,
2872
+ {
2873
+ chunk: {
2874
+ content: '',
2875
+ tool_call_chunks: [
2876
+ {
2877
+ id: 'call_weather',
2878
+ name: 'weather',
2879
+ args: '{"city":"NYC"}',
2880
+ index: 0,
2881
+ },
2882
+ ],
2883
+ } as unknown as t.StreamChunk,
2884
+ },
2885
+ metadata,
2886
+ graph
2887
+ );
2888
+ await handler.handle(
2889
+ GraphEvents.CHAT_MODEL_STREAM,
2890
+ {
2891
+ chunk: {
2892
+ content: '',
2893
+ tool_call_chunks: [
2894
+ {
2895
+ id: 'call_bash',
2896
+ name: Constants.BASH_TOOL,
2897
+ args: '{"command":"echo ok"}',
2898
+ index: 1,
2899
+ },
2900
+ ],
2901
+ } as unknown as t.StreamChunk,
2902
+ },
2903
+ metadata,
2904
+ graph
2905
+ );
2906
+
2907
+ expect(dispatchSpy).not.toHaveBeenCalledWith(
2908
+ GraphEvents.ON_TOOL_EXECUTE,
2909
+ expect.anything(),
2910
+ expect.anything()
2911
+ );
2912
+ expect(graph.eagerEventToolExecutions.size).toBe(0);
2913
+ expect(graph.eagerEventToolCallChunks.size).toBe(0);
2914
+ });
2915
+
2850
2916
  it('prestarts streamed remote bash tools when the next Anthropic tool call begins', async () => {
2851
2917
  const graph = createGraph({
2852
2918
  getAgentContext: jest.fn(
@@ -53,6 +53,7 @@ export class AgentContext {
53
53
  name,
54
54
  provider,
55
55
  clientOptions,
56
+ langfuse,
56
57
  tools,
57
58
  toolMap,
58
59
  toolEnd,
@@ -80,6 +81,7 @@ export class AgentContext {
80
81
  name: name ?? agentId,
81
82
  provider,
82
83
  clientOptions,
84
+ langfuse,
83
85
  maxContextTokens,
84
86
  streamBuffer,
85
87
  tools,
@@ -149,6 +151,8 @@ export class AgentContext {
149
151
  provider: Providers;
150
152
  /** Client options for this agent */
151
153
  clientOptions?: t.ClientOptions;
154
+ /** Per-agent Langfuse tracing configuration. */
155
+ langfuse?: t.LangfuseConfig;
152
156
  /** Token count map indexed by message position */
153
157
  indexTokenCountMap: Record<string, number | undefined> = {};
154
158
  /** Canonical pre-run token map used to restore token accounting on reset */
@@ -309,6 +313,7 @@ export class AgentContext {
309
313
  name,
310
314
  provider,
311
315
  clientOptions,
316
+ langfuse,
312
317
  maxContextTokens,
313
318
  streamBuffer,
314
319
  tokenCounter,
@@ -332,6 +337,7 @@ export class AgentContext {
332
337
  name?: string;
333
338
  provider: Providers;
334
339
  clientOptions?: t.ClientOptions;
340
+ langfuse?: t.LangfuseConfig;
335
341
  maxContextTokens?: number;
336
342
  streamBuffer?: number;
337
343
  tokenCounter?: t.TokenCounter;
@@ -355,6 +361,7 @@ export class AgentContext {
355
361
  this.name = name;
356
362
  this.provider = provider;
357
363
  this.clientOptions = clientOptions;
364
+ this.langfuse = langfuse;
358
365
  this.maxContextTokens = maxContextTokens;
359
366
  this.streamBuffer = streamBuffer;
360
367
  this.tokenCounter = tokenCounter;
@@ -458,11 +465,14 @@ export class AgentContext {
458
465
  }
459
466
 
460
467
  private hasAvailableTool(name: string): boolean {
461
- if (this.toolDefinitions?.some((tool) => tool.name === name)) return true;
462
- if (this.tools?.some((tool) => 'name' in tool && tool.name === name)) {
468
+ if (this.toolDefinitions?.some((tool) => tool.name === name) === true)
469
+ return true;
470
+ if (
471
+ this.tools?.some((tool) => 'name' in tool && tool.name === name) === true
472
+ ) {
463
473
  return true;
464
474
  }
465
- if (this.toolMap?.has(name)) return true;
475
+ if (this.toolMap?.has(name) === true) return true;
466
476
  return this.toolRegistry?.has(name) === true;
467
477
  }
468
478
 
@@ -58,8 +58,10 @@ import { createFakeStreamingLLM } from '@/llm/fake';
58
58
  import { handleToolCalls } from '@/tools/handlers';
59
59
  import { resolveLocalToolsForBinding } from '@/tools/local';
60
60
  import { createLocalCodingToolBundle } from '@/tools/local/LocalCodingTools';
61
+ import { createCloudflareCodingToolBundle } from '@/tools/cloudflare';
61
62
  import { isThinkingEnabled } from '@/llm/request';
62
63
  import { initializeModel } from '@/llm/init';
64
+ import { createLangfuseHandler, disposeLangfuseHandler } from '@/langfuse';
63
65
  import { HandlerRegistry } from '@/events';
64
66
  import { ChatOpenAI } from '@/llm/openai';
65
67
  import { partitionAndMarkOpenRouterToolCache } from '@/llm/openrouter/toolCache';
@@ -337,11 +339,12 @@ export abstract class Graph<
337
339
  /**
338
340
  * Single per-Run file checkpointer shared across every ToolNode the
339
341
  * graph compiles. Lazily constructed when
340
- * `toolExecution.local.fileCheckpointing === true` so multi-agent
341
- * graphs see ONE snapshot store, not one-per-agent. Returns
342
- * undefined when checkpointing is disabled or the local engine
343
- * isn't selected. Exposed via `Run.getFileCheckpointer()` /
344
- * `Run.rewindFiles()`.
342
+ * `toolExecution.local.fileCheckpointing === true` or
343
+ * `toolExecution.cloudflare.fileCheckpointing === true` so
344
+ * multi-agent graphs see ONE snapshot store, not one-per-agent.
345
+ * Returns undefined when checkpointing is disabled or a supported
346
+ * coding-tool engine isn't selected. Exposed via
347
+ * `Run.getFileCheckpointer()` / `Run.rewindFiles()`.
345
348
  */
346
349
  private _fileCheckpointer?: t.LocalFileCheckpointer;
347
350
  /**
@@ -364,20 +367,32 @@ export abstract class Graph<
364
367
  if (this._fileCheckpointer != null) {
365
368
  return this._fileCheckpointer;
366
369
  }
367
- if (
368
- this.toolExecution?.engine !== 'local' ||
369
- this.toolExecution.local?.fileCheckpointing !== true
370
- ) {
371
- return undefined;
372
- }
373
370
  // Eagerly create via the bundle factory so the construction path
374
371
  // matches the bundle-only callers (and future bundle-internal
375
372
  // cleanup hooks fire). The bundle factory itself accepts a pre-
376
373
  // supplied checkpointer when present, so re-injecting this one
377
374
  // into every ToolNode is idempotent.
378
- const bundle = createLocalCodingToolBundle(this.toolExecution.local ?? {});
379
- this._fileCheckpointer = bundle.checkpointer;
380
- return this._fileCheckpointer;
375
+ if (
376
+ this.toolExecution?.engine === 'local' &&
377
+ this.toolExecution.local?.fileCheckpointing === true
378
+ ) {
379
+ const bundle = createLocalCodingToolBundle(
380
+ this.toolExecution.local ?? {}
381
+ );
382
+ this._fileCheckpointer = bundle.checkpointer;
383
+ return this._fileCheckpointer;
384
+ }
385
+ if (
386
+ this.toolExecution?.engine === 'cloudflare-sandbox' &&
387
+ this.toolExecution.cloudflare?.fileCheckpointing === true
388
+ ) {
389
+ const bundle = createCloudflareCodingToolBundle(
390
+ this.toolExecution.cloudflare
391
+ );
392
+ this._fileCheckpointer = bundle.checkpointer;
393
+ return this._fileCheckpointer;
394
+ }
395
+ return undefined;
381
396
  }
382
397
  }
383
398
 
@@ -1318,6 +1333,26 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1318
1333
  { force: true }
1319
1334
  );
1320
1335
 
1336
+ const langfuseHandler = createLangfuseHandler({
1337
+ langfuse: agentContext.langfuse,
1338
+ userId: config.configurable?.user_id as string | undefined,
1339
+ sessionId: config.configurable?.thread_id as string | undefined,
1340
+ traceMetadata: {
1341
+ messageId: this.runId,
1342
+ parentMessageId: config.configurable?.requestBody?.parentMessageId,
1343
+ agentId,
1344
+ agentName: agentContext.name,
1345
+ },
1346
+ });
1347
+ const invokeConfig = langfuseHandler
1348
+ ? {
1349
+ ...config,
1350
+ callbacks: ((config.callbacks as t.ProvidedCallbacks) ?? []).concat(
1351
+ [langfuseHandler]
1352
+ ),
1353
+ }
1354
+ : config;
1355
+
1321
1356
  try {
1322
1357
  result = await attemptInvoke(
1323
1358
  {
@@ -1326,17 +1361,19 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1326
1361
  provider: agentContext.provider,
1327
1362
  context: this,
1328
1363
  },
1329
- config
1364
+ invokeConfig
1330
1365
  );
1331
1366
  } catch (primaryError) {
1332
1367
  result = await tryFallbackProviders({
1333
1368
  fallbacks,
1334
1369
  tools: agentContext.tools,
1335
1370
  messages: finalMessages,
1336
- config,
1371
+ config: invokeConfig,
1337
1372
  primaryError,
1338
1373
  context: this,
1339
1374
  });
1375
+ } finally {
1376
+ await disposeLangfuseHandler(langfuseHandler);
1340
1377
  }
1341
1378
 
1342
1379
  if (!result) {
package/src/index.ts CHANGED
@@ -27,6 +27,7 @@ export * from './tools/ToolNode';
27
27
  export * from './tools/schema';
28
28
  export * from './tools/handlers';
29
29
  export * from './tools/local';
30
+ export * from './tools/cloudflare';
30
31
  export * from './tools/search';
31
32
 
32
33
  /* Misc. */
@@ -0,0 +1,358 @@
1
+ import { CallbackHandler } from '@langfuse/langchain';
2
+ import { LangfuseSpanProcessor } from '@langfuse/otel';
3
+ import {
4
+ createObservationAttributes,
5
+ createTraceAttributes,
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 { Span } from '@opentelemetry/api';
14
+ import type * as t from '@/types';
15
+ import { isPresent } from '@/utils/misc';
16
+
17
+ type TraceMetadata = Record<string, unknown>;
18
+
19
+ type LangfuseHandlerParams = {
20
+ userId?: string;
21
+ sessionId?: string;
22
+ traceMetadata?: TraceMetadata;
23
+ };
24
+
25
+ type AgentLangfuseHandlerParams = LangfuseHandlerParams & {
26
+ langfuse?: t.LangfuseConfig;
27
+ };
28
+
29
+ type ResolvedLangfuseConfig = t.LangfuseConfig & {
30
+ enabled: true;
31
+ publicKey: string;
32
+ secretKey: string;
33
+ };
34
+
35
+ function getModelName(serialized: Serialized): string {
36
+ const serializedRecord = serialized as unknown as Record<string, unknown>;
37
+ const kwargs = serializedRecord.kwargs as Record<string, unknown> | undefined;
38
+ const modelName =
39
+ kwargs?.model ??
40
+ kwargs?.model_name ??
41
+ kwargs?.modelName ??
42
+ kwargs?.model_id ??
43
+ kwargs?.modelId ??
44
+ serializedRecord.name;
45
+
46
+ if (typeof modelName === 'string' && modelName.trim() !== '') {
47
+ return modelName;
48
+ }
49
+
50
+ if (Array.isArray(serializedRecord.id) && serializedRecord.id.length > 0) {
51
+ return String(serializedRecord.id[serializedRecord.id.length - 1]);
52
+ }
53
+
54
+ return 'ChatModel';
55
+ }
56
+
57
+ function getModelParameters(
58
+ extraParams?: Record<string, unknown>
59
+ ): Record<string, string | number> {
60
+ const invocationParams = extraParams?.invocation_params;
61
+ const params =
62
+ invocationParams != null && typeof invocationParams === 'object'
63
+ ? (invocationParams as Record<string, unknown>)
64
+ : (extraParams ?? {});
65
+
66
+ return Object.fromEntries(
67
+ Object.entries(params).filter(([, value]) => {
68
+ return typeof value === 'string' || typeof value === 'number';
69
+ })
70
+ ) as Record<string, string | number>;
71
+ }
72
+
73
+ function getOutput(output: LLMResult): unknown {
74
+ return output.generations.map((generation) =>
75
+ generation.map((item) => {
76
+ if ('message' in item && item.message != null) {
77
+ return (item.message as { content?: unknown }).content;
78
+ }
79
+ return item.text;
80
+ })
81
+ );
82
+ }
83
+
84
+ function getUsageDetails(
85
+ output: LLMResult
86
+ ): Record<string, number> | undefined {
87
+ const llmOutput = output.llmOutput as Record<string, unknown> | undefined;
88
+ const usage = llmOutput?.tokenUsage ?? llmOutput?.usage;
89
+ if (usage == null || typeof usage !== 'object') {
90
+ return undefined;
91
+ }
92
+
93
+ const usageEntries = Object.entries(usage as Record<string, unknown>).filter(
94
+ ([, value]) => typeof value === 'number'
95
+ );
96
+
97
+ return usageEntries.length > 0
98
+ ? (Object.fromEntries(usageEntries) as Record<string, number>)
99
+ : undefined;
100
+ }
101
+
102
+ function getTraceName(traceMetadata?: TraceMetadata): string {
103
+ const agentName = traceMetadata?.agentName;
104
+ return typeof agentName === 'string' && agentName.trim() !== ''
105
+ ? `LibreChat Agent: ${agentName}`
106
+ : 'LibreChat Agent';
107
+ }
108
+
109
+ export class LangfuseAgentCallbackHandler extends BaseCallbackHandler {
110
+ name = 'librechat_langfuse_agent_handler';
111
+
112
+ private readonly provider: BasicTracerProvider;
113
+ private readonly processor: LangfuseSpanProcessor;
114
+ private readonly userId?: string;
115
+ private readonly sessionId?: string;
116
+ private readonly traceMetadata?: TraceMetadata;
117
+ private readonly spans = new Map<string, Span>();
118
+
119
+ constructor({
120
+ langfuse,
121
+ userId,
122
+ sessionId,
123
+ traceMetadata,
124
+ }: LangfuseHandlerParams & { langfuse: ResolvedLangfuseConfig }) {
125
+ super();
126
+ this.userId = userId;
127
+ this.sessionId = sessionId;
128
+ this.traceMetadata = traceMetadata;
129
+ this.processor = new LangfuseSpanProcessor({
130
+ publicKey: langfuse.publicKey,
131
+ secretKey: langfuse.secretKey,
132
+ ...(isPresent(langfuse.baseUrl) ? { baseUrl: langfuse.baseUrl } : {}),
133
+ environment:
134
+ process.env.LANGFUSE_TRACING_ENVIRONMENT ??
135
+ process.env.NODE_ENV ??
136
+ 'development',
137
+ exportMode: 'immediate',
138
+ });
139
+ this.provider = new BasicTracerProvider({
140
+ spanProcessors: [this.processor],
141
+ });
142
+ }
143
+
144
+ private startGenerationSpan({
145
+ llm,
146
+ input,
147
+ runId,
148
+ extraParams,
149
+ metadata,
150
+ name,
151
+ }: {
152
+ llm: Serialized;
153
+ input: unknown;
154
+ runId: string;
155
+ extraParams?: Record<string, unknown>;
156
+ metadata?: Record<string, unknown>;
157
+ name?: string;
158
+ }): void {
159
+ if (this.spans.has(runId)) {
160
+ return;
161
+ }
162
+
163
+ const tracer = this.provider.getTracer('librechat-agents-langfuse');
164
+ const spanName =
165
+ typeof name === 'string' && name.trim() !== '' ? name : getModelName(llm);
166
+ const span = tracer.startSpan(spanName, {
167
+ attributes: {
168
+ ...createTraceAttributes({
169
+ name: getTraceName(this.traceMetadata),
170
+ userId: this.userId,
171
+ sessionId: this.sessionId,
172
+ metadata: this.traceMetadata,
173
+ }),
174
+ ...createObservationAttributes('generation', {
175
+ input,
176
+ model: getModelName(llm),
177
+ modelParameters: getModelParameters(extraParams),
178
+ metadata: {
179
+ ...metadata,
180
+ ...this.traceMetadata,
181
+ },
182
+ }),
183
+ },
184
+ });
185
+ this.spans.set(runId, span);
186
+ }
187
+
188
+ async handleChatModelStart(
189
+ llm: Serialized,
190
+ messages: BaseMessage[][],
191
+ runId: string,
192
+ _parentRunId?: string,
193
+ extraParams?: Record<string, unknown>,
194
+ _tags?: string[],
195
+ metadata?: Record<string, unknown>,
196
+ name?: string
197
+ ): Promise<void> {
198
+ this.startGenerationSpan({
199
+ llm,
200
+ input: messages,
201
+ runId,
202
+ extraParams,
203
+ metadata,
204
+ name,
205
+ });
206
+ }
207
+
208
+ async handleLLMStart(
209
+ llm: Serialized,
210
+ prompts: string[],
211
+ runId: string,
212
+ _parentRunId?: string,
213
+ extraParams?: Record<string, unknown>,
214
+ _tags?: string[],
215
+ metadata?: Record<string, unknown>,
216
+ name?: string
217
+ ): Promise<void> {
218
+ this.startGenerationSpan({
219
+ llm,
220
+ input: prompts,
221
+ runId,
222
+ extraParams,
223
+ metadata,
224
+ name,
225
+ });
226
+ }
227
+
228
+ async handleLLMEnd(output: LLMResult, runId: string): Promise<void> {
229
+ const span = this.spans.get(runId);
230
+ if (!span) {
231
+ return;
232
+ }
233
+
234
+ span.setAttributes(
235
+ createObservationAttributes('generation', {
236
+ output: getOutput(output),
237
+ usageDetails: getUsageDetails(output),
238
+ })
239
+ );
240
+ span.end();
241
+ this.spans.delete(runId);
242
+ await this.flush();
243
+ }
244
+
245
+ async handleLLMError(err: unknown, runId: string): Promise<void> {
246
+ const span = this.spans.get(runId);
247
+ if (!span) {
248
+ return;
249
+ }
250
+
251
+ const message = err instanceof Error ? err.message : String(err);
252
+ span.setStatus({ code: SpanStatusCode.ERROR, message });
253
+ span.setAttributes(
254
+ createObservationAttributes('generation', {
255
+ level: 'ERROR',
256
+ statusMessage: message,
257
+ })
258
+ );
259
+ span.end();
260
+ this.spans.delete(runId);
261
+ await this.flush();
262
+ }
263
+
264
+ private async flush(): Promise<void> {
265
+ try {
266
+ await this.provider.forceFlush();
267
+ } catch (error) {
268
+ process.emitWarning(
269
+ `[LangfuseAgentCallbackHandler] Failed to flush Langfuse spans: ${
270
+ error instanceof Error ? error.message : String(error)
271
+ }`
272
+ );
273
+ }
274
+ }
275
+
276
+ async dispose(): Promise<void> {
277
+ for (const span of this.spans.values()) {
278
+ span.end();
279
+ }
280
+ this.spans.clear();
281
+ await this.flush();
282
+ try {
283
+ await this.provider.shutdown();
284
+ } catch (error) {
285
+ process.emitWarning(
286
+ `[LangfuseAgentCallbackHandler] Failed to shut down Langfuse provider: ${
287
+ error instanceof Error ? error.message : String(error)
288
+ }`
289
+ );
290
+ }
291
+ }
292
+ }
293
+
294
+ function hasRequiredLangfuseConfig(
295
+ langfuse?: t.LangfuseConfig
296
+ ): langfuse is ResolvedLangfuseConfig {
297
+ return (
298
+ langfuse?.enabled === true &&
299
+ isPresent(langfuse.publicKey) &&
300
+ isPresent(langfuse.secretKey)
301
+ );
302
+ }
303
+
304
+ export function createLegacyLangfuseHandler(
305
+ params: LangfuseHandlerParams
306
+ ): CallbackHandler {
307
+ return new CallbackHandler(params);
308
+ }
309
+
310
+ export function createLangfuseHandler({
311
+ langfuse,
312
+ userId,
313
+ sessionId,
314
+ traceMetadata,
315
+ }: AgentLangfuseHandlerParams): LangfuseAgentCallbackHandler | undefined {
316
+ if (!hasRequiredLangfuseConfig(langfuse)) {
317
+ return undefined;
318
+ }
319
+
320
+ return new LangfuseAgentCallbackHandler({
321
+ langfuse,
322
+ userId,
323
+ sessionId,
324
+ traceMetadata,
325
+ });
326
+ }
327
+
328
+ export function hasExplicitLangfuseConfig(
329
+ contexts: Iterable<{ langfuse?: t.LangfuseConfig }>
330
+ ): boolean {
331
+ for (const context of contexts) {
332
+ if (context.langfuse != null) {
333
+ return true;
334
+ }
335
+ }
336
+ return false;
337
+ }
338
+
339
+ export function hasLangfuseEnvConfig(): boolean {
340
+ return (
341
+ isPresent(process.env.LANGFUSE_SECRET_KEY) &&
342
+ isPresent(process.env.LANGFUSE_PUBLIC_KEY) &&
343
+ isPresent(process.env.LANGFUSE_BASE_URL)
344
+ );
345
+ }
346
+
347
+ export function isLangfuseCallbackHandler(value: unknown): boolean {
348
+ return (
349
+ value instanceof CallbackHandler ||
350
+ value instanceof LangfuseAgentCallbackHandler
351
+ );
352
+ }
353
+
354
+ export async function disposeLangfuseHandler(value: unknown): Promise<void> {
355
+ if (value instanceof LangfuseAgentCallbackHandler) {
356
+ await value.dispose();
357
+ }
358
+ }