Week 5 Major Project: Architecture and Implementation of an Autonomous Coding Agent
Autonomous coding assistants such as Anthropic's Claude Code, OpenAI's Codex, and Google's Gemini CLI tools represent a fundamental shift in software engineering. Rather than treating Large Language Models (LLMs) as passive question-answering engines, modern developer tools structure them as active, iterative reasoning agents capable of navigating, modifying, and verifying codebases. This guide details the foundational architecture, tool interfaces, and execution loops required to design and implement an end-to-end autonomous coding agent from first principles.
1. The Context Window Bottleneck: Why Naive LLM Usage Fails
When developers first consider building a coding assistant, the common impulse is to replicate the standard web interface experience: take source code, paste it into a prompt along with an issue description, and ask the model to produce a fix.
While this naive strategy works for toy algorithmic problems (such as solving a 40-line LeetCode puzzle like Two Sum), it completely collapses in professional software engineering environments for three critical reasons:
- Context Window Exhaustion: Production enterprise systems—such as an Amazon or enterprise e-commerce platform—consist of millions of lines of code distributed across thousands of files, modules, and Git repositories. No real-world context window (, , or even tokens) can ingest an entire multi-repository enterprise system simultaneously.
- Attention Degradation (The "Needle in a Haystack" Problem): Even when large context windows physically accommodate several hundred thousand tokens, transformer attention mechanisms experience degradation over long contexts. Models lose precision, hallucinate non-existent imports, or overlook subtle edge cases buried in thousands of lines of irrelevant code.
- Whole-File Rewrites Induce Regressions: Asking an LLM to rewrite a 5,000-line file merely to toggle a single variable from
5%to10%consumes massive compute tokens, introduces subtle deletion errors, drops existing comments, and destroys clean Git diff histories.
Key Architectural Takeaway: An autonomous coding agent must never read an entire codebase up front. Instead, it must interact with the codebase programmatically through dynamic discovery, targeted retrieval, and surgical editing.
2. Core Mental Model: The Day-One Engineering Intern Analogy
To build an autonomous coding system, you must design its execution flow around the investigative behavior of a real software engineer.
Real-Life Analogy: The New Hire at Amazon
Imagine you join Amazon as an entry-level software engineer. On your very first morning, before you have had time to review architecture diagrams or understand the five million lines of production code, a senior engineer walks up to your desk with an urgent issue:
"Our Diwali sale starts tomorrow. Marketing configured a 10% promotional discount across the storefront, but customers in production are only receiving 5%. We don't have time to train you—go find the bug, fix it, verify it, and get it merged."
What do you do? As a human with bounded memory and time:
- You do not sit down to read all 5,000 files from top to bottom.
- You open VS Code and run a global search for the keyword
"discount".- Out of thousands of files, the search isolates 10 relevant candidate files.
- You narrow your attention to the file containing discount calculation logic (
pricing_service.py), open it, and inspect occurrences of"discount".- You spot the defect on line 70:
int discount = amount * 0.05;instead of0.10.- You surgically replace that single line rather than rewriting the entire file.
- You present the diff to your senior engineer for human review and approval.
- You execute the existing unit and integration test suites to verify that your change didn't break adjacent systems.
- Once the test suite passes, you commit and push the change.
An autonomous coding agent is mathematically and operationally identical to this workflow. It is simply an LLM equipped with the minimal set of programmatic tools required to simulate this exact investigation loop.
3. Naive Prompting vs. Tool-Augmented Agentic Architecture
The differences between conventional LLM prompting and tool-augmented agentic execution highlight why agents scale to enterprise codebases while naive prompting fails.
4. The 5 Essential Tool Primitives of a Coding Agent
To enable an LLM to investigate and resolve issues autonomously, the hosting runtime must expose five concrete tool primitives via function calling (API schema definitions).
1. search_codebase(query: str) -> list[str]
- Purpose: Searches the workspace directory for occurrences of a keyword or regular expression.
- Underlying Mechanism: Invokes a fast filesystem search tool (e.g.,
grep,git grep, orripgrep). - Output: Returns a concise list of matching file paths, preventing raw code from flooding the model's context window.
2. search_file(filepath: str, query: str) -> list[dict]
- Purpose: Inspects an individual file identified by the agent.
- Underlying Mechanism: Scans the designated file and extracts line numbers, target text, and a localized window of surrounding context lines (e.g., lines).
- Output: Returns exact line numbers and relevant code blocks without loading an entire multi-thousand-line file into memory.
3. replace_lines(filepath: str, target_line: int, new_content: str) -> bool
- Purpose: Modifies source code safely and deterministically.
- Underlying Mechanism: Reads the target file, replaces precisely the designated line number (or range of lines) with the newly proposed code, and writes the changes back to disk.
- Guardrail: Never overwrites the full file; strictly preserves the rest of the file structure intact.
4. request_human_approval(diff: str) -> bool
- Purpose: Establishes a Human-in-the-Loop (HITL) security gate prior to finalizing code changes.
- Underlying Mechanism: Halts execution, formats the staged change as a standardized Git diff, and prompts the user in the CLI interface (
[Accept / Reject]). - Guardrail: Prevents uncontrolled modifications and destructive filesystem operations.
5. run_tests(test_command: str) -> dict
- Purpose: Validates the correctness of modifications by executing the project's test suite.
- Underlying Mechanism: Executes shell commands (e.g.,
pytest,npm test,cargo test) in a subprocess and captures standard output (stdout), standard error (stderr), and the process exit code (0for pass, non-zero for fail). - Feedback Mechanism: If tests fail, the raw test failure traces are injected back into the LLM's conversation history, triggering an automated debugging cycle.
5. End-to-End Execution Trace
The diagram below maps the deterministic step-by-step sequence an autonomous agent traverses when resolving an issue:
6. Practical Implementation: Building the Tools and Driver Loop
Below is a complete, minimal implementation of an autonomous coding agent written in Python. It demonstrates the tool registration pattern, dynamic dispatch, and the core ReAct (Reasoning + Acting) execution loop.
Step 1: Defining the Tool Primitives
import os
import subprocess
from typing import Dict, Any, List
class CodebaseEnvironment:
def __init__(self, workspace_root: str):
self.workspace_root = os.path.abspath(workspace_root)
def search_codebase(self, query: str) -> List[str]:
"""Searches file paths containing the target query."""
matched_files = []
for root, _, files in os.walk(self.workspace_root):
for file in files:
if file.endswith(('.py', '.js', '.ts', '.java', '.go')):
full_path = os.path.join(root, file)
try:
with open(full_path, 'r', encoding='utf-8') as f:
if query in f.read():
rel_path = os.path.relpath(full_path, self.workspace_root)
matched_files.append(rel_path)
except (UnicodeDecodeError, PermissionError):
continue
return matched_files
def search_file(self, filepath: str, query: str) -> List[Dict[str, Any]]:
"""Finds matching lines and line numbers within a specific file."""
target_path = os.path.join(self.workspace_root, filepath)
results = []
if not os.path.exists(target_path):
return [{"error": f"File {filepath} not found."}]
with open(target_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
for idx, line in enumerate(lines, start=1):
if query in line:
results.append({
"line_number": idx,
"content": line.strip()
})
return results
def replace_lines(self, filepath: str, target_line: int, new_content: str) -> str:
"""Surgically replaces a specific line in a target file."""
target_path = os.path.join(self.workspace_root, filepath)
if not os.path.exists(target_path):
return f"Error: {filepath} does not exist."
with open(target_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
if target_line < 1 or target_line > len(lines):
return f"Error: Line index {target_line} out of range (1 - {len(lines)})."
original_line = lines[target_line - 1]
lines[target_line - 1] = new_content + "\n"
with open(target_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
return f"Successfully replaced line {target_line}. Previous: '{original_line.strip()}' -> New: '{new_content.strip()}'"
def request_human_approval(self, proposal: str) -> bool:
"""Prompts the user via CLI for approval."""
print("\n" + "="*50)
print("AGENT REQUESTING APPROVAL FOR MODIFICATION:")
print(proposal)
print("="*50)
response = input("Approve this modification? (y/n): ").strip().lower()
return response == 'y'
def run_tests(self, test_command: str) -> Dict[str, Any]:
"""Runs the validation test suite inside the workspace."""
try:
result = subprocess.run(
test_command,
shell=True,
cwd=self.workspace_root,
capture_output=True,
text=True,
timeout=60
)
return {
"exit_code": result.returncode,
"stdout": result.stdout,
"stderr": result.stderr,
"passed": result.returncode == 0
}
except subprocess.TimeoutExpired:
return {"exit_code": -1, "stdout": "", "stderr": "Execution timed out.", "passed": False}
Step 2: The Agentic Reasoning & Dispatch Loop
The agent maintains an append-only conversation history. It exposes its tool specifications to the LLM and continuously evaluates whether the model returns a final text response or initiates a tool call.
import json
class CodingAgentRunner:
def __init__(self, env: CodebaseEnvironment, llm_client: Any):
self.env = env
self.client = llm_client
self.conversation_history: List[Dict[str, Any]] = []
def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
"""Dispatches dynamic function calls triggered by the LLM."""
if tool_name == "search_codebase":
return self.env.search_codebase(arguments["query"])
elif tool_name == "search_file":
return self.env.search_file(arguments["filepath"], arguments["query"])
elif tool_name == "replace_lines":
# Human-in-the-Loop verification gate prior to disk mutations
proposal = f"File: {arguments['filepath']}\nLine: {arguments['target_line']}\nNew Content: {arguments['new_content']}"
if not self.env.request_human_approval(proposal):
return "Operation rejected by user. Please rethink the fix."
return self.env.replace_lines(arguments["filepath"], arguments["target_line"], arguments["new_content"])
elif tool_name == "run_tests":
return self.env.run_tests(arguments["test_command"])
else:
return f"Unknown tool: {tool_name}"
def run_task(self, task_instruction: str, max_iterations: int = 10):
self.conversation_history.append({
"role": "system",
"content": (
"You are an expert autonomous software engineer. "
"Investigate bugs by searching code, pinpointing root causes, "
"surgically replacing lines, and validating with unit tests. "
"Never guess lines or overwrite entire files without searching first."
)
})
self.conversation_history.append({"role": "user", "content": task_instruction})
for iteration in range(max_iterations):
# In production: pass tools parameter to API call (e.g. client.chat.completions.create)
response = self.client.generate_response(
messages=self.conversation_history,
tools=AGENT_TOOL_DEFINITIONS
)
# Check if LLM requested a tool execution
if response.get("tool_calls"):
for tool_call in response["tool_calls"]:
fn_name = tool_call["function"]["name"]
fn_args = json.loads(tool_call["function"]["arguments"])
# Execute tool against actual codebase
tool_output = self.execute_tool(fn_name, fn_args)
# Feed observation back to the model's context
self.conversation_history.append({
"role": "tool",
"tool_call_id": tool_call["id"],
"name": fn_name,
"content": json.dumps(tool_output)
})
else:
# LLM determined the task is complete
print("\n[Agent Completed Task]:", response["content"])
return response["content"]
print("\n[Terminated]: Agent reached maximum iteration threshold without finishing.")
7. Automated Self-Correction via Test Feedback
A hallmark of true agentic autonomy is closed-loop self-correction. When human engineers write a fix that causes tests to fail, they inspect the stack trace, formulate a revised hypothesis, and apply a secondary edit.
Failure Feedback Scenario
Suppose the agent updates pricing_service.py to fix the Diwali discount from 0.05 to 0.10, but runs the test suite and receives:
AssertionError: Expected order total for cart #1042 to equal $90.00, received $95.00
assert calculate_total(cart) == 90.0
When this failure output is appended to the agent's context history:
- The LLM recognizes that the test calculation did not reflect the new discount rate.
- It queries where
calculate_total()is defined usingsearch_file(). - It discovers that a downstream caching layer (
cache_service.py) was memoizing the old discount configuration. - It updates the caching logic accordingly, re-runs
run_tests(), and verifies that all assertions pass.
8. Real-World Engineering Realities: Scaling Beyond Toy Implementations
While the 5-tool architecture forms the fundamental foundation, production-grade agents (such as Claude Code, GitHub Copilot Workspace, or Devin) incorporate several critical infrastructure layers:
1. Execution Isolation & Sandboxing
Allowing an LLM to execute arbitrary shell commands (subprocess.run(shell=True)) on a host machine poses severe security risks. A rogue or hallucinated command (e.g., rm -rf / or unintended network requests) can compromise local environments. Production architectures execute code exclusively inside ephemeral, locked-down Docker containers or microVMs (e.g., Firecracker) with restricted filesystem permissions and strict network policies.
2. AST-Based Parsing vs. Naive String Matching
Simple string matching (e.g., Python's in operator) cannot distinguish between a variable named discount inside an active function and the word "discount" inside a docstring or comment. Production agents utilize Abstract Syntax Tree (AST) parsers—such as Tree-sitter—to navigate code symbols, definitions, usages, and scope graphs semantically.
3. Model Context Protocol (MCP)
Modern developer environments standardize tool integrations using Anthropic's Model Context Protocol (MCP). Instead of hardcoding bespoke tool wrappers for every local environment, agents connect to standardized MCP servers that securely expose filesystem operations, GitHub issue tracking, database queries, and terminal sessions over a uniform protocol.
4. Context Budget Management and Token Pruning
Every search result and stack trace consumes tokens within the active context window. If an agent inspects 20 files sequentially, the context window fills up, increasing inference latency and API cost. Production agents continually prune their conversation history—summarizing past inspection steps, evicting stale file contents, and retaining only the current hypotheses, pending diffs, and latest test results.
9. Major Project Assignment: Build and Verify Your Coding Agent
Objective
Design and implement a functional CLI-based coding agent in Python that autonomously identifies and resolves an intentional bug in a local mock repository without human code editing.
Assignment Specifications & Deliverables
- Mock Codebase Setup:
- Create a local directory simulating an e-commerce backend with at least 3 distinct files (e.g.,
order_service.py,discount_calculator.py, andtest_suite.py). - Introduce an intentional bug: set a promotional discount calculation to
0.05instead of0.10. - Write a unit test in
test_suite.pythat fails due to this bug.
- Create a local directory simulating an e-commerce backend with at least 3 distinct files (e.g.,
- Implement the 5 Core Tools:
- File discovery (
search_codebase) - In-file line locator (
search_file) - Surgical line patcher (
replace_lines) - Interactive CLI prompt for human approval (
request_human_approval) - Subprocess test execution (
run_tests)
- File discovery (
- Agent Loop Construction:
- Implement the ReAct loop using an LLM of your choice (OpenAI GPT-4o, Anthropic Claude 3.5 Sonnet, or an open-weights model via Ollama).
- Provide the agent with the user task:
"Fix the Diwali promotion discount bug so that all unit tests pass."
- Verification Criteria:
- The agent must find the defect without loading the entire codebase into memory.
- The agent must prompt for human approval before modifying disk state.
- The agent must run the test suite and confirm an exit code of
0before exiting.
10. Summary and Key Takeaways
- Context Windows Do Not Scale to Monolithic Codebases: Pasting entire enterprise codebases into an LLM fails due to token limits, attention degradation, and high inference costs.
- The Intern Mental Model: Autonomous coding agents mirror how a junior engineer approaches a massive codebase: global search target inspection surgical modification human review test verification.
- Surgical Diffs Over Whole-File Rewrites: Agents must never rewrite full files to make localized changes. Targeted line replacements preserve file formatting, reduce token spend, and prevent regressions.
- Tool Primitives Form the Core Engine: An autonomous agent requires five foundational tools: codebase search, in-file line search, line replacement, human approval, and test suite execution.
- Closed-Loop Self-Healing: By executing test suites and feeding error traces back into the conversation history, agents can self-correct when initial fixes fail.
- Production Guardrails Are Non-Negotiable: Commercial implementations build upon this five-tool foundation by adding ephemeral container sandboxing, AST semantic indexing, Model Context Protocol (MCP) integrations, and active context compaction.