Part 2: The coding agent
In Part 1 we wrote the agentic loops ourselves. Now we port the same idea to the ToyAIKit framework, which handles the inner and outer loops for us. We also group the tools into a class, add the full five-tool set, and run the agent against a Django project template.
ToyAIKit framework
Install ToyAIKit:
uv add toyaikit
ToyAIKit replaces the manual loops from Part 1 with a runner that does the same thing. Register the two tools we already have:
from toyaikit.tools import Tools
from toyaikit.chat import IPythonChatInterface
from toyaikit.llm import OpenAIClient
from toyaikit.chat.runners import OpenAIResponsesRunner
tools_obj = Tools()
tools_obj.add_tool(see_file_tree, see_file_tree_description)
tools_obj.add_tool(read_file, read_file_description)
Create the runner:
chat_interface = IPythonChatInterface()
llm_client = OpenAIClient(
client=openai_client,
model=model
)
runner = OpenAIResponsesRunner(
tools=tools_obj,
developer_prompt=system_prompt,
chat_interface=chat_interface,
llm_client=llm_client
)
Run it:
results = runner.run()
Type stop to end the chat loop. The agent behavior did not change - we only
replaced our manual loops with a framework that does the same thing.
Tool definitions from docstrings
In Part 1 we maintained function schemas by hand (see_file_tree_description,
read_file_description). That works for two tools, but becomes tedious with
more.
With ToyAIKit, if we provide type hints and docstrings, the framework infers the tool definitions automatically. Rewrite the functions:
def see_file_tree(root_dir: str = ".") -> list[str]:
"""List files and directories in the current project.
Args:
root_dir: Directory to list. Defaults to the current directory.
"""
tree = []
for dirpath, dirnames, filenames in os.walk(root_dir):
dirnames[:] = [d for d in dirnames if d not in {".git", ".venv", "__pycache__", ".ipynb_checkpoints"}]
for name in sorted(dirnames + filenames):
full_path = os.path.join(dirpath, name)
tree.append(os.path.relpath(full_path, root_dir))
return tree
def read_file(filepath: str) -> str:
"""Read the contents of a file in the current project.
Args:
filepath: Path to the file to read.
"""
try:
if filepath == 'uv.lock':
return "you're not allowed to read this file"
with open(filepath, "r", encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
return f"Error: file '{filepath}' not found."
Now we register them without a separate schema:
tools_obj = Tools()
tools_obj.add_tool(see_file_tree)
tools_obj.add_tool(read_file)
No separate description dictionary is needed.
Grouping tools in a class
Several tools need shared state, especially the project directory they operate on. Instead of passing unrelated functions around, we can group them into a class. Start with a small class that contains the two tools we already have:
import os
from pathlib import Path
class ProjectTools:
def __init__(self, project_dir: Path):
self.project_dir = project_dir
def see_file_tree(self, root_dir: str = ".") -> list[str]:
"""List files and directories in the project.
Args:
root_dir: Directory to list relative to the project root.
"""
abs_root = self.project_dir / root_dir
tree = []
for dirpath, dirnames, filenames in os.walk(abs_root):
dirnames[:] = [d for d in dirnames if d not in {".git", ".venv", "__pycache__"}]
for name in sorted(dirnames + filenames):
full_path = os.path.join(dirpath, name)
tree.append(os.path.relpath(full_path, self.project_dir))
return tree
def read_file(self, filepath: str) -> str:
"""Read the contents of a file relative to the project root.
Args:
filepath: Path to the file to read.
"""
if 'uv.lock' in filepath:
return "you're not allowed to read this file"
abs_path = self.project_dir / filepath
try:
with open(abs_path, "r", encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
return f"Error: file '{filepath}' not found."
Initialize it for the current repository and register all methods at once:
project_tools = ProjectTools(Path("."))
tools_obj = Tools()
tools_obj.add_tools(project_tools)
Both tools share the same project root, both live in one place, and ToyAIKit infers schemas from the method signatures and docstrings.
The full AgentTools class
For the actual coding agent, we want all five tools. A ready implementation is in the coding-agent repo. Download a local copy:
wget https://raw.githubusercontent.com/alexeygrigorev/workshops/refs/heads/main/coding-agent/tools.py
The five tools this class provides:
read_file- read a file relative to the project directorywrite_file- write a file, creating missing directories if neededsee_file_tree- list project files, skipping.gitand.venvexecute_bash_command- run shell commands in the project directorysearch_in_files- search for text inside project files
Use it in the current repository:
import tools
agent_tools = tools.AgentTools(Path("."))
tools_obj = Tools()
tools_obj.add_tools(agent_tools)
The Django template
We do not want the agent to work in an unconstrained environment. If we just say "build me an app" with no structure, the agent has to invent the framework, the file layout, the organization, and how to verify the result. That makes the task slower and less reliable.
A template gives the agent constraints: the file tree is already there, the framework is chosen, and there is a known place for HTML, views, settings, and URLs. The agent spends less time inventing structure and more time modifying the existing project correctly.
Clone the prepared Django template:
git clone https://github.com/alexeygrigorev/django_template.git
If you want to verify the template works before using it with the agent:
cd django_template
uv sync
make migrate
make run
Create a helper that copies the template into a new project folder:
import os
import shutil
def start(project_name):
if not project_name:
print("Project name cannot be empty.")
return
if os.path.exists(project_name):
print(f"Directory '{project_name}' already exists.")
return
shutil.copytree("django_template", project_name)
print(f"Django template copied to '{project_name}' directory.")
return project_name
Use it:
project_name = input("Enter the new Django project name: ").strip()
start(project_name)
Now point the same tool class at the new Django project:
project_path = Path(project_name)
agent_tools = tools.AgentTools(project_path)
The detailed agent prompt
To make the agent work well, we need a stronger prompt than the exploratory one from Part 1. The prompt describes the Django template structure, tells the agent what framework and tools are available, and gives specific constraints:
django_agent_prompt = """
You are a coding agent. Your task is to modify the provided Django project template
according to user instructions. You don't tell the user what to do; you do it yourself using the
available tools. First, think about the sequence of steps you will do, and then
execute the sequence.
Always ensure changes are consistent with Django best practices and the project's structure.
## Project Overview
The project is a Django 5.2.4 web application scaffolded with standard best practices. It uses:
- Python 3.12+
- Django 5.2.4 (as specified in pyproject.toml)
- uv for Python environment and dependency management
- SQLite as the default database (see settings.py)
- Standard Django apps and a custom app called myapp
- HTML templates for rendering views
- TailwindCSS for styling
## File Tree
""".strip()
The full prompt includes the file tree, content descriptions, and additional
instructions about not running runserver, using Tailwind styles, and
keeping logic on the server side. See the full prompt in the workshop repo.
Running the coding agent
Now run the agent with the full tool class and the Django prompt:
tools_obj = Tools()
tools_obj.add_tools(agent_tools)
chat_interface = IPythonChatInterface()
llm_client = OpenAIClient(client=openai_client, model=model)
runner = OpenAIResponsesRunner(
tools=tools_obj,
developer_prompt=django_agent_prompt,
chat_interface=chat_interface,
llm_client=llm_client
)
runner.run()
Type stop to end the chat loop. Now we have a real coding agent: it works
inside a structured project, it has multiple project-aware tools, it has a
specific prompt, and the framework handles the agentic loop.
Try it with: "create a browser based vanilla JS snake game in snake.html".
Continue with Part 3: Skills to add the skills system.