@librechat/agents 3.1.91 → 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.
package/src/langfuse.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { CallbackHandler } from '@langfuse/langchain';
2
- import { LangfuseSpanProcessor } from '@langfuse/otel';
2
+ import { isDefaultExportSpan, LangfuseSpanProcessor } from '@langfuse/otel';
3
3
  import {
4
+ LangfuseOtelSpanAttributes,
4
5
  createObservationAttributes,
5
- createTraceAttributes,
6
6
  } from '@langfuse/tracing';
7
7
  import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
8
8
  import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';
@@ -10,16 +10,20 @@ import { SpanStatusCode } from '@opentelemetry/api';
10
10
  import type { Serialized } from '@langchain/core/load/serializable';
11
11
  import type { BaseMessage } from '@langchain/core/messages';
12
12
  import type { LLMResult } from '@langchain/core/outputs';
13
- import type { Span } from '@opentelemetry/api';
13
+ import type { Attributes, Span } from '@opentelemetry/api';
14
14
  import type * as t from '@/types';
15
15
  import { isPresent } from '@/utils/misc';
16
16
 
17
- type TraceMetadata = Record<string, unknown>;
17
+ const TRACE_METADATA_MAX_LENGTH = 200;
18
+ const LANGFUSE_TRACER_NAME = 'langfuse-sdk';
19
+
20
+ export type LangfuseTraceMetadata = Record<string, string>;
18
21
 
19
22
  type LangfuseHandlerParams = {
20
23
  userId?: string;
21
24
  sessionId?: string;
22
- traceMetadata?: TraceMetadata;
25
+ traceMetadata?: LangfuseTraceMetadata;
26
+ tags?: string[];
23
27
  };
24
28
 
25
29
  type AgentLangfuseHandlerParams = LangfuseHandlerParams & {
@@ -32,6 +36,49 @@ type ResolvedLangfuseConfig = t.LangfuseConfig & {
32
36
  secretKey: string;
33
37
  };
34
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
+
35
82
  function getModelName(serialized: Serialized): string {
36
83
  const serializedRecord = serialized as unknown as Record<string, unknown>;
37
84
  const kwargs = serializedRecord.kwargs as Record<string, unknown> | undefined;
@@ -99,11 +146,39 @@ function getUsageDetails(
99
146
  : undefined;
100
147
  }
101
148
 
102
- function getTraceName(traceMetadata?: TraceMetadata): string {
149
+ export function getLangfuseTraceName(
150
+ traceMetadata?: LangfuseTraceMetadata,
151
+ fallback: string = 'LibreChat Agent'
152
+ ): string {
103
153
  const agentName = traceMetadata?.agentName;
104
- return typeof agentName === 'string' && agentName.trim() !== ''
105
- ? `LibreChat Agent: ${agentName}`
106
- : 'LibreChat Agent';
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;
107
182
  }
108
183
 
109
184
  export class LangfuseAgentCallbackHandler extends BaseCallbackHandler {
@@ -113,7 +188,8 @@ export class LangfuseAgentCallbackHandler extends BaseCallbackHandler {
113
188
  private readonly processor: LangfuseSpanProcessor;
114
189
  private readonly userId?: string;
115
190
  private readonly sessionId?: string;
116
- private readonly traceMetadata?: TraceMetadata;
191
+ private readonly traceMetadata?: LangfuseTraceMetadata;
192
+ private readonly tags?: string[];
117
193
  private readonly spans = new Map<string, Span>();
118
194
 
119
195
  constructor({
@@ -121,11 +197,13 @@ export class LangfuseAgentCallbackHandler extends BaseCallbackHandler {
121
197
  userId,
122
198
  sessionId,
123
199
  traceMetadata,
200
+ tags,
124
201
  }: LangfuseHandlerParams & { langfuse: ResolvedLangfuseConfig }) {
125
202
  super();
126
203
  this.userId = userId;
127
204
  this.sessionId = sessionId;
128
205
  this.traceMetadata = traceMetadata;
206
+ this.tags = tags;
129
207
  this.processor = new LangfuseSpanProcessor({
130
208
  publicKey: langfuse.publicKey,
131
209
  secretKey: langfuse.secretKey,
@@ -135,6 +213,9 @@ export class LangfuseAgentCallbackHandler extends BaseCallbackHandler {
135
213
  process.env.NODE_ENV ??
136
214
  'development',
137
215
  exportMode: 'immediate',
216
+ shouldExportSpan: ({ otelSpan }): boolean =>
217
+ isDefaultExportSpan(otelSpan) ||
218
+ otelSpan.instrumentationScope.name === LANGFUSE_TRACER_NAME,
138
219
  });
139
220
  this.provider = new BasicTracerProvider({
140
221
  spanProcessors: [this.processor],
@@ -160,16 +241,16 @@ export class LangfuseAgentCallbackHandler extends BaseCallbackHandler {
160
241
  return;
161
242
  }
162
243
 
163
- const tracer = this.provider.getTracer('librechat-agents-langfuse');
244
+ const tracer = this.provider.getTracer(LANGFUSE_TRACER_NAME);
164
245
  const spanName =
165
246
  typeof name === 'string' && name.trim() !== '' ? name : getModelName(llm);
166
247
  const span = tracer.startSpan(spanName, {
167
248
  attributes: {
168
- ...createTraceAttributes({
169
- name: getTraceName(this.traceMetadata),
249
+ ...getTraceAttributes({
170
250
  userId: this.userId,
171
251
  sessionId: this.sessionId,
172
- metadata: this.traceMetadata,
252
+ traceMetadata: this.traceMetadata,
253
+ tags: this.tags,
173
254
  }),
174
255
  ...createObservationAttributes('generation', {
175
256
  input,
@@ -312,6 +393,7 @@ export function createLangfuseHandler({
312
393
  userId,
313
394
  sessionId,
314
395
  traceMetadata,
396
+ tags,
315
397
  }: AgentLangfuseHandlerParams): LangfuseAgentCallbackHandler | undefined {
316
398
  if (!hasRequiredLangfuseConfig(langfuse)) {
317
399
  return undefined;
@@ -322,6 +404,7 @@ export function createLangfuseHandler({
322
404
  userId,
323
405
  sessionId,
324
406
  traceMetadata,
407
+ tags,
325
408
  });
326
409
  }
327
410
 
@@ -340,7 +423,7 @@ export function hasLangfuseEnvConfig(): boolean {
340
423
  return (
341
424
  isPresent(process.env.LANGFUSE_SECRET_KEY) &&
342
425
  isPresent(process.env.LANGFUSE_PUBLIC_KEY) &&
343
- isPresent(process.env.LANGFUSE_BASE_URL)
426
+ isPresent(getEnvLangfuseBaseUrl())
344
427
  );
345
428
  }
346
429
 
package/src/run.ts CHANGED
@@ -30,10 +30,17 @@ import { initializeModel } from '@/llm/init';
30
30
  import { HandlerRegistry } from '@/events';
31
31
  import { executeHooks } from '@/hooks';
32
32
  import { isOpenAILike } from '@/utils/llm';
33
+ import {
34
+ appendCallbacks,
35
+ findCallback,
36
+ type CallbackEntry,
37
+ } from '@/utils/callbacks';
33
38
  import {
34
39
  createLegacyLangfuseHandler,
40
+ createLangfuseTraceMetadata,
35
41
  createLangfuseHandler,
36
42
  disposeLangfuseHandler,
43
+ getLangfuseTraceName,
37
44
  hasExplicitLangfuseConfig,
38
45
  hasLangfuseEnvConfig,
39
46
  isLangfuseCallbackHandler,
@@ -598,42 +605,48 @@ export class Run<_T extends t.BaseGraphState> {
598
605
  /** Custom event callback to intercept and handle custom events */
599
606
  const customEventCallback = this.createCustomEventCallback();
600
607
 
601
- const baseCallbacks = (config.callbacks as t.ProvidedCallbacks) ?? [];
602
608
  const streamCallbacks = streamOptions?.callbacks
603
609
  ? this.getCallbacks(streamOptions.callbacks)
604
- : [];
610
+ : undefined;
605
611
 
606
612
  const customHandler = BaseCallbackHandler.fromMethods({
607
613
  [Callback.CUSTOM_EVENT]: customEventCallback,
608
614
  });
609
615
  customHandler.awaitHandlers = true;
610
616
 
611
- config.callbacks = baseCallbacks
612
- .concat(streamCallbacks)
613
- .concat(customHandler);
617
+ config.callbacks = appendCallbacks(
618
+ config.callbacks,
619
+ streamCallbacks ? [streamCallbacks, customHandler] : [customHandler]
620
+ );
614
621
 
615
622
  if (
616
623
  hasLangfuseEnvConfig() &&
617
624
  !hasExplicitLangfuseConfig(this.Graph.agentContexts.values())
618
625
  ) {
619
- const userId = config.configurable?.user_id;
620
- 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;
621
634
  const primaryContext = this.Graph.agentContexts.get(
622
635
  this.Graph.defaultAgentId
623
636
  );
624
- const traceMetadata = {
637
+ const traceMetadata = createLangfuseTraceMetadata({
625
638
  messageId: this.id,
626
639
  parentMessageId: config.configurable?.requestBody?.parentMessageId,
627
640
  agentName: primaryContext?.name,
628
- };
641
+ });
629
642
  const handler = createLegacyLangfuseHandler({
630
643
  userId,
631
644
  sessionId,
632
645
  traceMetadata,
646
+ tags: ['librechat', 'agent'],
633
647
  });
634
- config.callbacks = (
635
- (config.callbacks as t.ProvidedCallbacks) ?? []
636
- ).concat([handler]);
648
+ config.runName = config.runName ?? getLangfuseTraceName(traceMetadata);
649
+ config.callbacks = appendCallbacks(config.callbacks, [handler]);
637
650
  }
638
651
 
639
652
  if (!this.id) {
@@ -1139,17 +1152,26 @@ export class Run<_T extends t.BaseGraphState> {
1139
1152
  titleMethod = TitleMethod.COMPLETION,
1140
1153
  titlePromptTemplate,
1141
1154
  }: t.RunTitleOptions): Promise<{ language?: string; title?: string }> {
1142
- let titleLangfuseHandler: unknown;
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
+
1143
1166
  if (chainOptions != null) {
1144
- const userId = chainOptions.configurable?.user_id;
1145
- const sessionId = chainOptions.configurable?.thread_id;
1146
- const titleContext = this.Graph?.agentContexts.get(
1147
- this.Graph.defaultAgentId
1148
- );
1149
- const traceMetadata = {
1150
- messageId: 'title-' + this.id,
1151
- agentName: titleContext?.name,
1152
- };
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;
1153
1175
  const hasExplicitLangfuse =
1154
1176
  this.Graph != null &&
1155
1177
  hasExplicitLangfuseConfig(this.Graph.agentContexts.values());
@@ -1159,20 +1181,20 @@ export class Run<_T extends t.BaseGraphState> {
1159
1181
  userId,
1160
1182
  sessionId,
1161
1183
  traceMetadata,
1184
+ tags: ['librechat', 'title'],
1162
1185
  });
1163
1186
  } else if (hasLangfuseEnvConfig() && !hasExplicitLangfuse) {
1164
1187
  titleLangfuseHandler = createLegacyLangfuseHandler({
1165
1188
  userId,
1166
1189
  sessionId,
1167
1190
  traceMetadata,
1191
+ tags: ['librechat', 'title'],
1168
1192
  });
1169
1193
  }
1170
1194
 
1171
1195
  if (titleLangfuseHandler != null) {
1172
- chainOptions.callbacks = (
1173
- (chainOptions.callbacks as t.ProvidedCallbacks) ?? []
1174
- ).concat([
1175
- titleLangfuseHandler as NonNullable<t.ProvidedCallbacks>[number],
1196
+ chainOptions.callbacks = appendCallbacks(chainOptions.callbacks, [
1197
+ titleLangfuseHandler,
1176
1198
  ]);
1177
1199
  }
1178
1200
  }
@@ -1236,6 +1258,7 @@ export class Run<_T extends t.BaseGraphState> {
1236
1258
  const invokeConfig = Object.assign({}, chainOptions, {
1237
1259
  run_id: this.id,
1238
1260
  runId: this.id,
1261
+ runName: chainOptions?.runName ?? titleRunName,
1239
1262
  });
1240
1263
 
1241
1264
  try {
@@ -1247,9 +1270,10 @@ export class Run<_T extends t.BaseGraphState> {
1247
1270
  } catch (_e) {
1248
1271
  // Fallback: strip callbacks to avoid EventStream tracer errors in certain environments
1249
1272
  // but preserve Langfuse tracing if it exists.
1250
- const langfuseHandler = (
1251
- invokeConfig.callbacks as t.ProvidedCallbacks
1252
- )?.find(isLangfuseCallbackHandler);
1273
+ const langfuseHandler = findCallback(
1274
+ invokeConfig.callbacks,
1275
+ isLangfuseCallbackHandler
1276
+ );
1253
1277
  const { callbacks: _cb, ...rest } = invokeConfig;
1254
1278
  const safeConfig = Object.assign({}, rest, {
1255
1279
  callbacks: langfuseHandler ? [langfuseHandler] : [],
@@ -0,0 +1,75 @@
1
+ import { CallbackManager } from '@langchain/core/callbacks/manager';
2
+ import { HumanMessage } from '@langchain/core/messages';
3
+ import { Providers } from '@/common';
4
+ import { Run } from '@/run';
5
+ import type * as t from '@/types';
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 mockForceFlush = jest.fn();
14
+ const mockShutdown = jest.fn();
15
+
16
+ jest.mock('@langfuse/otel', () => ({
17
+ LangfuseSpanProcessor: jest.fn().mockImplementation(() => ({})),
18
+ isDefaultExportSpan: jest.fn(() => false),
19
+ }));
20
+
21
+ jest.mock('@opentelemetry/sdk-trace-base', () => ({
22
+ BasicTracerProvider: jest.fn().mockImplementation(() => ({
23
+ forceFlush: mockForceFlush,
24
+ getTracer: jest.fn(() => ({
25
+ startSpan: mockStartSpan,
26
+ })),
27
+ shutdown: mockShutdown,
28
+ })),
29
+ }));
30
+
31
+ describe('Langfuse callback composition', () => {
32
+ beforeEach(() => {
33
+ jest.clearAllMocks();
34
+ });
35
+
36
+ it('runs explicit per-agent tracing when callbacks is a CallbackManager', async () => {
37
+ const manager = CallbackManager.fromHandlers({
38
+ handleCustomEvent: async (): Promise<void> => undefined,
39
+ });
40
+ const run = await Run.create<t.IState>({
41
+ runId: 'test-langfuse-callback-manager',
42
+ graphConfig: {
43
+ type: 'standard',
44
+ agents: [
45
+ {
46
+ agentId: 'agent_abc123',
47
+ name: 'DWAINE',
48
+ provider: Providers.OPENAI,
49
+ clientOptions: { model: 'gpt-4' },
50
+ tools: [],
51
+ langfuse: {
52
+ enabled: true,
53
+ publicKey: 'pk-test',
54
+ secretKey: 'sk-test',
55
+ },
56
+ },
57
+ ],
58
+ },
59
+ skipCleanup: true,
60
+ });
61
+
62
+ run.Graph?.overrideTestModel(['hello']);
63
+
64
+ const config = {
65
+ callbacks: manager,
66
+ configurable: { thread_id: 'thread-1', user_id: 'user-1' },
67
+ streamMode: 'values' as const,
68
+ version: 'v2' as const,
69
+ };
70
+
71
+ await run.processStream({ messages: [new HumanMessage('hello')] }, config);
72
+
73
+ expect(mockStartSpan).toHaveBeenCalled();
74
+ });
75
+ });
@@ -1,15 +1,28 @@
1
1
  import { LangfuseSpanProcessor } from '@langfuse/otel';
2
2
  import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';
3
+ import { HumanMessage } from '@langchain/core/messages';
4
+ import type { Serialized } from '@langchain/core/load/serializable';
3
5
  import { createLangfuseHandler } from '@/langfuse';
4
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
+ }));
16
+
5
17
  jest.mock('@langfuse/otel', () => ({
6
18
  LangfuseSpanProcessor: jest.fn().mockImplementation(() => ({})),
19
+ isDefaultExportSpan: jest.fn(() => false),
7
20
  }));
8
21
 
9
22
  jest.mock('@opentelemetry/sdk-trace-base', () => ({
10
23
  BasicTracerProvider: jest.fn().mockImplementation(() => ({
11
24
  forceFlush: jest.fn(),
12
- getTracer: jest.fn(),
25
+ getTracer: mockGetTracer,
13
26
  shutdown: jest.fn(),
14
27
  })),
15
28
  }));
@@ -42,6 +55,50 @@ describe('createLangfuseHandler', () => {
42
55
  expect(BasicTracerProvider).toHaveBeenCalledTimes(1);
43
56
  });
44
57
 
58
+ it('starts per-agent spans with v5 trace attributes', async () => {
59
+ const handler = createLangfuseHandler({
60
+ langfuse: {
61
+ enabled: true,
62
+ publicKey: 'pk-test',
63
+ secretKey: 'sk-test',
64
+ },
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
+ });
74
+
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
+ );
100
+ });
101
+
45
102
  it('does not create a handler when a required key is missing', () => {
46
103
  const handler = createLangfuseHandler({
47
104
  langfuse: {
package/src/types/run.ts CHANGED
@@ -3,10 +3,7 @@ import type * as z from 'zod';
3
3
  import type { BaseMessage } from '@langchain/core/messages';
4
4
  import type { StructuredTool } from '@langchain/core/tools';
5
5
  import type { RunnableConfig } from '@langchain/core/runnables';
6
- import type {
7
- BaseCallbackHandler,
8
- CallbackHandlerMethods,
9
- } from '@langchain/core/callbacks/base';
6
+ import type { Callbacks } from '@langchain/core/callbacks/manager';
10
7
  import type * as s from '@/types/stream';
11
8
  import type * as e from '@/common/enum';
12
9
  import type * as g from '@/types/graph';
@@ -213,9 +210,7 @@ export type RunConfig = {
213
210
  humanInTheLoop?: HumanInTheLoopConfig;
214
211
  };
215
212
 
216
- export type ProvidedCallbacks =
217
- | (BaseCallbackHandler | CallbackHandlerMethods)[]
218
- | undefined;
213
+ export type ProvidedCallbacks = Callbacks | undefined;
219
214
 
220
215
  export type TokenCounter = (message: BaseMessage) => number;
221
216
 
@@ -0,0 +1,39 @@
1
+ import { ensureHandler } from '@langchain/core/callbacks/manager';
2
+ import type {
3
+ BaseCallbackHandler,
4
+ CallbackHandlerMethods,
5
+ } from '@langchain/core/callbacks/base';
6
+ import type { Callbacks } from '@langchain/core/callbacks/manager';
7
+
8
+ export type CallbackEntry = BaseCallbackHandler | CallbackHandlerMethods;
9
+
10
+ export function appendCallbacks(
11
+ callbacks: Callbacks | undefined,
12
+ additions: readonly CallbackEntry[]
13
+ ): Callbacks {
14
+ if (additions.length === 0) {
15
+ return callbacks ?? [];
16
+ }
17
+
18
+ if (callbacks == null) {
19
+ return [...additions];
20
+ }
21
+
22
+ if (Array.isArray(callbacks)) {
23
+ return callbacks.concat(additions);
24
+ }
25
+
26
+ return callbacks.copy(additions.map(ensureHandler));
27
+ }
28
+
29
+ export function findCallback(
30
+ callbacks: Callbacks | undefined,
31
+ predicate: (callback: CallbackEntry) => boolean
32
+ ): CallbackEntry | undefined {
33
+ if (callbacks == null) {
34
+ return undefined;
35
+ }
36
+
37
+ const handlers = Array.isArray(callbacks) ? callbacks : callbacks.handlers;
38
+ return handlers.find(predicate);
39
+ }