@librechat/agents 3.1.77 → 3.1.78-dev.0

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 (181) hide show
  1. package/dist/cjs/common/enum.cjs +54 -0
  2. package/dist/cjs/common/enum.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +148 -4
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +291 -0
  6. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs.map +1 -0
  7. package/dist/cjs/main.cjs +90 -0
  8. package/dist/cjs/main.cjs.map +1 -1
  9. package/dist/cjs/messages/anthropicToolCache.cjs +102 -0
  10. package/dist/cjs/messages/anthropicToolCache.cjs.map +1 -0
  11. package/dist/cjs/messages/prune.cjs +27 -0
  12. package/dist/cjs/messages/prune.cjs.map +1 -1
  13. package/dist/cjs/messages/recency.cjs +99 -0
  14. package/dist/cjs/messages/recency.cjs.map +1 -0
  15. package/dist/cjs/run.cjs +30 -0
  16. package/dist/cjs/run.cjs.map +1 -1
  17. package/dist/cjs/summarization/node.cjs +100 -6
  18. package/dist/cjs/summarization/node.cjs.map +1 -1
  19. package/dist/cjs/tools/ToolNode.cjs +635 -23
  20. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  21. package/dist/cjs/tools/local/CompileCheckTool.cjs +227 -0
  22. package/dist/cjs/tools/local/CompileCheckTool.cjs.map +1 -0
  23. package/dist/cjs/tools/local/FileCheckpointer.cjs +90 -0
  24. package/dist/cjs/tools/local/FileCheckpointer.cjs.map +1 -0
  25. package/dist/cjs/tools/local/LocalCodingTools.cjs +1098 -0
  26. package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -0
  27. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +1042 -0
  28. package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -0
  29. package/dist/cjs/tools/local/LocalExecutionTools.cjs +122 -0
  30. package/dist/cjs/tools/local/LocalExecutionTools.cjs.map +1 -0
  31. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +453 -0
  32. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs.map +1 -0
  33. package/dist/cjs/tools/local/attachments.cjs +183 -0
  34. package/dist/cjs/tools/local/attachments.cjs.map +1 -0
  35. package/dist/cjs/tools/local/bashAst.cjs +129 -0
  36. package/dist/cjs/tools/local/bashAst.cjs.map +1 -0
  37. package/dist/cjs/tools/local/editStrategies.cjs +188 -0
  38. package/dist/cjs/tools/local/editStrategies.cjs.map +1 -0
  39. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs +141 -0
  40. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs.map +1 -0
  41. package/dist/cjs/tools/local/syntaxCheck.cjs +182 -0
  42. package/dist/cjs/tools/local/syntaxCheck.cjs.map +1 -0
  43. package/dist/cjs/tools/local/textEncoding.cjs +30 -0
  44. package/dist/cjs/tools/local/textEncoding.cjs.map +1 -0
  45. package/dist/cjs/tools/local/workspaceFS.cjs +51 -0
  46. package/dist/cjs/tools/local/workspaceFS.cjs.map +1 -0
  47. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -0
  48. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  49. package/dist/esm/common/enum.mjs +53 -1
  50. package/dist/esm/common/enum.mjs.map +1 -1
  51. package/dist/esm/graphs/Graph.mjs +149 -5
  52. package/dist/esm/graphs/Graph.mjs.map +1 -1
  53. package/dist/esm/hooks/createWorkspacePolicyHook.mjs +289 -0
  54. package/dist/esm/hooks/createWorkspacePolicyHook.mjs.map +1 -0
  55. package/dist/esm/main.mjs +17 -2
  56. package/dist/esm/main.mjs.map +1 -1
  57. package/dist/esm/messages/anthropicToolCache.mjs +99 -0
  58. package/dist/esm/messages/anthropicToolCache.mjs.map +1 -0
  59. package/dist/esm/messages/prune.mjs +26 -1
  60. package/dist/esm/messages/prune.mjs.map +1 -1
  61. package/dist/esm/messages/recency.mjs +97 -0
  62. package/dist/esm/messages/recency.mjs.map +1 -0
  63. package/dist/esm/run.mjs +30 -0
  64. package/dist/esm/run.mjs.map +1 -1
  65. package/dist/esm/summarization/node.mjs +100 -6
  66. package/dist/esm/summarization/node.mjs.map +1 -1
  67. package/dist/esm/tools/ToolNode.mjs +635 -23
  68. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  69. package/dist/esm/tools/local/CompileCheckTool.mjs +223 -0
  70. package/dist/esm/tools/local/CompileCheckTool.mjs.map +1 -0
  71. package/dist/esm/tools/local/FileCheckpointer.mjs +87 -0
  72. package/dist/esm/tools/local/FileCheckpointer.mjs.map +1 -0
  73. package/dist/esm/tools/local/LocalCodingTools.mjs +1075 -0
  74. package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -0
  75. package/dist/esm/tools/local/LocalExecutionEngine.mjs +1022 -0
  76. package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -0
  77. package/dist/esm/tools/local/LocalExecutionTools.mjs +117 -0
  78. package/dist/esm/tools/local/LocalExecutionTools.mjs.map +1 -0
  79. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +448 -0
  80. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs.map +1 -0
  81. package/dist/esm/tools/local/attachments.mjs +180 -0
  82. package/dist/esm/tools/local/attachments.mjs.map +1 -0
  83. package/dist/esm/tools/local/bashAst.mjs +126 -0
  84. package/dist/esm/tools/local/bashAst.mjs.map +1 -0
  85. package/dist/esm/tools/local/editStrategies.mjs +185 -0
  86. package/dist/esm/tools/local/editStrategies.mjs.map +1 -0
  87. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs +137 -0
  88. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs.map +1 -0
  89. package/dist/esm/tools/local/syntaxCheck.mjs +179 -0
  90. package/dist/esm/tools/local/syntaxCheck.mjs.map +1 -0
  91. package/dist/esm/tools/local/textEncoding.mjs +27 -0
  92. package/dist/esm/tools/local/textEncoding.mjs.map +1 -0
  93. package/dist/esm/tools/local/workspaceFS.mjs +49 -0
  94. package/dist/esm/tools/local/workspaceFS.mjs.map +1 -0
  95. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -0
  96. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  97. package/dist/types/common/enum.d.ts +39 -1
  98. package/dist/types/graphs/Graph.d.ts +34 -0
  99. package/dist/types/hooks/createWorkspacePolicyHook.d.ts +95 -0
  100. package/dist/types/hooks/index.d.ts +2 -0
  101. package/dist/types/index.d.ts +1 -0
  102. package/dist/types/messages/anthropicToolCache.d.ts +51 -0
  103. package/dist/types/messages/index.d.ts +2 -0
  104. package/dist/types/messages/prune.d.ts +11 -0
  105. package/dist/types/messages/recency.d.ts +64 -0
  106. package/dist/types/run.d.ts +21 -0
  107. package/dist/types/tools/ToolNode.d.ts +145 -2
  108. package/dist/types/tools/local/CompileCheckTool.d.ts +31 -0
  109. package/dist/types/tools/local/FileCheckpointer.d.ts +39 -0
  110. package/dist/types/tools/local/LocalCodingTools.d.ts +57 -0
  111. package/dist/types/tools/local/LocalExecutionEngine.d.ts +149 -0
  112. package/dist/types/tools/local/LocalExecutionTools.d.ts +9 -0
  113. package/dist/types/tools/local/LocalProgrammaticToolCalling.d.ts +21 -0
  114. package/dist/types/tools/local/attachments.d.ts +84 -0
  115. package/dist/types/tools/local/bashAst.d.ts +11 -0
  116. package/dist/types/tools/local/editStrategies.d.ts +28 -0
  117. package/dist/types/tools/local/index.d.ts +12 -0
  118. package/dist/types/tools/local/resolveLocalExecutionTools.d.ts +38 -0
  119. package/dist/types/tools/local/syntaxCheck.d.ts +42 -0
  120. package/dist/types/tools/local/textEncoding.d.ts +21 -0
  121. package/dist/types/tools/local/workspaceFS.d.ts +49 -0
  122. package/dist/types/types/hitl.d.ts +56 -27
  123. package/dist/types/types/run.d.ts +8 -1
  124. package/dist/types/types/summarize.d.ts +30 -0
  125. package/dist/types/types/tools.d.ts +341 -6
  126. package/package.json +21 -2
  127. package/src/common/enum.ts +54 -0
  128. package/src/graphs/Graph.ts +164 -6
  129. package/src/hooks/__tests__/compactHooks.test.ts +38 -2
  130. package/src/hooks/__tests__/createWorkspacePolicyHook.test.ts +393 -0
  131. package/src/hooks/createWorkspacePolicyHook.ts +355 -0
  132. package/src/hooks/index.ts +6 -0
  133. package/src/index.ts +1 -0
  134. package/src/messages/__tests__/anthropicToolCache.test.ts +125 -0
  135. package/src/messages/__tests__/recency.test.ts +267 -0
  136. package/src/messages/anthropicToolCache.ts +116 -0
  137. package/src/messages/index.ts +2 -0
  138. package/src/messages/prune.ts +27 -1
  139. package/src/messages/recency.ts +155 -0
  140. package/src/run.ts +31 -0
  141. package/src/scripts/compare_pi_vs_ours.ts +840 -0
  142. package/src/scripts/local_engine.ts +166 -0
  143. package/src/scripts/local_engine_checkpointer.ts +205 -0
  144. package/src/scripts/local_engine_compile.ts +263 -0
  145. package/src/scripts/local_engine_hooks.ts +226 -0
  146. package/src/scripts/local_engine_image.ts +201 -0
  147. package/src/scripts/local_engine_ptc.ts +151 -0
  148. package/src/scripts/local_engine_workspace.ts +258 -0
  149. package/src/scripts/summarization-recency.ts +462 -0
  150. package/src/specs/prune.test.ts +39 -0
  151. package/src/summarization/__tests__/node.test.ts +499 -3
  152. package/src/summarization/node.ts +124 -7
  153. package/src/tools/ToolNode.ts +769 -20
  154. package/src/tools/__tests__/LocalExecutionTools.test.ts +2647 -0
  155. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +175 -0
  156. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +114 -0
  157. package/src/tools/__tests__/ToolNode.session.test.ts +84 -0
  158. package/src/tools/__tests__/directToolHITLResumeScope.test.ts +467 -0
  159. package/src/tools/__tests__/directToolHooks.test.ts +411 -0
  160. package/src/tools/__tests__/localToolNames.test.ts +73 -0
  161. package/src/tools/__tests__/workspaceSeam.test.ts +134 -0
  162. package/src/tools/local/CompileCheckTool.ts +278 -0
  163. package/src/tools/local/FileCheckpointer.ts +93 -0
  164. package/src/tools/local/LocalCodingTools.ts +1342 -0
  165. package/src/tools/local/LocalExecutionEngine.ts +1329 -0
  166. package/src/tools/local/LocalExecutionTools.ts +167 -0
  167. package/src/tools/local/LocalProgrammaticToolCalling.ts +594 -0
  168. package/src/tools/local/__tests__/FileCheckpointer.test.ts +120 -0
  169. package/src/tools/local/__tests__/editStrategies.test.ts +134 -0
  170. package/src/tools/local/attachments.ts +251 -0
  171. package/src/tools/local/bashAst.ts +151 -0
  172. package/src/tools/local/editStrategies.ts +188 -0
  173. package/src/tools/local/index.ts +12 -0
  174. package/src/tools/local/resolveLocalExecutionTools.ts +208 -0
  175. package/src/tools/local/syntaxCheck.ts +243 -0
  176. package/src/tools/local/textEncoding.ts +37 -0
  177. package/src/tools/local/workspaceFS.ts +89 -0
  178. package/src/types/hitl.ts +56 -27
  179. package/src/types/run.ts +12 -1
  180. package/src/types/summarize.ts +31 -0
  181. package/src/types/tools.ts +359 -7
@@ -0,0 +1,278 @@
1
+ /**
2
+ * `compile_check` — a thin LLM-callable wrapper around the project's
3
+ * standard typecheck/lint command. Lets the agent answer "did my
4
+ * change break anything?" without us shipping a real LSP client.
5
+ *
6
+ * Auto-detection priority (first hit wins):
7
+ *
8
+ * 1. `local.compileCheck.command` — explicit override
9
+ * 2. `tsconfig.json` → `npx --no-install tsc --noEmit`
10
+ * 3. `package.json` with a typescript dep → same as 2
11
+ * 4. `pyproject.toml` or `setup.py` / `setup.cfg`
12
+ * with a dev dep on mypy → `python3 -m mypy .`
13
+ * else → `python3 -m py_compile <every .py>`
14
+ * (bounded by find-walk so node_modules
15
+ * and `.venv` don't blow up)
16
+ * 5. `Cargo.toml` → `cargo check --message-format=short`
17
+ * 6. `go.mod` → `go vet ./...`
18
+ * 7. otherwise → tells the agent there's
19
+ * no detected toolchain.
20
+ *
21
+ * Output is the spawn process's stdout/stderr passed through
22
+ * `truncateLocalOutput` so a 10MB tsc dump can't blow context. The
23
+ * exit code is reported.
24
+ */
25
+
26
+ import { resolve } from 'path';
27
+ import { tool } from '@langchain/core/tools';
28
+ import type { DynamicStructuredTool } from '@langchain/core/tools';
29
+ import type * as t from '@/types';
30
+ import {
31
+ getLocalCwd,
32
+ getWorkspaceFS,
33
+ spawnLocalProcess,
34
+ truncateLocalOutput,
35
+ validateBashCommand,
36
+ } from './LocalExecutionEngine';
37
+ import type { WorkspaceFS } from './workspaceFS';
38
+ import { Constants } from '@/common';
39
+
40
+ /** Back-compat alias; canonical name lives on `Constants.COMPILE_CHECK`. */
41
+ export const CompileCheckToolName = Constants.COMPILE_CHECK;
42
+
43
+ const CompileCheckSchema: t.JsonSchemaType = {
44
+ type: 'object',
45
+ properties: {
46
+ command: {
47
+ type: 'string',
48
+ description:
49
+ 'Optional explicit command to run instead of the auto-detected one. Runs verbatim from the local engine cwd; honours the standard sandbox/AST gate.',
50
+ },
51
+ timeout_ms: {
52
+ type: 'integer',
53
+ description:
54
+ 'Optional timeout in milliseconds. Defaults to 120000 (2 min).',
55
+ },
56
+ },
57
+ };
58
+
59
+ type DetectedKind =
60
+ | 'typescript'
61
+ | 'python-mypy'
62
+ | 'python-compile'
63
+ | 'rust'
64
+ | 'go'
65
+ | 'unknown';
66
+
67
+ type Detection = {
68
+ kind: DetectedKind;
69
+ command: string;
70
+ reason: string;
71
+ };
72
+
73
+ async function pathExists(fs: WorkspaceFS, p: string): Promise<boolean> {
74
+ try {
75
+ await fs.stat(p);
76
+ return true;
77
+ } catch {
78
+ return false;
79
+ }
80
+ }
81
+
82
+ // Probes for project markers via the configured WorkspaceFS so a Run
83
+ // with `local.exec.fs` (in-memory or remote engine) detects the right
84
+ // toolchain against the actual workspace — not the host filesystem.
85
+ // Codex P1 #25.
86
+ async function detect(cwd: string, fs: WorkspaceFS): Promise<Detection> {
87
+ if (await pathExists(fs, resolve(cwd, 'tsconfig.json'))) {
88
+ return {
89
+ kind: 'typescript',
90
+ command: 'npx --no-install tsc --noEmit',
91
+ reason: 'tsconfig.json present',
92
+ };
93
+ }
94
+ if (await pathExists(fs, resolve(cwd, 'package.json'))) {
95
+ const pkgRaw = await fs
96
+ .readFile(resolve(cwd, 'package.json'), 'utf8')
97
+ .catch(() => '');
98
+ if (pkgRaw.includes('"typescript"')) {
99
+ return {
100
+ kind: 'typescript',
101
+ command: 'npx --no-install tsc --noEmit',
102
+ reason: 'package.json declares typescript',
103
+ };
104
+ }
105
+ }
106
+ if (await pathExists(fs, resolve(cwd, 'Cargo.toml'))) {
107
+ return {
108
+ kind: 'rust',
109
+ command: 'cargo check --message-format=short',
110
+ reason: 'Cargo.toml present',
111
+ };
112
+ }
113
+ if (await pathExists(fs, resolve(cwd, 'go.mod'))) {
114
+ return {
115
+ kind: 'go',
116
+ command: 'go vet ./...',
117
+ reason: 'go.mod present',
118
+ };
119
+ }
120
+ if (
121
+ (await pathExists(fs, resolve(cwd, 'pyproject.toml'))) ||
122
+ (await pathExists(fs, resolve(cwd, 'setup.py'))) ||
123
+ (await pathExists(fs, resolve(cwd, 'setup.cfg')))
124
+ ) {
125
+ const pyToml = await fs
126
+ .readFile(resolve(cwd, 'pyproject.toml'), 'utf8')
127
+ .catch(() => '');
128
+ if (pyToml.includes('mypy')) {
129
+ return {
130
+ kind: 'python-mypy',
131
+ command: 'python3 -m mypy .',
132
+ reason: 'pyproject.toml declares mypy',
133
+ };
134
+ }
135
+ return {
136
+ kind: 'python-compile',
137
+ command:
138
+ 'python3 -c "import compileall, sys; sys.exit(0 if compileall.compile_dir(\'.\', quiet=1, rx=__import__(\'re\').compile(r\'(node_modules|\\.venv|\\.git|build|dist)\')) else 1)"',
139
+ reason: 'Python project (no mypy detected)',
140
+ };
141
+ }
142
+ return {
143
+ kind: 'unknown',
144
+ command: '',
145
+ reason:
146
+ 'no recognised project marker (tsconfig.json, package.json[typescript], Cargo.toml, go.mod, pyproject.toml, setup.py)',
147
+ };
148
+ }
149
+
150
+ const DEFAULT_TIMEOUT_MS = 120_000;
151
+
152
+ export function createCompileCheckTool(
153
+ config: t.LocalExecutionConfig = {}
154
+ ): DynamicStructuredTool {
155
+ return tool(
156
+ async (rawInput) => {
157
+ const input = rawInput as {
158
+ command?: string;
159
+ timeout_ms?: number;
160
+ };
161
+ const cwd = getLocalCwd(config);
162
+ const fs = getWorkspaceFS(config);
163
+ const overrideCommand = input.command ?? config.compileCheck?.command;
164
+ let detection: Detection;
165
+ if (overrideCommand != null && overrideCommand.trim() !== '') {
166
+ detection = {
167
+ kind: 'unknown',
168
+ command: overrideCommand,
169
+ reason: 'explicit override',
170
+ };
171
+ } else {
172
+ detection = await detect(cwd, fs);
173
+ }
174
+
175
+ if (detection.command === '') {
176
+ const explainer =
177
+ `compile_check: ${detection.reason}. Pass an explicit \`command\` (e.g. \`npm run typecheck\`) to override.`;
178
+ return [
179
+ explainer,
180
+ {
181
+ kind: detection.kind,
182
+ ran: false,
183
+ reason: detection.reason,
184
+ cwd,
185
+ },
186
+ ];
187
+ }
188
+
189
+ // Codex P1 #21: route the resolved command through the same
190
+ // safety gates the rest of the local engine uses. Without this
191
+ // a host with `readOnly: true` (or relying on the destructive-
192
+ // command guard) could be bypassed by passing a `command`
193
+ // override to compile_check that performs writes/deletes.
194
+ // Auto-detected commands (tsc/cargo/etc.) pass these gates
195
+ // unchanged — the validation is only blocking for genuinely
196
+ // mutating overrides.
197
+ const validation = await validateBashCommand(detection.command, config);
198
+ if (!validation.valid) {
199
+ const explainer =
200
+ `compile_check refused to run \`${detection.command}\`: ${validation.errors.join('; ')}`;
201
+ return [
202
+ explainer,
203
+ {
204
+ kind: detection.kind,
205
+ ran: false,
206
+ reason: validation.errors.join('; '),
207
+ cwd,
208
+ },
209
+ ];
210
+ }
211
+
212
+ const timeoutMs =
213
+ input.timeout_ms ??
214
+ config.compileCheck?.timeoutMs ??
215
+ DEFAULT_TIMEOUT_MS;
216
+ const result = await spawnLocalProcess(
217
+ config.shell ?? (process.platform === 'win32' ? 'bash.exe' : 'bash'),
218
+ ['-lc', detection.command],
219
+ {
220
+ ...config,
221
+ timeoutMs,
222
+ maxOutputChars: config.maxOutputChars ?? 8000,
223
+ }
224
+ );
225
+
226
+ const passed =
227
+ result.exitCode === 0 && !result.timedOut;
228
+ const headline = passed
229
+ ? `compile_check (${detection.kind}) PASSED via \`${detection.command}\``
230
+ : `compile_check (${detection.kind}) FAILED via \`${detection.command}\` ` +
231
+ `(exit=${result.exitCode ?? 'unknown'}${result.timedOut ? ', timed_out=true' : ''})`;
232
+
233
+ let body = '';
234
+ if (result.stdout !== '') {
235
+ body += `\n\nstdout:\n${truncateLocalOutput(result.stdout, 4000)}`;
236
+ }
237
+ if (result.stderr !== '') {
238
+ body += `\n\nstderr:\n${truncateLocalOutput(result.stderr, 4000)}`;
239
+ }
240
+ if (result.fullOutputPath != null) {
241
+ body += `\n\nfull_output_path: ${result.fullOutputPath}`;
242
+ }
243
+ const summary = `${headline}${body}\n\nworking_directory: ${cwd}\nreason: ${detection.reason}`;
244
+
245
+ return [
246
+ summary,
247
+ {
248
+ kind: detection.kind,
249
+ ran: true,
250
+ passed,
251
+ exit_code: result.exitCode,
252
+ timed_out: result.timedOut,
253
+ command: detection.command,
254
+ cwd,
255
+ },
256
+ ];
257
+ },
258
+ {
259
+ name: CompileCheckToolName,
260
+ description:
261
+ 'Run the project\'s standard typecheck or lint pass and return its output. Auto-detects from project markers (tsconfig.json/package.json -> tsc; Cargo.toml -> cargo check; go.mod -> go vet; pyproject.toml -> mypy or py_compile). Pass `command` to override.',
262
+ schema: CompileCheckSchema,
263
+ responseFormat: Constants.CONTENT_AND_ARTIFACT,
264
+ }
265
+ );
266
+ }
267
+
268
+ export function createCompileCheckToolDefinition(): t.LCTool {
269
+ return {
270
+ name: CompileCheckToolName,
271
+ description:
272
+ 'Run the project\'s standard typecheck or lint pass and return its output.',
273
+ parameters: CompileCheckSchema,
274
+ allowed_callers: ['direct', 'code_execution'],
275
+ responseFormat: Constants.CONTENT_AND_ARTIFACT,
276
+ toolType: 'builtin',
277
+ };
278
+ }
@@ -0,0 +1,93 @@
1
+ import { dirname } from 'path';
2
+ import { nodeWorkspaceFS } from './workspaceFS';
3
+ import type { WorkspaceFS } from './workspaceFS';
4
+ import type * as t from '@/types';
5
+
6
+ type Snapshot =
7
+ | { kind: 'absent' }
8
+ | { kind: 'present'; content: Buffer };
9
+
10
+ /**
11
+ * Per-Run snapshot store for write_file / edit_file. Captures the
12
+ * pre-write byte content of every path the local engine is about to
13
+ * mutate so a later `rewind()` can restore the working tree to its
14
+ * original state. Notes:
15
+ *
16
+ * - Idempotent per path: subsequent captures preserve the first
17
+ * snapshot (so rewind always restores the *original* content).
18
+ * - Captures missing files as `{ kind: 'absent' }`; rewind deletes
19
+ * those paths so created files are removed.
20
+ * - In-memory: snapshots live for the lifetime of this instance and
21
+ * are not persisted across processes. Tie the lifetime to a Run.
22
+ * - Bounded by `maxBytesPerFile` (default 32 MiB) to bound memory.
23
+ * A file larger than the cap is recorded but not snapshotted; the
24
+ * rewind of that path is best-effort and the caller is told via
25
+ * the result count not to trust it.
26
+ */
27
+ export class LocalFileCheckpointerImpl implements t.LocalFileCheckpointer {
28
+ private snapshots = new Map<string, Snapshot>();
29
+ private oversizePaths = new Set<string>();
30
+
31
+ constructor(
32
+ private readonly maxBytesPerFile: number = 32 * 1024 * 1024,
33
+ private readonly fs: WorkspaceFS = nodeWorkspaceFS
34
+ ) {}
35
+
36
+ async captureBeforeWrite(absolutePath: string): Promise<void> {
37
+ if (this.snapshots.has(absolutePath) || this.oversizePaths.has(absolutePath)) {
38
+ return;
39
+ }
40
+ let info;
41
+ try {
42
+ info = await this.fs.stat(absolutePath);
43
+ } catch {
44
+ this.snapshots.set(absolutePath, { kind: 'absent' });
45
+ return;
46
+ }
47
+ if (!info.isFile()) {
48
+ return;
49
+ }
50
+ if (info.size > this.maxBytesPerFile) {
51
+ this.oversizePaths.add(absolutePath);
52
+ return;
53
+ }
54
+ const content = (await this.fs.readFile(absolutePath)) as Buffer;
55
+ this.snapshots.set(absolutePath, { kind: 'present', content });
56
+ }
57
+
58
+ async rewind(): Promise<number> {
59
+ let restored = 0;
60
+ for (const [path, snapshot] of this.snapshots.entries()) {
61
+ if (snapshot.kind === 'absent') {
62
+ await this.fs.unlink(path).catch(() => undefined);
63
+ restored++;
64
+ continue;
65
+ }
66
+ try {
67
+ await this.fs.mkdir(dirname(path), { recursive: true });
68
+ await this.fs.writeFile(path, snapshot.content);
69
+ restored++;
70
+ } catch {
71
+ // Best-effort: ignore individual restore failures so the rest
72
+ // of the rewind continues.
73
+ }
74
+ }
75
+ return restored;
76
+ }
77
+
78
+ capturedPaths(): string[] {
79
+ return [...this.snapshots.keys(), ...this.oversizePaths];
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Convenience factory so callers don't have to reach for the impl
85
+ * class directly. Accepts an optional `WorkspaceFS` so a host using a
86
+ * non-default engine (remote sandbox, in-memory test FS, etc.) can
87
+ * route the checkpointer through the same I/O.
88
+ */
89
+ export function createLocalFileCheckpointer(
90
+ options: { maxBytesPerFile?: number; fs?: WorkspaceFS } = {}
91
+ ): t.LocalFileCheckpointer {
92
+ return new LocalFileCheckpointerImpl(options.maxBytesPerFile, options.fs);
93
+ }