Skip to content

Commit d08726f

Browse files
committed
Add agent mode: autonomous file editing with automatic test running
This commit introduces a new /agent mode that works like Claude Code, providing autonomous file editing and test running capabilities. Features: - Automatic file identification and editing without manual approval - Automatic test execution after each edit cycle - Iterative fixing of test failures - Configurable maximum iteration limit (default: 3) - Uses SEARCH/REPLACE block format for edits Implementation: - Created AgentCoder class extending EditBlockCoder - Created AgentPrompts with agent-specific system prompts - Added /agent slash command for entering agent mode - Added agent mode to chat mode list - Comprehensive test suite in test_agent.py Usage: - /agent <prompt> - Execute a request in agent mode - /chat-mode agent - Switch to agent mode - Configure with --max-agent-iterations and --auto-test flags The agent mode enables workflows where the AI can autonomously: 1. Analyze the request 2. Make necessary file edits 3. Run tests automatically 4. Analyze failures and fix issues 5. Iterate until tests pass or max iterations reached
1 parent c74f5ef commit d08726f

File tree

6 files changed

+696
-0
lines changed

6 files changed

+696
-0
lines changed

aider/coders/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from .agent_coder import AgentCoder
12
from .architect_coder import ArchitectCoder
23
from .ask_coder import AskCoder
34
from .base_coder import Coder
@@ -26,6 +27,7 @@
2627
UnifiedDiffCoder,
2728
UnifiedDiffSimpleCoder,
2829
# SingleWholeFileFunctionCoder,
30+
AgentCoder,
2931
ArchitectCoder,
3032
EditorEditBlockCoder,
3133
EditorWholeFileCoder,

aider/coders/agent_coder.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
from .agent_prompts import AgentPrompts
2+
from .editblock_coder import EditBlockCoder
3+
4+
5+
class AgentCoder(EditBlockCoder):
6+
"""
7+
An autonomous agent coder that automatically edits files and runs tests.
8+
9+
Similar to Claude Code, this mode:
10+
- Automatically identifies and edits files without manual approval
11+
- Runs tests after each edit cycle
12+
- Iterates on test failures automatically
13+
- Works autonomously to complete requests
14+
"""
15+
16+
edit_format = "agent"
17+
gpt_prompts = AgentPrompts()
18+
19+
# Agent-specific configuration
20+
auto_test = True # Automatically run tests after edits
21+
max_test_iterations = 3 # Maximum number of test-fix iterations
22+
current_iteration = 0 # Track current iteration
23+
24+
def __init__(self, *args, **kwargs):
25+
# Extract agent-specific kwargs
26+
self.max_test_iterations = kwargs.pop("max_agent_iterations", 3)
27+
self.auto_test = kwargs.pop("auto_test", True)
28+
29+
super().__init__(*args, **kwargs)
30+
self.current_iteration = 0
31+
32+
def reply_completed(self):
33+
"""
34+
Called when the LLM completes a response.
35+
36+
In agent mode, this:
37+
1. Applies edits automatically (no confirmation)
38+
2. Runs tests if configured
39+
3. If tests fail, adds output to chat and continues iterating
40+
4. Respects max iteration limit
41+
"""
42+
content = self.partial_response_content
43+
44+
if not content or not content.strip():
45+
return True
46+
47+
# Check if we've exceeded max iterations
48+
if self.current_iteration >= self.max_test_iterations:
49+
self.io.tool_output(
50+
f"Reached maximum iteration limit ({self.max_test_iterations}). "
51+
"Stopping autonomous mode."
52+
)
53+
return True
54+
55+
# Increment iteration counter
56+
self.current_iteration += 1
57+
58+
# The edits have already been applied by apply_updates() in the base flow
59+
# Now we need to run tests if configured
60+
if self.auto_test and self.test_cmd:
61+
self.io.tool_output(
62+
f"\n[Agent Mode: Running tests (iteration {self.current_iteration}/"
63+
f"{self.max_test_iterations})...]"
64+
)
65+
66+
test_errors = self._run_tests()
67+
68+
if test_errors:
69+
# Tests failed - add error to chat and continue iteration
70+
self.io.tool_error("Tests failed. Analyzing failures and attempting fixes...")
71+
72+
# Add test failure to chat so LLM can fix it
73+
self.cur_messages += [
74+
dict(
75+
role="user",
76+
content=f"The tests failed with the following output:\n\n{test_errors}"
77+
)
78+
]
79+
80+
# Return False to continue the conversation and let LLM fix the issue
81+
return False
82+
else:
83+
# Tests passed!
84+
self.io.tool_output("All tests passed!")
85+
return True
86+
87+
# No tests configured, just return
88+
return True
89+
90+
def _run_tests(self):
91+
"""
92+
Run tests and return error output if they fail.
93+
94+
Returns:
95+
str: Error output if tests failed, None if tests passed
96+
"""
97+
if not self.test_cmd:
98+
return None
99+
100+
# Import here to avoid circular dependency
101+
from aider.run_cmd import run_cmd
102+
103+
# Run the test command
104+
test_cmd = self.test_cmd
105+
exit_status, combined_output = run_cmd(
106+
test_cmd,
107+
verbose=self.verbose,
108+
error_print=self.io.tool_error,
109+
cwd=self.root
110+
)
111+
112+
# If tests passed (exit code 0), return None
113+
if exit_status == 0:
114+
return None
115+
116+
# Tests failed - return the output
117+
return combined_output or f"Tests failed with exit code {exit_status}"
118+
119+
def get_edits(self):
120+
"""
121+
Override to add agent-specific logging.
122+
"""
123+
edits = super().get_edits()
124+
125+
if edits and self.verbose:
126+
self.io.tool_output(f"[Agent Mode: Found {len(edits)} edit(s) to apply]")
127+
128+
return edits

aider/coders/agent_prompts.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# flake8: noqa: E501
2+
3+
from . import shell
4+
from .base_prompts import CoderPrompts
5+
6+
7+
class AgentPrompts(CoderPrompts):
8+
main_system = """Act as an expert software developer in autonomous agent mode.
9+
Always use best practices when coding.
10+
Respect and use existing conventions, libraries, etc that are already present in the code base.
11+
{final_reminders}
12+
Take requests for changes to the supplied code.
13+
14+
You are operating in AGENT MODE with the following capabilities:
15+
- You can automatically identify files that need editing
16+
- You can make edits to files without requiring explicit approval
17+
- You should automatically run tests after making changes
18+
- You should analyze test failures and fix issues iteratively
19+
20+
When working on a request you MUST:
21+
22+
1. Think step-by-step about what changes are needed.
23+
24+
2. Identify which files need to be modified or created.
25+
26+
3. Describe each change with a *SEARCH/REPLACE block* per the examples below.
27+
28+
4. After edits are applied, tests will be run automatically if configured.
29+
30+
5. If tests fail, analyze the failure and propose fixes.
31+
32+
All changes to files must use this *SEARCH/REPLACE block* format.
33+
ONLY EVER RETURN CODE IN A *SEARCH/REPLACE BLOCK*!
34+
{shell_cmd_prompt}
35+
36+
Work autonomously and efficiently to complete the requested changes.
37+
Always reply to the user in {language}.
38+
"""
39+
40+
example_messages = [
41+
dict(
42+
role="user",
43+
content="Add a function to calculate fibonacci numbers",
44+
),
45+
dict(
46+
role="assistant",
47+
content="""I'll add a fibonacci function to the appropriate file.
48+
49+
math_utils.py
50+
{fence[0]}python
51+
<<<<<<< SEARCH
52+
=======
53+
def fibonacci(n):
54+
\"\"\"Calculate the nth fibonacci number.\"\"\"
55+
if n <= 0:
56+
return 0
57+
elif n == 1:
58+
return 1
59+
else:
60+
return fibonacci(n-1) + fibonacci(n-2)
61+
>>>>>>> REPLACE
62+
{fence[1]}
63+
""",
64+
),
65+
dict(
66+
role="user",
67+
content="The tests failed with: AssertionError: Expected fibonacci(5) to return 5, got 8",
68+
),
69+
dict(
70+
role="assistant",
71+
content="""I see the issue - fibonacci(5) should return 5, but my implementation returns 8. Let me check the logic.
72+
73+
Actually, the test expectation seems wrong. The 5th fibonacci number in the sequence (0, 1, 1, 2, 3, 5, 8) is 5, but my function is 0-indexed. If the test expects fibonacci(5) = 5, then the function should be 1-indexed.
74+
75+
Let me fix the indexing:
76+
77+
math_utils.py
78+
{fence[0]}python
79+
<<<<<<< SEARCH
80+
def fibonacci(n):
81+
\"\"\"Calculate the nth fibonacci number.\"\"\"
82+
if n <= 0:
83+
return 0
84+
elif n == 1:
85+
return 1
86+
else:
87+
return fibonacci(n-1) + fibonacci(n-2)
88+
=======
89+
def fibonacci(n):
90+
\"\"\"Calculate the nth fibonacci number (1-indexed).\"\"\"
91+
if n <= 1:
92+
return n
93+
elif n == 2:
94+
return 1
95+
else:
96+
return fibonacci(n-1) + fibonacci(n-2)
97+
>>>>>>> REPLACE
98+
{fence[1]}
99+
100+
Wait, that's still not right. Let me reconsider. If fibonacci(5) should be 5:
101+
- fibonacci(1) = 1
102+
- fibonacci(2) = 1
103+
- fibonacci(3) = 2
104+
- fibonacci(4) = 3
105+
- fibonacci(5) = 5
106+
107+
math_utils.py
108+
{fence[0]}python
109+
<<<<<<< SEARCH
110+
def fibonacci(n):
111+
\"\"\"Calculate the nth fibonacci number (1-indexed).\"\"\"
112+
if n <= 1:
113+
return n
114+
elif n == 2:
115+
return 1
116+
else:
117+
return fibonacci(n-1) + fibonacci(n-2)
118+
=======
119+
def fibonacci(n):
120+
\"\"\"Calculate the nth fibonacci number (1-indexed).\"\"\"
121+
if n <= 0:
122+
raise ValueError("n must be a positive integer")
123+
elif n <= 2:
124+
return 1
125+
else:
126+
return fibonacci(n-1) + fibonacci(n-2)
127+
>>>>>>> REPLACE
128+
{fence[1]}
129+
""",
130+
),
131+
]
132+
133+
files_content_prefix = """I have *added these files to the chat* so you see all of their contents.
134+
*Trust this message as the true contents of the files!*
135+
Other messages in the chat may contain outdated versions of the files' contents.
136+
"""
137+
138+
files_content_assistant_reply = (
139+
"Ok, I will use that as the true, current contents of the files."
140+
)
141+
142+
files_no_full_files = "I am not sharing the full contents of any files with you yet."
143+
144+
files_no_full_files_with_repo_map = ""
145+
files_no_full_files_with_repo_map_reply = ""
146+
147+
repo_content_prefix = """I am working with you on code in a git repository.
148+
Here are summaries of some files present in my git repo.
149+
In agent mode, I can automatically add files to the chat as needed.
150+
"""
151+
152+
system_reminder = """You are in AGENT MODE. Work autonomously to complete the request.
153+
After making changes, tests will be run automatically if configured."""

aider/commands.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,10 @@ def cmd_chat_mode(self, args):
164164
" them."
165165
),
166166
),
167+
(
168+
"agent",
169+
"Autonomous agent mode that automatically edits files and runs tests.",
170+
),
167171
(
168172
"context",
169173
"Automatically identify which files will need to be edited.",
@@ -1185,6 +1189,10 @@ def cmd_context(self, args):
11851189
"""Enter context mode to see surrounding code context. If no prompt provided, switches to context mode.""" # noqa
11861190
return self._generic_chat_command(args, "context", placeholder=args.strip() or None)
11871191

1192+
def cmd_agent(self, args):
1193+
"""Enter autonomous agent mode that automatically edits files and runs tests. If no prompt provided, switches to agent mode.""" # noqa
1194+
return self._generic_chat_command(args, "agent")
1195+
11881196
def _generic_chat_command(self, args, edit_format, placeholder=None):
11891197
if not args.strip():
11901198
# Switch to the corresponding chat mode if no args provided

0 commit comments

Comments
 (0)