Skip to content
Open
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
7 changes: 7 additions & 0 deletions aider/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,13 @@ def get_parser(default_config_files, git_root):
metavar="ALIAS:MODEL",
help="Add a model alias (can be used multiple times)",
)
group.add_argument(
"--handlers",
action="append",
metavar="HANDLERS",
help="Specify a handler and optional handler config to load (can be used multiple times)",
default=None,
)
group.add_argument(
"--reasoning-effort",
type=str,
Expand Down
23 changes: 23 additions & 0 deletions aider/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ class Coder:
chat_language = None
commit_language = None
file_watcher = None
handler_manager = None

@classmethod
def create(
Expand Down Expand Up @@ -278,6 +279,10 @@ def get_announcements(self):
else:
lines.append("Repo-map: disabled")

if self.handler_manager and self.handler_manager.handlers:
handler_names = [h.name for h in self.handler_manager.handlers]
lines.append(f"Handlers: {' '.join(handler_names)}")

# Files
for fname in self.get_inchat_relative_files():
lines.append(f"Added {fname} to the chat.")
Expand Down Expand Up @@ -338,6 +343,7 @@ def __init__(
file_watcher=None,
auto_copy_context=False,
auto_accept_architect=True,
handlers=None,
):
# 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 @@ -541,6 +547,13 @@ def __init__(
self.io.tool_output("JSON Schema:")
self.io.tool_output(json.dumps(self.functions, indent=4))


if handlers:
from aider.extensions.handler_manager import HandlerManager
self.handler_manager = HandlerManager(self, handlers)
else:
self.handler_manager = None

def setup_lint_cmds(self, lint_cmds):
if not lint_cmds:
return
Expand Down Expand Up @@ -1428,6 +1441,12 @@ def send_message(self, inp):

chunks = self.format_messages()
messages = chunks.all_messages()

if self.handler_manager:
self.handler_manager.run(messages, "pre")
chunks = self.format_messages()
messages = chunks.all_messages()

if not self.check_tokens(messages):
return
self.warm_cache(chunks)
Expand Down Expand Up @@ -1596,6 +1615,10 @@ def send_message(self, inp):
if self.reflected_message:
return

if self.handler_manager:
messages = self.format_messages().all_messages()
self.handler_manager.run(messages, "post")

if edited and self.auto_lint:
lint_errors = self.lint_edited(edited)
self.auto_commit(edited, context="Ran the linter")
Expand Down
Empty file added aider/extensions/__init__.py
Empty file.
58 changes: 58 additions & 0 deletions aider/extensions/handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from abc import ABC, abstractmethod


class Handler(ABC):
"""
Base class for extension handlers.
"""

@property
@abstractmethod
def entrypoints(self) -> list[str]:
"""
The entrypoints at which this handler should be run.
e.g., ["pre", "post"]
"""
pass

@abstractmethod
def handle(self, messages) -> bool:
"""
Handle the given messages.
Return True if context was modified, False otherwise.
"""
pass



class MutableContextHandler(Handler):
"""
A handler that can modify the chat context.
"""

@abstractmethod
def handle(self, messages) -> bool:
"""
Handle the messages and return True if context was modified.
"""
pass


class ImmutableContextHandler(Handler):
"""
A handler that can inspect the context but not modify it.
"""

def handle(self, messages) -> bool:
"""
Handle the messages and return False, as context is not modified.
"""
self._handle(messages)
return False

@abstractmethod
def _handle(self, messages):
"""
Process the messages.
"""
pass
115 changes: 115 additions & 0 deletions aider/extensions/handler_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/usr/bin/env python

import importlib
import inspect
import ast

from .handler import (
Handler,
ImmutableContextHandler,
MutableContextHandler,
)


class HandlerManager:
"""
The HandlerManager is responsible for loading and running handlers.
"""

def __init__(self, main_coder, handlers=None):
"""
Initialize the HandlerManager.

:param main_coder: The main coder instance.
:param handlers: An optional list of handlers to use, from user config.
If None, no handlers will be used.
"""
self.main_coder = main_coder
self.handlers = []

if handlers:
self._load_handlers(handlers)

def _load_handlers(self, handlers_config):
"""
Load handlers based on the provided configuration.
"""
for handler_config in handlers_config:
if isinstance(handler_config, str):
try:
handler_config = ast.literal_eval(handler_config)
except (ValueError, SyntaxError):
pass # Keep it as a string, will be turned into a dict below

if isinstance(handler_config, str):
handler_config = dict(name=handler_config)

if not isinstance(handler_config, dict):
self.main_coder.io.tool_warning(
f"Invalid handler configuration: {handler_config}"
)
continue

handler_name = handler_config.get("name")
config = handler_config.get("config", {})

if not handler_name:
self.main_coder.io.tool_warning(
f"Handler configuration missing name: {handler_config}"
)
continue

self._load_handler(handler_name, config)

def _load_handler(self, handler_name, config):
"""
Dynamically load a single handler.
"""
try:
# Construct module name from handler name, e.g., 'file-adder' -> 'file_adder_handler'
module_name = handler_name.replace("-", "_") + "_handler"
module_path = f"aider.extensions.handlers.{module_name}"
module = importlib.import_module(module_path)

handler_class = None
for name, obj in inspect.getmembers(module, inspect.isclass):
if (
issubclass(obj, Handler)
and obj is not Handler
and obj is not MutableContextHandler
and obj is not ImmutableContextHandler
):
handler_class = obj
break

if handler_class:
handler_instance = handler_class(self.main_coder, **config)
handler_instance.name = handler_name
self.handlers.append(handler_instance)
else:
self.main_coder.io.tool_warning(
f"No handler class found in module for: {handler_name}"
)
except ImportError as e:
self.main_coder.io.tool_warning(f"Could not import handler: {handler_name}\n{e}")
except Exception as e:
self.main_coder.io.tool_warning(f"Failed to instantiate handler {handler_name}: {e}")

def run(self, messages, entrypoint):
"""
Execute the handler logic by running handlers for a specific entrypoint.

This method iterates through its handlers, allowing each to process and
potentially modify the chat context. If a handler modifies the
context, the message history is updated for subsequent handlers.

:param messages: The current list of messages in the chat.
:param entrypoint: The entrypoint to run handlers for (e.g., "pre").
"""
current_messages = messages
for handler in self.handlers:
if entrypoint in handler.entrypoints:
modified = handler.handle(current_messages)
if modified:
chunks = self.main_coder.format_messages()
current_messages = chunks.all_messages()
Empty file.
42 changes: 42 additions & 0 deletions aider/extensions/handlers/autotest_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from ..handler import MutableContextHandler


class AutoTestHandler(MutableContextHandler):
"""
A handler that runs tests automatically after code changes.
"""

handler_name = "autotest"
entrypoints = ["post"]

def __init__(self, main_coder, **kwargs):
"""
Initialize the AutoTestHandler.

:param main_coder: The main coder instance.
"""
self.main_coder = main_coder
self.test_cmd = kwargs.get("test_cmd")

def handle(self, messages) -> bool:
"""
Runs tests automatically after code changes.

:param messages: The current list of messages in the chat.
:return: True if a reflection message was set, False otherwise.
"""
if not self.test_cmd:
return False

if not self.main_coder.aider_edited_files:
return False

test_errors = self.main_coder.commands.cmd_test(self.test_cmd)
self.main_coder.test_outcome = not test_errors
if test_errors:
ok = self.main_coder.io.confirm_ask("Attempt to fix test errors?")
if ok:
self.main_coder.reflected_message = test_errors
return True

return False
Loading