@agi-cli/sdk 0.1.79 → 0.1.80
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/package.json +5 -1
- package/src/core/src/index.ts +13 -0
- package/src/core/src/tools/builtin/bash.ts +46 -14
- package/src/core/src/tools/builtin/finish.txt +9 -4
- package/src/core/src/tools/builtin/fs/read.ts +44 -5
- package/src/core/src/tools/builtin/fs/write.ts +61 -16
- package/src/core/src/tools/builtin/patch.ts +159 -38
- package/src/core/src/tools/builtin/patch.txt +11 -4
- package/src/core/src/tools/error.ts +67 -0
- package/src/prompts/src/base.txt +12 -2
- package/src/prompts/src/providers/anthropic.txt +4 -3
- package/src/prompts/src/providers/default.txt +5 -4
- package/src/prompts/src/providers/google.txt +4 -3
- package/src/prompts/src/providers/openai.txt +5 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agi-cli/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.80",
|
|
4
4
|
"description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
|
|
5
5
|
"author": "ntishxyz",
|
|
6
6
|
"license": "MIT",
|
|
@@ -65,6 +65,10 @@
|
|
|
65
65
|
"import": "./src/core/src/tools/builtin/websearch.ts",
|
|
66
66
|
"types": "./src/core/src/tools/builtin/websearch.ts"
|
|
67
67
|
},
|
|
68
|
+
"./tools/error": {
|
|
69
|
+
"import": "./src/core/src/tools/error.ts",
|
|
70
|
+
"types": "./src/core/src/tools/error.ts"
|
|
71
|
+
},
|
|
68
72
|
"./prompts/*": "./src/prompts/src/*"
|
|
69
73
|
},
|
|
70
74
|
"files": [
|
package/src/core/src/index.ts
CHANGED
|
@@ -32,6 +32,19 @@ export type { ProviderId, ModelInfo } from '../../types/src/index.ts';
|
|
|
32
32
|
export { discoverProjectTools } from './tools/loader';
|
|
33
33
|
export type { DiscoveredTool } from './tools/loader';
|
|
34
34
|
|
|
35
|
+
// Tool error handling utilities
|
|
36
|
+
export {
|
|
37
|
+
isToolError,
|
|
38
|
+
extractToolError,
|
|
39
|
+
createToolError,
|
|
40
|
+
} from './tools/error';
|
|
41
|
+
export type {
|
|
42
|
+
ToolErrorType,
|
|
43
|
+
ToolErrorResponse,
|
|
44
|
+
ToolSuccessResponse,
|
|
45
|
+
ToolResponse,
|
|
46
|
+
} from './tools/error';
|
|
47
|
+
|
|
35
48
|
// Re-export builtin tools for direct access
|
|
36
49
|
export { buildFsTools } from './tools/builtin/fs/index';
|
|
37
50
|
export { buildGitTools } from './tools/builtin/git';
|
|
@@ -2,6 +2,7 @@ import { tool, type Tool } from 'ai';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import DESCRIPTION from './bash.txt' with { type: 'text' };
|
|
5
|
+
import { createToolError, type ToolResponse } from '../error.ts';
|
|
5
6
|
|
|
6
7
|
function normalizePath(p: string) {
|
|
7
8
|
const parts = p.replace(/\\/g, '/').split('/');
|
|
@@ -58,11 +59,16 @@ export function buildBashTool(projectRoot: string): {
|
|
|
58
59
|
cwd?: string;
|
|
59
60
|
allowNonZeroExit?: boolean;
|
|
60
61
|
timeout?: number;
|
|
61
|
-
})
|
|
62
|
+
}): Promise<
|
|
63
|
+
ToolResponse<{
|
|
64
|
+
exitCode: number;
|
|
65
|
+
stdout: string;
|
|
66
|
+
stderr: string;
|
|
67
|
+
}>
|
|
68
|
+
> {
|
|
62
69
|
const absCwd = resolveSafePath(projectRoot, cwd || '.');
|
|
63
70
|
|
|
64
|
-
return new Promise((resolve
|
|
65
|
-
// Use spawn with shell: true for cross-platform compatibility
|
|
71
|
+
return new Promise((resolve) => {
|
|
66
72
|
const proc = spawn(cmd, {
|
|
67
73
|
cwd: absCwd,
|
|
68
74
|
shell: true,
|
|
@@ -93,24 +99,50 @@ export function buildBashTool(projectRoot: string): {
|
|
|
93
99
|
if (timeoutId) clearTimeout(timeoutId);
|
|
94
100
|
|
|
95
101
|
if (didTimeout) {
|
|
96
|
-
|
|
102
|
+
resolve(
|
|
103
|
+
createToolError(
|
|
104
|
+
`Command timed out after ${timeout}ms: ${cmd}`,
|
|
105
|
+
'timeout',
|
|
106
|
+
{
|
|
107
|
+
parameter: 'timeout',
|
|
108
|
+
value: timeout,
|
|
109
|
+
suggestion: 'Increase timeout or optimize the command',
|
|
110
|
+
},
|
|
111
|
+
),
|
|
112
|
+
);
|
|
97
113
|
} else if (exitCode !== 0 && !allowNonZeroExit) {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
114
|
+
const errorDetail = stderr.trim() || stdout.trim() || '';
|
|
115
|
+
const errorMsg = `Command failed with exit code ${exitCode}${errorDetail ? `\n\n${errorDetail}` : ''}`;
|
|
116
|
+
resolve(
|
|
117
|
+
createToolError(errorMsg, 'execution', {
|
|
118
|
+
exitCode,
|
|
119
|
+
stdout,
|
|
120
|
+
stderr,
|
|
121
|
+
cmd,
|
|
122
|
+
suggestion:
|
|
123
|
+
'Check command syntax or use allowNonZeroExit: true',
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
104
126
|
} else {
|
|
105
|
-
resolve({
|
|
127
|
+
resolve({
|
|
128
|
+
ok: true,
|
|
129
|
+
exitCode: exitCode ?? 0,
|
|
130
|
+
stdout,
|
|
131
|
+
stderr,
|
|
132
|
+
});
|
|
106
133
|
}
|
|
107
134
|
});
|
|
108
135
|
|
|
109
136
|
proc.on('error', (err) => {
|
|
110
137
|
if (timeoutId) clearTimeout(timeoutId);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
`Command execution failed: ${err.message}
|
|
138
|
+
resolve(
|
|
139
|
+
createToolError(
|
|
140
|
+
`Command execution failed: ${err.message}`,
|
|
141
|
+
'execution',
|
|
142
|
+
{
|
|
143
|
+
cmd,
|
|
144
|
+
originalError: err.message,
|
|
145
|
+
},
|
|
114
146
|
),
|
|
115
147
|
);
|
|
116
148
|
});
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
- Agent should stream the summary directly as assistant message not using the finish tool
|
|
1
|
+
Call this tool AFTER you have completed all your work AND streamed your final summary/response to the user.
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
This signals the end of your turn and that you are done with the current request.
|
|
4
|
+
|
|
5
|
+
**CRITICAL**: You MUST always call this tool when you are completely finished. The workflow is:
|
|
6
|
+
1. Perform all necessary tool calls (read files, make changes, etc.)
|
|
7
|
+
2. Stream your final text response/summary to the user
|
|
8
|
+
3. Call the finish tool to signal completion
|
|
9
|
+
|
|
10
|
+
Do NOT call finish before streaming your response. Do NOT forget to call finish after responding.
|
|
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { expandTilde, isAbsoluteLike, resolveSafePath } from './util.ts';
|
|
5
5
|
import DESCRIPTION from './read.txt' with { type: 'text' };
|
|
6
|
+
import { createToolError, type ToolResponse } from '../../error.ts';
|
|
6
7
|
|
|
7
8
|
const embeddedTextAssets: Record<string, string> = {};
|
|
8
9
|
|
|
@@ -43,7 +44,27 @@ export function buildReadTool(projectRoot: string): {
|
|
|
43
44
|
path: string;
|
|
44
45
|
startLine?: number;
|
|
45
46
|
endLine?: number;
|
|
46
|
-
})
|
|
47
|
+
}): Promise<
|
|
48
|
+
ToolResponse<{
|
|
49
|
+
path: string;
|
|
50
|
+
content: string;
|
|
51
|
+
size: number;
|
|
52
|
+
lineRange?: string;
|
|
53
|
+
totalLines?: number;
|
|
54
|
+
}>
|
|
55
|
+
> {
|
|
56
|
+
if (!path || path.trim().length === 0) {
|
|
57
|
+
return createToolError(
|
|
58
|
+
'Missing required parameter: path',
|
|
59
|
+
'validation',
|
|
60
|
+
{
|
|
61
|
+
parameter: 'path',
|
|
62
|
+
value: path,
|
|
63
|
+
suggestion: 'Provide a file path to read',
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
47
68
|
const req = expandTilde(path);
|
|
48
69
|
const abs = isAbsoluteLike(req) ? req : resolveSafePath(projectRoot, req);
|
|
49
70
|
|
|
@@ -57,6 +78,7 @@ export function buildReadTool(projectRoot: string): {
|
|
|
57
78
|
const selectedLines = lines.slice(start, end);
|
|
58
79
|
content = selectedLines.join('\n');
|
|
59
80
|
return {
|
|
81
|
+
ok: true,
|
|
60
82
|
path: req,
|
|
61
83
|
content,
|
|
62
84
|
size: content.length,
|
|
@@ -65,14 +87,31 @@ export function buildReadTool(projectRoot: string): {
|
|
|
65
87
|
};
|
|
66
88
|
}
|
|
67
89
|
|
|
68
|
-
return { path: req, content, size: content.length };
|
|
69
|
-
} catch (
|
|
90
|
+
return { ok: true, path: req, content, size: content.length };
|
|
91
|
+
} catch (error: unknown) {
|
|
70
92
|
const embedded = embeddedTextAssets[req];
|
|
71
93
|
if (embedded) {
|
|
72
94
|
const content = await readFile(embedded, 'utf-8');
|
|
73
|
-
return { path: req, content, size: content.length };
|
|
95
|
+
return { ok: true, path: req, content, size: content.length };
|
|
74
96
|
}
|
|
75
|
-
|
|
97
|
+
const isEnoent =
|
|
98
|
+
error &&
|
|
99
|
+
typeof error === 'object' &&
|
|
100
|
+
'code' in error &&
|
|
101
|
+
error.code === 'ENOENT';
|
|
102
|
+
return createToolError(
|
|
103
|
+
isEnoent
|
|
104
|
+
? `File not found: ${req}`
|
|
105
|
+
: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
|
|
106
|
+
isEnoent ? 'not_found' : 'execution',
|
|
107
|
+
{
|
|
108
|
+
parameter: 'path',
|
|
109
|
+
value: req,
|
|
110
|
+
suggestion: isEnoent
|
|
111
|
+
? 'Use ls or tree to find available files'
|
|
112
|
+
: undefined,
|
|
113
|
+
},
|
|
114
|
+
);
|
|
76
115
|
}
|
|
77
116
|
},
|
|
78
117
|
});
|
|
@@ -8,8 +8,7 @@ import {
|
|
|
8
8
|
isAbsoluteLike,
|
|
9
9
|
} from './util.ts';
|
|
10
10
|
import DESCRIPTION from './write.txt' with { type: 'text' };
|
|
11
|
-
|
|
12
|
-
// description imported above
|
|
11
|
+
import { createToolError, type ToolResponse } from '../../error.ts';
|
|
13
12
|
|
|
14
13
|
export function buildWriteTool(projectRoot: string): {
|
|
15
14
|
name: string;
|
|
@@ -34,27 +33,73 @@ export function buildWriteTool(projectRoot: string): {
|
|
|
34
33
|
path: string;
|
|
35
34
|
content: string;
|
|
36
35
|
createDirs?: boolean;
|
|
37
|
-
})
|
|
36
|
+
}): Promise<
|
|
37
|
+
ToolResponse<{
|
|
38
|
+
path: string;
|
|
39
|
+
bytes: number;
|
|
40
|
+
artifact: unknown;
|
|
41
|
+
}>
|
|
42
|
+
> {
|
|
43
|
+
if (!path || path.trim().length === 0) {
|
|
44
|
+
return createToolError(
|
|
45
|
+
'Missing required parameter: path',
|
|
46
|
+
'validation',
|
|
47
|
+
{
|
|
48
|
+
parameter: 'path',
|
|
49
|
+
value: path,
|
|
50
|
+
suggestion: 'Provide a file path to write',
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
38
55
|
const req = expandTilde(path);
|
|
39
56
|
if (isAbsoluteLike(req)) {
|
|
40
|
-
|
|
57
|
+
return createToolError(
|
|
41
58
|
`Refusing to write outside project root: ${req}. Use a relative path within the project.`,
|
|
59
|
+
'permission',
|
|
60
|
+
{
|
|
61
|
+
parameter: 'path',
|
|
62
|
+
value: req,
|
|
63
|
+
suggestion: 'Use a relative path within the project',
|
|
64
|
+
},
|
|
42
65
|
);
|
|
43
66
|
}
|
|
44
67
|
const abs = resolveSafePath(projectRoot, req);
|
|
45
|
-
|
|
46
|
-
const dirPath = abs.slice(0, abs.lastIndexOf('/'));
|
|
47
|
-
await mkdir(dirPath, { recursive: true });
|
|
48
|
-
}
|
|
49
|
-
let existed = false;
|
|
50
|
-
let oldText = '';
|
|
68
|
+
|
|
51
69
|
try {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
70
|
+
if (createDirs) {
|
|
71
|
+
const dirPath = abs.slice(0, abs.lastIndexOf('/'));
|
|
72
|
+
await mkdir(dirPath, { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
let existed = false;
|
|
75
|
+
let oldText = '';
|
|
76
|
+
try {
|
|
77
|
+
oldText = await readFile(abs, 'utf-8');
|
|
78
|
+
existed = true;
|
|
79
|
+
} catch {}
|
|
80
|
+
await writeFile(abs, content);
|
|
81
|
+
const artifact = await buildWriteArtifact(
|
|
82
|
+
req,
|
|
83
|
+
existed,
|
|
84
|
+
oldText,
|
|
85
|
+
content,
|
|
86
|
+
);
|
|
87
|
+
return {
|
|
88
|
+
ok: true,
|
|
89
|
+
path: req,
|
|
90
|
+
bytes: content.length,
|
|
91
|
+
artifact,
|
|
92
|
+
};
|
|
93
|
+
} catch (error: unknown) {
|
|
94
|
+
return createToolError(
|
|
95
|
+
`Failed to write file: ${error instanceof Error ? error.message : String(error)}`,
|
|
96
|
+
'execution',
|
|
97
|
+
{
|
|
98
|
+
parameter: 'path',
|
|
99
|
+
value: req,
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
}
|
|
58
103
|
},
|
|
59
104
|
});
|
|
60
105
|
return { name: 'write', tool: write };
|
|
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
import { readFile, writeFile, unlink, mkdir } from 'node:fs/promises';
|
|
4
4
|
import { dirname, resolve, relative, isAbsolute } from 'node:path';
|
|
5
5
|
import DESCRIPTION from './patch.txt' with { type: 'text' };
|
|
6
|
+
import { createToolError, type ToolResponse } from '../error.ts';
|
|
6
7
|
|
|
7
8
|
interface PatchAddOperation {
|
|
8
9
|
kind: 'add';
|
|
@@ -106,6 +107,49 @@ function ensureTrailingNewline(lines: string[]) {
|
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Normalize whitespace for fuzzy matching.
|
|
112
|
+
* Converts tabs to spaces and trims leading/trailing whitespace.
|
|
113
|
+
*/
|
|
114
|
+
function normalizeWhitespace(line: string): string {
|
|
115
|
+
return line.replace(/\t/g, ' ').trim();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Find subsequence with optional whitespace normalization for fuzzy matching.
|
|
120
|
+
* Falls back to normalized matching if exact match fails.
|
|
121
|
+
*/
|
|
122
|
+
function findSubsequenceWithFuzzy(
|
|
123
|
+
lines: string[],
|
|
124
|
+
pattern: string[],
|
|
125
|
+
startIndex: number,
|
|
126
|
+
useFuzzy: boolean,
|
|
127
|
+
): number {
|
|
128
|
+
// Try exact match first
|
|
129
|
+
const exactMatch = findSubsequence(lines, pattern, startIndex);
|
|
130
|
+
if (exactMatch !== -1) return exactMatch;
|
|
131
|
+
|
|
132
|
+
// If fuzzy matching is enabled and exact match failed, try normalized matching
|
|
133
|
+
if (useFuzzy && pattern.length > 0) {
|
|
134
|
+
const normalizedLines = lines.map(normalizeWhitespace);
|
|
135
|
+
const normalizedPattern = pattern.map(normalizeWhitespace);
|
|
136
|
+
|
|
137
|
+
const start = Math.max(0, startIndex);
|
|
138
|
+
for (let i = start; i <= lines.length - pattern.length; i++) {
|
|
139
|
+
let matches = true;
|
|
140
|
+
for (let j = 0; j < pattern.length; j++) {
|
|
141
|
+
if (normalizedLines[i + j] !== normalizedPattern[j]) {
|
|
142
|
+
matches = false;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (matches) return i;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return -1;
|
|
151
|
+
}
|
|
152
|
+
|
|
109
153
|
function findSubsequence(
|
|
110
154
|
lines: string[],
|
|
111
155
|
pattern: string[],
|
|
@@ -415,6 +459,7 @@ function applyHunksToLines(
|
|
|
415
459
|
originalLines: string[],
|
|
416
460
|
hunks: PatchHunk[],
|
|
417
461
|
filePath: string,
|
|
462
|
+
useFuzzy: boolean = false,
|
|
418
463
|
): { lines: string[]; applied: AppliedHunkResult[] } {
|
|
419
464
|
const lines = [...originalLines];
|
|
420
465
|
let searchIndex = 0;
|
|
@@ -445,11 +490,16 @@ function applyHunksToLines(
|
|
|
445
490
|
: searchIndex;
|
|
446
491
|
|
|
447
492
|
let matchIndex = hasExpected
|
|
448
|
-
?
|
|
493
|
+
? findSubsequenceWithFuzzy(
|
|
494
|
+
lines,
|
|
495
|
+
expected,
|
|
496
|
+
Math.max(0, hint - 3),
|
|
497
|
+
useFuzzy,
|
|
498
|
+
)
|
|
449
499
|
: -1;
|
|
450
500
|
|
|
451
501
|
if (hasExpected && matchIndex === -1) {
|
|
452
|
-
matchIndex =
|
|
502
|
+
matchIndex = findSubsequenceWithFuzzy(lines, expected, 0, useFuzzy);
|
|
453
503
|
}
|
|
454
504
|
|
|
455
505
|
if (matchIndex === -1 && hasExpected && hunk.header.context) {
|
|
@@ -471,9 +521,20 @@ function applyHunksToLines(
|
|
|
471
521
|
const contextInfo = hunk.header.context
|
|
472
522
|
? ` near context '${hunk.header.context}'`
|
|
473
523
|
: '';
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
);
|
|
524
|
+
|
|
525
|
+
// Provide helpful error with nearby context
|
|
526
|
+
const nearbyStart = Math.max(0, hint - 2);
|
|
527
|
+
const nearbyEnd = Math.min(lines.length, hint + 5);
|
|
528
|
+
const nearbyLines = lines.slice(nearbyStart, nearbyEnd);
|
|
529
|
+
const lineNumberInfo =
|
|
530
|
+
nearbyStart > 0 ? ` (around line ${nearbyStart + 1})` : '';
|
|
531
|
+
|
|
532
|
+
let errorMsg = `Failed to apply patch hunk in ${filePath}${contextInfo}.\n`;
|
|
533
|
+
errorMsg += `Expected to find:\n${expected.map((l) => ` ${l}`).join('\n')}\n`;
|
|
534
|
+
errorMsg += `Nearby context${lineNumberInfo}:\n${nearbyLines.map((l, idx) => ` ${nearbyStart + idx + 1}: ${l}`).join('\n')}\n`;
|
|
535
|
+
errorMsg += `Hint: Check for whitespace differences (tabs vs spaces). Try enabling fuzzyMatch option.`;
|
|
536
|
+
|
|
537
|
+
throw new Error(errorMsg);
|
|
477
538
|
}
|
|
478
539
|
|
|
479
540
|
const deleteCount = hasExpected ? expected.length : 0;
|
|
@@ -532,6 +593,7 @@ function computeInsertionIndex(
|
|
|
532
593
|
async function applyUpdateOperation(
|
|
533
594
|
projectRoot: string,
|
|
534
595
|
operation: PatchUpdateOperation,
|
|
596
|
+
useFuzzy: boolean = false,
|
|
535
597
|
): Promise<AppliedOperationRecord> {
|
|
536
598
|
const targetPath = resolveProjectPath(projectRoot, operation.filePath);
|
|
537
599
|
let originalContent: string;
|
|
@@ -549,6 +611,7 @@ async function applyUpdateOperation(
|
|
|
549
611
|
originalLines,
|
|
550
612
|
operation.hunks,
|
|
551
613
|
operation.filePath,
|
|
614
|
+
useFuzzy,
|
|
552
615
|
);
|
|
553
616
|
ensureTrailingNewline(updatedLines);
|
|
554
617
|
await writeFile(targetPath, joinLines(updatedLines, newline), 'utf-8');
|
|
@@ -665,7 +728,11 @@ function formatNormalizedPatch(operations: AppliedOperationRecord[]): string {
|
|
|
665
728
|
return lines.join('\n');
|
|
666
729
|
}
|
|
667
730
|
|
|
668
|
-
async function applyEnvelopedPatch(
|
|
731
|
+
async function applyEnvelopedPatch(
|
|
732
|
+
projectRoot: string,
|
|
733
|
+
patch: string,
|
|
734
|
+
useFuzzy: boolean = false,
|
|
735
|
+
) {
|
|
669
736
|
const operations = parseEnvelopedPatch(patch);
|
|
670
737
|
const applied: AppliedOperationRecord[] = [];
|
|
671
738
|
|
|
@@ -675,7 +742,9 @@ async function applyEnvelopedPatch(projectRoot: string, patch: string) {
|
|
|
675
742
|
} else if (operation.kind === 'delete') {
|
|
676
743
|
applied.push(await applyDeleteOperation(projectRoot, operation));
|
|
677
744
|
} else {
|
|
678
|
-
applied.push(
|
|
745
|
+
applied.push(
|
|
746
|
+
await applyUpdateOperation(projectRoot, operation, useFuzzy),
|
|
747
|
+
);
|
|
679
748
|
}
|
|
680
749
|
}
|
|
681
750
|
|
|
@@ -700,46 +769,98 @@ export function buildApplyPatchTool(projectRoot: string): {
|
|
|
700
769
|
.describe(
|
|
701
770
|
'Allow hunks to be rejected without failing the whole operation',
|
|
702
771
|
),
|
|
772
|
+
fuzzyMatch: z
|
|
773
|
+
.boolean()
|
|
774
|
+
.optional()
|
|
775
|
+
.default(true)
|
|
776
|
+
.describe(
|
|
777
|
+
'Enable fuzzy matching with whitespace normalization (converts tabs to spaces for matching)',
|
|
778
|
+
),
|
|
703
779
|
}),
|
|
704
|
-
async execute({
|
|
780
|
+
async execute({
|
|
781
|
+
patch,
|
|
782
|
+
fuzzyMatch,
|
|
783
|
+
}: {
|
|
784
|
+
patch: string;
|
|
785
|
+
allowRejects?: boolean;
|
|
786
|
+
fuzzyMatch?: boolean;
|
|
787
|
+
}): Promise<
|
|
788
|
+
ToolResponse<{
|
|
789
|
+
output: string;
|
|
790
|
+
changes: unknown[];
|
|
791
|
+
artifact: unknown;
|
|
792
|
+
}>
|
|
793
|
+
> {
|
|
794
|
+
if (!patch || patch.trim().length === 0) {
|
|
795
|
+
return createToolError(
|
|
796
|
+
'Missing required parameter: patch',
|
|
797
|
+
'validation',
|
|
798
|
+
{
|
|
799
|
+
parameter: 'patch',
|
|
800
|
+
value: patch,
|
|
801
|
+
suggestion: 'Provide patch content in enveloped format',
|
|
802
|
+
},
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
|
|
705
806
|
if (
|
|
706
807
|
!patch.includes(PATCH_BEGIN_MARKER) ||
|
|
707
808
|
!patch.includes(PATCH_END_MARKER)
|
|
708
809
|
) {
|
|
709
|
-
|
|
810
|
+
return createToolError(
|
|
710
811
|
'Only enveloped patch format is supported. Patch must start with "*** Begin Patch" and contain "*** Add File:", "*** Update File:", or "*** Delete File:" directives.',
|
|
812
|
+
'validation',
|
|
813
|
+
{
|
|
814
|
+
parameter: 'patch',
|
|
815
|
+
suggestion:
|
|
816
|
+
'Use enveloped patch format starting with *** Begin Patch',
|
|
817
|
+
},
|
|
711
818
|
);
|
|
712
819
|
}
|
|
713
820
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
821
|
+
try {
|
|
822
|
+
const { operations, normalizedPatch } = await applyEnvelopedPatch(
|
|
823
|
+
projectRoot,
|
|
824
|
+
patch,
|
|
825
|
+
fuzzyMatch ?? true,
|
|
826
|
+
);
|
|
827
|
+
const summary = summarizeOperations(operations);
|
|
828
|
+
const changes = operations.map((operation) => ({
|
|
829
|
+
filePath: operation.filePath,
|
|
830
|
+
kind: operation.kind,
|
|
831
|
+
hunks: operation.hunks.map((hunk) => ({
|
|
832
|
+
oldStart: hunk.oldStart,
|
|
833
|
+
oldLines: hunk.oldLines,
|
|
834
|
+
newStart: hunk.newStart,
|
|
835
|
+
newLines: hunk.newLines,
|
|
836
|
+
additions: hunk.additions,
|
|
837
|
+
deletions: hunk.deletions,
|
|
838
|
+
context: hunk.header.context,
|
|
839
|
+
})),
|
|
840
|
+
}));
|
|
841
|
+
|
|
842
|
+
return {
|
|
843
|
+
ok: true,
|
|
844
|
+
output: 'Applied enveloped patch',
|
|
845
|
+
changes,
|
|
846
|
+
artifact: {
|
|
847
|
+
kind: 'file_diff',
|
|
848
|
+
patch: normalizedPatch,
|
|
849
|
+
summary,
|
|
850
|
+
},
|
|
851
|
+
};
|
|
852
|
+
} catch (error: unknown) {
|
|
853
|
+
const errorMessage =
|
|
854
|
+
error instanceof Error ? error.message : String(error);
|
|
855
|
+
return createToolError(
|
|
856
|
+
`Failed to apply patch: ${errorMessage}`,
|
|
857
|
+
'execution',
|
|
858
|
+
{
|
|
859
|
+
suggestion:
|
|
860
|
+
'Check that the patch format is correct and target files exist',
|
|
861
|
+
},
|
|
862
|
+
);
|
|
863
|
+
}
|
|
743
864
|
},
|
|
744
865
|
});
|
|
745
866
|
return { name: 'apply_patch', tool: applyPatch };
|
|
@@ -2,13 +2,18 @@ Apply a patch to modify one or more files using the enveloped patch format.
|
|
|
2
2
|
|
|
3
3
|
**RECOMMENDED: Use apply_patch for targeted file edits to avoid rewriting entire files and wasting tokens.**
|
|
4
4
|
|
|
5
|
+
**FUZZY MATCHING**: By default, fuzzy matching is enabled to handle whitespace differences (tabs vs spaces).
|
|
6
|
+
Exact matching is tried first, then normalized matching if exact fails. Disable with `fuzzyMatch: false` if needed.
|
|
7
|
+
|
|
5
8
|
Use `apply_patch` only when:
|
|
6
9
|
- You want to make targeted edits to specific lines (primary use case)
|
|
7
10
|
- You want to make multiple related changes across different files in a single operation
|
|
8
11
|
- You need to add/delete entire files along with modifications
|
|
9
|
-
- You have JUST read the file and are confident the content hasn't changed
|
|
12
|
+
- You have JUST read the file immediately before (within the same response) and are confident the content hasn't changed
|
|
10
13
|
|
|
11
|
-
**
|
|
14
|
+
**CRITICAL - ALWAYS READ BEFORE PATCHING**: You MUST read the file content immediately before creating a patch.
|
|
15
|
+
Never rely on memory or previous reads. Even with fuzzy matching enabled (tolerates tabs vs spaces),
|
|
16
|
+
If the file content has changed significantly since you last read it, the patch may still fail.
|
|
12
17
|
|
|
13
18
|
**Alternative: Use the `edit` tool if you need fuzzy matching or structured operations.**
|
|
14
19
|
|
|
@@ -55,7 +60,7 @@ All patches must be wrapped in markers and use explicit file directives:
|
|
|
55
60
|
```
|
|
56
61
|
*** Begin Patch
|
|
57
62
|
*** Update File: src/app.ts
|
|
58
|
-
@@ function main()
|
|
63
|
+
@@ function main() - locates the change position
|
|
59
64
|
function main() {
|
|
60
65
|
- console.log("old");
|
|
61
66
|
+ console.log("new");
|
|
@@ -63,7 +68,9 @@ All patches must be wrapped in markers and use explicit file directives:
|
|
|
63
68
|
*** End Patch
|
|
64
69
|
```
|
|
65
70
|
|
|
66
|
-
The `@@ context line`
|
|
71
|
+
**IMPORTANT**: The `@@ context line` is a hint for finding the location - it's NOT a line from the file.
|
|
72
|
+
It should describe what to look for (e.g., `@@ inside main function` or `@@ config section`).
|
|
73
|
+
The actual context lines (with leading space) come AFTER the `@@` line.
|
|
67
74
|
|
|
68
75
|
### Delete a file:
|
|
69
76
|
```
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export type ToolErrorType =
|
|
2
|
+
| 'validation'
|
|
3
|
+
| 'not_found'
|
|
4
|
+
| 'permission'
|
|
5
|
+
| 'execution'
|
|
6
|
+
| 'timeout'
|
|
7
|
+
| 'unsupported';
|
|
8
|
+
|
|
9
|
+
export type ToolErrorResponse = {
|
|
10
|
+
ok: false;
|
|
11
|
+
error: string;
|
|
12
|
+
errorType?: ToolErrorType;
|
|
13
|
+
details?: {
|
|
14
|
+
parameter?: string;
|
|
15
|
+
value?: unknown;
|
|
16
|
+
constraint?: string;
|
|
17
|
+
suggestion?: string;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
};
|
|
20
|
+
stack?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type ToolSuccessResponse<T = unknown> = {
|
|
24
|
+
ok: true;
|
|
25
|
+
} & T;
|
|
26
|
+
|
|
27
|
+
export type ToolResponse<T = unknown> =
|
|
28
|
+
| ToolSuccessResponse<T>
|
|
29
|
+
| ToolErrorResponse;
|
|
30
|
+
|
|
31
|
+
export function isToolError(result: unknown): result is ToolErrorResponse {
|
|
32
|
+
if (!result || typeof result !== 'object') return false;
|
|
33
|
+
const obj = result as Record<string, unknown>;
|
|
34
|
+
return obj.ok === false || 'error' in obj || obj.success === false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function extractToolError(
|
|
38
|
+
result: unknown,
|
|
39
|
+
topLevelError?: string,
|
|
40
|
+
): string | undefined {
|
|
41
|
+
if (topLevelError?.trim()) return topLevelError.trim();
|
|
42
|
+
if (!result || typeof result !== 'object') return undefined;
|
|
43
|
+
|
|
44
|
+
const obj = result as Record<string, unknown>;
|
|
45
|
+
const keys = ['error', 'stderr', 'message', 'detail', 'details', 'reason'];
|
|
46
|
+
for (const key of keys) {
|
|
47
|
+
const value = obj[key];
|
|
48
|
+
if (typeof value === 'string') {
|
|
49
|
+
const trimmed = value.trim();
|
|
50
|
+
if (trimmed.length) return trimmed;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createToolError(
|
|
57
|
+
error: string,
|
|
58
|
+
errorType?: ToolErrorType,
|
|
59
|
+
details?: ToolErrorResponse['details'],
|
|
60
|
+
): ToolErrorResponse {
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
error,
|
|
64
|
+
errorType,
|
|
65
|
+
details,
|
|
66
|
+
};
|
|
67
|
+
}
|
package/src/prompts/src/base.txt
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
You are a helpful, concise assistant.
|
|
2
|
-
- Stream the final answer as assistant text; call finish when done.
|
|
3
2
|
- CRITICAL: Emit progress updates using the `progress_update` tool at key milestones — at the start (planning), after initial repo discovery (discovering), before file edits (preparing), during edits (writing), and when validating (verifying). Prefer short messages (<= 80 chars).
|
|
4
3
|
- Do not print pseudo tool calls like `call:tool{}`; invoke tools directly.
|
|
5
4
|
- Use sensible default filenames when needed.
|
|
6
5
|
- Prefer minimal, precise outputs and actionable steps.
|
|
7
6
|
|
|
7
|
+
## Finish Tool - CRITICAL
|
|
8
|
+
|
|
9
|
+
You MUST call the `finish` tool at the end of every response to signal completion. The correct workflow is:
|
|
10
|
+
|
|
11
|
+
1. Perform all necessary work (tool calls, file edits, searches, etc.)
|
|
12
|
+
2. Stream your final text response or summary to the user explaining what you did
|
|
13
|
+
3. **Call the `finish` tool** to signal you are done
|
|
14
|
+
|
|
15
|
+
**IMPORTANT**: Do NOT call `finish` before streaming your response. Always stream your message first, then call `finish`. If you forget to call `finish`, the system will hang and not complete properly.
|
|
16
|
+
|
|
8
17
|
File Editing Best Practices:
|
|
18
|
+
- ALWAYS read a file immediately before using apply_patch on it - never patch from memory
|
|
9
19
|
- When making multiple edits to the same file, combine them into a single edit operation with multiple ops
|
|
10
20
|
- Each edit operation re-reads the file, so ops within a single edit call are applied sequentially to the latest content
|
|
11
21
|
- If you need to make edits based on previous edits, ensure they're in the same edit call or re-read the file between calls
|
|
12
22
|
- Never assume file content remains unchanged between separate edit operations
|
|
13
23
|
- When using apply_patch, ensure the patch is based on the current file content, not stale versions
|
|
14
|
-
- If a patch fails, read the file first
|
|
24
|
+
- If a patch fails, it means you didn't read the file first or the content doesn't match what you expected
|
|
@@ -83,12 +83,13 @@ When making changes to files, first understand the file's code conventions. Mimi
|
|
|
83
83
|
## File Editing Best Practices
|
|
84
84
|
|
|
85
85
|
**Using the `apply_patch` Tool** (Recommended):
|
|
86
|
+
- **CRITICAL**: ALWAYS read the target file immediately before creating a patch - never patch from memory
|
|
86
87
|
- Primary choice for targeted file edits - avoids rewriting entire files
|
|
87
88
|
- Only requires the specific lines you want to change
|
|
88
89
|
- Format: `*** Begin Patch` ... `*** Update File: path` ... `-old` / `+new` ... `*** End Patch`
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
- If patch fails,
|
|
90
|
+
- Workflow: 1) Read file, 2) Create patch based on what you just read, 3) Apply patch
|
|
91
|
+
- The `-` lines in your patch MUST match exactly what's in the file character-for-character
|
|
92
|
+
- If patch fails, it means the file content doesn't match - read it again and retry
|
|
92
93
|
|
|
93
94
|
**Using the `edit` Tool** (Alternative):
|
|
94
95
|
- Specify the file path and a list of operations
|
|
@@ -47,12 +47,13 @@ You have access to a rich set of specialized tools optimized for coding tasks:
|
|
|
47
47
|
## File Editing Best Practices
|
|
48
48
|
|
|
49
49
|
**Using the `apply_patch` Tool** (Recommended):
|
|
50
|
+
- **CRITICAL**: ALWAYS read the target file immediately before creating a patch - never patch from memory
|
|
50
51
|
- Primary choice for targeted file edits - avoids rewriting entire files
|
|
51
52
|
- Only requires the specific lines you want to change
|
|
52
53
|
- Format: `*** Begin Patch` ... `*** Update File: path` ... `-old` / `+new` ... `*** End Patch`
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
- If patch fails,
|
|
54
|
+
- Workflow: 1) Read file, 2) Create patch based on what you just read, 3) Apply patch
|
|
55
|
+
- The `-` lines in your patch MUST match exactly what's in the file character-for-character
|
|
56
|
+
- If patch fails, it means the file content doesn't match - read it again and retry
|
|
56
57
|
|
|
57
58
|
**Using the `edit` Tool** (Alternative):
|
|
58
59
|
- Specify the file path and a list of operations
|
|
@@ -238,7 +239,7 @@ You MUST adhere to the following criteria when solving queries:
|
|
|
238
239
|
- Working on the repo(s) in the current environment is allowed, even if they are proprietary.
|
|
239
240
|
- Analyzing code for vulnerabilities is allowed.
|
|
240
241
|
- Showing user code and tool call details is allowed.
|
|
241
|
-
- Use the `apply_patch` tool to edit files (NEVER try `applypatch` or `apply-patch`, only `apply_patch`): {"command":["apply_patch","*** Begin Patch\\n*** Update File: path/to/file.py\\n
|
|
242
|
+
- Use the `apply_patch` tool to edit files (NEVER try `applypatch` or `apply-patch`, only `apply_patch`): {"command":["apply_patch","*** Begin Patch\\n*** Update File: path/to/file.py\\n def example():\\n- pass\\n+ return 123\\n*** End Patch"]}
|
|
242
243
|
|
|
243
244
|
If completing the user's task requires writing or modifying files, your code and final answer should follow these coding guidelines, though user instructions (i.e. AGENTS.md) may override these guidelines:
|
|
244
245
|
|
|
@@ -29,12 +29,13 @@ call with multiple ops. Each separate `edit` operation re-reads the file fresh.
|
|
|
29
29
|
## File Editing Best Practices
|
|
30
30
|
|
|
31
31
|
**Using the `apply_patch` Tool** (Recommended):
|
|
32
|
+
- **CRITICAL**: ALWAYS read the target file immediately before creating a patch - never patch from memory
|
|
32
33
|
- Primary choice for targeted file edits - avoids rewriting entire files
|
|
33
34
|
- Only requires the specific lines you want to change
|
|
34
35
|
- Format: `*** Begin Patch` ... `*** Update File: path` ... `-old` / `+new` ... `*** End Patch`
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
- If patch fails,
|
|
36
|
+
- Workflow: 1) Read file, 2) Create patch based on what you just read, 3) Apply patch
|
|
37
|
+
- The `-` lines in your patch MUST match exactly what's in the file character-for-character
|
|
38
|
+
- If patch fails, it means the file content doesn't match - read it again and retry
|
|
38
39
|
|
|
39
40
|
**Using the `edit` Tool** (Alternative):
|
|
40
41
|
- Specify the file path and a list of operations
|
|
@@ -73,12 +73,13 @@ Your toolset includes specialized file editing and search tools. Follow these gu
|
|
|
73
73
|
## File Editing Best Practices
|
|
74
74
|
|
|
75
75
|
**Using the `apply_patch` Tool** (Recommended):
|
|
76
|
+
- **CRITICAL**: ALWAYS read the target file immediately before creating a patch - never patch from memory
|
|
76
77
|
- Primary choice for targeted file edits - avoids rewriting entire files
|
|
77
78
|
- Only requires the specific lines you want to change
|
|
78
79
|
- Format: `*** Begin Patch` ... `*** Update File: path` ... `-old` / `+new` ... `*** End Patch`
|
|
79
|
-
-
|
|
80
|
-
-
|
|
81
|
-
- If patch fails,
|
|
80
|
+
- Workflow: 1) Read file, 2) Create patch based on what you just read, 3) Apply patch
|
|
81
|
+
- The `-` lines in your patch MUST match exactly what's in the file character-for-character
|
|
82
|
+
- If patch fails, it means the file content doesn't match - read it again and retry
|
|
82
83
|
|
|
83
84
|
**Using the `edit` Tool** (Alternative):
|
|
84
85
|
- Specify the file path and a list of operations
|
|
@@ -227,7 +228,7 @@ You MUST adhere to the following criteria when solving queries:
|
|
|
227
228
|
- Working on the repo(s) in the current environment is allowed, even if they are proprietary.
|
|
228
229
|
- Analyzing code for vulnerabilities is allowed.
|
|
229
230
|
- Showing user code and tool call details is allowed.
|
|
230
|
-
- Use the `apply_patch` tool to edit files (NEVER try `applypatch` or `apply-patch`, only `apply_patch`): {"command":["apply_patch","*** Begin Patch\\n*** Update File: path/to/file.py\\n
|
|
231
|
+
- Use the `apply_patch` tool to edit files (NEVER try `applypatch` or `apply-patch`, only `apply_patch`): {"command":["apply_patch","*** Begin Patch\\n*** Update File: path/to/file.py\\n def example():\\n- pass\\n+ return 123\\n*** End Patch"]}
|
|
231
232
|
|
|
232
233
|
If completing the user's task requires writing or modifying files, your code and final answer should follow these coding guidelines, though user instructions (i.e. AGENTS.md) may override these guidelines:
|
|
233
234
|
|