Chapter 12
20 min read
Section 76 of 175

Debugging and Iteration Loop

Building a Coding Agent

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 TypeDetectionTypical Fix
Syntax ErrorParser/compiler outputFix malformed code
Type ErrorType checker outputAdd types, fix mismatches
Import ErrorModule not foundInstall package, fix path
Runtime ErrorStack traceFix logic, handle edge cases
Test FailureAssertion failedFix implementation or test
Lint ErrorLinter outputApply formatting/style fixes
Build ErrorBuild tool outputFix config or dependencies
🐍python
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 None

Error Analysis Engine

Once errors are parsed, we need to analyze them to understand root causes and potential fixes:

🐍python
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)
Pattern-based analysis is faster and more reliable for common errors. Reserve LLM analysis for complex or unusual cases.

Fix Strategies

Different error types require different fix strategies. We implement a strategy pattern for applying fixes:

🐍python
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 results

The Iteration Loop

The iteration loop ties everything together, repeatedly analyzing and fixing errors until the code works or limits are reached:

🐍python
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 None
Implement a maximum iteration limit to prevent infinite loops when errors can't be fixed. Also track repeated errors to detect when the agent is stuck.

Learning from Fixes

Agents can learn from successful fixes to handle similar errors faster in the future:

🐍python
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        }
Pattern learning allows agents to build up expertise over time. Store patterns persistently so they survive across sessions.

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.