@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.
Files changed (37) hide show
  1. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  2. package/dist/cjs/graphs/Graph.cjs +4 -1
  3. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  4. package/dist/cjs/llm/bedrock/index.cjs +9 -1
  5. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  6. package/dist/cjs/main.cjs +4 -0
  7. package/dist/cjs/messages/cache.cjs +21 -0
  8. package/dist/cjs/messages/cache.cjs.map +1 -1
  9. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs +2 -2
  10. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs.map +1 -1
  11. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs +87 -7
  12. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs.map +1 -1
  13. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  14. package/dist/esm/graphs/Graph.mjs +5 -2
  15. package/dist/esm/graphs/Graph.mjs.map +1 -1
  16. package/dist/esm/llm/bedrock/index.mjs +10 -2
  17. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  18. package/dist/esm/main.mjs +3 -3
  19. package/dist/esm/messages/cache.mjs +21 -1
  20. package/dist/esm/messages/cache.mjs.map +1 -1
  21. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs +3 -3
  22. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs.map +1 -1
  23. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs +85 -8
  24. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs.map +1 -1
  25. package/dist/types/llm/bedrock/index.d.ts +7 -0
  26. package/dist/types/messages/cache.d.ts +17 -0
  27. package/dist/types/tools/cloudflare/CloudflareSandboxExecutionEngine.d.ts +44 -0
  28. package/package.json +1 -1
  29. package/src/agents/AgentContext.ts +2 -0
  30. package/src/graphs/Graph.ts +17 -5
  31. package/src/llm/bedrock/index.ts +18 -2
  32. package/src/llm/bedrock/llm.spec.ts +97 -0
  33. package/src/messages/cache.test.ts +31 -0
  34. package/src/messages/cache.ts +25 -0
  35. package/src/tools/__tests__/CloudflareSandboxExecution.test.ts +131 -0
  36. package/src/tools/cloudflare/CloudflareProgrammaticToolCalling.ts +7 -2
  37. 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 ctx.sandbox.exec(timedCommand, execOptions);
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 ctx.sandbox.exec(
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 ctx.sandbox.exec(
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
- await ctx.sandbox
723
- .exec(`rm -rf ${quote(tempDir)}`, {
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
- .catch(() => undefined);
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