@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.
- package/dist/cjs/graphs/Graph.cjs +5 -3
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/instrumentation.cjs +2 -7
- package/dist/cjs/instrumentation.cjs.map +1 -1
- package/dist/cjs/langfuse.cjs +62 -11
- package/dist/cjs/langfuse.cjs.map +1 -1
- package/dist/cjs/run.cjs +33 -19
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs +1 -0
- package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs.map +1 -1
- package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs +13 -7
- package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs.map +1 -1
- package/dist/cjs/utils/callbacks.cjs +27 -0
- package/dist/cjs/utils/callbacks.cjs.map +1 -0
- package/dist/esm/graphs/Graph.mjs +6 -4
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/instrumentation.mjs +2 -7
- package/dist/esm/instrumentation.mjs.map +1 -1
- package/dist/esm/langfuse.mjs +63 -14
- package/dist/esm/langfuse.mjs.map +1 -1
- package/dist/esm/run.mjs +34 -20
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs +1 -0
- package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs.map +1 -1
- package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs +13 -7
- package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs.map +1 -1
- package/dist/esm/utils/callbacks.mjs +24 -0
- package/dist/esm/utils/callbacks.mjs.map +1 -0
- package/dist/types/langfuse.d.ts +13 -4
- package/dist/types/types/run.d.ts +2 -2
- package/dist/types/types/tools.d.ts +6 -0
- package/dist/types/utils/callbacks.d.ts +5 -0
- package/package.json +4 -4
- package/src/graphs/Graph.ts +10 -6
- package/src/instrumentation.ts +2 -7
- package/src/langfuse.ts +98 -15
- package/src/run.ts +53 -29
- package/src/specs/langfuse-callbacks.test.ts +75 -0
- package/src/specs/langfuse-config.test.ts +58 -1
- package/src/tools/__tests__/CloudflareSandboxExecution.test.ts +87 -8
- package/src/tools/cloudflare/CloudflareBridgeRuntime.ts +1 -0
- package/src/tools/cloudflare/CloudflareSandboxExecutionEngine.ts +13 -7
- package/src/types/run.ts +2 -7
- package/src/types/tools.ts +6 -0
- 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 =
|
|
612
|
-
.
|
|
613
|
-
|
|
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 =
|
|
620
|
-
|
|
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.
|
|
635
|
-
|
|
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:
|
|
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 =
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
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
|
-
|
|
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
|
|
1252
|
-
|
|
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:
|
|
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('
|
|
111
|
-
let
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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 () => {
|
|
@@ -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
|
|
package/src/types/tools.ts
CHANGED
|
@@ -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
|
+
}
|