Table of Contents

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:

  1. explain: type any topic, get a clear, jargon-free explanation.
  2. pros-cons: type any decision or idea, get a balanced pros and cons analysis.
  3. 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 optionally version.
  • Everything below the frontmatter is the instructions the model will follow.
  • The name must be lowercase with hyphens only (e.g. explain, pros-cons).
  • The description tells 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:

  1. The SkillTool description dynamically lists all available skills by name.
  2. When the model determines a skill is relevant, it calls activate_skill with the skill name.
  3. The tool returns the skill's full instructions as a tool result.
  4. 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/ or checklists/: documentation files
  • templates/: template files
  • examples/: 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