@librechat/agents 3.1.91 → 3.1.93

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 (45) hide show
  1. package/dist/cjs/graphs/Graph.cjs +5 -3
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/instrumentation.cjs +2 -7
  4. package/dist/cjs/instrumentation.cjs.map +1 -1
  5. package/dist/cjs/langfuse.cjs +62 -11
  6. package/dist/cjs/langfuse.cjs.map +1 -1
  7. package/dist/cjs/run.cjs +33 -19
  8. package/dist/cjs/run.cjs.map +1 -1
  9. package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs +1 -0
  10. package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs.map +1 -1
  11. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs +13 -7
  12. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs.map +1 -1
  13. package/dist/cjs/utils/callbacks.cjs +27 -0
  14. package/dist/cjs/utils/callbacks.cjs.map +1 -0
  15. package/dist/esm/graphs/Graph.mjs +6 -4
  16. package/dist/esm/graphs/Graph.mjs.map +1 -1
  17. package/dist/esm/instrumentation.mjs +2 -7
  18. package/dist/esm/instrumentation.mjs.map +1 -1
  19. package/dist/esm/langfuse.mjs +63 -14
  20. package/dist/esm/langfuse.mjs.map +1 -1
  21. package/dist/esm/run.mjs +34 -20
  22. package/dist/esm/run.mjs.map +1 -1
  23. package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs +1 -0
  24. package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs.map +1 -1
  25. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs +13 -7
  26. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs.map +1 -1
  27. package/dist/esm/utils/callbacks.mjs +24 -0
  28. package/dist/esm/utils/callbacks.mjs.map +1 -0
  29. package/dist/types/langfuse.d.ts +13 -4
  30. package/dist/types/types/run.d.ts +2 -2
  31. package/dist/types/types/tools.d.ts +6 -0
  32. package/dist/types/utils/callbacks.d.ts +5 -0
  33. package/package.json +4 -4
  34. package/src/graphs/Graph.ts +10 -6
  35. package/src/instrumentation.ts +2 -7
  36. package/src/langfuse.ts +98 -15
  37. package/src/run.ts +53 -29
  38. package/src/specs/langfuse-callbacks.test.ts +75 -0
  39. package/src/specs/langfuse-config.test.ts +58 -1
  40. package/src/tools/__tests__/CloudflareSandboxExecution.test.ts +87 -8
  41. package/src/tools/cloudflare/CloudflareBridgeRuntime.ts +1 -0
  42. package/src/tools/cloudflare/CloudflareSandboxExecutionEngine.ts +13 -7
  43. package/src/types/run.ts +2 -7
  44. package/src/types/tools.ts +6 -0
  45. package/src/utils/callbacks.ts +39 -0
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: {
@@ -107,26 +107,105 @@ describe('Cloudflare sandbox execution backend', () => {
107
107
  expect(listPaths).toEqual(['/workspace']);
108
108
  });
109
109
 
110
- it('aborts remote exec when the local timeout kills the spawn wrapper', async () => {
111
- let signal: AbortSignal | undefined;
110
+ it('does not pass AbortSignal to Cloudflare spawn exec options', async () => {
111
+ let resolveExecCalled!: () => void;
112
+ const execCalled = new Promise<void>((resolve) => {
113
+ resolveExecCalled = resolve;
114
+ });
115
+ let receivedOptions: t.CloudflareSandboxExecOptions | undefined;
116
+ const sandbox = createRuntime({
117
+ exec: (_command, options) => {
118
+ receivedOptions = options;
119
+ resolveExecCalled();
120
+ return new Promise<t.CloudflareSandboxExecResult>(() => undefined);
121
+ },
122
+ });
123
+ const config = createCloudflareLocalExecutionConfig({
124
+ sandbox,
125
+ timeoutMs: 50,
126
+ workspaceRoot: '/workspace',
127
+ });
128
+
129
+ const resultPromise = spawnLocalProcess(
130
+ 'bash',
131
+ ['-lc', 'sleep 10'],
132
+ config
133
+ );
134
+ await execCalled;
135
+ const result = await resultPromise;
136
+
137
+ expect(receivedOptions).not.toHaveProperty('signal');
138
+ expect(result.timedOut).toBe(true);
139
+ expect(result.exitCode).toBe(143);
140
+ });
141
+
142
+ it('passes AbortSignal to signal-aware runtimes and aborts it on kill', async () => {
143
+ let resolveExecCalled!: () => void;
144
+ const execCalled = new Promise<void>((resolve) => {
145
+ resolveExecCalled = resolve;
146
+ });
147
+ let receivedSignal: AbortSignal | undefined;
148
+ let abortEvents = 0;
112
149
  const sandbox = createRuntime({
113
- exec: (_command, options) =>
114
- new Promise<t.CloudflareSandboxExecResult>((_resolve, reject) => {
115
- signal = options?.signal;
116
- signal?.addEventListener('abort', () => reject(new Error('aborted')));
117
- }),
150
+ supportsExecSignal: true,
151
+ exec: (_command, options) => {
152
+ receivedSignal = options?.signal;
153
+ receivedSignal?.addEventListener('abort', () => {
154
+ abortEvents += 1;
155
+ });
156
+ resolveExecCalled();
157
+ return new Promise<t.CloudflareSandboxExecResult>(() => undefined);
158
+ },
118
159
  });
119
160
  const config = createCloudflareLocalExecutionConfig({
120
161
  sandbox,
162
+ timeoutMs: 50,
163
+ workspaceRoot: '/workspace',
164
+ });
165
+
166
+ const resultPromise = spawnLocalProcess(
167
+ 'bash',
168
+ ['-lc', 'sleep 10'],
169
+ config
170
+ );
171
+ await execCalled;
172
+ const result = await resultPromise;
173
+
174
+ expect(receivedSignal).toBeDefined();
175
+ expect(receivedSignal?.aborted).toBe(true);
176
+ expect(abortEvents).toBe(1);
177
+ expect(result.timedOut).toBe(true);
178
+ expect(result.exitCode).toBe(143);
179
+ });
180
+
181
+ it('does not start remote exec when killed before async sandbox resolution finishes', async () => {
182
+ let execCalls = 0;
183
+ let resolveSandbox!: (runtime: t.CloudflareSandboxRuntime) => void;
184
+ const sandboxPromise = new Promise<t.CloudflareSandboxRuntime>(
185
+ (resolve) => {
186
+ resolveSandbox = resolve;
187
+ }
188
+ );
189
+ const config = createCloudflareLocalExecutionConfig({
190
+ sandbox: () => sandboxPromise,
121
191
  timeoutMs: 10,
122
192
  workspaceRoot: '/workspace',
123
193
  });
124
194
 
125
195
  const result = await spawnLocalProcess('bash', ['-lc', 'sleep 10'], config);
196
+ resolveSandbox(
197
+ createRuntime({
198
+ exec: async () => {
199
+ execCalls += 1;
200
+ return { exitCode: 0, stdout: '', stderr: '' };
201
+ },
202
+ })
203
+ );
204
+ await new Promise((resolve) => setTimeout(resolve, 0));
126
205
 
127
- expect(signal?.aborted).toBe(true);
128
206
  expect(result.timedOut).toBe(true);
129
207
  expect(result.exitCode).toBe(143);
208
+ expect(execCalls).toBe(0);
130
209
  });
131
210
 
132
211
  it('memoizes sandbox factory results per config object', async () => {
@@ -469,6 +469,7 @@ export function createCloudflareBridgeRuntime(
469
469
  }
470
470
 
471
471
  return {
472
+ supportsExecSignal: true,
472
473
  getSandboxId,
473
474
  exec,
474
475
  readFile,
@@ -400,8 +400,8 @@ function createCloudflareSpawn(
400
400
  return (command, args, options) => {
401
401
  const stdout = new PassThrough();
402
402
  const stderr = new PassThrough();
403
- const abortController = new AbortController();
404
403
  const child = new EventEmitter() as ChildProcessWithoutNullStreams;
404
+ const abortController = new AbortController();
405
405
  const state = { closed: false };
406
406
  const closeOnce = (
407
407
  exitCode: number | null,
@@ -451,13 +451,19 @@ function createCloudflareSpawn(
451
451
  const timedCommand = withInSandboxTimeout(rendered, timeoutMs);
452
452
  const cwd =
453
453
  options.cwd == null ? ctx.workspaceRoot : options.cwd.toString();
454
+ if (state.closed) {
455
+ return;
456
+ }
457
+ const execOptions: t.CloudflareSandboxExecOptions = {
458
+ cwd,
459
+ env: ctx.env,
460
+ timeout: outerTimeoutMs(timeoutMs),
461
+ };
462
+ if (ctx.sandbox.supportsExecSignal === true) {
463
+ execOptions.signal = abortController.signal;
464
+ }
454
465
  try {
455
- const result = await ctx.sandbox.exec(timedCommand, {
456
- cwd,
457
- env: ctx.env,
458
- timeout: outerTimeoutMs(timeoutMs),
459
- signal: abortController.signal,
460
- });
466
+ const result = await ctx.sandbox.exec(timedCommand, execOptions);
461
467
  if (state.closed) {
462
468
  return;
463
469
  }
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
 
@@ -853,6 +853,12 @@ export type CloudflareSandboxListFilesResult =
853
853
  };
854
854
 
855
855
  export interface CloudflareSandboxRuntime {
856
+ /**
857
+ * True when this runtime can consume AbortSignal values in exec options.
858
+ * Native Cloudflare Sandbox Durable Object RPC cannot clone AbortSignal,
859
+ * but HTTP bridge runtimes can use it to abort the underlying fetch.
860
+ */
861
+ supportsExecSignal?: boolean;
856
862
  exec(
857
863
  command: string,
858
864
  options?: CloudflareSandboxExecOptions
@@ -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
+ }