@librechat/agents 3.1.77-dev.1 → 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 (188) 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/llm/openai/index.cjs +317 -1
  8. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  9. package/dist/cjs/main.cjs +90 -0
  10. package/dist/cjs/main.cjs.map +1 -1
  11. package/dist/cjs/messages/anthropicToolCache.cjs +102 -0
  12. package/dist/cjs/messages/anthropicToolCache.cjs.map +1 -0
  13. package/dist/cjs/messages/prune.cjs +27 -0
  14. package/dist/cjs/messages/prune.cjs.map +1 -1
  15. package/dist/cjs/messages/recency.cjs +99 -0
  16. package/dist/cjs/messages/recency.cjs.map +1 -0
  17. package/dist/cjs/run.cjs +30 -0
  18. package/dist/cjs/run.cjs.map +1 -1
  19. package/dist/cjs/summarization/node.cjs +100 -6
  20. package/dist/cjs/summarization/node.cjs.map +1 -1
  21. package/dist/cjs/tools/ToolNode.cjs +635 -23
  22. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  23. package/dist/cjs/tools/local/CompileCheckTool.cjs +227 -0
  24. package/dist/cjs/tools/local/CompileCheckTool.cjs.map +1 -0
  25. package/dist/cjs/tools/local/FileCheckpointer.cjs +90 -0
  26. package/dist/cjs/tools/local/FileCheckpointer.cjs.map +1 -0
  27. package/dist/cjs/tools/local/LocalCodingTools.cjs +1098 -0
  28. package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -0
  29. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +1042 -0
  30. package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -0
  31. package/dist/cjs/tools/local/LocalExecutionTools.cjs +122 -0
  32. package/dist/cjs/tools/local/LocalExecutionTools.cjs.map +1 -0
  33. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +453 -0
  34. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs.map +1 -0
  35. package/dist/cjs/tools/local/attachments.cjs +183 -0
  36. package/dist/cjs/tools/local/attachments.cjs.map +1 -0
  37. package/dist/cjs/tools/local/bashAst.cjs +129 -0
  38. package/dist/cjs/tools/local/bashAst.cjs.map +1 -0
  39. package/dist/cjs/tools/local/editStrategies.cjs +188 -0
  40. package/dist/cjs/tools/local/editStrategies.cjs.map +1 -0
  41. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs +141 -0
  42. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs.map +1 -0
  43. package/dist/cjs/tools/local/syntaxCheck.cjs +182 -0
  44. package/dist/cjs/tools/local/syntaxCheck.cjs.map +1 -0
  45. package/dist/cjs/tools/local/textEncoding.cjs +30 -0
  46. package/dist/cjs/tools/local/textEncoding.cjs.map +1 -0
  47. package/dist/cjs/tools/local/workspaceFS.cjs +51 -0
  48. package/dist/cjs/tools/local/workspaceFS.cjs.map +1 -0
  49. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -0
  50. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  51. package/dist/esm/common/enum.mjs +53 -1
  52. package/dist/esm/common/enum.mjs.map +1 -1
  53. package/dist/esm/graphs/Graph.mjs +149 -5
  54. package/dist/esm/graphs/Graph.mjs.map +1 -1
  55. package/dist/esm/hooks/createWorkspacePolicyHook.mjs +289 -0
  56. package/dist/esm/hooks/createWorkspacePolicyHook.mjs.map +1 -0
  57. package/dist/esm/llm/openai/index.mjs +318 -2
  58. package/dist/esm/llm/openai/index.mjs.map +1 -1
  59. package/dist/esm/main.mjs +17 -2
  60. package/dist/esm/main.mjs.map +1 -1
  61. package/dist/esm/messages/anthropicToolCache.mjs +99 -0
  62. package/dist/esm/messages/anthropicToolCache.mjs.map +1 -0
  63. package/dist/esm/messages/prune.mjs +26 -1
  64. package/dist/esm/messages/prune.mjs.map +1 -1
  65. package/dist/esm/messages/recency.mjs +97 -0
  66. package/dist/esm/messages/recency.mjs.map +1 -0
  67. package/dist/esm/run.mjs +30 -0
  68. package/dist/esm/run.mjs.map +1 -1
  69. package/dist/esm/summarization/node.mjs +100 -6
  70. package/dist/esm/summarization/node.mjs.map +1 -1
  71. package/dist/esm/tools/ToolNode.mjs +635 -23
  72. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  73. package/dist/esm/tools/local/CompileCheckTool.mjs +223 -0
  74. package/dist/esm/tools/local/CompileCheckTool.mjs.map +1 -0
  75. package/dist/esm/tools/local/FileCheckpointer.mjs +87 -0
  76. package/dist/esm/tools/local/FileCheckpointer.mjs.map +1 -0
  77. package/dist/esm/tools/local/LocalCodingTools.mjs +1075 -0
  78. package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -0
  79. package/dist/esm/tools/local/LocalExecutionEngine.mjs +1022 -0
  80. package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -0
  81. package/dist/esm/tools/local/LocalExecutionTools.mjs +117 -0
  82. package/dist/esm/tools/local/LocalExecutionTools.mjs.map +1 -0
  83. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +448 -0
  84. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs.map +1 -0
  85. package/dist/esm/tools/local/attachments.mjs +180 -0
  86. package/dist/esm/tools/local/attachments.mjs.map +1 -0
  87. package/dist/esm/tools/local/bashAst.mjs +126 -0
  88. package/dist/esm/tools/local/bashAst.mjs.map +1 -0
  89. package/dist/esm/tools/local/editStrategies.mjs +185 -0
  90. package/dist/esm/tools/local/editStrategies.mjs.map +1 -0
  91. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs +137 -0
  92. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs.map +1 -0
  93. package/dist/esm/tools/local/syntaxCheck.mjs +179 -0
  94. package/dist/esm/tools/local/syntaxCheck.mjs.map +1 -0
  95. package/dist/esm/tools/local/textEncoding.mjs +27 -0
  96. package/dist/esm/tools/local/textEncoding.mjs.map +1 -0
  97. package/dist/esm/tools/local/workspaceFS.mjs +49 -0
  98. package/dist/esm/tools/local/workspaceFS.mjs.map +1 -0
  99. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -0
  100. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  101. package/dist/types/common/enum.d.ts +39 -1
  102. package/dist/types/graphs/Graph.d.ts +34 -0
  103. package/dist/types/hooks/createWorkspacePolicyHook.d.ts +95 -0
  104. package/dist/types/hooks/index.d.ts +2 -0
  105. package/dist/types/index.d.ts +1 -0
  106. package/dist/types/llm/openai/index.d.ts +17 -0
  107. package/dist/types/messages/anthropicToolCache.d.ts +51 -0
  108. package/dist/types/messages/index.d.ts +2 -0
  109. package/dist/types/messages/prune.d.ts +11 -0
  110. package/dist/types/messages/recency.d.ts +64 -0
  111. package/dist/types/run.d.ts +21 -0
  112. package/dist/types/tools/ToolNode.d.ts +145 -2
  113. package/dist/types/tools/local/CompileCheckTool.d.ts +31 -0
  114. package/dist/types/tools/local/FileCheckpointer.d.ts +39 -0
  115. package/dist/types/tools/local/LocalCodingTools.d.ts +57 -0
  116. package/dist/types/tools/local/LocalExecutionEngine.d.ts +149 -0
  117. package/dist/types/tools/local/LocalExecutionTools.d.ts +9 -0
  118. package/dist/types/tools/local/LocalProgrammaticToolCalling.d.ts +21 -0
  119. package/dist/types/tools/local/attachments.d.ts +84 -0
  120. package/dist/types/tools/local/bashAst.d.ts +11 -0
  121. package/dist/types/tools/local/editStrategies.d.ts +28 -0
  122. package/dist/types/tools/local/index.d.ts +12 -0
  123. package/dist/types/tools/local/resolveLocalExecutionTools.d.ts +38 -0
  124. package/dist/types/tools/local/syntaxCheck.d.ts +42 -0
  125. package/dist/types/tools/local/textEncoding.d.ts +21 -0
  126. package/dist/types/tools/local/workspaceFS.d.ts +49 -0
  127. package/dist/types/types/hitl.d.ts +56 -27
  128. package/dist/types/types/run.d.ts +8 -1
  129. package/dist/types/types/summarize.d.ts +30 -0
  130. package/dist/types/types/tools.d.ts +341 -6
  131. package/package.json +21 -2
  132. package/src/common/enum.ts +54 -0
  133. package/src/graphs/Graph.ts +164 -6
  134. package/src/hooks/__tests__/compactHooks.test.ts +38 -2
  135. package/src/hooks/__tests__/createWorkspacePolicyHook.test.ts +393 -0
  136. package/src/hooks/createWorkspacePolicyHook.ts +355 -0
  137. package/src/hooks/index.ts +6 -0
  138. package/src/index.ts +1 -0
  139. package/src/llm/openai/deepseek.test.ts +479 -0
  140. package/src/llm/openai/index.ts +484 -1
  141. package/src/messages/__tests__/anthropicToolCache.test.ts +125 -0
  142. package/src/messages/__tests__/recency.test.ts +267 -0
  143. package/src/messages/anthropicToolCache.ts +116 -0
  144. package/src/messages/index.ts +2 -0
  145. package/src/messages/prune.ts +27 -1
  146. package/src/messages/recency.ts +155 -0
  147. package/src/run.ts +31 -0
  148. package/src/scripts/compare_pi_vs_ours.ts +840 -0
  149. package/src/scripts/local_engine.ts +166 -0
  150. package/src/scripts/local_engine_checkpointer.ts +205 -0
  151. package/src/scripts/local_engine_compile.ts +263 -0
  152. package/src/scripts/local_engine_hooks.ts +226 -0
  153. package/src/scripts/local_engine_image.ts +201 -0
  154. package/src/scripts/local_engine_ptc.ts +151 -0
  155. package/src/scripts/local_engine_workspace.ts +258 -0
  156. package/src/scripts/summarization-recency.ts +462 -0
  157. package/src/specs/prune.test.ts +39 -0
  158. package/src/summarization/__tests__/node.test.ts +499 -3
  159. package/src/summarization/node.ts +124 -7
  160. package/src/tools/ToolNode.ts +769 -20
  161. package/src/tools/__tests__/LocalExecutionTools.test.ts +2647 -0
  162. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +175 -0
  163. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +114 -0
  164. package/src/tools/__tests__/ToolNode.session.test.ts +84 -0
  165. package/src/tools/__tests__/directToolHITLResumeScope.test.ts +467 -0
  166. package/src/tools/__tests__/directToolHooks.test.ts +411 -0
  167. package/src/tools/__tests__/localToolNames.test.ts +73 -0
  168. package/src/tools/__tests__/workspaceSeam.test.ts +134 -0
  169. package/src/tools/local/CompileCheckTool.ts +278 -0
  170. package/src/tools/local/FileCheckpointer.ts +93 -0
  171. package/src/tools/local/LocalCodingTools.ts +1342 -0
  172. package/src/tools/local/LocalExecutionEngine.ts +1329 -0
  173. package/src/tools/local/LocalExecutionTools.ts +167 -0
  174. package/src/tools/local/LocalProgrammaticToolCalling.ts +594 -0
  175. package/src/tools/local/__tests__/FileCheckpointer.test.ts +120 -0
  176. package/src/tools/local/__tests__/editStrategies.test.ts +134 -0
  177. package/src/tools/local/attachments.ts +251 -0
  178. package/src/tools/local/bashAst.ts +151 -0
  179. package/src/tools/local/editStrategies.ts +188 -0
  180. package/src/tools/local/index.ts +12 -0
  181. package/src/tools/local/resolveLocalExecutionTools.ts +208 -0
  182. package/src/tools/local/syntaxCheck.ts +243 -0
  183. package/src/tools/local/textEncoding.ts +37 -0
  184. package/src/tools/local/workspaceFS.ts +89 -0
  185. package/src/types/hitl.ts +56 -27
  186. package/src/types/run.ts +12 -1
  187. package/src/types/summarize.ts +31 -0
  188. package/src/types/tools.ts +359 -7
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Single-occurrence string-replacement strategies for `edit_file`.
3
+ *
4
+ * The LLM frequently emits an `oldString` whose whitespace, indentation,
5
+ * or escape sequences are slightly off from the on-disk content. Rather
6
+ * than failing the call (which forces a re-read + retry round-trip),
7
+ * we walk a chain of progressively looser matchers, stopping at the
8
+ * first one that locates exactly one match. The matched on-disk slice
9
+ * is then literally replaced with `newString` — we never modify
10
+ * `newString`, only the search.
11
+ *
12
+ * Strategies are ordered from strict to lenient so we don't accidentally
13
+ * over-match a more specific pattern with a looser one. Inspired by
14
+ * opencode's nine-strategy chain (sst/opencode), trimmed to the four
15
+ * highest-yield strategies for a first cut. Add more (block-anchor +
16
+ * Levenshtein, escape-normalized, etc.) as needed.
17
+ */
18
+
19
+ export interface EditMatch {
20
+ /** Strategy name that produced the match, for telemetry/diagnostics. */
21
+ strategy: string;
22
+ /** Starting offset in the source. */
23
+ start: number;
24
+ /** Ending offset (exclusive). */
25
+ end: number;
26
+ }
27
+
28
+ export type EditStrategy = (
29
+ source: string,
30
+ oldString: string
31
+ ) => EditMatch | null;
32
+
33
+ const exactStrategy: EditStrategy = (source, oldString) => {
34
+ if (oldString === '') return null;
35
+ const first = source.indexOf(oldString);
36
+ if (first === -1) return null;
37
+ const second = source.indexOf(oldString, first + oldString.length);
38
+ if (second !== -1) return null;
39
+ return { strategy: 'exact', start: first, end: first + oldString.length };
40
+ };
41
+
42
+ /**
43
+ * Match per-line, ignoring trailing whitespace differences. Useful for
44
+ * the very common case where the LLM stripped trailing spaces or added
45
+ * an extra blank.
46
+ */
47
+ const lineTrimmedStrategy: EditStrategy = (source, oldString) => {
48
+ if (oldString === '') return null;
49
+ const sourceLines = source.split('\n');
50
+ const oldLines = oldString.split('\n');
51
+ if (oldLines.length === 0 || oldLines.length > sourceLines.length) {
52
+ return null;
53
+ }
54
+
55
+ let foundAt = -1;
56
+ for (let i = 0; i <= sourceLines.length - oldLines.length; i++) {
57
+ let ok = true;
58
+ for (let j = 0; j < oldLines.length; j++) {
59
+ if (sourceLines[i + j].trimEnd() !== oldLines[j].trimEnd()) {
60
+ ok = false;
61
+ break;
62
+ }
63
+ }
64
+ if (!ok) continue;
65
+ if (foundAt !== -1) return null; // multiple matches
66
+ foundAt = i;
67
+ }
68
+ if (foundAt === -1) return null;
69
+
70
+ let start = 0;
71
+ for (let i = 0; i < foundAt; i++) start += sourceLines[i].length + 1;
72
+ let end = start;
73
+ for (let i = 0; i < oldLines.length; i++) {
74
+ end += sourceLines[foundAt + i].length;
75
+ if (i < oldLines.length - 1) end += 1;
76
+ }
77
+ return { strategy: 'line-trimmed', start, end };
78
+ };
79
+
80
+ /**
81
+ * Collapse all runs of whitespace to a single space and match. Catches
82
+ * cases where the LLM normalised tabs to spaces or vice-versa.
83
+ */
84
+ const whitespaceNormalizedStrategy: EditStrategy = (source, oldString) => {
85
+ if (oldString === '') return null;
86
+ const norm = (s: string): string => s.replace(/\s+/g, ' ').trim();
87
+ const normalizedNeedle = norm(oldString);
88
+ if (normalizedNeedle === '') return null;
89
+
90
+ const sourceLines = source.split('\n');
91
+ const needleLines = oldString.split('\n');
92
+ if (needleLines.length > sourceLines.length) return null;
93
+
94
+ let foundAt = -1;
95
+ for (let i = 0; i <= sourceLines.length - needleLines.length; i++) {
96
+ const candidate = sourceLines
97
+ .slice(i, i + needleLines.length)
98
+ .join('\n');
99
+ if (norm(candidate) !== normalizedNeedle) continue;
100
+ if (foundAt !== -1) return null;
101
+ foundAt = i;
102
+ }
103
+ if (foundAt === -1) return null;
104
+
105
+ let start = 0;
106
+ for (let i = 0; i < foundAt; i++) start += sourceLines[i].length + 1;
107
+ let end = start;
108
+ for (let i = 0; i < needleLines.length; i++) {
109
+ end += sourceLines[foundAt + i].length;
110
+ if (i < needleLines.length - 1) end += 1;
111
+ }
112
+ return { strategy: 'whitespace-normalized', start, end };
113
+ };
114
+
115
+ /**
116
+ * Strip the common leading-indent from each line of the needle and
117
+ * each candidate window of the source. Catches the very common case
118
+ * where the LLM omitted the indentation it should have copied.
119
+ */
120
+ const indentationFlexibleStrategy: EditStrategy = (source, oldString) => {
121
+ if (oldString === '') return null;
122
+
123
+ const stripCommonIndent = (block: string): string => {
124
+ const lines = block.split('\n');
125
+ let common = Number.POSITIVE_INFINITY;
126
+ for (const line of lines) {
127
+ if (line.trim() === '') continue;
128
+ const m = /^(\s*)/.exec(line);
129
+ const indent = m ? m[1].length : 0;
130
+ if (indent < common) common = indent;
131
+ if (common === 0) break;
132
+ }
133
+ if (!Number.isFinite(common) || common === 0) return block;
134
+ return lines
135
+ .map((l) => (l.length >= common ? l.slice(common) : l))
136
+ .join('\n');
137
+ };
138
+
139
+ const normalizedNeedle = stripCommonIndent(oldString);
140
+ if (normalizedNeedle === '') return null;
141
+
142
+ const sourceLines = source.split('\n');
143
+ const needleLines = oldString.split('\n');
144
+ if (needleLines.length > sourceLines.length) return null;
145
+
146
+ let foundAt = -1;
147
+ for (let i = 0; i <= sourceLines.length - needleLines.length; i++) {
148
+ const window = sourceLines.slice(i, i + needleLines.length).join('\n');
149
+ if (stripCommonIndent(window) !== normalizedNeedle) continue;
150
+ if (foundAt !== -1) return null;
151
+ foundAt = i;
152
+ }
153
+ if (foundAt === -1) return null;
154
+
155
+ let start = 0;
156
+ for (let i = 0; i < foundAt; i++) start += sourceLines[i].length + 1;
157
+ let end = start;
158
+ for (let i = 0; i < needleLines.length; i++) {
159
+ end += sourceLines[foundAt + i].length;
160
+ if (i < needleLines.length - 1) end += 1;
161
+ }
162
+ return { strategy: 'indentation-flexible', start, end };
163
+ };
164
+
165
+ const STRATEGY_CHAIN: EditStrategy[] = [
166
+ exactStrategy,
167
+ lineTrimmedStrategy,
168
+ whitespaceNormalizedStrategy,
169
+ indentationFlexibleStrategy,
170
+ ];
171
+
172
+ export function locateEdit(source: string, oldString: string): EditMatch | null {
173
+ for (const strategy of STRATEGY_CHAIN) {
174
+ const match = strategy(source, oldString);
175
+ if (match != null) {
176
+ return match;
177
+ }
178
+ }
179
+ return null;
180
+ }
181
+
182
+ export function applyEdit(
183
+ source: string,
184
+ match: EditMatch,
185
+ newString: string
186
+ ): string {
187
+ return source.slice(0, match.start) + newString + source.slice(match.end);
188
+ }
@@ -0,0 +1,12 @@
1
+ export * from './CompileCheckTool';
2
+ export * from './FileCheckpointer';
3
+ export * from './LocalCodingTools';
4
+ export * from './LocalExecutionEngine';
5
+ export * from './LocalExecutionTools';
6
+ export * from './LocalProgrammaticToolCalling';
7
+ export * from './resolveLocalExecutionTools';
8
+ export * from './attachments';
9
+ export * from './bashAst';
10
+ export * from './editStrategies';
11
+ export * from './syntaxCheck';
12
+ export * from './textEncoding';
@@ -0,0 +1,208 @@
1
+ import { Constants, CODE_EXECUTION_TOOLS } from '@/common';
2
+ import {
3
+ createLocalBashExecutionTool,
4
+ createLocalCodeExecutionTool,
5
+ } from './LocalExecutionTools';
6
+ import {
7
+ createLocalCodingToolBundle,
8
+ createLocalCodingToolDefinitions,
9
+ createLocalCodingTools,
10
+ } from './LocalCodingTools';
11
+ import {
12
+ createLocalBashProgrammaticToolCallingTool,
13
+ createLocalProgrammaticToolCallingTool,
14
+ } from './LocalProgrammaticToolCalling';
15
+ import type * as t from '@/types';
16
+
17
+ type ResolveLocalToolsResult = {
18
+ toolMap: t.ToolMap;
19
+ directToolNames: Set<string>;
20
+ /**
21
+ * Set when `local.fileCheckpointing === true` AND the auto-bind
22
+ * coding suite is in use. ToolNode stashes this on the node and
23
+ * exposes it via `getFileCheckpointer()` so the host can call
24
+ * `rewind()` after a failed batch. Manual review (finding E)
25
+ * flagged that the config flag was previously a no-op in the
26
+ * Run/ToolNode auto-bind path — only direct
27
+ * `createLocalCodingToolBundle()` callers could access the
28
+ * checkpointer.
29
+ */
30
+ fileCheckpointer?: t.LocalFileCheckpointer;
31
+ };
32
+
33
+ function shouldUseLocalExecution(config?: t.ToolExecutionConfig): boolean {
34
+ return config?.engine === 'local';
35
+ }
36
+
37
+ function shouldIncludeCodingTools(config?: t.ToolExecutionConfig): boolean {
38
+ return (
39
+ shouldUseLocalExecution(config) &&
40
+ config?.local?.includeCodingTools !== false
41
+ );
42
+ }
43
+
44
+ function createLocalExecutionTool(
45
+ name: string,
46
+ config: t.LocalExecutionConfig
47
+ ): t.GenericTool | undefined {
48
+ switch (name) {
49
+ case Constants.EXECUTE_CODE:
50
+ return createLocalCodeExecutionTool(config);
51
+ case Constants.BASH_TOOL:
52
+ return createLocalBashExecutionTool({ config });
53
+ case Constants.PROGRAMMATIC_TOOL_CALLING:
54
+ return createLocalProgrammaticToolCallingTool(config);
55
+ case Constants.BASH_PROGRAMMATIC_TOOL_CALLING:
56
+ return createLocalBashProgrammaticToolCallingTool(config);
57
+ default:
58
+ return undefined;
59
+ }
60
+ }
61
+
62
+ function mergeToolsByName(
63
+ baseTools: t.GraphTools | undefined,
64
+ localTools: t.GenericTool[]
65
+ ): t.GraphTools {
66
+ const orderedTools: t.GenericTool[] = [];
67
+ const indexByName = new Map<string, number>();
68
+
69
+ for (const tool of (baseTools as t.GenericTool[] | undefined) ?? []) {
70
+ if ('name' in tool && typeof tool.name === 'string') {
71
+ indexByName.set(tool.name, orderedTools.length);
72
+ }
73
+ orderedTools.push(tool);
74
+ }
75
+
76
+ for (const tool of localTools) {
77
+ const existingIndex = indexByName.get(tool.name);
78
+ if (existingIndex == null) {
79
+ indexByName.set(tool.name, orderedTools.length);
80
+ orderedTools.push(tool);
81
+ continue;
82
+ }
83
+ orderedTools[existingIndex] = tool;
84
+ }
85
+
86
+ return orderedTools;
87
+ }
88
+
89
+ export function resolveLocalToolsForBinding(args: {
90
+ tools?: t.GraphTools;
91
+ toolExecution?: t.ToolExecutionConfig;
92
+ }): t.GraphTools | undefined {
93
+ if (!shouldUseLocalExecution(args.toolExecution)) {
94
+ return args.tools;
95
+ }
96
+
97
+ const localConfig = args.toolExecution?.local ?? {};
98
+ if (shouldIncludeCodingTools(args.toolExecution)) {
99
+ return mergeToolsByName(args.tools, createLocalCodingTools(localConfig));
100
+ }
101
+
102
+ const replacements = ((args.tools as t.GenericTool[] | undefined) ?? [])
103
+ .filter(
104
+ (existingTool): existingTool is t.GenericTool & { name: string } =>
105
+ 'name' in existingTool &&
106
+ typeof existingTool.name === 'string' &&
107
+ CODE_EXECUTION_TOOLS.has(existingTool.name)
108
+ )
109
+ .map((existingTool) =>
110
+ createLocalExecutionTool(existingTool.name, localConfig)
111
+ )
112
+ .filter((localTool): localTool is t.GenericTool => localTool != null);
113
+
114
+ return replacements.length === 0
115
+ ? args.tools
116
+ : mergeToolsByName(args.tools, replacements);
117
+ }
118
+
119
+ export function resolveLocalToolRegistry(args: {
120
+ toolRegistry?: t.LCToolRegistry;
121
+ toolExecution?: t.ToolExecutionConfig;
122
+ }): t.LCToolRegistry | undefined {
123
+ if (!shouldIncludeCodingTools(args.toolExecution)) {
124
+ return args.toolRegistry;
125
+ }
126
+
127
+ const registry = new Map(args.toolRegistry ?? []);
128
+ for (const definition of createLocalCodingToolDefinitions()) {
129
+ registry.set(definition.name, definition);
130
+ }
131
+ return registry;
132
+ }
133
+
134
+ export function resolveLocalExecutionTools(args: {
135
+ toolMap: t.ToolMap;
136
+ toolExecution?: t.ToolExecutionConfig;
137
+ /**
138
+ * Caller-provided checkpointer that overrides the bundle's
139
+ * auto-created one. The Graph layer threads a single per-Run
140
+ * instance so every ToolNode it compiles shares one snapshot
141
+ * store — without that, a multi-agent graph would each get a
142
+ * private checkpointer and `Run.rewindFiles()` couldn't reach
143
+ * any of them.
144
+ */
145
+ fileCheckpointer?: t.LocalFileCheckpointer;
146
+ }): ResolveLocalToolsResult {
147
+ const directToolNames = new Set<string>();
148
+ if (!shouldUseLocalExecution(args.toolExecution)) {
149
+ return {
150
+ toolMap: args.toolMap,
151
+ directToolNames,
152
+ };
153
+ }
154
+
155
+ const localConfig = args.toolExecution?.local ?? {};
156
+ const toolMap = new Map(args.toolMap);
157
+ let fileCheckpointer: t.LocalFileCheckpointer | undefined;
158
+
159
+ if (shouldIncludeCodingTools(args.toolExecution)) {
160
+ // Use the bundle factory when fileCheckpointing is on so we can
161
+ // surface the checkpointer back to the caller — without this, the
162
+ // execution-path tools each captured into a checkpointer that was
163
+ // immediately discarded, making the public `fileCheckpointing`
164
+ // config flag a silent no-op outside of direct
165
+ // `createLocalCodingToolBundle()` use.
166
+ if (localConfig.fileCheckpointing === true || args.fileCheckpointer != null) {
167
+ const bundle = createLocalCodingToolBundle(localConfig, {
168
+ checkpointer: args.fileCheckpointer,
169
+ });
170
+ fileCheckpointer = bundle.checkpointer;
171
+ for (const localTool of bundle.tools) {
172
+ toolMap.set(localTool.name, localTool);
173
+ directToolNames.add(localTool.name);
174
+ }
175
+ } else {
176
+ for (const localTool of createLocalCodingTools(localConfig)) {
177
+ toolMap.set(localTool.name, localTool);
178
+ directToolNames.add(localTool.name);
179
+ }
180
+ }
181
+ }
182
+
183
+ // When the coding-tool bundle was already installed above, it
184
+ // already created `bash_tool` / `execute_code` / programmatic-tool
185
+ // variants. Skip re-creating them here — the audit-of-audit (manual
186
+ // finding #4) flagged that the original loop overwrote those bundle
187
+ // instances with fresh ones via `createLocalExecutionTool`, wasting
188
+ // work and (more importantly) replacing tools the bundle had
189
+ // already wired up with shared state. The CODE_EXECUTION_TOOLS
190
+ // loop is now only relevant when the host pre-bound a tool with
191
+ // one of these names (the `toolMap.has(name)` branch) and coding
192
+ // tools are off.
193
+ const includeCodingTools = shouldIncludeCodingTools(args.toolExecution);
194
+ for (const name of CODE_EXECUTION_TOOLS) {
195
+ if (includeCodingTools) continue;
196
+ if (!toolMap.has(name)) continue;
197
+
198
+ const localTool = createLocalExecutionTool(name, localConfig);
199
+ if (localTool == null) {
200
+ continue;
201
+ }
202
+
203
+ toolMap.set(name, localTool);
204
+ directToolNames.add(name);
205
+ }
206
+
207
+ return { toolMap, directToolNames, fileCheckpointer };
208
+ }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Per-file syntax check used by `edit_file` / `write_file` to surface
3
+ * obvious errors immediately after the write — strictly cheaper than
4
+ * full LSP integration and catches the bulk of "you broke the file"
5
+ * regressions a vision-less agent loop would otherwise miss until
6
+ * the next call.
7
+ *
8
+ * Each checker is a tiny shell-out (or in-process function) keyed on
9
+ * file extension. Failures are returned as a single short message;
10
+ * the wiring layer decides whether to append it to the tool result
11
+ * advisorily (`auto`) or to throw and force the model to react
12
+ * (`strict`).
13
+ *
14
+ * We deliberately do NOT cover TypeScript here because per-file `tsc`
15
+ * is slow and per-file syntax (without type info) misses most TS
16
+ * errors anyway. Use the project-level `compile_check` tool for that.
17
+ */
18
+
19
+ import { extname } from 'path';
20
+ import type * as t from '@/types';
21
+ import {
22
+ getSpawn,
23
+ getWorkspaceFS,
24
+ spawnLocalProcess,
25
+ } from './LocalExecutionEngine';
26
+
27
+ export type SyntaxCheckOutcome =
28
+ | { ok: true }
29
+ | { ok: false; checker: string; output: string };
30
+
31
+ export type SyntaxChecker = (
32
+ path: string,
33
+ config: t.LocalExecutionConfig
34
+ ) => Promise<SyntaxCheckOutcome>;
35
+
36
+ /**
37
+ * Per-backend availability cache for the post-edit syntax-check probe
38
+ * tools (node, python3, bash). Keyed on the *effective spawn backend*
39
+ * — see `getSpawn(config)` in LocalExecutionEngine — so a Run that
40
+ * probes node over Node's child_process can't poison a subsequent Run
41
+ * whose `local.exec.spawn` routes elsewhere (a remote sandbox might
42
+ * have python but not node, etc.).
43
+ *
44
+ * Mirrors the same fix that landed for the ripgrep cache in
45
+ * `LocalCodingTools.ts` after the first round of Codex review.
46
+ * WeakMap keying lets disposed backends GC their entry; the test
47
+ * reset hook re-creates the map.
48
+ */
49
+ type ProbeKind = 'hasNode' | 'hasPython' | 'hasBash';
50
+ type ProbeCache = Partial<Record<ProbeKind, Promise<boolean>>>;
51
+
52
+ // Per-backend × per-env cache. Codex P2 #40 — keying by spawn
53
+ // backend alone misses env-driven availability changes (e.g. PATH
54
+ // loses node between Runs that share the same backend). Same fix
55
+ // shape as the ripgrep cache (Codex P1 #34).
56
+ let probeCacheByBackend = new WeakMap<
57
+ t.LocalSpawn,
58
+ Map<string, ProbeCache>
59
+ >();
60
+
61
+ function envCacheKey(env: NodeJS.ProcessEnv | undefined): string {
62
+ if (env == null) return '';
63
+ const sorted: Record<string, string | undefined> = {};
64
+ for (const k of Object.keys(env).sort()) {
65
+ sorted[k] = env[k];
66
+ }
67
+ return JSON.stringify(sorted);
68
+ }
69
+
70
+ function cacheFor(
71
+ config: t.LocalExecutionConfig
72
+ ): ProbeCache {
73
+ const backend = getSpawn(config);
74
+ let envMap = probeCacheByBackend.get(backend);
75
+ if (envMap == null) {
76
+ envMap = new Map();
77
+ probeCacheByBackend.set(backend, envMap);
78
+ }
79
+ const envKey = envCacheKey(config.env);
80
+ let entry = envMap.get(envKey);
81
+ if (entry == null) {
82
+ entry = {};
83
+ envMap.set(envKey, entry);
84
+ }
85
+ return entry;
86
+ }
87
+
88
+ async function probe(
89
+ command: string,
90
+ args: string[],
91
+ cached: ProbeKind,
92
+ config: t.LocalExecutionConfig
93
+ ): Promise<boolean> {
94
+ const entry = cacheFor(config);
95
+ let probePromise = entry[cached];
96
+ if (probePromise == null) {
97
+ probePromise = spawnLocalProcess(
98
+ command,
99
+ args,
100
+ { ...config, timeoutMs: 5000, sandbox: { enabled: false } },
101
+ { internal: true }
102
+ )
103
+ .then((result) => result != null && result.exitCode === 0)
104
+ .catch(() => false);
105
+ entry[cached] = probePromise;
106
+ }
107
+ return probePromise;
108
+ }
109
+
110
+ /**
111
+ * Test-only reset hook. Clears the per-backend probe cache so tests
112
+ * can swap in mocked spawn backends and reprobe deterministically.
113
+ *
114
+ * @internal Not part of the public SDK surface.
115
+ */
116
+ export function _resetSyntaxCheckProbeCacheForTests(): void {
117
+ probeCacheByBackend = new WeakMap();
118
+ }
119
+
120
+ const jsCheck: SyntaxChecker = async (path, config) => {
121
+ if (!(await probe('node', ['--version'], 'hasNode', config))) {
122
+ return { ok: true };
123
+ }
124
+ const result = await spawnLocalProcess(
125
+ 'node',
126
+ ['--check', path],
127
+ { ...config, timeoutMs: 5000, sandbox: { enabled: false } },
128
+ { internal: true }
129
+ );
130
+ if (result.exitCode === 0) return { ok: true };
131
+ return {
132
+ ok: false,
133
+ checker: 'node --check',
134
+ output: result.stderr.trim() || result.stdout.trim() || 'syntax error',
135
+ };
136
+ };
137
+
138
+ const pythonCheck: SyntaxChecker = async (path, config) => {
139
+ if (!(await probe('python3', ['--version'], 'hasPython', config))) {
140
+ return { ok: true };
141
+ }
142
+ const program =
143
+ 'import py_compile, sys\n' +
144
+ 'try:\n' +
145
+ ' py_compile.compile(sys.argv[1], doraise=True)\n' +
146
+ 'except py_compile.PyCompileError as e:\n' +
147
+ ' print(e.msg.strip(), file=sys.stderr)\n' +
148
+ ' sys.exit(1)\n';
149
+ const result = await spawnLocalProcess(
150
+ 'python3',
151
+ ['-c', program, path],
152
+ { ...config, timeoutMs: 5000, sandbox: { enabled: false } },
153
+ { internal: true }
154
+ );
155
+ if (result.exitCode === 0) return { ok: true };
156
+ return {
157
+ ok: false,
158
+ checker: 'py_compile',
159
+ output: result.stderr.trim() || result.stdout.trim() || 'syntax error',
160
+ };
161
+ };
162
+
163
+ const jsonCheck: SyntaxChecker = async (path, config) => {
164
+ // Route through the configured WorkspaceFS so a Run with a custom
165
+ // `local.exec.fs` (in-memory or remote engine) validates the same
166
+ // file the write_file/edit_file path actually wrote — pre-fix this
167
+ // read went to the host fs and either silently passed (no host
168
+ // file → catch returns undefined → ok: true) or read a different
169
+ // file with the same absolute path. Codex P1 #24.
170
+ const fs = getWorkspaceFS(config);
171
+ const raw = await fs.readFile(path, 'utf8').catch(() => undefined);
172
+ if (raw == null) return { ok: true };
173
+ try {
174
+ JSON.parse(raw);
175
+ return { ok: true };
176
+ } catch (err) {
177
+ return {
178
+ ok: false,
179
+ checker: 'JSON.parse',
180
+ output: (err as Error).message,
181
+ };
182
+ }
183
+ };
184
+
185
+ const bashCheck: SyntaxChecker = async (path, config) => {
186
+ if (!(await probe('bash', ['--version'], 'hasBash', config))) {
187
+ return { ok: true };
188
+ }
189
+ const result = await spawnLocalProcess(
190
+ 'bash',
191
+ ['-n', path],
192
+ { ...config, timeoutMs: 5000, sandbox: { enabled: false } },
193
+ { internal: true }
194
+ );
195
+ if (result.exitCode === 0) return { ok: true };
196
+ return {
197
+ ok: false,
198
+ checker: 'bash -n',
199
+ output: result.stderr.trim() || result.stdout.trim() || 'syntax error',
200
+ };
201
+ };
202
+
203
+ const CHECKERS_BY_EXT: Record<string, SyntaxChecker> = {
204
+ '.js': jsCheck,
205
+ '.mjs': jsCheck,
206
+ '.cjs': jsCheck,
207
+ '.jsx': jsCheck,
208
+ '.py': pythonCheck,
209
+ '.pyw': pythonCheck,
210
+ '.json': jsonCheck,
211
+ '.sh': bashCheck,
212
+ '.bash': bashCheck,
213
+ };
214
+
215
+ /**
216
+ * Run the post-edit syntax check for `absolutePath`. Returns
217
+ * `null` when no checker matches the extension (most files), or a
218
+ * `SyntaxCheckOutcome`.
219
+ *
220
+ * Truncates `output` to `maxOutputChars` (default 4096) so a
221
+ * 10MB-of-errors transpiler dump can't blow the model context.
222
+ */
223
+ export async function runPostEditSyntaxCheck(
224
+ absolutePath: string,
225
+ config: t.LocalExecutionConfig
226
+ ): Promise<SyntaxCheckOutcome | null> {
227
+ const ext = extname(absolutePath).toLowerCase();
228
+ const checker = (CHECKERS_BY_EXT as Record<string, SyntaxChecker | undefined>)[ext];
229
+ if (checker == null) return null;
230
+ try {
231
+ const result = await checker(absolutePath, config);
232
+ if (!result.ok) {
233
+ return {
234
+ ok: false,
235
+ checker: result.checker,
236
+ output: result.output.slice(0, 4096),
237
+ };
238
+ }
239
+ return result;
240
+ } catch {
241
+ return null;
242
+ }
243
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * BOM and line-ending preservation helpers for the local engine's
3
+ * file-mutating tools. We never *introduce* a BOM or change line
4
+ * endings — only preserve what was already on disk so a Windows-
5
+ * checked-in source file stays CRLF and a UTF-8-with-BOM JSON file
6
+ * keeps its BOM after an edit.
7
+ *
8
+ * Inspired by opencode's `Bom` helper. Trimmed to the cases that
9
+ * actually matter for editing source code (UTF-8 BOM only;
10
+ * UTF-16/UTF-32 are out of scope).
11
+ */
12
+
13
+ const UTF8_BOM = '';
14
+
15
+ export interface EncodedFile {
16
+ /** File contents with BOM stripped. */
17
+ text: string;
18
+ /** Whether the on-disk file started with a UTF-8 BOM. */
19
+ hasBom: boolean;
20
+ /** Detected newline style. CRLF wins if any CRLF is present. */
21
+ newline: '\n' | '\r\n';
22
+ }
23
+
24
+ export function decodeFile(raw: string): EncodedFile {
25
+ const hasBom = raw.startsWith(UTF8_BOM);
26
+ const stripped = hasBom ? raw.slice(1) : raw;
27
+ const newline = stripped.includes('\r\n') ? '\r\n' : '\n';
28
+ // Internally we always work in LF; encode() restores CRLF on write.
29
+ const lf = newline === '\r\n' ? stripped.replace(/\r\n/g, '\n') : stripped;
30
+ return { text: lf, hasBom, newline };
31
+ }
32
+
33
+ export function encodeFile(text: string, encoding: EncodedFile): string {
34
+ const out =
35
+ encoding.newline === '\r\n' ? text.replace(/\n/g, '\r\n') : text;
36
+ return encoding.hasBom ? `${UTF8_BOM}${out}` : out;
37
+ }