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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@
# Ignore specific files
aider/__version__.py
aider/_version.py
*.pyc
*.pyc
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.87.0.dev"
__version__ = "0.87.1.dev"
safe_version = __version__

try:
Expand Down
33 changes: 33 additions & 0 deletions aider/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,30 @@ def get_parser(default_config_files, git_root):
),
)

##########
group = parser.add_argument_group("Context Compaction")
group.add_argument(
"--enable-context-compaction",
action=argparse.BooleanOptionalAction,
default=False,
help="Enable automatic compaction of chat history to conserve tokens (default: False)",
)
group.add_argument(
"--context-compaction-max-tokens",
type=int,
default=None,
help=(
"The maximum number of tokens in the conversation before context compaction is"
" triggered. (default: 80%% of model's context window)"
),
)
group.add_argument(
"--context-compaction-summary-tokens",
type=int,
default=4096,
help="The target maximum number of tokens for the generated summary. (default: 4096)",
)

##########
group = parser.add_argument_group("Cache settings")
group.add_argument(
Expand Down Expand Up @@ -272,6 +296,15 @@ def get_parser(default_config_files, git_root):
default=2,
help="Multiplier for map tokens when no files are specified (default: 2)",
)
group.add_argument(
"--map-max-line-length",
type=int,
default=100,
help=(
"Maximum line length for the repo map code. Prevents sending crazy long lines of"
" minified JS files etc. (default: 100)"
),
)

##########
group = parser.add_argument_group("History Files")
Expand Down
16 changes: 8 additions & 8 deletions aider/coders/architect_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@


class ArchitectPrompts(CoderPrompts):
main_system = """Act as an expert architect engineer and provide direction to your editor engineer.
Study the change request and the current code.
Describe how to modify the code to complete the request.
The editor engineer will rely solely on your instructions, so make them unambiguous and complete.
Explain all needed code changes clearly and completely, but concisely.
Just show the changes needed.

DO NOT show the entire updated function/file/etc!
main_system = """Act as an expert architect engineer providing direction to an editor engineer.
Deeply understand the user's change request and the provided code context.
Think step-by-step to develop a clear plan for the required code modifications.
Consider potential edge cases and how the changes should be verified.
Describe the plan and the necessary modifications to the editor engineer. Your instructions must be unambiguous, complete, and concise as the editor will rely solely on them.
Focus on *what* needs to change and *why*.

DO NOT show large blocks of code or the entire updated file content. Explain the changes conceptually.

Always reply to the user in {language}.
"""
Expand Down
176 changes: 126 additions & 50 deletions aider/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,7 @@
self.edit_format = edit_format
self.valid_formats = valid_formats
super().__init__(
f"Unknown edit format {edit_format}. Valid formats are: {
', '.join(valid_formats)}"
f"Unknown edit format {edit_format}. Valid formats are: {', '.join(valid_formats)}"
)


Expand Down Expand Up @@ -129,10 +128,10 @@
file_watcher = None
mcp_servers = None
mcp_tools = None

Check failure on line 131 in aider/coders/base_coder.py

View workflow job for this annotation

GitHub Actions / pre-commit

W293 blank line contains whitespace

Check failure on line 131 in aider/coders/base_coder.py

View workflow job for this annotation

GitHub Actions / pre-commit

W293 blank line contains whitespace
# Context management settings (for all modes)
context_management_enabled = False # Disabled by default except for navigator mode
large_file_token_threshold = 25000 # Files larger than this will be truncated when context management is enabled

Check failure on line 134 in aider/coders/base_coder.py

View workflow job for this annotation

GitHub Actions / pre-commit

E501 line too long (117 > 100 characters)

Check failure on line 134 in aider/coders/base_coder.py

View workflow job for this annotation

GitHub Actions / pre-commit

E501 line too long (117 > 100 characters)

@classmethod
def create(
Expand Down Expand Up @@ -340,6 +339,7 @@
test_cmd=None,
aider_commit_hashes=None,
map_mul_no_files=8,
map_max_line_length=100,
commands=None,
summarizer=None,
total_cost=0.0,
Expand All @@ -358,6 +358,9 @@
auto_copy_context=False,
auto_accept_architect=True,
mcp_servers=None,
enable_context_compaction=False,
context_compaction_max_tokens=None,
context_compaction_summary_tokens=8192,
):
# Fill in a dummy Analytics if needed, but it is never .enable()'d
self.analytics = analytics if analytics is not None else Analytics()
Expand Down Expand Up @@ -386,6 +389,11 @@

self.num_cache_warming_pings = num_cache_warming_pings
self.mcp_servers = mcp_servers
self.enable_context_compaction = enable_context_compaction


self.context_compaction_max_tokens = context_compaction_max_tokens

Check failure on line 395 in aider/coders/base_coder.py

View workflow job for this annotation

GitHub Actions / pre-commit

E303 too many blank lines (2)

Check failure on line 395 in aider/coders/base_coder.py

View workflow job for this annotation

GitHub Actions / pre-commit

E303 too many blank lines (2)
self.context_compaction_summary_tokens = context_compaction_summary_tokens

if not fnames:
fnames = []
Expand Down Expand Up @@ -531,6 +539,7 @@
max_inp_tokens,
map_mul_no_files=map_mul_no_files,
refresh=map_refresh,
max_code_line_length=map_max_line_length,
)

self.summarizer = summarizer or ChatSummary(
Expand Down Expand Up @@ -681,16 +690,16 @@
if self.context_management_enabled:
# Calculate tokens for this file
file_tokens = self.main_model.token_count(content)

Check failure on line 693 in aider/coders/base_coder.py

View workflow job for this annotation

GitHub Actions / pre-commit

W293 blank line contains whitespace

Check failure on line 693 in aider/coders/base_coder.py

View workflow job for this annotation

GitHub Actions / pre-commit

W293 blank line contains whitespace
if file_tokens > self.large_file_token_threshold:
# Truncate the file content
lines = content.splitlines()
total_lines = len(lines)

Check failure on line 697 in aider/coders/base_coder.py

View workflow job for this annotation

GitHub Actions / pre-commit

F841 local variable 'total_lines' is assigned to but never used

Check failure on line 697 in aider/coders/base_coder.py

View workflow job for this annotation

GitHub Actions / pre-commit

F841 local variable 'total_lines' is assigned to but never used

Check failure on line 698 in aider/coders/base_coder.py

View workflow job for this annotation

GitHub Actions / pre-commit

W293 blank line contains whitespace

Check failure on line 698 in aider/coders/base_coder.py

View workflow job for this annotation

GitHub Actions / pre-commit

W293 blank line contains whitespace
# Keep the first and last parts of the file with a marker in between
keep_lines = self.large_file_token_threshold // 40 # Rough estimate of tokens per line

Check failure on line 700 in aider/coders/base_coder.py

View workflow job for this annotation

GitHub Actions / pre-commit

E501 line too long (111 > 100 characters)

Check failure on line 700 in aider/coders/base_coder.py

View workflow job for this annotation

GitHub Actions / pre-commit

E501 line too long (111 > 100 characters)
first_chunk = lines[:keep_lines//2]

Check failure on line 701 in aider/coders/base_coder.py

View workflow job for this annotation

GitHub Actions / pre-commit

E226 missing whitespace around arithmetic operator

Check failure on line 701 in aider/coders/base_coder.py

View workflow job for this annotation

GitHub Actions / pre-commit

E226 missing whitespace around arithmetic operator
last_chunk = lines[-(keep_lines//2):]

Check failure on line 702 in aider/coders/base_coder.py

View workflow job for this annotation

GitHub Actions / pre-commit

E226 missing whitespace around arithmetic operator

Check failure on line 702 in aider/coders/base_coder.py

View workflow job for this annotation

GitHub Actions / pre-commit

E226 missing whitespace around arithmetic operator

truncated_content = "\n".join(first_chunk)
truncated_content += f"\n\n... [File truncated due to size ({file_tokens} tokens). Use /context-management to toggle truncation off] ...\n\n"
Expand Down Expand Up @@ -979,6 +988,7 @@
if not self.io.placeholder:
self.copy_context()
user_message = self.get_input()
self.compact_context_if_needed()
self.run_one(user_message, preproc)
self.show_undo_hint()
except KeyboardInterrupt:
Expand Down Expand Up @@ -1099,7 +1109,7 @@
self.last_keyboard_interrupt = now

def summarize_start(self):
if not self.summarizer.too_big(self.done_messages):
if not self.summarizer.check_max_tokens(self.done_messages):
return

self.summarize_end()
Expand All @@ -1113,10 +1123,10 @@
def summarize_worker(self):
self.summarizing_messages = list(self.done_messages)
try:
self.summarized_done_messages = self.summarizer.summarize(
self.summarizing_messages)
self.summarized_done_messages = self.summarizer.summarize(self.summarizing_messages)
except ValueError as err:
self.io.tool_warning(err.args[0])
self.summarized_done_messages = self.summarizing_messages

if self.verbose:
self.io.tool_output("Finished summarizing chat history.")
Expand All @@ -1133,9 +1143,49 @@
self.summarizing_messages = None
self.summarized_done_messages = []

def compact_context_if_needed(self):
if not self.enable_context_compaction:
self.summarize_start()
return

if not self.summarizer.check_max_tokens(self.done_messages, max_tokens=self.context_compaction_max_tokens):
return

self.io.tool_output("Compacting chat history to make room for new messages...")

try:
# Create a summary of the conversation
summary_text = 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."
),
},
]
self.io.tool_output("...chat history compacted.")
except Exception as e:
self.io.tool_warning(f"Context compaction failed: {e}")
self.io.tool_warning("Proceeding with full history for now.")
self.summarize_start()
return

def move_back_cur_messages(self, message):
self.done_messages += self.cur_messages
self.summarize_start()

# TODO check for impact on image messages
if message:
Expand Down Expand Up @@ -2341,59 +2391,63 @@
self.partial_response_tool_call = []

for chunk in completion:
if len(chunk.choices) == 0:
continue
if isinstance(chunk, str):
text = chunk
received_content = True
else:
if len(chunk.choices) == 0:
continue

if (
hasattr(chunk.choices[0], "finish_reason")
and chunk.choices[0].finish_reason == "length"
):
raise FinishReasonLength()
if (
hasattr(chunk.choices[0], "finish_reason")
and chunk.choices[0].finish_reason == "length"
):
raise FinishReasonLength()

if chunk.choices[0].delta.tool_calls:
self.partial_response_tool_call.append(chunk)
if chunk.choices[0].delta.tool_calls:
self.partial_response_tool_call.append(chunk)

try:
func = chunk.choices[0].delta.function_call
# dump(func)
for k, v in func.items():
if k in self.partial_response_function_call:
self.partial_response_function_call[k] += v
else:
self.partial_response_function_call[k] = v
try:
func = chunk.choices[0].delta.function_call
# dump(func)
for k, v in func.items():
if k in self.partial_response_function_call:
self.partial_response_function_call[k] += v
else:
self.partial_response_function_call[k] = v

received_content = True
except AttributeError:
pass
received_content = True
except AttributeError:
pass

text = ""
text = ""

try:
reasoning_content = chunk.choices[0].delta.reasoning_content
except AttributeError:
try:
reasoning_content = chunk.choices[0].delta.reasoning
reasoning_content = chunk.choices[0].delta.reasoning_content
except AttributeError:
reasoning_content = None

if reasoning_content:
if not self.got_reasoning_content:
text += f"<{REASONING_TAG}>\n\n"
text += reasoning_content
self.got_reasoning_content = True
received_content = True

try:
content = chunk.choices[0].delta.content
if content:
if self.got_reasoning_content and not self.ended_reasoning_content:
text += f"\n\n</{self.reasoning_tag_name}>\n\n"
self.ended_reasoning_content = True

text += content
try:
reasoning_content = chunk.choices[0].delta.reasoning
except AttributeError:
reasoning_content = None

if reasoning_content:
if not self.got_reasoning_content:
text += f"<{REASONING_TAG}>\n\n"
text += reasoning_content
self.got_reasoning_content = True
received_content = True
except AttributeError:
pass

try:
content = chunk.choices[0].delta.content
if content:
if self.got_reasoning_content and not self.ended_reasoning_content:
text += f"\n\n</{self.reasoning_tag_name}>\n\n"
self.ended_reasoning_content = True

text += content
received_content = True
except AttributeError:
pass

if received_content:
self._stop_waiting_spinner()
Expand Down Expand Up @@ -2806,6 +2860,28 @@
except JSONDecodeError:
pass

def _find_occurrences(self, content, pattern, near_context=None):
"""Find all occurrences of pattern, optionally filtered by near_context."""
occurrences = []
start = 0
while True:
index = content.find(pattern, start)
if index == -1:
break

if near_context:
# Check if near_context is within a window around the match
window_start = max(0, index - 200)
window_end = min(len(content), index + len(pattern) + 200)
window = content[window_start:window_end]
if near_context in window:
occurrences.append(index)
else:
occurrences.append(index)

start = index + 1 # Move past this occurrence's start
return occurrences

# commits...

def get_context_from_history(self, history):
Expand Down
12 changes: 12 additions & 0 deletions aider/coders/base_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,15 @@ class CoderPrompts:

rename_with_shell = ""
go_ahead_tip = ""

compaction_prompt = """You are an expert at summarizing conversations.
The user is going to provide you with a conversation that 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:
- 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?

Here is the conversation so far:
"""
5 changes: 4 additions & 1 deletion aider/coders/editblock_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ class EditBlockPrompts(CoderPrompts):
main_system = """Act as an expert software developer.
Always use best practices when coding.
Respect and use existing conventions, libraries, etc that are already present in the code base.
Think step-by-step. Plan your changes carefully. Explain your plan first.
{final_reminders}
Take requests for changes to the supplied code.
If the request is ambiguous, ask questions.
Consider potential edge cases.
Think about how to verify your changes are correct.

Always reply to the user in {language}.

Once you understand the request you MUST:
Once you understand the request and have a plan, you MUST:

1. Decide if you need to propose *SEARCH/REPLACE* edits to any files that haven't been added to the chat. You can create new files without asking!

Expand Down
5 changes: 4 additions & 1 deletion aider/coders/editor_editblock_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@


class EditorEditBlockPrompts(EditBlockPrompts):
main_system = """Act as an expert software developer who edits source code.
main_system = """Act as an expert software developer tasked with editing source code based on instructions from an architect.
Carefully implement the changes described in the request.
{final_reminders}
Describe each change with a *SEARCH/REPLACE block* per the examples below.
Ensure the SEARCH block exactly matches the original code.
Ensure the REPLACE block correctly implements the requested change.
All changes to files must use this *SEARCH/REPLACE block* format.
ONLY EVER RETURN CODE IN A *SEARCH/REPLACE BLOCK*!
"""
Expand Down
Loading
Loading