05
Skills — модульная архитектура по стандарту agentskills.io
Архитектура

В уроке 04 tools были захардкожены прямо в коде. Добавить новый — значит переписать агента. Skill решает эту проблему: это папка с файлом SKILL.md, которую агент подключает как плагин. Стандарт agentskills.io — открытый формат, чтобы разные агенты могли использовать одни и те же skills.

Ключевая идея — progressive disclosure (постепенное раскрытие). Агент не загружает все skills целиком. Вместо этого — три уровня:

1. Discovery 2. Activation 3. Execution
Discovery (~100 токенов): только name + description из frontmatter
Activation (<5000 токенов): полный текст SKILL.md с инструкциями
Execution (по запросу): файлы из references/, скрипты, ассеты

Зачем? Если у агента 50 skills, загрузить все instructions — это 250K токенов. С progressive disclosure агент сначала видит список из 50 коротких описаний (~5K токенов), активирует 2–3 нужных, и только потом погружается в детали.

Шаг 1. Создаём skill — filesystem

Каждый skill — это папка. Внутри — SKILL.md с YAML-frontmatter и инструкциями на естественном языке. Опционально — папка references/ для детальной документации.

Создаём структуру
mkdir -p skills/filesystem/references
skills/filesystem/SKILL.md
---
name: filesystem
description: Read, write, and list files. Use when the agent needs to work with the file system.
metadata:
  author: ai-agents-book
  version: "1.0"
---

# Filesystem Skill

Ты умеешь работать с файлами: читать, записывать и просматривать директории.

## Доступные инструменты
- **read_file** — прочитать содержимое файла по пути
- **write_file** — записать содержимое в файл
- **list_dir** — получить список файлов в директории

## Правила
- Перед записью всегда проверяй, существует ли файл
- Для больших директорий используй фильтрацию
- Подробности по каждому инструменту — см. references/REFERENCE.md

Разберём frontmatter:

  • name — уникальный идентификатор skill'а
  • description — короткое описание для Discovery-уровня. Агент читает его, чтобы решить: нужен ли этот skill?
  • metadata — автор, версия, любые дополнительные поля

Тело после --- — это Activation-уровень: полные инструкции, которые агент получает в system prompt при активации skill'а.

skills/filesystem/references/REFERENCE.md
# Filesystem Skill — Reference

## read_file
- **Параметры:** `path` (string) — путь к файлу
- **Возвращает:** содержимое файла в UTF-8
- **Ошибки:** если файл не найден — вернёт сообщение об ошибке

## write_file
- **Параметры:** `path` (string), `content` (string)
- **Возвращает:** подтверждение записи
- **Ошибки:** если нет прав на запись

## list_dir
- **Параметры:** `path` (string) — путь к директории
- **Возвращает:** вывод `ls -la`

references/ — третий уровень disclosure. Агент заглядывает сюда только когда ему нужны детали конкретного tool. Это экономит токены: основной SKILL.md остаётся компактным.

Проверяем структуру
tree skills/filesystem
# skills/filesystem
# ├── SKILL.md
# └── references/
#     └── REFERENCE.md

Шаг 2. Создаём второй skill — web-search

Два skill'а — минимум, чтобы увидеть модульность в действии. Обрати внимание на description: формулировка "Use when..." помогает агенту понять, когда активировать этот skill.

Создаём структуру
mkdir -p skills/web-search
skills/web-search/SKILL.md
---
name: web-search
description: Search the web for current information. Use when the agent needs up-to-date data, documentation, or facts beyond its training cutoff.
metadata:
  author: ai-agents-book
  version: "1.0"
---

# Web Search Skill

You can search the web to find current information.

## Available Tools
- **web_search** — perform a web search query and return top results

## Rules
- Use concise, specific search queries
- Prefer official sources (documentation, GitHub, Wikipedia)
- Always cite the source of information in your response
Два независимых skill'а
ls skills/
# filesystem/  web-search/

Шаг 3. Пишем загрузчик skills

Теперь — код. Нам нужно программно загружать SKILL.md, парсить frontmatter и отдавать агенту. Это реализация progressive disclosure в TypeScript:

src/05-skills.ts
// src/05-skills.ts
import OpenAI from "openai";
import { execSync } from "child_process";
import { readFileSync, writeFileSync, readdirSync, statSync } from "fs";
import { join } from "path";
import dotenv from "dotenv";

dotenv.config();

const client = new OpenAI({
    baseURL: "https://openrouter.ai/api/v1",
    apiKey: process.env.OPENROUTER_API_KEY ?? "",
});

// === Интерфейс Skill ===
interface Skill {
    name: string;
    description: string;       // Discovery-уровень: когда использовать
    instructions: string;      // Activation-уровень: полные инструкции
    location: string;          // путь к папке skill'а
    tools: OpenAI.ChatCompletionTool[];
    handlers: Record<string, (args: any) => string>;
}

// === Парсер YAML frontmatter ===
function parseFrontmatter(raw: string) {
    const parts = raw.split("---");
    if (parts.length < 3) {
        throw new Error("Invalid SKILL.md: no frontmatter found");
    }
    const frontmatter = parts[1].trim();
    const body = parts.slice(2).join("---").trim();

    const meta: Record<string, string> = {};
    for (const line of frontmatter.split("\n")) {
        const idx = line.indexOf(":");
        if (idx > 0 && !line.startsWith(" ")) {
            meta[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
        }
    }

    return { name: meta.name, description: meta.description, body };
}

// === Discovery: сканируем папку, извлекаем name + description ===
function discoverSkills(dir: string) {
    const entries = readdirSync(dir)
        .filter(name => {
            const fullPath = join(dir, name, "SKILL.md");
            try { statSync(fullPath); return true; }
            catch { return false; }
        });

    console.log(`\n📋 Discovery: найдено ${entries.length} skill(s)`);

    return entries.map(name => {
        const skillPath = join(dir, name);
        const raw = readFileSync(join(skillPath, "SKILL.md"), "utf-8");
        const { name: skillName, description } = parseFrontmatter(raw);
        console.log(`  • ${skillName}: ${description}`);
        return { name: skillName, description, location: skillPath };
    });
}

// === Activation: загружаем полный SKILL.md ===
function activateSkill(location: string) {
    const raw = readFileSync(join(location, "SKILL.md"), "utf-8");
    const { name, description, body } = parseFrontmatter(raw);
    console.log(`  ✅ ${name} activated (${body.length} chars)`);
    return { name, description, instructions: body, location };
}

discoverSkills() — это Discovery-уровень: быстрый скан папки, только name + description. activateSkill() — Activation: загружаем полные инструкции. Два вызова — два уровня раскрытия.

Шаг 4. Собираем агента с skills

Для каждого skill создаём tools + handlers, потом передаём в createAgent(). System prompt формируется по стандарту agentskills.io — с XML-блоком <available_skills>:

src/05-skills.ts
// === Skill: filesystem — tools + handlers ===
function createFilesystemSkill(meta: ReturnType<typeof activateSkill>): Skill {
    return {
        ...meta,
        tools: [
            {
                type: "function",
                function: {
                    name: "read_file",
                    description: "Прочитать файл",
                    parameters: {
                        type: "object",
                        properties: {
                            path: { type: "string", description: "Путь к файлу" },
                        },
                        required: ["path"],
                    },
                },
            },
            {
                type: "function",
                function: {
                    name: "write_file",
                    description: "Записать содержимое в файл",
                    parameters: {
                        type: "object",
                        properties: {
                            path: { type: "string", description: "Путь к файлу" },
                            content: { type: "string", description: "Содержимое" },
                        },
                        required: ["path", "content"],
                    },
                },
            },
            {
                type: "function",
                function: {
                    name: "list_dir",
                    description: "Список файлов в директории",
                    parameters: {
                        type: "object",
                        properties: {
                            path: { type: "string", description: "Путь к директории" },
                        },
                        required: ["path"],
                    },
                },
            },
        ],
        handlers: {
            read_file: ({ path }) => {
                try { return readFileSync(path, "utf-8"); }
                catch (e: any) { return `Ошибка: ${e.message}`; }
            },
            write_file: ({ path, content }) => {
                try { writeFileSync(path, content); return `Файл ${path} записан`; }
                catch (e: any) { return `Ошибка: ${e.message}`; }
            },
            list_dir: ({ path }) => {
                try { return execSync(`ls -la ${path}`, { encoding: "utf-8" }); }
                catch (e: any) { return `Ошибка: ${e.message}`; }
            },
        },
    };
}

// === Skill: web-search — tools + handler-заглушка ===
function createWebSearchSkill(meta: ReturnType<typeof activateSkill>): Skill {
    return {
        ...meta,
        tools: [
            {
                type: "function",
                function: {
                    name: "web_search",
                    description: "Поиск в интернете",
                    parameters: {
                        type: "object",
                        properties: {
                            query: { type: "string", description: "Поисковый запрос" },
                        },
                        required: ["query"],
                    },
                },
            },
        ],
        handlers: {
            web_search: ({ query }) =>
                `[Заглушка] Результаты поиска по "${query}": ` +
                `1. Official docs — https://example.com/docs\n` +
                `2. Tutorial — https://example.com/tutorial`,
        },
    };
}

// === Создание агента из массива skills ===
function createAgent(skills: Skill[]) {
    const allTools = skills.flatMap(s => s.tools);
    const allHandlers = Object.assign({}, ...skills.map(s => s.handlers));

    // System prompt по стандарту agentskills.io: XML-блок available_skills
    const skillsXml = skills.map(s =>
        `  <skill name="${s.name}">\n${s.instructions}\n  </skill>`
    ).join("\n");

    const systemPrompt = `Ты полезный ассистент. Вот твои навыки:

<available_skills>
${skillsXml}
</available_skills>

Используй инструменты когда нужно. Отвечай на русском.`;

    return async function run(userMessage: string) {
        const messages: OpenAI.ChatCompletionMessageParam[] = [
            { role: "system", content: systemPrompt },
            { role: "user", content: userMessage },
        ];

        let iteration = 0;

        while (iteration < 10) {
            iteration++;
            console.log(`\n--- Итерация ${iteration} ---`);

            const response = await client.chat.completions.create({
                model: "anthropic/claude-sonnet-4.5",
                messages,
                tools: allTools,
            });

            const message = response.choices[0].message;
            messages.push(message);

            if (!message.tool_calls?.length) {
                console.log("\nОтвет:", message.content);
                return message.content;
            }

            for (const toolCall of message.tool_calls) {
                const name = toolCall.function.name;
                const args = JSON.parse(toolCall.function.arguments);
                console.log(`Tool: ${name}(${JSON.stringify(args)})`);

                const result = allHandlers[name]?.(args)
                    ?? `Tool "${name}" не найден`;
                console.log(`→ ${result.slice(0, 150)}`);

                messages.push({
                    role: "tool",
                    tool_call_id: toolCall.id,
                    content: result,
                });
            }
        }
    };
}

// === main: Discovery → Activation → Execution ===
async function main() {
    // 1. Discovery — сканируем все skills
    const discovered = discoverSkills("./skills");

    // 2. Activation — активируем нужные skills и создаём реализации
    console.log("\n🔌 Activation:");
    const fsMeta = activateSkill(discovered[0].location);
    const wsMeta = activateSkill(discovered[1].location);

    const skills = [
        createFilesystemSkill(fsMeta),
        createWebSearchSkill(wsMeta),
    ];

    // 3. Execution — запускаем агента
    console.log(`\n🚀 Agent создан с ${skills.length} skills: ${skills.map(s => s.name).join(", ")}`);
    const agent = createAgent(skills);

    await agent(
        "Посмотри файлы проекта в текущей директории, " +
        "прочитай package.json и создай README.md с кратким описанием проекта."
    );
}

main();
npx tsx src/05-skills.ts
Демо в формате диалога
1/1
Пример вывода. У тебя может быть иначе — это нормально.
✅ Что должно получиться
В консоли три фазы: Discovery (список skills), Activation (загрузка инструкций), Execution (итерации агента с tool calls). Агент использует list_dir, read_file, write_file — все из filesystem skill.
🧯 Если не работает
Проверь структуру: в каждой папке skills/*/ должен быть файл SKILL.md с корректным YAML-frontmatter (три дефиса --- сверху и снизу). Если discoverSkills() находит 0 skills — проверь путь ./skills.
💡 Ключевая идея: createAgent([...skills]) — модульность. Хочешь добавить работу с API — создаёшь папку с SKILL.md и подключаешь. Хочешь убрать web-search — просто не активируешь.

Progressive disclosure — экономия. Агент тратит ~100 токенов на Discovery каждого skill, а не ~5000 на полные инструкции. При 50 skills это разница между 5K и 250K токенов.

Стандарт agentskills.io — переносимость. Skill в формате SKILL.md можно переиспользовать между разными агентами и фреймворками. XML-блок <available_skills> в system prompt — каноничный способ передать инструкции модели.