Chapter 20 · Capstone: Coding Agent
"This is the final exam. Build an agent that writes code, tests it, fixes bugs, and deploys — all while you watch."
You've reached the summit. This chapter is the integration of everything you've learned: agent loops, tool use, memory, reasoning, code execution, safety, and deployment. You'll build a coding agent — a system that takes a software task, writes the code, tests it, fixes bugs, and produces working software.
This is not a toy. By the end, you'll have an agent that can contribute real code.
Section 1: Architecture
The coding agent uses a plan-and-execute architecture with safety at every layer:
┌──────────────┐
│ User Task │
│ "Build a..." │
└──────┬───────┘
│
┌──────▼───────┐
│ Planner │ ← Decomposes task into steps
└──────┬───────┘
│
┌──────▼───────┐
│ Coder │ ← Writes code for each step
└──────┬───────┘
│
┌───────────┼───────────┐
│ │ │
┌─────▼─────┐ ┌──▼───┐ ┌────▼─────┐
│ Safety │ │ Test │ │ Execute │
│ Review │ │ Gen │ │ │
└─────┬─────┘ └──┬───┘ └────┬─────┘
│ │ │
└───────────┼───────────┘
│
┌──────▼───────┐
│ Debugger │ ← Fixes failures
└──────┬───────┘
│
┌──────▼───────┐
│ Reviewer │ ← Final quality check
└──────┬───────┘
│
┌──────▼───────┐
│ Working │
│ Software │
└──────────────┘
Section 2: The Planner
The planner decomposes the user's task into implementable steps. This is critical — a good plan prevents the coder from going down rabbit holes.
class CodingPlanner:
async def plan(self, task: str, context: dict = None) -> dict:
"""Decompose a coding task into implementable steps."""
plan = await llm.generate_structured(
system="""You are a senior software architect. Given a coding task,
create a detailed implementation plan.
For each step, specify:
- What file(s) to create or modify
- What the code should do
- What dependencies it needs
- How to verify the step is complete
Rules:
- Each step should be small and testable
- Steps should be ordered by dependency
- Include error handling in every step
- Prefer standard library over external dependencies""",
prompt=f"Task: {task}\n\nExisting codebase context: {context or 'New project'}",
schema={
"type": "object",
"properties": {
"overview": {"type": "string"},
"tech_stack": {
"type": "object",
"properties": {
"language": {"type": "string"},
"framework": {"type": "string"},
"dependencies": {"type": "array", "items": {"type": "string"}},
},
},
"steps": {
"type": "array",
"items": {
"type": "object",
"properties": {
"step_number": {"type": "integer"},
"description": {"type": "string"},
"files_to_create": {"type": "array", "items": {"type": "string"}},
"files_to_modify": {"type": "array", "items": {"type": "string"}},
"verification": {"type": "string"},
"dependencies": {"type": "array", "items": {"type": "integer"}},
},
},
},
},
},
)
return plan
Section 3: The Coder
The coder writes code for each step, with awareness of what came before.
class CodingAgent:
def __init__(self, workspace: str = "/tmp/coding_agent"):
self.workspace = workspace
self.safety = SafetyReviewer()
self.executor = SandboxExecutor(workspace)
self.debugger = Debugger()
async def implement_step(self, step: dict, plan: dict, existing_code: dict) -> dict:
"""Implement one step of the plan."""
max_attempts = 3
for attempt in range(max_attempts):
# Generate code
code = await self._generate_code(step, plan, existing_code)
# Safety review
safety = self.safety.review(code, step)
if not safety["safe"]:
if attempt == max_attempts - 1:
return {"success": False, "error": f"Safety review failed: {safety['issues']}"}
continue # Try again with safety feedback
# Write code to files
self._write_files(code)
# Generate and run tests
tests = await self._generate_tests(code, step)
test_result = self.executor.run_tests(tests)
if test_result["passed"]:
return {
"success": True,
"code": code,
"tests": tests,
"test_result": test_result,
}
# Tests failed — debug
if attempt < max_attempts - 1:
fix = await self.debugger.debug(
code=code,
test_failures=test_result["failures"],
step=step,
)
existing_code = {**existing_code, **fix.get("changes", {})}
return {"success": False, "error": "Max attempts reached"}
async def _generate_code(self, step: dict, plan: dict, existing_code: dict) -> dict:
"""Generate code for a single step."""
response = await llm.generate_structured(
system="""You are an expert programmer. Write clean, well-documented code
for the specified step. The code must:
- Follow the plan exactly
- Be compatible with existing code
- Include error handling
- Be well-typed (use type hints in Python)
- Include docstrings for all public functions""",
prompt=f"""Plan overview: {plan['overview']}
Tech stack: {json.dumps(plan['tech_stack'])}
Current step ({step['step_number']}): {step['description']}
Files to create: {step['files_to_create']}
Files to modify: {step['files_to_modify']}
Existing code:
{json.dumps(existing_code, indent=2)}
Write the code for this step. Output each file's complete content.""",
schema={
"type": "object",
"properties": {
"files": {
"type": "array",
"items": {
"type": "object",
"properties": {
"path": {"type": "string"},
"content": {"type": "string"},
"description": {"type": "string"},
},
},
},
},
},
)
return response
Section 4: Safety Reviewer
Every line of generated code goes through safety review before execution.
class SafetyReviewer:
DANGEROUS_PATTERNS = [
(r"os\.system\s*\(", "Shell command execution"),
(r"subprocess\.(run|Popen|call)\s*\(", "Subprocess execution"),
(r"__import__\s*\(", "Dynamic import"),
(r"eval\s*\(", "Code evaluation"),
(r"exec\s*\(", "Code execution"),
(r"open\s*\([^)]*['\"]w", "File write outside workspace"),
(r"requests\.(post|put|delete|patch)\s*\(", "Outbound HTTP request"),
(r"socket\.", "Network socket"),
(r"shutil\.(rmtree|move|copy)\s*\(", "File system modification"),
(r"while\s+True\s*:", "Potential infinite loop"),
]
def review(self, code: dict, step: dict) -> dict:
"""Review generated code for safety issues."""
issues = []
for file_info in code.get("files", []):
content = file_info["content"]
file_path = file_info["path"]
# Check for dangerous patterns
for pattern, description in self.DANGEROUS_PATTERNS:
if re.search(pattern, content):
issues.append(f"{file_path}: {description}")
# Check file path is within workspace
if not file_path.startswith("/tmp/coding_agent"):
issues.append(f"{file_path}: File outside workspace")
# Check for reasonable file size
if len(content) > 100_000:
issues.append(f"{file_path}: File too large ({len(content)} chars)")
return {
"safe": len(issues) == 0,
"issues": issues,
}
Section 5: The Debugger
When tests fail, the debugger analyzes the failure and fixes the code.
class Debugger:
async def debug(self, code: dict, test_failures: list[dict], step: dict) -> dict:
"""Debug failing code and propose fixes."""
fix = await llm.generate_structured(
system="""You are an expert debugger. Given failing code and test failures,
identify the root cause and fix it.
Rules:
- Make minimal changes — fix the bug, don't rewrite everything
- Explain what was wrong and why your fix works
- If you can't determine the cause, say so rather than guessing""",
prompt=f"""Step: {step['description']}
Current code:
{json.dumps(code, indent=2)}
Test failures:
{json.dumps(test_failures, indent=2)}
Identify the bugs and provide fixed code.""",
schema={
"type": "object",
"properties": {
"root_cause": {"type": "string"},
"fix_description": {"type": "string"},
"changes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"file": {"type": "string"},
"old_code": {"type": "string"},
"new_code": {"type": "string"},
"reason": {"type": "string"},
},
},
},
},
},
)
return fix
Section 6: The Complete Coding Agent
class CompleteCodingAgent:
def __init__(self, workspace: str = "/tmp/coding_agent"):
self.workspace = workspace
self.planner = CodingPlanner()
self.coder = CodingAgent(workspace)
self.reviewer = CodeReviewer()
os.makedirs(workspace, exist_ok=True)
async def build(self, task: str) -> dict:
"""Build software from a natural language task."""
print(f"Task: {task}\n")
# Phase 1: Plan
print("[Planner] Creating implementation plan...")
plan = await self.planner.plan(task)
print(f"[Planner] {len(plan['steps'])} steps planned")
print(f"[Planner] Tech stack: {plan['tech_stack']['language']} / {plan['tech_stack']['framework']}")
# Phase 2: Implement each step
existing_code = {}
results = []
for step in plan["steps"]:
print(f"\n[Step {step['step_number']}/{len(plan['steps'])}] {step['description']}")
result = await self.coder.implement_step(step, plan, existing_code)
results.append(result)
if result["success"]:
print(f" ✓ Implemented in {len(result.get('code', {}).get('files', []))} files")
print(f" ✓ Tests passing: {result['test_result']['total']} tests")
# Update existing code with new files
for file_info in result["code"].get("files", []):
existing_code[file_info["path"]] = file_info["content"]
else:
print(f" ✗ Failed: {result['error']}")
return {
"success": False,
"plan": plan,
"failed_step": step["step_number"],
"error": result["error"],
"results": results,
}
# Phase 3: Final review
print("\n[Reviewer] Final quality review...")
review = await self.reviewer.review_all(existing_code, plan, task)
print(f"[Reviewer] Score: {review['overall_score']}/10")
if review["issues"]:
for issue in review["issues"]:
print(f" - {issue}")
return {
"success": True,
"plan": plan,
"code": existing_code,
"results": results,
"review": review,
}
Section 7: Running the Coding Agent
async def main():
agent = CompleteCodingAgent()
task = """
Build a REST API for a todo list application with the following features:
- Create, read, update, and delete todo items
- Each item has: id, title, description, status (pending/in_progress/done), created_at, updated_at
- Filter items by status
- Sort items by created_at
- Input validation (title required, status must be valid)
- Proper error handling and HTTP status codes
- Use FastAPI and SQLite (with SQLAlchemy)
- Include a full test suite with pytest
"""
result = await agent.build(task)
if result["success"]:
print("\n" + "=" * 60)
print("BUILD SUCCESSFUL")
print("=" * 60)
print(f"\nFiles created: {len(result['code'])}")
for path in result["code"]:
print(f" {path} ({len(result['code'][path])} bytes)")
print(f"\nReview score: {result['review']['overall_score']}/10")
else:
print(f"\nBuild failed at step {result['failed_step']}: {result['error']}")
asyncio.run(main())
Section 8: Where to Go from Here
You've reached the end of this book. You've built:
- An agent loop from scratch (Chapter 4)
- Tool-using agents (Chapter 5)
- Agents with memory (Chapter 6)
- Agents that reason (Chapter 7)
- Multi-agent systems (Chapter 11)
- RAG-powered agents (Chapter 12)
- Secure, evaluated agents (Chapters 13-14)
- Production workflows (Chapter 15)
- Code-generating agents (Chapter 16)
- Computer-use agents (Chapter 17)
- Deployed production agents (Chapter 18)
- A complete research assistant (Chapter 19)
- A complete coding agent (this chapter)
What's next:
-
Build something real. Pick a problem you care about. Build an agent for it. Ship it. The best way to solidify these skills is to use them.
-
Stay current. The agent ecosystem moves fast. Follow the major framework releases. Read the model release notes. But don't chase every new thing — the fundamentals you've learned (the loop, tools, memory, reasoning) will outlast any framework.
-
Go deep on what interests you. Multi-agent systems? RAG? Computer use? Code generation? Pick one and become the expert. The breadth you have from this book gives you the foundation to go deep anywhere.
-
Contribute back. Write about what you build. Open-source your agents. Help others on the same journey. The agent ecosystem is young, and the best practices are still being written — by people like you.
The Turn
You started this book knowing how to call an LLM API. You're finishing it knowing how to build systems that think, decide, act, remember, collaborate, and create.
The brain is out of the jar. It has hands, memory, and purpose. What you do with it is up to you.
Go build something that matters.