Chapter 05 · Systems

Skills System

Skills are how this harness teaches an agent what it knows, what it can do, and when to do it. They're plain Markdown files — humans write them, agents read them, the harness loads them in three tiers to keep the token budget under control. This is the most Claude-Code-inspired part of the codebase.

The mental model

Think of a skill as a "job description" the agent receives at runtime. The job description says: here's your role, here are the tools you can use, here's how to think about typical tasks, and here are the deeper references if you need them. The harness can have dozens of skill files on disk. A single-skill agent has one skill "in role" at any given turn; a multi-skill agent merges instructions and tools from several skills simultaneously.

The three tiers — progressive disclosure

An LLM has a finite context window. If you eagerly load every detail of every skill at startup, you blow the budget before the first user message lands. The harness solves this the same way Claude Code does: skills are loaded in three tiers, only as deeply as needed.

TierWhat's in itApprox. tokensWhen loaded
Tier 1 — Index Card ID, name, description, category, tags ~100 At agent startup. The agent always sees the index for every available skill.
Tier 2 — Folder Full instructions, behavioral guidelines, tool declarations ~5,000 When the agent (or the orchestrator) selects this skill for the turn.
Tier 3 — Filing Cabinet Scripts, reference docs, templates, examples, schemas Unbounded Only when the skill actively executes and needs that resource.
Why this works

At 100 tokens per skill, even 50 skills add only ~5K tokens to the startup budget — enough to let the agent know what it has access to without committing resources. Tier 2 is loaded one-at-a-time per turn. Tier 3 is loaded only when a tool physically opens that file. The token budget is never spent speculatively.

Anatomy of a SKILL.md file

Skills live under skills/ at the repo root (configured via AppConfig.AI.Skills.BasePath). Each skill is a single Markdown file with two parts: a block of structured metadata at the top, and human-readable instructions below.

YAML, frontmatter, and Markdown — quick definitions

Markdown is the lightweight plain-text formatting language you're already used to from READMEs (# headings, **bold**, etc.). YAML is a structured text format that looks like indented key: value pairs — much friendlier for humans than JSON. Frontmatter is a block of YAML at the top of a Markdown file, fenced between two --- lines. It's how a Markdown file carries metadata (id, tags, dependencies) that tooling can parse without parsing the prose.

Here's what a real SKILL.md looks like:

markdown · skills/research/SKILL.md
---
id: research
name: Research Agent
description: Finds and synthesizes information from documents and the web.
category: information-gathering
tags: [research, search, summarize]
allowed-tools:
  - file_system
  - document_search
version: 1.0.0
model-override: gpt-4o    # optional override of AppConfig default
skill_type: research
---

# Research Agent

## Role
You are a thorough research assistant. When given a question, you:
1. Identify what is and isn't known.
2. Use the document_search tool to gather sources.
3. Synthesize findings with citations.

## Behavioral guidelines
- Always cite sources by URL or document ID.
- Prefer primary sources over summaries.
- If sources conflict, surface the conflict rather than picking one.
...

The frontmatter is the Tier-1 metadata: id, name, description, category, tags are the index card; allowed-tools declares the tool surface. The parser (SkillMetadataParser) reads a fixed set of keys — category, tags, version, model-override, agent-id, allowed-tools, prerequisites, completion_tool, skill_type — anything else lands in a generic Metadata bag. The Markdown body is the Tier-2 content — the agent's instructions for this role. Tier-3 resources aren't listed in frontmatter at all: they're discovered from the skill folder's references/, templates/, and scripts/ subdirectories.

The SkillDefinition domain type

When the loader parses a SKILL.md, it produces a SkillDefinition:

C# · Domain.AI/Skills/SkillDefinition.cs
public class SkillDefinition
{
    // Level 1 — Index Card (always loaded)
    public string Id { get; set; } = string.Empty;
    public string Name { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;

    // Level 2 — Folder (on demand)
    public string? Instructions { get; set; } = string.Empty;   // Markdown body

    // Categorization + runtime config
    public string? Version { get; set; }
    public string? Category { get; set; }
    public string? SkillType { get; set; }                      // from skill_type
    public IList<string> Tags { get; set; } = new List<string>();
    public IList<string>? AllowedTools { get; set; }
    public string? ModelOverride { get; set; }                  // from model-override
    public string? AgentId { get; set; }                        // from agent-id
    public string? PluginSource { get; set; }                   // plugin ID, if Injected

    // Prerequisite ordering
    public IList<string> Prerequisites { get; set; } = new List<string>();
    public string? CompletionTool { get; set; }                 // from completion_tool

    // Level 3 — Filing Cabinet (discovered from subfolders)
    public IList<SkillResource> Templates  { get; set; } = new List<SkillResource>();
    public IList<SkillResource> References { get; set; } = new List<SkillResource>();
    public IList<SkillResource> Scripts    { get; set; } = new List<SkillResource>();

    // Computed — NOT settable
    public bool IsPluginSkill => !string.IsNullOrEmpty(PluginSource);
    public SkillMode Mode => IsPluginSkill && !HasToolDeclarations && !HasToolRestrictions
        ? SkillMode.Injected : SkillMode.Managed;
    // ... Author, License, StateConfiguration, DecisionFramework, etc.
}

Notice this is a plain mutable classget; set; properties and IList<> collections, rather than the immutable records used almost everywhere else in this codebase. That is deliberate: the parser fills it in field by field as it reads down the SKILL.md, so it needs somewhere to accumulate.

The one thing you can't set is Mode: it's a computed property derived from IsPluginSkill plus whether the skill declares any tools. The PluginSource and Prerequisites properties are covered in the sections below.

How a skill becomes an agent

The runtime journey of a skill, end-to-end:

  1. Discovery at startup

    SkillMetadataParser parses each SKILL.md frontmatter into a SkillDefinition, and SkillMetadataRegistry (behind ISkillMetadataRegistry) holds them, keyed by id. Only the Tier-1 metadata is eagerly in memory — the Markdown body is held lazily.

  2. Selection and assembly per turn

    When ExecuteAgentTurnCommandHandler runs, it looks up the agent's skill references. For a single-skill agent, one skill is selected. For a multi-skill agent, AgentExecutionContextFactory merges instructions and combines tool lists from all referenced skills into one unified AgentExecutionContext. If any skill has prerequisites, the factory enforces ordering — prerequisite skills must be marked complete (via their CompletionTool) before dependent skills activate.

  3. Tier-2 promotion

    The selected skill's full instructions (Markdown body) are loaded and stitched into the system prompt. Now the agent knows how to play this role.

  4. Tool resolution

    For each entry in allowed-tools, the harness resolves a keyed singleton from DI — sp.GetRequiredKeyedService<ITool>("file_system") — then converts it to an AITool and attaches it to the agent. See Tools & Keyed DI.

  5. Tier-3 access at runtime

    A resource under the skill folder's references/ subdirectory isn't read until the agent's file_system tool actually opens it. The path is exposed via the tool, not preloaded into the prompt.

Skill modes — Managed vs Injected

Not all skills originate inside the harness. The SkillMode enum distinguishes two patterns:

ModeTool resolutionWhen to use
Managed (default) Only the tools listed in allowed-tools are resolved from keyed DI or MCP. The harness controls exactly what the agent can call. Skills you author inside the harness — full control over tool surface.
Injected All MCP tools from the skill's parent plugin are passed through automatically, bypassing allowed-tools declarations. Skills provided by local plugins — the plugin author controls the tool surface.

Injected skills always set PluginSource to the owning plugin's ID. The harness still applies plugin-boundary governance (AllowedTools / DeniedTools) even when tools are passed through — see Observability & Safety.

Prerequisites and completion tracking

In a multi-skill agent, some skills depend on the output of others. A "data gathering" skill should finish before an "analysis" skill starts. The harness supports this with two properties:

  • Prerequisites — a list of skill IDs that must complete before this skill activates.
  • CompletionTool — the tool name whose invocation marks this skill as complete. When the agent calls this tool, the harness records the skill as done, unlocking any dependents.
markdown · SKILL.md frontmatter
---
id: analysis
name: Analysis Agent
prerequisites: [data-gathering]    # must complete first
completion_tool: submit_report     # calling this marks analysis as done
allowed-tools:
  - submit_report
  - file_system
---
Ordering is skill-level, not step-level

Prerequisites enforce that one skill finishes before another starts — they don't control the order of tool calls within a skill. The agent still decides how to use its tools on each turn. For fine-grained step control, use the DAG plan executor instead.

Multi-skill agents

An agent's AGENT.md can reference multiple skills in its skills: list. At context assembly time, AgentExecutionContextFactory merges instructions from all referenced skills and combines their tool lists into a single AgentExecutionContext.

markdown · agents/review-bot/AGENT.md (excerpt)
---
skills:
  - data-gathering      # prerequisite — runs first
  - analysis            # depends on data-gathering
  - code-reviewer       # independent — can run anytime
---

The factory handles each skill according to its SkillMode: Managed skills get explicit tool resolution; Injected skills get pass-through MCP tools. Prerequisites are enforced across the merged set. The result is one agent with the combined capabilities of all its skills.

The context budget tracker

While all of the above is happening, IContextBudgetTracker is keeping score. For the current turn, it tracks how many tokens have been committed to each of four things:

  • the system prompt
  • the skill content that has been loaded
  • the tool schemas
  • the conversation history

Those four compete for the same finite budget. As it starts to run out, the assembler stops promoting full Tier-2 skill content and serves only the short Tier-1 descriptions instead — the agent still knows the skill exists, it just no longer carries the full text.

!
Two budgets: per-turn vs whole-conversation

IContextBudgetTracker governs a single turn, sized by AppConfig:AI:AgentFramework:DefaultTokenBudget (default 200,000 tokens). A separate IConversationBudgetTracker (a singleton) enforces a cross-turn ceiling for the whole conversation (ConversationTokenBudget, default 1,000,000 tokens) and gracefully breaks the loop when a long session runs out. If you're seeing skills "disappear" mid-conversation, the per-turn tracker is preserving budget; if a run stops cleanly after many turns, that's the conversation tracker. (Note: there is no MaxTurnsPerConversation property — budgets are token-based, not turn-count-based.)

Adding a new skill

The end-to-end recipe — covered in more detail on Extending the Harness:

  1. Create a folder under skills/<your-skill-id>/.
  2. Add a SKILL.md with frontmatter (id, name, description, allowed-tools) and a Markdown body with the instructions.
  3. Add any Tier-3 references under references/, templates/, or scripts/ in that folder.
  4. Make sure every tool in allowed-tools is registered in DI under that key.
  5. (Optionally) reference the skill from an AGENT.md so an orchestrator can route to it.

You don't need to redeploy or recompile. The skill loader picks it up at next startup. There is no hot reload today — the host must be restarted for a changed SKILL.md to take effect.

Skill amendments and learnings

Skills aren't static. The meta-harness (see Observability & Safety) can propose SkillAmendments based on what the agent has learned across runs — tweaks to instructions, added examples, refined tool restrictions. These flow through the same loader pipeline and become part of the next session's Tier-2 content.


Where to go from here