Part 6: The skill tool and prompt injection
The loader can read skills. The agent still cannot 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 are not magic. They are loaded 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. The difference is that 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 does not know when to
call it. If you test with a greeting now, the model can respond normally
without loading 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. The workshop uses the prompt because it is simpler in ToyAIKit and worked better in the presenter's 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 it 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. The full body is loaded 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,
)
Run it:
result = runner.run()
Ask for a greeting:
hello
Now the model should call the skill tool with {"name": "hello"} before
answering. The sequence is:
- 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 version separates the wrapper into prototype/src/skill_tool.py:
from .skills import SkillLoader
class SkillToolsWrapper:
"""Wrapper class for the skill tool that toyaikit can discover."""
def __init__(self, loader: SkillLoader):
self._loader = loader
def skill(self, name: str) -> dict:
"""Load a skill to get specialized instructions."""
result = self._loader.load(name)
return {
"name": result.name,
"description": result.description,
"content": result.content,
}
The prototype SkillLoader.description is more explicit than the notebook
version. It tells the agent to check whether a skill matches before responding,
and it 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.