Add Skills to Your AI Assistant
Agent Skills let you turn a generic LLM into a specialist by loading a simple markdown file. Instead of writing long system prompts inline, you package instructions into reusable SKILL.md files that any project can load.
LM-Kit supports two activation modes for skills:
| Mode | How It Works | Best For |
|---|---|---|
| Manual (SkillActivator) | Your code injects skill instructions into messages using slash commands | Predictable apps, menu-driven UIs |
| Model-driven (SkillTool) | The model discovers and activates skills autonomously via function calling | Autonomous agents, conversational UIs |
This tutorial covers both approaches, starting with a console assistant that loads skills from a folder.
What You Will Build
A chat assistant that loads three skills from a skills/ folder:
- explain: type any topic, get a clear, jargon-free explanation.
- pros-cons: type any decision or idea, get a balanced pros and cons analysis.
- email-writer: type a one-line description, get a complete professional email.
Users activate a skill by typing its name (e.g. /explain), type a short input, and get structured output. The model follows the skill's instructions until the user deactivates it.
Prerequisites
| Requirement | Minimum |
|---|---|
| .NET SDK | 8.0+ |
| VRAM | 4+ GB (Gemma 3 4B) or 9+ GB (Gemma 3 12B, recommended) |
Step 1: Create the Project
dotnet new console -n SkillAssistant
cd SkillAssistant
dotnet add package LM-Kit.NET
Step 2: Understand the Architecture
Skills can reach the model through two paths: manual injection or model-driven tool calling.
Your Application
┌──────────────────────────────────────────────────────────┐
│ │
│ SkillRegistry │
│ ┌────────────┐ │
│ │ Load from │───┬─────────────────────────────┐ │
│ │ skills/ │ │ │ │
│ └────────────┘ │ │ │
│ ▼ ▼ │
│ (Manual) SkillActivator (Model-driven) SkillTool │
│ ┌───────────────────┐ ┌──────────────────┐ │
│ │ FormatForInjection│ │ activate_skill │ │
│ │ → enriched prompt │ │ → tool for LLM │ │
│ └────────┬──────────┘ └────────┬─────────┘ │
│ │ │ │
│ └──────────┬──────────────────┘ │
│ ▼ │
│ MultiTurnConversation │
└──────────────────────────────────────────────────────────┘
skills/
├── explain/
│ └── SKILL.md ← name + description + instructions
├── pros-cons/
│ └── SKILL.md
└── email-writer/
└── SKILL.md
| Class | What It Does |
|---|---|
SkillRegistry |
Discovers and stores skills from folders or URLs |
SkillActivator |
Formats a skill's instructions for injection into a conversation (manual mode) |
SkillTool |
Exposes skills as a callable function for the model (model-driven mode) |
AgentSkill |
One loaded skill: its name, description, instructions, and resources |
SkillInjectionMode |
Controls how instructions are injected (SystemPrompt, UserMessage, or ToolResult) |
Step 3: Create a Skill
Create a folder skills/explain/ in your project root and add a file called SKILL.md:
---
name: explain
description: Explains any topic in plain language. Type a word or phrase and get a clear, jargon-free explanation.
version: 1.0.0
---
# Plain Language Explainer
You explain topics so anyone can understand them. The user gives you a word,
phrase, or concept. You explain it clearly.
## Output Format
## <Topic>
**In one sentence:** <simple definition>
**How it works:** <2-3 sentences using an everyday analogy>
**Why it matters:** <1-2 sentences on why someone should care>
**Example:** <one concrete, real-world example>
## Rules
1. No jargon. If you must use a technical term, define it in parentheses.
2. Use analogies. Compare unfamiliar concepts to everyday things.
3. Be concise. The entire explanation fits on one screen.
4. Assume zero background knowledge.
Key parts of the format:
- The YAML frontmatter (
---block) contains metadata:name,description, and optionallyversion. - Everything below the frontmatter is the instructions the model will follow.
- The
namemust be lowercase with hyphens only (e.g.explain,pros-cons). - The
descriptiontells users (and tools) when to use this skill.
Step 4: Build the Assistant (Manual Activation)
using System.Text;
using LMKit.Model;
using LMKit.Agents.Skills;
using LMKit.TextGeneration;
using LMKit.TextGeneration.Chat;
using LMKit.TextGeneration.Sampling;
LMKit.Licensing.LicenseManager.SetLicenseKey("");
Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;
// 1. Load skills from a folder
var registry = new SkillRegistry();
var activator = new SkillActivator(registry);
int loaded = registry.LoadFromDirectory("./skills");
Console.WriteLine($"Loaded {loaded} skills.\n");
// 2. Load a model
Console.WriteLine("Loading model...");
using LM model = LM.LoadFromModelID("gemma3:4b",
downloadingProgress: (_, len, read) =>
{
if (len.HasValue) Console.Write($"\r Downloading: {(double)read / len.Value * 100:F1}% ");
return true;
},
loadingProgress: p => { Console.Write($"\r Loading: {p * 100:F0}% "); return true; });
Console.WriteLine("\n");
// 3. Create a conversation
var chat = new MultiTurnConversation(model);
chat.MaximumCompletionTokens = 4096;
chat.SamplingMode = new RandomSampling { Temperature = 0.7f };
// Stream tokens as they are generated
chat.AfterTextCompletion += (_, e) => Console.Write(e.Text);
// 4. Chat loop with skill activation
AgentSkill? activeSkill = null;
Console.WriteLine("Skills loaded:");
foreach (var skill in registry.Skills)
Console.WriteLine($" /{skill.Name} - {skill.Description}");
Console.WriteLine("\nType /<skill-name> to activate, /off to deactivate.\n");
while (true)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.Write("You: ");
Console.ResetColor();
string? input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input)) break;
// Handle skill activation: /explain, /pros-cons, etc.
if (registry.TryParseSlashCommand(input, out var skill, out _))
{
activeSkill = skill;
Console.WriteLine($"Skill activated: {skill.Name}\n");
continue;
}
if (input.Trim().ToLower() == "/off")
{
activeSkill = null;
Console.WriteLine("Skill deactivated.\n");
continue;
}
// Build prompt: inject skill instructions if active
string prompt = input;
if (activeSkill != null)
{
string instructions = activator.FormatForInjection(
activeSkill,
SkillInjectionMode.UserMessage);
prompt = instructions + "\n\n---\n\nUser request: " + input;
}
// Generate response
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write("\nAssistant: ");
Console.ResetColor();
var result = chat.Submit(prompt);
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($"\n({result.TokenGenerationRate:F1} tok/s)\n");
Console.ResetColor();
}
Step 5: Copy Skills to Output
Add this to your .csproj so skills are copied when you build:
<ItemGroup>
<None Include="skills\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>skills\%(RecursiveDir)%(Filename)%(Extension)</Link>
</None>
</ItemGroup>
Run the app:
dotnet run
Type /explain then type blockchain. The model will produce a structured, jargon-free explanation following the skill's format.
How Skill Injection Works
When you call activator.FormatForInjection(skill, mode), the skill's full instructions are formatted for injection. The SkillInjectionMode controls where they go:
| Mode | Behavior | Best For |
|---|---|---|
UserMessage |
Prepended to the user's message | Task-specific skills (the default in this tutorial) |
SystemPrompt |
Added to the system prompt | Persistent behavioral rules |
ToolResult |
Returned as a tool call result | Agents with function calling |
Alternative: Model-Driven Activation (SkillTool)
Instead of manually injecting instructions, you can let the model activate skills on its own using function calling. Register a SkillTool on the conversation, and the model will discover and invoke skills autonomously.
using LMKit.Agents.Skills;
using LMKit.Model;
using LMKit.TextGeneration;
var registry = new SkillRegistry();
registry.LoadFromDirectory("./skills");
using LM model = LM.LoadFromModelID("gemma3:4b");
var chat = new MultiTurnConversation(model);
// Register SkillTool: the model can now call activate_skill
chat.Tools.Register(new SkillTool(registry));
// No slash commands needed. The model decides when to use a skill.
var result = chat.Submit("explain what blockchain is");
// The model calls activate_skill("explain") internally, receives
// the skill's instructions, and generates a structured response.
How it works internally:
- The
SkillTooldescription dynamically lists all available skills by name. - When the model determines a skill is relevant, it calls
activate_skillwith the skill name. - The tool returns the skill's full instructions as a tool result.
- The model follows those instructions to generate its response.
When to use model-driven activation:
- Building autonomous agents that should pick the right skill on their own.
- Conversational UIs where users describe tasks in natural language.
- Scenarios where you do not want to expose slash commands to users.
When to use manual activation instead:
- You need deterministic, predictable skill selection.
- Building menu-driven or form-based interfaces.
- You want the application (not the model) to control which skill is used.
Adding Resources to a Skill
Skills can bundle reference files (templates, checklists, examples) alongside the SKILL.md:
skills/
email-writer/
SKILL.md
templates/
follow-up-template.md
examples/
formal-email.md
Access resources in code:
var skill = registry.Get("email-writer");
foreach (var resource in skill.Resources)
{
Console.WriteLine($" {resource.RelativePath} ({resource.Type})");
string content = resource.GetContent(); // lazy-loaded on first access
}
Resources are organized by folder convention:
references/orchecklists/: documentation filestemplates/: template filesexamples/: example files
Creating Skills Programmatically
For skills that do not need a file on disk, use SkillBuilder:
var skill = new SkillBuilder()
.WithName("bullet-summarizer")
.WithDescription("Condenses any text into 3-5 bullet points.")
.WithVersion("1.0.0")
.WithInstructions(@"
# Bullet Point Summarizer
You summarize any text into bullet points.
Rules:
1. Always produce exactly 3 to 5 bullet points.
2. Each bullet is one sentence maximum.
3. Capture the main ideas, not minor details.
4. Start each bullet with a strong verb or key noun.")
.Build();
registry.Register(skill);
Common Issues
| Problem | Cause | Fix |
|---|---|---|
| Skill not found | Folder does not contain SKILL.md | Ensure the file is named exactly SKILL.md (case-sensitive on Linux/macOS) |
| Skill not loading | Invalid YAML frontmatter | Check that name and description are present and the --- delimiters are correct |
| Model ignores skill instructions | Instructions too long for context | Use a model with larger context (8K+) or shorten the skill instructions |
| "Invalid skill name" error | Name contains uppercase or spaces | Use only lowercase letters, numbers, and hyphens (e.g. my-skill) |
Next Steps
- Samples: Skill-Based Assistant: the full demo with three bundled skills.
- Create an AI Agent with Tools: combine skills with tool-calling agents.
- Build a Multi-Agent Workflow: coordinate multiple agents with different skills.
- Agent Skills Specification: the open standard for SKILL.md files.