Part 3: Skills
Now we extend the coding agent from Part 2 with skills. Skills are reusable instruction files that teach the agent how to handle a specific kind of task. Instead of putting every possible workflow into one long system prompt, we keep the base agent smaller and let it load extra instructions only when they are relevant.
This matters for two reasons: the agent stays simpler and easier to control, and we can add new capabilities without rewriting the whole agent.
Creating the skills folder
Install python-frontmatter for parsing SKILL.md files:
uv add python-frontmatter
Create a skills folder and download an example skill that fetches files from GitHub:
mkdir -p skills/gh-fetch
wget https://raw.githubusercontent.com/alexeygrigorev/workshops/refs/heads/main/agent-skills/gh-fetch-skill.md -O skills/gh-fetch/SKILL.md
The skill is a markdown file with frontmatter metadata and instruction content. The agent will be able to load it on demand.
The Skill dataclass
Create a data class to represent a loaded skill:
from dataclasses import dataclass
from pathlib import Path
import frontmatter
@dataclass
class Skill:
name: str
description: str
content: str
The SkillLoader class
The skills live as files on disk, but the agent needs a structured way to
work with them. The loader does three things: given a skill name, it reads
skills/<name>/SKILL.md; it lists all available skills in the skills/
folder; and it prepares a description of available skills so we can tell the
agent what it can load.
class SkillLoader:
def __init__(self, skills_dir: Path | str = None):
self.skills_dir = Path(skills_dir)
def load_skill(self, name: str) -> Skill | None:
skill_file = self.skills_dir / name / "SKILL.md"
if not skill_file.exists():
return None
parsed = frontmatter.load(skill_file)
return Skill(
name=parsed.metadata.get("name", name),
description=parsed.metadata.get("description", ""),
content=parsed.content,
)
def list_skills(self) -> list[Skill]:
skills = []
if not self.skills_dir.exists():
return skills
for skill_dir in sorted(self.skills_dir.iterdir()):
if not skill_dir.is_dir():
continue
skill = self.load_skill(skill_dir.name)
if skill:
skills.append(skill)
return skills
The get_description method formats the skill list for injection into the
prompt:
def get_description(self) -> str:
skills = self.list_skills()
skills_listing = "\n".join(
f" - {s.name}: {s.description}"
for s in skills
)
return skills_listing
The SkillsTool wrapper
The loader is a normal Python object, but the model can only call tools that are registered in the agent. To let the agent decide on its own when to load a skill, we expose the skill loader as a tool:
class SkillsTool:
"""Wrapper for the skill loader that exposes skill() as a tool."""
def __init__(self, skill_loader: SkillLoader):
self.skill_loader = skill_loader
def skill(self, name: str) -> dict:
"""Load a skill to get specialized instructions."""
result = self.skill_loader.load_skill(name)
return {
"name": result.name,
"description": result.description,
"content": result.content,
}
Now the model can see from the prompt that some skills are available, decide
that one is relevant, call the skill tool with the skill name, and read the
returned instructions to continue the task.
The skills injection prompt
We keep using the Django prompt from Part 2 and add the skills injection on top of it:
skill_loader = SkillLoader(Path("skills/"))
skills_tool = SkillsTool(skill_loader)
SKILL_INJECTION_PROMPT = f'''
You have the following skills available which you can load with the skills tool:
{skill_loader.get_description()}
'''.strip()
AGENT_WITH_SKILLS_INSTRUCTIONS = django_agent_prompt + '\n\n' + SKILL_INJECTION_PROMPT
Running the agent with skills
Create a fresh project folder and point the coding-agent tools at it:
skills_project_name = input("Enter the project name for the skills agent: ").strip()
start(skills_project_name)
project_path = Path(skills_project_name)
agent_tools = tools.AgentTools(project_path)
tools_obj = Tools()
tools_obj.add_tools(agent_tools)
tools_obj.add_tools(skills_tool)
Run the updated agent:
runner = OpenAIResponsesRunner(
tools=tools_obj,
developer_prompt=AGENT_WITH_SKILLS_INSTRUCTIONS,
llm_client=llm_client,
chat_interface=chat_interface,
)
runner.run()
Type stop to end the chat loop. The agent is the same coding agent as
before, but now it can load skills when they are relevant to the task.
Try it with a prompt that needs the gh-fetch skill:
Create a browser-based todo app in this Django project. First fetch
coding-agent-v2/plan.md from alexeygrigorev/workshops, save it under
references/, read the spec there, and then implement it here. Use Django
only to serve the page. The todo functionality itself should use vanilla
JavaScript, HTML, and localStorage.
The agent should load the gh-fetch skill, use it to fetch the file from
GitHub, and then proceed with the implementation.
Continue with Part 4: From toyaikit to PydanticAI to port the same agent to PydanticAI.