diff --git a/aider/__init__.py b/aider/__init__.py index e7136a82fda..c7b30bd3c60 100644 --- a/aider/__init__.py +++ b/aider/__init__.py @@ -1,6 +1,6 @@ from packaging import version -__version__ = "0.88.22.dev" +__version__ = "0.88.24.dev" safe_version = __version__ try: diff --git a/aider/args.py b/aider/args.py index 8c0f64a7fb2..f75556d3090 100644 --- a/aider/args.py +++ b/aider/args.py @@ -248,7 +248,7 @@ def get_parser(default_config_files, git_root): default=None, help=( "The maximum number of tokens in the conversation before context compaction is" - " triggered. (default: 80%% of model's context window)" + " triggered. (default: 80% of model's context window)" ), ) group.add_argument( diff --git a/aider/coders/agent_coder.py b/aider/coders/agent_coder.py index f11de0976a1..17ee14ac641 100644 --- a/aider/coders/agent_coder.py +++ b/aider/coders/agent_coder.py @@ -140,6 +140,7 @@ def __init__(self, *args, **kwargs): self.skip_cli_confirmations = False + self.agent_finished = False self._get_agent_config() super().__init__(*args, **kwargs) @@ -944,6 +945,7 @@ async def process_tool_calls(self, tool_call_response): """ Track tool usage before calling the base implementation. """ + self.agent_finished = False self.auto_save_session() if self.partial_response_tool_calls: @@ -968,7 +970,6 @@ async def reply_completed(self): a final answer to the user's question. """ # 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: @@ -2000,7 +2001,7 @@ def get_todo_list(self): if not os.path.isfile(abs_path): return ( '\n' - "Todo list does not exist. Please update it." + "Todo list does not exist. Please update it with the `UpdataTodoList` tool." "" ) @@ -2012,7 +2013,7 @@ def get_todo_list(self): # Format the todo list context block result = '\n' result += "## Current Todo List\n\n" - result += "Below is the current todo list managed via `UpdateTodoList` tool:\n\n" + result += "Below is the current todo list managed via the `UpdateTodoList` tool:\n\n" result += f"```\n{content}\n```\n" result += "" diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 7b8b0f4d220..85628b0f6a5 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -885,7 +885,15 @@ def get_repo_map(self, force_refresh=False): self.io.update_spinner("Updating repo map") cur_msg_text = self.get_cur_message_text() - staged_files_hash = hash(str([item.a_path for item in self.repo.repo.index.diff("HEAD")])) + try: + staged_files_hash = hash( + str([item.a_path for item in self.repo.repo.index.diff("HEAD")]) + ) + except ANY_GIT_ERROR as err: + # Handle git errors gracefully - use a fallback hash + self.io.tool_warning(f"Git error while checking staged files for repo map: {err}") + staged_files_hash = hash(str(time.time())) # Use timestamp as fallback + read_only_count = len(set(self.abs_read_only_fnames)) + len( set(self.abs_read_only_stubs_fnames) ) @@ -896,7 +904,6 @@ def get_repo_map(self, force_refresh=False): or read_only_count != self.data_cache["repo"]["read_only_count"] ): self.data_cache["repo"]["last_key"] = staged_files_hash - mentioned_idents = self.data_cache["repo"]["mentioned_idents"] mentioned_fnames = self.get_file_mentions(cur_msg_text) mentioned_fnames.update(self.get_ident_filename_matches(mentioned_idents)) @@ -1334,9 +1341,10 @@ async def generate(self, user_message, preproc): await asyncio.sleep(0.1) try: - self.compact_context_completed = False - await self.compact_context_if_needed() - self.compact_context_completed = True + if not self.enable_context_compaction: + self.compact_context_completed = False + await self.compact_context_if_needed() + self.compact_context_completed = True self.run_one_completed = False await self.run_one(user_message, preproc) @@ -1426,6 +1434,9 @@ async def run_one(self, user_message, preproc): else: message = self.reflected_message + if self.enable_context_compaction: + await self.compact_context_if_needed() + async def check_and_open_urls(self, exc, friendly_msg=None): """Check exception for URLs, offer to open in a browser, with user-friendly error msgs.""" text = str(exc) @@ -1518,38 +1529,81 @@ async def compact_context_if_needed(self): self.summarize_start() return - if not self.summarizer.check_max_tokens( - self.done_messages, max_tokens=self.context_compaction_max_tokens - ): + # Check if combined messages exceed the token limit, + # Exclude first cur_message since that's the user's initial input + done_tokens = self.summarizer.count_tokens(self.done_messages) + cur_tokens = self.summarizer.count_tokens(self.cur_messages[1:]) + combined_tokens = done_tokens + cur_tokens + + if combined_tokens < self.context_compaction_max_tokens: return self.io.tool_output("Compacting chat history to make room for new messages...") + self.io.update_spinner("Compacting...") try: - # Create a summary of the conversation - summary_text = await self.summarizer.summarize_all_as_text( - self.done_messages, - self.gpt_prompts.compaction_prompt, - self.context_compaction_summary_tokens, - ) - if not summary_text: - raise ValueError("Summarization returned an empty result.") + # Check if done_messages alone exceed the limit + if done_tokens > self.context_compaction_max_tokens or done_tokens > cur_tokens: + # Create a summary of the done_messages + summary_text = await self.summarizer.summarize_all_as_text( + self.done_messages, + self.gpt_prompts.compaction_prompt, + self.context_compaction_summary_tokens, + ) + + if not summary_text: + raise ValueError("Summarization returned an empty result.") + + # Replace old messages with the summary + self.done_messages = [ + { + "role": "user", + "content": summary_text, + }, + { + "role": "assistant", + "content": ( + "Ok, I will use this summary as the context for our conversation going" + " forward." + ), + }, + ] + + # Check if cur_messages alone exceed the limit (after potentially compacting done_messages) + if cur_tokens > self.context_compaction_max_tokens or cur_tokens > done_tokens: + # Create a summary of the cur_messages + cur_summary_text = await self.summarizer.summarize_all_as_text( + self.cur_messages, + self.gpt_prompts.compaction_prompt, + self.context_compaction_summary_tokens, + ) + + if not cur_summary_text: + raise ValueError("Summarization of current messages returned an empty result.") + + # Replace current messages with the summary + self.cur_messages = [ + self.cur_messages[0], + { + "role": "assistant", + "content": "Ok. I am awaiting your summary of our goals to proceed.", + }, + { + "role": "user", + "content": f"Here is a summary of our current goals:\n{cur_summary_text}", + }, + { + "role": "assistant", + "content": ( + "Ok, I will use this summary and proceed with our task." + " I will first apply any changes in the summary and then" + " continue exploration as necessary." + ), + }, + ] - # Replace old messages with the summary - self.done_messages = [ - { - "role": "user", - "content": summary_text, - }, - { - "role": "assistant", - "content": ( - "Ok, I will use this summary as the context for our conversation going" - " forward." - ), - }, - ] self.io.tool_output("...chat history compacted.") + self.io.update_spinner(self.io.last_spinner_text) except Exception as e: self.io.tool_warning(f"Context compaction failed: {e}") self.io.tool_warning("Proceeding with full history for now.") @@ -3318,14 +3372,19 @@ def is_file_safe(self, fname): def get_all_relative_files(self): if self.repo_map and self.repo: - staged_files_hash = hash( - str([item.a_path for item in self.repo.repo.index.diff("HEAD")]) - ) - if ( - staged_files_hash == self.data_cache["repo"]["last_key"] - and self.data_cache["relative_files"] - ): - return self.data_cache["relative_files"] + try: + staged_files_hash = hash( + str([item.a_path for item in self.repo.repo.index.diff("HEAD")]) + ) + if ( + staged_files_hash == self.data_cache["repo"]["last_key"] + and self.data_cache["relative_files"] + ): + return self.data_cache["relative_files"] + except ANY_GIT_ERROR as err: + # Handle git errors gracefully - fall back to getting tracked files + self.io.tool_warning(f"Git error while checking staged files: {err}") + # Continue to get tracked files normally if self.repo: files = self.repo.get_tracked_files() diff --git a/aider/coders/base_prompts.py b/aider/coders/base_prompts.py index 6a10f9cde07..30633a8abf4 100644 --- a/aider/coders/base_prompts.py +++ b/aider/coders/base_prompts.py @@ -77,11 +77,14 @@ class CoderPrompts: This conversation is getting too long to fit in the context window of a language model. You need to summarize the conversation to reduce its length, while retaining all the important information. -The summary should contain three parts: +The summary should contain four parts: - Overall Goal: What is the user trying to achieve with this conversation? - Next Steps: What are the next steps for the language model to take to help the user? - Create a checklist of what has been done and what is left to do. -- Active files: What files are currently in the context window? + Describe the current investigation path and intention. +- Key Findings: Keep information most important to prevent having to search for it again + This should be quite specific (e/g. relevant files, method names, relevant lines of code, and code structure) +- Active files: What files are currently most relevant to the discussion? + Be confident in proceeding with any in progress edits. Here is the conversation so far: """ diff --git a/aider/commands.py b/aider/commands.py index 0d24b7733f0..7a55af82f53 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -1498,8 +1498,8 @@ async def _generic_chat_command(self, args, edit_format, placeholder=None): from aider.coders.base_coder import Coder - main_model = self.coder.main_model - edit_format = self.coder.edit_format + original_main_model = self.coder.main_model + original_edit_format = self.coder.edit_format coder = await Coder.create( io=self.io, @@ -1515,8 +1515,8 @@ async def _generic_chat_command(self, args, edit_format, placeholder=None): self.coder.aider_commit_hashes = coder.aider_commit_hashes raise SwitchCoder( - main_model=main_model, - edit_format=edit_format, + main_model=original_main_model, + edit_format=original_edit_format, done_messages=coder.done_messages, cur_messages=coder.cur_messages, ) diff --git a/aider/history.py b/aider/history.py index 3a696a8280a..1b5fe8ee583 100644 --- a/aider/history.py +++ b/aider/history.py @@ -30,6 +30,11 @@ def tokenize(self, messages): sized.append((tokens, msg)) return sized + def count_tokens(self, messages): + sized = self.tokenize(messages) + total = sum(tokens for tokens, _msg in sized) + return total + async def summarize(self, messages, depth=0): messages = await self.summarize_real(messages) if messages and messages[-1]["role"] != "assistant":