Introduction
Debugging is where coding agents truly prove their value. When code doesn't work—whether from compilation errors, runtime exceptions, or failing tests—the agent must diagnose the problem, formulate a fix, and iterate until resolved. This section covers building robust debugging and iteration capabilities.
The Debugging Challenge: Unlike humans who can intuit likely causes, agents must systematically analyze errors, gather context, and apply fixes. The key is building a structured approach that handles the most common error patterns efficiently.
Error Categorization
Different types of errors require different debugging approaches. We start by categorizing errors:
| Error Type | Detection | Typical Fix |
|---|---|---|
| Syntax Error | Parser/compiler output | Fix malformed code |
| Type Error | Type checker output | Add types, fix mismatches |
| Import Error | Module not found | Install package, fix path |
| Runtime Error | Stack trace | Fix logic, handle edge cases |
| Test Failure | Assertion failed | Fix implementation or test |
| Lint Error | Linter output | Apply formatting/style fixes |
| Build Error | Build tool output | Fix config or dependencies |
1from enum import Enum
2from dataclasses import dataclass, field
3from typing import List, Dict, Any, Optional
4import re
5
6
7class ErrorType(Enum):
8 """Types of errors a coding agent may encounter."""
9 SYNTAX = "syntax"
10 TYPE = "type"
11 IMPORT = "import"
12 RUNTIME = "runtime"
13 ASSERTION = "assertion"
14 LINT = "lint"
15 BUILD = "build"
16 TIMEOUT = "timeout"
17 UNKNOWN = "unknown"
18
19
20class ErrorSeverity(Enum):
21 """Severity levels for errors."""
22 FATAL = "fatal" # Prevents any execution
23 ERROR = "error" # Breaks functionality
24 WARNING = "warning" # May cause issues
25 INFO = "info" # Informational
26
27
28@dataclass
29class ParsedError:
30 """A parsed and categorized error."""
31 error_type: ErrorType
32 severity: ErrorSeverity
33 message: str
34 file_path: Optional[str] = None
35 line_number: Optional[int] = None
36 column: Optional[int] = None
37 code_snippet: Optional[str] = None
38 stack_trace: Optional[str] = None
39 suggestion: Optional[str] = None
40 raw_output: str = ""
41
42 def to_context(self) -> str:
43 """Convert to context string for LLM."""
44 parts = [f"Error: {self.message}"]
45
46 if self.file_path:
47 location = f"Location: {self.file_path}"
48 if self.line_number:
49 location += f":{self.line_number}"
50 if self.column:
51 location += f":{self.column}"
52 parts.append(location)
53
54 if self.code_snippet:
55 parts.append(f"Code:\n{self.code_snippet}")
56
57 if self.stack_trace:
58 parts.append(f"Stack trace:\n{self.stack_trace}")
59
60 if self.suggestion:
61 parts.append(f"Suggestion: {self.suggestion}")
62
63 return "\n".join(parts)
64
65
66class ErrorParser:
67 """
68 Parse error output from various sources into structured errors.
69 """
70
71 # Common error patterns by language/tool
72 PATTERNS = {
73 "python_syntax": {
74 "pattern": r"SyntaxError: (.+?)(?:\n|$)",
75 "type": ErrorType.SYNTAX,
76 "severity": ErrorSeverity.FATAL,
77 },
78 "python_type": {
79 "pattern": r"TypeError: (.+?)(?:\n|$)",
80 "type": ErrorType.TYPE,
81 "severity": ErrorSeverity.ERROR,
82 },
83 "python_import": {
84 "pattern": r"(?:ModuleNotFoundError|ImportError): (.+?)(?:\n|$)",
85 "type": ErrorType.IMPORT,
86 "severity": ErrorSeverity.FATAL,
87 },
88 "python_name": {
89 "pattern": r"NameError: name '(\w+)' is not defined",
90 "type": ErrorType.RUNTIME,
91 "severity": ErrorSeverity.ERROR,
92 },
93 "python_attribute": {
94 "pattern": r"AttributeError: (.+?)(?:\n|$)",
95 "type": ErrorType.RUNTIME,
96 "severity": ErrorSeverity.ERROR,
97 },
98 "python_traceback": {
99 "pattern": r'File "(.+?)", line (\d+)',
100 "type": ErrorType.RUNTIME,
101 "severity": ErrorSeverity.ERROR,
102 },
103 "typescript_error": {
104 "pattern": r"error TS(\d+): (.+?)(?:\n|$)",
105 "type": ErrorType.TYPE,
106 "severity": ErrorSeverity.ERROR,
107 },
108 "eslint_error": {
109 "pattern": r"(\d+):(\d+)\s+error\s+(.+?)\s+",
110 "type": ErrorType.LINT,
111 "severity": ErrorSeverity.ERROR,
112 },
113 "jest_fail": {
114 "pattern": r"FAIL\s+(.+?)(?:\n|$)",
115 "type": ErrorType.ASSERTION,
116 "severity": ErrorSeverity.ERROR,
117 },
118 "npm_error": {
119 "pattern": r"npm ERR! (.+?)(?:\n|$)",
120 "type": ErrorType.BUILD,
121 "severity": ErrorSeverity.ERROR,
122 },
123 }
124
125 def parse(self, output: str, source: str = None) -> List[ParsedError]:
126 """Parse error output into structured errors."""
127 errors = []
128
129 for name, pattern_info in self.PATTERNS.items():
130 pattern = pattern_info["pattern"]
131 matches = re.finditer(pattern, output)
132
133 for match in matches:
134 error = ParsedError(
135 error_type=pattern_info["type"],
136 severity=pattern_info["severity"],
137 message=match.group(1) if match.lastindex else match.group(0),
138 raw_output=output
139 )
140
141 # Try to extract location
142 location = self._extract_location(output, match.start())
143 if location:
144 error.file_path = location.get("file")
145 error.line_number = location.get("line")
146 error.column = location.get("column")
147
148 # Try to extract code snippet
149 if error.file_path and error.line_number:
150 error.code_snippet = self._extract_snippet(
151 error.file_path,
152 error.line_number
153 )
154
155 errors.append(error)
156
157 # If no patterns matched, create a generic error
158 if not errors and output.strip():
159 errors.append(ParsedError(
160 error_type=ErrorType.UNKNOWN,
161 severity=ErrorSeverity.ERROR,
162 message=output[:500],
163 raw_output=output
164 ))
165
166 return errors
167
168 def _extract_location(
169 self,
170 output: str,
171 start_pos: int
172 ) -> Optional[Dict[str, Any]]:
173 """Extract file location from error output."""
174 # Look for common location patterns near the error
175 context = output[max(0, start_pos - 200):start_pos + 200]
176
177 # Python-style: File "path", line N
178 match = re.search(r'File "(.+?)", line (\d+)', context)
179 if match:
180 return {
181 "file": match.group(1),
182 "line": int(match.group(2))
183 }
184
185 # TypeScript/JavaScript style: path(line,col)
186 match = re.search(r"(\S+\.(?:ts|js|tsx|jsx))\((\d+),(\d+)\)", context)
187 if match:
188 return {
189 "file": match.group(1),
190 "line": int(match.group(2)),
191 "column": int(match.group(3))
192 }
193
194 # ESLint style: path:line:col
195 match = re.search(r"(\S+\.(?:ts|js|tsx|jsx)):(\d+):(\d+)", context)
196 if match:
197 return {
198 "file": match.group(1),
199 "line": int(match.group(2)),
200 "column": int(match.group(3))
201 }
202
203 return None
204
205 def _extract_snippet(
206 self,
207 file_path: str,
208 line_number: int,
209 context: int = 3
210 ) -> Optional[str]:
211 """Extract code snippet around the error line."""
212 try:
213 from pathlib import Path
214 path = Path(file_path)
215 if not path.exists():
216 return None
217
218 lines = path.read_text().split("\n")
219 start = max(0, line_number - context - 1)
220 end = min(len(lines), line_number + context)
221
222 snippet_lines = []
223 for i in range(start, end):
224 prefix = ">>> " if i == line_number - 1 else " "
225 snippet_lines.append(f"{prefix}{i + 1:4d} | {lines[i]}")
226
227 return "\n".join(snippet_lines)
228 except:
229 return NoneError Analysis Engine
Once errors are parsed, we need to analyze them to understand root causes and potential fixes:
1from typing import Tuple
2
3
4@dataclass
5class ErrorAnalysis:
6 """Analysis of an error with potential fixes."""
7 error: ParsedError
8 root_cause: str
9 fix_strategies: List[Dict[str, Any]]
10 related_files: List[str]
11 confidence: float # 0.0 to 1.0
12
13
14class ErrorAnalyzer:
15 """
16 Analyze errors to determine root causes and fix strategies.
17 """
18
19 def __init__(self, llm_client, workspace):
20 self.llm = llm_client
21 self.workspace = workspace
22
23 async def analyze(self, errors: List[ParsedError]) -> List[ErrorAnalysis]:
24 """Analyze a list of errors."""
25 analyses = []
26
27 for error in errors:
28 analysis = await self._analyze_single_error(error)
29 analyses.append(analysis)
30
31 # Check for related errors
32 self._identify_relationships(analyses)
33
34 return analyses
35
36 async def _analyze_single_error(self, error: ParsedError) -> ErrorAnalysis:
37 """Analyze a single error."""
38 # First, try pattern-based analysis
39 pattern_analysis = self._pattern_based_analysis(error)
40
41 if pattern_analysis and pattern_analysis["confidence"] > 0.8:
42 return ErrorAnalysis(
43 error=error,
44 root_cause=pattern_analysis["cause"],
45 fix_strategies=pattern_analysis["strategies"],
46 related_files=pattern_analysis.get("files", []),
47 confidence=pattern_analysis["confidence"]
48 )
49
50 # Fall back to LLM analysis
51 return await self._llm_analysis(error)
52
53 def _pattern_based_analysis(
54 self,
55 error: ParsedError
56 ) -> Optional[Dict[str, Any]]:
57 """Analyze error using known patterns."""
58 message = error.message.lower()
59
60 # Import errors
61 if error.error_type == ErrorType.IMPORT:
62 if "no module named" in message:
63 module_match = re.search(r"no module named ['"]?([\w.]+)", message)
64 if module_match:
65 module = module_match.group(1)
66 return {
67 "cause": f"Module '{module}' is not installed",
68 "strategies": [
69 {
70 "type": "install_package",
71 "action": f"pip install {module}",
72 "description": f"Install the missing module"
73 },
74 {
75 "type": "check_import",
76 "action": "verify_import_path",
77 "description": "Check if the import path is correct"
78 }
79 ],
80 "confidence": 0.9
81 }
82
83 # NameError - undefined variable
84 if "is not defined" in message or "undefined" in message:
85 name_match = re.search(r"['"]?(\w+)['"]? is not defined", message)
86 if name_match:
87 name = name_match.group(1)
88 return {
89 "cause": f"Variable or function '{name}' is not defined",
90 "strategies": [
91 {
92 "type": "add_import",
93 "action": f"import {name}",
94 "description": f"Import '{name}' if it's from a module"
95 },
96 {
97 "type": "define_variable",
98 "action": f"define_{name}",
99 "description": f"Define '{name}' before using it"
100 },
101 {
102 "type": "fix_typo",
103 "action": "check_spelling",
104 "description": f"Check if '{name}' is misspelled"
105 }
106 ],
107 "confidence": 0.85
108 }
109
110 # Type errors
111 if error.error_type == ErrorType.TYPE:
112 if "argument" in message and ("expected" in message or "got" in message):
113 return {
114 "cause": "Function called with wrong argument type",
115 "strategies": [
116 {
117 "type": "fix_argument_type",
118 "action": "convert_type",
119 "description": "Convert argument to expected type"
120 },
121 {
122 "type": "check_function_signature",
123 "action": "verify_signature",
124 "description": "Check function signature requirements"
125 }
126 ],
127 "confidence": 0.8
128 }
129
130 # Syntax errors
131 if error.error_type == ErrorType.SYNTAX:
132 if "unexpected" in message or "expected" in message:
133 return {
134 "cause": "Syntax error in code structure",
135 "strategies": [
136 {
137 "type": "fix_syntax",
138 "action": "correct_syntax",
139 "description": "Fix the syntax error at the indicated location"
140 },
141 {
142 "type": "check_brackets",
143 "action": "balance_brackets",
144 "description": "Check for unbalanced brackets/parentheses"
145 }
146 ],
147 "files": [error.file_path] if error.file_path else [],
148 "confidence": 0.85
149 }
150
151 return None
152
153 async def _llm_analysis(self, error: ParsedError) -> ErrorAnalysis:
154 """Use LLM to analyze complex errors."""
155 prompt = f"""Analyze this error and suggest fixes.
156
157Error Type: {error.error_type.value}
158Message: {error.message}
159
160{f'File: {error.file_path}' if error.file_path else ''}
161{f'Line: {error.line_number}' if error.line_number else ''}
162
163{f'Code context:\n{error.code_snippet}' if error.code_snippet else ''}
164
165{f'Stack trace:\n{error.stack_trace}' if error.stack_trace else ''}
166
167Provide analysis in this JSON format:
168{{
169 "root_cause": "Brief explanation of why this error occurred",
170 "fix_strategies": [
171 {{
172 "type": "strategy_type",
173 "action": "specific action to take",
174 "description": "what this fix does",
175 "code_change": "optional: specific code to change"
176 }}
177 ],
178 "related_files": ["list of files that may need changes"],
179 "confidence": 0.0 to 1.0
180}}"""
181
182 response = await self.llm.generate(prompt)
183
184 try:
185 import json
186 data = json.loads(response)
187 return ErrorAnalysis(
188 error=error,
189 root_cause=data.get("root_cause", "Unknown"),
190 fix_strategies=data.get("fix_strategies", []),
191 related_files=data.get("related_files", []),
192 confidence=data.get("confidence", 0.5)
193 )
194 except:
195 return ErrorAnalysis(
196 error=error,
197 root_cause="Could not determine root cause",
198 fix_strategies=[{
199 "type": "manual_review",
200 "action": "review_error",
201 "description": "Manually review the error"
202 }],
203 related_files=[],
204 confidence=0.3
205 )
206
207 def _identify_relationships(self, analyses: List[ErrorAnalysis]):
208 """Identify relationships between errors."""
209 # Check if multiple errors share the same root cause
210 by_file = {}
211 for analysis in analyses:
212 if analysis.error.file_path:
213 if analysis.error.file_path not in by_file:
214 by_file[analysis.error.file_path] = []
215 by_file[analysis.error.file_path].append(analysis)
216
217 # Update related files
218 for file_path, file_analyses in by_file.items():
219 for analysis in file_analyses:
220 for other_file in by_file.keys():
221 if other_file != file_path and other_file not in analysis.related_files:
222 analysis.related_files.append(other_file)Fix Strategies
Different error types require different fix strategies. We implement a strategy pattern for applying fixes:
1from abc import ABC, abstractmethod
2from pathlib import Path
3
4
5class FixStrategy(ABC):
6 """Base class for error fix strategies."""
7
8 @property
9 @abstractmethod
10 def name(self) -> str:
11 pass
12
13 @abstractmethod
14 async def can_apply(self, analysis: ErrorAnalysis, context: Dict) -> bool:
15 """Check if this strategy can be applied."""
16 pass
17
18 @abstractmethod
19 async def apply(self, analysis: ErrorAnalysis, context: Dict) -> Dict[str, Any]:
20 """Apply the fix and return results."""
21 pass
22
23
24class ImportFixStrategy(FixStrategy):
25 """Fix missing import errors."""
26
27 @property
28 def name(self) -> str:
29 return "import_fix"
30
31 async def can_apply(self, analysis: ErrorAnalysis, context: Dict) -> bool:
32 return analysis.error.error_type == ErrorType.IMPORT
33
34 async def apply(self, analysis: ErrorAnalysis, context: Dict) -> Dict[str, Any]:
35 file_tools = context["file_tools"]
36 sandbox = context["sandbox"]
37
38 message = analysis.error.message
39
40 # Extract module name
41 module_match = re.search(r"No module named ['"]?([\w.]+)", message)
42 if not module_match:
43 return {"success": False, "reason": "Could not extract module name"}
44
45 module = module_match.group(1)
46
47 # Try to install the package
48 result = await sandbox.execute(f"pip install {module}")
49
50 if result.success:
51 return {
52 "success": True,
53 "action": f"Installed module: {module}",
54 "command": f"pip install {module}"
55 }
56
57 # Check if it's a local import issue
58 base_module = module.split(".")[0]
59 search_result = await file_tools.glob(f"**/{base_module}.py")
60
61 if search_result:
62 return {
63 "success": False,
64 "reason": f"Module {module} not in pip, but {base_module}.py exists locally",
65 "suggestion": f"Check if the import path is correct",
66 "local_file": search_result[0]
67 }
68
69 return {
70 "success": False,
71 "reason": f"Could not install {module}",
72 "error": result.stderr
73 }
74
75
76class SyntaxFixStrategy(FixStrategy):
77 """Fix syntax errors using LLM."""
78
79 def __init__(self, llm_client):
80 self.llm = llm_client
81
82 @property
83 def name(self) -> str:
84 return "syntax_fix"
85
86 async def can_apply(self, analysis: ErrorAnalysis, context: Dict) -> bool:
87 return analysis.error.error_type == ErrorType.SYNTAX
88
89 async def apply(self, analysis: ErrorAnalysis, context: Dict) -> Dict[str, Any]:
90 file_tools = context["file_tools"]
91 error = analysis.error
92
93 if not error.file_path:
94 return {"success": False, "reason": "No file path in error"}
95
96 # Read the file
97 content = await file_tools.read(error.file_path)
98
99 prompt = f"""Fix the syntax error in this code.
100
101Error: [error.message]
102Line: [error.line_number]
103
104Code:
105[content]
106
107Return ONLY the corrected code, no explanations."""
108
109 fixed_code = await self.llm.generate(prompt)
110
111 # Extract code from response (look for markdown code blocks)
112 pattern = r'[BACKTICK][BACKTICK][BACKTICK]\w*\n(.*?)[BACKTICK][BACKTICK][BACKTICK]'
113 code_match = re.search(pattern.replace('[BACKTICK]', chr(96)), fixed_code, re.DOTALL)
114 if code_match:
115 fixed_code = code_match.group(1)
116
117 # Write the fix
118 await file_tools.write(error.file_path, fixed_code)
119
120 return {
121 "success": True,
122 "action": f"Fixed syntax error in [error.file_path]",
123 "file": error.file_path
124 }
125
126
127class TypeFixStrategy(FixStrategy):
128 """Fix type errors."""
129
130 def __init__(self, llm_client):
131 self.llm = llm_client
132
133 @property
134 def name(self) -> str:
135 return "type_fix"
136
137 async def can_apply(self, analysis: ErrorAnalysis, context: Dict) -> bool:
138 return analysis.error.error_type == ErrorType.TYPE
139
140 async def apply(self, analysis: ErrorAnalysis, context: Dict) -> Dict[str, Any]:
141 file_tools = context["file_tools"]
142 error = analysis.error
143
144 if not error.file_path:
145 return {"success": False, "reason": "No file path in error"}
146
147 content = await file_tools.read(error.file_path)
148
149 code_context = f"Code context:\n[error.code_snippet]" if error.code_snippet else ""
150
151 prompt = f"""Fix the type error in this code.
152
153Error: [error.message]
154Line: [error.line_number]
155
156[code_context]
157
158Full file:
159[content]
160
161Return ONLY the corrected code, no explanations."""
162
163 fixed_code = await self.llm.generate(prompt)
164
165 # Extract code from markdown code blocks
166 pattern = r'[BACKTICK][BACKTICK][BACKTICK]\w*\n(.*?)[BACKTICK][BACKTICK][BACKTICK]'
167 code_match = re.search(pattern.replace('[BACKTICK]', chr(96)), fixed_code, re.DOTALL)
168 if code_match:
169 fixed_code = code_match.group(1)
170
171 await file_tools.write(error.file_path, fixed_code)
172
173 return {
174 "success": True,
175 "action": f"Fixed type error in [error.file_path]",
176 "file": error.file_path
177 }
178
179
180class LintFixStrategy(FixStrategy):
181 """Fix lint errors using auto-fixers."""
182
183 @property
184 def name(self) -> str:
185 return "lint_fix"
186
187 async def can_apply(self, analysis: ErrorAnalysis, context: Dict) -> bool:
188 return analysis.error.error_type == ErrorType.LINT
189
190 async def apply(self, analysis: ErrorAnalysis, context: Dict) -> Dict[str, Any]:
191 sandbox = context["sandbox"]
192 workspace = context["workspace"]
193
194 # Try common auto-fix commands
195 fix_commands = [
196 "npx eslint --fix .",
197 "npx prettier --write .",
198 "black .",
199 "isort .",
200 ]
201
202 for cmd in fix_commands:
203 tool_name = cmd.split()[1] if "npx" in cmd else cmd.split()[0]
204
205 # Check if tool is available
206 check = await sandbox.execute(f"which [tool_name]")
207 if not check.success and "npx" not in cmd:
208 continue
209
210 result = await sandbox.execute(cmd)
211
212 if result.success:
213 return {
214 "success": True,
215 "action": f"Applied auto-fix with [tool_name]",
216 "command": cmd
217 }
218
219 return {
220 "success": False,
221 "reason": "No auto-fixer available"
222 }
223
224
225class FixApplicator:
226 """
227 Apply fix strategies to resolve errors.
228 """
229
230 def __init__(self, llm_client, workspace, sandbox, file_tools):
231 self.strategies = [
232 ImportFixStrategy(),
233 SyntaxFixStrategy(llm_client),
234 TypeFixStrategy(llm_client),
235 LintFixStrategy(),
236 ]
237 self.context = {
238 "workspace": workspace,
239 "sandbox": sandbox,
240 "file_tools": file_tools,
241 "llm": llm_client,
242 }
243
244 async def apply_fixes(
245 self,
246 analyses: List[ErrorAnalysis],
247 max_attempts: int = 3
248 ) -> Dict[str, Any]:
249 """Apply fixes for analyzed errors."""
250 results = {
251 "fixed": [],
252 "failed": [],
253 "skipped": []
254 }
255
256 for analysis in analyses:
257 fixed = False
258
259 for strategy in self.strategies:
260 if await strategy.can_apply(analysis, self.context):
261 for attempt in range(max_attempts):
262 try:
263 result = await strategy.apply(analysis, self.context)
264
265 if result.get("success"):
266 results["fixed"].append({
267 "error": analysis.error.message,
268 "strategy": strategy.name,
269 "result": result
270 })
271 fixed = True
272 break
273 except Exception as e:
274 if attempt == max_attempts - 1:
275 results["failed"].append({
276 "error": analysis.error.message,
277 "strategy": strategy.name,
278 "exception": str(e)
279 })
280
281 if fixed:
282 break
283
284 if not fixed and analysis not in [r["error"] for r in results["failed"]]:
285 results["skipped"].append({
286 "error": analysis.error.message,
287 "reason": "No applicable strategy"
288 })
289
290 return resultsThe Iteration Loop
The iteration loop ties everything together, repeatedly analyzing and fixing errors until the code works or limits are reached:
1from typing import AsyncGenerator
2
3
4class DebugIterationLoop:
5 """
6 The main debugging loop that iteratively fixes errors.
7 """
8
9 MAX_ITERATIONS = 10
10 MAX_SAME_ERROR = 3 # Max times to attempt fixing the same error
11
12 def __init__(
13 self,
14 llm_client,
15 workspace,
16 sandbox,
17 file_tools,
18 test_runner
19 ):
20 self.llm = llm_client
21 self.workspace = workspace
22 self.sandbox = sandbox
23 self.file_tools = file_tools
24 self.test_runner = test_runner
25
26 self.error_parser = ErrorParser()
27 self.error_analyzer = ErrorAnalyzer(llm_client, workspace)
28 self.fix_applicator = FixApplicator(
29 llm_client, workspace, sandbox, file_tools
30 )
31
32 self.error_history = [] # Track errors across iterations
33
34 async def debug_loop(
35 self,
36 initial_command: str = None,
37 verification_command: str = None
38 ) -> AsyncGenerator[Dict[str, Any], None]:
39 """
40 Run the debugging loop.
41
42 Args:
43 initial_command: Command to run (e.g., "python main.py")
44 verification_command: Command to verify fix (e.g., "pytest")
45 """
46 iteration = 0
47 last_error_signature = None
48 same_error_count = 0
49
50 while iteration < self.MAX_ITERATIONS:
51 iteration += 1
52
53 yield {
54 "phase": "execute",
55 "iteration": iteration,
56 "message": f"Running: {initial_command or verification_command}"
57 }
58
59 # Run the command
60 cmd = initial_command or verification_command or "python -m pytest"
61 result = await self.sandbox.execute(cmd)
62
63 if result.success:
64 yield {
65 "phase": "success",
66 "iteration": iteration,
67 "message": "Execution successful!",
68 "output": result.stdout[:500]
69 }
70 return
71
72 # Parse errors
73 yield {
74 "phase": "parse",
75 "iteration": iteration,
76 "message": "Analyzing errors"
77 }
78
79 errors = self.error_parser.parse(
80 result.stdout + "\n" + result.stderr
81 )
82
83 if not errors:
84 yield {
85 "phase": "unknown_error",
86 "iteration": iteration,
87 "message": "Execution failed but no parseable errors",
88 "output": result.stderr[:500]
89 }
90 break
91
92 # Check for repeated errors
93 error_signature = self._get_error_signature(errors)
94 if error_signature == last_error_signature:
95 same_error_count += 1
96 if same_error_count >= self.MAX_SAME_ERROR:
97 yield {
98 "phase": "stuck",
99 "iteration": iteration,
100 "message": f"Same error {same_error_count} times, giving up",
101 "errors": [e.message for e in errors]
102 }
103 break
104 else:
105 same_error_count = 1
106 last_error_signature = error_signature
107
108 # Analyze errors
109 yield {
110 "phase": "analyze",
111 "iteration": iteration,
112 "error_count": len(errors),
113 "errors": [
114 {"type": e.error_type.value, "message": e.message[:100]}
115 for e in errors
116 ]
117 }
118
119 analyses = await self.error_analyzer.analyze(errors)
120
121 # Apply fixes
122 yield {
123 "phase": "fix",
124 "iteration": iteration,
125 "message": "Applying fixes"
126 }
127
128 fix_results = await self.fix_applicator.apply_fixes(analyses)
129
130 yield {
131 "phase": "fix_results",
132 "iteration": iteration,
133 "fixed": len(fix_results["fixed"]),
134 "failed": len(fix_results["failed"]),
135 "skipped": len(fix_results["skipped"])
136 }
137
138 # Track in history
139 self.error_history.append({
140 "iteration": iteration,
141 "errors": [e.message for e in errors],
142 "fixes_applied": fix_results["fixed"]
143 })
144
145 if not fix_results["fixed"]:
146 yield {
147 "phase": "no_fix",
148 "iteration": iteration,
149 "message": "Could not apply any fixes",
150 "failed": fix_results["failed"],
151 "skipped": fix_results["skipped"]
152 }
153 break
154
155 yield {
156 "phase": "max_iterations",
157 "iterations": iteration,
158 "history": self.error_history
159 }
160
161 def _get_error_signature(self, errors: List[ParsedError]) -> str:
162 """Get a signature for a set of errors to detect loops."""
163 signatures = []
164 for error in errors:
165 sig = f"{error.error_type.value}:{error.message[:50]}"
166 if error.file_path and error.line_number:
167 sig += f"@{error.file_path}:{error.line_number}"
168 signatures.append(sig)
169 return "|".join(sorted(signatures))
170
171 async def smart_debug(
172 self,
173 task_description: str,
174 target_files: List[str]
175 ) -> AsyncGenerator[Dict[str, Any], None]:
176 """
177 Smart debugging that uses context about the task.
178 """
179 yield {
180 "phase": "setup",
181 "message": "Gathering context for smart debugging"
182 }
183
184 # Gather context
185 context = {
186 "task": task_description,
187 "files": {},
188 "recent_changes": []
189 }
190
191 for file_path in target_files:
192 try:
193 content = await self.file_tools.read(file_path)
194 context["files"][file_path] = content
195 except:
196 pass
197
198 # Run initial verification
199 initial_result = await self.test_runner.run_tests()
200
201 if initial_result.success:
202 yield {
203 "phase": "success",
204 "message": "All tests pass!"
205 }
206 return
207
208 yield {
209 "phase": "initial_failure",
210 "test_results": initial_result.summary()
211 }
212
213 # Smart debugging with task context
214 for failed_test in initial_result.failed_tests():
215 yield {
216 "phase": "debugging_test",
217 "test": failed_test.name,
218 "error": failed_test.error_message
219 }
220
221 # Generate fix using task context
222 fix = await self._generate_contextual_fix(
223 failed_test,
224 context
225 )
226
227 if fix:
228 yield {
229 "phase": "applying_fix",
230 "test": failed_test.name,
231 "fix": fix["description"]
232 }
233
234 await self.file_tools.edit(
235 fix["file"],
236 fix["old_content"],
237 fix["new_content"]
238 )
239
240 # Verify
241 result = await self.test_runner.run_tests(
242 specific_test=failed_test.name
243 )
244
245 if result.success:
246 yield {
247 "phase": "test_fixed",
248 "test": failed_test.name
249 }
250 else:
251 yield {
252 "phase": "test_still_failing",
253 "test": failed_test.name,
254 "new_error": result.failed_tests()[0].error_message if result.failed_tests() else "Unknown"
255 }
256
257 async def _generate_contextual_fix(
258 self,
259 test: TestResult,
260 context: Dict[str, Any]
261 ) -> Optional[Dict[str, Any]]:
262 """Generate a fix using task context."""
263 prompt = f"""Generate a fix for this failing test.
264
265Task: {context['task']}
266
267Failing test: {test.name}
268Error: {test.error_message}
269
270Relevant files:
271{chr(10).join(f'--- {f} ---{chr(10)}{c[:1000]}' for f, c in list(context['files'].items())[:3])}
272
273Respond in JSON:
274{{
275 "file": "path/to/file",
276 "old_content": "exact content to replace",
277 "new_content": "new content",
278 "description": "what this fix does"
279}}"""
280
281 response = await self.llm.generate(prompt)
282
283 try:
284 import json
285 return json.loads(response)
286 except:
287 return NoneLearning from Fixes
Agents can learn from successful fixes to handle similar errors faster in the future:
1from dataclasses import dataclass, field
2from datetime import datetime
3from typing import List, Dict, Any
4import json
5from pathlib import Path
6
7
8@dataclass
9class FixPattern:
10 """A learned pattern for fixing errors."""
11 error_signature: str
12 fix_strategy: str
13 fix_details: Dict[str, Any]
14 success_count: int = 0
15 failure_count: int = 0
16 last_used: str = field(default_factory=lambda: datetime.now().isoformat())
17
18 @property
19 def success_rate(self) -> float:
20 total = self.success_count + self.failure_count
21 return self.success_count / total if total > 0 else 0.0
22
23
24class FixPatternLearner:
25 """
26 Learn and store successful fix patterns.
27 """
28
29 def __init__(self, storage_path: Path = None):
30 self.storage_path = storage_path or Path(".agent/fix_patterns.json")
31 self.patterns: Dict[str, FixPattern] = {}
32 self._load_patterns()
33
34 def _load_patterns(self):
35 """Load patterns from storage."""
36 if self.storage_path.exists():
37 try:
38 data = json.loads(self.storage_path.read_text())
39 for sig, pattern_data in data.items():
40 self.patterns[sig] = FixPattern(**pattern_data)
41 except:
42 pass
43
44 def _save_patterns(self):
45 """Save patterns to storage."""
46 self.storage_path.parent.mkdir(parents=True, exist_ok=True)
47 data = {
48 sig: {
49 "error_signature": p.error_signature,
50 "fix_strategy": p.fix_strategy,
51 "fix_details": p.fix_details,
52 "success_count": p.success_count,
53 "failure_count": p.failure_count,
54 "last_used": p.last_used
55 }
56 for sig, p in self.patterns.items()
57 }
58 self.storage_path.write_text(json.dumps(data, indent=2))
59
60 def record_fix(
61 self,
62 error: ParsedError,
63 strategy: str,
64 details: Dict[str, Any],
65 success: bool
66 ):
67 """Record a fix attempt."""
68 signature = self._get_signature(error)
69
70 if signature not in self.patterns:
71 self.patterns[signature] = FixPattern(
72 error_signature=signature,
73 fix_strategy=strategy,
74 fix_details=details
75 )
76
77 pattern = self.patterns[signature]
78 pattern.last_used = datetime.now().isoformat()
79
80 if success:
81 pattern.success_count += 1
82 # Update fix details if this was successful
83 pattern.fix_details = details
84 else:
85 pattern.failure_count += 1
86
87 self._save_patterns()
88
89 def get_known_fix(self, error: ParsedError) -> Optional[FixPattern]:
90 """Get a known fix for an error if available."""
91 signature = self._get_signature(error)
92
93 if signature in self.patterns:
94 pattern = self.patterns[signature]
95 if pattern.success_rate >= 0.7: # Only suggest high-success fixes
96 return pattern
97
98 # Try partial matching
99 for sig, pattern in self.patterns.items():
100 if self._partial_match(signature, sig) and pattern.success_rate >= 0.8:
101 return pattern
102
103 return None
104
105 def _get_signature(self, error: ParsedError) -> str:
106 """Generate a signature for an error."""
107 # Normalize the error message
108 message = error.message.lower()
109
110 # Remove specific identifiers
111 message = re.sub(r"['"]\w+['"]", "'X'", message)
112 message = re.sub(r"\d+", "N", message)
113
114 return f"{error.error_type.value}:{message[:100]}"
115
116 def _partial_match(self, sig1: str, sig2: str) -> bool:
117 """Check if two signatures partially match."""
118 parts1 = sig1.split(":")
119 parts2 = sig2.split(":")
120
121 if parts1[0] != parts2[0]: # Same error type
122 return False
123
124 # Check message similarity
125 from difflib import SequenceMatcher
126 ratio = SequenceMatcher(None, parts1[1], parts2[1]).ratio()
127 return ratio > 0.7
128
129 def get_statistics(self) -> Dict[str, Any]:
130 """Get learning statistics."""
131 return {
132 "total_patterns": len(self.patterns),
133 "high_confidence_patterns": len([
134 p for p in self.patterns.values()
135 if p.success_rate >= 0.8
136 ]),
137 "total_fixes": sum(
138 p.success_count + p.failure_count
139 for p in self.patterns.values()
140 ),
141 "overall_success_rate": sum(
142 p.success_count for p in self.patterns.values()
143 ) / max(1, sum(
144 p.success_count + p.failure_count
145 for p in self.patterns.values()
146 ))
147 }Summary
In this section, we built comprehensive debugging capabilities for our coding agent:
- Error Categorization: Parsing and categorizing errors by type and severity
- Error Analysis: Pattern-based and LLM-powered analysis to determine root causes
- Fix Strategies: Specialized strategies for different error types (imports, syntax, types, lint)
- Iteration Loop: A robust loop that repeatedly fixes errors with cycle detection
- Pattern Learning: Building knowledge of successful fixes for faster future debugging
In the final section, we'll bring everything together into a complete, production-ready coding agent implementation.