Week 6 Project: Building an Autonomous Coding Agent
An autonomous coding agent is an LLM-driven program equipped with specific filesystem tools that allow it to locate, inspect, modify, and verify codebases based on natural language bug reports. By building an agent from scratch in Python 3.11 without heavy orchestration frameworks like LangGraph, we can clearly understand the core mechanics of tool calling, deterministic state loops, and bounded retry protocols.
1. Why Build an Autonomous Coding Agent?
Traditional software development requires engineers to manually read error logs, scan repository directories, pinpoint problematic lines, write fixes, and re-run test suites. While general-purpose chat interfaces can suggest code snippets, they lack situational context:
- They cannot inspect the local filesystem directly.
- They cannot locate the specific files where an error originates.
- They cannot autonomously apply surgical diffs or line replacements.
- They lack a closed-loop feedback mechanism to verify if an edit resolves the problem.
A Coding Agent bridges this gap. It turns a Large Language Model (LLM) into an active runtime operator that receives a high-level bug report in plain English, formulates an execution plan, invokes filesystem tools to discover and patch the code, and reports the resolution.
2. The Mental Model: The Autonomous Workshop Assistant
Real-Life Analogy
Imagine hiring an apprentice mechanic to fix a strange clicking sound in a car engine. You do not hand them a completely disassembled engine. Instead:
- You describe the symptom: "There is a knocking sound on cold starts."
- The mechanic grabs an inventory catalog to identify the likely assembly (File Search).
- They open the hood and read the gauge at a specific valve (File Inspection with Line Numbers).
- They swap out only the worn gasket, rather than rebuilding the entire engine block from scratch (Line Replacement Tool).
- If the knock persists, they try again, up to a strict limit of 3 attempts before asking a senior engineer for help (Bounded Retry Loop).
3. Architecture: The "No-Framework" Philosophy
While frameworks like LangChain and LangGraph offer pre-built abstractions, they often obscure the underlying state machine, introduce breaking dependency changes, and inflate token overhead. Building an agent using native Python 3.11 and the Groq SDK provides significant architectural benefits:
- Zero Abstraction Overhead: Pure Python control loops (
whileloops, dictionaries, and conditional statements) make debugging transparent. - Deterministic Control: Exact handling of tool definitions, error returns, and token payloads without hidden framework prompts.
- High-Speed Inference: Groq's LPU (Language Processing Unit) architecture drastically reduces latency, enabling iterative tool-calling loops to complete in seconds rather than minutes.
- Bounded Execution: Hardcoded guardrails prevent infinite execution loops by enforcing a maximum retry ceiling ().
4. Core Tooling Primitives
To repair bugs without hallucinating changes or rewriting entire repositories, an agent requires three fundamental primitives:
Tool 1: search_files
Scans the current working directory or subdirectories for files matching specific extensions or keywords related to the bug.
Tool 2: read_file_with_lines
Reads the target file and returns its content with explicit line numbers prepended. This allows the LLM to understand precise spatial coordinates (e.g., line 18) before formulating a replacement.
Tool 3: replace_line
Performs surgical line-level edits. Replacing entire files often leads to hallucinated omissions or truncated code; replacing single lines preserves file integrity.
import os
from typing import Dict, Any
def search_files(extension: str = ".cpp") -> str:
"""Searches directory for relevant source code files."""
matches = []
for root, _, files in os.walk("."):
for file in files:
if file.endswith(extension):
matches.append(os.path.join(root, file))
return "\n".join(matches) if matches else "No matching files found."
def read_file_with_lines(file_path: str) -> str:
"""Reads a file and returns contents formatted with 1-indexed line numbers."""
if not os.path.exists(file_path):
return f"Error: File '{file_path}' does not exist."
with open(file_path, "r", encoding="utf-8") as f:
lines = f.readlines()
return "".join(f"{idx + 1}: {line}" for idx, line in enumerate(lines))
def replace_line(file_path: str, line_number: int, new_content: str) -> str:
"""Replaces a specific 1-indexed line in a file with new content."""
if not os.path.exists(file_path):
return f"Error: File '{file_path}' does not exist."
with open(file_path, "r", encoding="utf-8") as f:
lines = f.readlines()
if line_number < 1 or line_number > len(lines):
return f"Error: Line number {line_number} is out of bounds (1-{len(lines)})."
# Preserve trailing newline if not present in new_content
if not new_content.endswith("\n"):
new_content += "\n"
lines[line_number - 1] = new_content
with open(file_path, "w", encoding="utf-8") as f:
f.writelines(lines)
return f"Successfully replaced line {line_number} in '{file_path}'."
5. Case Study: The C++ Accumulate Overflow Bug
To validate the coding agent, we construct a representative systems programming bug involving std::accumulate and integer overflow.
The Buggy Source Code (main.cpp)
#include <iostream>
#include <vector>
#include <numeric>
// Function intended to compute the sum of large positive numbers
long long sum_array(const std::vector<long long>& arr) {
// BUG: The third argument '0' has type int (32-bit signed).
// std::accumulate uses the type of the initial value as the accumulator type!
// Even though arr contains long long and return type is long long,
// the summation overflows a 32-bit signed int during accumulation.
long long total = std::accumulate(arr.begin(), arr.end(), 0);
return total;
}
int main() {
// Array containing values that exceed 32-bit signed integer limits (~2.14 x 10^9)
std::vector<long long> data = {1000000000LL, 1500000000LL, 2000000000LL};
long long result = sum_array(data);
std::cout << "Sum: " << result << std::endl;
return 0;
}
The Root Cause Analysis
In C++, the signature of std::accumulate is:
template<class InputIt, class T>
T accumulate(InputIt first, InputIt last, T init);
The accumulator type is deduced strictly from the type of init:
- Passing
0sets (typically 32 bits, max value ). - When summing , 32-bit signed integer overflow occurs, wrapping the value into negative numbers via two's complement representation.
- The fix requires changing
0to0LL(setting ).
6. End-to-End Implementation of the Coding Agent
The following script implements the complete autonomous agent using Python 3.11 and the Groq API. It defines tool declarations, executes tool calls locally, maintains conversational context, and respects a 3-attempt safety retry limit.
import json
import os
from groq import Groq
# 1. Initialize client
client = Groq(api_key=os.environ.get("GROQ_API_KEY"))
MODEL_NAME = "llama-3.3-70b-versatile"
# 2. Tool Definitions for the LLM
tools = [
{
"type": "function",
"function": {
"name": "search_files",
"description": "Searches the project directory for source code files.",
"parameters": {
"type": "object",
"properties": {
"extension": {
"type": "string",
"description": "File extension to search for, e.g., '.cpp' or '.py'"
}
},
"required": ["extension"]
}
}
},
{
"type": "function",
"function": {
"name": "read_file_with_lines",
"description": "Reads file contents with 1-indexed line numbers.",
"parameters": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "Path to the target file."
}
},
"required": ["file_path"]
}
}
},
{
"type": "function",
"function": {
"name": "replace_line",
"description": "Replaces a specific line in a file with new code.",
"parameters": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "Path to the target file."
},
"line_number": {
"type": "integer",
"description": "Line number to replace (1-indexed)."
},
"new_content": {
"type": "string",
"description": "The replacement line content."
}
},
"required": ["file_path", "line_number", "new_content"]
}
}
}
]
# Map names to actual callables
TOOL_MAP = {
"search_files": search_files,
"read_file_with_lines": read_file_with_lines,
"replace_line": replace_line
}
def run_coding_agent(user_error_description: str):
messages = [
{
"role": "system",
"content": (
"You are an autonomous bug-fixing coding agent. "
"Your job is to find the cause of the reported error in the codebase and patch it. "
"Always inspect files before modifying them. Use replace_line to make surgical edits. "
"Once the fix is applied, provide a concise explanation."
)
},
{
"role": "user",
"content": user_error_description
}
]
MAX_RETRIES = 3
retry_count = 0
print("\n[Agent] Starting investigation...")
while retry_count < MAX_RETRIES:
# Request completion with tool availability
response = client.chat.completions.create(
model=MODEL_NAME,
messages=messages,
tools=tools,
tool_choice="auto"
)
response_message = response.choices[0].message
messages.append(response_message)
# Check if model opted to invoke a tool
if response_message.tool_calls:
for tool_call in response_message.tool_calls:
fn_name = tool_call.function.name
fn_args = json.loads(tool_call.function.arguments)
print(f"[Tool Call] Invoking '{fn_name}' with args: {fn_args}")
if fn_name in TOOL_MAP:
tool_output = TOOL_MAP[fn_name](**fn_args)
else:
tool_output = f"Error: Tool '{fn_name}' not implemented."
# Append execution output back into context
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"name": fn_name,
"content": str(tool_output)
})
else:
# Model produced final text without calling more tools
print("\n[Agent Resolution]:")
print(response_message.content)
return
retry_count += 1
print(f"\n[Agent Failure] Unable to resolve the issue within {MAX_RETRIES} iterations.")
if __name__ == "__main__":
user_bug = input("Describe the bug in plain English: ")
run_coding_agent(user_bug)
7. Complete Execution Trace
Below is the step-by-step trace of how the agent resolves the std::accumulate issue in main.cpp:
8. Common Agent Failure Modes and Mitigations
When building autonomous coding agents, predictable points of failure emerge in runtime environments:
-
Hallucinated Line Numbers:
- Symptom: Agent targets line 25 when the target statement is on line 18.
- Fix: Guarantee that the read tool injects literal line markers (
1:,2:) into the context.
-
Full File Truncation:
- Symptom: When asked to rewrite a file, models frequently output
// ... existing code ..., accidentally deleting hundreds of lines. - Fix: Restrict the agent to line-level diffs (
replace_line) or unified diff format parsers.
- Symptom: When asked to rewrite a file, models frequently output
-
Infinite Hallucination Loops:
- Symptom: Agent calls the same tool repeatedly with identical failing parameters.
- Fix: Impose a strict retry budget () and inject explicit tool failure warnings back into the conversational thread.
9. Architectural Comparison: Agent Frameworks vs. Native Loops
Summary and Key Takeaways
- Autonomous Agent Definition: An autonomous agent combines an LLM's reasoning engine with tools (search, read, write) executed inside an iterative state loop.
- Framework Overhead Elimination: Complex libraries like LangGraph are unnecessary for linear diagnostic workflows; a native Python
whileloop offers superior transparency, lower latency, and easier debugging. - The Surgical Edit Rule: Avoid having LLMs rewrite entire source files to prevent accidental truncation (
// ... rest of code); enforce line-based replacements viareplace_line. - Bounded Retry Ceiling: Always impose a strict retry limit () to prevent runaway execution costs or infinite loop traps when an agent fails to diagnose a bug.
- Context Injection with Indices: Providing line-numbered code (
1: int x = 0;) prevents spatial hallucinations and ensures edits land on the correct lines. - The Accumulator Edge Case: In C++,
std::accumulate(begin, end, 0)deduces anintaccumulator regardless of the container's element type, leading to silent 32-bit overflows unless explicitly typed with0LL. - Full Operational Observability: Log every tool invocation and parameter set to stdout (
[Tool Call] Invoking 'replace_line' with...) to maintain clear visibility during automated operations.