Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds
Powrót do kolekcji
Przewodnik26 min czytania

Agent Skills, otwarty standard SKILL.md dla agentów

Agent Skills to otwarty format pakowania wiedzy dla agentów AI. Budowa pliku SKILL.md, stopniowe odsłanianie, obsługa w narzędziach i typowe błędy.

Agent Skills, otwarty standard SKILL.md dla agentów

Agent Skills to otwarty format pakowania wiedzy proceduralnej dla agentów AI. Pojedyncza umiejętność to katalog z plikiem SKILL.md, w którym opisujesz, jak wykonać określone zadanie, a agent wczytuje ten opis dopiero wtedy, gdy jest potrzebny. Format opracowała firma Anthropic i wydała go jako otwarty standard, przyjęty następnie przez kilkadziesiąt narzędzi konkurencji.

Skąd ten standard i dlaczego wygrał

Specyfikację opublikowano 18 grudnia 2025 roku i tempo jej przyjęcia było nietypowe nawet jak na tę branżę. W ciągu kilku miesięcy ten sam plik SKILL.md z tego samego katalogu czytają narzędzia firm, które ze sobą konkurują: Claude Code, VS Code z Copilotem, Codex od OpenAI, Cursor, Gemini CLI od Google'a, Junie od JetBrains, Kiro od Amazona oraz goose.

Powód tej zgody jest bardziej pragmatyczny niż ideowy. Każde z tych narzędzi miało własny sposób przyjmowania instrukcji od użytkownika, przez co zespół pracujący na trzech różnych agentach utrzymywał trzy wersje tej samej wiedzy. Wspólny format usuwa ten problem bez oddawania komukolwiek kontroli, bo to zwykły katalog z plikiem tekstowym, a nie usługa, do której trzeba się podłączyć.

Nadzór nad standardem prowadzi Agentic AI Foundation przy Linux Foundation, ta sama, do której trafił Model Context Protocol. Dla zespołu rozważającego inwestycję w pisanie umiejętności ma to znaczenie: format nie zależy od priorytetów jednej firmy.

Budowa umiejętności

Umiejętność to katalog, w którym obowiązkowy jest tylko jeden plik.

Code
TEXT
moja-umiejetnosc/
├── SKILL.md          # wymagany: metadane i instrukcje
├── scripts/          # opcjonalnie: kod do wykonania
├── references/       # opcjonalnie: dokumentacja
└── assets/           # opcjonalnie: szablony i zasoby

Sam plik zaczyna się od metadanych, w których minimum to nazwa i opis, a dalej jest zwykły tekst z instrukcjami.

Code
Markdown
---
name: raport-tygodniowy
description: Generuje raport tygodniowy ze zgłoszeń w systemie obsługi. Użyj, gdy ktoś prosi o podsumowanie tygodnia albo o zestawienie zgłoszeń.
---

# Raport tygodniowy

1. Pobierz zgłoszenia z ostatnich siedmiu dni skryptem `scripts/pobierz.py`.
2. Pogrupuj je według kategorii z pliku `references/kategorie.md`.
3. Złóż raport według szablonu `assets/szablon.md`.
4. Nie dopisuj wniosków, których nie da się wyprowadzić z danych.

Pole opisu jest tu ważniejsze, niż wygląda, i wrócę do niego przy typowych błędach. To jedyne, co agent widzi na starcie, więc od jego sformułowania zależy, czy umiejętność zostanie w ogóle użyta.

Stopniowe odsłanianie, czyli sedno pomysłu

Mechanizm, na którym stoi cały format, nazywa się stopniowym odsłanianiem i działa w trzech etapach.

Przy uruchomieniu agent wczytuje wyłącznie nazwę i opis każdej dostępnej umiejętności, czyli kilkadziesiąt słów na sztukę. To wystarcza, żeby wiedzieć, kiedy dana umiejętność może się przydać, i kosztuje tyle mało, że można ich mieć w gotowości kilkadziesiąt.

Kiedy zadanie pasuje do opisu, agent wczytuje pełną treść pliku do kontekstu. Dopiero na tym etapie płacisz za dłuższe instrukcje, i płacisz tylko wtedy, gdy są potrzebne.

W trzecim etapie agent wykonuje instrukcje, sięgając w razie potrzeby po dołączone skrypty i pliki pomocnicze. Plik z dokumentacją na dziesięć tysięcy słów nie obciąża kontekstu, dopóki instrukcja nie każe go otworzyć.

Ta konstrukcja rozwiązuje problem, który wcześniej nie miał dobrego rozwiązania. Wrzucenie całej wiedzy zespołu do jednego pliku z instrukcjami działa przy jednej stronie i przestaje działać przy dwudziestu, bo wszystko trafia do kontekstu przy każdym zadaniu i wypycha z niego to, co faktycznie istotne. Umiejętności odwracają ten układ: wiedzy może być dowolnie dużo, bo wczytuje się wyłącznie ta pasująca.

Umiejętności a MCP, czyli dwie różne warstwy

To rozróżnienie generuje najwięcej zamieszania, więc warto je postawić wprost.

Model Context Protocol daje agentowi możliwości: dostęp do bazy, do repozytorium, do przeglądarki. Odpowiada na pytanie, co agent potrafi zrobić. Umiejętności dają agentowi wiedzę: jak wykonać określone zadanie w Twojej organizacji, w jakiej kolejności, czego unikać. Odpowiadają na pytanie, jak to zrobić dobrze.

Z tego wynika, że nie konkurują ze sobą, tylko się uzupełniają, i najczęściej występują razem. Umiejętność opisująca proces wdrożenia korzysta z serwera MCP dającego dostęp do systemu wdrożeń, a bez niego byłaby instrukcją bez narzędzi. Odwrotnie, sam serwer MCP daje dostęp bez wiedzy, jak z niego korzystać zgodnie z Waszymi zasadami. Więcej o samym protokole znajdziesz przy okazji zestawu narzędzi MCP.

Praktyczna reguła doboru: jeśli brakuje agentowi dostępu do czegoś, potrzebujesz serwera MCP. Jeśli agent ma dostęp, ale robi rzeczy niezgodnie z Waszym procesem, potrzebujesz umiejętności.

Czym są Agent Skills?

Agent Skills to wzorce, narzędzia i możliwości, które pozwalają AI agentom wykonywać specjalistyczne zadania wykraczające poza generowanie tekstu. Gdy model językowy (LLM) ma dostęp do skills/tools, staje się prawdziwym agentem - może wchodzić w interakcję ze światem zewnętrznym: scrapować strony, wykonywać zapytania do bazy danych, manipulować plikami, wysyłać emaile i integrować się z dowolnym API.

Koncepcja skills/tools jest fundamentem nowoczesnych systemów AI i występuje pod różnymi nazwami w zależności od platformy:

  • Tool Use (Anthropic Claude)
  • Function Calling (OpenAI)
  • Tools (LangChain)
  • MCP (Model Context Protocol - Anthropic)
  • Actions (GPTs)
  • Skills (Custom agents)

Ewolucja od chatbotów do agentów

Tradycyjne chatboty mogły tylko generować tekst na podstawie promptu. Nowoczesne AI agenty potrafią:

  1. Analizować - Zrozumieć zadanie użytkownika
  2. Planować - Zdecydować, które narzędzia użyć
  3. Wykonywać - Wywołać odpowiednie skills/tools
  4. Iterować - Wykorzystać wyniki do dalszych działań
  5. Raportować - Przedstawić końcowy rezultat

Ten model "ReAct" (Reasoning + Acting) pozwala agentom rozwiązywać kompleksowe, wieloetapowe problemy.

Architektura systemu ze skills

Code
TEXT
┌──────────────────────────────────────────────────────────┐
│                      User Request                         │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│                     AI Agent (LLM)                        │
│  ┌────────────────────────────────────────────────────┐  │
│  │  System Prompt + Context + Conversation History    │  │
│  └────────────────────────────────────────────────────┘  │
│  ┌────────────────────────────────────────────────────┐  │
│  │  Available Tools/Skills (descriptions + schemas)   │  │
│  └────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────┘
                              ▼ (Tool Call)
┌──────────────────────────────────────────────────────────┐
│                    Skills Registry                        │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐    │
│  │Web Scraper│ │ Database │ │ FileSystem│ │   API    │    │
│  └──────────┘ └──────────┘ └──────────┘ └──────────┘    │
└──────────────────────────────────────────────────────────┘
                              ▼ (Tool Result)
┌──────────────────────────────────────────────────────────┐
│                AI Agent (continues reasoning)             │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│                     Final Response                        │
└──────────────────────────────────────────────────────────┘

Dlaczego Skills są ważne?

1. Przełamanie ograniczeń LLM

Modele językowe same w sobie:

  • Nie mają dostępu do internetu w czasie rzeczywistym
  • Nie mogą wykonywać kodu
  • Nie znają aktualnych danych (knowledge cutoff)
  • Nie mogą modyfikować systemów zewnętrznych

Skills rozwiązują te ograniczenia, dając LLM "ręce" do działania w świecie.

2. Specjalizacja i modularność

Zamiast próbować zbudować jeden "super-model" który umie wszystko, skills pozwalają:

  • Dodawać specjalistyczne możliwości modułowo
  • Aktualizować pojedyncze funkcje bez zmiany całego systemu
  • Łączyć różne skills w zależności od potrzeb
  • Testować każdy skill niezależnie

3. Bezpieczeństwo i kontrola

Skills działają jako kontrolowany interfejs między AI a światem:

  • Możesz ograniczyć, co agent może robić
  • Możesz logować i audytować wszystkie akcje
  • Możesz dodać rate limiting i sandboxing
  • Możesz wymagać approval dla niebezpiecznych operacji

4. Redukcja halucynacji

Gdy LLM ma dostęp do rzeczywistych danych przez skills:

  • Nie musi "zgadywać" informacji
  • Może zweryfikować fakty przed odpowiedzią
  • Odpowiedzi są oparte na aktualnych danych
  • Mniejsze ryzyko confabulation

Anatomia umiejętności

Podstawowa struktura umiejętności

Code
TypeScript
interface Skill {
  // Identyfikator używany przez LLM do wywołania
  name: string

  // Opis dla LLM - co robi ten skill
  description: string

  // Schema parametrów (JSON Schema lub Zod)
  inputSchema: JSONSchema | ZodSchema

  // Opcjonalny schema outputu
  outputSchema?: JSONSchema | ZodSchema

  // Główna logika wykonania
  execute: (params: unknown) => Promise<unknown>

  // Opcjonalne metadane
  metadata?: {
    category?: string
    requiresAuth?: boolean
    rateLimit?: number
    timeout?: number
  }
}

Przykład kompletnej umiejętności

Code
TypeScript
import { z } from 'zod'

// Schema parametrów z Zod
const weatherInputSchema = z.object({
  city: z.string().describe('Nazwa miasta'),
  units: z.enum(['metric', 'imperial']).default('metric').describe('Jednostki temperatury'),
  lang: z.string().default('pl').describe('Język odpowiedzi')
})

// Schema outputu
const weatherOutputSchema = z.object({
  temperature: z.number(),
  description: z.string(),
  humidity: z.number(),
  wind_speed: z.number()
})

// Skill definition
export const weatherSkill: Skill = {
  name: 'get_weather',
  description: `Pobiera aktualną pogodę dla podanego miasta.
    Użyj tego narzędzia gdy użytkownik pyta o pogodę, temperaturę,
    czy będzie padać, jakie warunki atmosferyczne.`,

  inputSchema: weatherInputSchema,
  outputSchema: weatherOutputSchema,

  metadata: {
    category: 'information',
    requiresAuth: true,  // Wymaga API key
    rateLimit: 60,       // Max 60 requests/minute
    timeout: 5000        // 5 second timeout
  },

  async execute(params) {
    const { city, units, lang } = weatherInputSchema.parse(params)

    const apiKey = process.env.OPENWEATHER_API_KEY
    const url = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&units=${units}&lang=${lang}&appid=${apiKey}`

    const response = await fetch(url)

    if (!response.ok) {
      throw new Error(`Weather API error: ${response.status}`)
    }

    const data = await response.json()

    return weatherOutputSchema.parse({
      temperature: data.main.temp,
      description: data.weather[0].description,
      humidity: data.main.humidity,
      wind_speed: data.wind.speed
    })
  }
}

Popularne kategorie umiejętności

1. Umiejętności sieciowe

Code
TypeScript
// Web Scraper - pobieranie treści ze stron
export const webScraperSkill = {
  name: 'web_scrape',
  description: 'Pobiera i parsuje treść ze strony internetowej',

  inputSchema: z.object({
    url: z.string().url(),
    selector: z.string().optional().describe('CSS selector do wybranych elementów'),
    format: z.enum(['text', 'html', 'markdown']).default('text')
  }),

  async execute({ url, selector, format }) {
    // Fetch z headless browser lub prostym fetch
    const response = await fetch(url, {
      headers: {
        'User-Agent': 'Mozilla/5.0 (compatible; Bot/1.0)'
      }
    })

    const html = await response.text()

    // Parse HTML
    const dom = new JSDOM(html)
    const document = dom.window.document

    let content: string

    if (selector) {
      const elements = document.querySelectorAll(selector)
      content = Array.from(elements).map(el => el.textContent).join('\n')
    } else {
      // Wyciągnij główną treść
      const article = document.querySelector('article, main, .content')
      content = article?.textContent || document.body.textContent || ''
    }

    // Format output
    if (format === 'markdown') {
      return turndownService.turndown(content)
    }

    return content.trim()
  }
}

// Search - wyszukiwanie w internecie
export const searchSkill = {
  name: 'web_search',
  description: 'Wyszukuje informacje w internecie używając DuckDuckGo/Serper',

  inputSchema: z.object({
    query: z.string(),
    num_results: z.number().min(1).max(10).default(5)
  }),

  async execute({ query, num_results }) {
    // Użyj Serper, SerpAPI, lub DuckDuckGo API
    const response = await fetch('https://google.serper.dev/search', {
      method: 'POST',
      headers: {
        'X-API-KEY': process.env.SERPER_API_KEY!,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ q: query, num: num_results })
    })

    const data = await response.json()

    return data.organic.map((result: any) => ({
      title: result.title,
      link: result.link,
      snippet: result.snippet
    }))
  }
}

2. Umiejętności bazodanowe

Code
TypeScript
// Safe SQL Query
export const databaseQuerySkill = {
  name: 'database_query',
  description: `Wykonuje bezpieczne zapytanie SQL do bazy danych.
    TYLKO SELECT queries są dozwolone. Użyj parametrów dla wartości.`,

  inputSchema: z.object({
    query: z.string().describe('SQL SELECT query'),
    params: z.array(z.any()).default([]).describe('Parametry do query')
  }),

  async execute({ query, params }) {
    // Walidacja - tylko SELECT
    const normalizedQuery = query.trim().toLowerCase()
    if (!normalizedQuery.startsWith('select')) {
      throw new Error('Only SELECT queries are allowed')
    }

    // Blacklist niebezpiecznych słów kluczowych
    const forbidden = ['drop', 'delete', 'update', 'insert', 'alter', 'create', 'truncate']
    for (const word of forbidden) {
      if (normalizedQuery.includes(word)) {
        throw new Error(`Forbidden keyword: ${word}`)
      }
    }

    // Wykonaj query z parametrami (prevents SQL injection)
    const result = await db.query(query, params)

    // Ogranicz ilość wyników
    return result.rows.slice(0, 100)
  }
}

// Prisma Query (type-safe)
export const prismaSkill = {
  name: 'prisma_query',
  description: 'Wykonuje type-safe query przez Prisma ORM',

  inputSchema: z.object({
    model: z.enum(['User', 'Post', 'Comment', 'Product']),
    operation: z.enum(['findMany', 'findFirst', 'count']),
    where: z.record(z.any()).optional(),
    select: z.array(z.string()).optional(),
    take: z.number().max(100).optional(),
    skip: z.number().optional(),
    orderBy: z.record(z.enum(['asc', 'desc'])).optional()
  }),

  async execute({ model, operation, where, select, take, skip, orderBy }) {
    const prismaModel = prisma[model.toLowerCase() as keyof typeof prisma]

    const query: any = {
      where,
      take: take || 50,
      skip
    }

    if (select) {
      query.select = Object.fromEntries(select.map(s => [s, true]))
    }

    if (orderBy) {
      query.orderBy = orderBy
    }

    // @ts-ignore - dynamic model access
    return await prismaModel[operation](query)
  }
}

3. Umiejętności systemu plików

Code
TypeScript
// Bezpieczne operacje na plikach
export const fileSystemSkill = {
  name: 'filesystem',
  description: 'Operacje na systemie plików w sandboxowanym katalogu',

  inputSchema: z.object({
    action: z.enum(['read', 'write', 'list', 'exists', 'mkdir', 'delete']),
    path: z.string(),
    content: z.string().optional()
  }),

  metadata: {
    // Sandbox do określonego katalogu
    sandboxPath: '/workspace',
    maxFileSize: 10 * 1024 * 1024 // 10MB
  },

  async execute({ action, path, content }) {
    // Walidacja ścieżki - nie pozwalaj na wyjście z sandbox
    const sandboxPath = '/workspace'
    const fullPath = join(sandboxPath, path)
    const normalizedPath = normalize(fullPath)

    if (!normalizedPath.startsWith(sandboxPath)) {
      throw new Error('Path traversal attack detected')
    }

    switch (action) {
      case 'read':
        const fileContent = await fs.readFile(normalizedPath, 'utf-8')
        return { content: fileContent.slice(0, 100000) } // Limit size

      case 'write':
        if (!content) throw new Error('Content required for write')
        if (content.length > 10 * 1024 * 1024) throw new Error('File too large')
        await fs.writeFile(normalizedPath, content)
        return { success: true, path: normalizedPath }

      case 'list':
        const entries = await fs.readdir(normalizedPath, { withFileTypes: true })
        return entries.map(e => ({
          name: e.name,
          type: e.isDirectory() ? 'directory' : 'file'
        }))

      case 'exists':
        try {
          await fs.access(normalizedPath)
          return { exists: true }
        } catch {
          return { exists: false }
        }

      case 'mkdir':
        await fs.mkdir(normalizedPath, { recursive: true })
        return { success: true }

      case 'delete':
        await fs.unlink(normalizedPath)
        return { success: true }
    }
  }
}

4. Umiejętności wykonywania kodu

Code
TypeScript
// JavaScript/TypeScript execution (sandboxed)
export const codeExecutionSkill = {
  name: 'execute_code',
  description: 'Wykonuje kod JavaScript w bezpiecznym sandbox',

  inputSchema: z.object({
    code: z.string(),
    language: z.enum(['javascript', 'typescript']).default('javascript'),
    timeout: z.number().max(30000).default(5000)
  }),

  async execute({ code, language, timeout }) {
    // Użyj VM2 lub isolated-vm dla bezpieczeństwa
    const vm = new NodeVM({
      timeout,
      sandbox: {
        console: {
          log: (...args: any[]) => logs.push(args.join(' ')),
          error: (...args: any[]) => errors.push(args.join(' '))
        }
      },
      require: {
        external: false, // Nie pozwalaj na require
        builtin: ['util', 'path'] // Tylko bezpieczne moduły
      }
    })

    const logs: string[] = []
    const errors: string[] = []

    try {
      // Dla TypeScript - transpiluj najpierw
      let executableCode = code
      if (language === 'typescript') {
        const result = ts.transpileModule(code, {
          compilerOptions: { module: ts.ModuleKind.CommonJS }
        })
        executableCode = result.outputText
      }

      const result = vm.run(executableCode)

      return {
        success: true,
        result,
        logs,
        errors
      }
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : 'Unknown error',
        logs,
        errors
      }
    }
  }
}

// Python execution (via subprocess)
export const pythonSkill = {
  name: 'execute_python',
  description: 'Wykonuje kod Python',

  inputSchema: z.object({
    code: z.string(),
    timeout: z.number().max(60000).default(10000)
  }),

  async execute({ code, timeout }) {
    return new Promise((resolve, reject) => {
      const python = spawn('python3', ['-c', code], {
        timeout,
        env: {
          ...process.env,
          PYTHONDONTWRITEBYTECODE: '1'
        }
      })

      let stdout = ''
      let stderr = ''

      python.stdout.on('data', (data) => { stdout += data })
      python.stderr.on('data', (data) => { stderr += data })

      python.on('close', (exitCode) => {
        resolve({
          success: exitCode === 0,
          stdout: stdout.trim(),
          stderr: stderr.trim(),
          exitCode
        })
      })

      python.on('error', (err) => {
        reject(new Error(`Python execution failed: ${err.message}`))
      })
    })
  }
}

5. Umiejętności integracji z API

Code
TypeScript
// Generic API caller
export const apiCallerSkill = {
  name: 'api_call',
  description: 'Wykonuje HTTP request do zewnętrznego API',

  inputSchema: z.object({
    url: z.string().url(),
    method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).default('GET'),
    headers: z.record(z.string()).optional(),
    body: z.any().optional(),
    timeout: z.number().max(30000).default(10000)
  }),

  async execute({ url, method, headers, body, timeout }) {
    // Whitelist dozwolonych domen (security)
    const allowedDomains = [
      'api.openai.com',
      'api.anthropic.com',
      'api.github.com',
      'api.stripe.com'
    ]

    const urlObj = new URL(url)
    if (!allowedDomains.some(d => urlObj.hostname.endsWith(d))) {
      throw new Error(`Domain not allowed: ${urlObj.hostname}`)
    }

    const controller = new AbortController()
    const timeoutId = setTimeout(() => controller.abort(), timeout)

    try {
      const response = await fetch(url, {
        method,
        headers: {
          'Content-Type': 'application/json',
          ...headers
        },
        body: body ? JSON.stringify(body) : undefined,
        signal: controller.signal
      })

      const contentType = response.headers.get('content-type')
      const data = contentType?.includes('application/json')
        ? await response.json()
        : await response.text()

      return {
        status: response.status,
        statusText: response.statusText,
        data
      }
    } finally {
      clearTimeout(timeoutId)
    }
  }
}

// GitHub API skill
export const githubSkill = {
  name: 'github',
  description: 'Interakcja z GitHub API - repos, issues, PRs',

  inputSchema: z.object({
    action: z.enum([
      'get_repo',
      'list_issues',
      'create_issue',
      'get_pr',
      'list_prs',
      'get_file'
    ]),
    owner: z.string(),
    repo: z.string(),
    params: z.record(z.any()).optional()
  }),

  async execute({ action, owner, repo, params }) {
    const octokit = new Octokit({
      auth: process.env.GITHUB_TOKEN
    })

    switch (action) {
      case 'get_repo':
        return await octokit.repos.get({ owner, repo })

      case 'list_issues':
        return await octokit.issues.listForRepo({
          owner,
          repo,
          state: params?.state || 'open',
          per_page: params?.limit || 10
        })

      case 'create_issue':
        return await octokit.issues.create({
          owner,
          repo,
          title: params?.title,
          body: params?.body,
          labels: params?.labels
        })

      case 'list_prs':
        return await octokit.pulls.list({
          owner,
          repo,
          state: params?.state || 'open',
          per_page: params?.limit || 10
        })

      case 'get_file':
        const content = await octokit.repos.getContent({
          owner,
          repo,
          path: params?.path
        })
        // Decode base64 content
        if ('content' in content.data) {
          return {
            ...content.data,
            decoded: Buffer.from(content.data.content, 'base64').toString()
          }
        }
        return content.data
    }
  }
}

6. Umiejętności komunikacyjne

Code
TypeScript
// Email sending skill
export const emailSkill = {
  name: 'send_email',
  description: 'Wysyła email przez Resend/SendGrid',

  inputSchema: z.object({
    to: z.string().email(),
    subject: z.string().max(200),
    body: z.string().max(10000),
    html: z.boolean().default(false)
  }),

  metadata: {
    requiresApproval: true, // Wymaga potwierdzenia użytkownika
    rateLimit: 10 // Max 10 emails/hour
  },

  async execute({ to, subject, body, html }) {
    const resend = new Resend(process.env.RESEND_API_KEY)

    const result = await resend.emails.send({
      from: 'assistant@example.com',
      to,
      subject,
      [html ? 'html' : 'text']: body
    })

    return { success: true, id: result.id }
  }
}

// Slack messaging skill
export const slackSkill = {
  name: 'slack_message',
  description: 'Wysyła wiadomość na Slack',

  inputSchema: z.object({
    channel: z.string(),
    message: z.string(),
    thread_ts: z.string().optional()
  }),

  async execute({ channel, message, thread_ts }) {
    const slack = new WebClient(process.env.SLACK_BOT_TOKEN)

    const result = await slack.chat.postMessage({
      channel,
      text: message,
      thread_ts
    })

    return {
      success: true,
      ts: result.ts,
      channel: result.channel
    }
  }
}

Rejestr umiejętności

Implementacja rejestru

TSskills/registry.ts
TypeScript
// skills/registry.ts
import { Skill } from './types'

class SkillsRegistry {
  private skills: Map<string, Skill> = new Map()
  private categories: Map<string, Skill[]> = new Map()

  register(skill: Skill) {
    // Walidacja skill
    this.validateSkill(skill)

    // Rejestracja
    this.skills.set(skill.name, skill)

    // Kategoryzacja
    const category = skill.metadata?.category || 'general'
    if (!this.categories.has(category)) {
      this.categories.set(category, [])
    }
    this.categories.get(category)!.push(skill)

    console.log(`Registered skill: ${skill.name}`)
  }

  private validateSkill(skill: Skill) {
    if (!skill.name || typeof skill.name !== 'string') {
      throw new Error('Skill must have a name')
    }
    if (!skill.description || typeof skill.description !== 'string') {
      throw new Error('Skill must have a description')
    }
    if (typeof skill.execute !== 'function') {
      throw new Error('Skill must have an execute function')
    }
    if (this.skills.has(skill.name)) {
      throw new Error(`Skill ${skill.name} already registered`)
    }
  }

  get(name: string): Skill | undefined {
    return this.skills.get(name)
  }

  getAll(): Skill[] {
    return Array.from(this.skills.values())
  }

  getByCategory(category: string): Skill[] {
    return this.categories.get(category) || []
  }

  // Format dla LLM (tool definitions)
  toToolDefinitions() {
    return this.getAll().map(skill => ({
      name: skill.name,
      description: skill.description,
      input_schema: this.zodToJsonSchema(skill.inputSchema)
    }))
  }

  private zodToJsonSchema(schema: z.ZodSchema) {
    // Konwertuj Zod schema do JSON Schema
    return zodToJsonSchema(schema)
  }

  async execute(name: string, params: unknown) {
    const skill = this.get(name)
    if (!skill) {
      throw new Error(`Skill not found: ${name}`)
    }

    // Rate limiting
    if (skill.metadata?.rateLimit) {
      await this.checkRateLimit(name, skill.metadata.rateLimit)
    }

    // Timeout
    const timeout = skill.metadata?.timeout || 30000
    const timeoutPromise = new Promise((_, reject) => {
      setTimeout(() => reject(new Error(`Skill ${name} timed out`)), timeout)
    })

    // Execute with timeout
    try {
      const result = await Promise.race([
        skill.execute(params),
        timeoutPromise
      ])

      // Log execution
      this.logExecution(name, params, result, true)

      return result
    } catch (error) {
      this.logExecution(name, params, error, false)
      throw error
    }
  }

  private async checkRateLimit(skillName: string, limit: number) {
    // Implementacja rate limiting (np. z Redis)
    const key = `ratelimit:${skillName}`
    const count = await redis.incr(key)

    if (count === 1) {
      await redis.expire(key, 60) // 1 minute window
    }

    if (count > limit) {
      throw new Error(`Rate limit exceeded for skill: ${skillName}`)
    }
  }

  private logExecution(name: string, params: unknown, result: unknown, success: boolean) {
    console.log(JSON.stringify({
      timestamp: new Date().toISOString(),
      skill: name,
      params: this.sanitizeForLog(params),
      success,
      result: success ? this.sanitizeForLog(result) : result
    }))
  }

  private sanitizeForLog(data: unknown): unknown {
    // Usuń wrażliwe dane przed logowaniem
    if (typeof data === 'object' && data !== null) {
      const sanitized = { ...data as object }
      const sensitiveKeys = ['password', 'token', 'apiKey', 'secret']
      for (const key of sensitiveKeys) {
        if (key in sanitized) {
          (sanitized as any)[key] = '[REDACTED]'
        }
      }
      return sanitized
    }
    return data
  }
}

// Singleton instance
export const skillsRegistry = new SkillsRegistry()

// Rejestracja skills
import { webScraperSkill } from './skills/web-scraper'
import { databaseQuerySkill } from './skills/database'
import { fileSystemSkill } from './skills/filesystem'
import { weatherSkill } from './skills/weather'
import { githubSkill } from './skills/github'

skillsRegistry.register(webScraperSkill)
skillsRegistry.register(databaseQuerySkill)
skillsRegistry.register(fileSystemSkill)
skillsRegistry.register(weatherSkill)
skillsRegistry.register(githubSkill)

Integracja z Claude

Tool Use API

Code
TypeScript
import Anthropic from '@anthropic-ai/sdk'
import { skillsRegistry } from './skills/registry'

const anthropic = new Anthropic()

async function runAgentWithTools(userMessage: string) {
  // Pobierz definicje narzędzi
  const tools = skillsRegistry.toToolDefinitions()

  // Pierwszy request do Claude
  let response = await anthropic.messages.create({
    model: 'claude-opus-5',
    max_tokens: 4096,
    system: `Jesteś pomocnym asystentem z dostępem do narzędzi.
             Używaj narzędzi gdy potrzebujesz aktualnych informacji
             lub wykonać akcję. Odpowiadaj po polsku.`,
    tools,
    messages: [{ role: 'user', content: userMessage }]
  })

  // Pętla obsługi tool use
  while (response.stop_reason === 'tool_use') {
    const toolUseBlocks = response.content.filter(
      block => block.type === 'tool_use'
    )

    // Wykonaj wszystkie tool calls
    const toolResults = await Promise.all(
      toolUseBlocks.map(async (block) => {
        if (block.type !== 'tool_use') return null

        try {
          const result = await skillsRegistry.execute(block.name, block.input)
          return {
            type: 'tool_result' as const,
            tool_use_id: block.id,
            content: JSON.stringify(result)
          }
        } catch (error) {
          return {
            type: 'tool_result' as const,
            tool_use_id: block.id,
            content: JSON.stringify({
              error: error instanceof Error ? error.message : 'Unknown error'
            }),
            is_error: true
          }
        }
      })
    )

    // Kontynuuj konwersację z wynikami
    response = await anthropic.messages.create({
      model: 'claude-opus-5',
      max_tokens: 4096,
      system: `Jesteś pomocnym asystentem z dostępem do narzędzi.`,
      tools,
      messages: [
        { role: 'user', content: userMessage },
        { role: 'assistant', content: response.content },
        { role: 'user', content: toolResults.filter(Boolean) as any }
      ]
    })
  }

  // Zwróć finalną odpowiedź
  const textBlock = response.content.find(block => block.type === 'text')
  return textBlock?.type === 'text' ? textBlock.text : ''
}

// Użycie
const answer = await runAgentWithTools(
  'Jaka jest pogoda w Warszawie i ile mam otwartych issues na repo example/test?'
)

MCP (Model Context Protocol)

MCP to nowszy standard od Anthropic dla integracji narzędzi:

TSmcp-server/index.ts
TypeScript
// mcp-server/index.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'

const server = new Server(
  { name: 'my-skills-server', version: '1.0.0' },
  { capabilities: { tools: {} } }
)

// Rejestracja tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: 'get_weather',
        description: 'Pobiera aktualną pogodę dla miasta',
        inputSchema: {
          type: 'object',
          properties: {
            city: { type: 'string', description: 'Nazwa miasta' }
          },
          required: ['city']
        }
      }
    ]
  }
})

// Handler dla tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params

  switch (name) {
    case 'get_weather':
      const weather = await getWeather(args.city)
      return {
        content: [{ type: 'text', text: JSON.stringify(weather) }]
      }

    default:
      throw new Error(`Unknown tool: ${name}`)
  }
})

// Start server
const transport = new StdioServerTransport()
await server.connect(transport)

Dobre praktyki

1. Bezpieczeństwo przede wszystkim

Code
TypeScript
// Zawsze waliduj input
async execute(params) {
  // Zod schema validation
  const validated = inputSchema.parse(params)

  // Sanitize user input
  validated.query = sanitizeHtml(validated.query)

  // Check permissions
  if (!userHasPermission('skill:execute')) {
    throw new Error('Permission denied')
  }

  // ...
}

2. Obsługa błędów

Code
TypeScript
async execute(params) {
  try {
    const result = await someOperation()
    return { success: true, data: result }
  } catch (error) {
    // Log full error internally
    console.error('Skill error:', error)

    // Return sanitized error to LLM
    return {
      success: false,
      error: error instanceof Error
        ? error.message
        : 'An error occurred'
    }
  }
}

3. Idempotentność

Code
TypeScript
// Dla skills które modyfikują stan, używaj idempotency keys
async execute({ action, data, idempotencyKey }) {
  // Sprawdź czy już wykonano
  const existing = await cache.get(`idempotent:${idempotencyKey}`)
  if (existing) {
    return existing // Zwróć cached result
  }

  // Wykonaj akcję
  const result = await performAction(data)

  // Cache result
  await cache.set(`idempotent:${idempotencyKey}`, result, 3600)

  return result
}

4. Rejestrowanie i monitorowanie

Code
TypeScript
// Structured logging dla każdego skill execution
const executeWithLogging = async (skill: Skill, params: unknown) => {
  const startTime = Date.now()
  const requestId = crypto.randomUUID()

  console.log(JSON.stringify({
    event: 'skill_start',
    requestId,
    skill: skill.name,
    params: sanitize(params),
    timestamp: new Date().toISOString()
  }))

  try {
    const result = await skill.execute(params)

    console.log(JSON.stringify({
      event: 'skill_success',
      requestId,
      skill: skill.name,
      duration: Date.now() - startTime,
      resultSize: JSON.stringify(result).length
    }))

    return result
  } catch (error) {
    console.log(JSON.stringify({
      event: 'skill_error',
      requestId,
      skill: skill.name,
      duration: Date.now() - startTime,
      error: error instanceof Error ? error.message : 'Unknown'
    }))

    throw error
  }
}

5. Ograniczanie liczby wywołań

Code
TypeScript
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '1m'), // 10 requests per minute
})

async function executeWithRateLimit(skillName: string, userId: string) {
  const { success, limit, remaining, reset } = await ratelimit.limit(
    `${skillName}:${userId}`
  )

  if (!success) {
    throw new Error(`Rate limit exceeded. Try again in ${reset}ms`)
  }

  // Execute skill...
}

Testowanie umiejętności

Code
TypeScript
import { describe, it, expect, vi } from 'vitest'
import { weatherSkill } from './skills/weather'

describe('weatherSkill', () => {
  it('should return weather data for valid city', async () => {
    // Mock fetch
    vi.spyOn(global, 'fetch').mockResolvedValueOnce({
      ok: true,
      json: async () => ({
        main: { temp: 20, humidity: 65 },
        weather: [{ description: 'sunny' }],
        wind: { speed: 5 }
      })
    } as Response)

    const result = await weatherSkill.execute({ city: 'Warsaw' })

    expect(result).toEqual({
      temperature: 20,
      description: 'sunny',
      humidity: 65,
      wind_speed: 5
    })
  })

  it('should throw error for invalid city', async () => {
    vi.spyOn(global, 'fetch').mockResolvedValueOnce({
      ok: false,
      status: 404
    } as Response)

    await expect(
      weatherSkill.execute({ city: 'NonExistentCity12345' })
    ).rejects.toThrow('Weather API error: 404')
  })

  it('should validate input schema', async () => {
    await expect(
      weatherSkill.execute({ city: 123 }) // number instead of string
    ).rejects.toThrow()
  })
})

Cennik

KomponentKoszt
Skill developmentCzas developera
Claude API (tool use)Standardowe API rates
MCP Server hostingZależy od infrastruktury
Third-party APIsZależy od dostawcy

Skills są kodem - nie ma dodatkowych opłat za ich używanie. Koszty to:

  • API calls do LLM (Claude, OpenAI)
  • Hosting infrastruktury (serwery, bazy danych)
  • Third-party services (weather API, GitHub, etc.)

FAQ - Najczęściej zadawane pytania

Ile skills może mieć agent?

Sam format nie stawia tu limitu, a stopniowe odsłanianie sprawia, że na starcie wczytują się wyłącznie nazwy i opisy. Granice biorą się skądinąd:

  • każdy opis zajmuje stałe miejsce w kontekście przy każdym zapytaniu, więc kilkadziesiąt umiejętności to już zauważalny narzut;
  • im więcej zbliżonych do siebie opisów, tym częściej agent sięga nie po tę umiejętność, o którą chodziło;
  • konkretne produkty mają własne limity, na przykład agent w Claude Managed Agents przyjmuje najwyżej dwadzieścia umiejętności.

Warto rozdzielić dwie rzeczy, bo mieszają się notorycznie. Liczba umiejętności to nie to samo co liczba narzędzi. Przy bardzo dużych zbiorach narzędzi istnieje osobny mechanizm wyszukiwania, który dociąga ich definicje na żądanie zamiast trzymać wszystkie w kontekście. Praktyczna granica jest więc jakościowa: kilkanaście rozdzielnych umiejętności działa lepiej niż sto zachodzących na siebie.

Jak wybrać między Tool Use a MCP?

  • Tool Use API: Prostsze, direct integration, mniej setup
  • MCP: Standard protokół, lepsze tooling, reusable servers

Dla prostych przypadków użyj Tool Use. Dla złożonych systemów z wieloma integracjami rozważ MCP.

Czy skills mogą wywoływać inne skills?

Tak, ale z ostrożnością. Lepsze podejście:

  1. LLM decyduje o sekwencji skills
  2. Lub użyj "orchestrator" skill który koordynuje inne

Jak obsłużyć długie operacje?

Code
TypeScript
// Użyj async patterns
async execute(params) {
  // Start job
  const jobId = await startLongJob(params)

  // Return job ID, let LLM check status later
  return {
    status: 'started',
    jobId,
    checkStatusWith: 'check_job_status'
  }
}

Jak zabezpieczyć wrażliwe skills?

  1. Approval workflow - Niektóre skills wymagają potwierdzenia użytkownika
  2. Scoped permissions - Różni użytkownicy mają dostęp do różnych skills
  3. Audit logging - Loguj wszystkie wywołania
  4. Rate limiting - Ogranicz częstotliwość

Czy mogę używać skills z różnymi LLM?

Tak, i to jest sens standardu. Ten sam katalog z plikiem SKILL.md czytają dziś narzędzia różnych producentów, więc umiejętność napisana raz działa niezależnie od tego, którego agenta akurat używasz.

Jak pisać dobre umiejętności

Doświadczenie z tym formatem sprowadza się do kilku zasad, których nie znajdziesz w specyfikacji, a które decydują o tym, czy umiejętność jest używana, czy leży odłogiem.

Opis pisz pod wyszukiwanie, nie pod dokumentację. Agent widzi na starcie wyłącznie nazwę i opis, więc to jedyny moment, w którym decyduje o użyciu. Opis mówiący, czym umiejętność jest, działa gorzej niż opis mówiący, kiedy jej użyć. Zdanie w rodzaju „Użyj, gdy ktoś prosi o podsumowanie tygodnia albo o zestawienie zgłoszeń" trafia lepiej niż „Narzędzie do raportowania".

Instrukcje pisz jako procedurę, nie jako opis. Numerowana lista kroków z konkretnymi nazwami plików i poleceń działa wyraźnie lepiej niż akapit wyjaśniający ideę. Agent nie potrzebuje kontekstu historycznego, tylko kolejności działań.

Zapisuj też zakazy, nie tylko nakazy. Zdanie „nie modyfikuj plików w katalogu z migracjami" jest często cenniejsze niż trzy zdania o tym, co robić, bo blokuje najczęstszą pomyłkę. Umiejętność jest miejscem na wiedzę plemienną, którą zwykle przekazuje się ustnie przy pierwszym wdrożeniu.

Trzymaj jedną umiejętność przy jednym zadaniu. Kusi, żeby zrobić jedną dużą umiejętność opisującą cały proces wytwórczy, ale wtedy jej opis staje się ogólnikowy i agent nie wie, kiedy ją wczytać. Pięć wąskich umiejętności z precyzyjnymi opisami zadziała lepiej niż jedna szeroka.

Wersjonuj je razem z kodem. Umiejętność opisująca proces, który zmienił się pół roku temu, jest gorsza niż jej brak, bo agent wykona nieaktualną procedurę z pełnym przekonaniem. Trzymanie katalogu w repozytorium projektu sprawia, że zmiana procesu i zmiana instrukcji idą w jednym zgłoszeniu.

Ostatnia zasada dotyczy tego, skąd bierzesz umiejętności. Powstały już katalogi zbierające gotowe pozycje, w tym Skills.sh, i kuszące jest zainstalowanie kilkudziesięciu naraz. To zwykle pogarsza sytuację, bo opisy zaczynają się nakładać, a agent wybiera nie tę, którą trzeba. Obok rejestrów krążą też gotowe zestawy jednej osoby, na przykład Everything Claude Code, gdzie ponad sto umiejętności idzie w komplecie z hakami, poleceniami i regułami pracy. Taki zestaw czyta się jak cudzą konfigurację edytora: pomysły warto z niej wyjąć, całości nie warto przyjmować, bo odzwierciedla nawyki autora, a sto kilkadziesiąt zbliżonych opisów utrudnia agentowi trafienie w tę właściwą. Traktuj cudze umiejętności jak punkt wyjścia do przeczytania i dopasowania, a nie jak zależność do zainstalowania i zapomnienia. Najlepiej działają te napisane pod konkretny zespół i konkretne repozytorium, bo dokładnie tego rodzaju wiedzy modelowi brakuje.

Typowe błędy

Pierwszy to opis, po którym nie da się rozpoznać zastosowania. Jeśli agent nigdy nie sięga po Twoją umiejętność, w dziewięciu przypadkach na dziesięć problem leży w opisie, a nie w instrukcjach.

Drugi to wrzucanie do pliku głównego wszystkiego, co się da. Format ma trzy poziomy właśnie po to, żeby długie materiały leżały w plikach pomocniczych. Plik główny powinien mieścić się na ekranie, a szczegóły trafiać do katalogu z dokumentacją, po który agent sięgnie w razie potrzeby.

Trzeci to mylenie umiejętności z narzędziem. Umiejętność nie daje agentowi żadnych nowych możliwości technicznych: jeśli nie ma dostępu do bazy, żadna instrukcja tego nie zmieni. Dostęp zapewnia warstwa narzędziowa, a umiejętność mówi, jak z niego korzystać.

Czwarty to traktowanie dołączonych skryptów jak zaufanego kodu. Umiejętność pobrana z zewnętrznego katalogu może zawierać skrypty, które agent wykona z Twoimi uprawnieniami. To jest ta sama klasa ryzyka co instalowanie zależności z nieznanego źródła i zasługuje na ten sam poziom ostrożności.

Piąty to pisanie umiejętności do rzeczy, które lepiej opisać w kodzie. Jeśli procedurę da się zamknąć w skrypcie uruchamianym jednym poleceniem, zrób to i niech umiejętność jedynie mówi, kiedy ten skrypt uruchomić. Instrukcja opisująca dwadzieścia kroków, które komputer wykonałby deterministycznie, to zaproszenie do błędów.

Pełną specyfikację formatu znajdziesz na agentskills.io, a prace nad standardem toczą się w repozytorium projektu.