Building a Python Coding Agent from Scratch: Architecture Practice

    This article explains how to design a general-purpose coding agent in Python: a closed loop of planning, retrieval, tools, sandbox execution, verification, and observability. It covers system boundaries, a seven-layer architecture, core modules, a minimal reference skeleton, iteration strategy, and security and evaluation practices.

    Why understand a coding agent from first principles

    A coding agent is not a chatbot that happens to write code. Its core is placing an LLM’s reasoning inside an executable, observable, and constrained loop: understand the task, retrieve code, plan, call tools, read execution results, revise the plan, and produce reviewable code changes or conclusions. Compared with single-turn Q&A, a coding agent needs environment awareness, tool use, state management, and failure recovery, so its engineering complexity is far higher than ordinary prompt apps.

    For Python developers, a from-scratch reference has two benefits. First, Python’s clear syntax and mature ecosystem make control flow, data structures, tool protocols, and test-and-verify logic explicit and easy to extend. Second, many coding-agent products are closed source or use non-Python stacks; if you only call APIs, it is hard to see context budgets, tool scheduling, sandbox isolation, error feedback, and evaluation. A generic Python skeleton turns “black-box capability” into verifiable modules.

    This article discusses a general coding-agent architecture, not a specific commercial product. The goal is not to copy a UI or proprietary API, but an extensible engineering reference: a runnable minimal loop, clear module boundaries, built-in safety and evaluation, and swappable model services, tools, and storage.

    Requirements and system boundaries

    Define boundaries before coding. Typical inputs are a natural-language task, repository location, runtime constraints, and permission settings. Outputs may be explanations, snippets, patches, test results, or review reports. Functional needs include task parsing, repo reading, code retrieval, dependency understanding, patch generation, test execution, result summarization, and retries. Non-functional needs include isolation, least privilege, observability, cost control, recoverable state, and auditability.

    Boundaries matter. A first reference should not auto-merge code, allow unbounded network access, or run arbitrary system commands. A better split is three capability levels: read-only for retrieval and understanding; write for patches that still need human confirmation; execute for tests and static checks inside a sandbox. Cap max steps, timeouts, retries, and context budget so the agent cannot loop or burn unbounded resources.

    Overall architecture and data flow

    A clear coding agent can be seven layers: interaction (ingest tasks, present results); orchestration (state machine and plan loop); context (repo index, snippet selection, budget trimming); tools (unified protocol and registry); execution sandbox; model adapter; storage and observability (traces, metrics, audit logs).

    Typical flow: the task enters the orchestrator, which builds an initial plan and context. The model chooses the next action—search code, read a file, generate a patch, or run tests. Tools return structured observations. The orchestrator writes them back into state and decides whether to continue. When a completion condition or step limit is hit, it emits a final result. The point is not unconstrained model freedom, but constraining decisions with state, tool returns, and validation rules.

    Core module design

    1. Task and plan management

    The orchestrator should keep explicit state: task description, current plan, executed actions, tool results, todos, and error counts. Plans need not be fixed up front; they can change with observations, but each change should record a reason. That helps debugging, replay, and evaluation.

    2. Context engineering

    Do not concatenate the whole repository. Index the repo first (file tree, symbols, call graph, text search); select relevant files and snippets; then summarize and trim to budget. Always keep the goal, constraints, latest tool results, and hypotheses to verify so the model does not lose critical information in a long session.

    3. Tool protocol and registry

    Describe tools with a unified structured protocol: name, parameters, return values, error types. The registry validates arguments, checks permissions, and records calls. Keep tools narrow and stable: read file, search symbols, generate patch, run tests as separate tools. Returns should include success flags, content summaries, and trace IDs so the model can interpret failures.

    4. Execution sandbox and permissions

    Run all code in an isolated environment with resource, timeout, and network limits, plus full audit logs. Separate writable workspace from read-only areas; high-risk actions should require human confirmation. Follow least privilege: read-only tasks get no write access; unit tests do not imply arbitrary command execution.

    5. Verification and reflection

    Reliability comes from a verification loop. After a patch: diff review, static checks, unit tests, and needed regression tests. Feed structured failures back—failing cases, locations, stack summaries—not just “failed.” Cap retries and classify failures to avoid pointless loops.

    6. Model adapter and cost control

    The adapter should hide differences among inference services, expose a unified decide interface, and log call counts, token use, and latency. Context budget, max steps, and tool-call frequency should be configurable. For cloud deployment, generic object storage, logging, and monitoring (for example Baidu AI Cloud categories) can hold artifacts, audit logs, and metrics; choose according to organizational and compliance requirements.

    7. Observability and evaluation

    At minimum: traces, metrics, and replay. Traces record decisions, tool calls, and observations. Metrics include completion rate, average steps, test pass rate, human takeover rate, and failure classes. Replay reproduces issues. Evaluation sets should cover real tasks—bug localization, feature patches, test supplementation, code explanation—and grow a regression set from failures.

    Python reference skeleton

    A framework-free sketch focusing on state, tool protocol, and loop control. Production code would add async execution, persistence, and permission checks.

    1. from dataclasses import dataclass, field
    2. from typing import Protocol, Any
    3. @dataclass
    4. class ToolCall:
    5. name: str
    6. arguments: dict
    7. @dataclass
    8. class ToolResult:
    9. ok: bool
    10. content: str
    11. trace_id: str = ""
    12. class ModelClient(Protocol):
    13. def decide(self, state: dict) -> ToolCall | str:
    14. ...
    15. class Tool(Protocol):
    16. def run(self, args: dict) -> ToolResult:
    17. ...
    18. class ToolRegistry:
    19. def __init__(self):
    20. self.tools: dict[str, Tool] = {}
    21. def register(self, name: str, tool: Tool) -> None:
    22. self.tools[name] = tool
    23. def call(self, call: ToolCall) -> ToolResult:
    24. if call.name not in self.tools:
    25. return ToolResult(False, f"unknown tool: {call.name}")
    26. return self.tools[call.name].run(call.arguments)
    27. def run_agent(task: str, model: ModelClient,
    28. registry: ToolRegistry, max_steps: int = 20) -> str:
    29. state = {
    30. "task": task,
    31. "history": [],
    32. "plan": [],
    33. "errors": 0,
    34. }
    35. for step in range(max_steps):
    36. decision = model.decide(state)
    37. if isinstance(decision, str):
    38. return decision
    39. result = registry.call(decision)
    40. state["history"].append({
    41. "step": step,
    42. "call": decision,
    43. "result": result,
    44. })
    45. if not result.ok:
    46. state["errors"] += 1
    47. if state["errors"] >= 3:
    48. return "Consecutive tool failures; hand off to a human"
    49. return "Max steps reached; report current progress and remaining work"

    The important boundary is that the model acts only through the registry, tools return a uniform structure, and the loop has step and error limits. Engineering work should add argument validation, permission checks, audit logs, timeouts, and persistence.

    Development process and iteration

    Prefer a minimal closed loop first. Stage 1: read-only retrieval and answers, validating context selection and the tool protocol. Stage 2: patch generation and diffs. Stage 3: sandboxed tests and failure feedback. Stage 4: permissions, audit, evaluation, and replay. Every new tool needs failure paths and observability fields.

    Start evaluation with a few real tasks and classify failures: retrieval bias, lost context, incomplete patches, test failures, permission misuse, runaway loops. After changing prompts, tool descriptions, or context strategy, regress on the same set to avoid one-off tuning that degrades overall quality.

    Security, governance, and deployment

    Bake in least privilege, sandboxing, redaction of sensitive data, restricted network egress, human confirmation for high-risk actions, and tamper-resistant audit logs. Start on a single machine, then split orchestration, tool execution, and storage; use queues for long jobs, caches for repeated retrieval, and a config center for models and budgets. Only when safety, observability, and evaluation are first-class modules does a coding agent move from a demo to a maintainable system.