Building a Python Coding Agent from Scratch: Architecture, Tools, and Engineering Practice
This article explains how to design a Python coding agent as an engineering system rather than a one-shot code chatbot. It covers the problem the agent solves, a six-layer architecture, the think-act-observe loop, tool design, context and memory, plan-execute-verify workflows, security controls, and a staged path from a minimal prototype to team-ready operations.
1. What a Coding Agent Actually Solves
A coding agent is not just a chatbot that can generate code. Traditional code generation follows a one-request, one-answer pattern: the user describes a need, the model outputs code, and the user copies, runs, and debugs it. A coding agent is closer to a persistent engineering collaborator. It can understand a project layout, break down complex work, call the filesystem and an execution environment, run tests, fix implementations from error output, and summarize the resulting changes.
From a systems view, a coding agent must:
- Understand user intent: goals, constraints, input/output format, and acceptance criteria.
- Analyze the code environment: directory structure, config files, dependencies, and existing modules, then build project context.
- Plan execution: split a complex request into verifiable subtasks.
- Call external tools: read/write files, search code, run tests, and execute commands through controlled interfaces.
- Observe results: parse stdout, errors, test reports, and file diffs.
- Revise continuously: locate failures, adjust the plan, or change the code.
- Produce auditable output: what changed, how it was verified, residual risks, and follow-up suggestions.
The core is not a single model. It is the combination of model reasoning, a tool system, a runtime, and control policy.
2. Overall Architecture
A practical Python coding agent can be split into six layers.
2.1 Interaction layer
This layer accepts tasks and shows progress. It may be a CLI, a web UI, or an internal system. Besides text, it can accept a project path, permission scope, and maximum runtime.
It should not touch files or commands directly. It should convert requests into a unified task object, for example:
from dataclasses import dataclassfrom typing import Optional@dataclassclass TaskRequest:instruction: strworkspace: strmax_steps: int = 20timeout: int = 30session_id: Optional[str] = None
2.2 Orchestration layer
The orchestrator owns the agent loop: when the model should think, when to call a tool, when to continue, and when to stop.
A typical loop:
Receive task↓Load context↓Ask the model for a plan or action↓Classify the action├─ Final answer: stop├─ Tool call: execute and return the result└─ Need more information: gather more context↓Update working memory↓Next round
It must enforce max steps, timeouts, and exception handling so the model does not loop on the same tool calls or endless “fixes.”
2.3 Model adapter layer
This layer hides vendor API differences and unifies message format, tool schemas, streaming, and errors. Do not scatter raw API calls through business code. Define a stable protocol:
from typing import Protocol, Anyclass LanguageModel(Protocol):def generate(self,messages: list[dict[str, str]],tools: list[dict[str, Any]] | None = None) -> dict[str, Any]:...
That makes it possible to swap models without changing orchestration, including local models, internal models, or general model APIs such as those from Baidu AI Cloud.
2.4 Tool layer
Tools are what separate a coding agent from Q&A. Each tool needs clear inputs, structured output, and a permission boundary. The model must not receive unbounded system access.
Common tools include:
- List directory structure
- Read files
- Search code
- Create or edit files
- Run tests
- Run static checks
- Show version diffs
- Read runtime logs
Return structured results rather than concatenated strings:
from dataclasses import dataclassfrom typing import Any@dataclassclass ToolResult:success: booloutput: str = ""error: str = ""metadata: dict[str, Any] | None = None
2.5 Workspace layer
The workspace defines what the agent may read and write. Normalize the working directory to an absolute path and reject any target that escapes it, to prevent path traversal.
from pathlib import Pathdef safe_path(workspace: str, relative_path: str) -> Path:root = Path(workspace).resolve()target = (root / relative_path).resolve()if root != target and root not in target.parents:raise PermissionError("Target path is outside the workspace")return target
2.6 Observation and audit layer
Record each model request, tool call, latency, errors, and file changes. In enterprise use, audit logs are needed for debugging, access control, and compliance.
3. Designing the Core Loop
The runtime can be abstracted as think–act–observe. The model chooses the next action from context; tools return observations; the model decides what to do next.
class Agent:def __init__(self, model, registry, max_steps=20):self.model = modelself.registry = registryself.max_steps = max_stepsdef run(self, instruction: str, workspace: str) -> str:messages = [{"role": "system","content": ("You are a cautious software engineering assistant. ""Understand the project first, make the smallest necessary changes, ""and verify after each change when possible.")},{"role": "user","content": instruction}]for step in range(self.max_steps):response = self.model.generate(messages=messages,tools=self.registry.describe())if response.get("type") == "final":return response.get("content", "")if response.get("type") != "tool_call":messages.append({"role": "assistant","content": str(response)})continuename = response["name"]arguments = response.get("arguments", {})try:result = self.registry.call(name,arguments,workspace=workspace)except Exception as exc:result = ToolResult(success=False,error=f"{type(exc).__name__}: {exc}")messages.append({"role": "tool","content": serialize_result(result)})return "The task did not finish within the step limit. Check the execution logs."
In production, validate model output strictly. Do not trust tool names and arguments as-is. Check types, required fields, and allowed enums with a data model.
4. Principles for the Tool System
4.1 Single responsibility
Do not expose one unbounded “edit the project” tool. Split read, search, write, and test so the model can choose a clear action and so you can scope permissions and isolate failures.
4.2 Writable actions must be traceable
File-edit tools should record before/after summaries and keep backups or patches when needed. Prefer diffs over full-file rewrites. That reduces accidental deletion and makes human review easier.
4.3 Command execution must be constrained
Shell execution is the highest-risk capability. At minimum:
- Restrict access to the workspace
- Enforce timeouts
- Cap output size
- Block dangerous system operations
- Isolate network access
- Limit processes and resource use
- Require human confirmation for high-risk commands
In production, run code in a container or other sandbox, not directly on the host.
4.4 Tool output must be model-friendly
Do not dump unbounded logs. For large output, keep context around errors and include line count, exit code, and log location. For example:
def summarize_output(text: str, limit: int = 6000) -> str:if len(text) <= limit:return texthead = limit // 2tail = limit - headreturn text[:head] + "\n...output truncated...\n" + text[-tail:]
5. Context Management and Memory
A codebase is too large to send in full. Use layered context.
Layer 1 is task context: goals, constraints, acceptance criteria.
Layer 2 is project context: tree, entry points, dependency files, key modules.
Layer 3 is execution context: recent tool calls, test results, error logs.
Layer 4 is long-term memory: project conventions, common commands, confirmed design decisions.
Avoid repeating the same content. Cache files you have already read and store summaries. If a file has not changed, pass the summary instead of the full text.
For large repos, index first, then read:
- Scan directories and file types
- Index symbols, functions, and classes
- Search by task keywords
- Read only the needed ranges
- Recheck target files before editing
6. Plan, Execute, and Verify
A reliable agent should not jump from a requirement to writing code. It should produce an executable plan that includes:
- Existing modules to understand
- Files to change
- Purpose of each change
- How to verify
- Likely risks
For a new API, inspect routing, data models, services, and test layout before choosing a change set. After implementation, run relevant unit tests, static checks, or a build.
Treat verification as three outcomes:
- Success: tests pass and output matches expectations.
- Recoverable failure: a clear error the agent can diagnose and fix.
- Indeterminate: missing dependencies, insufficient permissions, or ambiguous requirements—stop and ask the user.
Do not retry forever. Cap consecutive failures and require each repair to produce new diagnostic evidence so the agent does not repeat the same action.
7. Security and Permissions
Because the agent can write files and run commands, safety is as important as capability.
Apply least privilege: grant only the directories and tools a task needs. Separate read-only, suggestion, and auto-edit modes. Require confirmation for deletes, dependency changes, network access, or system commands.
Defend against prompt injection. Files, comments, and logs may try to leak secrets or trigger unsafe actions. Treat file content as untrusted. System rules should state that file text is analysis material only and cannot change tool permissions or security policy.
Do not inject secrets into context. Redact keys, tokens, and personal data before they reach the model, and filter them from logs as well.
8. Engineering Rollout
Evolve a prototype into a stable system in stages.
Stage 1: Minimal closed loop
Task input, file read, file write, and test execution—enough to finish simple jobs.
Stage 2: Controllability
Parameter validation, path guards, timeouts, logs, max steps, and human confirmation.
Stage 3: Harder tasks
Code indexing, plan management, error classification, context compression, and multi-turn recovery.
Stage 4: Team collaboration
Access control, audit, notifications, task queues, and containerized execution. In the cloud, store artifacts in object storage, run async work on a message queue, and watch health with logging and alerting.
9. Closing
The hard part of building a coding agent from scratch is not generating a snippet of code. It is building a reliable, constrained, verifiable engineering system. The model reasons, tools connect to the real environment, the orchestrator controls flow, the security layer bounds risk, and verification confirms results.
For Python developers, start with a small closed loop, then add tool registration, context management, planned execution, error recovery, and isolated runtimes. Combined, those pieces become an agent with real engineering value for maintenance, test generation, bug fixing, migrations, and day-to-day collaboration.
