@librechat/agents 3.2.41 → 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/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/cloudflare/CloudflareProgrammaticToolCalling.cjs +2 -2
- 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/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/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/cloudflare/CloudflareProgrammaticToolCalling.mjs +3 -3
- 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/types/llm/bedrock/index.d.ts +7 -0
- package/dist/types/messages/cache.d.ts +17 -0
- 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/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/__tests__/CloudflareSandboxExecution.test.ts +131 -0
- package/src/tools/cloudflare/CloudflareProgrammaticToolCalling.ts +7 -2
- package/src/tools/cloudflare/CloudflareSandboxExecutionEngine.ts +165 -9
|
@@ -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
|
|