Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion aider/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from packaging import version

__version__ = "0.88.22.dev"
__version__ = "0.88.24.dev"
safe_version = __version__

try:
Expand Down
2 changes: 1 addition & 1 deletion aider/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
7 changes: 4 additions & 3 deletions aider/coders/agent_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -2000,7 +2001,7 @@ def get_todo_list(self):
if not os.path.isfile(abs_path):
return (
'<context name="todo_list">\n'
"Todo list does not exist. Please update it."
"Todo list does not exist. Please update it with the `UpdataTodoList` tool."
"</context>"
)

Expand All @@ -2012,7 +2013,7 @@ def get_todo_list(self):
# Format the todo list context block
result = '<context name="todo_list">\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 += "</context>"

Expand Down
135 changes: 97 additions & 38 deletions aider/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
Expand All @@ -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))
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.")
Expand Down Expand Up @@ -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()
Expand Down
9 changes: 6 additions & 3 deletions aider/coders/base_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
8 changes: 4 additions & 4 deletions aider/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
)
Expand Down
5 changes: 5 additions & 0 deletions aider/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
Loading