diff --git a/README.md b/README.md index 879fe204283..9c3e4df8f91 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,15 @@ 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 + * [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 This project aims to be compatible with upstream Aider, but with priority commits merged in and with some opportunistic bug fixes and optimizations @@ -63,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/__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: diff --git a/aider/args.py b/aider/args.py index 0a1bb532b87..4c8865f0040 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", @@ -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/__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 75% rename from aider/coders/navigator_coder.py rename to aider/coders/agent_coder.py index cfa0505353a..4d70b19d954 100644 --- a/aider/coders/navigator_coder.py +++ b/aider/coders/agent_coder.py @@ -23,106 +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_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, -# 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 NavigatorCoder(Coder): - """Mode where the LLM autonomously manages which files are in context.""" - edit_format = "navigator" - # TODO: We'll turn on granular editing by default once those tools stabilize - use_granular_editing = False +class AgentCoder(Coder): + """Mode where the LLM autonomously manages which files are in context.""" + + edit_format = "agent" 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() # Dictionary to track recently removed files self.recently_removed = {} @@ -154,17 +101,21 @@ 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 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() + # Initialize tool registry + self.args = kwargs.get("args") + self._tool_registry = self._build_tool_registry() + # Track files added during current exploration self.files_added_in_exploration = set() @@ -186,39 +137,132 @@ 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. + Handles agent configuration with whitelist/blacklist functionality. + + 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, ] + # 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"): + 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 = [] + + # 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() @@ -268,47 +312,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) @@ -476,16 +512,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 = NavigatorPrompts() if enabled else NavigatorLegacyPrompts() - def get_context_symbol_outline(self): """ Generate a symbol outline for files currently in context using Tree-sitter, @@ -931,35 +957,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: @@ -976,6 +975,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. @@ -1001,7 +1003,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. @@ -1108,7 +1110,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): """ @@ -1404,412 +1446,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)}" @@ -2197,7 +1840,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 +1848,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_prompts.py similarity index 92% rename from aider/coders/navigator_legacy_prompts.py rename to aider/coders/agent_prompts.py index 72beee97962..c29e7569b4b 100644 --- a/aider/coders/navigator_legacy_prompts.py +++ b/aider/coders/agent_prompts.py @@ -3,11 +3,11 @@ from .base_prompts import CoderPrompts -class NavigatorLegacyPrompts(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. """ @@ -27,8 +27,9 @@ class NavigatorLegacyPrompts(CoderPrompts): 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 6ea6ae4f9ec..589826f0681 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 ) @@ -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 @@ -2233,7 +2236,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/coders/navigator_prompts.py b/aider/coders/navigator_prompts.py deleted file mode 100644 index 1bf0a8a8466..00000000000 --- a/aider/coders/navigator_prompts.py +++ /dev/null @@ -1,103 +0,0 @@ -# flake8: noqa: E501 - -from .base_prompts import CoderPrompts - - -class NavigatorPrompts(CoderPrompts): - """ - Prompt templates for the Navigator mode, which enables autonomous codebase exploration. - - The NavigatorCoder 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 creating 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/commands.py b/aider/commands.py index ed530550416..3523e98be3b 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 @@ -1298,33 +1298,6 @@ def cmd_context_blocks(self, args=""): " be included." ) - def cmd_granular_editing(self, args=""): - "Toggle granular editing tools in navigator mode" - if not hasattr(self.coder, "use_granular_editing"): - self.io.tool_error("Granular editing toggle is only available in navigator mode.") - return - - # Toggle the setting using the navigator'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 - navigator 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" - " editing." - ) - def cmd_ls(self, args): "List all known files and indicate which are included in the chat session" @@ -1452,7 +1425,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 +1444,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/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( diff --git a/aider/main.py b/aider/main.py index 729eabe6ec1..c315338ab11 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: @@ -1056,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/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) ) 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", 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) diff --git a/aider/website/docs/config/agent-mode.md b/aider/website/docs/config/agent-mode.md new file mode 100644 index 00000000000..4140db63305 --- /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: +- `makeeditable` - Make files editable +- `replacetext` - Basic text replacement +- `view` - View files +- `finished` - Complete the task + +#### Usage Examples + +```bash +# Only allow specific tools +aider --agent --agent-config '{"tools_whitelist": ["view", "makeeditable", "replacetext", "finished"]}' + +# Exclude specific tools +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", "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. + +### 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