How to Build a Production-Grade Coding Agent with Tool Calling Capabilities

A production-grade coding agent combines an LLM provider, persistent state management, a tool registry with JSON schemas, and a ReAct loop to execute Python functions through structured tool calls.

Building a production-grade coding agent with tool calling capabilities requires more than connecting an LLM to an API. The bojieli/ai-agent-book repository demonstrates a complete, decoupled architecture based on the formula Agent = LLM + Context + Tools, enabling the agent to perform file manipulation, shell execution, and web searches through 17 pure-Python tools orchestrated via function calling.

Architecture Overview: Six Decoupled Layers

The coding agent in this repository follows a layered design where each component can be swapped or extended independently. Understanding these layers is essential before implementing your own agent.

Layer Responsibility Key Implementation
LLM Provider Connects to Anthropic, OpenAI, or OpenRouter APIs with streaming PROVIDERS.md
System Prompt & State Supplies static instructions, timestamps, and persistent JSON state system-prompt.md & system_state.py
Tool Registry Registers tools, maps LLM schemas to Python callables, enforces safety tool_registry.py
Tool Implementations Pure-Python utilities without external CLI dependencies tools/*.py
Agent Core Orchestrates the ReAct loop, parses tool calls, updates context agent.py & agent_new.py
Entry Point Loads configuration and executes user tasks main.py & quickstart.py

LLM Provider Abstraction

The agent abstracts the underlying model behind a provider interface documented in PROVIDERS.md. By default, the system uses the Anthropic claude-3-5-sonnet-20240620 model, but you can switch providers by modifying config.py or setting environment variables like ANTHROPIC_API_KEY or OPENAI_API_KEY.

The provider streams token-by-token output, which the agent consumes to build a thought-process log stored in the conversation context. This streaming approach enables real-time monitoring of the agent's reasoning before it commits to a tool call.

System Prompt and Persistent State

The system prompt resides in system-prompt.md and encodes the agent's persona, timestamp tracking, and TODO list management. It explicitly instructs the LLM on how to request tool calls using JSON schemas.

State persistence is handled by system_state.py, which saves the agent's internal state—including the current task, completed steps, and tool-call counters—to a JSON file. This design ensures the agent can resume after a crash without losing context.

Tool Registry and Schema Mapping

All callable tools are registered in tool_registry.py. This module builds a JSON schema that the LLM can understand (containing function names, descriptions, and parameters) and maps each schema entry to a concrete Python callable.

When the LLM returns a function_call block, the agent performs the following steps:

  1. Parses the JSON arguments
  2. Looks up the corresponding callable in the registry
  3. Validates arguments against the schema
  4. Executes the tool safely
  5. Injects the tool's result back into the conversation as a system message

Pure-Python Tool Implementations

The repository ships 17 fully implemented tools covering common development tasks. All implementations are pure Python to ensure cross-platform compatibility without requiring external binaries like ripgrep.

Tool Purpose File
write_tool Create or overwrite files tools/write_tool.py
read_tool Read file contents tools/read_tool.py
ls_tool List directory contents tools/ls_tool.py
grep_tool Pure-Python search matching ripgrep functionality tools/grep_tool.py
glob_tool Filename pattern matching tools/glob_tool.py
bash_tool Execute shell commands (sandboxed) tools/bash_tool.py
bash_output_tool Capture stdout/stderr tools/bash_output_tool.py
web_search_tool DuckDuckGo web search tools/web_search_tool.py
web_fetch_tool Retrieve raw URLs tools/web_fetch_tool.py
todo_write_tool Append to TODO files tools/todo_write_tool.py
edit_tool In-place regex editing tools/edit_tool.py
multi_edit_tool Batch edits across files tools/multi_edit_tool.py
notebook_edit_tool Manipulate Jupyter notebooks tools/notebook_edit_tool.py
shell_session Persistent interactive bash tools/shell_session.py
kill_bash_tool Terminate bash sessions tools/kill_bash_tool.py
exit_plan_mode_tool Switch to plan-only mode tools/exit_plan_mode_tool.py
task_tool High-level task orchestration tools/task_tool.py

Agent Core: The ReAct Loop

The reasoning and acting loop is implemented in agent.py (original) and agent_new.py (refactored with improved state handling). The workflow follows this deterministic sequence:

  1. Compose prompt = system prompt + conversation history + tool schemas
  2. Call LLM → receive response potentially containing function_call
  3. If function call present: Parse JSON, dispatch via registry, execute tool, append result to context
  4. If no function call: Treat response as final output and return to user
  5. Iterate until task completion or max-turn limit reached

This loop enables multi-step reasoning workflows such as "search codebase → read relevant files → edit implementation → run tests" without losing state between turns.

Entry Point and Configuration

The main.py file demonstrates how to wire components together:

from coding_agent.agent_new import CodingAgent
from coding_agent.config import Config

# Load configuration from environment variables

cfg = Config.load()
agent = CodingAgent(cfg)

# Execute a coding task

result = agent.run("Implement a Fibonacci function in Python")
print(result)

The quickstart.py script provides a ready-to-run demo showcasing end-to-end task completion.

Practical Implementation Examples

Instantiating the Agent

from coding_agent.agent_new import CodingAgent
from coding_agent.config import Config

# Load configuration (expects ANTHROPIC_API_KEY in .env)

cfg = Config.load()
agent = CodingAgent(cfg)

Running a Coding Task

task = """
Write a Python function `fib(n)` that returns the nth Fibonacci number.
Add a unit test for n=10.
"""
output = agent.run(task)

The agent will automatically:

  • Generate the function code
  • Call write_tool to save the file
  • Call bash_tool to run pytest
  • Return the test results

Direct Tool Invocation

For manual tool usage bypassing the LLM:

from coding_agent.tools.grep_tool import GrepTool

grep = GrepTool()
matches = grep.run({
    "path": "chapter5/coding-agent",
    "pattern": "def run"
})

Extending with Custom Tools

Create tools/markdown_formatter.py:

from typing import TypedDict
from coding_agent.base import BaseTool

class Args(TypedDict):
    text: str

class MarkdownFormatter(BaseTool):
    name = "markdown_formatter"
    description = "Format raw text into valid Markdown."
    args_schema = Args

    def run(self, args: Args) -> str:
        return f"# {args['text'].strip()}"

Register in tool_registry.py:

from .tools.markdown_formatter import MarkdownFormatter
registry.register(MarkdownFormatter())

Summary

  • Decoupled architecture: The agent separates LLM providers, state management, tool registries, and implementations into six independent layers
  • Pure-Python tools: All 17 tools avoid external CLI dependencies, ensuring portability across macOS, Linux, and Windows
  • ReAct loop: The core agent.py implements a deterministic loop that parses tool calls, executes them safely, and maintains conversation context
  • Extensibility: New tools inherit from BaseTool, define a TypedDict schema, and register via tool_registry.py to become available to the LLM
  • Production resilience: system_state.py persists state to JSON, enabling crash recovery and long-running task management

Frequently Asked Questions

What is the ReAct loop in coding agents?

The ReAct loop (Reasoning and Acting) is the core orchestration pattern in agent.py that alternates between LLM reasoning and tool execution. The agent receives a prompt, the LLM decides whether to make a tool call or provide a final answer, and the loop executes the tool and feeds results back into the context. This enables multi-step problem solving while maintaining a coherent conversation history.

How does the tool registry ensure type safety?

The tool_registry.py module enforces type safety by mapping each tool's JSON schema to a Python TypedDict. When the LLM returns a function call, the registry validates that the provided arguments match the expected types before invoking the tool's run method, preventing runtime errors from malformed LLM outputs.

Can I use models other than Claude?

Yes. The provider system documented in PROVIDERS.md supports Anthropic, OpenAI, and OpenRouter APIs. You configure your preferred provider by setting the appropriate environment variables (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.) and adjusting the model name in config.py without modifying the agent core logic.

How does the agent maintain context across long-running tasks?

The system_state.py module persists the conversation history, current task status, and TODO lists to a JSON file on disk. If the process crashes or restarts, the agent reloads this state and resumes from the last completed step, ensuring that long coding tasks don't lose progress due to interruptions.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →