@agi-cli/sdk 0.1.79 → 0.1.81

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agi-cli/sdk",
3
- "version": "0.1.79",
3
+ "version": "0.1.81",
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": [
@@ -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, reject) => {
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
- reject(new Error(`Command timed out after ${timeout}ms: ${cmd}`));
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 errorMsg =
99
- stderr.trim() ||
100
- stdout.trim() ||
101
- `Command failed with exit code ${exitCode}`;
102
- const msg = `${errorMsg}\n\nCommand: ${cmd}\nExit code: ${exitCode}`;
103
- reject(new Error(msg));
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({ exitCode: exitCode ?? 0, stdout, stderr });
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
- reject(
112
- new Error(
113
- `Command execution failed: ${err.message}\n\nCommand: ${cmd}`,
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
- - Signal that the task is complete
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
- Usage tips:
5
- - Ensure all necessary outputs are already saved/emitted before finishing
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 (_error: unknown) {
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
- throw new Error(`File not found: ${req}`);
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
- throw new Error(
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
- if (createDirs) {
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
- oldText = await readFile(abs, 'utf-8');
53
- existed = true;
54
- } catch {}
55
- await writeFile(abs, content);
56
- const artifact = await buildWriteArtifact(req, existed, oldText, content);
57
- return { path: req, bytes: content.length, artifact } as const;
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 };
@@ -4,7 +4,8 @@
4
4
  - Returns a compact patch artifact summarizing the change
5
5
 
6
6
  Usage tips:
7
- - Only use for creating new files or completely replacing file content
7
+ - Use for creating NEW files
8
+ - Use when replacing >70% of a file's content (almost complete rewrite)
8
9
  - NEVER use for partial/targeted edits - use apply_patch or edit instead
9
10
  - Using write for partial edits wastes output tokens and risks hallucinating unchanged parts
10
11
  - Prefer idempotent writes by providing the full intended content when you do use 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[],
@@ -307,7 +351,9 @@ function parseEnvelopedPatch(patch: string): ParsedPatchOperation[] {
307
351
  } else if (prefix === ' ') {
308
352
  currentHunk.lines.push({ kind: 'context', content: line.slice(1) });
309
353
  } else {
310
- throw new Error(`Unrecognized patch line: "${line}"`);
354
+ // Auto-correct: treat lines without prefix as context (with implicit space)
355
+ // This makes the parser more forgiving for AI-generated patches
356
+ currentHunk.lines.push({ kind: 'context', content: line });
311
357
  }
312
358
  }
313
359
 
@@ -415,6 +461,7 @@ function applyHunksToLines(
415
461
  originalLines: string[],
416
462
  hunks: PatchHunk[],
417
463
  filePath: string,
464
+ useFuzzy: boolean = false,
418
465
  ): { lines: string[]; applied: AppliedHunkResult[] } {
419
466
  const lines = [...originalLines];
420
467
  let searchIndex = 0;
@@ -445,11 +492,16 @@ function applyHunksToLines(
445
492
  : searchIndex;
446
493
 
447
494
  let matchIndex = hasExpected
448
- ? findSubsequence(lines, expected, Math.max(0, hint - 3))
495
+ ? findSubsequenceWithFuzzy(
496
+ lines,
497
+ expected,
498
+ Math.max(0, hint - 3),
499
+ useFuzzy,
500
+ )
449
501
  : -1;
450
502
 
451
503
  if (hasExpected && matchIndex === -1) {
452
- matchIndex = findSubsequence(lines, expected, 0);
504
+ matchIndex = findSubsequenceWithFuzzy(lines, expected, 0, useFuzzy);
453
505
  }
454
506
 
455
507
  if (matchIndex === -1 && hasExpected && hunk.header.context) {
@@ -471,9 +523,20 @@ function applyHunksToLines(
471
523
  const contextInfo = hunk.header.context
472
524
  ? ` near context '${hunk.header.context}'`
473
525
  : '';
474
- throw new Error(
475
- `Failed to apply patch hunk in ${filePath}${contextInfo}.`,
476
- );
526
+
527
+ // Provide helpful error with nearby context
528
+ const nearbyStart = Math.max(0, hint - 2);
529
+ const nearbyEnd = Math.min(lines.length, hint + 5);
530
+ const nearbyLines = lines.slice(nearbyStart, nearbyEnd);
531
+ const lineNumberInfo =
532
+ nearbyStart > 0 ? ` (around line ${nearbyStart + 1})` : '';
533
+
534
+ let errorMsg = `Failed to apply patch hunk in ${filePath}${contextInfo}.\n`;
535
+ errorMsg += `Expected to find:\n${expected.map((l) => ` ${l}`).join('\n')}\n`;
536
+ errorMsg += `Nearby context${lineNumberInfo}:\n${nearbyLines.map((l, idx) => ` ${nearbyStart + idx + 1}: ${l}`).join('\n')}\n`;
537
+ errorMsg += `Hint: Check for whitespace differences (tabs vs spaces). Try enabling fuzzyMatch option.`;
538
+
539
+ throw new Error(errorMsg);
477
540
  }
478
541
 
479
542
  const deleteCount = hasExpected ? expected.length : 0;
@@ -532,6 +595,7 @@ function computeInsertionIndex(
532
595
  async function applyUpdateOperation(
533
596
  projectRoot: string,
534
597
  operation: PatchUpdateOperation,
598
+ useFuzzy: boolean = false,
535
599
  ): Promise<AppliedOperationRecord> {
536
600
  const targetPath = resolveProjectPath(projectRoot, operation.filePath);
537
601
  let originalContent: string;
@@ -549,6 +613,7 @@ async function applyUpdateOperation(
549
613
  originalLines,
550
614
  operation.hunks,
551
615
  operation.filePath,
616
+ useFuzzy,
552
617
  );
553
618
  ensureTrailingNewline(updatedLines);
554
619
  await writeFile(targetPath, joinLines(updatedLines, newline), 'utf-8');
@@ -665,7 +730,11 @@ function formatNormalizedPatch(operations: AppliedOperationRecord[]): string {
665
730
  return lines.join('\n');
666
731
  }
667
732
 
668
- async function applyEnvelopedPatch(projectRoot: string, patch: string) {
733
+ async function applyEnvelopedPatch(
734
+ projectRoot: string,
735
+ patch: string,
736
+ useFuzzy: boolean = false,
737
+ ) {
669
738
  const operations = parseEnvelopedPatch(patch);
670
739
  const applied: AppliedOperationRecord[] = [];
671
740
 
@@ -675,7 +744,9 @@ async function applyEnvelopedPatch(projectRoot: string, patch: string) {
675
744
  } else if (operation.kind === 'delete') {
676
745
  applied.push(await applyDeleteOperation(projectRoot, operation));
677
746
  } else {
678
- applied.push(await applyUpdateOperation(projectRoot, operation));
747
+ applied.push(
748
+ await applyUpdateOperation(projectRoot, operation, useFuzzy),
749
+ );
679
750
  }
680
751
  }
681
752
 
@@ -700,46 +771,98 @@ export function buildApplyPatchTool(projectRoot: string): {
700
771
  .describe(
701
772
  'Allow hunks to be rejected without failing the whole operation',
702
773
  ),
774
+ fuzzyMatch: z
775
+ .boolean()
776
+ .optional()
777
+ .default(true)
778
+ .describe(
779
+ 'Enable fuzzy matching with whitespace normalization (converts tabs to spaces for matching)',
780
+ ),
703
781
  }),
704
- async execute({ patch }: { patch: string; allowRejects?: boolean }) {
782
+ async execute({
783
+ patch,
784
+ fuzzyMatch,
785
+ }: {
786
+ patch: string;
787
+ allowRejects?: boolean;
788
+ fuzzyMatch?: boolean;
789
+ }): Promise<
790
+ ToolResponse<{
791
+ output: string;
792
+ changes: unknown[];
793
+ artifact: unknown;
794
+ }>
795
+ > {
796
+ if (!patch || patch.trim().length === 0) {
797
+ return createToolError(
798
+ 'Missing required parameter: patch',
799
+ 'validation',
800
+ {
801
+ parameter: 'patch',
802
+ value: patch,
803
+ suggestion: 'Provide patch content in enveloped format',
804
+ },
805
+ );
806
+ }
807
+
705
808
  if (
706
809
  !patch.includes(PATCH_BEGIN_MARKER) ||
707
810
  !patch.includes(PATCH_END_MARKER)
708
811
  ) {
709
- throw new Error(
812
+ return createToolError(
710
813
  'Only enveloped patch format is supported. Patch must start with "*** Begin Patch" and contain "*** Add File:", "*** Update File:", or "*** Delete File:" directives.',
814
+ 'validation',
815
+ {
816
+ parameter: 'patch',
817
+ suggestion:
818
+ 'Use enveloped patch format starting with *** Begin Patch',
819
+ },
711
820
  );
712
821
  }
713
822
 
714
- const { operations, normalizedPatch } = await applyEnvelopedPatch(
715
- projectRoot,
716
- patch,
717
- );
718
- const summary = summarizeOperations(operations);
719
- const changes = operations.map((operation) => ({
720
- filePath: operation.filePath,
721
- kind: operation.kind,
722
- hunks: operation.hunks.map((hunk) => ({
723
- oldStart: hunk.oldStart,
724
- oldLines: hunk.oldLines,
725
- newStart: hunk.newStart,
726
- newLines: hunk.newLines,
727
- additions: hunk.additions,
728
- deletions: hunk.deletions,
729
- context: hunk.header.context,
730
- })),
731
- }));
732
-
733
- return {
734
- ok: true,
735
- output: 'Applied enveloped patch',
736
- changes,
737
- artifact: {
738
- kind: 'file_diff',
739
- patch: normalizedPatch,
740
- summary,
741
- },
742
- } as const;
823
+ try {
824
+ const { operations, normalizedPatch } = await applyEnvelopedPatch(
825
+ projectRoot,
826
+ patch,
827
+ fuzzyMatch ?? true,
828
+ );
829
+ const summary = summarizeOperations(operations);
830
+ const changes = operations.map((operation) => ({
831
+ filePath: operation.filePath,
832
+ kind: operation.kind,
833
+ hunks: operation.hunks.map((hunk) => ({
834
+ oldStart: hunk.oldStart,
835
+ oldLines: hunk.oldLines,
836
+ newStart: hunk.newStart,
837
+ newLines: hunk.newLines,
838
+ additions: hunk.additions,
839
+ deletions: hunk.deletions,
840
+ context: hunk.header.context,
841
+ })),
842
+ }));
843
+
844
+ return {
845
+ ok: true,
846
+ output: 'Applied enveloped patch',
847
+ changes,
848
+ artifact: {
849
+ kind: 'file_diff',
850
+ patch: normalizedPatch,
851
+ summary,
852
+ },
853
+ };
854
+ } catch (error: unknown) {
855
+ const errorMessage =
856
+ error instanceof Error ? error.message : String(error);
857
+ return createToolError(
858
+ `Failed to apply patch: ${errorMessage}`,
859
+ 'execution',
860
+ {
861
+ suggestion:
862
+ 'Check that the patch format is correct and target files exist',
863
+ },
864
+ );
865
+ }
743
866
  },
744
867
  });
745
868
  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
- **IMPORTANT: Patches require EXACT line matches. If the file content has changed since you last read it, the patch will fail.**
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
 
@@ -63,7 +68,33 @@ All patches must be wrapped in markers and use explicit file directives:
63
68
  *** End Patch
64
69
  ```
65
70
 
66
- The `@@ context line` helps locate the exact position, but the `-` lines must still match exactly.
71
+ **IMPORTANT**:
72
+ - The `@@` line is an OPTIONAL hint to help locate the change - it's a comment, not parsed as context
73
+ - REQUIRED: Actual context lines (starting with space ` `) that match the file exactly
74
+ - The context lines with space prefix are what the tool uses to find the location
75
+ - The `@@` line just helps humans/AI understand what section you're editing
76
+
77
+
78
+ ### Update multiple locations in the same file:
79
+ ```
80
+ *** Begin Patch
81
+ *** Update File: src/app.ts
82
+ @@ first section - near line 10
83
+ function init() {
84
+ - const port = 3000;
85
+ + const port = 8080;
86
+ return port;
87
+ }
88
+ @@ second section - near line 25
89
+ function start() {
90
+ - console.log("Starting...");
91
+ + console.log("Server starting...");
92
+ init();
93
+ }
94
+ *** End Patch
95
+ ```
96
+
97
+ **IMPORTANT**: Use separate `@@` headers for each non-consecutive change location. This allows multiple edits to the same file in one patch, saving tokens and reducing tool calls.
67
98
 
68
99
  ### Delete a file:
69
100
  ```
@@ -89,11 +120,17 @@ The `@@ context line` helps locate the exact position, but the `-` lines must st
89
120
  - Lines starting with `+` are added
90
121
  - Lines starting with `-` are removed
91
122
  - Lines starting with ` ` (space) are context (kept unchanged)
92
- - Lines starting with `@@` provide context for finding the location
123
+ - Lines starting with `@@` are optional hints/comments (not parsed as context)
93
124
 
94
125
  ## Common Errors
95
126
 
96
- **"Failed to find expected lines"**: The file content doesn't match your patch. The file may have changed, or you may have mistyped the lines. Solution: Use the `edit` tool instead.
127
+ **"Failed to find expected lines"**: The file content doesn't match your patch. Common causes:
128
+ - Missing context lines (lines with space prefix)
129
+ - Using `@@` line as context instead of real context lines
130
+ - The file content has changed since you read it
131
+ - Whitespace/indentation mismatch
132
+
133
+ **Solution**: Always read the file immediately before patching and include actual context lines with space prefix.
97
134
 
98
135
  ## Important Notes
99
136
 
@@ -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
+ }
@@ -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 to understand its current state before generating a new patch
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,27 @@ 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
- - Only use when you have a complete unified diff ready
90
- - Ensure patch is based on current file content (not stale)
91
- - If patch fails, read the file first to see current state before retrying
90
+ - For multiple changes in one file: use multiple `@@` headers to separate non-consecutive hunks
91
+ - MUST include context lines (space prefix) - the `@@` line is just an optional hint
92
+ - Workflow: 1) Read file, 2) Create patch based on what you just read, 3) Apply patch
93
+ - The `-` lines in your patch MUST match exactly what's in the file character-for-character
94
+ - If patch fails, it means the file content doesn't match - read it again and retry
95
+ - **Best for**: Small, surgical edits to code files (< 50 line changes per file)
96
+ - **Struggles with**: Large restructures (> 50 lines), major section reorganizations
97
+
98
+ **Patch Format Reminder**:
99
+ ```
100
+ *** Update File: path
101
+ @@ optional hint ← Optional comment/hint (not parsed)
102
+ actual line from file ← Context (space prefix) - REQUIRED
103
+ -line to remove ← Remove this line
104
+ +line to add ← Add this line
105
+ more context ← More context (space prefix)
106
+ ```
92
107
 
93
108
  **Using the `edit` Tool** (Alternative):
94
109
  - Specify the file path and a list of operations
@@ -98,13 +113,14 @@ When making changes to files, first understand the file's code conventions. Mimi
98
113
  - When making multiple changes to a file, use ONE `edit` call with multiple ops
99
114
 
100
115
  **Using the `write` Tool** (Last Resort):
101
- - Only use when creating new files or completely replacing file content
116
+ - Use for creating NEW files
117
+ - Use when replacing >70% of a file's content (almost complete rewrite)
102
118
  - NEVER use for targeted edits - it rewrites the entire file
103
119
  - Wastes output tokens and risks hallucinating unchanged parts
104
120
 
105
121
  **Never**:
106
122
  - Use `write` for partial file edits (use `apply_patch` or `edit` instead)
107
- - Make multiple separate `edit` or `apply_patch` calls for the same file
123
+ - Make multiple separate `edit` or `apply_patch` calls for the same file (use multiple hunks with @@ headers or multiple ops instead)
108
124
  - Assume file content remains unchanged between operations
109
125
  - Use `bash` with `sed`/`awk` for programmatic file editing (use `edit` instead)
110
126
 
@@ -47,12 +47,27 @@ 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
- - Only use when you have a complete unified diff ready
54
- - Ensure patch is based on current file content (not stale)
55
- - If patch fails, read the file first to see current state before retrying
54
+ - For multiple changes in one file: use multiple `@@` headers to separate non-consecutive hunks
55
+ - MUST include context lines (space prefix) - the `@@` line is just an optional hint
56
+ - Workflow: 1) Read file, 2) Create patch based on what you just read, 3) Apply patch
57
+ - The `-` lines in your patch MUST match exactly what's in the file character-for-character
58
+ - If patch fails, it means the file content doesn't match - read it again and retry
59
+ - **Best for**: Small, surgical edits to code files (< 50 line changes per file)
60
+ - **Struggles with**: Large restructures (> 50 lines), major section reorganizations
61
+
62
+ **Patch Format Reminder**:
63
+ ```
64
+ *** Update File: path
65
+ @@ optional hint ← Optional comment/hint (not parsed)
66
+ actual line from file ← Context (space prefix) - REQUIRED
67
+ -line to remove ← Remove this line
68
+ +line to add ← Add this line
69
+ more context ← More context (space prefix)
70
+ ```
56
71
 
57
72
  **Using the `edit` Tool** (Alternative):
58
73
  - Specify the file path and a list of operations
@@ -62,13 +77,14 @@ You have access to a rich set of specialized tools optimized for coding tasks:
62
77
  - When making multiple changes to a file, use ONE `edit` call with multiple ops
63
78
 
64
79
  **Using the `write` Tool** (Last Resort):
65
- - Only use when creating new files or completely replacing file content
80
+ - Use for creating NEW files
81
+ - Use when replacing >70% of a file's content (almost complete rewrite)
66
82
  - NEVER use for targeted edits - it rewrites the entire file
67
83
  - Wastes output tokens and risks hallucinating unchanged parts
68
84
 
69
85
  **Never**:
70
86
  - Use `write` for partial file edits (use `apply_patch` or `edit` instead)
71
- - Make multiple separate `edit` or `apply_patch` calls for the same file
87
+ - Make multiple separate `edit` or `apply_patch` calls for the same file (use multiple hunks with @@ headers or multiple ops instead)
72
88
  - Assume file content remains unchanged between operations
73
89
  - Use `bash` with `sed`/`awk` for programmatic file editing (use `edit` instead)
74
90
 
@@ -238,7 +254,7 @@ You MUST adhere to the following criteria when solving queries:
238
254
  - Working on the repo(s) in the current environment is allowed, even if they are proprietary.
239
255
  - Analyzing code for vulnerabilities is allowed.
240
256
  - 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@@ def example():\\n- pass\\n+ return 123\\n*** End Patch"]}
257
+ - 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
258
 
243
259
  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
260
 
@@ -29,12 +29,27 @@ 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
- - Only use when you have a complete unified diff ready
36
- - Ensure patch is based on current file content (not stale)
37
- - If patch fails, read the file first to see current state before retrying
36
+ - For multiple changes in one file: use multiple `@@` headers to separate non-consecutive hunks
37
+ - MUST include context lines (space prefix) - the `@@` line is just an optional hint
38
+ - Workflow: 1) Read file, 2) Create patch based on what you just read, 3) Apply patch
39
+ - The `-` lines in your patch MUST match exactly what's in the file character-for-character
40
+ - If patch fails, it means the file content doesn't match - read it again and retry
41
+ - **Best for**: Small, surgical edits to code files (< 50 line changes per file)
42
+ - **Struggles with**: Large restructures (> 50 lines), major section reorganizations
43
+
44
+ **Patch Format Reminder**:
45
+ ```
46
+ *** Update File: path
47
+ @@ optional hint ← Optional comment/hint (not parsed)
48
+ actual line from file ← Context (space prefix) - REQUIRED
49
+ -line to remove ← Remove this line
50
+ +line to add ← Add this line
51
+ more context ← More context (space prefix)
52
+ ```
38
53
 
39
54
  **Using the `edit` Tool** (Alternative):
40
55
  - Specify the file path and a list of operations
@@ -44,13 +59,14 @@ call with multiple ops. Each separate `edit` operation re-reads the file fresh.
44
59
  - When making multiple changes to a file, use ONE `edit` call with multiple ops
45
60
 
46
61
  **Using the `write` Tool** (Last Resort):
47
- - Only use when creating new files or completely replacing file content
62
+ - Use for creating NEW files
63
+ - Use when replacing >70% of a file's content (almost complete rewrite)
48
64
  - NEVER use for targeted edits - it rewrites the entire file
49
65
  - Wastes output tokens and risks hallucinating unchanged parts
50
66
 
51
67
  **Never**:
52
68
  - Use `write` for partial file edits (use `apply_patch` or `edit` instead)
53
- - Make multiple separate `edit` or `apply_patch` calls for the same file
69
+ - Make multiple separate `edit` or `apply_patch` calls for the same file (use multiple hunks with @@ headers or multiple ops instead)
54
70
  - Assume file content remains unchanged between operations
55
71
  - Use `bash` with `sed`/`awk` for programmatic file editing (use `edit` instead)
56
72
 
@@ -73,12 +73,27 @@ 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
- - Only use when you have a complete unified diff ready
80
- - Ensure patch is based on current file content (not stale)
81
- - If patch fails, read the file first to see current state before retrying
80
+ - For multiple changes in one file: use multiple `@@` headers to separate non-consecutive hunks
81
+ - MUST include context lines (space prefix) - the `@@` line is just an optional hint
82
+ - Workflow: 1) Read file, 2) Create patch based on what you just read, 3) Apply patch
83
+ - The `-` lines in your patch MUST match exactly what's in the file character-for-character
84
+ - If patch fails, it means the file content doesn't match - read it again and retry
85
+ - **Best for**: Small, surgical edits to code files (< 50 line changes per file)
86
+ - **Struggles with**: Large restructures (> 50 lines), major section reorganizations
87
+
88
+ **Patch Format Reminder**:
89
+ ```
90
+ *** Update File: path
91
+ @@ optional hint ← Optional comment/hint (not parsed)
92
+ actual line from file ← Context (space prefix) - REQUIRED
93
+ -line to remove ← Remove this line
94
+ +line to add ← Add this line
95
+ more context ← More context (space prefix)
96
+ ```
82
97
 
83
98
  **Using the `edit` Tool** (Alternative):
84
99
  - Specify the file path and a list of operations
@@ -88,13 +103,14 @@ Your toolset includes specialized file editing and search tools. Follow these gu
88
103
  - When making multiple changes to a file, use ONE `edit` call with multiple ops
89
104
 
90
105
  **Using the `write` Tool** (Last Resort):
91
- - Only use when creating new files or completely replacing file content
106
+ - Use for creating NEW files
107
+ - Use when replacing >70% of a file's content (almost complete rewrite)
92
108
  - NEVER use for targeted edits - it rewrites the entire file
93
109
  - Wastes output tokens and risks hallucinating unchanged parts
94
110
 
95
111
  **Never**:
96
112
  - Use `write` for partial file edits (use `apply_patch` or `edit` instead)
97
- - Make multiple separate `edit` or `apply_patch` calls for the same file
113
+ - Make multiple separate `edit` or `apply_patch` calls for the same file (use multiple hunks with @@ headers or multiple ops instead)
98
114
  - Assume file content remains unchanged between operations
99
115
  - Use `bash` with `sed`/`awk` for programmatic file editing (use `edit` instead)
100
116
 
@@ -227,7 +243,7 @@ You MUST adhere to the following criteria when solving queries:
227
243
  - Working on the repo(s) in the current environment is allowed, even if they are proprietary.
228
244
  - Analyzing code for vulnerabilities is allowed.
229
245
  - 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@@ def example():\\n- pass\\n+ return 123\\n*** End Patch"]}
246
+ - 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
247
 
232
248
  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
249