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":