From 3bbdacb056a7a3d380f2c9d04460458462ee5a81 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Sat, 8 Nov 2025 11:18:08 -0500 Subject: [PATCH 1/9] Bump Version --- aider/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/__init__.py b/aider/__init__.py index 88d5b450ff0..856da189bd7 100644 --- a/aider/__init__.py +++ b/aider/__init__.py @@ -1,6 +1,6 @@ from packaging import version -__version__ = "0.88.10.dev" +__version__ = "0.88.11.dev" safe_version = __version__ try: From f5edbf8e1f909df5d05affc7e08696869c5d074c Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Sat, 8 Nov 2025 12:09:21 -0500 Subject: [PATCH 2/9] Update debug output file locations to make sure aider's outputs are maximally neat --- aider/main.py | 14 ++++++++++---- aider/mcp/server.py | 3 ++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/aider/main.py b/aider/main.py index 729eabe6ec1..179e352bea7 100644 --- a/aider/main.py +++ b/aider/main.py @@ -493,9 +493,14 @@ def custom_tracer(frame, event, arg): line_no = frame.f_lineno if func_name not in file_blacklist: - log_file.write( - f"-> CALL (My Code): {func_name}() in {os.path.basename(filename)}:{line_no}\n" - ) + log_file.write(f"-> CALL: {func_name}() in {os.path.basename(filename)}:{line_no}\n") + + if event == "return": + func_name = frame.f_code.co_name + line_no = frame.f_lineno + + if func_name not in file_blacklist: + log_file.write(f"<- RETURN: {func_name}() in {os.path.basename(filename)}:{line_no}\n") # Must return the trace function (or a local one) for subsequent events return custom_tracer @@ -562,7 +567,8 @@ async def main_async(argv=None, input=None, output=None, force_git_root=None, re if args.debug: global log_file - log_file = open(".aider-debug.log", "w", buffering=1) + os.makedirs(".aider/logs/", exist_ok=True) + log_file = open(".aider/logs/debug.log", "w", buffering=1) sys.settrace(custom_tracer) if args.shell_completions: diff --git a/aider/mcp/server.py b/aider/mcp/server.py index 7fe770978a0..c2b3e52a483 100644 --- a/aider/mcp/server.py +++ b/aider/mcp/server.py @@ -51,7 +51,8 @@ async def connect(self): ) try: - with open(".aider-mcp-errors.log", "w") as err_file: + os.makedirs(".aider/logs/", exist_ok=True) + with open(".aider/logs/mcp-errors.log", "w") as err_file: stdio_transport = await self.exit_stack.enter_async_context( stdio_client(server_params, errlog=err_file) ) From bf333f68a9ab58a66e1e15c9e29f588a3ec9de91 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Sat, 8 Nov 2025 12:18:40 -0500 Subject: [PATCH 3/9] Rename "Navigator" mode to "Agent" mode for simplicity --- aider/args.py | 6 ++-- aider/coders/__init__.py | 4 +-- .../{navigator_coder.py => agent_coder.py} | 26 ++++++++-------- ...acy_prompts.py => agent_legacy_prompts.py} | 6 ++-- ...{navigator_prompts.py => agent_prompts.py} | 6 ++-- aider/coders/base_coder.py | 2 +- aider/commands.py | 30 +++++++++---------- aider/models.py | 4 +-- aider/repomap.py | 2 +- 9 files changed, 42 insertions(+), 44 deletions(-) rename aider/coders/{navigator_coder.py => agent_coder.py} (99%) rename aider/coders/{navigator_legacy_prompts.py => agent_legacy_prompts.py} (95%) rename aider/coders/{navigator_prompts.py => agent_prompts.py} (96%) diff --git a/aider/args.py b/aider/args.py index 0a1bb532b87..19954fe7f39 100644 --- a/aider/args.py +++ b/aider/args.py @@ -176,11 +176,11 @@ def get_parser(default_config_files, git_root): help="Use architect edit format for the main chat", ) group.add_argument( - "--navigator", + "--agent", action="store_const", dest="edit_format", - const="navigator", - help="Use navigator edit format for the main chat (autonomous file management)", + const="agent", + help="Use agent edit format for the main chat (autonomous file management)", ) group.add_argument( "--auto-accept-architect", diff --git a/aider/coders/__init__.py b/aider/coders/__init__.py index 648e36fb9a2..ebe4a47dd14 100644 --- a/aider/coders/__init__.py +++ b/aider/coders/__init__.py @@ -1,3 +1,4 @@ +from .agent_coder import AgentCoder from .architect_coder import ArchitectCoder from .ask_coder import AskCoder from .base_coder import Coder @@ -8,7 +9,6 @@ from .editor_editblock_coder import EditorEditBlockCoder from .editor_whole_coder import EditorWholeFileCoder from .help_coder import HelpCoder -from .navigator_coder import NavigatorCoder from .patch_coder import PatchCoder from .udiff_coder import UnifiedDiffCoder from .udiff_simple import UnifiedDiffSimpleCoder @@ -32,5 +32,5 @@ EditorWholeFileCoder, EditorDiffFencedCoder, ContextCoder, - NavigatorCoder, + AgentCoder, ] diff --git a/aider/coders/navigator_coder.py b/aider/coders/agent_coder.py similarity index 99% rename from aider/coders/navigator_coder.py rename to aider/coders/agent_coder.py index cfa0505353a..a0acacd17af 100644 --- a/aider/coders/navigator_coder.py +++ b/aider/coders/agent_coder.py @@ -87,10 +87,10 @@ from aider.tools.view_files_matching import execute_view_files_matching from aider.tools.view_files_with_symbol import _execute_view_files_with_symbol +from .agent_legacy_prompts import AgentLegacyPrompts +from .agent_prompts import AgentPrompts from .base_coder import ChatChunks, Coder from .editblock_coder import do_replace, find_original_update_blocks, find_similar_lines -from .navigator_legacy_prompts import NavigatorLegacyPrompts -from .navigator_prompts import NavigatorPrompts # UNUSED TOOL SCHEMAS # view_files_matching_schema, @@ -109,10 +109,10 @@ # show_numbered_context_schema, -class NavigatorCoder(Coder): +class AgentCoder(Coder): """Mode where the LLM autonomously manages which files are in context.""" - edit_format = "navigator" + edit_format = "agent" # TODO: We'll turn on granular editing by default once those tools stabilize use_granular_editing = False @@ -120,9 +120,7 @@ class NavigatorCoder(Coder): def __init__(self, *args, **kwargs): # Initialize appropriate prompt set before calling parent constructor # This needs to happen before super().__init__ so the parent class has access to gpt_prompts - self.gpt_prompts = ( - NavigatorPrompts() if self.use_granular_editing else NavigatorLegacyPrompts() - ) + self.gpt_prompts = AgentPrompts() if self.use_granular_editing else AgentLegacyPrompts() # Dictionary to track recently removed files self.recently_removed = {} @@ -159,8 +157,8 @@ def __init__(self, *args, **kwargs): ) self.max_files_per_glob = 50 # Maximum number of files to add at once via glob/grep - # Enable context management by default only in navigator mode - self.context_management_enabled = True # Enabled by default for navigator mode + # Enable context management by default only in agent mode + self.context_management_enabled = True # Enabled by default for agent mode # Initialize change tracker for granular editing self.change_tracker = ChangeTracker() @@ -484,7 +482,7 @@ def set_granular_editing(self, enabled): enabled (bool): True to use granular editing tools, False to use legacy search/replace """ self.use_granular_editing = enabled - self.gpt_prompts = NavigatorPrompts() if enabled else NavigatorLegacyPrompts() + self.gpt_prompts = AgentPrompts() if enabled else AgentLegacyPrompts() def get_context_symbol_outline(self): """ @@ -1001,7 +999,7 @@ async def reply_completed(self): edit_match = has_search_before and has_divider_before and has_replace_before if edit_match: - self.io.tool_output("Detected edit blocks, applying changes within Navigator...") + self.io.tool_output("Detected edit blocks, applying changes within Agent...") edited_files = await self._apply_edits_from_response() # If _apply_edits_from_response set a reflected_message (due to errors), # return False to trigger a reflection loop. @@ -2197,7 +2195,7 @@ def _process_file_mentions(self, content): # Get new files to add (not already in context) mentioned_files - current_files - # In navigator mode, we *only* add files via explicit tool commands (`View`, `ViewFilesAtGlob`, etc.). + # In agent mode, we *only* add files via explicit tool commands (`View`, `ViewFilesAtGlob`, etc.). # Do nothing here for implicit mentions. pass @@ -2205,11 +2203,11 @@ async def check_for_file_mentions(self, content): """ Override parent's method to use our own file processing logic. - Override parent's method to disable implicit file mention handling in navigator mode. + Override parent's method to disable implicit file mention handling in agent mode. Files should only be added via explicit tool commands (`View`, `ViewFilesAtGlob`, `ViewFilesMatching`, `ViewFilesWithSymbol`). """ - # Do nothing - disable implicit file adds in navigator mode. + # Do nothing - disable implicit file adds in agent mode. pass async def preproc_user_input(self, inp): diff --git a/aider/coders/navigator_legacy_prompts.py b/aider/coders/agent_legacy_prompts.py similarity index 95% rename from aider/coders/navigator_legacy_prompts.py rename to aider/coders/agent_legacy_prompts.py index 72beee97962..2d9963172d4 100644 --- a/aider/coders/navigator_legacy_prompts.py +++ b/aider/coders/agent_legacy_prompts.py @@ -3,11 +3,11 @@ from .base_prompts import CoderPrompts -class NavigatorLegacyPrompts(CoderPrompts): +class AgentLegacyPrompts(CoderPrompts): """ - Prompt templates for the Navigator mode, which enables autonomous codebase exploration. + Prompt templates for the Agent mode, which enables autonomous codebase exploration. - The NavigatorCoder uses these prompts to guide its behavior when exploring and modifying + The AgentCoder uses these prompts to guide its behavior when exploring and modifying a codebase using special tool commands like Glob, Grep, Add, etc. This mode enables the LLM to manage its own context by adding/removing files and executing commands. """ diff --git a/aider/coders/navigator_prompts.py b/aider/coders/agent_prompts.py similarity index 96% rename from aider/coders/navigator_prompts.py rename to aider/coders/agent_prompts.py index 1bf0a8a8466..a816b60bb2c 100644 --- a/aider/coders/navigator_prompts.py +++ b/aider/coders/agent_prompts.py @@ -3,11 +3,11 @@ from .base_prompts import CoderPrompts -class NavigatorPrompts(CoderPrompts): +class AgentPrompts(CoderPrompts): """ - Prompt templates for the Navigator mode, which enables autonomous codebase exploration. + Prompt templates for the Agent mode, which enables autonomous codebase exploration. - The NavigatorCoder uses these prompts to guide its behavior when exploring and modifying + The AgentCoder uses these prompts to guide its behavior when exploring and modifying a codebase using special tool commands like Glob, Grep, Add, etc. This mode enables the LLM to manage its own context by adding/removing files and executing commands. """ diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 6ea6ae4f9ec..d2ace56d64a 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -144,7 +144,7 @@ class Coder: tool_reflection = False # Context management settings (for all modes) - context_management_enabled = False # Disabled by default except for navigator mode + context_management_enabled = False # Disabled by default except for agent mode large_file_token_threshold = ( 25000 # Files larger than this will be truncated when context management is enabled ) diff --git a/aider/commands.py b/aider/commands.py index ed530550416..bfea15dc40a 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -494,7 +494,7 @@ def cmd_tokens(self, args): tokens = self.coder.main_model.token_count(repo_content) res.append((tokens, "repository map", "use --map-tokens to resize")) - # Enhanced context blocks (only for navigator mode) + # Enhanced context blocks (only for agent mode) if hasattr(self.coder, "use_enhanced_context") and self.coder.use_enhanced_context: # Force token calculation if it hasn't been done yet if hasattr(self.coder, "_calculate_context_block_tokens"): @@ -989,7 +989,7 @@ async def cmd_add(self, args): self.io.tool_output(f"Added {fname} to the chat") self.coder.check_added_files() - # Recalculate context block tokens if using navigator mode + # Recalculate context block tokens if using agent mode if ( hasattr(self.coder, "use_enhanced_context") and self.coder.use_enhanced_context @@ -1101,7 +1101,7 @@ def cmd_drop(self, args=""): self.io.tool_output(f"Removed {matched_file} from the chat") files_changed = True - # Recalculate context block tokens if any files were changed and using navigator mode + # Recalculate context block tokens if any files were changed and using agent mode if ( files_changed and hasattr(self.coder, "use_enhanced_context") @@ -1226,7 +1226,7 @@ def cmd_quit(self, args): def cmd_context_management(self, args=""): "Toggle context management for large files" if not hasattr(self.coder, "context_management_enabled"): - self.io.tool_error("Context management is only available in navigator mode.") + self.io.tool_error("Context management is only available in agent mode.") return # Toggle the setting @@ -1241,7 +1241,7 @@ def cmd_context_management(self, args=""): def cmd_context_blocks(self, args=""): "Toggle enhanced context blocks or print a specific block" if not hasattr(self.coder, "use_enhanced_context"): - self.io.tool_error("Enhanced context blocks are only available in navigator mode.") + self.io.tool_error("Enhanced context blocks are only available in agent mode.") return # If an argument is provided, try to print that specific context block @@ -1299,12 +1299,12 @@ def cmd_context_blocks(self, args=""): ) def cmd_granular_editing(self, args=""): - "Toggle granular editing tools in navigator mode" + "Toggle granular editing tools in agent mode" if not hasattr(self.coder, "use_granular_editing"): - self.io.tool_error("Granular editing toggle is only available in navigator mode.") + self.io.tool_error("Granular editing toggle is only available in agent mode.") return - # Toggle the setting using the navigator's method if available + # Toggle the setting using the agent's method if available new_state = not self.coder.use_granular_editing if hasattr(self.coder, "set_granular_editing"): @@ -1316,12 +1316,12 @@ def cmd_granular_editing(self, args=""): # Report the new state if self.coder.use_granular_editing: self.io.tool_output( - "Granular editing tools are now ON - navigator will use specific editing tools" + "Granular editing tools are now ON - agent will use specific editing tools" " instead of search/replace." ) else: self.io.tool_output( - "Granular editing tools are now OFF - navigator will use search/replace blocks for" + "Granular editing tools are now OFF - agent will use search/replace blocks for" " editing." ) @@ -1452,7 +1452,7 @@ def completions_architect(self): def completions_context(self): raise CommandCompletionException() - def completions_navigator(self): + def completions_agent(self): raise CommandCompletionException() async def cmd_ask(self, args): @@ -1471,14 +1471,14 @@ async def cmd_context(self, args): """Enter context mode to see surrounding code context. If no prompt provided, switches to context mode.""" # noqa return await self._generic_chat_command(args, "context", placeholder=args.strip() or None) - async def cmd_navigator(self, args): - """Enter navigator mode to autonomously discover and manage relevant files. If no prompt provided, switches to navigator mode.""" # noqa - # Enable context management when entering navigator mode + async def cmd_agent(self, args): + """Enter agent mode to autonomously discover and manage relevant files. If no prompt provided, switches to agent mode.""" # noqa + # Enable context management when entering agent mode if hasattr(self.coder, "context_management_enabled"): self.coder.context_management_enabled = True self.io.tool_output("Context management enabled for large files") - return await self._generic_chat_command(args, "navigator", placeholder=args.strip() or None) + return await self._generic_chat_command(args, "agent", placeholder=args.strip() or None) async def _generic_chat_command(self, args, edit_format, placeholder=None): if not args.strip(): diff --git a/aider/models.py b/aider/models.py index 4c09161d02a..8bcd3e07bb0 100644 --- a/aider/models.py +++ b/aider/models.py @@ -934,7 +934,7 @@ async def send_completion( kwargs["temperature"] = temperature # `tools` is for modern tool usage. `functions` is for legacy/forced calls. - # This handles `base_coder` sending both with same content for `navigator_coder`. + # This handles `base_coder` sending both with same content for `agent_coder`. effective_tools = tools if effective_tools is None and functions: @@ -945,7 +945,7 @@ async def send_completion( kwargs["tools"] = effective_tools # Forcing a function call is for legacy style `functions` with a single function. - # This is used by ArchitectCoder and not intended for NavigatorCoder's tools. + # This is used by ArchitectCoder and not intended for AgentCoder's tools. if functions and len(functions) == 1: function = functions[0] diff --git a/aider/repomap.py b/aider/repomap.py index 275fba3d206..40712213d45 100644 --- a/aider/repomap.py +++ b/aider/repomap.py @@ -87,7 +87,7 @@ class RepoMap: _initial_ident_to_files = None # Define kinds that typically represent definitions across languages - # Used by NavigatorCoder to filter tags for the symbol outline + # Used by AgentCoder to filter tags for the symbol outline definition_kinds = { "class", "struct", From 6413896e238c569971f8578421dbd190349bd702 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Sat, 8 Nov 2025 12:56:21 -0500 Subject: [PATCH 4/9] Update README.md with plans for agent mode --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 879fe204283..0358397759c 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,13 @@ The current priorities are to improve core capabilities and user experience of t * [x] Re-integrate pretty output formatting * [ ] Implement a response area, a prompt area with current auto completion capabilities, and a helper area for management utility commands +6. **Agent Mode** - [Discussion](https://github.com/dwash96/aider-ce/issues/111) + * [x] Renaming "navigator mode" to "agent mode" for simplicity + * [ ] Add an explicit "finished" internal tool + * [ ] Add a configuration json setting for agent mode to specify allowed local tools to use, tool call limits, etc. + * [ ] Add a RAG tool for the model to ask questions about the codebase + * [ ] Make the system prompts more aggressive about removing unneeded files/content from the context + ## Fork Additions This project aims to be compatible with upstream Aider, but with priority commits merged in and with some opportunistic bug fixes and optimizations From 62a80bc87c8f84dd8f219efd1f32df6d9a984e1b Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Sat, 8 Nov 2025 17:50:43 -0500 Subject: [PATCH 5/9] Update Agent Mode: - Modify local tools to have a common file structure - Refactor agent_coder so it can loop through the local tools instead of requiring them to be hard coded --- aider/args.py | 6 + aider/coders/agent_coder.py | 767 ++++++-------------------- aider/coders/agent_legacy_prompts.py | 103 ---- aider/coders/agent_prompts.py | 5 +- aider/coders/base_coder.py | 2 +- aider/commands.py | 27 - aider/tools/__init__.py | 109 ++-- aider/tools/command.py | 23 +- aider/tools/command_interactive.py | 23 +- aider/tools/delete_block.py | 41 +- aider/tools/delete_line.py | 27 +- aider/tools/delete_lines.py | 30 +- aider/tools/extract_lines.py | 44 +- aider/tools/finished.py | 48 ++ aider/tools/git.py | 142 ----- aider/tools/git_diff.py | 60 ++ aider/tools/git_log.py | 57 ++ aider/tools/git_show.py | 51 ++ aider/tools/git_status.py | 46 ++ aider/tools/grep.py | 39 +- aider/tools/indent_lines.py | 43 +- aider/tools/insert_block.py | 57 +- aider/tools/list_changes.py | 22 +- aider/tools/ls.py | 23 +- aider/tools/make_editable.py | 23 +- aider/tools/make_readonly.py | 23 +- aider/tools/remove.py | 23 +- aider/tools/replace_all.py | 30 +- aider/tools/replace_line.py | 31 +- aider/tools/replace_lines.py | 39 +- aider/tools/replace_text.py | 42 +- aider/tools/show_numbered_context.py | 30 +- aider/tools/undo_change.py | 22 +- aider/tools/update_todo_list.py | 27 +- aider/tools/view.py | 23 +- aider/tools/view_files_at_glob.py | 70 --- aider/tools/view_files_matching.py | 26 +- aider/tools/view_files_with_symbol.py | 23 +- aider/tools/view_todo_list.py | 57 -- 39 files changed, 1220 insertions(+), 1064 deletions(-) delete mode 100644 aider/coders/agent_legacy_prompts.py create mode 100644 aider/tools/finished.py delete mode 100644 aider/tools/git.py create mode 100644 aider/tools/git_diff.py create mode 100644 aider/tools/git_log.py create mode 100644 aider/tools/git_show.py create mode 100644 aider/tools/git_status.py delete mode 100644 aider/tools/view_files_at_glob.py delete mode 100644 aider/tools/view_todo_list.py diff --git a/aider/args.py b/aider/args.py index 19954fe7f39..4c8865f0040 100644 --- a/aider/args.py +++ b/aider/args.py @@ -888,6 +888,12 @@ def get_parser(default_config_files, git_root): " or home directory)" ), ).complete = shtab.FILE + group.add_argument( + "--agent-config", + metavar="AGENT_CONFIG_JSON", + help="Specify Agent Mode configuration as a JSON string", + default=None, + ) # This is a duplicate of the argument in the preparser and is a no-op by this time of # argument parsing, but it's here so that the help is displayed as expected. group.add_argument( diff --git a/aider/coders/agent_coder.py b/aider/coders/agent_coder.py index a0acacd17af..6076185cd7d 100644 --- a/aider/coders/agent_coder.py +++ b/aider/coders/agent_coder.py @@ -23,104 +23,53 @@ from aider.mcp.server import LocalServer from aider.repo import ANY_GIT_ERROR -# Import run_cmd for potentially interactive execution and run_cmd_subprocess for guaranteed non-interactive +# Import tool modules for registry from aider.tools import ( - command_interactive_schema, - command_schema, - delete_block_schema, - delete_line_schema, - delete_lines_schema, - extract_lines_schema, - grep_schema, - indent_lines_schema, - insert_block_schema, - list_changes_schema, - ls_schema, - make_editable_schema, - make_readonly_schema, - remove_schema, - replace_all_schema, - replace_line_schema, - replace_lines_schema, - replace_text_schema, - show_numbered_context_schema, - undo_change_schema, - update_todo_list_schema, - view_files_matching_schema, - view_files_with_symbol_schema, - view_schema, + command, + command_interactive, + delete_block, + delete_line, + delete_lines, + extract_lines, + finished, + git_diff, + git_log, + git_show, + git_status, + grep, + indent_lines, + insert_block, + list_changes, + ls, + make_editable, + make_readonly, + remove, + replace_all, + replace_line, + replace_lines, + replace_text, + show_numbered_context, + undo_change, + update_todo_list, + view, + view_files_matching, + view_files_with_symbol, ) -from aider.tools.command import _execute_command -from aider.tools.command_interactive import _execute_command_interactive -from aider.tools.delete_block import _execute_delete_block -from aider.tools.delete_line import _execute_delete_line -from aider.tools.delete_lines import _execute_delete_lines -from aider.tools.extract_lines import _execute_extract_lines -from aider.tools.git import ( - _execute_git_diff, - _execute_git_log, - _execute_git_show, - _execute_git_status, - git_diff_schema, - git_log_schema, - git_show_schema, - git_status_schema, -) -from aider.tools.grep import _execute_grep -from aider.tools.indent_lines import _execute_indent_lines -from aider.tools.insert_block import _execute_insert_block -from aider.tools.list_changes import _execute_list_changes -from aider.tools.ls import execute_ls -from aider.tools.make_editable import _execute_make_editable -from aider.tools.make_readonly import _execute_make_readonly -from aider.tools.remove import _execute_remove -from aider.tools.replace_all import _execute_replace_all -from aider.tools.replace_line import _execute_replace_line -from aider.tools.replace_lines import _execute_replace_lines -from aider.tools.replace_text import _execute_replace_text -from aider.tools.show_numbered_context import execute_show_numbered_context -from aider.tools.undo_change import _execute_undo_change -from aider.tools.update_todo_list import _execute_update_todo_list -from aider.tools.view import execute_view - -# Import tool functions -from aider.tools.view_files_matching import execute_view_files_matching -from aider.tools.view_files_with_symbol import _execute_view_files_with_symbol - -from .agent_legacy_prompts import AgentLegacyPrompts + from .agent_prompts import AgentPrompts from .base_coder import ChatChunks, Coder from .editblock_coder import do_replace, find_original_update_blocks, find_similar_lines -# UNUSED TOOL SCHEMAS -# view_files_matching_schema, -# grep_schema, -# replace_all_schema, -# insert_block_schema, -# delete_block_schema, -# replace_line_schema, -# replace_lines_schema, -# indent_lines_schema, -# delete_line_schema, -# delete_lines_schema, -# undo_change_schema, -# list_changes_schema, -# extract_lines_schema, -# show_numbered_context_schema, - class AgentCoder(Coder): """Mode where the LLM autonomously manages which files are in context.""" edit_format = "agent" - # TODO: We'll turn on granular editing by default once those tools stabilize - use_granular_editing = False - def __init__(self, *args, **kwargs): # Initialize appropriate prompt set before calling parent constructor # This needs to happen before super().__init__ so the parent class has access to gpt_prompts - self.gpt_prompts = AgentPrompts() if self.use_granular_editing else AgentLegacyPrompts() + self.gpt_prompts = AgentPrompts() # Dictionary to track recently removed files self.recently_removed = {} @@ -163,6 +112,9 @@ def __init__(self, *args, **kwargs): # Initialize change tracker for granular editing self.change_tracker = ChangeTracker() + # Initialize tool registry + self._tool_registry = self._build_tool_registry() + # Track files added during current exploration self.files_added_in_exploration = set() @@ -184,39 +136,71 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - def get_local_tool_schemas(self): - """Returns the JSON schemas for all local tools.""" - return [ - view_files_matching_schema, - ls_schema, - view_schema, - remove_schema, - make_editable_schema, - make_readonly_schema, - view_files_with_symbol_schema, - command_schema, - command_interactive_schema, - grep_schema, - replace_text_schema, - replace_all_schema, - insert_block_schema, - delete_block_schema, - replace_line_schema, - replace_lines_schema, - indent_lines_schema, - delete_line_schema, - delete_lines_schema, - undo_change_schema, - list_changes_schema, - extract_lines_schema, - show_numbered_context_schema, - update_todo_list_schema, - git_diff_schema, - git_log_schema, - git_show_schema, - git_status_schema, + def _build_tool_registry(self): + """ + Build a registry of available tools with their normalized names and process_response functions. + + Returns: + dict: Mapping of normalized tool names to tool modules + """ + registry = {} + + # Add tools that have been imported + tool_modules = [ + command, + command_interactive, + delete_block, + delete_line, + delete_lines, + extract_lines, + finished, + git_diff, + git_log, + git_show, + git_status, + grep, + indent_lines, + insert_block, + list_changes, + ls, + make_editable, + make_readonly, + remove, + replace_all, + replace_line, + replace_lines, + replace_text, + show_numbered_context, + undo_change, + update_todo_list, + view, + view_files_matching, + view_files_with_symbol, ] + for module in tool_modules: + if hasattr(module, "NORM_NAME") and hasattr(module, "process_response"): + registry[module.NORM_NAME] = module + + return registry + + def get_local_tool_schemas(self): + """Returns the JSON schemas for all local tools using the tool registry.""" + schemas = [] + + # Get schemas from the tool registry + for tool_module in self._tool_registry.values(): + if hasattr(tool_module, "schema"): + schemas.append(tool_module.schema) + + # Add git schemas from the tool registry + git_tools = [git_diff, git_log, git_show, git_status] + for git_tool in git_tools: + if hasattr(git_tool, "schema"): + schemas.append(git_tool.schema) + + return schemas + async def initialize_mcp_tools(self): await super().initialize_mcp_tools() @@ -266,47 +250,39 @@ async def _execute_local_tool_calls(self, tool_calls_list): norm_tool_name = tool_name.lower() tasks = [] - tool_functions = { - "viewfilesmatching": execute_view_files_matching, - "ls": execute_ls, - "view": execute_view, - "remove": _execute_remove, - "makeeditable": _execute_make_editable, - "makereadonly": _execute_make_readonly, - "viewfileswithsymbol": _execute_view_files_with_symbol, - "command": _execute_command, - "commandinteractive": _execute_command_interactive, - "grep": _execute_grep, - "replacetext": _execute_replace_text, - "replaceall": _execute_replace_all, - "insertblock": _execute_insert_block, - "deleteblock": _execute_delete_block, - "replaceline": _execute_replace_line, - "replacelines": _execute_replace_lines, - "indentlines": _execute_indent_lines, - "deleteline": _execute_delete_line, - "deletelines": _execute_delete_lines, - "undochange": _execute_undo_change, - "listchanges": _execute_list_changes, - "extractlines": _execute_extract_lines, - "shownumberedcontext": execute_show_numbered_context, - "updatetodolist": _execute_update_todo_list, - "git_diff": _execute_git_diff, - "git_log": _execute_git_log, - "git_show": _execute_git_show, - "git_status": _execute_git_status, - } - func = tool_functions.get(norm_tool_name) - - if func: + # Use the tool registry for execution + if norm_tool_name in self._tool_registry: + tool_module = self._tool_registry[norm_tool_name] for params in parsed_args_list: - if asyncio.iscoroutinefunction(func): - tasks.append(func(self, **params)) + # Use the process_response function from the tool module + result = tool_module.process_response(self, params) + # Handle async functions + if asyncio.iscoroutine(result): + tasks.append(result) else: - tasks.append(asyncio.to_thread(func, self, **params)) + tasks.append(asyncio.to_thread(lambda: result)) else: - all_results_content.append(f"Error: Unknown local tool name '{tool_name}'") + # Handle MCP tools for tools not in registry + if self.mcp_tools: + for server_name, server_tools in self.mcp_tools: + if any( + t.get("function", {}).get("name") == norm_tool_name + for t in server_tools + ): + server = next( + (s for s in self.mcp_servers if s.name == server_name), None + ) + if server: + for params in parsed_args_list: + tasks.append( + self._execute_mcp_tool(server, norm_tool_name, params) + ) + break + else: + all_results_content.append(f"Error: Unknown tool name '{tool_name}'") + else: + all_results_content.append(f"Error: Unknown tool name '{tool_name}'") if tasks: task_results = await asyncio.gather(*tasks) @@ -474,16 +450,6 @@ def get_cached_context_block(self, block_name): # Otherwise generate and cache the block return self._generate_context_block(block_name) - def set_granular_editing(self, enabled): - """ - Switch between granular editing tools and legacy search/replace. - - Args: - enabled (bool): True to use granular editing tools, False to use legacy search/replace - """ - self.use_granular_editing = enabled - self.gpt_prompts = AgentPrompts() if enabled else AgentLegacyPrompts() - def get_context_symbol_outline(self): """ Generate a symbol outline for files currently in context using Tree-sitter, @@ -929,35 +895,8 @@ async def reply_completed(self): iteratively discover and analyze relevant files before providing a final answer to the user's question. """ - # In granular editing mode, tool calls are handled by BaseCoder's process_tool_calls, - # which is overridden in this class to track tool usage. This method is now only for - # legacy tool call format and search/replace blocks. - if self.use_granular_editing: - # Handle SEARCH/REPLACE blocks - content = self.partial_response_content - if not content or not content.strip(): - return True - - # Check for search/replace blocks - has_search = "<<<<<<< SEARCH" in content - has_divider = "=======" in content - has_replace = ">>>>>>> REPLACE" in content - if has_search and has_divider and has_replace: - self.io.tool_output("Detected edit blocks, applying changes...") - edited_files = await self._apply_edits_from_response() - if self.reflected_message: - return False # Trigger reflection if edits failed - - # If edits were successful, we might want to reflect. - # For now, let's consider the turn complete. - - # Since tool calls are handled earlier, we finalize the turn. - self.tool_call_count = 0 - self.files_added_in_exploration = set() - self.move_back_cur_messages(None) - return True - # Legacy tool call processing for use_granular_editing=False + self.agent_finished = False content = self.partial_response_content if not content or not content.strip(): if len(self.tool_usage_history) > self.tool_usage_retries: @@ -974,6 +913,9 @@ async def reply_completed(self): tool_names_this_turn, ) = await self._process_tool_commands(content) + if self.agent_finished: + return True + # Since we are no longer suppressing, the partial_response_content IS the final content. # We might want to update it to the processed_content (without tool calls) if we don't # want the raw tool calls to remain in the final assistant message history. @@ -1106,7 +1048,47 @@ async def reply_completed(self): self.move_back_cur_messages( None ) # Pass None as we handled commit message earlier if needed - return True # Indicate exploration is finished for this round + + return False # Always Loop Until the Finished Tool is Called + + async def _execute_tool_with_registry(self, norm_tool_name, params): + """ + Execute a tool using the tool registry. + + Args: + norm_tool_name: Normalized tool name (lowercase) + params: Dictionary of parameters + + Returns: + str: Result message + """ + # Check if tool exists in registry + if norm_tool_name in self._tool_registry: + tool_module = self._tool_registry[norm_tool_name] + try: + # Use the process_response function from the tool module + result = tool_module.process_response(self, params) + # Handle async functions + if asyncio.iscoroutine(result): + result = await result + return result + except Exception as e: + self.io.tool_error( + f"Error during {norm_tool_name} execution: {e}\n{traceback.format_exc()}" + ) + return f"Error executing {norm_tool_name}: {str(e)}" + + # Handle MCP tools for tools not in registry + if self.mcp_tools: + for server_name, server_tools in self.mcp_tools: + if any(t.get("function", {}).get("name") == norm_tool_name for t in server_tools): + server = next((s for s in self.mcp_servers if s.name == server_name), None) + if server: + return await self._execute_mcp_tool(server, norm_tool_name, params) + else: + return f"Error: Could not find server instance for {server_name}" + + return f"Error: Unknown tool name '{norm_tool_name}'" async def _process_tool_commands(self, content): """ @@ -1402,412 +1384,13 @@ async def _process_tool_commands(self, content): result_messages.append(f"[Result (Parse Error): {result_message}]") continue - # Execute the tool based on its name + # Execute the tool using the registry try: # Normalize tool name for case-insensitive matching norm_tool_name = tool_name.lower() - if norm_tool_name == "viewfilesmatching": - pattern = params.get("pattern") - file_pattern = params.get("file_pattern") # Optional - regex = params.get("regex", False) # Default to False if not provided - if pattern is not None: - result_message = execute_view_files_matching( - self, pattern=pattern, file_pattern=file_pattern, regex=regex - ) - else: - result_message = "Error: Missing 'pattern' parameter for ViewFilesMatching" - elif norm_tool_name == "ls": - directory = params.get("directory") - if directory is not None: - result_message = execute_ls(self, directory) - else: - result_message = "Error: Missing 'directory' parameter for Ls" - elif norm_tool_name == "view": - file_path = params.get("file_path") - if file_path is not None: - result_message = execute_view(self, file_path) - else: - result_message = "Error: Missing 'file_path' parameter for View" - elif norm_tool_name == "remove": - file_path = params.get("file_path") - if file_path is not None: - result_message = _execute_remove(self, file_path) - else: - result_message = "Error: Missing 'file_path' parameter for Remove" - elif norm_tool_name == "makeeditable": - file_path = params.get("file_path") - if file_path is not None: - result_message = _execute_make_editable(self, file_path) - else: - result_message = "Error: Missing 'file_path' parameter for MakeEditable" - elif norm_tool_name == "makereadonly": - file_path = params.get("file_path") - if file_path is not None: - result_message = _execute_make_readonly(self, file_path) - else: - result_message = "Error: Missing 'file_path' parameter for MakeReadonly" - elif norm_tool_name == "viewfileswithsymbol": - symbol = params.get("symbol") - if symbol is not None: - # Call the imported function from the tools directory - result_message = _execute_view_files_with_symbol(self, symbol) - else: - result_message = "Error: Missing 'symbol' parameter for ViewFilesWithSymbol" - - # Command tools - elif norm_tool_name == "command": - command_string = params.get("command_string") - if command_string is not None: - result_message = await _execute_command(self, command_string) - else: - result_message = "Error: Missing 'command_string' parameter for Command" - elif norm_tool_name == "commandinteractive": - command_string = params.get("command_string") - if command_string is not None: - result_message = await _execute_command_interactive(self, command_string) - else: - result_message = ( - "Error: Missing 'command_string' parameter for CommandInteractive" - ) - - # Grep tool - elif norm_tool_name == "grep": - pattern = params.get("pattern") - file_pattern = params.get("file_pattern", "*") # Default to all files - directory = params.get("directory", ".") # Default to current directory - use_regex = params.get("use_regex", False) # Default to literal search - case_insensitive = params.get( - "case_insensitive", False - ) # Default to case-sensitive - context_before = params.get("context_before", 5) - context_after = params.get("context_after", 5) - - if pattern is not None: - result_message = await asyncio.to_thread( - _execute_grep, - self, - pattern, - file_pattern, - directory, - use_regex, - case_insensitive, - context_before, - context_after, - ) - else: - result_message = "Error: Missing required 'pattern' parameter for Grep" - - # Granular editing tools - elif norm_tool_name == "replacetext": - file_path = params.get("file_path") - find_text = params.get("find_text") - replace_text = params.get("replace_text") - near_context = params.get("near_context") - occurrence = params.get("occurrence", 1) # Default to first occurrence - change_id = params.get("change_id") - dry_run = params.get("dry_run", False) # Default to False - - if file_path is not None and find_text is not None and replace_text is not None: - result_message = _execute_replace_text( - self, - file_path, - find_text, - replace_text, - near_context, - occurrence, - change_id, - dry_run, - ) - else: - result_message = ( - "Error: Missing required parameters for ReplaceText (file_path," - " find_text, replace_text)" - ) - - elif norm_tool_name == "replaceall": - file_path = params.get("file_path") - find_text = params.get("find_text") - replace_text = params.get("replace_text") - change_id = params.get("change_id") - dry_run = params.get("dry_run", False) # Default to False - - if file_path is not None and find_text is not None and replace_text is not None: - result_message = _execute_replace_all( - self, file_path, find_text, replace_text, change_id, dry_run - ) - else: - result_message = ( - "Error: Missing required parameters for ReplaceAll (file_path," - " find_text, replace_text)" - ) - - elif norm_tool_name == "insertblock": - file_path = params.get("file_path") - content = params.get("content") - after_pattern = params.get("after_pattern") - before_pattern = params.get("before_pattern") - occurrence = params.get("occurrence", 1) # Default 1 - change_id = params.get("change_id") - dry_run = params.get("dry_run", False) # Default False - position = params.get("position") - auto_indent = params.get("auto_indent", True) # Default True - use_regex = params.get("use_regex", False) # Default False - - if ( - file_path is not None - and content is not None - and ( - after_pattern is not None - or before_pattern is not None - or position is not None - ) - ): - result_message = _execute_insert_block( - self, - file_path, - content, - after_pattern, - before_pattern, - occurrence, - change_id, - dry_run, - position, - auto_indent, - use_regex, - ) - - else: - result_message = ( - "Error: Missing required parameters for InsertBlock (file_path," - " content, and either after_pattern or before_pattern)" - ) - - elif norm_tool_name == "deleteblock": - file_path = params.get("file_path") - start_pattern = params.get("start_pattern") - end_pattern = params.get("end_pattern") - line_count = params.get("line_count") - near_context = params.get("near_context") # New - occurrence = params.get("occurrence", 1) # New, default 1 - change_id = params.get("change_id") - dry_run = params.get("dry_run", False) # New, default False - - if file_path is not None and start_pattern is not None: - result_message = _execute_delete_block( - self, - file_path, - start_pattern, - end_pattern, - line_count, - near_context, - occurrence, - change_id, - dry_run, - ) - else: - result_message = ( - "Error: Missing required parameters for DeleteBlock (file_path," - " start_pattern)" - ) - - elif norm_tool_name == "replaceline": - file_path = params.get("file_path") - line_number = params.get("line_number") - new_content = params.get("new_content") - change_id = params.get("change_id") - dry_run = params.get("dry_run", False) # New, default False - - if ( - file_path is not None - and line_number is not None - and new_content is not None - ): - result_message = _execute_replace_line( - self, file_path, line_number, new_content, change_id, dry_run - ) - else: - result_message = ( - "Error: Missing required parameters for ReplaceLine (file_path," - " line_number, new_content)" - ) - - elif norm_tool_name == "replacelines": - file_path = params.get("file_path") - start_line = params.get("start_line") - end_line = params.get("end_line") - new_content = params.get("new_content") - change_id = params.get("change_id") - dry_run = params.get("dry_run", False) # New, default False - - if ( - file_path is not None - and start_line is not None - and end_line is not None - and new_content is not None - ): - result_message = _execute_replace_lines( - self, file_path, start_line, end_line, new_content, change_id, dry_run - ) - else: - result_message = ( - "Error: Missing required parameters for ReplaceLines (file_path," - " start_line, end_line, new_content)" - ) - - elif norm_tool_name == "indentlines": - file_path = params.get("file_path") - start_pattern = params.get("start_pattern") - end_pattern = params.get("end_pattern") - line_count = params.get("line_count") - indent_levels = params.get("indent_levels", 1) # Default to indent 1 level - near_context = params.get("near_context") # New - occurrence = params.get("occurrence", 1) # New, default 1 - change_id = params.get("change_id") - dry_run = params.get("dry_run", False) # New, default False - - if file_path is not None and start_pattern is not None: - result_message = _execute_indent_lines( - self, - file_path, - start_pattern, - end_pattern, - line_count, - indent_levels, - near_context, - occurrence, - change_id, - dry_run, - ) - else: - result_message = ( - "Error: Missing required parameters for IndentLines (file_path," - " start_pattern)" - ) - - elif norm_tool_name == "deleteline": - file_path = params.get("file_path") - line_number = params.get("line_number") - change_id = params.get("change_id") - dry_run = params.get("dry_run", False) - - if file_path is not None and line_number is not None: - result_message = _execute_delete_line( - self, file_path, line_number, change_id, dry_run - ) - else: - result_message = ( - "Error: Missing required parameters for DeleteLine (file_path," - " line_number)" - ) - - elif norm_tool_name == "deletelines": - file_path = params.get("file_path") - start_line = params.get("start_line") - end_line = params.get("end_line") - change_id = params.get("change_id") - dry_run = params.get("dry_run", False) - - if file_path is not None and start_line is not None and end_line is not None: - result_message = _execute_delete_lines( - self, file_path, start_line, end_line, change_id, dry_run - ) - else: - result_message = ( - "Error: Missing required parameters for DeleteLines (file_path," - " start_line, end_line)" - ) - - elif norm_tool_name == "undochange": - change_id = params.get("change_id") - file_path = params.get("file_path") - - result_message = _execute_undo_change(self, change_id, file_path) - - elif norm_tool_name == "listchanges": - file_path = params.get("file_path") - limit = params.get("limit", 10) - - result_message = _execute_list_changes(self, file_path, limit) - - elif norm_tool_name == "extractlines": - source_file_path = params.get("source_file_path") - target_file_path = params.get("target_file_path") - start_pattern = params.get("start_pattern") - end_pattern = params.get("end_pattern") - line_count = params.get("line_count") - near_context = params.get("near_context") - occurrence = params.get("occurrence", 1) - dry_run = params.get("dry_run", False) - - if source_file_path and target_file_path and start_pattern: - result_message = _execute_extract_lines( - self, - source_file_path, - target_file_path, - start_pattern, - end_pattern, - line_count, - near_context, - occurrence, - dry_run, - ) - else: - result_message = ( - "Error: Missing required parameters for ExtractLines (source_file_path," - " target_file_path, start_pattern)" - ) - - elif norm_tool_name == "shownumberedcontext": - file_path = params.get("file_path") - pattern = params.get("pattern") - line_number = params.get("line_number") - context_lines = params.get("context_lines", 3) # Default context - - if file_path is not None and (pattern is not None or line_number is not None): - result_message = execute_show_numbered_context( - self, file_path, pattern, line_number, context_lines - ) - else: - result_message = ( - "Error: Missing required parameters for ViewNumberedContext (file_path" - " and either pattern or line_number)" - ) - - elif norm_tool_name == "updatetodolist": - content = params.get("content") - append = params.get("append", False) - change_id = params.get("change_id") - dry_run = params.get("dry_run", False) - - if content is not None: - result_message = _execute_update_todo_list( - self, content, append, change_id, dry_run - ) - else: - result_message = ( - "Error: Missing required 'content' parameter for UpdateTodoList" - ) - - else: - result_message = f"Error: Unknown tool name '{tool_name}'" - if self.mcp_tools: - for server_name, server_tools in self.mcp_tools: - if any( - t.get("function", {}).get("name") == tool_name for t in server_tools - ): - server = next( - (s for s in self.mcp_servers if s.name == server_name), None - ) - if server: - result_message = await self._execute_mcp_tool( - server, tool_name, params - ) - else: - result_message = ( - f"Error: Could not find server instance for {server_name}" - ) - break + # Use the tool registry for execution + result_message = await self._execute_tool_with_registry(norm_tool_name, params) except Exception as e: result_message = f"Error executing {tool_name}: {str(e)}" diff --git a/aider/coders/agent_legacy_prompts.py b/aider/coders/agent_legacy_prompts.py deleted file mode 100644 index 2d9963172d4..00000000000 --- a/aider/coders/agent_legacy_prompts.py +++ /dev/null @@ -1,103 +0,0 @@ -# flake8: noqa: E501 - -from .base_prompts import CoderPrompts - - -class AgentLegacyPrompts(CoderPrompts): - """ - Prompt templates for the Agent mode, which enables autonomous codebase exploration. - - The AgentCoder uses these prompts to guide its behavior when exploring and modifying - a codebase using special tool commands like Glob, Grep, Add, etc. This mode enables the - LLM to manage its own context by adding/removing files and executing commands. - """ - - main_system = r""" - -## Core Directives -- **Role**: Act as an expert software engineer. -- **Act Proactively**: Autonomously use file discovery and context management tools (`ViewFilesAtGlob`, `ViewFilesMatching`, `Ls`, `View`, `Remove`) to gather information and fulfill the user's request. Chain tool calls across multiple turns to continue exploration. -- **Be Decisive**: Do not ask the same question or search for the same term in multiple ways. Trust your initial valid findings. -- **Be Concise**: Keep all responses brief and direct (1-3 sentences). Avoid preamble, postamble, and unnecessary explanations. -- **Confirm Ambiguity**: Before applying complex or ambiguous edits, briefly state your plan and ask for confirmation. For simple, direct edits, proceed without confirmation. - - - -## Core Workflow -1. **Plan**: Determine the necessary changes. Use the `UpdateTodoList` tool to manage your plan. Always begin by the todo list. -2. **Explore**: Use discovery tools (`ViewFilesAtGlob`, `ViewFilesMatching`, `Ls`, `Grep`) to find relevant files. These tools add files to context as read-only. Use `Grep` first for broad searches to avoid context clutter. -3. **Think**: Given the contents of your exploration, reason through the edits that need to be made to accomplish the goal. For complex edits, briefly outline your plan for the user. -4. **Execute**: Use the appropriate editing tool. Remember to use `MakeEditable` on a file before modifying it. -5. **Verify & Recover**: After every edit, check the resulting diff snippet. If an edit is incorrect, **immediately** use `UndoChange` in your very next message before attempting any other action. - -## Todo List Management -- **Track Progress**: Use the `UpdateTodoList` tool to add or modify items. -- **Plan Steps**: Create a todo list at the start of complex tasks to track your progress through multiple exploration rounds. -- **Stay Organized**: Update the todo list as you complete steps every 3-10 tool calls to maintain context across multiple tool calls. - -## Code Editing Hierarchy -Your primary method for all modifications is through granular tool calls. Use SEARCH/REPLACE only as a last resort. - -### 1. Granular Tools (Always Preferred) -Use these for precision and safety. -- **Text/Block Manipulation**: `ReplaceText` (Preferred for the majority of edits), `InsertBlock`, `DeleteBlock`, `ReplaceAll` (use with `dry_run=True` for safety). -- **Line-Based Edits**: `ReplaceLine(s)`, `DeleteLine(s)`, `IndentLines`. -- **Refactoring & History**: `ExtractLines`, `ListChanges`, `UndoChange`. - -**MANDATORY Safety Protocol for Line-Based Tools:** Line numbers are fragile. You **MUST** use a two-turn process: -1. **Turn 1**: Use `ShowNumberedContext` to get the exact, current line numbers. -2. **Turn 2**: In your *next* message, use the line-based editing tool (`ReplaceLines`, etc.) with the verified numbers. - -### 2. SEARCH/REPLACE (Last Resort Only) -Use this format **only** when granular tools are demonstrably insufficient for the task (e.g., a complex, non-contiguous pattern change). Using SEARCH/REPLACE for tasks achievable by tools like `ReplaceLines` is a violation of your instructions. - -**You MUST include a justification comment explaining why granular tools cannot be used.** - -Justification: I'm using SEARCH/REPLACE because [specific reason granular tools are insufficient]. -path/to/file.ext <<<<<<< SEARCH Original code to be replaced. -New code to insert. - -REPLACE - - - -Always reply to the user in {language}. -""" - - files_content_assistant_reply = "I understand. I'll use these files to help with your request." - - files_no_full_files = ( - "I don't have full contents of any files yet. I'll add them" - " as needed using the tool commands." - ) - - files_no_full_files_with_repo_map = """ -I have a repository map but no full file contents yet. I will use my navigation tools to add relevant files to the context. - -""" - - files_no_full_files_with_repo_map_reply = """I understand. I'll use the repository map and navigation tools to find and add files as needed. -""" - - repo_content_prefix = """ -I am working with code in a git repository. Here are summaries of some files: - -""" - - system_reminder = """ - -## Reminders -- Any tool call automatically continues to the next turn. Provide no tool calls in your final answer. -- Prioritize granular tools. Using SEARCH/REPLACE unnecessarily is incorrect. -- For SEARCH/REPLACE, you MUST provide a justification. -- Use context blocks (directory structure, git status) to orient yourself. - -{lazy_prompt} -{shell_cmd_reminder} - -""" - - try_again = """I need to retry my exploration. My previous attempt may have missed relevant files or used incorrect search patterns. - -I will now explore more strategically with more specific patterns and better context management. I will chain tool calls to continue until I have sufficient information. -""" diff --git a/aider/coders/agent_prompts.py b/aider/coders/agent_prompts.py index a816b60bb2c..c29e7569b4b 100644 --- a/aider/coders/agent_prompts.py +++ b/aider/coders/agent_prompts.py @@ -24,11 +24,12 @@ class AgentPrompts(CoderPrompts): ## Core Workflow -1. **Plan**: Determine the necessary changes. Use the `UpdateTodoList` tool to manage your plan. Always begin by creating the todo list. +1. **Plan**: Determine the necessary changes. Use the `UpdateTodoList` tool to manage your plan. Always begin by the todo list. 2. **Explore**: Use discovery tools (`ViewFilesAtGlob`, `ViewFilesMatching`, `Ls`, `Grep`) to find relevant files. These tools add files to context as read-only. Use `Grep` first for broad searches to avoid context clutter. 3. **Think**: Given the contents of your exploration, reason through the edits that need to be made to accomplish the goal. For complex edits, briefly outline your plan for the user. -4. **Execute**: Use the appropriate editing tool. Remember to use `MakeEditable` on a file before modifying it. +4. **Execute**: Use the appropriate editing tool. Remember to use `MakeEditable` on a file before modifying it. Break large edits (those greater than 100 lines) into multiple steps 5. **Verify & Recover**: After every edit, check the resulting diff snippet. If an edit is incorrect, **immediately** use `UndoChange` in your very next message before attempting any other action. +6. **Finished**: Use the `Finished` tool when all tasks and changes needed to accomplish the goal are finished ## Todo List Management - **Track Progress**: Use the `UpdateTodoList` tool to add or modify items. diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index d2ace56d64a..c76bbaffd6d 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -2233,7 +2233,7 @@ async def process_tool_calls(self, tool_call_response): def _print_tool_call_info(self, server_tool_calls): """Print information about an MCP tool call.""" - self.io.tool_output("Preparing to run MCP tools", bold=False) + # self.io.tool_output("Preparing to run MCP tools", bold=False) for server, tool_calls in server_tool_calls.items(): for tool_call in tool_calls: diff --git a/aider/commands.py b/aider/commands.py index bfea15dc40a..3523e98be3b 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -1298,33 +1298,6 @@ def cmd_context_blocks(self, args=""): " be included." ) - def cmd_granular_editing(self, args=""): - "Toggle granular editing tools in agent mode" - if not hasattr(self.coder, "use_granular_editing"): - self.io.tool_error("Granular editing toggle is only available in agent mode.") - return - - # Toggle the setting using the agent's method if available - new_state = not self.coder.use_granular_editing - - if hasattr(self.coder, "set_granular_editing"): - self.coder.set_granular_editing(new_state) - else: - # Fallback if method doesn't exist - self.coder.use_granular_editing = new_state - - # Report the new state - if self.coder.use_granular_editing: - self.io.tool_output( - "Granular editing tools are now ON - agent will use specific editing tools" - " instead of search/replace." - ) - else: - self.io.tool_output( - "Granular editing tools are now OFF - agent will use search/replace blocks for" - " editing." - ) - def cmd_ls(self, args): "List all known files and indicate which are included in the chat session" diff --git a/aider/tools/__init__.py b/aider/tools/__init__.py index 3de1c4945fc..c1b2f6c710d 100644 --- a/aider/tools/__init__.py +++ b/aider/tools/__init__.py @@ -1,47 +1,68 @@ # flake8: noqa: F401 -# Import tool functions into the aider.tools namespace +# Import tool modules into the aider.tools namespace -from .command import _execute_command, command_schema -from .command_interactive import ( - _execute_command_interactive, - command_interactive_schema, -) -from .delete_block import _execute_delete_block, delete_block_schema -from .delete_line import _execute_delete_line, delete_line_schema -from .delete_lines import _execute_delete_lines, delete_lines_schema -from .extract_lines import _execute_extract_lines, extract_lines_schema -from .git import ( - _execute_git_diff, - _execute_git_log, - _execute_git_show, - _execute_git_status, - git_diff_schema, - git_log_schema, - git_show_schema, - git_status_schema, -) -from .grep import _execute_grep, grep_schema -from .indent_lines import _execute_indent_lines, indent_lines_schema -from .insert_block import _execute_insert_block, insert_block_schema -from .list_changes import _execute_list_changes, list_changes_schema -from .ls import execute_ls, ls_schema -from .make_editable import _execute_make_editable, make_editable_schema -from .make_readonly import _execute_make_readonly, make_readonly_schema -from .remove import _execute_remove, remove_schema -from .replace_all import _execute_replace_all, replace_all_schema -from .replace_line import _execute_replace_line, replace_line_schema -from .replace_lines import _execute_replace_lines, replace_lines_schema -from .replace_text import _execute_replace_text, replace_text_schema -from .show_numbered_context import ( - execute_show_numbered_context, - show_numbered_context_schema, -) -from .undo_change import _execute_undo_change, undo_change_schema -from .update_todo_list import _execute_update_todo_list, update_todo_list_schema -from .view import execute_view, view_schema -from .view_files_at_glob import execute_view_files_at_glob, view_files_at_glob_schema -from .view_files_matching import execute_view_files_matching, view_files_matching_schema -from .view_files_with_symbol import ( - _execute_view_files_with_symbol, - view_files_with_symbol_schema, +# Import all tool modules +from . import ( + command, + command_interactive, + delete_block, + delete_line, + delete_lines, + extract_lines, + finished, + git_diff, + git_log, + git_show, + git_status, + grep, + indent_lines, + insert_block, + list_changes, + ls, + make_editable, + make_readonly, + remove, + replace_all, + replace_line, + replace_lines, + replace_text, + show_numbered_context, + undo_change, + update_todo_list, + view, + view_files_matching, + view_files_with_symbol, ) + +# List of all available tool modules for dynamic discovery +TOOL_MODULES = [ + command, + command_interactive, + delete_block, + delete_line, + delete_lines, + extract_lines, + finished, + git_diff, + git_log, + git_show, + git_status, + grep, + indent_lines, + insert_block, + list_changes, + ls, + make_editable, + make_readonly, + remove, + replace_all, + replace_line, + replace_lines, + replace_text, + show_numbered_context, + undo_change, + update_todo_list, + view, + view_files_matching, + view_files_with_symbol, +] diff --git a/aider/tools/command.py b/aider/tools/command.py index 9dad217fe3e..99b6c2ec96f 100644 --- a/aider/tools/command.py +++ b/aider/tools/command.py @@ -1,7 +1,7 @@ # Import necessary functions from aider.run_cmd import run_cmd_subprocess -command_schema = { +schema = { "type": "function", "function": { "name": "Command", @@ -19,6 +19,9 @@ }, } +# Normalized tool name for lookup +NORM_NAME = "command" + def _execute_command(coder, command_string): """ @@ -74,3 +77,21 @@ def _execute_command(coder, command_string): # if coder.verbose: # coder.io.tool_error(traceback.format_exc()) return f"Error executing command: {str(e)}" + + +def process_response(coder, params): + """ + Process the Command tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters + + Returns: + str: Result message + """ + command_string = params.get("command_string") + if command_string is not None: + return _execute_command(coder, command_string) + else: + return "Error: Missing 'command_string' parameter for Command" diff --git a/aider/tools/command_interactive.py b/aider/tools/command_interactive.py index 7e4bc17d2fc..d64c05e6756 100644 --- a/aider/tools/command_interactive.py +++ b/aider/tools/command_interactive.py @@ -1,7 +1,7 @@ # Import necessary functions from aider.run_cmd import run_cmd -command_interactive_schema = { +schema = { "type": "function", "function": { "name": "CommandInteractive", @@ -19,6 +19,9 @@ }, } +# Normalized tool name for lookup +NORM_NAME = "commandinteractive" + def _execute_command_interactive(coder, command_string): """ @@ -69,3 +72,21 @@ def _execute_command_interactive(coder, command_string): # if coder.verbose: # coder.io.tool_error(traceback.format_exc()) return f"Error executing interactive command: {str(e)}" + + +def process_response(coder, params): + """ + Process the CommandInteractive tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters + + Returns: + str: Result message + """ + command_string = params.get("command_string") + if command_string is not None: + return _execute_command_interactive(coder, command_string) + else: + return "Error: Missing 'command_string' parameter for CommandInteractive" diff --git a/aider/tools/delete_block.py b/aider/tools/delete_block.py index 27b5f311e92..80a1f5b5a98 100644 --- a/aider/tools/delete_block.py +++ b/aider/tools/delete_block.py @@ -10,7 +10,7 @@ validate_file_for_edit, ) -delete_block_schema = { +schema = { "type": "function", "function": { "name": "DeleteBlock", @@ -32,6 +32,9 @@ }, } +# Normalized tool name for lookup +NORM_NAME = "deleteblock" + def _execute_delete_block( coder, @@ -141,3 +144,39 @@ def _execute_delete_block( except Exception as e: # Handle unexpected errors return handle_tool_error(coder, tool_name, e) + + +def process_response(coder, params): + """ + Process the DeleteBlock tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters + + Returns: + str: Result message + """ + file_path = params.get("file_path") + start_pattern = params.get("start_pattern") + end_pattern = params.get("end_pattern") + line_count = params.get("line_count") + near_context = params.get("near_context") + occurrence = params.get("occurrence", 1) + change_id = params.get("change_id") + dry_run = params.get("dry_run", False) + + if file_path is not None and start_pattern is not None: + return _execute_delete_block( + coder, + file_path, + start_pattern, + end_pattern, + line_count, + near_context, + occurrence, + change_id, + dry_run, + ) + else: + return "Error: Missing required parameters for DeleteBlock (file_path, start_pattern)" diff --git a/aider/tools/delete_line.py b/aider/tools/delete_line.py index 4b3fb2c1e6d..c0cd38f9488 100644 --- a/aider/tools/delete_line.py +++ b/aider/tools/delete_line.py @@ -8,7 +8,7 @@ handle_tool_error, ) -delete_line_schema = { +schema = { "type": "function", "function": { "name": "DeleteLine", @@ -26,6 +26,9 @@ }, } +# Normalized tool name for lookup +NORM_NAME = "deleteline" + def _execute_delete_line(coder, file_path, line_number, change_id=None, dry_run=False): """ @@ -128,3 +131,25 @@ def _execute_delete_line(coder, file_path, line_number, change_id=None, dry_run= except Exception as e: # Handle unexpected errors return handle_tool_error(coder, tool_name, e) + + +def process_response(coder, params): + """ + Process the DeleteLine tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters + + Returns: + str: Result message + """ + file_path = params.get("file_path") + line_number = params.get("line_number") + change_id = params.get("change_id") + dry_run = params.get("dry_run", False) + + if file_path is not None and line_number is not None: + return _execute_delete_line(coder, file_path, line_number, change_id, dry_run) + else: + return "Error: Missing required parameters for DeleteLine (file_path, line_number)" diff --git a/aider/tools/delete_lines.py b/aider/tools/delete_lines.py index 122f6a19c8e..d139233ba89 100644 --- a/aider/tools/delete_lines.py +++ b/aider/tools/delete_lines.py @@ -8,7 +8,7 @@ handle_tool_error, ) -delete_lines_schema = { +schema = { "type": "function", "function": { "name": "DeleteLines", @@ -27,6 +27,9 @@ }, } +# Normalized tool name for lookup +NORM_NAME = "deletelines" + def _execute_delete_lines(coder, file_path, start_line, end_line, change_id=None, dry_run=False): """ @@ -154,3 +157,28 @@ def _execute_delete_lines(coder, file_path, start_line, end_line, change_id=None except Exception as e: # Handle unexpected errors return handle_tool_error(coder, tool_name, e) + + +def process_response(coder, params): + """ + Process the DeleteLines tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters + + Returns: + str: Result message + """ + file_path = params.get("file_path") + start_line = params.get("start_line") + end_line = params.get("end_line") + change_id = params.get("change_id") + dry_run = params.get("dry_run", False) + + if file_path is not None and start_line is not None and end_line is not None: + return _execute_delete_lines(coder, file_path, start_line, end_line, change_id, dry_run) + else: + return ( + "Error: Missing required parameters for DeleteLines (file_path, start_line, end_line)" + ) diff --git a/aider/tools/extract_lines.py b/aider/tools/extract_lines.py index 36c1fca01b4..25c3b55e342 100644 --- a/aider/tools/extract_lines.py +++ b/aider/tools/extract_lines.py @@ -3,7 +3,7 @@ from .tool_utils import generate_unified_diff_snippet -extract_lines_schema = { +schema = { "type": "function", "function": { "name": "ExtractLines", @@ -25,6 +25,9 @@ }, } +# Normalized tool name for lookup +NORM_NAME = "extractlines" + def _execute_extract_lines( coder, @@ -297,3 +300,42 @@ def _execute_extract_lines( except Exception as e: coder.io.tool_error(f"Error in ExtractLines: {str(e)}\n{traceback.format_exc()}") return f"Error: {str(e)}" + + +def process_response(coder, params): + """ + Process the ExtractLines tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters + + Returns: + str: Result message + """ + source_file_path = params.get("source_file_path") + target_file_path = params.get("target_file_path") + start_pattern = params.get("start_pattern") + end_pattern = params.get("end_pattern") + line_count = params.get("line_count") + near_context = params.get("near_context") + occurrence = params.get("occurrence", 1) + dry_run = params.get("dry_run", False) + + if source_file_path and target_file_path and start_pattern: + return _execute_extract_lines( + coder, + source_file_path, + target_file_path, + start_pattern, + end_pattern, + line_count, + near_context, + occurrence, + dry_run, + ) + else: + return ( + "Error: Missing required parameters for ExtractLines (source_file_path," + " target_file_path, start_pattern)" + ) diff --git a/aider/tools/finished.py b/aider/tools/finished.py new file mode 100644 index 00000000000..564daf3c442 --- /dev/null +++ b/aider/tools/finished.py @@ -0,0 +1,48 @@ +schema = { + "type": "function", + "function": { + "name": "Finished", + "description": ( + "Declare that we are done with every single sub goal and no further work is needed." + ), + "parameters": { + "type": "object", + "properties": {}, + "required": [], + }, + }, +} + +# Normalized tool name for lookup +NORM_NAME = "finished" + + +def _execute_finished(coder): + """ + Mark that the current generation task needs no further effort. + + This gives the LLM explicit control over when it can stop looping + """ + + if coder: + coder.agent_finished = True + # coder.io.tool_output("Task Finished!") + return "Task Finished!" + + # coder.io.tool_Error("Error: Could not mark agent task as finished") + return "Error: Could not mark agent task as finished" + + +def process_response(coder, params): + """ + Process the Finished tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters (should be empty for Finished) + + Returns: + str: Result message + """ + # Finished tool has no parameters to validate + return _execute_finished(coder) diff --git a/aider/tools/git.py b/aider/tools/git.py deleted file mode 100644 index f9fefb7f507..00000000000 --- a/aider/tools/git.py +++ /dev/null @@ -1,142 +0,0 @@ -from aider.repo import ANY_GIT_ERROR - -git_diff_schema = { - "type": "function", - "function": { - "name": "git_diff", - "description": ( - "Show the diff between the current working directory and a git branch or commit." - ), - "parameters": { - "type": "object", - "properties": { - "branch": { - "type": "string", - "description": "The branch or commit hash to diff against. Defaults to HEAD.", - }, - }, - "required": [], - }, - }, -} - - -def _execute_git_diff(coder, branch=None): - """ - Show the diff between the current working directory and a git branch or commit. - """ - if not coder.repo: - return "Not in a git repository." - - try: - if branch: - diff = coder.repo.diff_commits(False, branch, "HEAD") - else: - diff = coder.repo.diff_commits(False, "HEAD", None) - - if not diff: - return "No differences found." - return diff - except ANY_GIT_ERROR as e: - coder.io.tool_error(f"Error running git diff: {e}") - return f"Error running git diff: {e}" - - -git_log_schema = { - "type": "function", - "function": { - "name": "git_log", - "description": "Show the git log.", - "parameters": { - "type": "object", - "properties": { - "limit": { - "type": "integer", - "description": "The maximum number of commits to show. Defaults to 10.", - }, - }, - "required": [], - }, - }, -} - - -def _execute_git_log(coder, limit=10): - """ - Show the git log. - """ - if not coder.repo: - return "Not in a git repository." - - try: - commits = list(coder.repo.repo.iter_commits(max_count=limit)) - log_output = [] - for commit in commits: - short_hash = commit.hexsha[:8] - message = commit.message.strip().split("\n")[0] - log_output.append(f"{short_hash} {message}") - return "\n".join(log_output) - except ANY_GIT_ERROR as e: - coder.io.tool_error(f"Error running git log: {e}") - return f"Error running git log: {e}" - - -git_show_schema = { - "type": "function", - "function": { - "name": "git_show", - "description": "Show various types of objects (blobs, trees, tags, and commits).", - "parameters": { - "type": "object", - "properties": { - "object": { - "type": "string", - "description": "The object to show. Defaults to HEAD.", - }, - }, - "required": [], - }, - }, -} - - -def _execute_git_show(coder, object="HEAD"): - """ - Show various types of objects (blobs, trees, tags, and commits). - """ - if not coder.repo: - return "Not in a git repository." - - try: - return coder.repo.repo.git.show(object) - except ANY_GIT_ERROR as e: - coder.io.tool_error(f"Error running git show: {e}") - return f"Error running git show: {e}" - - -git_status_schema = { - "type": "function", - "function": { - "name": "git_status", - "description": "Show the working tree status.", - "parameters": { - "type": "object", - "properties": {}, - "required": [], - }, - }, -} - - -def _execute_git_status(coder): - """ - Show the working tree status. - """ - if not coder.repo: - return "Not in a git repository." - - try: - return coder.repo.repo.git.status() - except ANY_GIT_ERROR as e: - coder.io.tool_error(f"Error running git status: {e}") - return f"Error running git status: {e}" diff --git a/aider/tools/git_diff.py b/aider/tools/git_diff.py new file mode 100644 index 00000000000..6a7632b69ec --- /dev/null +++ b/aider/tools/git_diff.py @@ -0,0 +1,60 @@ +from aider.repo import ANY_GIT_ERROR + +schema = { + "type": "function", + "function": { + "name": "GitDiff", + "description": ( + "Show the diff between the current working directory and a git branch or commit." + ), + "parameters": { + "type": "object", + "properties": { + "branch": { + "type": "string", + "description": "The branch or commit hash to diff against. Defaults to HEAD.", + }, + }, + "required": [], + }, + }, +} + +# Normalized tool name for lookup +NORM_NAME = "gitdiff" + + +def _execute_git_diff(coder, branch=None): + """ + Show the diff between the current working directory and a git branch or commit. + """ + if not coder.repo: + return "Not in a git repository." + + try: + if branch: + diff = coder.repo.diff_commits(False, branch, "HEAD") + else: + diff = coder.repo.diff_commits(False, "HEAD", None) + + if not diff: + return "No differences found." + return diff + except ANY_GIT_ERROR as e: + coder.io.tool_error(f"Error running git diff: {e}") + return f"Error running git diff: {e}" + + +def process_response(coder, params): + """ + Process the GitDiff tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters + + Returns: + str: Result message + """ + branch = params.get("branch") + return _execute_git_diff(coder, branch) diff --git a/aider/tools/git_log.py b/aider/tools/git_log.py new file mode 100644 index 00000000000..4db90e4428d --- /dev/null +++ b/aider/tools/git_log.py @@ -0,0 +1,57 @@ +from aider.repo import ANY_GIT_ERROR + +schema = { + "type": "function", + "function": { + "name": "GitLog", + "description": "Show the git log.", + "parameters": { + "type": "object", + "properties": { + "limit": { + "type": "integer", + "description": "The maximum number of commits to show. Defaults to 10.", + }, + }, + "required": [], + }, + }, +} + +# Normalized tool name for lookup +NORM_NAME = "gitlog" + + +def _execute_git_log(coder, limit=10): + """ + Show the git log. + """ + if not coder.repo: + return "Not in a git repository." + + try: + commits = list(coder.repo.repo.iter_commits(max_count=limit)) + log_output = [] + for commit in commits: + short_hash = commit.hexsha[:8] + message = commit.message.strip().split("\n")[0] + log_output.append(f"{short_hash} {message}") + return "\n".join(log_output) + except ANY_GIT_ERROR as e: + coder.io.tool_error(f"Error running git log: {e}") + return f"Error running git log: {e}" + + +def process_response(coder, params): + """ + Process the GitLog tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters + + Returns: + str: Result message + """ + limit = params.get("limit", 10) + return _execute_git_log(coder, limit) diff --git a/aider/tools/git_show.py b/aider/tools/git_show.py new file mode 100644 index 00000000000..69a702b9a72 --- /dev/null +++ b/aider/tools/git_show.py @@ -0,0 +1,51 @@ +from aider.repo import ANY_GIT_ERROR + +schema = { + "type": "function", + "function": { + "name": "GitShow", + "description": "Show various types of objects (blobs, trees, tags, and commits).", + "parameters": { + "type": "object", + "properties": { + "object": { + "type": "string", + "description": "The object to show. Defaults to HEAD.", + }, + }, + "required": [], + }, + }, +} + +# Normalized tool name for lookup +NORM_NAME = "gitshow" + + +def _execute_git_show(coder, object="HEAD"): + """ + Show various types of objects (blobs, trees, tags, and commits). + """ + if not coder.repo: + return "Not in a git repository." + + try: + return coder.repo.repo.git.show(object) + except ANY_GIT_ERROR as e: + coder.io.tool_error(f"Error running git show: {e}") + return f"Error running git show: {e}" + + +def process_response(coder, params): + """ + Process the GitShow tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters + + Returns: + str: Result message + """ + object = params.get("object", "HEAD") + return _execute_git_show(coder, object) diff --git a/aider/tools/git_status.py b/aider/tools/git_status.py new file mode 100644 index 00000000000..3e1c855119a --- /dev/null +++ b/aider/tools/git_status.py @@ -0,0 +1,46 @@ +from aider.repo import ANY_GIT_ERROR + +schema = { + "type": "function", + "function": { + "name": "GitStatus", + "description": "Show the working tree status.", + "parameters": { + "type": "object", + "properties": {}, + "required": [], + }, + }, +} + +# Normalized tool name for lookup +NORM_NAME = "gitstatus" + + +def _execute_git_status(coder): + """ + Show the working tree status. + """ + if not coder.repo: + return "Not in a git repository." + + try: + return coder.repo.repo.git.status() + except ANY_GIT_ERROR as e: + coder.io.tool_error(f"Error running git status: {e}") + return f"Error running git status: {e}" + + +def process_response(coder, params): + """ + Process the GitStatus tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters (should be empty for GitStatus) + + Returns: + str: Result message + """ + # GitStatus tool has no parameters to validate + return _execute_git_status(coder) diff --git a/aider/tools/grep.py b/aider/tools/grep.py index 1eac5e7b141..4be93b162c8 100644 --- a/aider/tools/grep.py +++ b/aider/tools/grep.py @@ -5,7 +5,7 @@ from aider.run_cmd import run_cmd_subprocess -grep_schema = { +schema = { "type": "function", "function": { "name": "Grep", @@ -49,6 +49,9 @@ }, } +# Normalized tool name for lookup +NORM_NAME = "grep" + def _find_search_tool(): """Find the best available command-line search tool (rg, ag, grep).""" @@ -214,3 +217,37 @@ def _execute_grep( cmd_str_info = f"'{command_string}' " if "command_string" in locals() else "" coder.io.tool_error(f"Error executing {tool_name} command {cmd_str_info}: {str(e)}") return f"Error executing {tool_name}: {str(e)}" + + +def process_response(coder, params): + """ + Process the Grep tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters + + Returns: + str: Result message + """ + pattern = params.get("pattern") + file_pattern = params.get("file_pattern", "*") # Default to all files + directory = params.get("directory", ".") # Default to current directory + use_regex = params.get("use_regex", False) # Default to literal search + case_insensitive = params.get("case_insensitive", False) # Default to case-sensitive + context_before = params.get("context_before", 5) + context_after = params.get("context_after", 5) + + if pattern is not None: + return _execute_grep( + coder, + pattern, + file_pattern, + directory, + use_regex, + case_insensitive, + context_before, + context_after, + ) + else: + return "Error: Missing required 'pattern' parameter for Grep" diff --git a/aider/tools/indent_lines.py b/aider/tools/indent_lines.py index d30070d4513..6dd9380a48d 100644 --- a/aider/tools/indent_lines.py +++ b/aider/tools/indent_lines.py @@ -10,7 +10,7 @@ validate_file_for_edit, ) -indent_lines_schema = { +schema = { "type": "function", "function": { "name": "IndentLines", @@ -33,6 +33,9 @@ }, } +# Normalized tool name for lookup +NORM_NAME = "indentlines" + def _execute_indent_lines( coder, @@ -178,3 +181,41 @@ def _execute_indent_lines( except Exception as e: # Handle unexpected errors return handle_tool_error(coder, tool_name, e) + + +def process_response(coder, params): + """ + Process the IndentLines tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters + + Returns: + str: Result message + """ + file_path = params.get("file_path") + start_pattern = params.get("start_pattern") + end_pattern = params.get("end_pattern") + line_count = params.get("line_count") + indent_levels = params.get("indent_levels", 1) + near_context = params.get("near_context") + occurrence = params.get("occurrence", 1) + change_id = params.get("change_id") + dry_run = params.get("dry_run", False) + + if file_path is not None and start_pattern is not None: + return _execute_indent_lines( + coder, + file_path, + start_pattern, + end_pattern, + line_count, + indent_levels, + near_context, + occurrence, + change_id, + dry_run, + ) + else: + return "Error: Missing required parameters for IndentLines (file_path, start_pattern)" diff --git a/aider/tools/insert_block.py b/aider/tools/insert_block.py index e6a02d3a070..a8fb622fe4f 100644 --- a/aider/tools/insert_block.py +++ b/aider/tools/insert_block.py @@ -12,7 +12,7 @@ validate_file_for_edit, ) -insert_block_schema = { +schema = { "type": "function", "function": { "name": "InsertBlock", @@ -36,6 +36,9 @@ }, } +# Normalized tool name for lookup +NORM_NAME = "insertblock" + def _execute_insert_block( coder, @@ -90,10 +93,10 @@ def _execute_insert_block( if position: # Handle special positions - if position == "start_of_file": + if position == "start_of_file" or position == "top": insertion_line_idx = 0 pattern_type = "at start of" - elif position == "end_of_file": + elif position == "end_of_file" or position == "bottom": insertion_line_idx = len(lines) pattern_type = "at end of" else: @@ -235,3 +238,51 @@ def _execute_insert_block( f"Error in InsertBlock: {str(e)}\n{traceback.format_exc()}" ) # Add traceback return f"Error: {str(e)}" + + +def process_response(coder, params): + """ + Process the InsertBlock tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters + + Returns: + str: Result message + """ + file_path = params.get("file_path") + content = params.get("content") + after_pattern = params.get("after_pattern") + before_pattern = params.get("before_pattern") + occurrence = params.get("occurrence", 1) + change_id = params.get("change_id") + dry_run = params.get("dry_run", False) + position = params.get("position") + auto_indent = params.get("auto_indent", True) + use_regex = params.get("use_regex", False) + + if ( + file_path is not None + and content is not None + and (after_pattern is not None or before_pattern is not None or position is not None) + ): + return _execute_insert_block( + coder, + file_path, + content, + after_pattern, + before_pattern, + occurrence, + change_id, + dry_run, + position, + auto_indent, + use_regex, + ) + + else: + return ( + "Error: Missing required parameters for InsertBlock (file_path," + " content, and either after_pattern or before_pattern)" + ) diff --git a/aider/tools/list_changes.py b/aider/tools/list_changes.py index 9e4372b79e3..1a1b054c452 100644 --- a/aider/tools/list_changes.py +++ b/aider/tools/list_changes.py @@ -1,7 +1,7 @@ import traceback from datetime import datetime -list_changes_schema = { +schema = { "type": "function", "function": { "name": "ListChanges", @@ -16,6 +16,9 @@ }, } +# Normalized tool name for lookup +NORM_NAME = "listchanges" + def _execute_list_changes(coder, file_path=None, limit=10): """ @@ -64,3 +67,20 @@ def _execute_list_changes(coder, file_path=None, limit=10): f"Error in ListChanges: {str(e)}\n{traceback.format_exc()}" ) # Add traceback return f"Error: {str(e)}" + + +def process_response(coder, params): + """ + Process the ListChanges tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters + + Returns: + str: Result message + """ + file_path = params.get("file_path") + limit = params.get("limit", 10) + + return _execute_list_changes(coder, file_path, limit) diff --git a/aider/tools/ls.py b/aider/tools/ls.py index 2e969faa6c1..96119d9f4ff 100644 --- a/aider/tools/ls.py +++ b/aider/tools/ls.py @@ -1,6 +1,6 @@ import os -ls_schema = { +schema = { "type": "function", "function": { "name": "Ls", @@ -18,6 +18,9 @@ }, } +# Normalized tool name for lookup +NORM_NAME = "ls" + def execute_ls(coder, dir_path=None, directory=None): # Handle both positional and keyword arguments for backward compatibility @@ -70,3 +73,21 @@ def execute_ls(coder, dir_path=None, directory=None): except Exception as e: coder.io.tool_error(f"Error in ls: {str(e)}") return f"Error: {str(e)}" + + +def process_response(coder, params): + """ + Process the Ls tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters + + Returns: + str: Result message + """ + directory = params.get("directory") + if directory is not None: + return execute_ls(coder, directory) + else: + return "Error: Missing 'directory' parameter for Ls" diff --git a/aider/tools/make_editable.py b/aider/tools/make_editable.py index 5ca0f0e7093..f84c9831cf3 100644 --- a/aider/tools/make_editable.py +++ b/aider/tools/make_editable.py @@ -1,6 +1,6 @@ import os -make_editable_schema = { +schema = { "type": "function", "function": { "name": "MakeEditable", @@ -18,6 +18,9 @@ }, } +# Normalized tool name for lookup +NORM_NAME = "makeeditable" + # Keep the underscore prefix as this function is primarily for internal coder use def _execute_make_editable(coder, file_path): @@ -62,3 +65,21 @@ def _execute_make_editable(coder, file_path): except Exception as e: coder.io.tool_error(f"Error in MakeEditable for '{file_path}': {str(e)}") return f"Error: {str(e)}" + + +def process_response(coder, params): + """ + Process the MakeEditable tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters + + Returns: + str: Result message + """ + file_path = params.get("file_path") + if file_path is not None: + return _execute_make_editable(coder, file_path) + else: + return "Error: Missing 'file_path' parameter for MakeEditable" diff --git a/aider/tools/make_readonly.py b/aider/tools/make_readonly.py index 5712a672ac2..3dc3247f627 100644 --- a/aider/tools/make_readonly.py +++ b/aider/tools/make_readonly.py @@ -1,4 +1,4 @@ -make_readonly_schema = { +schema = { "type": "function", "function": { "name": "MakeReadonly", @@ -16,6 +16,9 @@ }, } +# Normalized tool name for lookup +NORM_NAME = "makereadonly" + def _execute_make_readonly(coder, file_path): """ @@ -46,3 +49,21 @@ def _execute_make_readonly(coder, file_path): except Exception as e: coder.io.tool_error(f"Error making file read-only: {str(e)}") return f"Error: {str(e)}" + + +def process_response(coder, params): + """ + Process the MakeReadonly tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters + + Returns: + str: Result message + """ + file_path = params.get("file_path") + if file_path is not None: + return _execute_make_readonly(coder, file_path) + else: + return "Error: Missing 'file_path' parameter for MakeReadonly" diff --git a/aider/tools/remove.py b/aider/tools/remove.py index bbed05d0bed..d236eef2a5b 100644 --- a/aider/tools/remove.py +++ b/aider/tools/remove.py @@ -1,6 +1,6 @@ import time -remove_schema = { +schema = { "type": "function", "function": { "name": "Remove", @@ -22,6 +22,9 @@ }, } +# Normalized tool name for lookup +NORM_NAME = "remove" + def _execute_remove(coder, file_path): """ @@ -68,3 +71,21 @@ def _execute_remove(coder, file_path): except Exception as e: coder.io.tool_error(f"Error removing file: {str(e)}") return f"Error: {str(e)}" + + +def process_response(coder, params): + """ + Process the Remove tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters + + Returns: + str: Result message + """ + file_path = params.get("file_path") + if file_path is not None: + return _execute_remove(coder, file_path) + else: + return "Error: Missing 'file_path' parameter for Remove" diff --git a/aider/tools/replace_all.py b/aider/tools/replace_all.py index 96c16ad715d..ca6fdaa7b0d 100644 --- a/aider/tools/replace_all.py +++ b/aider/tools/replace_all.py @@ -7,7 +7,7 @@ validate_file_for_edit, ) -replace_all_schema = { +schema = { "type": "function", "function": { "name": "ReplaceAll", @@ -26,6 +26,9 @@ }, } +# Normalized tool name for lookup +NORM_NAME = "replaceall" + def _execute_replace_all(coder, file_path, find_text, replace_text, change_id=None, dry_run=False): """ @@ -96,3 +99,28 @@ def _execute_replace_all(coder, file_path, find_text, replace_text, change_id=No except Exception as e: # Handle unexpected errors return handle_tool_error(coder, tool_name, e) + + +def process_response(coder, params): + """ + Process the ReplaceAll tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters + + Returns: + str: Result message + """ + file_path = params.get("file_path") + find_text = params.get("find_text") + replace_text = params.get("replace_text") + change_id = params.get("change_id") + dry_run = params.get("dry_run", False) + + if file_path is not None and find_text is not None and replace_text is not None: + return _execute_replace_all(coder, file_path, find_text, replace_text, change_id, dry_run) + else: + return ( + "Error: Missing required parameters for ReplaceAll (file_path, find_text, replace_text)" + ) diff --git a/aider/tools/replace_line.py b/aider/tools/replace_line.py index 25acbf3e826..1e45497ab00 100644 --- a/aider/tools/replace_line.py +++ b/aider/tools/replace_line.py @@ -1,7 +1,7 @@ import os import traceback -replace_line_schema = { +schema = { "type": "function", "function": { "name": "ReplaceLine", @@ -20,6 +20,9 @@ }, } +# Normalized tool name for lookup +NORM_NAME = "replaceline" + def _execute_replace_line( coder, file_path, line_number, new_content, change_id=None, dry_run=False @@ -142,3 +145,29 @@ def _execute_replace_line( except Exception as e: coder.io.tool_error(f"Error in ReplaceLine: {str(e)}\n{traceback.format_exc()}") return f"Error: {str(e)}" + + +def process_response(coder, params): + """ + Process the ReplaceLine tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters + + Returns: + str: Result message + """ + file_path = params.get("file_path") + line_number = params.get("line_number") + new_content = params.get("new_content") + change_id = params.get("change_id") + dry_run = params.get("dry_run", False) + + if file_path is not None and line_number is not None and new_content is not None: + return _execute_replace_line(coder, file_path, line_number, new_content, change_id, dry_run) + else: + return ( + "Error: Missing required parameters for ReplaceLine (file_path," + " line_number, new_content)" + ) diff --git a/aider/tools/replace_lines.py b/aider/tools/replace_lines.py index 859983ea0ab..9815bb28754 100644 --- a/aider/tools/replace_lines.py +++ b/aider/tools/replace_lines.py @@ -8,7 +8,7 @@ handle_tool_error, ) -replace_lines_schema = { +schema = { "type": "function", "function": { "name": "ReplaceLines", @@ -28,6 +28,9 @@ }, } +# Normalized tool name for lookup +NORM_NAME = "replacelines" + def _execute_replace_lines( coder, file_path, start_line, end_line, new_content, change_id=None, dry_run=False @@ -178,3 +181,37 @@ def _execute_replace_lines( except Exception as e: # Handle unexpected errors return handle_tool_error(coder, tool_name, e) + + +def process_response(coder, params): + """ + Process the ReplaceLines tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters + + Returns: + str: Result message + """ + file_path = params.get("file_path") + start_line = params.get("start_line") + end_line = params.get("end_line") + new_content = params.get("new_content") + change_id = params.get("change_id") + dry_run = params.get("dry_run", False) + + if ( + file_path is not None + and start_line is not None + and end_line is not None + and new_content is not None + ): + return _execute_replace_lines( + coder, file_path, start_line, end_line, new_content, change_id, dry_run + ) + else: + return ( + "Error: Missing required parameters for ReplaceLines (file_path," + " start_line, end_line, new_content)" + ) diff --git a/aider/tools/replace_text.py b/aider/tools/replace_text.py index 9c3233adb92..724736cdf35 100644 --- a/aider/tools/replace_text.py +++ b/aider/tools/replace_text.py @@ -7,7 +7,7 @@ validate_file_for_edit, ) -replace_text_schema = { +schema = { "type": "function", "function": { "name": "ReplaceText", @@ -28,6 +28,9 @@ }, } +# Normalized tool name for lookup +NORM_NAME = "replacetext" + def _execute_replace_text( coder, @@ -145,3 +148,40 @@ def _execute_replace_text( except Exception as e: # Handle unexpected errors return handle_tool_error(coder, tool_name, e) + + +def process_response(coder, params): + """ + Process the ReplaceText tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters + + Returns: + str: Result message + """ + file_path = params.get("file_path") + find_text = params.get("find_text") + replace_text = params.get("replace_text") + near_context = params.get("near_context") + occurrence = params.get("occurrence", 1) + change_id = params.get("change_id") + dry_run = params.get("dry_run", False) + + if file_path is not None and find_text is not None and replace_text is not None: + return _execute_replace_text( + coder, + file_path, + find_text, + replace_text, + near_context, + occurrence, + change_id, + dry_run, + ) + else: + return ( + "Error: Missing required parameters for ReplaceText (file_path," + " find_text, replace_text)" + ) diff --git a/aider/tools/show_numbered_context.py b/aider/tools/show_numbered_context.py index 0debee9d277..160697fbdac 100644 --- a/aider/tools/show_numbered_context.py +++ b/aider/tools/show_numbered_context.py @@ -2,7 +2,7 @@ from .tool_utils import ToolError, handle_tool_error, resolve_paths -show_numbered_context_schema = { +schema = { "type": "function", "function": { "name": "ShowNumberedContext", @@ -20,6 +20,9 @@ }, } +# Normalized tool name for lookup +NORM_NAME = "shownumberedcontext" + def execute_show_numbered_context( coder, file_path, pattern=None, line_number=None, context_lines=3 @@ -117,3 +120,28 @@ def execute_show_numbered_context( except Exception as e: # Handle unexpected errors during processing return handle_tool_error(coder, tool_name, e) + + +def process_response(coder, params): + """ + Process the ShowNumberedContext tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters + + Returns: + str: Result message + """ + file_path = params.get("file_path") + pattern = params.get("pattern") + line_number = params.get("line_number") + context_lines = params.get("context_lines", 3) + + if file_path is not None and (pattern is not None or line_number is not None): + return execute_show_numbered_context(coder, file_path, pattern, line_number, context_lines) + else: + return ( + "Error: Missing required parameters for ViewNumberedContext (file_path" + " and either pattern or line_number)" + ) diff --git a/aider/tools/undo_change.py b/aider/tools/undo_change.py index 6917a01ba9f..923919601d3 100644 --- a/aider/tools/undo_change.py +++ b/aider/tools/undo_change.py @@ -1,6 +1,6 @@ import traceback -undo_change_schema = { +schema = { "type": "function", "function": { "name": "UndoChange", @@ -15,6 +15,9 @@ }, } +# Normalized tool name for lookup +NORM_NAME = "undochange" + def _execute_undo_change(coder, change_id=None, file_path=None): """ @@ -73,3 +76,20 @@ def _execute_undo_change(coder, change_id=None, file_path=None): except Exception as e: coder.io.tool_error(f"Error in UndoChange: {str(e)}\n{traceback.format_exc()}") return f"Error: {str(e)}" + + +def process_response(coder, params): + """ + Process the UndoChange tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters + + Returns: + str: Result message + """ + change_id = params.get("change_id") + file_path = params.get("file_path") + + return _execute_undo_change(coder, change_id, file_path) diff --git a/aider/tools/update_todo_list.py b/aider/tools/update_todo_list.py index 4ae335c2197..4dcf765950a 100644 --- a/aider/tools/update_todo_list.py +++ b/aider/tools/update_todo_list.py @@ -5,7 +5,7 @@ handle_tool_error, ) -update_todo_list_schema = { +schema = { "type": "function", "function": { "name": "UpdateTodoList", @@ -41,6 +41,9 @@ }, } +# Normalized tool name for lookup +NORM_NAME = "updatetodolist" + def _execute_update_todo_list(coder, content, append=False, change_id=None, dry_run=False): """ @@ -129,3 +132,25 @@ def _execute_update_todo_list(coder, content, append=False, change_id=None, dry_ return handle_tool_error(coder, tool_name, e, add_traceback=False) except Exception as e: return handle_tool_error(coder, tool_name, e) + + +def process_response(coder, params): + """ + Process the UpdateTodoList tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters + + Returns: + str: Result message + """ + content = params.get("content") + append = params.get("append", False) + change_id = params.get("change_id") + dry_run = params.get("dry_run", False) + + if content is not None: + return _execute_update_todo_list(coder, content, append, change_id, dry_run) + else: + return "Error: Missing required 'content' parameter for UpdateTodoList" diff --git a/aider/tools/view.py b/aider/tools/view.py index 845894fdd32..867666e0ab6 100644 --- a/aider/tools/view.py +++ b/aider/tools/view.py @@ -1,4 +1,4 @@ -view_schema = { +schema = { "type": "function", "function": { "name": "View", @@ -20,6 +20,9 @@ }, } +# Normalized tool name for lookup +NORM_NAME = "view" + def execute_view(coder, file_path): """ @@ -34,3 +37,21 @@ def execute_view(coder, file_path): except Exception as e: coder.io.tool_error(f"Error viewing file: {str(e)}") return f"Error: {str(e)}" + + +def process_response(coder, params): + """ + Process the View tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters + + Returns: + str: Result message + """ + file_path = params.get("file_path") + if file_path is not None: + return execute_view(coder, file_path) + else: + return "Error: Missing 'file_path' parameter for View" diff --git a/aider/tools/view_files_at_glob.py b/aider/tools/view_files_at_glob.py deleted file mode 100644 index d96668fc211..00000000000 --- a/aider/tools/view_files_at_glob.py +++ /dev/null @@ -1,70 +0,0 @@ -import fnmatch -import os - -view_files_at_glob_schema = { - "type": "function", - "function": { - "name": "ViewFilesAtGlob", - "description": "View files matching a glob pattern.", - "parameters": { - "type": "object", - "properties": { - "pattern": { - "type": "string", - "description": "The glob pattern to match files.", - }, - }, - "required": ["pattern"], - }, - }, -} - - -def execute_view_files_at_glob(coder, pattern): - """ - Execute a glob pattern and return matching files as text. - - This tool helps the LLM find files by pattern matching, similar to - how a developer would use glob patterns to find files. - """ - try: - # Find files matching the pattern - matching_files = [] - - # Make the pattern relative to root if it's absolute - if pattern.startswith("/"): - pattern = os.path.relpath(pattern, coder.root) - - # Get all files in the repo - all_files = coder.get_all_relative_files() - - # Find matches with pattern matching - for file in all_files: - if fnmatch.fnmatch(file, pattern): - matching_files.append(file) - - # Return formatted text instead of adding to context - if matching_files: - if len(matching_files) > 10: - result = ( - f"Found {len(matching_files)} files matching '{pattern}':" - f" {', '.join(matching_files[:10])} and {len(matching_files) - 10} more" - ) - coder.io.tool_output(f"📂 Found {len(matching_files)} files matching '{pattern}'") - else: - result = ( - f"Found {len(matching_files)} files matching '{pattern}':" - f" {', '.join(matching_files)}" - ) - coder.io.tool_output( - f"📂 Found files matching '{pattern}':" - f" {', '.join(matching_files[:5])}{' and more' if len(matching_files) > 5 else ''}" - ) - - return result - else: - coder.io.tool_output(f"⚠️ No files found matching '{pattern}'") - return f"No files found matching '{pattern}'" - except Exception as e: - coder.io.tool_error(f"Error in ViewFilesAtGlob: {str(e)}") - return f"Error: {str(e)}" diff --git a/aider/tools/view_files_matching.py b/aider/tools/view_files_matching.py index 0f061dbb97b..3ef6f9c75be 100644 --- a/aider/tools/view_files_matching.py +++ b/aider/tools/view_files_matching.py @@ -1,7 +1,7 @@ import fnmatch import re -view_files_matching_schema = { +schema = { "type": "function", "function": { "name": "ViewFilesMatching", @@ -29,6 +29,9 @@ }, } +# Normalized tool name for lookup +NORM_NAME = "viewfilesmatching" + def execute_view_files_matching(coder, pattern, file_pattern=None, regex=False): """ @@ -115,3 +118,24 @@ def execute_view_files_matching(coder, pattern, file_pattern=None, regex=False): except Exception as e: coder.io.tool_error(f"Error in ViewFilesMatching: {str(e)}") return f"Error: {str(e)}" + + +def process_response(coder, params): + """ + Process the ViewFilesMatching tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters + + Returns: + str: Result message + """ + pattern = params.get("pattern") + file_pattern = params.get("file_pattern") + regex = params.get("regex", False) + + if pattern is not None: + return execute_view_files_matching(coder, pattern, file_pattern, regex) + else: + return "Error: Missing 'pattern' parameter for ViewFilesMatching" diff --git a/aider/tools/view_files_with_symbol.py b/aider/tools/view_files_with_symbol.py index 34f0fe8a052..8d012e9fbbf 100644 --- a/aider/tools/view_files_with_symbol.py +++ b/aider/tools/view_files_with_symbol.py @@ -1,4 +1,4 @@ -view_files_with_symbol_schema = { +schema = { "type": "function", "function": { "name": "ViewFilesWithSymbol", @@ -16,6 +16,9 @@ }, } +# Normalized tool name for lookup +NORM_NAME = "viewfileswithsymbol" + def _execute_view_files_with_symbol(coder, symbol): """ @@ -106,3 +109,21 @@ def _execute_view_files_with_symbol(coder, symbol): except Exception as e: coder.io.tool_error(f"Error in ViewFilesWithSymbol: {str(e)}") return f"Error: {str(e)}" + + +def process_response(coder, params): + """ + Process the ViewFilesWithSymbol tool response. + + Args: + coder: The Coder instance + params: Dictionary of parameters + + Returns: + str: Result message + """ + symbol = params.get("symbol") + if symbol is not None: + return _execute_view_files_with_symbol(coder, symbol) + else: + return "Error: Missing 'symbol' parameter for ViewFilesWithSymbol" diff --git a/aider/tools/view_todo_list.py b/aider/tools/view_todo_list.py deleted file mode 100644 index c2540e58392..00000000000 --- a/aider/tools/view_todo_list.py +++ /dev/null @@ -1,57 +0,0 @@ -from .tool_utils import ToolError, format_tool_result, handle_tool_error - -view_todo_list_schema = { - "type": "function", - "function": { - "name": "ViewTodoList", - "description": "View the current todo list for tracking conversation steps and progress.", - "parameters": { - "type": "object", - "properties": {}, - "required": [], - }, - }, -} - - -def _execute_view_todo_list(coder): - """ - View the current todo list from .aider.todo.txt file. - Returns the todo list content or creates an empty one if it doesn't exist. - """ - tool_name = "ViewTodoList" - try: - # Define the todo file path - todo_file_path = ".aider.todo.txt" - abs_path = coder.abs_root_path(todo_file_path) - - # Check if file exists - import os - - if os.path.isfile(abs_path): - # Read existing todo list - content = coder.io.read_text(abs_path) - if content is None: - raise ToolError(f"Could not read todo list file: {todo_file_path}") - - # Check if content exceeds 4096 characters and warn - if len(content) > 4096: - coder.io.tool_warning( - "⚠️ Todo list content exceeds 4096 characters. Consider summarizing the plan" - " before proceeding." - ) - - if content.strip(): - result_message = f"Current todo list:\n```\n{content}\n```" - else: - result_message = "Todo list is empty. Use UpdateTodoList to add items." - else: - # Create empty todo list - result_message = "Todo list is empty. Use UpdateTodoList to add items." - - return format_tool_result(coder, tool_name, result_message) - - except ToolError as e: - return handle_tool_error(coder, tool_name, e, add_traceback=False) - except Exception as e: - return handle_tool_error(coder, tool_name, e) From c2ef1754917303f4098b98f4e4ab15b73e991254 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Sat, 8 Nov 2025 19:07:50 -0500 Subject: [PATCH 6/9] Add Agent Mode configuration and documentation of that --- README.md | 9 +- aider/coders/agent_coder.py | 66 +++++++- aider/coders/base_coder.py | 5 +- aider/main.py | 1 + aider/website/docs/config/agent-mode.md | 192 ++++++++++++++++++++++++ 5 files changed, 267 insertions(+), 6 deletions(-) create mode 100644 aider/website/docs/config/agent-mode.md diff --git a/README.md b/README.md index 0358397759c..9c3e4df8f91 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,12 @@ The current priorities are to improve core capabilities and user experience of t 6. **Agent Mode** - [Discussion](https://github.com/dwash96/aider-ce/issues/111) * [x] Renaming "navigator mode" to "agent mode" for simplicity - * [ ] Add an explicit "finished" internal tool - * [ ] Add a configuration json setting for agent mode to specify allowed local tools to use, tool call limits, etc. + * [x] Add an explicit "finished" internal tool + * [x] Add a configuration json setting for agent mode to specify allowed local tools to use, tool call limits, etc. * [ ] Add a RAG tool for the model to ask questions about the codebase * [ ] Make the system prompts more aggressive about removing unneeded files/content from the context + * [ ] Add a plugin-like system for allowing agent mode to use user-defined tools in simple python files + * [ ] Add a dynamic tool discovery tool to allow the system to have only the tools it needs in context ## Fork Additions @@ -70,7 +72,8 @@ This project aims to be compatible with upstream Aider, but with priority commit * [Allow Benchmarks to Use Repo Map For Better Accuracy](https://github.com/dwash96/aider-ce/pull/25) * [Read File Globbing](https://github.com/Aider-AI/aider/pull/3395) -### Other Notes +### Documentation and Other Notes +* [Agent Mode](https://github.com/dwash96/aider-ce/blob/main/aider/website/docs/config/agent-mode.md) * [MCP Configuration](https://github.com/dwash96/aider-ce/blob/main/aider/website/docs/config/mcp.md) * [Session Management](https://github.com/dwash96/aider-ce/blob/main/aider/website/docs/sessions.md) diff --git a/aider/coders/agent_coder.py b/aider/coders/agent_coder.py index 6076185cd7d..4d70b19d954 100644 --- a/aider/coders/agent_coder.py +++ b/aider/coders/agent_coder.py @@ -101,10 +101,10 @@ def __init__(self, *args, **kwargs): self.max_tool_calls = 100 # Maximum number of tool calls per response # Context management parameters + # Will be overridden by agent_config if provided self.large_file_token_threshold = ( 25000 # Files larger than this in tokens are considered large ) - self.max_files_per_glob = 50 # Maximum number of files to add at once via glob/grep # Enable context management by default only in agent mode self.context_management_enabled = True # Enabled by default for agent mode @@ -113,6 +113,7 @@ def __init__(self, *args, **kwargs): self.change_tracker = ChangeTracker() # Initialize tool registry + self.args = kwargs.get("args") self._tool_registry = self._build_tool_registry() # Track files added during current exploration @@ -139,6 +140,7 @@ def __init__(self, *args, **kwargs): def _build_tool_registry(self): """ Build a registry of available tools with their normalized names and process_response functions. + Handles agent configuration with whitelist/blacklist functionality. Returns: dict: Mapping of normalized tool names to tool modules @@ -178,12 +180,72 @@ def _build_tool_registry(self): view_files_with_symbol, ] + # Process agent configuration if provided + agent_config = self._get_agent_config() + tools_whitelist = agent_config.get("tools_whitelist", []) + tools_blacklist = agent_config.get("tools_blacklist", []) + + # Always include essential tools regardless of whitelist/blacklist + essential_tools = {"makeeditable", "replacetext", "view", "finished"} for module in tool_modules: if hasattr(module, "NORM_NAME") and hasattr(module, "process_response"): - registry[module.NORM_NAME] = module + tool_name = module.NORM_NAME + + # Check if tool should be included based on configuration + should_include = True + + # If whitelist is specified, only include tools in whitelist + if tools_whitelist: + should_include = tool_name in tools_whitelist + + # Always include essential tools + if tool_name in essential_tools: + should_include = True + + # Exclude tools in blacklist (unless they're essential) + if tool_name in tools_blacklist and tool_name not in essential_tools: + should_include = False + + if should_include: + registry[tool_name] = module return registry + def _get_agent_config(self): + """ + Parse and return agent configuration from args.agent_config. + + Returns: + dict: Agent configuration with defaults for missing values + """ + config = {} + + # Check if agent_config is provided via args + if ( + hasattr(self, "args") + and self.args + and hasattr(self.args, "agent_config") + and self.args.agent_config + ): + try: + config = json.loads(self.args.agent_config) + except (json.JSONDecodeError, TypeError) as e: + self.io.tool_warning(f"Failed to parse agent-config JSON: {e}") + return {} + + # Set defaults for missing values + if "large_file_token_threshold" not in config: + config["large_file_token_threshold"] = 25000 + if "tools_whitelist" not in config: + config["tools_whitelist"] = [] + if "tools_blacklist" not in config: + config["tools_blacklist"] = [] + + # Apply configuration to instance + self.large_file_token_threshold = config["large_file_token_threshold"] + + return config + def get_local_tool_schemas(self): """Returns the JSON schemas for all local tools using the tool registry.""" schemas = [] diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index c76bbaffd6d..589826f0681 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -157,6 +157,7 @@ async def create( io=None, from_coder=None, summarize_from_coder=True, + args=None, **kwargs, ): import aider.coders as coders @@ -220,7 +221,7 @@ async def create( for coder in coders.__all__: if hasattr(coder, "edit_format") and coder.edit_format == edit_format: - res = coder(main_model, io, **kwargs) + res = coder(main_model, io, args=args, **kwargs) await res.initialize_mcp_tools() res.original_kwargs = dict(kwargs) return res @@ -334,6 +335,7 @@ def __init__( self, main_model, io, + args=None, repo=None, fnames=None, add_gitignore_files=False, @@ -411,6 +413,7 @@ def __init__( self.suggest_shell_commands = suggest_shell_commands self.detect_urls = detect_urls + self.args = args self.num_cache_warming_pings = num_cache_warming_pings self.mcp_servers = mcp_servers diff --git a/aider/main.py b/aider/main.py index 179e352bea7..c315338ab11 100644 --- a/aider/main.py +++ b/aider/main.py @@ -1062,6 +1062,7 @@ def get_io(pretty): main_model=main_model, edit_format=args.edit_format, io=io, + args=args, repo=repo, fnames=fnames, read_only_fnames=read_only_fnames, diff --git a/aider/website/docs/config/agent-mode.md b/aider/website/docs/config/agent-mode.md new file mode 100644 index 00000000000..466f3710052 --- /dev/null +++ b/aider/website/docs/config/agent-mode.md @@ -0,0 +1,192 @@ +# Agent Mode + +Agent Mode is an operational mode in aider-ce that enables autonomous codebase exploration and modification using local tools. Instead of relying on traditional edit formats, Agent Mode uses a tool-based approach where the LLM can discover, analyze, and modify files through a series of tool calls. + +Agent Mode can be activated in the following ways + +In the interface: + +``` +/agent +``` + +In the command line: + +``` +aider-ce ... --agent +``` + +In the configuration files: + +``` +agent: true +``` + +## How Agent Mode Works + +### Core Architecture + +Agent Mode operates through a continuous loop where the LLM: + +1. **Receives a user request** and analyzes the current context +2. **Uses discovery tools** to find relevant files and information +3. **Executes editing tools** to make changes +4. **Processes results** and continues exploration and editing until the task is complete + +This loop continues automatically until the `Finished` tool is called, or the maximum number of iterations is reached. + +### Key Components + +#### Tool Registry System + +Agent Mode uses a centralized local tool registry that manages all available tools: + +- **File Discovery Tools**: `View`, `ViewFilesMatching`, `ViewFilesWithSymbol`, `Ls`, `Grep` +- **Editing Tools**: `ReplaceText`, `InsertBlock`, `DeleteBlock`, `ReplaceLines`, `DeleteLines` +- **Context Management Tools**: `MakeEditable`, `MakeReadonly`, `Remove` +- **Git Tools**: `GitDiff`, `GitLog`, `GitShow`, `GitStatus` +- **Utility Tools**: `UpdateTodoList`, `ListChanges`, `UndoChange`, `Finished` + +#### Enhanced Context Management + +Agent Mode includes some useful context management features: + +- **Automatic file tracking**: Files added during exploration are tracked separately +- **Context blocks**: Directory structure, git status, symbol outlines, and environment info +- **Token management**: Automatic calculation of context usage and warnings when approaching limits +- **Tool usage history**: Tracks repetitive tool usage to prevent exploration loops + +### Key Features + +#### Autonomous Context Management + +- **Proactive file discovery**: LLM can find relevant files without user guidance +- **Smart file removal**: Large files can be removed from context to save tokens +- **Dynamic context updates**: Context blocks provide real-time project information + +#### Granular Editing Capabilities + +Agent Mode prioritizes granular tools over SEARCH/REPLACE: + +- **Precision editing**: `ReplaceText` for targeted changes +- **Block operations**: `InsertBlock`, `DeleteBlock` for larger modifications +- **Line-based editing**: `ReplaceLines`, `DeleteLines` with safety protocols +- **Refactoring support**: `ExtractLines` for code reorganization + +#### Safety and Recovery + +- **Undo capability**: `UndoChange` tool for immediate recovery from mistakes +- **Dry run support**: Tools can be tested with `dry_run=True` +- **Line number verification**: Two-step process for line-based edits to prevents errors +- **Tool usage monitoring**: Prevents infinite loops by tracking repetitive patterns + + +### Workflow Process + +#### 1. Exploration Phase + +The LLM uses discovery tools to gather information: + +``` +Tool Call: ViewFilesMatching +Arguments: {"pattern": "config", "file_pattern": "*.py"} + +Tool Call: View +Arguments: {"file_path": "main.py"} + +Tool Call: Grep +Arguments: {"pattern": "function_name"} +``` + +Files found during exploration are added to context as read-only, allowing the LLM to analyze them without immediate editing. + +#### 2. Planning Phase + +The LLM uses the `UpdateTodoList` tool to track progress and plan complex changes: + +``` +Tool Call: UpdateTodoList +Arguments: {"content": "## Task: Add new feature\n- [ ] Analyze existing code\n- [ ] Implement new function\n- [ ] Add tests\n- [ ] Update documentation"} +``` + +#### 3. Execution Phase + +Files are made editable and modifications are applied: + +``` +Tool Call: MakeEditable +Arguments: {"file_path": "main.py"} + +Tool Call: ReplaceText +Arguments: {"file_path": "main.py", "find_text": "old_function", "replace_text": "new_function"} + +Tool Call: InsertBlock +Arguments: {"file_path": "main.py", "after_pattern": "import statements", "content": "new_imports"} +``` + +#### 4. Verification Phase + +Changes are verified and the process continues: + +``` +Tool Call: GitDiff +Arguments: {} + +Tool Call: ListChanges +Arguments: {} +``` + +#### 5. Completion Phase + +The above continues over and over until: + +``` +Tool Call: Finished +Arguments: {} +``` + +### Agent Configuration + +Agent Mode can be configured using the `--agent-config` command line argument, which accepts a JSON string for fine-grained control over tool availability and behavior. + +#### Configuration Options + +- **`tools_whitelist`**: Array of tool names to allow (only these tools will be available) +- **`tools_blacklist`**: Array of tool names to exclude (these tools will be disabled) +- **`large_file_token_threshold`**: Maximum token threshold for large file warnings (default: 25000) + +#### Essential Tools + +Certain tools are always available regardless of whitelist/blacklist settings: +- `make_editable` - Make files editable +- `replace_text` - Basic text replacement +- `view` - View files +- `finished` - Complete the task + +#### Usage Examples + +```bash +# Only allow specific tools +aider --agent --agent-config '{"tools_whitelist": ["view", "make_editable", "replace_text", "finished"]}' + +# Exclude specific tools +aider --agent --agent-config '{"tools_blacklist": ["command", "command_interactive"]}' + +# Custom large file threshold +aider --agent --agent-config '{"large_file_token_threshold": 10000}' + +# Combined configuration +aider --agent --agent-config '{"large_file_token_threshold": 10000, "tools_whitelist": ["view", "make_editable", "replace_text", "finished", "git_diff"]}' +``` + +This configuration system allows for fine-grained control over which tools are available in Agent Mode, enabling security-conscious deployments and specialized workflows while maintaining essential functionality. + +### Benefits + +- **Autonomous operation**: Reduces need for manual file management +- **Context awareness**: Real-time project information improves decision making +- **Precision editing**: Granular tools reduce errors compared to SEARCH/REPLACE +- **Scalable exploration**: Can handle large codebases through strategic context management +- **Recovery mechanisms**: Built-in undo and safety features + +Agent Mode represents a significant evolution in aider's capabilities, enabling more sophisticated and autonomous codebase manipulation while maintaining safety and control through the tool-based architecture. \ No newline at end of file From 3516d7b7511b3fba45eac1e90d932d00f080fd32 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Sat, 8 Nov 2025 19:12:15 -0500 Subject: [PATCH 7/9] Update Agent Mode Documentation --- aider/website/docs/config/agent-mode.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/aider/website/docs/config/agent-mode.md b/aider/website/docs/config/agent-mode.md index 466f3710052..a443744c920 100644 --- a/aider/website/docs/config/agent-mode.md +++ b/aider/website/docs/config/agent-mode.md @@ -158,8 +158,8 @@ Agent Mode can be configured using the `--agent-config` command line argument, w #### Essential Tools Certain tools are always available regardless of whitelist/blacklist settings: -- `make_editable` - Make files editable -- `replace_text` - Basic text replacement +- `makeeditable` - Make files editable +- `replacetext` - Basic text replacement - `view` - View files - `finished` - Complete the task @@ -167,7 +167,7 @@ Certain tools are always available regardless of whitelist/blacklist settings: ```bash # Only allow specific tools -aider --agent --agent-config '{"tools_whitelist": ["view", "make_editable", "replace_text", "finished"]}' +aider --agent --agent-config '{"tools_whitelist": ["view", "makeeditable", "replacetext", "finished"]}' # Exclude specific tools aider --agent --agent-config '{"tools_blacklist": ["command", "command_interactive"]}' @@ -176,7 +176,7 @@ aider --agent --agent-config '{"tools_blacklist": ["command", "command_interacti aider --agent --agent-config '{"large_file_token_threshold": 10000}' # Combined configuration -aider --agent --agent-config '{"large_file_token_threshold": 10000, "tools_whitelist": ["view", "make_editable", "replace_text", "finished", "git_diff"]}' +aider --agent --agent-config '{"large_file_token_threshold": 10000, "tools_whitelist": ["view", "makeeditable", "replacetext", "finished", "git_diff"]}' ``` This configuration system allows for fine-grained control over which tools are available in Agent Mode, enabling security-conscious deployments and specialized workflows while maintaining essential functionality. From 4656ea5dbc12e0c77814267e9cf4f938f102f195 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Sat, 8 Nov 2025 19:13:39 -0500 Subject: [PATCH 8/9] Update Agent Mode Documentation Part 2 --- aider/website/docs/config/agent-mode.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aider/website/docs/config/agent-mode.md b/aider/website/docs/config/agent-mode.md index a443744c920..4140db63305 100644 --- a/aider/website/docs/config/agent-mode.md +++ b/aider/website/docs/config/agent-mode.md @@ -170,13 +170,13 @@ Certain tools are always available regardless of whitelist/blacklist settings: aider --agent --agent-config '{"tools_whitelist": ["view", "makeeditable", "replacetext", "finished"]}' # Exclude specific tools -aider --agent --agent-config '{"tools_blacklist": ["command", "command_interactive"]}' +aider --agent --agent-config '{"tools_blacklist": ["command", "commandinteractive"]}' # Custom large file threshold aider --agent --agent-config '{"large_file_token_threshold": 10000}' # Combined configuration -aider --agent --agent-config '{"large_file_token_threshold": 10000, "tools_whitelist": ["view", "makeeditable", "replacetext", "finished", "git_diff"]}' +aider --agent --agent-config '{"large_file_token_threshold": 10000, "tools_whitelist": ["view", "makeeditable", "replacetext", "finished", "gitdiff"]}' ``` This configuration system allows for fine-grained control over which tools are available in Agent Mode, enabling security-conscious deployments and specialized workflows while maintaining essential functionality. From da36c952678ea50845379dbe1f2740dafcdd2ce2 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Sat, 8 Nov 2025 19:20:16 -0500 Subject: [PATCH 9/9] Add LiteLLM BadGateway Error to Exceptions List --- aider/exceptions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aider/exceptions.py b/aider/exceptions.py index 0348df5b4b0..5fb84d992c6 100644 --- a/aider/exceptions.py +++ b/aider/exceptions.py @@ -20,6 +20,7 @@ class ExInfo: "The API provider is not able to authenticate you. Check your API key.", ), ExInfo("AzureOpenAIError", True, None), + ExInfo("BadGatewayError", False, None), ExInfo("BadRequestError", False, None), ExInfo("BudgetExceededError", True, None), ExInfo(