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.

Questions & Answers

Sign in to ask questions