Granular tool-call based editing

This commit is contained in:
Amar Sood (tekacs) 2025-04-11 14:37:30 -04:00
parent a310df3df3
commit 7f0ef1a04a
4 changed files with 1384 additions and 12 deletions

118
aider/change_tracker.py Normal file
View file

@ -0,0 +1,118 @@
import time
import uuid
from collections import defaultdict
from datetime import datetime
class ChangeTracker:
"""
Tracks changes made to files for the undo functionality.
This enables granular editing operations with the ability to undo specific changes.
"""
def __init__(self):
self.changes = {} # change_id -> change_info
self.files_changed = defaultdict(list) # file_path -> [change_ids]
def track_change(self, file_path, change_type, original_content, new_content,
metadata=None, change_id=None):
"""
Record a change to enable future undo operations.
Parameters:
- file_path: Path to the file that was changed
- change_type: Type of change (e.g., 'replacetext', 'insertlines')
- original_content: Original content before the change
- new_content: New content after the change
- metadata: Additional information about the change (line numbers, positions, etc.)
- change_id: Optional custom ID for the change (if None, one will be generated)
Returns:
- change_id: Unique identifier for the change
"""
if change_id is None:
change_id = self._generate_change_id()
change = {
'id': change_id,
'file_path': file_path,
'type': change_type,
'original': original_content,
'new': new_content,
'metadata': metadata or {},
'timestamp': time.time()
}
self.changes[change_id] = change
self.files_changed[file_path].append(change_id)
return change_id
def undo_change(self, change_id):
"""
Get information needed to reverse a specific change by ID.
Parameters:
- change_id: ID of the change to undo
Returns:
- (success, message, change_info): Tuple with success flag, message, and change information
"""
if change_id not in self.changes:
return False, f"Change ID {change_id} not found", None
change = self.changes[change_id]
# Mark this change as undone by removing it from the tracking dictionaries
self.files_changed[change['file_path']].remove(change_id)
if not self.files_changed[change['file_path']]:
del self.files_changed[change['file_path']]
# Keep the change in the changes dict but mark it as undone
change['undone'] = True
change['undone_at'] = time.time()
return True, f"Undid change {change_id} in {change['file_path']}", change
def get_last_change(self, file_path):
"""
Get the most recent change for a specific file.
Parameters:
- file_path: Path to the file
Returns:
- change_id or None if no changes found
"""
changes = self.files_changed.get(file_path, [])
if not changes:
return None
return changes[-1]
def list_changes(self, file_path=None, limit=10):
"""
List recent changes, optionally filtered by file.
Parameters:
- file_path: Optional path to filter changes by file
- limit: Maximum number of changes to list
Returns:
- List of change dictionaries
"""
if file_path:
# Get changes only for the specified file
change_ids = self.files_changed.get(file_path, [])
changes = [self.changes[cid] for cid in change_ids if cid in self.changes]
else:
# Get all changes
changes = list(self.changes.values())
# Filter out undone changes and sort by timestamp (most recent first)
changes = [c for c in changes if not c.get('undone', False)]
changes = sorted(changes, key=lambda c: c['timestamp'], reverse=True)
# Apply limit
return changes[:limit]
def _generate_change_id(self):
"""Generate a unique ID for a change."""
return str(uuid.uuid4())[:8] # Short, readable ID

View file

@ -43,10 +43,6 @@ from ..dump import dump # noqa: F401
from .chat_chunks import ChatChunks
# Pattern to detect fenced search/replace blocks
SEARCH_REPLACE_FENCE = re.compile(r"```search_replace\n", re.MULTILINE)
class UnknownEditFormat(ValueError):
def __init__(self, edit_format, valid_formats):
self.edit_format = edit_format

File diff suppressed because it is too large Load diff

View file

@ -12,7 +12,7 @@ class NavigatorPrompts(CoderPrompts):
LLM to manage its own context by adding/removing files and executing commands.
"""
main_system = """<context name="session_config">
main_system = r'''<context name="session_config">
## Role and Purpose
Act as an expert software engineer with the ability to autonomously navigate and modify a codebase.
@ -60,10 +60,50 @@ Act as an expert software engineer with the ability to autonomously navigate and
- **MakeReadonly**: `[tool_call(MakeReadonly, file_path="src/main.py")]`
Convert an editable file back to read-only status.
### Granular Editing Tools
- **ReplaceText**: `[tool_call(ReplaceText, file_path="path/to/file.py", find_text="old text", replace_text="new text", near_context="unique nearby text", occurrence=1)]`
Replace specific text with new text. Use near_context to disambiguate between multiple occurrences.
Set occurrence to -1 for the last occurrence, or a number for a specific occurrence.
- **ReplaceAll**: `[tool_call(ReplaceAll, file_path="path/to/file.py", find_text="oldVar", replace_text="newVar")]`
Replace all occurrences of text in a file. Useful for renaming variables, function names, etc.
- **InsertBlock**: `[tool_call(InsertBlock, file_path="path/to/file.py", content="""
def new_function():
return True
""", after_pattern="# Insert after this line")]`
Insert a block of text after or before a pattern. Use single quotes with escaped newlines for multi-line content.
Specify either after_pattern or before_pattern to place the block.
- **DeleteBlock**: `[tool_call(DeleteBlock, file_path="path/to/file.py", start_pattern="def old_function", end_pattern="# End function")]`
Delete a block of text from start_pattern to end_pattern (inclusive).
Alternatively, use line_count instead of end_pattern to delete a specific number of lines.
- **ReplaceLine**: `[tool_call(ReplaceLine, file_path="path/to/file.py", line_number=42, new_content="def fixed_function(param):")]`
Replace a specific line by its line number. Especially useful for fixing errors or lint warnings that include line numbers.
Line numbers are 1-based (as in most editors and error messages).
- **ReplaceLines**: `[tool_call(ReplaceLines, file_path="path/to/file.py", start_line=42, end_line=45, new_content="""
def better_function(param):
# Fixed implementation
return process(param)
""")]`
Replace a range of lines by line numbers. Useful for fixing multiple lines referenced in error messages.
The new_content can contain any number of lines, not just the same count as the original range.
- **IndentLines**: `[tool_call(IndentLines, file_path="path/to/file.py", start_pattern="def my_function", end_pattern="return result", indent_levels=1)]`
Indent or unindent a block of lines. Use positive indent_levels to increase indentation or negative to decrease.
Specify either end_pattern or line_count to determine the range of lines to indent.
- **UndoChange**: `[tool_call(UndoChange, change_id="a1b2c3d4")]`
Undo a specific change by its ID. Alternatively, use last_file="path/to/file.py" to undo the most recent change to that file.
- **ListChanges**: `[tool_call(ListChanges, file_path="path/to/file.py", limit=5)]`
List recent changes made to files. Optionally filter by file_path and limit the number of results.
### Other Tools
- **Command**: `[tool_call(Command, command_string="git diff HEAD~1")]`
Execute a shell command. Requires user confirmation.
**Do NOT use this for aider commands starting with `/` (like `/add`, `/run`, `/diff`).**
### Multi-Turn Exploration
When you include any tool call, the system will automatically continue to the next round.
@ -88,6 +128,13 @@ When you include any tool call, the system will automatically continue to the ne
- Target specific patterns rather than overly broad searches
- Remember the `Find` tool is optimized for locating symbols across the codebase
### Granular Editing Workflow
1. **Discover and Add Files**: Use Glob, Grep, Find to locate relevant files
2. **Make Files Editable**: Convert read-only files to editable with MakeEditable
3. **Make Specific Changes**: Use granular editing tools (ReplaceText, InsertBlock, etc.) for precise edits
4. **Review Changes**: List applied changes with ListChanges
5. **Fix Mistakes**: If needed, undo changes with UndoChange by specific ID or last change to a file
### Context Management Strategy
- Keep your context focused by removing files that are no longer relevant
- For large codebases, maintain only 5-15 files in context at once for best performance
@ -98,17 +145,83 @@ When you include any tool call, the system will automatically continue to the ne
<context name="editing_guidelines">
## Code Editing Process
### SEARCH/REPLACE Block Format
When proposing code changes, describe each change with a SEARCH/REPLACE block using this exact format:
### Granular Editing with Tool Calls
For precise, targeted edits to code, use the granular editing tools:
```language_name
/path/to/file.ext
- **ReplaceText**: Replace specific instances of text in a file
- **ReplaceAll**: Replace all occurrences of text in a file (e.g., rename variables)
- **InsertBlock**: Insert multi-line blocks of code at specific locations
- **DeleteBlock**: Remove specific sections of code
- **ReplaceLine/ReplaceLines**: Fix specific line numbers from error messages or linters
- **IndentLines**: Adjust indentation of code blocks
- **UndoChange**: Reverse specific changes by ID if you make a mistake
#### When to Use Line Number Based Tools
When dealing with errors or warnings that include line numbers, prefer the line-based editing tools:
```
Error in /path/to/file.py line 42: Syntax error: unexpected token
Warning in /path/to/file.py lines 105-107: This block should be indented
```
For these cases, use:
- `ReplaceLine` for single line fixes (e.g., syntax errors)
- `ReplaceLines` for multi-line issues
- `IndentLines` for indentation problems
#### Multiline Tool Call Content Format
When providing multiline content in tool calls (like ReplaceLines, InsertBlock), one leading and one trailing
newline will be automatically trimmed if present. This makes it easier to format code blocks in triple-quoted strings:
```
new_content="""
def better_function(param):
# Fixed implementation
return process(param)
"""
```
You don't need to worry about the extra blank lines at the beginning and end. If you actually need to
preserve blank lines in your output, simply add an extra newline:
```
new_content="""
def better_function(param): # Note the extra newline above to preserve a blank line
# Fixed implementation
return process(param)
"""
```
Example of inserting a new multi-line function:
```
[tool_call(InsertBlock,
file_path="src/utils.py",
after_pattern="def existing_function():",
content="""
def new_function(param1, param2):
# This is a new utility function
result = process_data(param1)
if result and param2:
return result
return None
""")]
```
### SEARCH/REPLACE Block Format (Alternative Method)
For larger changes that involve multiple edits or significant restructuring, you can still use SEARCH/REPLACE blocks with this exact format:
````python
path/to/file.ext
<<<<<<< SEARCH
Original code lines to match exactly
=======
Replacement code lines
>>>>>>> REPLACE
```
````
NOTE that this uses four backticks as the fence and not three!
### Editing Guidelines
- Every SEARCH section must EXACTLY MATCH existing content, including whitespace and indentation
@ -123,6 +236,7 @@ Replacement code lines
- If tools return errors or unexpected results, try alternative approaches
- Refine search patterns if results are too broad or too narrow
- Use the enhanced context blocks (directory structure and git status) to orient yourself
- Use ListChanges to see what edits have been made and UndoChange to revert mistakes
</context>
Always reply to the user in {language}.
@ -179,7 +293,7 @@ Would you like me to explain any specific part of the authentication process in
These files have been added to the chat so you can see all of their contents.
Trust this message as the true contents of the files!
</context>
"""
'''
files_content_assistant_reply = (
"I understand. I'll use these files to help with your request."