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:
- Parses the JSON arguments
- Looks up the corresponding callable in the registry
- Validates arguments against the schema
- Executes the tool safely
- 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:
- Compose prompt = system prompt + conversation history + tool schemas
- Call LLM → receive response potentially containing
function_call - If function call present: Parse JSON, dispatch via registry, execute tool, append result to context
- If no function call: Treat response as final output and return to user
- 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_toolto save the file - Call
bash_toolto runpytest - 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.pyimplements 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 viatool_registry.pyto become available to the LLM - Production resilience:
system_state.pypersists 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →