@molecule/api-ai-tools 1.0.6 → 1.0.7
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/tools.d.ts.map +1 -1
- package/dist/tools.js +121 -0
- package/package.json +1 -1
package/dist/tools.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAG9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAsBnE;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAG9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAsBnE;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAyyBxF"}
|
package/dist/tools.js
CHANGED
|
@@ -89,6 +89,102 @@ export function buildTools(backend, config) {
|
|
|
89
89
|
result = isEnvFilePath(path) ? redactSecrets(result) : redactSecretsInCode(result);
|
|
90
90
|
return result;
|
|
91
91
|
}
|
|
92
|
+
// ── Empty-read verification ────────────────────────────────────
|
|
93
|
+
/**
|
|
94
|
+
* The file's size in bytes as the backend's own shell reports it, or `null`
|
|
95
|
+
* when that could not be established (the probe failed, timed out, or printed
|
|
96
|
+
* something unparseable).
|
|
97
|
+
*
|
|
98
|
+
* `null` means "I could not look" and is deliberately DISTINCT from `0` ("I
|
|
99
|
+
* looked and the file is empty") — conflating the two is the very failure this
|
|
100
|
+
* probe exists to prevent. The last integer in stdout is taken rather than the
|
|
101
|
+
* whole string, so a consumer that wraps commands in environment sourcing
|
|
102
|
+
* cannot break the parse with stray output.
|
|
103
|
+
*
|
|
104
|
+
* @param path - The already-resolved, symlink-checked path.
|
|
105
|
+
* @returns The byte count, or null when it could not be determined.
|
|
106
|
+
*/
|
|
107
|
+
async function probeFileSize(path) {
|
|
108
|
+
try {
|
|
109
|
+
const result = await backend.run(`wc -c < ${shellQuote(path)}`, { timeout: 10_000 });
|
|
110
|
+
if (result.exitCode !== 0)
|
|
111
|
+
return null;
|
|
112
|
+
const match = /(\d+)\s*$/.exec(result.stdout);
|
|
113
|
+
if (!match)
|
|
114
|
+
return null;
|
|
115
|
+
const bytes = Number.parseInt(match[1], 10);
|
|
116
|
+
return Number.isFinite(bytes) ? bytes : null;
|
|
117
|
+
}
|
|
118
|
+
catch (_error) {
|
|
119
|
+
// The probe is the cross-check for an ALREADY-suspect read; if it cannot
|
|
120
|
+
// run we must report "unverified", never "empty" — returning 0 here would
|
|
121
|
+
// reintroduce exactly the conflation this function exists to remove.
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Decide what an EMPTY string from `backend.readFile` actually means.
|
|
127
|
+
*
|
|
128
|
+
* A backend read can come back empty without throwing: a sandbox exec whose
|
|
129
|
+
* output stream ends early still reports the process's own exit code, an HTTP
|
|
130
|
+
* file read can return a 200 with no body, and a concurrent writer can be
|
|
131
|
+
* observed mid-truncate. Observed in production on 2026-09-16 (X0 rehearsal
|
|
132
|
+
* 84): several `read_file` calls on two real components returned no content
|
|
133
|
+
* while an exec shell showed both files present at 3,617 and 25,746 bytes; a
|
|
134
|
+
* later read of the same paths succeeded, so the failure was transient.
|
|
135
|
+
*
|
|
136
|
+
* Returning `{ content: '' }` for that is indistinguishable from a successful
|
|
137
|
+
* read of an empty file — and a model that believes a file is empty writes it
|
|
138
|
+
* from scratch, destroying work it never saw. So an empty read is verified
|
|
139
|
+
* against the file's real size before it is reported as content: genuinely
|
|
140
|
+
* empty is returned as empty and SAID so; non-empty is retried once and then
|
|
141
|
+
* reported as a FAILED read; unverifiable is also a failure, never an empty
|
|
142
|
+
* file.
|
|
143
|
+
*
|
|
144
|
+
* @param path - The already-resolved, symlink-checked path.
|
|
145
|
+
* @param tool - The calling tool's name, for the error message.
|
|
146
|
+
* @returns The file's real content (possibly empty, with a note), or an error.
|
|
147
|
+
*/
|
|
148
|
+
async function classifyEmptyRead(path, tool) {
|
|
149
|
+
const bytes = await probeFileSize(path);
|
|
150
|
+
if (bytes === 0) {
|
|
151
|
+
return {
|
|
152
|
+
content: '',
|
|
153
|
+
note: `${path} exists and is EMPTY (0 bytes) — that is the file's real content, not a failed read.`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (bytes === null) {
|
|
157
|
+
return {
|
|
158
|
+
error: `${tool} obtained NO CONTENT for ${path}, and could not confirm the file's size ` +
|
|
159
|
+
`(the \`wc -c ${path}\` probe failed), so this may be a FAILED read rather than an ` +
|
|
160
|
+
`empty file. Do NOT write or edit ${path} from memory or from the plan — that would ` +
|
|
161
|
+
`overwrite a file you have never seen. Call read_file again, or run \`cat ${path}\` ` +
|
|
162
|
+
`with exec_command, and only proceed once you actually have its contents.`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
// Non-empty on disk: the read failed, and the observed failure is transient —
|
|
166
|
+
// so retry it once here rather than spending an executor turn on it.
|
|
167
|
+
try {
|
|
168
|
+
const retry = await backend.readFile(path);
|
|
169
|
+
if (typeof retry === 'string' && retry !== '') {
|
|
170
|
+
return {
|
|
171
|
+
content: retry,
|
|
172
|
+
note: `The first read of ${path} returned nothing; this content came from an immediate retry.`,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (_error) {
|
|
177
|
+
// The retry's own failure adds nothing to the message below — the size
|
|
178
|
+
// probe is the authoritative fact, and it already said the file has bytes.
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
error: `${tool} obtained NO CONTENT for ${path}, but the file is ${bytes} bytes on disk — the ` +
|
|
182
|
+
`read FAILED, this is not an empty file (a retry returned nothing either). Do NOT write ` +
|
|
183
|
+
`or edit ${path} from memory or from the plan — that would overwrite a file you have ` +
|
|
184
|
+
`never seen. Call read_file again, or run \`cat ${path}\` with exec_command, and only ` +
|
|
185
|
+
`proceed once you actually have its contents.`,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
92
188
|
// ── Diff computation ───────────────────────────────────────────
|
|
93
189
|
/**
|
|
94
190
|
* Compute a lightweight diff summary for telemetry and UI badges.
|
|
@@ -134,6 +230,22 @@ export function buildTools(backend, config) {
|
|
|
134
230
|
return { error: symlinkErr };
|
|
135
231
|
try {
|
|
136
232
|
const content = await backend.readFile(path);
|
|
233
|
+
// A read that produced no usable text is verified against the file's real
|
|
234
|
+
// size before it can be reported as content (see classifyEmptyRead). The
|
|
235
|
+
// non-string arm covers a backend whose transport handed back an empty
|
|
236
|
+
// body as `undefined`; `content.length` below would otherwise raise an
|
|
237
|
+
// unactionable "Cannot read properties of undefined".
|
|
238
|
+
if (typeof content !== 'string' || content === '') {
|
|
239
|
+
const verdict = await classifyEmptyRead(path, 'read_file');
|
|
240
|
+
if ('error' in verdict)
|
|
241
|
+
return verdict;
|
|
242
|
+
return {
|
|
243
|
+
path,
|
|
244
|
+
content: sanitizeFileContent(verdict.content, path),
|
|
245
|
+
...(verdict.content === '' ? { empty: true } : {}),
|
|
246
|
+
...(verdict.note ? { note: verdict.note } : {}),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
137
249
|
if (content.length > MAX_READ_SIZE)
|
|
138
250
|
return {
|
|
139
251
|
error: `File too large (${Math.round(content.length / 1024)}KB). Maximum is ${MAX_READ_SIZE / 1024 / 1024}MB.`,
|
|
@@ -251,6 +363,15 @@ export function buildTools(backend, config) {
|
|
|
251
363
|
return { error: symlinkErr };
|
|
252
364
|
try {
|
|
253
365
|
let content = await backend.readFile(path);
|
|
366
|
+
// A transiently-empty read here made every old_string "not found", which
|
|
367
|
+
// steers the model to re-read and then rewrite a file it never saw. Same
|
|
368
|
+
// verification as read_file: empty is only believed once it is confirmed.
|
|
369
|
+
if (typeof content !== 'string' || content === '') {
|
|
370
|
+
const verdict = await classifyEmptyRead(path, 'edit_file');
|
|
371
|
+
if ('error' in verdict)
|
|
372
|
+
return verdict;
|
|
373
|
+
content = verdict.content;
|
|
374
|
+
}
|
|
254
375
|
const oldContent = content;
|
|
255
376
|
for (const { old_string: oldString, new_string: newString } of replacements) {
|
|
256
377
|
// Validate each replacement is well-formed BEFORE touching content. A
|