Part 6: The skill tool and prompt injection
The loader can read skills, but the agent still can't use them. To connect the
two, we expose one tool named skill and tell the model which skills exist.
This mirrors Claude Code and OpenCode at the same basic level. Skills aren't magic, and they load through ordinary tool calling.
The SkillsTool wrapper
ToyAIKit discovers Python methods and turns them into tool schemas.
Wrap the loader in a class with one public method:
class SkillsTool:
"""Wrapper for the skill loader that exposes skill() as a tool."""
def __init__(self, skill_loader: SkillLoader):
self.skill_loader = skill_loader
The tool method loads one skill by exact name:
def skill(self, name: str) -> dict:
"""Load a skill to get specialized instructions.
Args:
name: The exact skill name to load.
Returns:
Dictionary with skill information including name, description, and content.
"""
result = self.skill_loader.load_skill(name)
return {
"name": result.name,
"description": result.description,
"content": result.content,
}
Create the tool wrapper and test it directly:
skills_tool = SkillsTool(skill_loader)
skills_tool.skill("hello")
This returns the same skill data as the loader, so ToyAIKit can now expose
skill() to the model.
Add it to the existing tool registry:
tools_obj.add_tools(skills_tool)
Look at the tools:
tools_obj.get_tools()
You should now see the original coding tools plus the new skill tool.
The missing piece
Now the agent has a skill tool, but it still doesn't know when to call it. If
you test with a greeting now, the model responds normally and doesn't load the
hello skill, because nothing tells it that the skill exists.
The agent needs a compact list of available skills.
There are two common places to put that list:
- In the
skilltool description. - In the system or developer prompt.
OpenCode puts the list into the tool description. We put it in the prompt because that's simpler in ToyAIKit and worked better in our experiments.
Inject available skills into the prompt
Create a prompt fragment from the loader description:
SKILL_INJECTION_PROMPT = f"""
You have the following skills available which you can load with the skills tool:
{skill_loader.get_description()}
""".strip()
Append it to the base coding-agent instructions:
AGENT_WITH_SKILLS_INSTRUCTIONS = (
AGENT_INSTRUCTIONS + "\n\n" + SKILL_INJECTION_PROMPT
)
Print the updated prompt once:
print(AGENT_WITH_SKILLS_INSTRUCTIONS)
The prompt now contains the original general-purpose coding instructions plus the compact skill list:
You have the following skills available which you can load with the skills tool:
- coding_standards: Use this when writing or reviewing code - provides project-specific coding standards
- counter: Count things or list items with numbers
- deploy_app: Deploy the application using deployment scripts and templates
- hello: Skill for ALL greeting requests
- joke: Skill for ALL joke requests
This is the lazy-loading tradeoff. The prompt includes only the name and
description, and the full body loads only after the model calls skill().
Run the skills-aware agent
Create a new runner with the updated prompt:
runner = OpenAIResponsesRunner(
tools=tools_obj,
developer_prompt=AGENT_WITH_SKILLS_INSTRUCTIONS,
llm_client=llm_client,
chat_interface=chat_interface,
)
Start the skills-aware runner:
result = runner.run()
Ask for a greeting:
hello
Now the model should call the skill tool with {"name": "hello"} before
answering.
The sequence has these steps:
- User says
hello. - Model sees the available skill descriptions.
- Model decides the
helloskill matches. - Model calls
skill(name="hello"). - Python executes
SkillsTool.skill(). - The skill content is added to the message history as a tool result.
- The model answers using the loaded instructions.
For your own agents, skill loading happens locally in your runner, like every other tool call. The model requests the tool. Your code runs the tool. The result goes back into context.
The prototype wrapper
The prototype splits the wrapper out into its own module,
prototype/src/skill_tool.py. The class there, SkillToolsWrapper, matches the
notebook SkillsTool above: it holds a SkillLoader and exposes a single
skill(name) method that ToyAIKit can discover. The only real difference is
that it calls the loader's load() (with file-reference resolution) instead of
load_skill(). The full source is in the code repo at
agent-skills/prototype/src/skill_tool.py.
The prototype SkillLoader.description is more explicit than the notebook
version. It tells the agent to check whether a skill matches before responding.
It also reminds the model that the name parameter must exactly match one of
the listed skills.
That extra wording exists because smaller models need more direct instructions.
The same applies to slash commands: with gpt-4o-mini, be more explicit than
you would be with a stronger model.
Continue with Part 7: Implementing commands to add the explicit user-facing command flow.