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:

    1. Perception – Reads user input (natural language or code snippets), parses the project file structure, and extracts relevant context.
    2. Planning – Leverages LLM reasoning to break a complex programming request into an ordered sequence of executable subtasks and define an execution strategy.
    3. Execution – Invokes a toolchain (file I/O, code execution, terminal commands, search queries, etc.) to carry out each subtask.
    4. 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

    1. ┌─────────────────────────────────────────┐
    2. │ User Interface │
    3. │ (CLI / API / IDE Plugin) │
    4. └──────────────────┬──────────────────────┘
    5. ▼
    6. ┌─────────────────────────────────────────┐
    7. │ Task Parsing & Planning Module │
    8. │ (Prompt Engineering + LLM Calls) │
    9. └──────────────────┬──────────────────────┘
    10. ▼
    11. ┌─────────────────────────────────────────┐
    12. │ Tool Dispatch & Execution │
    13. │ (Tool Registry + Sandbox Runner + │
    14. │ Result Aggregator) │
    15. └──────────────────┬──────────────────────┘
    16. ▼
    17. ┌─────────────────────────────────────────┐
    18. │ Project Context Manager │
    19. │ (Filesystem Abstraction + Code Parser + │
    20. │ Index Cache) │
    21. └─────────────────────────────────────────┘

    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.

    1. import os
    2. from dataclasses import dataclass
    3. from typing import List, Dict
    4. @dataclass
    5. class FileContext:
    6. path: str
    7. content: str
    8. language: str
    9. class ProjectContext:
    10. def __init__(self, root_dir: str):
    11. self.root_dir = root_dir
    12. self.files: Dict[str, FileContext] = {}
    13. self._scan()
    14. def _scan(self):
    15. """Recursively scan the project directory to build a file index."""
    16. for dirpath, _, filenames in os.walk(self.root_dir):
    17. for fname in filenames:
    18. fpath = os.path.join(dirpath, fname)
    19. if self._is_source_file(fpath):
    20. with open(fpath, 'r', encoding='utf-8') as f:
    21. self.files[fpath] = FileContext(
    22. path=fpath,
    23. content=f.read(),
    24. language=self._detect_language(fname)
    25. )
    26. def _is_source_file(self, path: str) -> bool:
    27. return any(path.endswith(ext) for ext in ['.py', '.js', '.ts', '.java'])
    28. def _detect_language(self, filename: str) -> str:
    29. ext_map = {'.py': 'python', '.js': 'javascript', '.ts': 'typescript', '.java': 'java'}
    30. return ext_map.get(os.path.splitext(filename)[1], 'unknown')
    31. def get_relevant_files(self, query: str, top_k: int = 5) -> List[FileContext]:
    32. """Return relevant files based on simple keyword matching (replaceable with vector search)."""
    33. scored = []
    34. for ctx in self.files.values():
    35. score = sum(1 for word in query.lower().split() if word in ctx.content.lower())
    36. scored.append((score, ctx))
    37. scored.sort(reverse=True)
    38. 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.

    1. from abc import ABC, abstractmethod
    2. from typing import Any, Callable
    3. class Tool(ABC):
    4. @property
    5. @abstractmethod
    6. def name(self) -> str: ...
    7. @property
    8. @abstractmethod
    9. def description(self) -> str: ...
    10. @abstractmethod
    11. def execute(self, **kwargs) -> Any: ...
    12. class ToolRegistry:
    13. def __init__(self):
    14. self._tools: Dict[str, Tool] = {}
    15. def register(self, tool: Tool):
    16. self._tools[tool.name] = tool
    17. def get_tool(self, name: str) -> Tool:
    18. return self._tools[name]
    19. def list_tools(self) -> List[Dict[str, str]]:
    20. return [{"name": t.name, "description": t.description} for t in self._tools.values()]
    21. # Example: file read/write tools
    22. class ReadFileTool(Tool):
    23. @property
    24. def name(self): return "read_file"
    25. @property
    26. def description(self): return "Read the content of a file at the given path"
    27. def execute(self, file_path: str) -> str:
    28. with open(file_path, 'r', encoding='utf-8') as f:
    29. return f.read()
    30. class WriteFileTool(Tool):
    31. @property
    32. def name(self): return "write_file"
    33. @property
    34. def description(self): return "Write content to a file at the given path"
    35. def execute(self, file_path: str, content: str) -> str:
    36. os.makedirs(os.path.dirname(file_path), exist_ok=True)
    37. with open(file_path, 'w', encoding='utf-8') as f:
    38. f.write(content)
    39. 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.

    1. import subprocess
    2. import tempfile
    3. import shutil
    4. class SandboxExecutor:
    5. def __init__(self, timeout: int = 30, memory_limit: str = "256m"):
    6. self.timeout = timeout
    7. self.memory_limit = memory_limit
    8. def execute_python(self, code: str) -> Dict[str, Any]:
    9. """Execute Python code in a temporary directory, capturing stdout and stderr."""
    10. tmpdir = tempfile.mkdtemp()
    11. try:
    12. script_path = os.path.join(tmpdir, "task_script.py")
    13. with open(script_path, 'w') as f:
    14. f.write(code)
    15. result = subprocess.run(
    16. ["python3", script_path],
    17. capture_output=True,
    18. text=True,
    19. timeout=self.timeout,
    20. cwd=tmpdir
    21. )
    22. return {
    23. "stdout": result.stdout,
    24. "stderr": result.stderr,
    25. "return_code": result.returncode,
    26. "success": result.returncode == 0
    27. }
    28. except subprocess.TimeoutExpired:
    29. return {"stdout": "", "stderr": "Execution timed out", "success": False}
    30. finally:
    31. 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.

    1. import json
    2. class TaskPlanner:
    3. def __init__(self, llm_client):
    4. """
    5. llm_client: an object wrapping LLM API calls.
    6. It must accept a list of messages and return a text response.
    7. """
    8. self.llm = llm_client
    9. self.system_prompt = """You are a code agent assistant. Your responsibilities are:
    10. 1. Analyze the user's programming request;
    11. 2. Break it down into concrete subtasks;
    12. 3. Select appropriate execution steps based on available tools;
    13. 4. Output a task plan in JSON format with these fields:
    14. - "goal": the overall objective
    15. - "steps": [{"step_id": int, "action": str, "tool": str, "params": dict}]"""
    16. def plan(self, user_input: str, available_tools: list) -> dict:
    17. messages = [
    18. {"role": "system", "content": self.system_prompt},
    19. {"role": "user", "content": f"User request: {user_input}\nAvailable tools: {json.dumps(available_tools, ensure_ascii=False)}"}
    20. ]
    21. response = self.llm.chat(messages)
    22. try:
    23. plan = json.loads(response)
    24. return plan
    25. except json.JSONDecodeError:
    26. # Fallback: return a simple plan
    27. return {"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:

    1. Initialize project context → Initialize tool registry → Load sandbox executor
    2. Loop:
    3. Receive user input
    4. Call the planner to generate a task plan
    5. For each subtask:
    6. Retrieve the corresponding tool from the registry
    7. Call tool.execute(params)
    8. If code execution is involved → invoke the sandbox executor
    9. Collect execution results
    10. Summarize results and present to the user
    11. If the user requests continuation → next iteration
    12. If 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

    1. 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.
    2. Vectorized project indexing – Use a vector database to semantically index code snippets, improving context retrieval in large codebases.
    3. Test-driven validation – Auto-generate unit tests for produced code and run them in the sandbox, creating a generate → verify → fix quality loop.
    4. 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.
    5. 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.