Building a Code Agent from Scratch: Core Architecture and Development in Python
This article breaks down the essential architecture of a code agent—a system that uses large language models to automatically generate, complete, and repair code. It covers the four foundational layers (perception, planning, execution, feedback), walks through functional and non-functional requirements, and provides a complete Python reference implementation including a project context manager, a tool registry, a sandboxed executor, and an LLM-driven task planner. Developers will gain a clear, hands-on understanding of how to build a secure, extensible code agent from the ground up.
1. Introduction: Why Understand a Code Agent from First Principles
As large language models continue to grow more capable, “code agents”—systems that automatically generate code, offer intelligent completions, and autonomously fix bugs—are becoming a staple of everyday developer workflows. Yet most practitioners interact with these tools at a surface level: call an API, wait for results. Few have a systematic understanding of how the agent perceives file context, decomposes complex programming tasks, or safely executes generated code.
This gap is especially acute for teams whose primary language is Python. Existing reference implementations are often closed-source or written in other languages, making it hard to study the underlying logic. This article fills that gap by dissecting the architecture of a code agent from a technology-agnostic perspective and delivering a full Python reference implementation that spans requirements definition through core module development.
2. Core Architecture Overview
A typical code agent system can be abstracted into four layers:
- Perception – Reads user input (natural language or code snippets), parses the project file structure, and extracts relevant context.
- Planning – Leverages LLM reasoning to break a complex programming request into an ordered sequence of executable subtasks and define an execution strategy.
- Execution – Invokes a toolchain (file I/O, code execution, terminal commands, search queries, etc.) to carry out each subtask.
- Feedback – Collects execution results, error messages, and user feedback to close the loop and inform the next decision cycle.
These layers communicate through a message bus or event-driven mechanism, forming a closed-loop system capable of perceive → think → act → reflect.
3. Requirements Analysis and System Design
3.1 Functional Requirements
- Accept natural language task descriptions (e.g., “add a user login endpoint to the current project”).
- Automatically scan the project directory and locate relevant source files.
- Generate code that conforms to project conventions and write it to specified files.
- Execute generated code in a sandboxed environment, capturing both output and exceptions.
- Support multi-turn interaction so users can refine and iterate on results.
3.2 Non-Functional Requirements
- Security – Code execution must be isolated from the host to prevent arbitrary system calls.
- Observability – Every decision and tool call must be logged for debugging and auditing.
- Extensibility – The tool registry must support dynamic addition of new capabilities (e.g., code search, dependency analysis).
- Fault tolerance – A single step failure should not crash the entire task; retry and fallback strategies are required.
3.3 Overall Architecture
┌─────────────────────────────────────────┐│ User Interface ││ (CLI / API / IDE Plugin) │└──────────────────┬──────────────────────┘▼┌─────────────────────────────────────────┐│ Task Parsing & Planning Module ││ (Prompt Engineering + LLM Calls) │└──────────────────┬──────────────────────┘▼┌─────────────────────────────────────────┐│ Tool Dispatch & Execution ││ (Tool Registry + Sandbox Runner + ││ Result Aggregator) │└──────────────────┬──────────────────────┘▼┌─────────────────────────────────────────┐│ Project Context Manager ││ (Filesystem Abstraction + Code Parser + ││ Index Cache) │└─────────────────────────────────────────┘
4. Core Module Implementation in Python
4.1 Project Context Manager
This module builds a structured representation of the project so the agent can “understand” the codebase.
import osfrom dataclasses import dataclassfrom typing import List, Dict@dataclassclass FileContext:path: strcontent: strlanguage: strclass ProjectContext:def __init__(self, root_dir: str):self.root_dir = root_dirself.files: Dict[str, FileContext] = {}self._scan()def _scan(self):"""Recursively scan the project directory to build a file index."""for dirpath, _, filenames in os.walk(self.root_dir):for fname in filenames:fpath = os.path.join(dirpath, fname)if self._is_source_file(fpath):with open(fpath, 'r', encoding='utf-8') as f:self.files[fpath] = FileContext(path=fpath,content=f.read(),language=self._detect_language(fname))def _is_source_file(self, path: str) -> bool:return any(path.endswith(ext) for ext in ['.py', '.js', '.ts', '.java'])def _detect_language(self, filename: str) -> str:ext_map = {'.py': 'python', '.js': 'javascript', '.ts': 'typescript', '.java': 'java'}return ext_map.get(os.path.splitext(filename)[1], 'unknown')def get_relevant_files(self, query: str, top_k: int = 5) -> List[FileContext]:"""Return relevant files based on simple keyword matching (replaceable with vector search)."""scored = []for ctx in self.files.values():score = sum(1 for word in query.lower().split() if word in ctx.content.lower())scored.append((score, ctx))scored.sort(reverse=True)return [ctx for _, ctx in scored[:top_k]]
The implementation indexes files via recursive traversal and provides keyword-based retrieval. In production, vector embeddings and semantic search can replace the simple scoring to improve recall.
4.2 Tool Registry and Dispatch Engine
Tools are the agent’s interface to the outside world. A unified registration mechanism makes it easy to extend the system.
from abc import ABC, abstractmethodfrom typing import Any, Callableclass Tool(ABC):@property@abstractmethoddef name(self) -> str: ...@property@abstractmethoddef description(self) -> str: ...@abstractmethoddef execute(self, **kwargs) -> Any: ...class ToolRegistry:def __init__(self):self._tools: Dict[str, Tool] = {}def register(self, tool: Tool):self._tools[tool.name] = tooldef get_tool(self, name: str) -> Tool:return self._tools[name]def list_tools(self) -> List[Dict[str, str]]:return [{"name": t.name, "description": t.description} for t in self._tools.values()]# Example: file read/write toolsclass ReadFileTool(Tool):@propertydef name(self): return "read_file"@propertydef description(self): return "Read the content of a file at the given path"def execute(self, file_path: str) -> str:with open(file_path, 'r', encoding='utf-8') as f:return f.read()class WriteFileTool(Tool):@propertydef name(self): return "write_file"@propertydef description(self): return "Write content to a file at the given path"def execute(self, file_path: str, content: str) -> str:os.makedirs(os.path.dirname(file_path), exist_ok=True)with open(file_path, 'w', encoding='utf-8') as f:f.write(content)return f"Successfully wrote {len(content)} chars to {file_path}"
A real system would also include a sandboxed code-execution tool, a terminal-command tool, a web-search tool, and so on. Each tool follows the same interface, enabling unified management and dynamic loading.
4.3 Sandboxed Executor
Code execution is the highest-risk operation in a code agent. Generated code must run in an isolated environment to protect the host.
import subprocessimport tempfileimport shutilclass SandboxExecutor:def __init__(self, timeout: int = 30, memory_limit: str = "256m"):self.timeout = timeoutself.memory_limit = memory_limitdef execute_python(self, code: str) -> Dict[str, Any]:"""Execute Python code in a temporary directory, capturing stdout and stderr."""tmpdir = tempfile.mkdtemp()try:script_path = os.path.join(tmpdir, "task_script.py")with open(script_path, 'w') as f:f.write(code)result = subprocess.run(["python3", script_path],capture_output=True,text=True,timeout=self.timeout,cwd=tmpdir)return {"stdout": result.stdout,"stderr": result.stderr,"return_code": result.returncode,"success": result.returncode == 0}except subprocess.TimeoutExpired:return {"stdout": "", "stderr": "Execution timed out", "success": False}finally:shutil.rmtree(tmpdir)
This implementation relies on temporary-directory isolation and process-level timeouts as baseline safety measures. For stricter security, consider containerized execution (e.g., lightweight containers provisioned via a container platform) or syscall interception (e.g., a sandboxing framework that restricts network and filesystem access).
4.4 Task Planner and LLM Interaction Module
The planner is the agent’s “brain.” It translates user intent into an executable task chain by crafting high-quality prompts and managing multi-turn context.
import jsonclass TaskPlanner:def __init__(self, llm_client):"""llm_client: an object wrapping LLM API calls.It must accept a list of messages and return a text response."""self.llm = llm_clientself.system_prompt = """You are a code agent assistant. Your responsibilities are:1. Analyze the user's programming request;2. Break it down into concrete subtasks;3. Select appropriate execution steps based on available tools;4. Output a task plan in JSON format with these fields:- "goal": the overall objective- "steps": [{"step_id": int, "action": str, "tool": str, "params": dict}]"""def plan(self, user_input: str, available_tools: list) -> dict:messages = [{"role": "system", "content": self.system_prompt},{"role": "user", "content": f"User request: {user_input}\nAvailable tools: {json.dumps(available_tools, ensure_ascii=False)}"}]response = self.llm.chat(messages)try:plan = json.loads(response)return planexcept json.JSONDecodeError:# Fallback: return a simple planreturn {"goal": user_input, "steps": [{"step_id": 1, "action": "explore", "tool": "read_file", "params": {}}]}
The module uses a system prompt to steer the model toward structured output that is easy to parse programmatically. In deployment, leverage a cloud provider’s LLM inference service for efficient model calls, and use the platform’s logging and monitoring capabilities to maintain observability across inference rounds.
5. Main Control Loop and Multi-Turn Interaction
With the modules above in place, the top-level loop follows this logic:
Initialize project context → Initialize tool registry → Load sandbox executorLoop:Receive user inputCall the planner to generate a task planFor each subtask:Retrieve the corresponding tool from the registryCall tool.execute(params)If code execution is involved → invoke the sandbox executorCollect execution resultsSummarize results and present to the userIf the user requests continuation → next iterationIf the user is satisfied or the task is complete → exit
A key design point: the context from each round (conversation history, generated code, execution outcomes) feeds into the next planning cycle, giving the agent a form of “memory” so it can refine its work based on prior results.
6. Security and Governance Practices
Security must be baked into the architecture:
- Input validation – Filter sensitive terms and detect malicious intent in natural language instructions to prevent dangerous operations.
- Least-privilege access – Restrict the sandbox to only the file paths it needs; block access to critical system directories.
- Audit logging – Persist every tool call’s inputs, outputs, and timestamps for post-hoc review. Centralized log management via a cloud platform’s logging service is a practical approach.
- Human-in-the-loop approval – Require manual confirmation for high-risk actions such as file deletion, external network requests, or database operations.
7. Extension Directions and Best Practices
- AST-based code analysis – Integrate abstract syntax tree parsing into the context manager so the agent can understand function signatures, class hierarchies, and deeper structural information.
- Vectorized project indexing – Use a vector database to semantically index code snippets, improving context retrieval in large codebases.
- Test-driven validation – Auto-generate unit tests for produced code and run them in the sandbox, creating a generate → verify → fix quality loop.
- Multi-agent collaboration – For complex scenarios, deploy specialized agents (e.g., a code-generation agent, a testing agent, a review agent) that work together, each focused on its role.
- Continuous evaluation – Maintain a benchmark dataset and regularly measure correctness, security, and style of generated code to drive ongoing prompt and model tuning.
8. Conclusion
Building a code agent from scratch is an achievable engineering effort. With clear architectural layering, modular tool design, a safe execution environment, and tight integration with LLM inference services, Python developers can construct their own intelligent programming assistant. The reference implementation in this article covers the full pipeline from requirements analysis through core module development. Readers can customize and extend it for their specific scenarios, moving from merely using code agents to truly mastering the technology.
