@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agi-cli/sdk",
3
- "version": "0.1.79",
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": [
@@ -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 };
@@ -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
- ? findSubsequence(lines, expected, Math.max(0, hint - 3))
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 = findSubsequence(lines, expected, 0);
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
- throw new Error(
475
- `Failed to apply patch hunk in ${filePath}${contextInfo}.`,
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(projectRoot: string, patch: string) {
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(await applyUpdateOperation(projectRoot, operation));
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({ patch }: { patch: string; allowRejects?: boolean }) {
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
- throw new Error(
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
- 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;
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
- **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
 
@@ -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` helps locate the exact position, but the `-` lines must still match exactly.
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
+ }
@@ -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,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
- - 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
+ - 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
- - 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
+ - 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@@ def example():\\n- pass\\n+ return 123\\n*** End Patch"]}
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
- - 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
+ - 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
- - 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
+ - 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@@ def example():\\n- pass\\n+ return 123\\n*** End Patch"]}
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