@vybestack/llxprt-code-tools 0.10.0 → 0.11.0-nightly.260728.ffaa8d5d7
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/src/index.d.ts +3 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/interfaces/IShellToolHost.d.ts +2 -1
- package/dist/src/tools/ast-edit/ast-edit-invocation.d.ts +3 -2
- package/dist/src/tools/ast-edit/ast-edit-invocation.js +38 -11
- package/dist/src/tools/ast-edit/ast-edit-invocation.js.map +1 -1
- package/dist/src/tools/ast-edit/ast-read-file-invocation.d.ts +2 -2
- package/dist/src/tools/ast-edit/ast-read-file-invocation.js.map +1 -1
- package/dist/src/tools/ast-edit/edit-calculator.d.ts +6 -3
- package/dist/src/tools/ast-edit/edit-calculator.js +24 -6
- package/dist/src/tools/ast-edit/edit-calculator.js.map +1 -1
- package/dist/src/tools/ast-edit/validation-categorizer.d.ts +58 -0
- package/dist/src/tools/ast-edit/validation-categorizer.js +207 -0
- package/dist/src/tools/ast-edit/validation-categorizer.js.map +1 -0
- package/dist/src/tools/codesearch.js.map +1 -1
- package/dist/src/tools/direct-web-fetch.js.map +1 -1
- package/dist/src/tools/exa-web-search.js.map +1 -1
- package/dist/src/tools/shell.d.ts +2 -2
- package/dist/src/tools/shell.js +17 -3
- package/dist/src/tools/shell.js.map +1 -1
- package/dist/src/tools/todo-pause.d.ts +2 -2
- package/dist/src/tools/todo-pause.js +1 -1
- package/dist/src/tools/todo-pause.js.map +1 -1
- package/dist/src/tools/todo-read.d.ts +2 -2
- package/dist/src/tools/todo-read.js +1 -1
- package/dist/src/tools/todo-read.js.map +1 -1
- package/dist/src/tools/todo-write.d.ts +2 -2
- package/dist/src/tools/todo-write.js +1 -1
- package/dist/src/tools/todo-write.js.map +1 -1
- package/dist/src/tools/tool-registry.d.ts +3 -3
- package/dist/src/tools/tool-registry.js +6 -43
- package/dist/src/tools/tool-registry.js.map +1 -1
- package/dist/src/tools/tools.d.ts +6 -5
- package/dist/src/tools/tools.js.map +1 -1
- package/dist/src/utils/fileUtils.js +8 -1
- package/dist/src/utils/fileUtils.js.map +1 -1
- package/dist/src/utils/terminalSerializer.d.ts +12 -0
- package/package.json +151 -1
- package/src/index.ts +11 -0
- package/src/interfaces/IShellToolHost.ts +3 -1
- package/src/tools/ast-edit/ast-edit-invocation.ts +82 -12
- package/src/tools/ast-edit/ast-read-file-invocation.ts +2 -2
- package/src/tools/ast-edit/edit-calculator.ts +26 -6
- package/src/tools/ast-edit/validation-categorizer.ts +263 -0
- package/src/tools/codesearch.ts +2 -1
- package/src/tools/direct-web-fetch.ts +2 -1
- package/src/tools/exa-web-search.ts +2 -1
- package/src/tools/shell.ts +19 -6
- package/src/tools/todo-pause.ts +7 -2
- package/src/tools/todo-read.ts +7 -2
- package/src/tools/todo-write.ts +7 -2
- package/src/tools/tool-registry.ts +16 -82
- package/src/tools/tools.ts +10 -6
- package/src/utils/fileUtils.ts +11 -1
- package/src/utils/terminalSerializer.ts +9 -0
package/src/index.ts
CHANGED
|
@@ -236,6 +236,7 @@ export type {
|
|
|
236
236
|
ToolInfoConfirmationDetails,
|
|
237
237
|
ToolCallConfirmationDetails,
|
|
238
238
|
ToolLocation,
|
|
239
|
+
LiveOutputUpdate,
|
|
239
240
|
} from './tools/tools.js';
|
|
240
241
|
|
|
241
242
|
export {
|
|
@@ -294,6 +295,7 @@ export {
|
|
|
294
295
|
OUTPUT_UPDATE_INTERVAL_MS,
|
|
295
296
|
type ShellToolParams,
|
|
296
297
|
} from './tools/shell.js';
|
|
298
|
+
export { buildCommandToExecute } from './tools/shell-helpers.js';
|
|
297
299
|
export {
|
|
298
300
|
DISCOVERED_TOOL_PREFIX,
|
|
299
301
|
ToolRegistry,
|
|
@@ -428,6 +430,15 @@ export { ASTConfig } from './tools/ast-edit/ast-config.js';
|
|
|
428
430
|
export { ASTQueryExtractor } from './tools/ast-edit/ast-query-extractor.js';
|
|
429
431
|
export { RepositoryContextProvider } from './tools/ast-edit/repository-context-provider.js';
|
|
430
432
|
export { validateASTSyntax } from './tools/ast-edit/edit-calculator.js';
|
|
433
|
+
export {
|
|
434
|
+
summarizeAstValidation,
|
|
435
|
+
computeLineDelta,
|
|
436
|
+
findEditStartLine,
|
|
437
|
+
extractErrorLineNumber,
|
|
438
|
+
formatValidationLineLabel,
|
|
439
|
+
type AstValidationResult,
|
|
440
|
+
type AstValidationSummary,
|
|
441
|
+
} from './tools/ast-edit/validation-categorizer.js';
|
|
431
442
|
export {
|
|
432
443
|
detectLanguage,
|
|
433
444
|
extractImports,
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* SPDX-License-Identifier: Apache-2.0
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import type { AnsiOutput } from '../utils/terminalSerializer.js';
|
|
13
|
+
|
|
12
14
|
/**
|
|
13
15
|
* Tools-owned interface for shell tool host dependencies.
|
|
14
16
|
*
|
|
@@ -210,6 +212,6 @@ export interface IShellToolHost {
|
|
|
210
212
|
*/
|
|
211
213
|
export interface ShellOutputEvent {
|
|
212
214
|
type: 'data' | 'binary_detected' | 'binary_progress';
|
|
213
|
-
chunk?: string;
|
|
215
|
+
chunk?: string | AnsiOutput;
|
|
214
216
|
bytesReceived?: number;
|
|
215
217
|
}
|
|
@@ -32,7 +32,7 @@ import { toIdeConnectionStatus } from '../edit-utils.js';
|
|
|
32
32
|
import { hasLspCap } from '../../interfaces/host-capabilities.js';
|
|
33
33
|
import { DEFAULT_CREATE_PATCH_OPTIONS } from '../../utils/diffOptions.js';
|
|
34
34
|
import { collectLspDiagnosticsBlock } from '../../utils/lsp-diagnostics-helper.js';
|
|
35
|
-
import type {
|
|
35
|
+
import type { LiveOutputUpdate } from '../../utils/terminalSerializer.js';
|
|
36
36
|
import { ensureParentDirectoriesExist } from '../../utils/ensure-dirs.js';
|
|
37
37
|
|
|
38
38
|
import type { ASTEditToolParams } from './types.js';
|
|
@@ -44,6 +44,13 @@ import {
|
|
|
44
44
|
getFileLastModified,
|
|
45
45
|
type CalculatedEdit,
|
|
46
46
|
} from './edit-calculator.js';
|
|
47
|
+
import {
|
|
48
|
+
summarizeAstValidation,
|
|
49
|
+
computeLineDelta,
|
|
50
|
+
findEditStartLine,
|
|
51
|
+
formatValidationLineLabel,
|
|
52
|
+
type AstValidationResult,
|
|
53
|
+
} from './validation-categorizer.js';
|
|
47
54
|
|
|
48
55
|
function normalizeSeverity(severity: unknown): string {
|
|
49
56
|
if (typeof severity !== 'number') {
|
|
@@ -144,6 +151,7 @@ export class ASTEditToolInvocation
|
|
|
144
151
|
ideConfirmation,
|
|
145
152
|
metadata: {
|
|
146
153
|
astValidation: editData.astValidation,
|
|
154
|
+
preEditValidation: editData.preEditValidation,
|
|
147
155
|
fileFreshness: editData.fileFreshness,
|
|
148
156
|
},
|
|
149
157
|
};
|
|
@@ -178,7 +186,7 @@ export class ASTEditToolInvocation
|
|
|
178
186
|
|
|
179
187
|
async execute(
|
|
180
188
|
signal: AbortSignal,
|
|
181
|
-
_updateOutput?: (
|
|
189
|
+
_updateOutput?: (update: LiveOutputUpdate) => void,
|
|
182
190
|
_terminalColumns?: number,
|
|
183
191
|
_terminalRows?: number,
|
|
184
192
|
_setPidCallback?: (pid: number) => void,
|
|
@@ -217,9 +225,8 @@ export class ASTEditToolInvocation
|
|
|
217
225
|
workspaceRoot,
|
|
218
226
|
);
|
|
219
227
|
|
|
220
|
-
const astValidation =
|
|
221
|
-
editData
|
|
222
|
-
this.validateASTSyntax(this.params.file_path, editData.newContent);
|
|
228
|
+
const { astValidation, preEditValidation, lineDelta, editStartLine } =
|
|
229
|
+
this.computeValidationContext(editData);
|
|
223
230
|
|
|
224
231
|
const fileName = path.basename(this.params.file_path);
|
|
225
232
|
const fileDiff = Diff.createPatch(
|
|
@@ -234,6 +241,9 @@ export class ASTEditToolInvocation
|
|
|
234
241
|
const editPreviewLlmContent = this.buildPreviewLlmContent(
|
|
235
242
|
enhancedContext,
|
|
236
243
|
astValidation,
|
|
244
|
+
preEditValidation,
|
|
245
|
+
lineDelta,
|
|
246
|
+
editStartLine,
|
|
237
247
|
currentMtime,
|
|
238
248
|
workspaceRoot,
|
|
239
249
|
);
|
|
@@ -243,7 +253,7 @@ export class ASTEditToolInvocation
|
|
|
243
253
|
fileName,
|
|
244
254
|
originalContent: currentContent,
|
|
245
255
|
newContent: editData.newContent,
|
|
246
|
-
metadata: { astValidation, currentMtime },
|
|
256
|
+
metadata: { astValidation, preEditValidation, currentMtime },
|
|
247
257
|
};
|
|
248
258
|
|
|
249
259
|
return {
|
|
@@ -263,20 +273,60 @@ export class ASTEditToolInvocation
|
|
|
263
273
|
}
|
|
264
274
|
}
|
|
265
275
|
|
|
276
|
+
private computeValidationContext(editData: CalculatedEdit): {
|
|
277
|
+
astValidation: { valid: boolean; errors: string[] };
|
|
278
|
+
preEditValidation: AstValidationResult | undefined;
|
|
279
|
+
lineDelta: number;
|
|
280
|
+
editStartLine: number | null;
|
|
281
|
+
} {
|
|
282
|
+
const astValidation =
|
|
283
|
+
editData.astValidation ??
|
|
284
|
+
this.validateASTSyntax(this.params.file_path, editData.newContent);
|
|
285
|
+
const preEditValidation: AstValidationResult | undefined =
|
|
286
|
+
editData.preEditValidation ??
|
|
287
|
+
(editData.currentContent !== null
|
|
288
|
+
? this.validateASTSyntax(this.params.file_path, editData.currentContent)
|
|
289
|
+
: undefined);
|
|
290
|
+
const lineDelta = computeLineDelta(
|
|
291
|
+
this.params.old_string,
|
|
292
|
+
this.params.new_string,
|
|
293
|
+
);
|
|
294
|
+
const editStartLine = findEditStartLine(
|
|
295
|
+
editData.currentContent,
|
|
296
|
+
this.params.old_string,
|
|
297
|
+
);
|
|
298
|
+
return { astValidation, preEditValidation, lineDelta, editStartLine };
|
|
299
|
+
}
|
|
300
|
+
|
|
266
301
|
private buildPreviewLlmContent(
|
|
267
302
|
enhancedContext: Awaited<
|
|
268
303
|
ReturnType<ASTContextCollector['collectEnhancedContext']>
|
|
269
304
|
>,
|
|
270
305
|
astValidation: { valid: boolean; errors: string[] },
|
|
306
|
+
preEditValidation: { valid: boolean; errors: string[] } | undefined,
|
|
307
|
+
lineDelta: number,
|
|
308
|
+
editStartLine: number | null,
|
|
271
309
|
currentMtime: number | null,
|
|
272
310
|
workspaceRoot: string,
|
|
273
311
|
): string {
|
|
312
|
+
const summary = summarizeAstValidation(
|
|
313
|
+
preEditValidation,
|
|
314
|
+
astValidation,
|
|
315
|
+
lineDelta,
|
|
316
|
+
editStartLine,
|
|
317
|
+
);
|
|
274
318
|
return [
|
|
275
319
|
`LLXPRT EDIT PREVIEW: ${this.params.file_path}`,
|
|
276
320
|
`- Context: ${enhancedContext.language} file with ${enhancedContext.declarations.length} declarations`,
|
|
277
321
|
`- Functions: ${enhancedContext.languageContext.functions.length}`,
|
|
278
322
|
`- Classes: ${enhancedContext.languageContext.classes.length}`,
|
|
279
|
-
`- AST validation: ${
|
|
323
|
+
`- AST validation: ${summary.label}`,
|
|
324
|
+
preEditValidation && !preEditValidation.valid
|
|
325
|
+
? `- Pre-existing syntax errors: Yes${formatValidationLineLabel(preEditValidation.errors)}`
|
|
326
|
+
: '',
|
|
327
|
+
!astValidation.valid
|
|
328
|
+
? `- AST errors: ${astValidation.errors.join(', ')}`
|
|
329
|
+
: '',
|
|
280
330
|
`- Relevant snippets: ${enhancedContext.relevantSnippets.length} found`,
|
|
281
331
|
enhancedContext.repositoryContext
|
|
282
332
|
? `- Repository: ${enhancedContext.repositoryContext.gitUrl}`
|
|
@@ -285,9 +335,6 @@ export class ASTEditToolInvocation
|
|
|
285
335
|
? `- Related files: ${enhancedContext.relatedFiles.length}`
|
|
286
336
|
: '',
|
|
287
337
|
this.formatConnectedFilesContext(enhancedContext, workspaceRoot),
|
|
288
|
-
!astValidation.valid
|
|
289
|
-
? `- AST errors: ${astValidation.errors.join(', ')}`
|
|
290
|
-
: '',
|
|
291
338
|
currentMtime !== null ? `- Timestamp: ${currentMtime}` : '',
|
|
292
339
|
'',
|
|
293
340
|
'ENHANCED CONTEXT ANALYSIS:',
|
|
@@ -408,21 +455,44 @@ export class ASTEditToolInvocation
|
|
|
408
455
|
DEFAULT_CREATE_PATCH_OPTIONS,
|
|
409
456
|
);
|
|
410
457
|
|
|
458
|
+
const { astValidation, preEditValidation, lineDelta, editStartLine } =
|
|
459
|
+
this.computeValidationContext(editData);
|
|
411
460
|
const displayResult = {
|
|
412
461
|
fileDiff,
|
|
413
462
|
fileName,
|
|
414
463
|
originalContent: editData.currentContent,
|
|
415
464
|
newContent: contentToWrite,
|
|
416
465
|
applied: true,
|
|
417
|
-
metadata: {
|
|
466
|
+
metadata: {
|
|
467
|
+
astValidation,
|
|
468
|
+
preEditValidation,
|
|
469
|
+
},
|
|
418
470
|
};
|
|
471
|
+
const summary = summarizeAstValidation(
|
|
472
|
+
preEditValidation,
|
|
473
|
+
astValidation,
|
|
474
|
+
lineDelta,
|
|
475
|
+
editStartLine,
|
|
476
|
+
);
|
|
419
477
|
|
|
420
478
|
const llmSuccessMessageParts: string[] = [
|
|
421
479
|
`Successfully applied edit to: ${this.params.file_path}`,
|
|
422
480
|
`- Changes: ${editData.occurrences} replacement(s) applied`,
|
|
423
|
-
`- AST validation: ${
|
|
481
|
+
`- AST validation: ${summary.label}`,
|
|
424
482
|
];
|
|
425
483
|
|
|
484
|
+
if (preEditValidation && !preEditValidation.valid) {
|
|
485
|
+
llmSuccessMessageParts.push(
|
|
486
|
+
`- Pre-existing syntax errors: Yes${formatValidationLineLabel(preEditValidation.errors)}${summary.newlyIntroduced ? '' : ' (not introduced by this edit)'}`,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (summary.newlyIntroduced) {
|
|
491
|
+
llmSuccessMessageParts.push(
|
|
492
|
+
`- AST errors: ${astValidation.errors.join(', ')}`,
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
426
496
|
await this.appendLspDiagnostics(llmSuccessMessageParts);
|
|
427
497
|
|
|
428
498
|
return {
|
|
@@ -18,7 +18,7 @@ import { makeRelative, shortenPath } from '../../utils/paths.js';
|
|
|
18
18
|
import { isNodeError } from '../../utils/errors.js';
|
|
19
19
|
import { countLines } from '../../utils/fileUtils.js';
|
|
20
20
|
import type { IToolHost } from '../../interfaces/index.js';
|
|
21
|
-
import type {
|
|
21
|
+
import type { LiveOutputUpdate } from '../../utils/terminalSerializer.js';
|
|
22
22
|
|
|
23
23
|
import type { ASTReadFileToolParams } from './types.js';
|
|
24
24
|
import { ASTConfig } from './ast-config.js';
|
|
@@ -51,7 +51,7 @@ export class ASTReadFileToolInvocation
|
|
|
51
51
|
|
|
52
52
|
async execute(
|
|
53
53
|
_signal?: AbortSignal,
|
|
54
|
-
_updateOutput?: (
|
|
54
|
+
_updateOutput?: (update: LiveOutputUpdate) => void,
|
|
55
55
|
_terminalColumns?: number,
|
|
56
56
|
_terminalRows?: number,
|
|
57
57
|
_setPidCallback?: (pid: number) => void,
|
|
@@ -26,6 +26,7 @@ export interface CalculatedEdit {
|
|
|
26
26
|
error?: { display: string; raw: string; type: ToolErrorType };
|
|
27
27
|
isNewFile: boolean;
|
|
28
28
|
astValidation?: { valid: boolean; errors: string[] };
|
|
29
|
+
preEditValidation?: { valid: boolean; errors: string[] };
|
|
29
30
|
fileFreshness?: number | null;
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -86,6 +87,14 @@ export async function calculateEdit(
|
|
|
86
87
|
params.file_path,
|
|
87
88
|
);
|
|
88
89
|
|
|
90
|
+
// Validate the original (pre-edit) content so post-edit errors can be
|
|
91
|
+
// categorized as pre-existing vs newly-introduced (issue #2124).
|
|
92
|
+
// Skipped for new files where there is no prior content to baseline.
|
|
93
|
+
const preEditValidation =
|
|
94
|
+
!noChangeError && currentContent !== null
|
|
95
|
+
? validateASTSyntax(params.file_path, currentContent)
|
|
96
|
+
: undefined;
|
|
97
|
+
|
|
89
98
|
let astValidation: { valid: boolean; errors: string[] } | undefined;
|
|
90
99
|
if (!noChangeError) {
|
|
91
100
|
astValidation = validateASTSyntax(params.file_path, newContent);
|
|
@@ -98,6 +107,7 @@ export async function calculateEdit(
|
|
|
98
107
|
error: noChangeError,
|
|
99
108
|
isNewFile,
|
|
100
109
|
astValidation,
|
|
110
|
+
preEditValidation,
|
|
101
111
|
fileFreshness: currentMtime,
|
|
102
112
|
};
|
|
103
113
|
}
|
|
@@ -164,6 +174,7 @@ function checkFreshness(
|
|
|
164
174
|
},
|
|
165
175
|
isNewFile: false,
|
|
166
176
|
astValidation: undefined,
|
|
177
|
+
preEditValidation: undefined,
|
|
167
178
|
fileFreshness: currentMtime,
|
|
168
179
|
};
|
|
169
180
|
}
|
|
@@ -203,6 +214,12 @@ function validateEditParams(
|
|
|
203
214
|
raw: `Failed to edit, 0 occurrences found for old_string in ${params.file_path}. No edits made.`,
|
|
204
215
|
type: ToolErrorType.EDIT_NO_OCCURRENCE_FOUND,
|
|
205
216
|
};
|
|
217
|
+
} else if (occurrences > 1) {
|
|
218
|
+
error = {
|
|
219
|
+
display: `old_string appears ${occurrences} times. Include more surrounding context to target a specific occurrence.`,
|
|
220
|
+
raw: `old_string appears ${occurrences} times in ${params.file_path}. Provide more surrounding context to disambiguate.`,
|
|
221
|
+
type: ToolErrorType.EDIT_EXPECTED_OCCURRENCE_MISMATCH,
|
|
222
|
+
};
|
|
206
223
|
} else if (normalizedOldString === normalizedNewString) {
|
|
207
224
|
error = {
|
|
208
225
|
display: `No changes to apply. The old_string and new_string are identical.`,
|
|
@@ -234,12 +251,11 @@ function checkNoChange(
|
|
|
234
251
|
}
|
|
235
252
|
|
|
236
253
|
/**
|
|
237
|
-
*
|
|
238
|
-
* Returns 0/1 not true count, aligned with String.replace() single-replacement semantics.
|
|
254
|
+
* Returns the true number of occurrences of searchString in content.
|
|
239
255
|
*
|
|
240
256
|
* @param content - File content
|
|
241
257
|
* @param searchString - String to search for
|
|
242
|
-
* @returns 0 if not found
|
|
258
|
+
* @returns Number of occurrences (0 if not found)
|
|
243
259
|
*/
|
|
244
260
|
export function countOccurrences(
|
|
245
261
|
content: string,
|
|
@@ -247,9 +263,13 @@ export function countOccurrences(
|
|
|
247
263
|
): number {
|
|
248
264
|
if (!searchString) return 0;
|
|
249
265
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
266
|
+
let count = 0;
|
|
267
|
+
let idx = content.indexOf(searchString);
|
|
268
|
+
while (idx !== -1) {
|
|
269
|
+
count++;
|
|
270
|
+
idx = content.indexOf(searchString, idx + searchString.length);
|
|
271
|
+
}
|
|
272
|
+
return count;
|
|
253
273
|
}
|
|
254
274
|
|
|
255
275
|
/**
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Vybestack LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*
|
|
6
|
+
* Categorizes AST validation results into pre-existing vs newly-introduced
|
|
7
|
+
* errors by comparing the pre-edit and post-edit parse trees.
|
|
8
|
+
*
|
|
9
|
+
* Background (issue #2124): tree-sitter is error-recovering and will report
|
|
10
|
+
* ERROR/MISSING nodes for any pre-existing syntax weirdness in a file. Without
|
|
11
|
+
* a baseline, every post-edit validation failure looks like the edit's fault,
|
|
12
|
+
* causing the LLM to either blindly force the edit or abandon it for a less
|
|
13
|
+
* capable tool. Comparing against the pre-edit baseline lets us tell the LLM
|
|
14
|
+
* whether the failure was already there.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export type AstValidationResult = { valid: boolean; errors: string[] };
|
|
18
|
+
|
|
19
|
+
export interface AstValidationSummary {
|
|
20
|
+
/** Post-edit validation status. */
|
|
21
|
+
status: 'PASSED' | 'FAILED' | 'SKIPPED';
|
|
22
|
+
/** True when the post-edit error(s) also existed in the pre-edit content. */
|
|
23
|
+
preExisting: boolean;
|
|
24
|
+
/** True when the edit likely introduced an error that did not exist before. */
|
|
25
|
+
newlyIntroduced: boolean;
|
|
26
|
+
/** One-line label suitable for LLM-facing output. */
|
|
27
|
+
label: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Extracts the leading line number from a validation error message such as
|
|
32
|
+
* "Syntax error at line 173, column 5". Returns null when no line is found.
|
|
33
|
+
*/
|
|
34
|
+
export function extractErrorLineNumber(error: string): number | null {
|
|
35
|
+
const match = /line (\d+)/i.exec(error);
|
|
36
|
+
return match ? parseInt(match[1], 10) : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ErrorLocation {
|
|
40
|
+
line: number;
|
|
41
|
+
column: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function extractErrorLocation(error: string): ErrorLocation | null {
|
|
45
|
+
const match = /line (\d+), column (\d+)/i.exec(error);
|
|
46
|
+
if (!match) return null;
|
|
47
|
+
return {
|
|
48
|
+
line: parseInt(match[1], 10),
|
|
49
|
+
column: parseInt(match[2], 10),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Finds the 1-based line number where an edit begins in the original content.
|
|
55
|
+
* Returns null when the position cannot be determined (e.g. new file) or when
|
|
56
|
+
* oldString appears more than once (ambiguous — indexOf would return the first
|
|
57
|
+
* match, but the actual edit could target a different occurrence, making the
|
|
58
|
+
* computed line unreliable for shift matching).
|
|
59
|
+
*/
|
|
60
|
+
export function findEditStartLine(
|
|
61
|
+
content: string | null,
|
|
62
|
+
oldString: string,
|
|
63
|
+
): number | null {
|
|
64
|
+
if (!content || !oldString) return null;
|
|
65
|
+
const firstIdx = content.indexOf(oldString);
|
|
66
|
+
if (firstIdx === -1) return null;
|
|
67
|
+
// Ambiguous: oldString appears more than once, so we cannot reliably
|
|
68
|
+
// determine which occurrence the edit targets. Return null to fall back
|
|
69
|
+
// to the less precise (but safe) lineDelta-based matching.
|
|
70
|
+
if (content.indexOf(oldString, firstIdx + 1) !== -1) return null;
|
|
71
|
+
return content.substring(0, firstIdx).split('\n').length;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Counts newline delimiters, which determine shifts below an edit. */
|
|
75
|
+
function countLineBreaks(s: string | undefined): number {
|
|
76
|
+
if (!s) return 0;
|
|
77
|
+
return s.split('\n').length - 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Computes the net line delta an edit introduces. A positive value means the
|
|
82
|
+
* edit added lines (shifting errors below it downward); negative means removed.
|
|
83
|
+
*/
|
|
84
|
+
export function computeLineDelta(
|
|
85
|
+
oldString: string | undefined,
|
|
86
|
+
newString: string | undefined,
|
|
87
|
+
): number {
|
|
88
|
+
return countLineBreaks(newString) - countLineBreaks(oldString);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Determines whether a pre-edit error line and a post-edit error line refer to
|
|
93
|
+
* the same location, accounting for line shifts caused by the edit.
|
|
94
|
+
*
|
|
95
|
+
* When the edit's start line is known, errors at or below the edit must have
|
|
96
|
+
* shifted by `lineDelta`, while errors above it did not shift — this avoids the
|
|
97
|
+
* false positive of matching a newly-introduced error to a nearby pre-existing
|
|
98
|
+
* one. The validator emits deterministic line and column coordinates, so both
|
|
99
|
+
* must match exactly after applying the known line shift.
|
|
100
|
+
*
|
|
101
|
+
* When the edit position is unknown, the two cases are made mutually exclusive
|
|
102
|
+
* based on whether `lineDelta` is zero, which is the safest fallback.
|
|
103
|
+
*/
|
|
104
|
+
function locationsMatch(
|
|
105
|
+
pre: ErrorLocation,
|
|
106
|
+
post: ErrorLocation,
|
|
107
|
+
lineDelta: number,
|
|
108
|
+
editStartLine: number | null,
|
|
109
|
+
): boolean {
|
|
110
|
+
if (pre.column !== post.column) return false;
|
|
111
|
+
|
|
112
|
+
if (editStartLine !== null) {
|
|
113
|
+
if (pre.line > editStartLine) {
|
|
114
|
+
return pre.line + lineDelta === post.line;
|
|
115
|
+
}
|
|
116
|
+
if (pre.line === editStartLine) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
return pre.line === post.line;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return lineDelta === 0 && pre.line === post.line;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Categorizes post-edit AST validation results relative to the pre-edit
|
|
127
|
+
* baseline so the LLM understands whether failures are its fault.
|
|
128
|
+
*
|
|
129
|
+
* @param preEdit - Validation result of the original file content.
|
|
130
|
+
* @param postEdit - Validation result of the edited file content.
|
|
131
|
+
* @param lineDelta - Net lines added/removed by the edit (for shift matching).
|
|
132
|
+
* @param editStartLine - 1-based line where the edit begins (for precise matching).
|
|
133
|
+
*/
|
|
134
|
+
export function summarizeAstValidation(
|
|
135
|
+
preEdit: AstValidationResult | undefined,
|
|
136
|
+
postEdit: AstValidationResult | undefined,
|
|
137
|
+
lineDelta = 0,
|
|
138
|
+
editStartLine: number | null = null,
|
|
139
|
+
): AstValidationSummary {
|
|
140
|
+
if (!postEdit) {
|
|
141
|
+
return {
|
|
142
|
+
status: 'SKIPPED',
|
|
143
|
+
preExisting: false,
|
|
144
|
+
newlyIntroduced: false,
|
|
145
|
+
label: 'SKIPPED (validation not performed)',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Post-edit is valid.
|
|
150
|
+
if (postEdit.valid) {
|
|
151
|
+
if (preEdit && !preEdit.valid) {
|
|
152
|
+
return {
|
|
153
|
+
status: 'PASSED',
|
|
154
|
+
preExisting: false,
|
|
155
|
+
newlyIntroduced: false,
|
|
156
|
+
label: `PASSED (edit may have resolved pre-existing error${formatLineLabel(preEdit.errors)})`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
status: 'PASSED',
|
|
161
|
+
preExisting: false,
|
|
162
|
+
newlyIntroduced: false,
|
|
163
|
+
label: 'PASSED',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Post-edit is invalid.
|
|
168
|
+
const postErrors = postEdit.errors;
|
|
169
|
+
const preHadErrors = preEdit !== undefined && !preEdit.valid;
|
|
170
|
+
|
|
171
|
+
// Pre-edit was clean (or no baseline / new file) → the edit introduced the error.
|
|
172
|
+
if (!preHadErrors) {
|
|
173
|
+
return {
|
|
174
|
+
status: 'FAILED',
|
|
175
|
+
preExisting: false,
|
|
176
|
+
newlyIntroduced: true,
|
|
177
|
+
label: `FAILED (new error introduced by this edit${formatLineLabel(postErrors)})`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Both pre- and post-edit are invalid. Determine whether every post-edit
|
|
182
|
+
// error corresponds to the same location as a pre-edit one. Iterate over the
|
|
183
|
+
// raw error strings (not just those with parseable line numbers) so that
|
|
184
|
+
// unparseable post-edit errors are conservatively treated as unmatched
|
|
185
|
+
// rather than silently dropped.
|
|
186
|
+
const unmatchedPreLocations = preEdit.errors
|
|
187
|
+
.map(extractErrorLocation)
|
|
188
|
+
.filter((location): location is ErrorLocation => location !== null);
|
|
189
|
+
let matchedCount = 0;
|
|
190
|
+
let allMatched = postErrors.length > 0;
|
|
191
|
+
for (const postErr of postErrors) {
|
|
192
|
+
const postLocation = extractErrorLocation(postErr);
|
|
193
|
+
const matchingIndex =
|
|
194
|
+
postLocation === null
|
|
195
|
+
? -1
|
|
196
|
+
: unmatchedPreLocations.findIndex((preLocation) =>
|
|
197
|
+
locationsMatch(preLocation, postLocation, lineDelta, editStartLine),
|
|
198
|
+
);
|
|
199
|
+
if (matchingIndex === -1) {
|
|
200
|
+
allMatched = false;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
matchedCount++;
|
|
204
|
+
unmatchedPreLocations.splice(matchingIndex, 1);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (allMatched) {
|
|
208
|
+
return {
|
|
209
|
+
status: 'FAILED',
|
|
210
|
+
preExisting: true,
|
|
211
|
+
newlyIntroduced: false,
|
|
212
|
+
label: `FAILED (pre-existing error${formatLineLabel(postErrors)} — present before this edit)`,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// File was already broken, but the post-edit errors don't line up with the
|
|
217
|
+
// pre-existing ones. They may be cascading from the same root cause or newly
|
|
218
|
+
// introduced; surface both for the LLM to judge.
|
|
219
|
+
return formatMixedValidationSummary(
|
|
220
|
+
preEdit.errors,
|
|
221
|
+
postErrors,
|
|
222
|
+
matchedCount > 0,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function formatMixedValidationSummary(
|
|
227
|
+
preErrors: string[],
|
|
228
|
+
postErrors: string[],
|
|
229
|
+
hasMatchedError: boolean,
|
|
230
|
+
): AstValidationSummary {
|
|
231
|
+
const preErrorNoun = preErrors.length === 1 ? 'error' : 'errors';
|
|
232
|
+
let postErrorDescription =
|
|
233
|
+
'post-edit validation failed without error details';
|
|
234
|
+
if (postErrors.length > 0) {
|
|
235
|
+
const postErrorNoun = postErrors.length === 1 ? 'error' : 'errors';
|
|
236
|
+
postErrorDescription = `post-edit ${postErrorNoun}${formatLineLabel(postErrors)} may be newly introduced`;
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
status: 'FAILED',
|
|
240
|
+
preExisting: hasMatchedError,
|
|
241
|
+
newlyIntroduced: true,
|
|
242
|
+
label: `FAILED (file had pre-existing ${preErrorNoun}${formatLineLabel(preErrors)}; ${postErrorDescription})`,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function extractErrorLineNumbers(errors: string[]): number[] {
|
|
247
|
+
const lines: number[] = [];
|
|
248
|
+
for (const err of errors) {
|
|
249
|
+
const line = extractErrorLineNumber(err);
|
|
250
|
+
if (line !== null) lines.push(line);
|
|
251
|
+
}
|
|
252
|
+
return lines;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function formatValidationLineLabel(errors: string[]): string {
|
|
256
|
+
const lines = extractErrorLineNumbers(errors);
|
|
257
|
+
if (lines.length === 0) return '';
|
|
258
|
+
return ` at line${lines.length > 1 ? 's' : ''} ${lines.join(', ')}`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function formatLineLabel(errors: string[]): string {
|
|
262
|
+
return formatValidationLineLabel(errors);
|
|
263
|
+
}
|
package/src/tools/codesearch.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
Kind,
|
|
24
24
|
type ToolInvocation,
|
|
25
25
|
type ToolResult,
|
|
26
|
+
type LiveOutputUpdate,
|
|
26
27
|
} from './tools.js';
|
|
27
28
|
|
|
28
29
|
const API_CONFIG = {
|
|
@@ -140,7 +141,7 @@ class CodeSearchToolInvocation extends BaseToolInvocation<
|
|
|
140
141
|
|
|
141
142
|
async execute(
|
|
142
143
|
signal: AbortSignal,
|
|
143
|
-
_updateOutput?: (
|
|
144
|
+
_updateOutput?: (update: LiveOutputUpdate) => void,
|
|
144
145
|
): Promise<ToolResult> {
|
|
145
146
|
const codeRequest: McpCodeRequest = {
|
|
146
147
|
jsonrpc: '2.0',
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
Kind,
|
|
15
15
|
type ToolInvocation,
|
|
16
16
|
type ToolResult,
|
|
17
|
+
type LiveOutputUpdate,
|
|
17
18
|
} from './tools.js';
|
|
18
19
|
import { DIRECT_WEB_FETCH_TOOL } from '../types/tool-names.js';
|
|
19
20
|
import { ToolErrorType } from '../types/tool-error.js';
|
|
@@ -103,7 +104,7 @@ class DirectWebFetchToolInvocation extends BaseToolInvocation<
|
|
|
103
104
|
|
|
104
105
|
async execute(
|
|
105
106
|
signal: AbortSignal,
|
|
106
|
-
_updateOutput?: (
|
|
107
|
+
_updateOutput?: (update: LiveOutputUpdate) => void,
|
|
107
108
|
): Promise<ToolResult> {
|
|
108
109
|
const protocolError = this.validateUrlProtocol();
|
|
109
110
|
if (protocolError) return protocolError;
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
Kind,
|
|
20
20
|
type ToolInvocation,
|
|
21
21
|
type ToolResult,
|
|
22
|
+
type LiveOutputUpdate,
|
|
22
23
|
} from './tools.js';
|
|
23
24
|
|
|
24
25
|
const API_CONFIG = {
|
|
@@ -154,7 +155,7 @@ class ExaWebSearchToolInvocation extends BaseToolInvocation<
|
|
|
154
155
|
|
|
155
156
|
async execute(
|
|
156
157
|
signal: AbortSignal,
|
|
157
|
-
_updateOutput?: (
|
|
158
|
+
_updateOutput?: (update: LiveOutputUpdate) => void,
|
|
158
159
|
): Promise<ToolResult> {
|
|
159
160
|
const searchRequest: McpSearchRequest = {
|
|
160
161
|
jsonrpc: '2.0',
|