@librechat/agents 3.2.42 → 3.2.44
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 +4 -3
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +1 -1
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +1 -1
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +2 -0
- package/dist/cjs/messages/cache.cjs +26 -4
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/messages/reducer.cjs +15 -2
- package/dist/cjs/messages/reducer.cjs.map +1 -1
- package/dist/cjs/session/JsonlSessionStore.cjs +1 -1
- package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs +32 -16
- package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs.map +1 -1
- package/dist/cjs/tools/local/CompileCheckTool.cjs +11 -3
- package/dist/cjs/tools/local/CompileCheckTool.cjs.map +1 -1
- package/dist/cjs/tools/local/FileCheckpointer.cjs +8 -3
- package/dist/cjs/tools/local/FileCheckpointer.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalCodingTools.cjs +26 -7
- package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs +2 -2
- package/dist/cjs/tools/local/syntaxCheck.cjs +6 -2
- package/dist/cjs/tools/local/syntaxCheck.cjs.map +1 -1
- package/dist/cjs/tools/local/workspaceFS.cjs +20 -0
- package/dist/cjs/tools/local/workspaceFS.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +5 -4
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +2 -2
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/hooks/createWorkspacePolicyHook.mjs +1 -1
- package/dist/esm/llm/bedrock/index.mjs +2 -2
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/main.mjs +3 -3
- package/dist/esm/messages/cache.mjs +26 -5
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/messages/reducer.mjs +15 -2
- package/dist/esm/messages/reducer.mjs.map +1 -1
- package/dist/esm/session/JsonlSessionStore.mjs +1 -1
- package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs +32 -17
- package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs.map +1 -1
- package/dist/esm/tools/local/CompileCheckTool.mjs +11 -3
- package/dist/esm/tools/local/CompileCheckTool.mjs.map +1 -1
- package/dist/esm/tools/local/FileCheckpointer.mjs +9 -4
- package/dist/esm/tools/local/FileCheckpointer.mjs.map +1 -1
- package/dist/esm/tools/local/LocalCodingTools.mjs +26 -7
- package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -1
- package/dist/esm/tools/local/LocalExecutionEngine.mjs +2 -2
- package/dist/esm/tools/local/syntaxCheck.mjs +6 -2
- package/dist/esm/tools/local/syntaxCheck.mjs.map +1 -1
- package/dist/esm/tools/local/workspaceFS.mjs +19 -1
- package/dist/esm/tools/local/workspaceFS.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +3 -2
- package/dist/types/messages/cache.d.ts +12 -2
- package/dist/types/tools/cloudflare/CloudflareSandboxExecutionEngine.d.ts +9 -0
- package/dist/types/tools/local/workspaceFS.d.ts +13 -0
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +8 -3
- package/src/graphs/Graph.ts +7 -1
- package/src/llm/bedrock/index.ts +2 -2
- package/src/messages/cache.test.ts +32 -0
- package/src/messages/cache.ts +31 -5
- package/src/messages/reducer.spec.ts +32 -0
- package/src/messages/reducer.ts +23 -7
- package/src/specs/vllm-reasoning-toolcalls.test.ts +340 -0
- package/src/tools/__tests__/CloudflareSandboxExecution.test.ts +192 -0
- package/src/tools/__tests__/LocalExecutionTools.test.ts +54 -0
- package/src/tools/cloudflare/CloudflareSandboxExecutionEngine.ts +106 -30
- package/src/tools/local/CompileCheckTool.ts +19 -3
- package/src/tools/local/FileCheckpointer.ts +20 -4
- package/src/tools/local/LocalCodingTools.ts +61 -8
- package/src/tools/local/__tests__/FileCheckpointer.test.ts +42 -0
- package/src/tools/local/syntaxCheck.ts +14 -2
- package/src/tools/local/workspaceFS.ts +27 -0
|
@@ -3,6 +3,7 @@ import { createTwoFilesPatch } from 'diff';
|
|
|
3
3
|
import { tool } from '@langchain/core/tools';
|
|
4
4
|
import type { DynamicStructuredTool } from '@langchain/core/tools';
|
|
5
5
|
import type * as t from '@/types';
|
|
6
|
+
import { isWorkspaceClientTimeoutError } from './workspaceFS';
|
|
6
7
|
import {
|
|
7
8
|
createLocalBashProgrammaticToolCallingTool,
|
|
8
9
|
createLocalProgrammaticToolCallingTool,
|
|
@@ -294,8 +295,14 @@ async function revertStrictWrite(
|
|
|
294
295
|
} else {
|
|
295
296
|
await fs.unlink(path);
|
|
296
297
|
}
|
|
297
|
-
} catch {
|
|
298
|
-
|
|
298
|
+
} catch (error) {
|
|
299
|
+
// A timed-out revert leaves the rejected write on disk — the caller would
|
|
300
|
+
// otherwise claim "reverted to pre-write state" while the bad bytes remain.
|
|
301
|
+
// Surface it; for other failures stay best-effort (caller sees the original
|
|
302
|
+
// syntax error).
|
|
303
|
+
if (isWorkspaceClientTimeoutError(error)) {
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
299
306
|
}
|
|
300
307
|
}
|
|
301
308
|
|
|
@@ -538,7 +545,12 @@ export function createLocalWriteFileTool(
|
|
|
538
545
|
before = decoded.text;
|
|
539
546
|
encoding = decoded;
|
|
540
547
|
existed = true;
|
|
541
|
-
} catch {
|
|
548
|
+
} catch (error) {
|
|
549
|
+
// A stalled-RPC timeout is NOT "file absent" — treating it as such would
|
|
550
|
+
// overwrite an existing file with fresh content. Surface it instead.
|
|
551
|
+
if (isWorkspaceClientTimeoutError(error)) {
|
|
552
|
+
throw error;
|
|
553
|
+
}
|
|
542
554
|
existed = false;
|
|
543
555
|
}
|
|
544
556
|
|
|
@@ -546,7 +558,21 @@ export function createLocalWriteFileTool(
|
|
|
546
558
|
const finalText = encodeFile(input.content, encoding);
|
|
547
559
|
await fs.writeFile(path, finalText, 'utf8');
|
|
548
560
|
|
|
549
|
-
|
|
561
|
+
let syntax;
|
|
562
|
+
try {
|
|
563
|
+
syntax = await maybeRunSyntaxCheck(path, config);
|
|
564
|
+
} catch (error) {
|
|
565
|
+
// A validation-read timeout must still honor strict mode's revert
|
|
566
|
+
// contract: restore the pre-write state before surfacing (best-effort —
|
|
567
|
+
// the revert may itself time out on the same stalled container).
|
|
568
|
+
if (
|
|
569
|
+
isWorkspaceClientTimeoutError(error) &&
|
|
570
|
+
config.postEditSyntaxCheck === 'strict'
|
|
571
|
+
) {
|
|
572
|
+
await revertStrictWrite(fs, path, existed, before, encoding);
|
|
573
|
+
}
|
|
574
|
+
throw error;
|
|
575
|
+
}
|
|
550
576
|
|
|
551
577
|
const diff = existed
|
|
552
578
|
? summariseDiff(path, before, input.content)
|
|
@@ -642,7 +668,20 @@ export function createLocalEditFileTool(
|
|
|
642
668
|
const finalText = encodeFile(next, encoding);
|
|
643
669
|
await fs.writeFile(path, finalText, 'utf8');
|
|
644
670
|
|
|
645
|
-
|
|
671
|
+
let syntax;
|
|
672
|
+
try {
|
|
673
|
+
syntax = await maybeRunSyntaxCheck(path, config);
|
|
674
|
+
} catch (error) {
|
|
675
|
+
// As in write_file: a validation-read timeout still triggers strict
|
|
676
|
+
// mode's revert (edit_file always operates on an existing file).
|
|
677
|
+
if (
|
|
678
|
+
isWorkspaceClientTimeoutError(error) &&
|
|
679
|
+
config.postEditSyntaxCheck === 'strict'
|
|
680
|
+
) {
|
|
681
|
+
await revertStrictWrite(fs, path, true, original, encoding);
|
|
682
|
+
}
|
|
683
|
+
throw error;
|
|
684
|
+
}
|
|
646
685
|
|
|
647
686
|
const diff = summariseDiff(path, original, next);
|
|
648
687
|
const fuzzy = strategiesUsed.some((s) => s !== 'exact');
|
|
@@ -826,7 +865,12 @@ async function* walkFiles(
|
|
|
826
865
|
let entries;
|
|
827
866
|
try {
|
|
828
867
|
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
829
|
-
} catch {
|
|
868
|
+
} catch (error) {
|
|
869
|
+
// A stalled-RPC timeout must surface, not be silently skipped as an
|
|
870
|
+
// unreadable directory — that would corrupt search results ("no matches").
|
|
871
|
+
if (isWorkspaceClientTimeoutError(error)) {
|
|
872
|
+
throw error;
|
|
873
|
+
}
|
|
830
874
|
continue;
|
|
831
875
|
}
|
|
832
876
|
for (const entry of entries) {
|
|
@@ -1009,7 +1053,12 @@ async function fallbackGrep(
|
|
|
1009
1053
|
let stat;
|
|
1010
1054
|
try {
|
|
1011
1055
|
stat = await fs.stat(file);
|
|
1012
|
-
} catch {
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
// A stalled-RPC timeout must surface, not be skipped — skipping a file can
|
|
1058
|
+
// report "no matches" even when the (unreadable-due-to-stall) file matched.
|
|
1059
|
+
if (isWorkspaceClientTimeoutError(error)) {
|
|
1060
|
+
throw error;
|
|
1061
|
+
}
|
|
1013
1062
|
continue;
|
|
1014
1063
|
}
|
|
1015
1064
|
if (stat.size > FALLBACK_GREP_MAX_FILE_BYTES) {
|
|
@@ -1021,7 +1070,11 @@ async function fallbackGrep(
|
|
|
1021
1070
|
let content;
|
|
1022
1071
|
try {
|
|
1023
1072
|
content = await fs.readFile(file, 'utf8');
|
|
1024
|
-
} catch {
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
// As above: a client-timeout means a stalled read, not an unreadable file.
|
|
1075
|
+
if (isWorkspaceClientTimeoutError(error)) {
|
|
1076
|
+
throw error;
|
|
1077
|
+
}
|
|
1025
1078
|
continue;
|
|
1026
1079
|
}
|
|
1027
1080
|
if (content.includes('\0')) {
|
|
@@ -3,6 +3,10 @@ import { join } from 'path';
|
|
|
3
3
|
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'fs/promises';
|
|
4
4
|
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
|
5
5
|
import { LocalFileCheckpointerImpl } from '../FileCheckpointer';
|
|
6
|
+
import {
|
|
7
|
+
WorkspaceClientTimeoutError,
|
|
8
|
+
type WorkspaceFS,
|
|
9
|
+
} from '../workspaceFS';
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
12
|
* Pins the LocalFileCheckpointer's per-Run snapshot/rewind contract.
|
|
@@ -63,6 +67,44 @@ describe('LocalFileCheckpointerImpl', () => {
|
|
|
63
67
|
await expect(stat(file)).rejects.toThrow();
|
|
64
68
|
});
|
|
65
69
|
|
|
70
|
+
it('rethrows a client-timeout from stat instead of snapshotting as absent', async () => {
|
|
71
|
+
// A stalled stat() must NOT be recorded as "absent" — that would delete an
|
|
72
|
+
// existing file on rewind. The timeout has to surface.
|
|
73
|
+
const fs = {
|
|
74
|
+
stat: async () => {
|
|
75
|
+
throw new WorkspaceClientTimeoutError(
|
|
76
|
+
'cloudflare sandbox listFiles exceeded 6000ms client-side timeout'
|
|
77
|
+
);
|
|
78
|
+
},
|
|
79
|
+
} as unknown as WorkspaceFS;
|
|
80
|
+
const cp = new LocalFileCheckpointerImpl(32 * 1024 * 1024, fs);
|
|
81
|
+
|
|
82
|
+
await expect(cp.captureBeforeWrite('/workspace/a.txt')).rejects.toThrow(
|
|
83
|
+
/client-side timeout/
|
|
84
|
+
);
|
|
85
|
+
// Nothing was snapshotted, so rewind is a no-op (no spurious deletion).
|
|
86
|
+
expect(await cp.rewind()).toBe(0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('rewind rethrows a client-timeout from a restore write instead of claiming success', async () => {
|
|
90
|
+
// A stalled restore write leaves the bad bytes on disk — rewind must surface
|
|
91
|
+
// the timeout, not silently count the path as restored.
|
|
92
|
+
const fs = {
|
|
93
|
+
stat: async () => ({ isFile: () => true, size: 5 }),
|
|
94
|
+
readFile: async () => Buffer.from('orig\n'),
|
|
95
|
+
mkdir: async () => undefined,
|
|
96
|
+
writeFile: async () => {
|
|
97
|
+
throw new WorkspaceClientTimeoutError(
|
|
98
|
+
'cloudflare sandbox writeFile exceeded 6000ms client-side timeout'
|
|
99
|
+
);
|
|
100
|
+
},
|
|
101
|
+
} as unknown as WorkspaceFS;
|
|
102
|
+
const cp = new LocalFileCheckpointerImpl(32 * 1024 * 1024, fs);
|
|
103
|
+
|
|
104
|
+
await cp.captureBeforeWrite('/workspace/a.txt'); // snapshots 'present'
|
|
105
|
+
await expect(cp.rewind()).rejects.toThrow(/client-side timeout/);
|
|
106
|
+
});
|
|
107
|
+
|
|
66
108
|
it('rewinds across multiple files in a single pass', async () => {
|
|
67
109
|
const a = join(dir, 'multi-a.txt');
|
|
68
110
|
const b = join(dir, 'multi-b.txt');
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import { extname } from 'path';
|
|
20
20
|
import type * as t from '@/types';
|
|
21
|
+
import { isWorkspaceClientTimeoutError } from './workspaceFS';
|
|
21
22
|
import {
|
|
22
23
|
getSpawn,
|
|
23
24
|
getWorkspaceFS,
|
|
@@ -168,7 +169,13 @@ const jsonCheck: SyntaxChecker = async (path, config) => {
|
|
|
168
169
|
// file → catch returns undefined → ok: true) or read a different
|
|
169
170
|
// file with the same absolute path. Codex P1 #24.
|
|
170
171
|
const fs = getWorkspaceFS(config);
|
|
171
|
-
const raw = await fs.readFile(path, 'utf8').catch(() =>
|
|
172
|
+
const raw = await fs.readFile(path, 'utf8').catch((error) => {
|
|
173
|
+
// A stalled read must not pass the syntax gate as ok:true — surface it.
|
|
174
|
+
if (isWorkspaceClientTimeoutError(error)) {
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
178
|
+
});
|
|
172
179
|
if (raw == null) return { ok: true };
|
|
173
180
|
try {
|
|
174
181
|
JSON.parse(raw);
|
|
@@ -237,7 +244,12 @@ export async function runPostEditSyntaxCheck(
|
|
|
237
244
|
};
|
|
238
245
|
}
|
|
239
246
|
return result;
|
|
240
|
-
} catch {
|
|
247
|
+
} catch (error) {
|
|
248
|
+
// Don't swallow a stalled-RPC timeout as "no checker outcome" (which would
|
|
249
|
+
// let the write through without a real syntax gate) — surface it.
|
|
250
|
+
if (isWorkspaceClientTimeoutError(error)) {
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
241
253
|
return null;
|
|
242
254
|
}
|
|
243
255
|
}
|
|
@@ -39,6 +39,33 @@ export type ReaddirEntry = {
|
|
|
39
39
|
isSymbolicLink(): boolean;
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Thrown when a {@link WorkspaceFS} operation is abandoned by a client-side
|
|
44
|
+
* backstop timeout (a stalled remote/sandbox RPC that never returned). This is
|
|
45
|
+
* DISTINCT from "file not found": callers that catch generic FS failures as
|
|
46
|
+
* absence — e.g. a pre-read before an overwrite, or a `stat` before snapshotting
|
|
47
|
+
* — MUST rethrow this so a stalled read isn't mistaken for a missing file (which
|
|
48
|
+
* would otherwise overwrite an existing file or snapshot it as absent).
|
|
49
|
+
*/
|
|
50
|
+
export class WorkspaceClientTimeoutError extends Error {
|
|
51
|
+
readonly code = 'WORKSPACE_CLIENT_TIMEOUT';
|
|
52
|
+
constructor(message: string) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = 'WorkspaceClientTimeoutError';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function isWorkspaceClientTimeoutError(
|
|
59
|
+
error: unknown
|
|
60
|
+
): error is WorkspaceClientTimeoutError {
|
|
61
|
+
return (
|
|
62
|
+
error instanceof WorkspaceClientTimeoutError ||
|
|
63
|
+
(typeof error === 'object' &&
|
|
64
|
+
error !== null &&
|
|
65
|
+
(error as { code?: unknown }).code === 'WORKSPACE_CLIENT_TIMEOUT')
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
42
69
|
export interface WorkspaceFS {
|
|
43
70
|
readFile(path: string, encoding: 'utf8'): Promise<string>;
|
|
44
71
|
readFile(path: string): Promise<Buffer>;
|