Building a Python Code Agent from Scratch: Architecture and Implementation Guide
This article explains how to design and implement a Python-based code agent that can understand development requests, inspect project context, plan tasks, call controlled tools, modify code, and validate results. It covers the agent’s observe-think-act-verify loop, modular architecture, shared data structures, filesystem and command tools, model/tool protocol, main execution loop, context management, modification strategy, safety boundaries, testing and evaluation, and a staged path from a minimal prototype to a more capable system.
1. Why Understand Code Agents
As large language models enter the software development workflow, AI programming has evolved from simple code completion into code agents that can understand project structure, break down development tasks, call tools, modify files, and run tests. Unlike ordinary question-answering programs, a code agent is not a single-turn model that takes a question and outputs code. It is a closed-loop system built around software engineering tasks.
A mature code agent typically needs to do the following:
- Understand the user’s natural language request.
- Analyze the current project’s directory, dependencies, and code state.
- Break complex tasks into executable steps.
- Read files, search code, or run commands as required.
- Continuously revise its judgment based on tool results.
- Modify code and run tests.
- Summarize changes, risks, and follow-up recommendations.
Many developers can call model APIs, but they do not clearly understand how a complete agent controls flow, manages context, and ensures execution safety. By implementing a simplified but complete code agent from scratch, you can combine model capabilities, program logic, and software engineering practices, and build a holistic understanding of agent systems.
2. The Basic Operating Loop of a Code Agent
The core of a code agent can be abstracted into four stages: observe, think, act, and verify.
1. Observe: Collect Project Context
The agent first needs to understand the current environment, including:
- The project root directory;
- The file tree structure;
- Key configuration files;
- Source code related to the task;
- Test files and how to run them;
- The current version control state;
- The most recent tool execution result.
The observation stage cannot read every file without limit. When a project is large, blindly loading the full codebase creates excessive context, increases cost, and loses focus. The agent should therefore use a progressive acquisition strategy: first inspect directories and file summaries, then search for relevant content based on task keywords, and finally read only the necessary local code.
2. Think: Create a Task Plan
The model needs to convert a natural language request into a structured plan. For example, “add batch import functionality to the user module” can be broken down as follows:
- Locate the user data model;
- Find the existing service logic for creating users;
- Confirm data validation rules;
- Design the batch import interface;
- Add exception handling;
- Write unit tests;
- Run tests and fix issues.
The value of a plan is that it reduces无序 operations. For complex tasks, the agent should also record the completion status, dependencies, and failure reasons for each step, instead of deciding the next step only from the previous conversation turn each time.
3. Act: Call Controlled Tools
The agent cannot directly modify a project “out of thin air.” It needs to interact with the external environment through tools. Common tools include:
- Reading files;
- Writing files;
- Searching text;
- Listing directories;
- Running tests;
- Running static checks;
- Viewing version differences;
- Querying runtime logs.
Tool calls should use structured parameters rather than letting the model generate arbitrary commands. This makes programmatic validation easier and reduces the risk of accidental operations.
4. Verify: Confirm Whether the Result Is Reliable
After code is modified, the task is not over. The agent needs to verify at least the following:
- Whether the code can be parsed correctly;
- Whether unit tests pass;
- Whether key functionality meets the requirement;
- Whether obvious type or dependency issues have been introduced;
- Whether the scope of changes exceeds expectations;
- Whether sensitive files or content that should not be committed are present.
Verification results should be fed back to the model. If tests fail, the agent needs to analyze the error message, locate the cause, generate a fix plan, and verify again until a stopping condition is reached.
3. Overall Architecture Design
A maintainable Python code agent can be divided into the following modules:
code_agent/├── main.py # Program entry point├── model.py # Model call wrapper├── agent.py # Main loop and state management├── planner.py # Task planning├── context.py # Context collection and compression├── tools/│ ├── filesystem.py # File read/write tools│ ├── search.py # Code search tools│ └── executor.py # Command execution tools├── safety.py # Permission and risk control└── validator.py # Test and result validation
This layered design has three advantages.
First, model calls are separated from business flow. When changing model services in the future, there is no need to rewrite tools and state management logic.
Second, tool responsibilities are clear. File operations, search operations, and command execution are maintained separately, which makes testing and auditing easier.
Third, it is easier to add security policies. All high-risk operations can be intercepted uniformly at the tool layer instead of relying on prompt constraints.
4. Define Unified Data Structures
Modules in the agent should pass information using explicit data structures. Data classes can be used to represent task state:
from dataclasses import dataclass, fieldfrom typing import Any@dataclassclass TaskState:request: strplan: list[str] = field(default_factory=list)completed_steps: list[str] = field(default_factory=list)observations: list[str] = field(default_factory=list)tool_results: list[dict[str, Any]] = field(default_factory=list)changed_files: list[str] = field(default_factory=list)errors: list[str] = field(default_factory=list)status: str = "pending"
Tool calls should also have a unified format:
@dataclassclass ToolCall:name: strarguments: dict[str, Any]@dataclassclass ToolResult:success: booloutput: str = ""error: str = ""
Unified data structures prevent different modules from using inconsistent fields and make it easier to save task logs, resume interrupted tasks, and perform automated testing.
5. Design the Tool Layer
1. File Reading Tool
The file reading tool needs to restrict access scope to prevent the agent from reading sensitive files outside the project.
from pathlib import Pathclass FileManager:def __init__(self, workspace: str):self.workspace = Path(workspace).resolve()def _safe_path(self, relative_path: str) -> Path:target = (self.workspace / relative_path).resolve()if self.workspace not in target.parents and target != self.workspace:raise PermissionError("Access to paths outside the workspace is prohibited")return targetdef read(self, relative_path: str, max_chars: int = 20000) -> str:path = self._safe_path(relative_path)if not path.is_file():raise FileNotFoundError(relative_path)return path.read_text(encoding="utf-8")[:max_chars]def write(self, relative_path: str, content: str) -> None:path = self._safe_path(relative_path)path.parent.mkdir(parents=True, exist_ok=True)path.write_text(content, encoding="utf-8")
Here, max_chars limits the size of a single read. A production system can also add binary file detection, sensitive file filtering, and file size limits.
2. Text Search Tool
Code agents often need to locate code based on function names, class names, or error messages. A simple recursive search can provide the basic capability first:
def search_text(root: Path, keyword: str) -> list[str]:results = []for path in root.rglob("*"):if not path.is_file():continueif path.suffix not in {".py", ".js", ".ts", ".java", ".go", ".json", ".yaml"}:continuetry:text = path.read_text(encoding="utf-8")except UnicodeDecodeError:continuefor number, line in enumerate(text.splitlines(), start=1):if keyword.lower() in line.lower():results.append(f"{path}:{number}: {line.strip()}")return results[:200]
In production, you can further add result pagination, regular expression restrictions, ignored directories, and binary detection.
3. Command Execution Tool
Command execution is the highest-risk tool. Implement it according to the principle of least privilege, and do not allow the model to execute arbitrary string commands directly.
import subprocessALLOWED_COMMANDS = {"python -m pytest","python -m compileall",}def run_command(command: str, cwd: str, timeout: int = 60) -> ToolResult:if command not in ALLOWED_COMMANDS:return ToolResult(False, error="Command did not pass the security policy")try:completed = subprocess.run(command.split(),cwd=cwd,capture_output=True,text=True,timeout=timeout,)output = completed.stdout + completed.stderrreturn ToolResult(success=completed.returncode == 0,output=output,error="" if completed.returncode == 0 else "Command execution failed",)except subprocess.TimeoutExpired:return ToolResult(False, error="Command execution timed out")
A more complete implementation should use containers or isolated sandboxes to restrict network access, file permissions, process resources, and runtime duration. High-risk actions such as deleting files, modifying configurations, and uploading data should require human confirmation.
6. Model Calls and Tool Protocol
The responsibility of the model call layer is not to orchestrate the entire flow, but to complete two types of work:
- Generate a plan or next action based on context;
- Adjust the plan and generate the final explanation based on tool results.
To enable the model to call tools, clear tool descriptions must be defined. For example:
TOOLS = [{"name": "read_file","description": "Read a text file within the workspace","parameters": {"type": "object","properties": {"path": {"type": "string"},"max_chars": {"type": "integer"}},"required": ["path"]}},{"name": "search_text","description": "Search for a keyword in project source code","parameters": {"type": "object","properties": {"keyword": {"type": "string"}},"required": ["keyword"]}}]
Model output cannot be trusted directly. The system must perform secondary validation of tool names, parameter types, path scope, and parameter lengths. Even if the prompt already states the rules, program-side validation cannot be skipped.
7. Implement the Agent Main Loop
The main loop is the core of a code agent. It must continuously process model decisions and tool results until the task is complete or the maximum number of loop iterations is reached.
class CodeAgent:def __init__(self, model, tools, max_steps: int = 20):self.model = modelself.tools = toolsself.max_steps = max_stepsdef run(self, request: str) -> str:messages = [{"role": "system","content": ("You are a code agent. Analyze the task first, then call tools carefully. ""All modifications must explain the reason and, when possible, run validation."),},{"role": "user", "content": request},]for _ in range(self.max_steps):response = self.model.generate(messages, tools=self.tools)if response.get("type") == "final":return response.get("content", "")if response.get("type") != "tool_call":return "The model returned an unrecognized format"tool_name = response["name"]arguments = response.get("arguments", {})result = self.dispatch(tool_name, arguments)messages.append({"role": "assistant", "content": response})messages.append({"role": "tool","name": tool_name,"content": result.output if result.success else result.error,})return "The task reached the maximum number of execution steps and stopped"def dispatch(self, name: str, arguments: dict):tool = self.tools.get(name)if tool is None:return ToolResult(False, error=f"Unknown tool: {name}")try:return tool(**arguments)except Exception as exc:return ToolResult(False, error=f"Tool execution exception: {exc}")
The main loop needs explicit stopping conditions, including:
- The model returns a final result;
- The maximum number of steps is reached;
- Multiple consecutive tool calls fail;
- A high-risk action is detected;
- The user actively cancels the task;
- The budget or time limit is exceeded.
An agent without stopping conditions may fall into repeated searches, repeated modifications, or infinite execution.
8. Context Management Is Critical to Success
Code projects are usually much larger than a model’s context window, so context management strategies must be designed.
1. Relevance First
Do not pass the entire project to the model. Instead, prioritize:
- Files explicitly mentioned by the user;
- Code snippets corresponding to search results;
- The most recent error message;
- The current task plan;
- Diff content of modified files.
2. Combine Summaries with Original Text
For longer files, first generate a structural summary covering classes, functions, dependencies, and key configuration. Only when the model needs to make further modifications should it read the code around specific functions.
3. Preserve State and Compress History
Multiple rounds of tool calls quickly increase the number of messages. The system can compress earlier processes into summaries, for example:
Completed:1. Located the user service module;2. Confirmed that the existing interface supports only single-record creation;3. The data validation function is in validation.py.Current issue:Batch import needs to reuse the existing validation logic and add failure records.
This preserves decision basis while reducing irrelevant history.
9. Code Modification Strategy
A code agent should not rewrite an entire file by default. Safer modification methods include:
- Small, localized patches;
- Point replacements based on functions or classes;
- Saving the original content before modification;
- Generating a diff after modification;
- Requiring human confirmation when the diff is too large.
The modification result needs to show the user:
- Which files were modified;
- What was completed in each file;
- Whether dependencies were added;
- Test execution results;
- Unresolved issues;
- Possible compatibility risks.
A transparent change explanation is more valuable than simply telling the user that the task is “done.”
10. Security Control and Engineering Boundaries
A code agent has file and command permissions, so security design cannot be treated as an add-on feature.
File Security
- Restrict the workspace scope;
- Prohibit access to sensitive files such as keys and credentials;
- Limit the size of individual reads and writes;
- Prevent path traversal;
- Require confirmation for deletion and overwrite operations.
Command Security
- Use a command allowlist;
- Disable network access by default;
- Set timeouts and resource limits;
- Isolate the runtime environment;
- Log commands, parameters, and output.
Data Security
- Do not send sensitive configuration directly to the model;
- Redact logs and error messages;
- Avoid injecting complete environment variables into context;
- Protect against prompt injection from external input.
Human-AI Collaboration
For tasks involving production environments, database schemas, dependency upgrades, permission configuration, and large-scale refactoring, use a human approval mechanism. Agents are suitable for analysis, generation, and validation work, but the final responsibility boundary must be clear.
11. Testing and Evaluation System
Evaluating a code agent should not focus only on whether it can generate code. It should also examine the entire task loop.
A test set can be built around the following dimensions:
- Whether it correctly understands the requirement;
- Whether it finds the relevant files;
- Whether tool parameters are correct;
- Whether it follows permission restrictions;
- Whether modifications meet expectations;
- Whether tests are executed correctly;
- Whether it can recover after failure;
- Whether it produces unrelated changes;
- Whether the final explanation is complete and accurate.
Test tasks should cover scenarios such as simple modifications, cross-file refactoring, bug fixes, test additions, and requirement clarification. For each task, save the input, tool trace, modification diff, and final result to support regression analysis.
12. Expand Gradually from a Minimal Prototype
It is recommended to implement the system in the following order:
Phase 1: Support only reading files, searching text, and generating analysis results, with a focus on validating communication between the model and tools.
Phase 2: Add file writing and diff display so the agent gains controlled modification capabilities.
Phase 3: Add test execution, error feedback, and automatic fixes to form a “modify-validate-correct” loop.
Phase 4: Introduce task planning, context compression, permission approval, and execution logs.
Phase 5: Then consider advanced capabilities such as parallel multi-task execution, project knowledge indexing, long-term memory, and team collaboration.
This incremental path controls complexity and avoids building a large system that is difficult to debug from the start.
Conclusion
The essence of a code agent is not simply connecting a large language model to an editor. It is combining model reasoning with tool calls, project context, execution environment, security policies, and validation mechanisms into a controllable closed loop. Python is suitable for implementing this process because it has clear syntax, a rich engineering ecosystem, and low prototyping cost.
When building a code agent from scratch, the most important goal is not to generate all the code at once. It is to establish a reliable workflow: understand the task first, then collect evidence; plan steps first, then execute actions; after modifying code, always validate the result; when failures occur, preserve context and recover safely.
After mastering these basic methods, developers can not only build a working intelligent programming assistant, but also further understand the general design principles of agent systems in software engineering, automated operations, test generation, and development collaboration.
