@librechat/agents 3.1.92 → 3.1.94
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/tools/ProgrammaticToolCalling.cjs +85 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.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/esm/tools/ProgrammaticToolCalling.mjs +85 -1
- package/dist/esm/tools/ProgrammaticToolCalling.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/types/types/tools.d.ts +8 -2
- package/package.json +1 -1
- package/src/tools/ProgrammaticToolCalling.ts +117 -1
- package/src/tools/__tests__/CloudflareSandboxExecution.test.ts +87 -8
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +129 -0
- package/src/tools/cloudflare/CloudflareBridgeRuntime.ts +1 -0
- package/src/tools/cloudflare/CloudflareSandboxExecutionEngine.ts +13 -7
- package/src/types/tools.ts +8 -2
|
@@ -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 () => {
|
|
@@ -86,6 +86,135 @@ describe('ProgrammaticToolCalling', () => {
|
|
|
86
86
|
});
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
+
it('parses JSON-string inputs before invoking structured tools', async () => {
|
|
90
|
+
const toolCalls: t.PTCToolCall[] = [
|
|
91
|
+
{
|
|
92
|
+
id: 'call_001',
|
|
93
|
+
name: 'get_weather',
|
|
94
|
+
input: '{"city":"San Francisco"}',
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
const results = await executeTools(toolCalls, toolMap);
|
|
99
|
+
|
|
100
|
+
expect(results).toHaveLength(1);
|
|
101
|
+
expect(results[0].call_id).toBe('call_001');
|
|
102
|
+
expect(results[0].is_error).toBe(false);
|
|
103
|
+
expect(results[0].result).toEqual({
|
|
104
|
+
temperature: 65,
|
|
105
|
+
condition: 'Foggy',
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('preserves JSON-looking strings for string-input tools', async () => {
|
|
110
|
+
const invoke = jest.fn<
|
|
111
|
+
(_input: unknown, _config: unknown) => Promise<unknown>
|
|
112
|
+
>(async (input) => input);
|
|
113
|
+
const customTool = {
|
|
114
|
+
name: 'string_tool',
|
|
115
|
+
schema: { type: 'string' },
|
|
116
|
+
invoke,
|
|
117
|
+
} as unknown as t.GenericTool;
|
|
118
|
+
const customToolMap: t.ToolMap = new Map([['string_tool', customTool]]);
|
|
119
|
+
const toolCalls: t.PTCToolCall[] = [
|
|
120
|
+
{
|
|
121
|
+
id: 'call_001',
|
|
122
|
+
name: 'string_tool',
|
|
123
|
+
input: '{"raw":true}',
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
const results = await executeTools(toolCalls, customToolMap);
|
|
128
|
+
|
|
129
|
+
expect(results[0].is_error).toBe(false);
|
|
130
|
+
expect(results[0].result).toBe('{"raw":true}');
|
|
131
|
+
expect(invoke).toHaveBeenCalledWith('{"raw":true}', {
|
|
132
|
+
metadata: { [Constants.PROGRAMMATIC_TOOL_CALLING]: true },
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('stringifies object inputs before invoking string-input tools', async () => {
|
|
137
|
+
const invoke = jest.fn<
|
|
138
|
+
(_input: unknown, _config: unknown) => Promise<unknown>
|
|
139
|
+
>(async (input) => input);
|
|
140
|
+
const customTool = {
|
|
141
|
+
name: 'string_tool',
|
|
142
|
+
schema: { type: 'string' },
|
|
143
|
+
invoke,
|
|
144
|
+
} as unknown as t.GenericTool;
|
|
145
|
+
const customToolMap: t.ToolMap = new Map([['string_tool', customTool]]);
|
|
146
|
+
const toolCalls: t.PTCToolCall[] = [
|
|
147
|
+
{
|
|
148
|
+
id: 'call_001',
|
|
149
|
+
name: 'string_tool',
|
|
150
|
+
input: { raw: true },
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
const results = await executeTools(toolCalls, customToolMap);
|
|
155
|
+
|
|
156
|
+
expect(results[0].is_error).toBe(false);
|
|
157
|
+
expect(results[0].result).toBe('{"raw":true}');
|
|
158
|
+
expect(invoke).toHaveBeenCalledWith('{"raw":true}', {
|
|
159
|
+
metadata: { [Constants.PROGRAMMATIC_TOOL_CALLING]: true },
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('preserves object inputs for mixed object-or-string schemas', async () => {
|
|
164
|
+
const invoke = jest.fn<
|
|
165
|
+
(_input: unknown, _config: unknown) => Promise<unknown>
|
|
166
|
+
>(async (input) => input);
|
|
167
|
+
const customTool = {
|
|
168
|
+
name: 'mixed_tool',
|
|
169
|
+
schema: { type: ['object', 'string'] },
|
|
170
|
+
invoke,
|
|
171
|
+
} as unknown as t.GenericTool;
|
|
172
|
+
const customToolMap: t.ToolMap = new Map([['mixed_tool', customTool]]);
|
|
173
|
+
const input = { raw: true };
|
|
174
|
+
const toolCalls: t.PTCToolCall[] = [
|
|
175
|
+
{
|
|
176
|
+
id: 'call_001',
|
|
177
|
+
name: 'mixed_tool',
|
|
178
|
+
input,
|
|
179
|
+
},
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
const results = await executeTools(toolCalls, customToolMap);
|
|
183
|
+
|
|
184
|
+
expect(results[0].is_error).toBe(false);
|
|
185
|
+
expect(results[0].result).toBe(input);
|
|
186
|
+
expect(invoke).toHaveBeenCalledWith(input, {
|
|
187
|
+
metadata: { [Constants.PROGRAMMATIC_TOOL_CALLING]: true },
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('preserves JSON-looking strings for mixed object-or-string schemas', async () => {
|
|
192
|
+
const invoke = jest.fn<
|
|
193
|
+
(_input: unknown, _config: unknown) => Promise<unknown>
|
|
194
|
+
>(async (input) => input);
|
|
195
|
+
const customTool = {
|
|
196
|
+
name: 'mixed_tool',
|
|
197
|
+
schema: { type: ['object', 'string'] },
|
|
198
|
+
invoke,
|
|
199
|
+
} as unknown as t.GenericTool;
|
|
200
|
+
const customToolMap: t.ToolMap = new Map([['mixed_tool', customTool]]);
|
|
201
|
+
const toolCalls: t.PTCToolCall[] = [
|
|
202
|
+
{
|
|
203
|
+
id: 'call_001',
|
|
204
|
+
name: 'mixed_tool',
|
|
205
|
+
input: '{"raw":true}',
|
|
206
|
+
},
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
const results = await executeTools(toolCalls, customToolMap);
|
|
210
|
+
|
|
211
|
+
expect(results[0].is_error).toBe(false);
|
|
212
|
+
expect(results[0].result).toBe('{"raw":true}');
|
|
213
|
+
expect(invoke).toHaveBeenCalledWith('{"raw":true}', {
|
|
214
|
+
metadata: { [Constants.PROGRAMMATIC_TOOL_CALLING]: true },
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
89
218
|
it('marks bash PTC inner tool invocations with bash metadata', async () => {
|
|
90
219
|
const invoke = jest.fn<
|
|
91
220
|
(_input: unknown, _config: unknown) => Promise<{ ok: boolean }>
|
|
@@ -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/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
|
|
@@ -1019,9 +1025,9 @@ export type PTCToolCall = {
|
|
|
1019
1025
|
id: string;
|
|
1020
1026
|
/** Tool name */
|
|
1021
1027
|
name: string;
|
|
1022
|
-
/** Input parameters */
|
|
1028
|
+
/** Input parameters. Some bridges may serialize object input as JSON text. */
|
|
1023
1029
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1024
|
-
input: Record<string, any
|
|
1030
|
+
input: Record<string, any> | string;
|
|
1025
1031
|
};
|
|
1026
1032
|
|
|
1027
1033
|
/**
|