From 5ae376c4de3709f9c840b666c6178e850383c9b5 Mon Sep 17 00:00:00 2001 From: pjcreath Date: Mon, 24 Nov 2025 09:34:20 -0500 Subject: [PATCH 01/11] Add .aider* and virtual env to repo gitignore. --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 2c147719a65..d2cffc639ff 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ aider/__version__.py aider/_version.py *.pyc +.aider* +env/ From dde21de034229a21a628eb2e8805660ba5d31902 Mon Sep 17 00:00:00 2001 From: pjcreath Date: Sun, 23 Nov 2025 15:21:16 -0500 Subject: [PATCH 02/11] Add profile method to InputOutput. --- aider/io.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/aider/io.py b/aider/io.py index 37458214dbd..5c45527a452 100644 --- a/aider/io.py +++ b/aider/io.py @@ -343,6 +343,8 @@ def __init__( self.bell_on_next_input = False self.notifications = notifications self.verbose = verbose + self.profile_start_time = None + self.profile_last_time = None # Variables used to interface with base_coder self.coder = None @@ -1354,6 +1356,25 @@ def tool_output(self, *messages, log_only=False, bold=False): self.stream_print(*messages, style=style) + def profile(self, *messages, start=False): + if not self.verbose: + return + + now = time.time() + message_str = " ".join(map(str, messages)) + + # Treat uninitialized as an implicit start. + if start or self.profile_start_time is None: + self.profile_start_time = now + self.stream_print(f"PROFILE: {message_str}") + else: + total_elapsed = now - self.profile_start_time + last_elapsed = now - self.profile_last_time + output_message = f"PROFILE: [+{last_elapsed:6.2f}s] {message_str} (total {total_elapsed:.2f}s)" + self.stream_print(output_message) + + self.profile_last_time = now + def assistant_output(self, message, pretty=None): if not message: self.tool_warning("Empty response received from LLM. Check your provider account?") From 929ec02e7ea8d9ece1d8aeedb443ef4c29f90905 Mon Sep 17 00:00:00 2001 From: pjcreath Date: Mon, 24 Nov 2025 09:40:41 -0500 Subject: [PATCH 03/11] Add profiling to repo map generation. --- aider/repomap.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/aider/repomap.py b/aider/repomap.py index 38b6e427eec..9e7c579ba23 100644 --- a/aider/repomap.py +++ b/aider/repomap.py @@ -198,6 +198,9 @@ def __init__( self._mentioned_ident_similarity = 0.8 if self.verbose: + self.io.tool_output( + f"RepoMap loaded entries from tags cache: {len(self.TAGS_CACHE)}" + ) self.io.tool_output( f"RepoMap initialized with map_mul_no_files: {self.map_mul_no_files}" ) @@ -696,6 +699,8 @@ def get_ranked_tags( if tag.specific_kind == "import": file_imports[rel_fname].add(tag.name) + self.io.profile("process files") + if self.use_enhanced_map and len(file_imports) > 0: import_ast_mode = True @@ -791,6 +796,8 @@ def get_ranked_tags( weight = num_refs * use_mul * 2 ** (-1 * path_distance) G.add_edge(referencer, definer, weight=weight, key=ident, ident=ident) + self.io.profile("build graph") + if not references: pass @@ -808,6 +815,8 @@ def get_ranked_tags( except ZeroDivisionError: return [] + self.io.profile("pagerank") + # distribute the rank from each source node, across all of its out edges ranked_definitions = defaultdict(float) for src in G.nodes: @@ -822,6 +831,8 @@ def get_ranked_tags( ident = data["ident"] ranked_definitions[(dst, ident)] += data["rank"] + self.io.profile("distribute rank") + ranked_tags = [] ranked_definitions = sorted( ranked_definitions.items(), reverse=True, key=lambda x: (x[1], x[0]) @@ -905,9 +916,11 @@ def get_ranked_tags_map( # Check if the result is in the cache if use_cache and cache_key in self.map_cache: + self.io.tool_output("DEBUG: get_ranked_tags_map cache hit") return self.map_cache[cache_key] # If not in cache or force_refresh is True, generate the map + self.io.tool_output("DEBUG: get_ranked_tags_map cache miss, generating map") start_time = time.time() result = self.get_ranked_tags_map_uncached( chat_fnames, other_fnames, max_map_tokens, mentioned_fnames, mentioned_idents @@ -929,6 +942,7 @@ def get_ranked_tags_map_uncached( mentioned_fnames=None, mentioned_idents=None, ): + self.io.profile("get_ranked_tags_map_uncached", start=True) if not other_fnames: other_fnames = list() if not max_map_tokens: @@ -943,6 +957,7 @@ def get_ranked_tags_map_uncached( ranked_tags = self.get_ranked_tags( chat_fnames, other_fnames, mentioned_fnames, mentioned_idents, True ) + self.io.profile("get_ranked_tags") other_rel_fnames = sorted(set(self.get_rel_fname(fname) for fname in other_fnames)) special_fnames = filter_important_files(other_rel_fnames) @@ -992,6 +1007,7 @@ def get_ranked_tags_map_uncached( middle = int((lower_bound + upper_bound) // 2) + self.io.profile("binary search and to_tree") return best_tree tree_cache = dict() From 7eac657231630a253c7de20a119a734c2e018ee6 Mon Sep 17 00:00:00 2001 From: pjcreath Date: Mon, 24 Nov 2025 11:45:38 -0500 Subject: [PATCH 04/11] Add "preserve" option to line_endings setting. --- aider/io.py | 23 ++++++++++++++++++++--- tests/basic/test_io.py | 5 +++-- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/aider/io.py b/aider/io.py index 37458214dbd..4ecc2118376 100644 --- a/aider/io.py +++ b/aider/io.py @@ -417,14 +417,15 @@ def __init__( self.chat_history_file = None self.encoding = encoding - valid_line_endings = {"platform", "lf", "crlf"} + valid_line_endings = {"platform", "lf", "crlf", "preserve"} if line_endings not in valid_line_endings: raise ValueError( f"Invalid line_endings value: {line_endings}. " f"Must be one of: {', '.join(valid_line_endings)}" ) + self.line_endings = line_endings self.newline = ( - None if line_endings == "platform" else "\n" if line_endings == "lf" else "\r\n" + None if line_endings in ("platform", "preserve") else "\n" if line_endings == "lf" else "\r\n" ) self.dry_run = dry_run @@ -643,6 +644,18 @@ def read_text(self, filename, silent=False): self.tool_error("Use --encoding to set the unicode encoding.") return + def _detect_newline(self, filename): + try: + with open(filename, "rb") as f: + chunk = f.read(1024) + if b"\r\n" in chunk: + return "\r\n" + elif b"\n" in chunk: + return "\n" + except (FileNotFoundError, IsADirectoryError): + pass # File doesn't exist or is a directory, will use default + return None + def write_text(self, filename, content, max_retries=5, initial_delay=0.1): """ Writes content to a file, retrying with progressive backoff if the file is locked. @@ -655,10 +668,14 @@ def write_text(self, filename, content, max_retries=5, initial_delay=0.1): if self.dry_run: return + newline = self.newline + if self.line_endings == "preserve": + newline = self._detect_newline(filename) or self.newline + delay = initial_delay for attempt in range(max_retries): try: - with open(str(filename), "w", encoding=self.encoding, newline=self.newline) as f: + with open(str(filename), "w", encoding=self.encoding, newline=newline) as f: f.write(content) return # Successfully wrote the file except PermissionError as err: diff --git a/tests/basic/test_io.py b/tests/basic/test_io.py index 2d499c3c205..7ecde2f567b 100644 --- a/tests/basic/test_io.py +++ b/tests/basic/test_io.py @@ -16,10 +16,10 @@ class TestInputOutput(unittest.TestCase): def test_line_endings_validation(self): # Test valid line endings - for ending in ["platform", "lf", "crlf"]: + for ending in ["platform", "lf", "crlf", "preserve"]: io = InputOutput(line_endings=ending) self.assertEqual( - io.newline, None if ending == "platform" else "\n" if ending == "lf" else "\r\n" + io.newline, None if ending in ("platform", "preserve") else "\n" if ending == "lf" else "\r\n" ) # Test invalid line endings @@ -30,6 +30,7 @@ def test_line_endings_validation(self): self.assertIn("platform", str(cm.exception)) self.assertIn("crlf", str(cm.exception)) self.assertIn("lf", str(cm.exception)) + self.assertIn("preserve", str(cm.exception)) def test_no_color_environment_variable(self): with patch.dict(os.environ, {"NO_COLOR": "1"}): From 508c94c8fc9ff6296ba6e0b70878ca92c380fad1 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Mon, 24 Nov 2025 18:19:41 -0500 Subject: [PATCH 05/11] Bump Version --- aider/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/__init__.py b/aider/__init__.py index f4541f52532..5271e256d1a 100644 --- a/aider/__init__.py +++ b/aider/__init__.py @@ -1,6 +1,6 @@ from packaging import version -__version__ = "0.88.28.dev" +__version__ = "0.88.29.dev" safe_version = __version__ try: From c3bd1d691f95cfd5574d7a9a09112de54c8d6edb Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Mon, 24 Nov 2025 18:31:08 -0500 Subject: [PATCH 06/11] Bump Versions --- requirements.txt | 49 +++++++++++++--------- requirements/common-constraints.txt | 60 +++++++++++++++------------ requirements/requirements-browser.txt | 4 +- requirements/requirements-dev.txt | 8 ++-- requirements/requirements-help.txt | 14 +++---- 5 files changed, 76 insertions(+), 59 deletions(-) diff --git a/requirements.txt b/requirements.txt index e26ec6fe6f6..b85f28e7246 100644 --- a/requirements.txt +++ b/requirements.txt @@ -57,6 +57,7 @@ certifi==2025.11.12 cffi==2.0.0 # via # -c requirements/common-constraints.txt + # cryptography # sounddevice # soundfile charset-normalizer==3.4.4 @@ -72,6 +73,10 @@ configargparse==1.7.1 # via # -c requirements/common-constraints.txt # -r requirements/requirements.in +cryptography==46.0.3 + # via + # -c requirements/common-constraints.txt + # pyjwt diff-match-patch==20241021 # via # -c requirements/common-constraints.txt @@ -167,7 +172,7 @@ h11==0.16.0 # -c requirements/common-constraints.txt # httpcore # uvicorn -hf-xet==1.1.7 +hf-xet==1.2.0 # via # -c requirements/common-constraints.txt # huggingface-hub @@ -187,11 +192,11 @@ httpx==0.28.1 # mcp # mixpanel # openai -httpx-sse==0.4.0 +httpx-sse==0.4.3 # via # -c requirements/common-constraints.txt # mcp -huggingface-hub==0.34.4 +huggingface-hub==0.36.0 # via # -c requirements/common-constraints.txt # tokenizers @@ -202,7 +207,7 @@ idna==3.11 # httpx # requests # yarl -importlib-metadata==7.2.1 +importlib-metadata==8.7.0 # via # -c requirements/common-constraints.txt # -r requirements/requirements.in @@ -229,11 +234,11 @@ jsonschema==4.25.1 # -r requirements/requirements.in # litellm # mcp -jsonschema-specifications==2025.4.1 +jsonschema-specifications==2025.9.1 # via # -c requirements/common-constraints.txt # jsonschema -litellm==1.80.0 +litellm==1.80.5 # via # -c requirements/common-constraints.txt # -r requirements/requirements.in @@ -249,7 +254,7 @@ mccabe==0.7.0 # via # -c requirements/common-constraints.txt # flake8 -mcp==1.12.3 +mcp==1.22.0 # via # -c requirements/common-constraints.txt # -r requirements/requirements.in @@ -270,16 +275,16 @@ multidict==6.7.0 # -c requirements/common-constraints.txt # aiohttp # yarl -networkx==3.4.2 +networkx==3.6 # via # -c requirements/common-constraints.txt # -r requirements/requirements.in -numpy==1.26.4 +numpy==2.3.5 # via # -c requirements/common-constraints.txt # scipy # soundfile -openai==1.99.9 +openai==2.8.1 # via # -c requirements/common-constraints.txt # litellm @@ -370,7 +375,7 @@ pydantic-core==2.41.5 # via # -c requirements/common-constraints.txt # pydantic -pydantic-settings==2.9.1 +pydantic-settings==2.12.0 # via # -c requirements/common-constraints.txt # mcp @@ -386,6 +391,10 @@ pygments==2.19.2 # via # -c requirements/common-constraints.txt # rich +pyjwt[crypto]==2.10.1 + # via + # -c requirements/common-constraints.txt + # mcp pypandoc==1.16.2 # via # -c requirements/common-constraints.txt @@ -402,7 +411,7 @@ python-dateutil==2.9.0.post0 # via # -c requirements/common-constraints.txt # posthog -python-dotenv==1.1.1 +python-dotenv==1.2.1 # via # -c requirements/common-constraints.txt # litellm @@ -446,7 +455,7 @@ rsa==4.9.1 # via # -c requirements/common-constraints.txt # google-auth -scipy==1.15.3 +scipy==1.16.3 # via # -c requirements/common-constraints.txt # -r requirements/requirements.in @@ -484,15 +493,14 @@ soupsieve==2.8 # via # -c requirements/common-constraints.txt # beautifulsoup4 -sse-starlette==2.3.3 +sse-starlette==3.0.3 # via # -c requirements/common-constraints.txt # mcp -starlette==0.46.2 +starlette==0.50.0 # via # -c requirements/common-constraints.txt # mcp - # sse-starlette tiktoken==0.12.0 # via # -c requirements/common-constraints.txt @@ -519,7 +527,7 @@ tree-sitter-embedded-template==0.25.0 # via # -c requirements/common-constraints.txt # tree-sitter-language-pack -tree-sitter-language-pack==0.9.0 +tree-sitter-language-pack==0.11.0 # via # -c requirements/common-constraints.txt # grep-ast @@ -536,15 +544,18 @@ typing-extensions==4.15.0 # google-generativeai # grpcio # huggingface-hub + # mcp # openai # posthog # pydantic # pydantic-core # referencing + # starlette # typing-inspection typing-inspection==0.4.2 # via # -c requirements/common-constraints.txt + # mcp # pydantic # pydantic-settings uritemplate==4.2.0 @@ -555,11 +566,11 @@ urllib3==2.5.0 # via # -c requirements/common-constraints.txt # requests -uvicorn==0.34.2 +uvicorn==0.38.0 # via # -c requirements/common-constraints.txt # mcp -watchfiles==1.1.0 +watchfiles==1.1.1 # via # -c requirements/common-constraints.txt # -r requirements/requirements.in diff --git a/requirements/common-constraints.txt b/requirements/common-constraints.txt index ee65dbadfe8..bca94710130 100644 --- a/requirements/common-constraints.txt +++ b/requirements/common-constraints.txt @@ -53,6 +53,7 @@ certifi==2025.11.12 # requests cffi==2.0.0 # via + # cryptography # sounddevice # soundfile cfgv==3.5.0 @@ -77,6 +78,8 @@ configargparse==1.7.1 # via -r requirements/requirements.in contourpy==1.3.3 # via matplotlib +cryptography==46.0.3 + # via pyjwt cycler==0.12.1 # via matplotlib dataclasses-json==0.6.7 @@ -181,7 +184,7 @@ h11==0.16.0 # via # httpcore # uvicorn -hf-xet==1.1.7 +hf-xet==1.2.0 # via huggingface-hub httpcore==1.0.9 # via httpx @@ -196,9 +199,9 @@ httpx==0.28.1 # mcp # mixpanel # openai -httpx-sse==0.4.0 +httpx-sse==0.4.3 # via mcp -huggingface-hub[inference]==0.34.4 +huggingface-hub[inference]==0.36.0 # via # llama-index-embeddings-huggingface # sentence-transformers @@ -214,7 +217,7 @@ idna==3.11 # yarl imgcat==0.6.0 # via -r requirements/requirements-dev.in -importlib-metadata==7.2.1 +importlib-metadata==8.7.0 # via # -r requirements/requirements.in # litellm @@ -243,11 +246,11 @@ jsonschema==4.25.1 # altair # litellm # mcp -jsonschema-specifications==2025.4.1 +jsonschema-specifications==2025.9.1 # via jsonschema kiwisolver==1.4.9 # via matplotlib -litellm==1.80.0 +litellm==1.80.5 # via -r requirements/requirements.in llama-index-core==0.14.8 # via llama-index-embeddings-huggingface @@ -255,7 +258,7 @@ llama-index-embeddings-huggingface==0.6.1 # via -r requirements/requirements-help.in llama-index-instrumentation==0.4.2 # via llama-index-workflows -llama-index-workflows==2.11.4 +llama-index-workflows==2.11.5 # via llama-index-core lox==1.0.0 # via -r requirements/requirements-dev.in @@ -269,7 +272,7 @@ matplotlib==3.10.7 # via -r requirements/requirements-dev.in mccabe==0.7.0 # via flake8 -mcp==1.12.3 +mcp==1.22.0 # via -r requirements/requirements.in mdurl==0.1.2 # via markdown-it-py @@ -289,7 +292,7 @@ narwhals==2.12.0 # via altair nest-asyncio==1.6.0 # via llama-index-core -networkx==3.4.2 +networkx==3.6 # via # -r requirements/requirements.in # llama-index-core @@ -298,7 +301,7 @@ nltk==3.9.2 # via llama-index-core nodeenv==1.9.1 # via pre-commit -numpy==1.26.4 +numpy==2.3.5 # via # -r requirements/requirements-help.in # contourpy @@ -350,7 +353,7 @@ nvidia-nvshmem-cu12==3.3.20 # via torch nvidia-nvtx-cu12==12.8.90 # via torch -openai==1.99.9 +openai==2.8.1 # via litellm oslex==0.1.3 # via -r requirements/requirements.in @@ -398,7 +401,7 @@ pluggy==1.6.0 # via pytest posthog==7.0.1 # via -r requirements/requirements.in -pre-commit==4.4.0 +pre-commit==4.5.0 # via -r requirements/requirements-dev.in prompt-toolkit==3.0.52 # via -r requirements/requirements.in @@ -449,7 +452,7 @@ pydantic==2.12.4 # pydantic-settings pydantic-core==2.41.5 # via pydantic -pydantic-settings==2.9.1 +pydantic-settings==2.12.0 # via mcp pydeck==0.9.1 # via streamlit @@ -463,6 +466,8 @@ pygments==2.19.2 # via # pytest # rich +pyjwt[crypto]==2.10.1 + # via mcp pypandoc==1.16.2 # via -r requirements/requirements.in pyparsing==3.2.5 @@ -490,7 +495,7 @@ python-dateutil==2.9.0.post0 # matplotlib # pandas # posthog -python-dotenv==1.1.1 +python-dotenv==1.2.1 # via # litellm # pydantic-settings @@ -539,7 +544,7 @@ safetensors==0.7.0 # via transformers scikit-learn==1.7.2 # via sentence-transformers -scipy==1.15.3 +scipy==1.16.3 # via # -r requirements/requirements.in # scikit-learn @@ -577,12 +582,10 @@ soupsieve==2.8 # via beautifulsoup4 sqlalchemy[asyncio]==2.0.44 # via llama-index-core -sse-starlette==2.3.3 +sse-starlette==3.0.3 + # via mcp +starlette==0.50.0 # via mcp -starlette==0.46.2 - # via - # mcp - # sse-starlette streamlit==1.51.0 # via -r requirements/requirements-browser.in sympy==1.14.0 @@ -616,9 +619,9 @@ tqdm==4.67.1 # openai # sentence-transformers # transformers -transformers==4.57.1 +transformers==4.57.2 # via sentence-transformers -tree-sitter==0.25.1 +tree-sitter==0.25.2 # via # -r requirements/requirements.in # tree-sitter-language-pack @@ -626,13 +629,13 @@ tree-sitter-c-sharp==0.23.1 # via tree-sitter-language-pack tree-sitter-embedded-template==0.25.0 # via tree-sitter-language-pack -tree-sitter-language-pack==0.9.0 +tree-sitter-language-pack==0.11.0 # via grep-ast tree-sitter-yaml==0.7.2 # via tree-sitter-language-pack triton==3.5.1 # via torch -typer==0.16.0 +typer==0.20.0 # via -r requirements/requirements-dev.in typing-extensions==4.15.0 # via @@ -646,6 +649,7 @@ typing-extensions==4.15.0 # huggingface-hub # llama-index-core # llama-index-workflows + # mcp # openai # posthog # pydantic @@ -655,6 +659,7 @@ typing-extensions==4.15.0 # referencing # sentence-transformers # sqlalchemy + # starlette # streamlit # torch # typer @@ -666,6 +671,7 @@ typing-inspect==0.9.0 # llama-index-core typing-inspection==0.4.2 # via + # mcp # pydantic # pydantic-settings tzdata==2025.2 @@ -676,13 +682,13 @@ urllib3==2.5.0 # via requests uv==0.9.11 # via -r requirements/requirements-dev.in -uvicorn==0.34.2 +uvicorn==0.38.0 # via mcp -virtualenv==20.34.0 +virtualenv==20.35.4 # via pre-commit watchdog==6.0.0 # via streamlit -watchfiles==1.1.0 +watchfiles==1.1.1 # via -r requirements/requirements.in wcwidth==0.2.14 # via prompt-toolkit diff --git a/requirements/requirements-browser.txt b/requirements/requirements-browser.txt index 84b7c9b7038..c3eba12883d 100644 --- a/requirements/requirements-browser.txt +++ b/requirements/requirements-browser.txt @@ -50,7 +50,7 @@ jsonschema==4.25.1 # via # -c requirements/common-constraints.txt # altair -jsonschema-specifications==2025.4.1 +jsonschema-specifications==2025.9.1 # via # -c requirements/common-constraints.txt # jsonschema @@ -62,7 +62,7 @@ narwhals==2.12.0 # via # -c requirements/common-constraints.txt # altair -numpy==1.26.4 +numpy==2.3.5 # via # -c requirements/common-constraints.txt # pandas diff --git a/requirements/requirements-dev.txt b/requirements/requirements-dev.txt index dbb327e608e..cedd67fab61 100644 --- a/requirements/requirements-dev.txt +++ b/requirements/requirements-dev.txt @@ -134,7 +134,7 @@ nodeenv==1.9.1 # via # -c requirements/common-constraints.txt # pre-commit -numpy==1.26.4 +numpy==2.3.5 # via # -c requirements/common-constraints.txt # contourpy @@ -171,7 +171,7 @@ pluggy==1.6.0 # via # -c requirements/common-constraints.txt # pytest -pre-commit==4.4.0 +pre-commit==4.5.0 # via # -c requirements/common-constraints.txt # -r requirements/requirements-dev.in @@ -266,7 +266,7 @@ six==1.17.0 # via # -c requirements/common-constraints.txt # python-dateutil -typer==0.16.0 +typer==0.20.0 # via # -c requirements/common-constraints.txt # -r requirements/requirements-dev.in @@ -288,7 +288,7 @@ uv==0.9.11 # via # -c requirements/common-constraints.txt # -r requirements/requirements-dev.in -virtualenv==20.34.0 +virtualenv==20.35.4 # via # -c requirements/common-constraints.txt # pre-commit diff --git a/requirements/requirements-help.txt b/requirements/requirements-help.txt index a4fca43bb60..1b1b4ce392d 100644 --- a/requirements/requirements-help.txt +++ b/requirements/requirements-help.txt @@ -98,7 +98,7 @@ h11==0.16.0 # via # -c requirements/common-constraints.txt # httpcore -hf-xet==1.1.7 +hf-xet==1.2.0 # via # -c requirements/common-constraints.txt # huggingface-hub @@ -110,7 +110,7 @@ httpx==0.28.1 # via # -c requirements/common-constraints.txt # llama-index-core -huggingface-hub[inference]==0.34.4 +huggingface-hub[inference]==0.36.0 # via # -c requirements/common-constraints.txt # llama-index-embeddings-huggingface @@ -146,7 +146,7 @@ llama-index-instrumentation==0.4.2 # via # -c requirements/common-constraints.txt # llama-index-workflows -llama-index-workflows==2.11.4 +llama-index-workflows==2.11.5 # via # -c requirements/common-constraints.txt # llama-index-core @@ -175,7 +175,7 @@ nest-asyncio==1.6.0 # via # -c requirements/common-constraints.txt # llama-index-core -networkx==3.4.2 +networkx==3.6 # via # -c requirements/common-constraints.txt # llama-index-core @@ -184,7 +184,7 @@ nltk==3.9.2 # via # -c requirements/common-constraints.txt # llama-index-core -numpy==1.26.4 +numpy==2.3.5 # via # -c requirements/common-constraints.txt # -r requirements/requirements-help.in @@ -317,7 +317,7 @@ scikit-learn==1.7.2 # via # -c requirements/common-constraints.txt # sentence-transformers -scipy==1.15.3 +scipy==1.16.3 # via # -c requirements/common-constraints.txt # scikit-learn @@ -371,7 +371,7 @@ tqdm==4.67.1 # nltk # sentence-transformers # transformers -transformers==4.57.1 +transformers==4.57.2 # via # -c requirements/common-constraints.txt # sentence-transformers From 32df721f124434d992dbe51fed265f9f0ed80e85 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Mon, 24 Nov 2025 18:38:23 -0500 Subject: [PATCH 07/11] #187: Add SwitchCoder to propagate /add command to terminal --- aider/commands.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/aider/commands.py b/aider/commands.py index 4085b4510e9..924e949ab30 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -998,6 +998,22 @@ async def cmd_add(self, args): if hasattr(self.coder, "_calculate_context_block_tokens"): self.coder._calculate_context_block_tokens() + if self.coder.repo_map: + map_tokens = self.coder.repo_map.max_map_tokens + map_mul_no_files = self.coder.repo_map.map_mul_no_files + else: + map_tokens = 0 + map_mul_no_files = 1 + + raise SwitchCoder( + edit_format=self.coder.edit_format, + summarize_from_coder=False, + from_coder=self.coder, + map_tokens=map_tokens, + map_mul_no_files=map_mul_no_files, + show_announcements=False, + ) + def completions_drop(self): files = self.coder.get_inchat_relative_files() read_only_files = [ From bc2d2990781d1fb65851772ffb81005a867db903 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Mon, 24 Nov 2025 22:46:40 -0500 Subject: [PATCH 08/11] Multiple Changes: - #189: Allow plain json responses to be interpreted as tool calls - Add a "Thinking" tool so the model has a space to place freeform notes and thoughts --- aider/coders/agent_coder.py | 86 +++++++++++++++++++++++++++++++++-- aider/coders/agent_prompts.py | 4 +- aider/tools/__init__.py | 2 + aider/tools/thinking.py | 52 +++++++++++++++++++++ 4 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 aider/tools/thinking.py diff --git a/aider/coders/agent_coder.py b/aider/coders/agent_coder.py index ea8e2f7e72e..cf61d2445c2 100644 --- a/aider/coders/agent_coder.py +++ b/aider/coders/agent_coder.py @@ -52,6 +52,7 @@ replace_lines, replace_text, show_numbered_context, + thinking, undo_change, update_todo_list, view, @@ -182,6 +183,7 @@ def _build_tool_registry(self): replace_lines, replace_text, show_numbered_context, + thinking, undo_change, update_todo_list, view, @@ -1165,6 +1167,73 @@ async def _execute_tool_with_registry(self, norm_tool_name, params): return f"Error: Unknown tool name '{norm_tool_name}'" + def _convert_concatenated_json_to_tool_calls(self, content): + """ + Check if content contains concatenated JSON objects and convert them to tool call format. + + Args: + content (str): Content to check for concatenated JSON + + Returns: + str: Content with concatenated JSON converted to tool call format, or original content if no JSON found + """ + try: + # Use split_concatenated_json to detect and split concatenated JSON objects + json_chunks = utils.split_concatenated_json(content) + + # If we found multiple JSON objects, convert them to tool call format + if len(json_chunks) >= 1: + tool_calls = [] + for chunk in json_chunks: + try: + json_obj = json.loads(chunk) + # Check if this looks like a tool call JSON object + if ( + isinstance(json_obj, dict) + and "name" in json_obj + and "arguments" in json_obj + ): + tool_name = json_obj["name"] + arguments = json_obj["arguments"] + + # Convert arguments dictionary to keyword arguments string + kw_args = [] + for key, value in arguments.items(): + if isinstance(value, str): + # Escape quotes and wrap in quotes + escaped_value = value.replace('"', '\\"') + kw_args.append(f'{key}="{escaped_value}"') + elif isinstance(value, bool): + kw_args.append(f"{key}={str(value).lower()}") + elif value is None: + kw_args.append(f"{key}=None") + else: + # For numbers and other types, use repr for safe representation + kw_args.append(f"{key}={repr(value)}") + + # Join keyword arguments + kw_args_str = ", ".join(kw_args) + + # Convert to [tool_call(ToolName, key1="value1", key2="value2")] format + tool_call = f"[tool_call({tool_name}, {kw_args_str})]" + tool_calls.append(tool_call) + else: + # Not a tool call JSON, keep as is + tool_calls.append(chunk) + except json.JSONDecodeError: + # Invalid JSON, keep as is + tool_calls.append(chunk) + + # If we found any tool calls, replace the content + if any(call.startswith("[tool_") for call in tool_calls): + return "".join(tool_calls) + + except Exception as e: + # If anything goes wrong, return original content + self.io.tool_warning(f"Error converting concatenated JSON to tool calls: {str(e)}") + + return content + async def _process_tool_commands(self, content): """ Process tool commands in the `[tool_call(name, param=value)]` format within the content. @@ -1184,20 +1253,31 @@ async def _process_tool_commands(self, content): max_calls = self.max_tool_calls tool_names = [] + # Check if content contains concatenated JSON and convert to tool call format + content = self._convert_concatenated_json_to_tool_calls(content) + # Check if there's a '---' separator and only process tool calls after the LAST one separator_marker = "---" content_parts = content.split(separator_marker) # If there's no separator, treat the entire content as before the separator + # But only return immediately if no tool calls were found in the JSON conversion if len(content_parts) == 1: - # Return the original content with no tool calls processed, and the content itself as before_separator - return content, result_messages, False, content, tool_names + # Check if we have any tool calls in the content after JSON conversion + # If we have tool calls, we should process them even without a separator + tool_call_pattern = r"\[tool_call\([^\]]+\)\]" + if re.search(tool_call_pattern, content): + # We have tool calls, so continue processing + content_before_separator = "" + content_after_separator = content + else: + # No tool calls found, return the original content + return content, result_messages, False, content, tool_names # Take everything before the last separator (including intermediate separators) content_before_separator = separator_marker.join(content_parts[:-1]) # Take only what comes after the last separator content_after_separator = content_parts[-1] - # Find tool calls using a more robust method, but only in the content after separator processed_content = content_before_separator + separator_marker last_index = 0 diff --git a/aider/coders/agent_prompts.py b/aider/coders/agent_prompts.py index c29e7569b4b..237dcb0aa3a 100644 --- a/aider/coders/agent_prompts.py +++ b/aider/coders/agent_prompts.py @@ -25,8 +25,8 @@ class AgentPrompts(CoderPrompts): ## Core Workflow 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. +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. Concisely describe your search strategy with the `Thinking` tool. +3. **Think**: Given the contents of your exploration, concisely reason through the edits with the `Thinking` tool 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. 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 diff --git a/aider/tools/__init__.py b/aider/tools/__init__.py index 60b3b06c5af..03a5f8c2b45 100644 --- a/aider/tools/__init__.py +++ b/aider/tools/__init__.py @@ -29,6 +29,7 @@ replace_lines, replace_text, show_numbered_context, + thinking, undo_change, update_todo_list, view, @@ -64,6 +65,7 @@ replace_lines, replace_text, show_numbered_context, + thinking, undo_change, update_todo_list, view, diff --git a/aider/tools/thinking.py b/aider/tools/thinking.py new file mode 100644 index 00000000000..8bb39581b22 --- /dev/null +++ b/aider/tools/thinking.py @@ -0,0 +1,52 @@ +import json + +from aider.tools.utils.base_tool import BaseTool +from aider.tools.utils.output import color_markers, tool_footer, tool_header + + +class Tool(BaseTool): + NORM_NAME = "thinking" + SCHEMA = { + "type": "function", + "function": { + "name": "Thinking", + "description": ( + "Use this tool to store useful facts for later " + "keep a scratch pad of your current efforts " + "and clarify your thoughts and intentions for your next steps." + ), + "parameters": { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "Textual information to record in the context", + }, + }, + "required": ["content"], + }, + }, + } + + @classmethod + def execute(cls, coder, content): + """ + A place to allow the model to record freeform text as it + iterates over tools to ideally help it guide itself to a proper solution + """ + coder.io.tool_output("🧠 Thoughts recorded in context") + return content + + @classmethod + def format_output(cls, coder, mcp_server, tool_response): + color_start, color_end = color_markers(coder) + params = json.loads(tool_response.function.arguments) + + tool_header(coder=coder, mcp_server=mcp_server, tool_response=tool_response) + + coder.io.tool_output("") + coder.io.tool_output(f"{color_start}Thoughts:{color_end}") + coder.io.tool_output(params["content"]) + coder.io.tool_output("") + + tool_footer(coder=coder, tool_response=tool_response) From e7299d3b7a65a387905f187e3f0700ae0f76ddbc Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Mon, 24 Nov 2025 22:48:54 -0500 Subject: [PATCH 09/11] Update formatting with black --- aider/io.py | 8 +++++--- tests/basic/test_io.py | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/aider/io.py b/aider/io.py index 4ecc2118376..14c4d6308b0 100644 --- a/aider/io.py +++ b/aider/io.py @@ -425,7 +425,9 @@ def __init__( ) self.line_endings = line_endings self.newline = ( - None if line_endings in ("platform", "preserve") else "\n" if line_endings == "lf" else "\r\n" + None + if line_endings in ("platform", "preserve") + else "\n" if line_endings == "lf" else "\r\n" ) self.dry_run = dry_run @@ -744,10 +746,10 @@ async def get_input( show = "" if rel_fnames: rel_read_only_fnames = [ - get_rel_fname(fname, root) for fname in (abs_read_only_fnames or []) + get_rel_fname(fname, root) for fname in abs_read_only_fnames or [] ] rel_read_only_stubs_fnames = [ - get_rel_fname(fname, root) for fname in (abs_read_only_stubs_fnames or []) + get_rel_fname(fname, root) for fname in abs_read_only_stubs_fnames or [] ] show = self.format_files_for_input( rel_fnames, rel_read_only_fnames, rel_read_only_stubs_fnames diff --git a/tests/basic/test_io.py b/tests/basic/test_io.py index 7ecde2f567b..c4d9b8dda01 100644 --- a/tests/basic/test_io.py +++ b/tests/basic/test_io.py @@ -19,7 +19,8 @@ def test_line_endings_validation(self): for ending in ["platform", "lf", "crlf", "preserve"]: io = InputOutput(line_endings=ending) self.assertEqual( - io.newline, None if ending in ("platform", "preserve") else "\n" if ending == "lf" else "\r\n" + io.newline, + None if ending in ("platform", "preserve") else "\n" if ending == "lf" else "\r\n", ) # Test invalid line endings From fba60a540eb73285f1570e621145bbe7adfaf090 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Mon, 24 Nov 2025 23:07:40 -0500 Subject: [PATCH 10/11] Format Merge Request, Update profile messages --- aider/io.py | 4 +++- aider/repomap.py | 20 ++++++++------------ 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/aider/io.py b/aider/io.py index ea567ab1a8d..0323d9b3b11 100644 --- a/aider/io.py +++ b/aider/io.py @@ -1389,7 +1389,9 @@ def profile(self, *messages, start=False): else: total_elapsed = now - self.profile_start_time last_elapsed = now - self.profile_last_time - output_message = f"PROFILE: [+{last_elapsed:6.2f}s] {message_str} (total {total_elapsed:.2f}s)" + output_message = ( + f"PROFILE: [+{last_elapsed:6.2f}s] {message_str} (total {total_elapsed:.2f}s)" + ) self.stream_print(output_message) self.profile_last_time = now diff --git a/aider/repomap.py b/aider/repomap.py index 9e7c579ba23..03e5a5b5e60 100644 --- a/aider/repomap.py +++ b/aider/repomap.py @@ -198,9 +198,7 @@ def __init__( self._mentioned_ident_similarity = 0.8 if self.verbose: - self.io.tool_output( - f"RepoMap loaded entries from tags cache: {len(self.TAGS_CACHE)}" - ) + self.io.tool_output(f"RepoMap loaded entries from tags cache: {len(self.TAGS_CACHE)}") self.io.tool_output( f"RepoMap initialized with map_mul_no_files: {self.map_mul_no_files}" ) @@ -699,7 +697,7 @@ def get_ranked_tags( if tag.specific_kind == "import": file_imports[rel_fname].add(tag.name) - self.io.profile("process files") + self.io.profile("Process Files") if self.use_enhanced_map and len(file_imports) > 0: import_ast_mode = True @@ -796,7 +794,7 @@ def get_ranked_tags( weight = num_refs * use_mul * 2 ** (-1 * path_distance) G.add_edge(referencer, definer, weight=weight, key=ident, ident=ident) - self.io.profile("build graph") + self.io.profile("Build Graph") if not references: pass @@ -815,7 +813,7 @@ def get_ranked_tags( except ZeroDivisionError: return [] - self.io.profile("pagerank") + self.io.profile("PageRank") # distribute the rank from each source node, across all of its out edges ranked_definitions = defaultdict(float) @@ -831,7 +829,7 @@ def get_ranked_tags( ident = data["ident"] ranked_definitions[(dst, ident)] += data["rank"] - self.io.profile("distribute rank") + self.io.profile("Distribute Rank") ranked_tags = [] ranked_definitions = sorted( @@ -916,11 +914,9 @@ def get_ranked_tags_map( # Check if the result is in the cache if use_cache and cache_key in self.map_cache: - self.io.tool_output("DEBUG: get_ranked_tags_map cache hit") return self.map_cache[cache_key] # If not in cache or force_refresh is True, generate the map - self.io.tool_output("DEBUG: get_ranked_tags_map cache miss, generating map") start_time = time.time() result = self.get_ranked_tags_map_uncached( chat_fnames, other_fnames, max_map_tokens, mentioned_fnames, mentioned_idents @@ -942,7 +938,7 @@ def get_ranked_tags_map_uncached( mentioned_fnames=None, mentioned_idents=None, ): - self.io.profile("get_ranked_tags_map_uncached", start=True) + self.io.profile("Start Rank Tags Map Uncached", start=True) if not other_fnames: other_fnames = list() if not max_map_tokens: @@ -957,7 +953,7 @@ def get_ranked_tags_map_uncached( ranked_tags = self.get_ranked_tags( chat_fnames, other_fnames, mentioned_fnames, mentioned_idents, True ) - self.io.profile("get_ranked_tags") + self.io.profile("Finish Getting Ranked Tags") other_rel_fnames = sorted(set(self.get_rel_fname(fname) for fname in other_fnames)) special_fnames = filter_important_files(other_rel_fnames) @@ -1007,7 +1003,7 @@ def get_ranked_tags_map_uncached( middle = int((lower_bound + upper_bound) // 2) - self.io.profile("binary search and to_tree") + self.io.profile("Calculate Best Tree") return best_tree tree_cache = dict() From bf9b8b54b566aa7e9c00fdb608c3472c3269799f Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Mon, 24 Nov 2025 23:08:41 -0500 Subject: [PATCH 11/11] I'm slightly anal retentive on spacing between different types of lines --- aider/repomap.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aider/repomap.py b/aider/repomap.py index 03e5a5b5e60..db16671009d 100644 --- a/aider/repomap.py +++ b/aider/repomap.py @@ -939,6 +939,7 @@ def get_ranked_tags_map_uncached( mentioned_idents=None, ): self.io.profile("Start Rank Tags Map Uncached", start=True) + if not other_fnames: other_fnames = list() if not max_map_tokens: @@ -953,6 +954,7 @@ def get_ranked_tags_map_uncached( ranked_tags = self.get_ranked_tags( chat_fnames, other_fnames, mentioned_fnames, mentioned_idents, True ) + self.io.profile("Finish Getting Ranked Tags") other_rel_fnames = sorted(set(self.get_rel_fname(fname) for fname in other_fnames)) @@ -1004,6 +1006,7 @@ def get_ranked_tags_map_uncached( middle = int((lower_bound + upper_bound) // 2) self.io.profile("Calculate Best Tree") + return best_tree tree_cache = dict()