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