@librechat/agents 3.2.39 → 3.2.42
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/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +4 -1
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +4 -3
- package/dist/cjs/hooks/createWorkspacePolicyHook.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +9 -1
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +4 -0
- package/dist/cjs/messages/cache.cjs +21 -0
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/tools/ReadFile.cjs +2 -2
- package/dist/cjs/tools/ReadFile.cjs.map +1 -1
- package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs +13 -13
- package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs +87 -7
- package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalCodingTools.cjs +11 -11
- package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +5 -2
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/hooks/createWorkspacePolicyHook.mjs +4 -3
- package/dist/esm/hooks/createWorkspacePolicyHook.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +10 -2
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/main.mjs +3 -3
- package/dist/esm/messages/cache.mjs +21 -1
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/tools/ReadFile.mjs +2 -2
- package/dist/esm/tools/ReadFile.mjs.map +1 -1
- package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs +14 -14
- package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs +85 -8
- package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs.map +1 -1
- package/dist/esm/tools/local/LocalCodingTools.mjs +11 -11
- package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -1
- package/dist/types/llm/bedrock/index.d.ts +7 -0
- package/dist/types/messages/cache.d.ts +17 -0
- package/dist/types/tools/ReadFile.d.ts +4 -4
- package/dist/types/tools/cloudflare/CloudflareSandboxExecutionEngine.d.ts +44 -0
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +2 -0
- package/src/graphs/Graph.ts +17 -5
- package/src/hooks/__tests__/createWorkspacePolicyHook.test.ts +12 -12
- package/src/hooks/createWorkspacePolicyHook.ts +7 -6
- package/src/llm/bedrock/index.ts +18 -2
- package/src/llm/bedrock/llm.spec.ts +97 -0
- package/src/messages/cache.test.ts +31 -0
- package/src/messages/cache.ts +25 -0
- package/src/tools/ReadFile.ts +2 -2
- package/src/tools/__tests__/CloudflareSandboxExecution.test.ts +131 -0
- package/src/tools/__tests__/LocalExecutionTools.test.ts +25 -25
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +5 -5
- package/src/tools/__tests__/ReadFile.test.ts +3 -3
- package/src/tools/__tests__/ToolNode.session.test.ts +2 -2
- package/src/tools/__tests__/workspaceSeam.test.ts +2 -2
- package/src/tools/cloudflare/CloudflareProgrammaticToolCalling.ts +18 -13
- package/src/tools/cloudflare/CloudflareSandboxExecutionEngine.ts +165 -9
- package/src/tools/local/LocalCodingTools.ts +14 -14
|
@@ -131,6 +131,135 @@ function isInSandboxTimeoutExit(exitCode: number | null): boolean {
|
|
|
131
131
|
return exitCode === 124 || exitCode === 137;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Client-side backstop timeout for a `sandbox.exec()` await: a few seconds beyond
|
|
136
|
+
* the exec's own `timeout` option, so a stalled exec that never honors `timeout`
|
|
137
|
+
* still can't outlast this.
|
|
138
|
+
*/
|
|
139
|
+
export function clientExecTimeoutMs(timeoutMs: number): number {
|
|
140
|
+
return outerTimeoutMs(timeoutMs) + 5000;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Bound a `sandbox.exec()` await with a CLIENT-SIDE timeout.
|
|
145
|
+
*
|
|
146
|
+
* The native Cloudflare Sandbox Durable Object `exec()` is effectively
|
|
147
|
+
* uncancellable from the host: `ExecOptions` has no `signal` (so
|
|
148
|
+
* `supportsExecSignal` is false for the native transport), and its `timeout`
|
|
149
|
+
* option is not reliably enforced when the container/RPC itself stalls — while
|
|
150
|
+
* the in-sandbox `timeout(1)` wrapper only bounds a command that is actually
|
|
151
|
+
* running. So a stalled exec (an unresponsive/cold container) otherwise hangs
|
|
152
|
+
* until the host's run-level abort, burning the whole run budget on one tool
|
|
153
|
+
* call. This race guarantees the host await settles within `timeoutMs`
|
|
154
|
+
* regardless of the transport.
|
|
155
|
+
*
|
|
156
|
+
* On timeout the underlying `exec` promise may keep running in the DO (a
|
|
157
|
+
* native-DO exec cannot be truly cancelled), so its late settlement is swallowed
|
|
158
|
+
* to avoid an unhandled rejection.
|
|
159
|
+
*/
|
|
160
|
+
export async function withClientTimeout<T>(
|
|
161
|
+
exec: Promise<T>,
|
|
162
|
+
timeoutMs: number,
|
|
163
|
+
label: string,
|
|
164
|
+
options: {
|
|
165
|
+
/**
|
|
166
|
+
* Detach the backstop timer from the event loop. Use ONLY when something else
|
|
167
|
+
* already settles the caller (e.g. the spawn path, where spawnLocalProcess's
|
|
168
|
+
* own timer resolves the child). The awaited direct-exec paths must leave it
|
|
169
|
+
* REF'd so the timeout is guaranteed to fire even if nothing else is pending.
|
|
170
|
+
*/
|
|
171
|
+
unref?: boolean;
|
|
172
|
+
/** Invoked when the client timeout fires — e.g. abort a signal-aware exec. */
|
|
173
|
+
onTimeout?: () => void;
|
|
174
|
+
} = {}
|
|
175
|
+
): Promise<T> {
|
|
176
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
177
|
+
return exec;
|
|
178
|
+
}
|
|
179
|
+
// Swallow a late rejection from the losing promise after the race settles.
|
|
180
|
+
exec.catch(() => undefined);
|
|
181
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
182
|
+
try {
|
|
183
|
+
return await Promise.race([
|
|
184
|
+
exec,
|
|
185
|
+
new Promise<never>((_resolve, reject) => {
|
|
186
|
+
timer = setTimeout(() => {
|
|
187
|
+
// Reject FIRST so this client-timeout message reliably wins the race;
|
|
188
|
+
// only then abort a signal-aware exec, whose resulting AbortError must
|
|
189
|
+
// not surface to the caller instead of the timeout.
|
|
190
|
+
reject(
|
|
191
|
+
new Error(
|
|
192
|
+
`${label} exceeded ${timeoutMs}ms client-side timeout (sandbox exec did not return)`
|
|
193
|
+
)
|
|
194
|
+
);
|
|
195
|
+
options.onTimeout?.();
|
|
196
|
+
}, timeoutMs);
|
|
197
|
+
if (options.unref === true) {
|
|
198
|
+
(timer as { unref?: () => void } | undefined)?.unref?.();
|
|
199
|
+
}
|
|
200
|
+
}),
|
|
201
|
+
]);
|
|
202
|
+
} finally {
|
|
203
|
+
if (timer !== undefined) {
|
|
204
|
+
clearTimeout(timer);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Run `sandbox.exec()` bounded by a client-side timeout, and — for signal-aware
|
|
211
|
+
* transports (e.g. the HTTP bridge, `supportsExecSignal === true`) — abort the
|
|
212
|
+
* underlying exec when the timeout fires instead of merely abandoning it. The
|
|
213
|
+
* native DO transport ignores `signal`, so it only gets the timeout. Leaves the
|
|
214
|
+
* backstop timer ref'd (this is an awaited direct-exec path).
|
|
215
|
+
*/
|
|
216
|
+
export async function execWithClientTimeout(
|
|
217
|
+
sandbox: t.CloudflareSandboxRuntime,
|
|
218
|
+
command: string,
|
|
219
|
+
options: t.CloudflareSandboxExecOptions,
|
|
220
|
+
timeoutMs: number,
|
|
221
|
+
label: string,
|
|
222
|
+
runOptions: { unref?: boolean } = {}
|
|
223
|
+
): Promise<t.CloudflareSandboxExecResult> {
|
|
224
|
+
const controller = new AbortController();
|
|
225
|
+
const execOptions: t.CloudflareSandboxExecOptions = { ...options };
|
|
226
|
+
const callerSignal = options.signal;
|
|
227
|
+
let onCallerAbort: (() => void) | undefined;
|
|
228
|
+
if (sandbox.supportsExecSignal === true) {
|
|
229
|
+
// Compose the caller's signal (e.g. run/user cancellation) with our timeout
|
|
230
|
+
// controller so EITHER source cancels the exec — don't clobber the caller's.
|
|
231
|
+
if (callerSignal != null) {
|
|
232
|
+
if (callerSignal.aborted) {
|
|
233
|
+
controller.abort();
|
|
234
|
+
} else {
|
|
235
|
+
onCallerAbort = (): void => controller.abort();
|
|
236
|
+
callerSignal.addEventListener('abort', onCallerAbort, { once: true });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
execOptions.signal = controller.signal;
|
|
240
|
+
} else if ('signal' in execOptions) {
|
|
241
|
+
// Native DO RPC cannot consume an AbortSignal (and would fail to clone it).
|
|
242
|
+
// Strip any caller-provided one so the spread above can't reintroduce it.
|
|
243
|
+
delete execOptions.signal;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
return await withClientTimeout(
|
|
247
|
+
sandbox.exec(command, execOptions),
|
|
248
|
+
timeoutMs,
|
|
249
|
+
label,
|
|
250
|
+
{
|
|
251
|
+
unref: runOptions.unref,
|
|
252
|
+
onTimeout: () => controller.abort(),
|
|
253
|
+
}
|
|
254
|
+
);
|
|
255
|
+
} finally {
|
|
256
|
+
// Don't leave a listener attached to a long-lived/shared caller signal.
|
|
257
|
+
if (onCallerAbort != null && callerSignal != null) {
|
|
258
|
+
callerSignal.removeEventListener('abort', onCallerAbort);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
134
263
|
function truncateOutput(value: string, maxChars: number): string {
|
|
135
264
|
if (maxChars <= 0 || value.length <= maxChars) {
|
|
136
265
|
return value;
|
|
@@ -466,7 +595,14 @@ function createCloudflareSpawn(
|
|
|
466
595
|
execOptions.signal = abortController.signal;
|
|
467
596
|
}
|
|
468
597
|
try {
|
|
469
|
-
const result = await
|
|
598
|
+
const result = await withClientTimeout(
|
|
599
|
+
ctx.sandbox.exec(timedCommand, execOptions),
|
|
600
|
+
clientExecTimeoutMs(timeoutMs),
|
|
601
|
+
'cloudflare sandbox exec',
|
|
602
|
+
// spawnLocalProcess's own timer already resolves the child, so this
|
|
603
|
+
// backstop may safely detach; abort the (signal-aware) exec on timeout.
|
|
604
|
+
{ unref: true, onTimeout: () => abortController.abort() }
|
|
605
|
+
);
|
|
470
606
|
if (isClosed()) {
|
|
471
607
|
return;
|
|
472
608
|
}
|
|
@@ -551,13 +687,16 @@ export async function executeCloudflareBash(
|
|
|
551
687
|
args.length > 0
|
|
552
688
|
? `${ctx.shell} -lc ${quote(command)} -- ${args.map(quote).join(' ')}`
|
|
553
689
|
: `${ctx.shell} -lc ${quote(command)}`;
|
|
554
|
-
const result = await
|
|
690
|
+
const result = await execWithClientTimeout(
|
|
691
|
+
ctx.sandbox,
|
|
555
692
|
withInSandboxTimeout(shellCommand, ctx.timeoutMs),
|
|
556
693
|
{
|
|
557
694
|
cwd: ctx.workspaceRoot,
|
|
558
695
|
env: ctx.env,
|
|
559
696
|
timeout: outerTimeoutMs(ctx.timeoutMs),
|
|
560
|
-
}
|
|
697
|
+
},
|
|
698
|
+
clientExecTimeoutMs(ctx.timeoutMs),
|
|
699
|
+
'cloudflare sandbox bash exec'
|
|
561
700
|
);
|
|
562
701
|
return {
|
|
563
702
|
stdout: truncateOutput(result.stdout, ctx.maxOutputChars),
|
|
@@ -703,15 +842,20 @@ export async function executeCloudflareCode(
|
|
|
703
842
|
}
|
|
704
843
|
);
|
|
705
844
|
}
|
|
845
|
+
let execSucceeded = false;
|
|
706
846
|
try {
|
|
707
|
-
const result = await
|
|
847
|
+
const result = await execWithClientTimeout(
|
|
848
|
+
ctx.sandbox,
|
|
708
849
|
withInSandboxTimeout(runtime.command, ctx.timeoutMs),
|
|
709
850
|
{
|
|
710
851
|
cwd: ctx.workspaceRoot,
|
|
711
852
|
env: ctx.env,
|
|
712
853
|
timeout: outerTimeoutMs(ctx.timeoutMs),
|
|
713
|
-
}
|
|
854
|
+
},
|
|
855
|
+
clientExecTimeoutMs(ctx.timeoutMs),
|
|
856
|
+
'cloudflare sandbox code-exec'
|
|
714
857
|
);
|
|
858
|
+
execSucceeded = true;
|
|
715
859
|
return {
|
|
716
860
|
stdout: truncateOutput(result.stdout, ctx.maxOutputChars),
|
|
717
861
|
stderr: truncateOutput(result.stderr, ctx.maxOutputChars),
|
|
@@ -719,13 +863,25 @@ export async function executeCloudflareCode(
|
|
|
719
863
|
timedOut: isInSandboxTimeoutExit(result.exitCode),
|
|
720
864
|
};
|
|
721
865
|
} finally {
|
|
722
|
-
|
|
723
|
-
|
|
866
|
+
// After a normal run, AWAIT cleanup so the temp dir is gone before returning.
|
|
867
|
+
// After a stalled/failed run, detach it (unref'd) so we don't pile a second
|
|
868
|
+
// client timeout onto the caller's latency; cleanup still runs best-effort.
|
|
869
|
+
const detach = !execSucceeded;
|
|
870
|
+
const cleanup = execWithClientTimeout(
|
|
871
|
+
ctx.sandbox,
|
|
872
|
+
`rm -rf ${quote(tempDir)}`,
|
|
873
|
+
{
|
|
724
874
|
cwd: ctx.workspaceRoot,
|
|
725
875
|
env: ctx.env,
|
|
726
876
|
timeout: 10000,
|
|
727
|
-
}
|
|
728
|
-
|
|
877
|
+
},
|
|
878
|
+
clientExecTimeoutMs(10000),
|
|
879
|
+
'cloudflare sandbox cleanup',
|
|
880
|
+
{ unref: detach }
|
|
881
|
+
).catch(() => undefined);
|
|
882
|
+
if (!detach) {
|
|
883
|
+
await cleanup;
|
|
884
|
+
}
|
|
729
885
|
}
|
|
730
886
|
}
|
|
731
887
|
|
|
@@ -49,7 +49,7 @@ export const LocalListDirectoryToolName = Constants.LIST_DIRECTORY;
|
|
|
49
49
|
export const LocalReadFileToolSchema: t.JsonSchemaType = {
|
|
50
50
|
type: 'object',
|
|
51
51
|
properties: {
|
|
52
|
-
|
|
52
|
+
path: {
|
|
53
53
|
type: 'string',
|
|
54
54
|
description:
|
|
55
55
|
'Path to a local file, relative to the configured cwd unless absolute paths are allowed.',
|
|
@@ -63,13 +63,13 @@ export const LocalReadFileToolSchema: t.JsonSchemaType = {
|
|
|
63
63
|
description: 'Optional maximum number of lines to return.',
|
|
64
64
|
},
|
|
65
65
|
},
|
|
66
|
-
required: ['
|
|
66
|
+
required: ['path'],
|
|
67
67
|
};
|
|
68
68
|
|
|
69
69
|
export const LocalWriteFileToolSchema: t.JsonSchemaType = {
|
|
70
70
|
type: 'object',
|
|
71
71
|
properties: {
|
|
72
|
-
|
|
72
|
+
path: {
|
|
73
73
|
type: 'string',
|
|
74
74
|
description:
|
|
75
75
|
'Path to write, relative to the configured cwd unless absolute paths are allowed.',
|
|
@@ -79,13 +79,13 @@ export const LocalWriteFileToolSchema: t.JsonSchemaType = {
|
|
|
79
79
|
description: 'Complete file contents to write.',
|
|
80
80
|
},
|
|
81
81
|
},
|
|
82
|
-
required: ['
|
|
82
|
+
required: ['path', 'content'],
|
|
83
83
|
};
|
|
84
84
|
|
|
85
85
|
export const LocalEditFileToolSchema: t.JsonSchemaType = {
|
|
86
86
|
type: 'object',
|
|
87
87
|
properties: {
|
|
88
|
-
|
|
88
|
+
path: {
|
|
89
89
|
type: 'string',
|
|
90
90
|
description:
|
|
91
91
|
'Path to edit, relative to the configured cwd unless absolute paths are allowed.',
|
|
@@ -112,7 +112,7 @@ export const LocalEditFileToolSchema: t.JsonSchemaType = {
|
|
|
112
112
|
},
|
|
113
113
|
},
|
|
114
114
|
},
|
|
115
|
-
required: ['
|
|
115
|
+
required: ['path'],
|
|
116
116
|
};
|
|
117
117
|
|
|
118
118
|
export const LocalGrepSearchToolSchema: t.JsonSchemaType = {
|
|
@@ -391,18 +391,18 @@ export function createLocalReadFileTool(
|
|
|
391
391
|
return tool(
|
|
392
392
|
async (rawInput) => {
|
|
393
393
|
const input = rawInput as {
|
|
394
|
-
|
|
394
|
+
path: string;
|
|
395
395
|
offset?: number;
|
|
396
396
|
limit?: number;
|
|
397
397
|
};
|
|
398
398
|
const path = await resolveWorkspacePathSafe(
|
|
399
|
-
input.
|
|
399
|
+
input.path,
|
|
400
400
|
config,
|
|
401
401
|
'read'
|
|
402
402
|
);
|
|
403
403
|
const fileStat = await fs.stat(path);
|
|
404
404
|
if (!fileStat.isFile()) {
|
|
405
|
-
throw new Error(`Path is not a file: ${input.
|
|
405
|
+
throw new Error(`Path is not a file: ${input.path}`);
|
|
406
406
|
}
|
|
407
407
|
const maxBytes = Math.max(
|
|
408
408
|
config.maxReadBytes ?? DEFAULT_MAX_READ_BYTES,
|
|
@@ -514,12 +514,12 @@ export function createLocalWriteFileTool(
|
|
|
514
514
|
const fs = getWorkspaceFS(config);
|
|
515
515
|
return tool(
|
|
516
516
|
async (rawInput) => {
|
|
517
|
-
const input = rawInput as {
|
|
517
|
+
const input = rawInput as { path: string; content: string };
|
|
518
518
|
if (config.readOnly === true) {
|
|
519
519
|
throw new Error('write_file is blocked in read-only local mode.');
|
|
520
520
|
}
|
|
521
521
|
const path = await resolveWorkspacePathSafe(
|
|
522
|
-
input.
|
|
522
|
+
input.path,
|
|
523
523
|
config,
|
|
524
524
|
'write'
|
|
525
525
|
);
|
|
@@ -598,7 +598,7 @@ export function createLocalEditFileTool(
|
|
|
598
598
|
return tool(
|
|
599
599
|
async (rawInput) => {
|
|
600
600
|
const input = rawInput as {
|
|
601
|
-
|
|
601
|
+
path: string;
|
|
602
602
|
old_text?: string;
|
|
603
603
|
new_text?: string;
|
|
604
604
|
edits?: Array<{ old_text?: string; new_text?: string }>;
|
|
@@ -612,7 +612,7 @@ export function createLocalEditFileTool(
|
|
|
612
612
|
}
|
|
613
613
|
|
|
614
614
|
const path = await resolveWorkspacePathSafe(
|
|
615
|
-
input.
|
|
615
|
+
input.path,
|
|
616
616
|
config,
|
|
617
617
|
'write'
|
|
618
618
|
);
|
|
@@ -627,7 +627,7 @@ export function createLocalEditFileTool(
|
|
|
627
627
|
const match = locateEdit(next, edit.oldText);
|
|
628
628
|
if (match == null) {
|
|
629
629
|
throw new Error(
|
|
630
|
-
`Edit ${i + 1}/${edits.length}: could not locate old_text in ${input.
|
|
630
|
+
`Edit ${i + 1}/${edits.length}: could not locate old_text in ${input.path}. ` +
|
|
631
631
|
'Tried exact, line-trimmed, whitespace-normalized, and indentation-flexible matching. ' +
|
|
632
632
|
'Re-read the file and copy the literal lines.'
|
|
633
633
|
);
|